Skip to content

Commit

Permalink
Fixe for approvalprofile generator
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon-Rey committed Oct 5, 2023
1 parent 6b9636a commit 75eb3b3
Show file tree
Hide file tree
Showing 7 changed files with 55 additions and 15 deletions.
5 changes: 3 additions & 2 deletions pabutools/election/profile/approvalprofile.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ def get_random_approval_profile(instance: Instance, num_agents: int) -> Approval

def get_all_approval_profiles(
instance: Instance, num_agents: int
) -> Generator[Iterable[Project]]:
) -> Generator[ApprovalProfile]:
"""
Returns a generator over all the possible profile for a given instance of a given length.
Expand All @@ -332,7 +332,8 @@ def get_all_approval_profiles(
Generator[Iterable[:py:class:`~pabutools.election.instance.Project`]]
Generator over subsets of projects.
"""
return product(powerset(instance), repeat=num_agents)
for p in product(powerset(instance), repeat=num_agents):
yield ApprovalProfile([ApprovalBallot(b) for b in p], instance=instance)


class ApprovalMultiProfile(MultiProfile, AbstractApprovalProfile):
Expand Down
23 changes: 23 additions & 0 deletions pabutools/election/satisfaction/satisfactionmeasure.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,26 @@ def total_satisfaction(self, projects: Iterable[Project]) -> Number:
for sat in self:
res += sat.sat(projects) * self.multiplicity(sat)
return res

@abstractmethod
def remove_satisfied(
self, sat_bound: dict[AbstractBallot, Number], projects: Iterable[Project]
) -> GroupSatisfactionMeasure:
"""
Returns a new satisfaction profile excluding the satisfaction measurs corresponding to satisfied voters, i.e.,
who have met or exceeded their satisfaction bound for a given collection of projects.
Parameters
----------
sat_bound : dict[str, Number]
A dictionary of ballot names to numbers, specifying for each ballot the satisfaction bound above which
the voter is considered satisfied. Note that the keys are ballot names, and that nothing ensures ballot
names to be unique, so be careful here.
projects : Iterable[:py:class:`~pabutools.election.instance.Project`]
The collection of projects.
Returns
-------
:py:class:`~pabutools.election.satisfaction.satisfactionmeasure.GroupSatisfactionMeasure`
The new satisfaction profile.
"""
16 changes: 15 additions & 1 deletion pabutools/election/satisfaction/satisfactionprofile.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

from collections import Counter
from collections.abc import Iterable
from numbers import Number

from pabutools.election.satisfaction.satisfactionmeasure import (
SatisfactionMeasure,
GroupSatisfactionMeasure,
)
from pabutools.election.instance import Instance
from pabutools.election.instance import Instance, Project
from pabutools.election.ballot.ballot import AbstractBallot

from typing import TYPE_CHECKING

Expand Down Expand Up @@ -116,6 +118,12 @@ def multiplicity(self, sat: SatisfactionMeasure) -> int:
"""
return 1

def remove_satisfied(self, sat_bound: dict[str, Number],
projects: Iterable[Project]) -> SatisfactionProfile:
res = SatisfactionProfile((s for s in self if s.sat(projects) < sat_bound[s.ballot.name]), instance=self.instance)
res.sat_class = self.sat_class
return res

@classmethod
def _wrap_methods(cls, names):
def wrap_method_closure(name):
Expand Down Expand Up @@ -296,6 +304,12 @@ def extend_from_multiprofile(
def multiplicity(self, sat: SatisfactionMeasure) -> int:
return self[sat]

def remove_satisfied(self, sat_bound: dict[AbstractBallot, Number],
projects: Iterable[Project]) -> SatisfactionMultiProfile:
res = SatisfactionMultiProfile({s: m for s, m in self.items() if s.sat(projects) < sat_bound[s.ballot.name]}, instance=self.instance)
res.sat_class = self.sat_class
return res

@classmethod
def _wrap_methods(cls, names):
def wrap_method_closure(name):
Expand Down
9 changes: 7 additions & 2 deletions pabutools/rules/greedywelfare.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from copy import copy
from collections.abc import Iterable
from math import inf
from numbers import Number

from pabutools.election import AbstractBallot
from pabutools.election.profile import AbstractProfile

from pabutools.fractions import frac
Expand All @@ -24,6 +26,7 @@ def greedy_utilitarian_scheme(
budget_allocation: Iterable[Project],
tie_breaking: TieBreakingRule,
resoluteness: bool = True,
sat_bounds: dict[AbstractBallot, Number] = None,
) -> Iterable[Project] | Iterable[Iterable[Project]]:
"""
The inner algorithm for the greedy rule. It selects projects in rounds, each time selecting a project that
Expand Down Expand Up @@ -71,7 +74,8 @@ def aux(inst, prof, sats, allocs, alloc, tie, resolute):
new_alloc = copy(alloc) + [project]
if project.cost > 0:
total_marginal_score = frac(
sats.total_satisfaction(new_alloc) - sats.total_satisfaction(alloc),
sats.total_satisfaction(new_alloc)
- sats.total_satisfaction(alloc),
project.cost,
)
else:
Expand Down Expand Up @@ -164,6 +168,7 @@ def satisfaction_density(proj):
return frac(total_sat, proj.cost)
return inf
return 0

# We sort based on a tuple to ensure ties are broken as intended
ordered_projects = sorted(
projects, key=lambda p: (-satisfaction_density(p), projects.index(p))
Expand Down Expand Up @@ -233,7 +238,7 @@ def greedy_utilitarian_welfare(
budget_allocation = []
if sat_class is None:
if sat_profile is None:
raise ValueError("Satisfaction and sat_profile cannot both be None.")
raise ValueError("sat_class and sat_profile cannot both be None.")
else:
if sat_profile is None:
sat_profile = profile.as_sat_profile(sat_class)
Expand Down
1 change: 0 additions & 1 deletion pabutools/rules/maxwelfare.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ def max_additive_utilitarian_welfare(
else:
if sat_profile is None:
sat_profile = profile.as_sat_profile(sat_class=sat_class)

return max_additive_utilitarian_welfare_scheme(
instance, sat_profile, budget_allocation, resoluteness=resoluteness
)
6 changes: 3 additions & 3 deletions tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ def test_approval_profile(self):
new_inst = Instance(
[Project("p1", 1), Project("p2", 1), Project("p3", 1)], budget_limit=3
)
assert len(set(get_all_approval_profiles(new_inst, 1))) == 8
assert len(set(get_all_approval_profiles(new_inst, 2))) == 8 * 8
assert len(set(get_all_approval_profiles(new_inst, 3))) == 8 * 8 * 8
assert len(list(get_all_approval_profiles(new_inst, 1))) == 8
assert len(list(get_all_approval_profiles(new_inst, 2))) == 8 * 8
assert len(list(get_all_approval_profiles(new_inst, 3))) == 8 * 8 * 8

def test_app_multiprofile(self):
projects = [Project("p" + str(i), cost=2) for i in range(10)]
Expand Down
10 changes: 4 additions & 6 deletions tests/test_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,10 @@ def dummy_elections():
test_election.irr_results_sat[max_additive_utilitarian_welfare][Cost_Sat] = sorted(
[[p[0], p[2]], [p[2]]]
)
test_election.irr_results_sat[max_additive_utilitarian_welfare][Cardinality_Sat] = sorted(
[[p[0], p[2]]]
)
test_election.irr_results_sat[method_of_equal_shares][Cost_Sat] = sorted(
[[]]
)
test_election.irr_results_sat[max_additive_utilitarian_welfare][
Cardinality_Sat
] = sorted([[p[0], p[2]]])
test_election.irr_results_sat[method_of_equal_shares][Cost_Sat] = sorted([[]])
test_election.irr_results_sat[method_of_equal_shares][Cardinality_Sat] = sorted(
[[p[0]]]
)
Expand Down

0 comments on commit 75eb3b3

Please sign in to comment.