Skip to content

Commit

Permalink
Merge pull request #22 from Kubvv/exhaustive-stop-condition
Browse files Browse the repository at this point in the history
Implement exhaustive_stop argument for exhaustion rule
  • Loading branch information
Simon-Rey authored Apr 12, 2024
2 parents 4b198f3 + fce650d commit ca5f44c
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 18 deletions.
32 changes: 24 additions & 8 deletions pabutools/analysis/mesanalytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pabutools.utils import Numeric
from pabutools.election.instance import Instance, Project
from pabutools.election.profile import Profile
from pabutools.rules.budgetallocation import AllocationDetails
from pabutools.rules.budgetallocation import BudgetAllocation, AllocationDetails
from pabutools.rules.mes.mes_rule import method_of_equal_shares


Expand Down Expand Up @@ -95,11 +95,12 @@ def calculate_project_loss(
project_losses = []
voter_count = len(allocation_details.iterations[0].voters_budget)
voter_multiplicity = allocation_details.voter_multiplicity
iterations_count = len(allocation_details.iterations)
voter_spendings: dict[int, list[tuple[Project, Numeric]]] = {}
for idx in range(voter_count):
voter_spendings[idx] = []

for iteration in allocation_details.iterations:
for idx, iteration in enumerate(allocation_details.iterations):
project_losses.append(
_create_project_loss(
iteration.selected_project,
Expand All @@ -120,7 +121,10 @@ def calculate_project_loss(
)

for project_detail in iteration:
if project_detail.discarded:
if project_detail.discarded or (
idx == iterations_count - 1
and project_detail.project != iteration.selected_project
):
project_losses.append(
_create_project_loss(
project_detail.project,
Expand All @@ -137,6 +141,7 @@ def calculate_project_loss(
def calculate_effective_supports(
instance: Instance,
profile: Profile,
allocation: BudgetAllocation,
mes_params: dict | None = None,
final_budget: Numeric | None = None,
) -> dict[Project, int]:
Expand All @@ -150,8 +155,10 @@ def calculate_effective_supports(
----------
instance: :py:class:`~pabutools.election.instance.Instance`
The instance.
profile : :py:class:`~pabutools.election.profile.profile.AbstractProfile`
profile: :py:class:`~pabutools.election.profile.profile.AbstractProfile`
The profile.
allocation: :py:class:`~pabutools.rules.budgetallocation.BudgetAllocation`
Resulting allocation of the above instance & profile.
mes_params: dict, optional
Dictionary of additional parameters that are passed as keyword arguments to the MES rule.
Defaults to None.
Expand All @@ -164,12 +171,14 @@ def calculate_effective_supports(
dict[:py:class:`~pabutools.election.instance.Project`, int]
Dictionary of pairs (:py:class:`~pabutools.election.instance.Project`, effective support).
"""
if mes_params is None:
mes_params = {}
effective_supports: dict[Project, int] = {}
if final_budget:
instance.budget_limit = final_budget
for project in instance:
effective_supports[project] = calculate_effective_support(
instance, profile, project, mes_params
instance, profile, project, project in allocation, mes_params
)

return effective_supports
Expand All @@ -179,11 +188,12 @@ def calculate_effective_support(
instance: Instance,
profile: Profile,
project: Project,
was_picked: bool,
mes_params: dict | None = None,
) -> int:
"""
Calculates the effective support of a given project in a given instance, profile and mes election.
Effective support for a project is an analytical metric which allows to measure the ratio of
Effective support for a project is an analytical metric which allows to measure the ratio of
initial budget received to minimal budget required to win. Effective support is represented in percentages.
Parameters
Expand All @@ -194,6 +204,8 @@ def calculate_effective_support(
The profile.
project: :py:class:`~pabutools.election.instance.Project`
Project for which effective support is calculated. Must be a part of the instance.
was_picked: bool
Whether the considerd project was picked as a winner in the allocation.
mes_params: dict, optional
Dictionary of additional parameters that are passed as keyword arguments to the MES rule.
Defaults to `{}`.
Expand All @@ -210,9 +222,13 @@ def calculate_effective_support(
mes_params["analytics"] = True
mes_params["skipped_project"] = project
mes_params["resoluteness"] = True
return method_of_equal_shares(
details = method_of_equal_shares(
instance, profile, **mes_params
).details.skipped_project_eff_support
).details
effective_support = details.skipped_project_eff_support
if was_picked:
effective_support = max(effective_support, 100)
return effective_support


def _create_project_loss(
Expand Down
8 changes: 6 additions & 2 deletions pabutools/rules/exhaustion.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def exhaustion_by_budget_increase(
rule_params: dict | None = None,
initial_budget_allocation: Iterable[Project] | None = None,
resoluteness: bool = True,
exhaustive_stop: bool = True,
budget_step: Numeric | None = None,
budget_bound: Numeric | None = None,
) -> BudgetAllocation | list[BudgetAllocation]:
Expand All @@ -127,6 +128,9 @@ def exhaustion_by_budget_increase(
resoluteness : bool, optional
Set to `False` to obtain an irresolute outcome, where all tied budget allocations are returned.
Defaults to True.
exhaustive_stop: bool, optional
Set to `False` to disable the exhaustive allocation stop condition, leaving only non-feasibility as
th stop condition of this rule. Defaults to True.
budget_step: Numeric
The step at which the budget is increased. Defaults to 1% of the budget limit.
budget_bound: Numeric
Expand Down Expand Up @@ -161,14 +165,14 @@ def exhaustion_by_budget_increase(
if resoluteness:
if not instance.is_feasible(outcome):
return previous_outcome
if instance.is_exhaustive(outcome):
if exhaustive_stop and instance.is_exhaustive(outcome):
return outcome
current_instance.budget_limit += budget_step
previous_outcome = outcome
else:
if any(not instance.is_feasible(o) for o in outcome):
return previous_outcome
if any(instance.is_exhaustive(o) for o in outcome):
if exhaustive_stop and any(instance.is_exhaustive(o) for o in outcome):
return outcome
current_instance.budget_limit += budget_step
previous_outcome = outcome
Expand Down
17 changes: 10 additions & 7 deletions tests/test_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ def test_project_loss(self):
{},
]

assert len(project_losses) == len(projects)
for idx, project_loss in enumerate(project_losses):
assert project_loss.name == projects[idx].name
assert project_loss.supporters_budget == expected_budgets[idx]
Expand All @@ -321,12 +322,12 @@ def test_project_loss(self):

@parameterized.expand(
[
([1, 1, 2, 1, 2], [200, 150, 37, 75, 50]),
([5, 1, 2, 1, 2], [60, 200, 50, 100, 50]),
([5, 5, 5, 5, 5], [80, 40, 30, 20, 20])
([1, 1, 2, 1, 2], [0, 1], [200, 150, 37, 75, 50]),
([5, 1, 2, 1, 2], [1, 3], [60, 200, 50, 100, 50]),
([5, 5, 5, 5, 5], [], [80, 40, 30, 20, 20]),
]
)
def test_effective_support(self, costs, expected_effective_support):
def test_effective_support(self, costs, allocation, expected_effective_support):
projects = [Project(chr(ord("a") + idx), costs[idx]) for idx in range(0, 5)]
instance = Instance(projects, budget_limit=2)
profile = ApprovalProfile(
Expand All @@ -343,12 +344,14 @@ def test_effective_support(self, costs, expected_effective_support):
ApprovalBallot({projects[4]}),
]
)

result = calculate_effective_supports(instance, profile, {"sat_class": Cost_Sat}, 5)
budget_allocation = BudgetAllocation(allocation)

result = calculate_effective_supports(
instance, profile, budget_allocation, {"sat_class": Cost_Sat}, 5
)
assert len(result) == len(projects)
sorted_projects = sorted(list(result), key=lambda proj: proj.name)

for idx, project in enumerate(sorted_projects):
assert project.name == chr(ord("a") + idx)
assert result[project] == expected_effective_support[idx]

6 changes: 5 additions & 1 deletion tests/test_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,8 @@ def test_mes_approval(self):
with self.assertRaises(ValueError):
method_of_equal_shares(Instance(), ApprovalProfile())

def test_iterated_exhaustion(self):
@parameterized.expand([(True,), (False,)])
def test_iterated_exhaustion(self, exhaustive_stop):
projects = [
Project("a", 1),
Project("b", 1),
Expand Down Expand Up @@ -540,6 +541,7 @@ def test_iterated_exhaustion(self):
method_of_equal_shares,
{"sat_class": Cost_Sat},
budget_step=frac(1, 24),
exhaustive_stop=exhaustive_stop
)
assert sorted(budget_allocation_mes_iterated) == [
projects[0],
Expand All @@ -555,6 +557,7 @@ def test_iterated_exhaustion(self):
{"sat_class": Cost_Sat},
budget_step=frac(1, 24),
initial_budget_allocation=[projects[6]],
exhaustive_stop=exhaustive_stop
)
assert sorted(budget_allocation_mes_iterated) == [
projects[0],
Expand All @@ -569,6 +572,7 @@ def test_iterated_exhaustion(self):
method_of_equal_shares,
{"sat_class": Cost_Sat},
budget_step=5,
exhaustive_stop=exhaustive_stop
)
assert budget_allocation_mes_iterated_big_steps == [projects[0]]

Expand Down

0 comments on commit ca5f44c

Please sign in to comment.