diff --git a/docs-source/source/reference/analysis/index.rst b/docs-source/source/reference/analysis/index.rst index abff91ea..56b2ef00 100644 --- a/docs-source/source/reference/analysis/index.rst +++ b/docs-source/source/reference/analysis/index.rst @@ -14,3 +14,6 @@ Analysis module .. automodule:: pabutools.analysis.category :members: + +.. automodule:: pabutools.analysis.projectloss + :members: diff --git a/docs-source/source/reference/rules/index.rst b/docs-source/source/reference/rules/index.rst index 8177be58..b44765b6 100644 --- a/docs-source/source/reference/rules/index.rst +++ b/docs-source/source/reference/rules/index.rst @@ -3,20 +3,47 @@ Rules module .. automodule:: pabutools.rules +Budget Allocation +----------------- + .. autoclass:: pabutools.rules.budgetallocation.BudgetAllocation +.. autoclass:: pabutools.rules.budgetallocation.AllocationDetails + +Greedy Utilitarian Rule +----------------------- + .. autofunction:: pabutools.rules.greedywelfare.greedy_utilitarian_welfare +Additive Utilitarian Welfare Maximiser +-------------------------------------- + .. autofunction:: pabutools.rules.maxwelfare.max_additive_utilitarian_welfare +Sequential Phragmén's Rule +-------------------------- + .. autofunction:: pabutools.rules.phragmen.sequential_phragmen +Method of Equal Shares (MES) +---------------------------- + .. autofunction:: pabutools.rules.mes.method_of_equal_shares +.. autoclass:: pabutools.rules.mes.MESAllocationDetails + +.. autoclass:: pabutools.rules.mes.MESIteration + +Exhaustion Methods +------------------ + .. autofunction:: pabutools.rules.exhaustion.completion_by_rule_combination .. autofunction:: pabutools.rules.exhaustion.exhaustion_by_budget_increase +Rule Composition +---------------- + .. autofunction:: pabutools.rules.composition.popularity_comparison .. autofunction:: pabutools.rules.composition.social_welfare_comparison diff --git a/docs-source/source/usage.rst b/docs-source/source/usage.rst index ce7e000e..cf0f12a8 100644 --- a/docs-source/source/usage.rst +++ b/docs-source/source/usage.rst @@ -527,6 +527,8 @@ outcome (for visualisation/explanation purposes). Additive Utilitarian Welfare Maximiser ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:py:func:`~pabutools.rules.maxwelfare.max_additive_utilitarian_welfare` + The first rule provided is the Additive Utilitarian Welfare Maximiser. It aims to return budget allocations that maximize the utilitarian social welfare when the satisfaction measure is additive. @@ -584,6 +586,8 @@ for non-additive satisfaction measures. Greedy Approximation of the Welfare Maximiser ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:py:func:`~pabutools.rules.greedywelfare.greedy_utilitarian_welfare` + The library also implements standard greedy rules. The primary rule used in this context is the Greedy Utilitarian Welfare. It behaves similarly to the Utilitarian Welfare Maximiser but offers additional functionalities: it is not limited @@ -653,6 +657,8 @@ to additive satisfaction measures (and runs faster). Sequential Phragmén's Rule ^^^^^^^^^^^^^^^^^^^^^^^^^^ +:py:func:`~pabutools.rules.phragmen.sequential_phragmen` + Another rule provided is the Sequential Phragmén's Rule, which is different from the previous two as it does not rely on a satisfaction measure. @@ -700,6 +706,8 @@ previous two as it does not rely on a satisfaction measure. Method of Equal Shares (MES) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:py:func:`~pabutools.rules.mes.method_of_equal_shares` + The Method of Equal Shares is another rule that returns budget allocations based on the satisfaction measure provided. For more details, see the `equalshares.net `_ website. @@ -773,6 +781,10 @@ performances, one should use the following: Exhaustion Methods ^^^^^^^^^^^^^^^^^^ +:py:func:`~pabutools.rules.exhaustion.completion_by_rule_combination` + +:py:func:`~pabutools.rules.exhaustion.exhaustion_by_budget_increase` + Since not all rules return exhaustive budget allocations, the library offers standard methods to render their outcome exhaustive. @@ -856,6 +868,10 @@ parameter directly to obtain the iterated version. Rule Composition ^^^^^^^^^^^^^^^^ +:py:func:`~pabutools.rules.composition.popularity_comparison` + +:py:func:`~pabutools.rules.composition.social_welfare_comparison` + The library also provides ways to compose rules, such as selecting the outcome that is preferred by the largest number of voters for a given satisfaction measure. @@ -929,13 +945,33 @@ the following: We also provide a similar comparison using utilitarian social welfare through the function :py:func:`~pabutools.rules.composition.social_welfare_comparison`. +Details for the Budget Allocation Rule +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some rules, for instance :py:func:`~pabutools.rules.mes.method_of_equal_shares`, accept a +:code:`analytics` boolean argument to activate the storage of additional information +regarding the budget allocations output by the rule. When :code:`analytics = True`, +the rule populate the :code:`details` member of the +:py:class:`~pabutools.rules.budgetallocation.BudgetAllocation` object it returns. +The stored information can then be used for analytical purposes. + +.. list-table:: + :widths: 50 50 + :header-rows: 1 + + * - Rule + - Details class + * - :py:func:`~pabutools.rules.mes.method_of_equal_shares` + - :py:class:`~pabutools.rules.mes.MESAllocationDetails` + + Tie-Breaking ------------ For reference, see the module :py:mod:`~pabutools.tiebreaking`. We provide several ways to break ties between several projects. All tie-breaking rules are -instantiations of the :py:class:`pabutools.tiebreaking.TieBreakingRule` class. +instantiations of the :py:class:`~pabutools.tiebreaking.TieBreakingRule` class. This class defines two functions `untie` and `order` that respectively return a single project from a set of several or order a list of projects. diff --git a/docs/_sources/usage.rst b/docs/_sources/usage.rst index ce7e000e..12908242 100644 --- a/docs/_sources/usage.rst +++ b/docs/_sources/usage.rst @@ -935,7 +935,7 @@ Tie-Breaking For reference, see the module :py:mod:`~pabutools.tiebreaking`. We provide several ways to break ties between several projects. All tie-breaking rules are -instantiations of the :py:class:`pabutools.tiebreaking.TieBreakingRule` class. +instantiations of the :py:class:`~pabutools.tiebreaking.TieBreakingRule` class. This class defines two functions `untie` and `order` that respectively return a single project from a set of several or order a list of projects. diff --git a/pabutools/analysis/justifiedrepresentation.py b/pabutools/analysis/justifiedrepresentation.py index fe29caf8..f1926276 100644 --- a/pabutools/analysis/justifiedrepresentation.py +++ b/pabutools/analysis/justifiedrepresentation.py @@ -13,7 +13,8 @@ Additive_Cardinal_Sat, AbstractCardinalProfile, ApprovalBallot, - total_cost, AbstractProfile, + total_cost, + AbstractProfile, ) from pabutools.utils import powerset @@ -39,7 +40,10 @@ def is_in_core( sat = sat_class(instance, profile, ballot) surplus = 0 if up_to_func is not None: - surplus = up_to_func(sat.sat_project(p) for p in project_set if p not in budget_allocation + surplus = up_to_func( + sat.sat_project(p) + for p in project_set + if p not in budget_allocation ) if sat.sat(budget_allocation) + surplus >= sat.sat(project_set): all_better_alone = False diff --git a/pabutools/analysis/projectloss.py b/pabutools/analysis/projectloss.py index d4eb70de..8c762f17 100644 --- a/pabutools/analysis/projectloss.py +++ b/pabutools/analysis/projectloss.py @@ -10,24 +10,23 @@ class ProjectLoss(Project): """ Class used to represent the projects and how much budget they lost due to other projects being picked. + This extends the :py:class:`~pabutools.election.instance.Project` and thus represents the project itself. Parameters ---------- - project: Project + project: :py:class:`~pabutools.election.instance.Project` Project for which analytics is calculated. - supporters_budget: Numeric + supporters_budget: :py:class:`~pabutools.utils.Numeric` The collective budget of the project supporters when project was considered by a rule. - budget_lost: dict[Project, Numeric] + budget_lost: dict[:py:class:`~pabutools.election.instance.Project`, :py:class:`~pabutools.utils.Numeric`] Describes the amount of budget project supporters spent on other projects prior to this projects' consideration. Attributes ---------- - project: Project - Project for which analytics is calculated. - supporters_budget: Numeric + supporters_budget: :py:class:`~pabutools.utils.Numeric` The collective budget of the project supporters when project was considered by rule. - budget_lost: dict[Project, Numeric] + budget_lost: dict[:py:class:`~pabutools.election.instance.Project`, :py:class:`~pabutools.utils.Numeric`] Describes the amount of budget project supporters spent on other projects prior to this projects' consideration. """ @@ -50,7 +49,7 @@ def total_budget_lost(self) -> Numeric: Returns ------- - Numeric + :py:class:`~pabutools.utils.Numeric` The total budget spent. """ return sum(self.budget_lost.values()) @@ -65,12 +64,28 @@ def __repr__(self): def calculate_project_loss( allocation_details: AllocationDetails, verbose: bool = False ) -> list[ProjectLoss]: - if ( - allocation_details.iterations is None - or allocation_details.initial_budget_per_voter is None + """Returns a list of :py:class:`~pabutools.analysis.projectloss.ProjectLoss` objects for the projects. + + Parameters + ---------- + allocation_details: :py:class:`~pabutools.rules.budgetallocation.AllocationDetails` + The details of the budget allocation considered. + verbose: bool, optional + (De)Activate the display of additional information. + Defaults to `False`. + + Returns + ------- + list[:py:class:`~pabutools.analysis.projectloss.ProjectLoss`] + List of :py:class:`~pabutools.analysis.projectloss.ProjectLoss` objects. + + """ + if not hasattr(allocation_details, "iterations") or not hasattr( + allocation_details, "initial_budget_per_voter" ): raise ValueError( - "Provided allocation details do not support calculating project loss" + "Provided budget allocation details do not support calculating project loss. The allocation_details " + "should have an 'iterations' and an 'initial_budget_per_voter' attributes." ) if len(allocation_details.iterations) == 0: if verbose: @@ -87,31 +102,33 @@ def calculate_project_loss( allocation_details.initial_budget_per_voter for _ in range(voter_count) ] - for iter in allocation_details.iterations: + for iteration in allocation_details.iterations: if verbose: - print(f"Considering: {iter.project.name}, status: {iter.was_picked}") + print( + f"Considering: {iteration.project.name}, status: {iteration.was_picked}" + ) budget_lost = {} - for spending in [voter_spendings[i] for i in iter.supporter_indices]: + for spending in [voter_spendings[i] for i in iteration.supporter_indices]: for project, spent in spending: if project not in budget_lost.keys(): budget_lost[project] = 0 budget_lost[project] = budget_lost[project] + spent project_losses.append( ProjectLoss( - iter.project, - sum(current_voters_budget[i] for i in iter.supporter_indices), + iteration.project, + sum(current_voters_budget[i] for i in iteration.supporter_indices), budget_lost, ) ) - if iter.was_picked: - for supporter_idx in iter.supporter_indices: + if iteration.was_picked: + for supporter_idx in iteration.supporter_indices: voter_spendings[supporter_idx].append( ( - iter.project, + iteration.project, current_voters_budget[supporter_idx] - - iter.voters_budget[supporter_idx], + - iteration.voters_budget[supporter_idx], ) ) - current_voters_budget = iter.voters_budget + current_voters_budget = iteration.voters_budget return project_losses diff --git a/pabutools/election/ballot/approvalballot.py b/pabutools/election/ballot/approvalballot.py index c46ed4a9..f13ae2aa 100644 --- a/pabutools/election/ballot/approvalballot.py +++ b/pabutools/election/ballot/approvalballot.py @@ -1,6 +1,7 @@ """ Approval ballots, i.e., ballots in which the voters indicate which projects they approve of. """ + from __future__ import annotations import random diff --git a/pabutools/election/ballot/ballot.py b/pabutools/election/ballot/ballot.py index 769266d1..ce6e8755 100644 --- a/pabutools/election/ballot/ballot.py +++ b/pabutools/election/ballot/ballot.py @@ -1,6 +1,7 @@ """ Ballots, that is, the information submitted by the voters during the election. """ + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/pabutools/election/ballot/cardinalballot.py b/pabutools/election/ballot/cardinalballot.py index 2b185a3b..9790c6c0 100644 --- a/pabutools/election/ballot/cardinalballot.py +++ b/pabutools/election/ballot/cardinalballot.py @@ -1,6 +1,7 @@ """ Cardinal ballots, i.e., ballots in which the voters map projects to scores. """ + from __future__ import annotations from abc import ABC diff --git a/pabutools/election/ballot/cumulativeballot.py b/pabutools/election/ballot/cumulativeballot.py index 42543a47..035729dc 100644 --- a/pabutools/election/ballot/cumulativeballot.py +++ b/pabutools/election/ballot/cumulativeballot.py @@ -1,6 +1,7 @@ """ Cumulative ballots, i.e., ballots in which the voters distribute a given amount of points to the projects. """ + from __future__ import annotations from abc import ABC diff --git a/pabutools/election/ballot/ordinalballot.py b/pabutools/election/ballot/ordinalballot.py index f9974c93..4e6259a8 100644 --- a/pabutools/election/ballot/ordinalballot.py +++ b/pabutools/election/ballot/ordinalballot.py @@ -1,6 +1,7 @@ """ Ordinal ballots, i.e., ballots in which the voters order the projects given their preferences. """ + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/pabutools/election/instance.py b/pabutools/election/instance.py index dc37594b..9146b36a 100644 --- a/pabutools/election/instance.py +++ b/pabutools/election/instance.py @@ -3,6 +3,7 @@ The :py:class:`~pabutools.election.instance.Project` and the :py:class:`~pabutools.election.instance.Instance` classes are defined here. """ + from __future__ import annotations from collections.abc import Collection, Generator diff --git a/pabutools/election/pabulib.py b/pabutools/election/pabulib.py index f08c8fee..c71f4204 100644 --- a/pabutools/election/pabulib.py +++ b/pabutools/election/pabulib.py @@ -1,6 +1,7 @@ """ Tools to work with PaBuLib. """ + from natsort import natsorted from copy import deepcopy @@ -410,10 +411,15 @@ def update_meta_value(meta_dict, inst_meta, field, mandatory=False): file_str += f"{key};{value}\n" file_str += "PROJECTS\n" + ";".join(project_keys) + "\n" for project_dict in project_dicts: - file_str += ";".join([str(project_dict.get(key, "None")) for key in project_keys]) + "\n" + file_str += ( + ";".join([str(project_dict.get(key, "None")) for key in project_keys]) + + "\n" + ) file_str += "VOTES\n" + ";".join(vote_keys) + "\n" for vote_dict in vote_dicts: - file_str += ";".join([str(vote_dict.get(key, "None")) for key in vote_keys]) + "\n" + file_str += ( + ";".join([str(vote_dict.get(key, "None")) for key in vote_keys]) + "\n" + ) return file_str diff --git a/pabutools/election/preflib.py b/pabutools/election/preflib.py index 1330dbe0..2254c959 100644 --- a/pabutools/election/preflib.py +++ b/pabutools/election/preflib.py @@ -1,6 +1,7 @@ """ Tools to work with PrefLib. """ + from __future__ import annotations import preflibtools.instances as preflib diff --git a/pabutools/election/profile/__init__.py b/pabutools/election/profile/__init__.py index acaf0132..99369f19 100644 --- a/pabutools/election/profile/__init__.py +++ b/pabutools/election/profile/__init__.py @@ -33,7 +33,6 @@ """ - from pabutools.election.profile.profile import AbstractProfile, Profile, MultiProfile from pabutools.election.profile.approvalprofile import ( AbstractApprovalProfile, diff --git a/pabutools/election/profile/approvalprofile.py b/pabutools/election/profile/approvalprofile.py index 978c1ac6..83afb65f 100644 --- a/pabutools/election/profile/approvalprofile.py +++ b/pabutools/election/profile/approvalprofile.py @@ -1,6 +1,7 @@ """ Approval profiles, i.e., collections of approval ballots. """ + from __future__ import annotations from abc import ABC diff --git a/pabutools/election/profile/profile.py b/pabutools/election/profile/profile.py index 6a5f48a0..940b852d 100644 --- a/pabutools/election/profile/profile.py +++ b/pabutools/election/profile/profile.py @@ -1,6 +1,7 @@ """ Profiles, i.e., collections of ballots. """ + from __future__ import annotations from collections import Counter diff --git a/pabutools/election/satisfaction/additivesatisfaction.py b/pabutools/election/satisfaction/additivesatisfaction.py index b8adafc5..5fc9095c 100644 --- a/pabutools/election/satisfaction/additivesatisfaction.py +++ b/pabutools/election/satisfaction/additivesatisfaction.py @@ -1,6 +1,7 @@ """ Additive satisfaction measures. """ + from __future__ import annotations from collections.abc import Callable, Collection diff --git a/pabutools/election/satisfaction/functionalsatisfaction.py b/pabutools/election/satisfaction/functionalsatisfaction.py index 5be6dda7..b03d7129 100644 --- a/pabutools/election/satisfaction/functionalsatisfaction.py +++ b/pabutools/election/satisfaction/functionalsatisfaction.py @@ -1,6 +1,7 @@ """ Functional satisfaction measures. """ + from __future__ import annotations from collections.abc import Callable, Iterable, Collection diff --git a/pabutools/election/satisfaction/positionalsatisfaction.py b/pabutools/election/satisfaction/positionalsatisfaction.py index 110e8fc6..3cc9e192 100644 --- a/pabutools/election/satisfaction/positionalsatisfaction.py +++ b/pabutools/election/satisfaction/positionalsatisfaction.py @@ -1,6 +1,7 @@ """ Positional satisfaction measures. """ + from __future__ import annotations from collections.abc import Callable, Iterable, Collection diff --git a/pabutools/election/satisfaction/satisfactionmeasure.py b/pabutools/election/satisfaction/satisfactionmeasure.py index 2da3f2e2..478f4c4c 100644 --- a/pabutools/election/satisfaction/satisfactionmeasure.py +++ b/pabutools/election/satisfaction/satisfactionmeasure.py @@ -1,6 +1,7 @@ """ Satisfaction measures. """ + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/pabutools/election/satisfaction/satisfactionprofile.py b/pabutools/election/satisfaction/satisfactionprofile.py index 79b51d71..56e191a3 100644 --- a/pabutools/election/satisfaction/satisfactionprofile.py +++ b/pabutools/election/satisfaction/satisfactionprofile.py @@ -1,6 +1,7 @@ """ Satisfaction profiles. """ + from __future__ import annotations from collections import Counter @@ -206,9 +207,9 @@ class SatisfactionMultiProfile(Counter, GroupSatisfactionMeasure): def __init__( self, - init: Iterable[SatisfactionMeasure] - | dict[SatisfactionMeasure, int] - | None = None, + init: ( + Iterable[SatisfactionMeasure] | dict[SatisfactionMeasure, int] | None + ) = None, instance: Instance | None = None, profile: Profile | None = None, multiprofile: MultiProfile | None = None, diff --git a/pabutools/fractions.py b/pabutools/fractions.py index 3c11fb84..35d0aeaa 100644 --- a/pabutools/fractions.py +++ b/pabutools/fractions.py @@ -1,6 +1,7 @@ """ Module introducing all the functions used to handle fractions. """ + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/pabutools/rules/__init__.py b/pabutools/rules/__init__.py index 7fd1f604..0ae45a14 100644 --- a/pabutools/rules/__init__.py +++ b/pabutools/rules/__init__.py @@ -17,20 +17,23 @@ All rules return one or several lists of projects called budget allocations, represented by the class :py:class:`~pabutools.rules.budgetallocation.BudgetAllocation`. """ + from pabutools.rules.exhaustion import ( completion_by_rule_combination, exhaustion_by_budget_increase, ) from pabutools.rules.greedywelfare import greedy_utilitarian_welfare from pabutools.rules.maxwelfare import max_additive_utilitarian_welfare -from pabutools.rules.mes import method_of_equal_shares +from pabutools.rules.mes import ( + method_of_equal_shares, + MESAllocationDetails, + MESIteration, +) from pabutools.rules.phragmen import sequential_phragmen from pabutools.rules.composition import social_welfare_comparison, popularity_comparison from pabutools.rules.budgetallocation import ( BudgetAllocation, AllocationDetails, - MESAllocationDetails, - MESIteration, ) __all__ = [ diff --git a/pabutools/rules/budgetallocation.py b/pabutools/rules/budgetallocation.py index 2d0384ab..d1e0980c 100644 --- a/pabutools/rules/budgetallocation.py +++ b/pabutools/rules/budgetallocation.py @@ -1,8 +1,9 @@ +from __future__ import annotations + from collections.abc import Iterable from pabutools.election.instance import Project -from collections.abc import Collection from pabutools.utils import Numeric @@ -16,94 +17,6 @@ def __init__(self): pass -class MESAllocationDetails(AllocationDetails): - """ - Class representing the details of MES rule. - This class represents the MES details using an iteration approach: at each iteration of the MES rule some - crucial informations are saved which allow for reconstruction of the whole run. Iteration happens whenever - a project got picked or discraded during a run. - - Parameters - ---------- - initial_budget_per_voter: Numeric - Describes the starting budget of each voter. - - - Attributes - ---------- - initial_budget_per_voter: Numeric - Describes the starting budget of each voter. - iterations: Iterable[:py:class:`~pabutools.rules.budgetallocation.MESIteration`] - A list of all iterations of a MES rule run. It is progressively populated during a MES rule run. - """ - def __init__( - self, - initial_budget_per_voter: Numeric, - ): - self.initial_budget_per_voter: Numeric = initial_budget_per_voter - self.iterations: list[MESIteration] = [] - - def __str__(self): - return f"MESAllocationDetails[{self.iterations}]" - - def __repr__(self): - return f"MESAllocationDetails[{self.iterations}]" - - -class MESIteration: - """ - Class representing a single iteration of a MES rule run, solely used in :py:class:`~pabutools.rules.budgetallocation.MESAllocationDetails` - Each iteration consist of information necessary for reconstructing a MES rule run. This includes the project the iteration is about, its - supporters, whether it was picked or discarded, and the voters budgets after potential 'purchase' of the project. - - Parameters - ---------- - project: :py:class:`~pabutools.election.instance.Project` - The project that was considered by a MES iteration. - supporter_indices: list[int] - Stores all indices of voters which supported the aforementioned project. - Those indices match with indices of voters_budget attribute. - was_picked: bool - Indicates whether the aforementioned project was picked or discraded by MES. - voters_budget: list[int], optional - Describes the budget of each voter at the current iteration. If a project was picked, then the voters budgets - describe the state after the purchase. If the project wasn't picked, it stays the same (compared to previous iteration). - Defaults to `[]`. - - - Attributes - ---------- - project: :py:class:`~pabutools.election.instance.Project` - The project that was considered by a MES iteration. - supporter_indices: list[int] - Stores all indices of voters which supported the aforementioned project. - Those indices match with indices of voters_budget attribute. - was_picked: bool - Indicates whether the aforementioned project was picked or discraded by MES. - voters_budget: list[int], optional - Describes the budget of each voter at the current iteration. If a project was picked, then the voters budgets - describe the state after the purchase. If the project wasn't picked, it stays the same (compared to previous iteration). - Defaults to `[]`. - """ - def __init__( - self, - project: Project, - supporter_indices: list[int], - was_picked: bool, - voters_budget: list[int] = [], - ): - self.project: Project = project - self.supporter_indices: list[int] = supporter_indices - self.was_picked: bool = was_picked - self.voters_budget: list[int] = voters_budget - - def __str__(self): - return f"MESIteration[Project: {self.project.name} {'was picked' if self.was_picked else 'was not picked'}]" - - def __repr__(self): - return f"MESIteration[Project: {self.project.name} {'was picked' if self.was_picked else 'was not picked'}]" - - class BudgetAllocation(list[Project]): """ A budget allocation is the outcome of rule. It simply corresponds to a list of projects. @@ -127,9 +40,7 @@ class BudgetAllocation(list[Project]): """ def __init__( - self, - init: Iterable[Project] = (), - details: AllocationDetails | None = None + self, init: Iterable[Project] = (), details: AllocationDetails | None = None ) -> None: list.__init__(self, init) if details is None: diff --git a/pabutools/rules/composition.py b/pabutools/rules/composition.py index 6a0adb58..a006dc1a 100644 --- a/pabutools/rules/composition.py +++ b/pabutools/rules/composition.py @@ -1,6 +1,7 @@ """ Module implementing different ways to compose rules. """ + from __future__ import annotations from collections.abc import Collection, Callable, Iterable diff --git a/pabutools/rules/greedywelfare.py b/pabutools/rules/greedywelfare.py index 71314a36..91dff60e 100644 --- a/pabutools/rules/greedywelfare.py +++ b/pabutools/rules/greedywelfare.py @@ -1,6 +1,7 @@ """ Greedy approximations of the maximum welfare. """ + from __future__ import annotations from copy import copy diff --git a/pabutools/rules/maxwelfare.py b/pabutools/rules/maxwelfare.py index af55209c..96b879cf 100644 --- a/pabutools/rules/maxwelfare.py +++ b/pabutools/rules/maxwelfare.py @@ -1,6 +1,7 @@ """ Welfare-maximizing rules. """ + from __future__ import annotations from collections.abc import Collection diff --git a/pabutools/rules/mes/__init__.py b/pabutools/rules/mes/__init__.py new file mode 100644 index 00000000..1ab35fa2 --- /dev/null +++ b/pabutools/rules/mes/__init__.py @@ -0,0 +1,18 @@ +from pabutools.rules.mes.mes_rule import ( + method_of_equal_shares, + naive_mes, + method_of_equal_shares_scheme, + mes_inner_algo, + affordability_poor_rich, +) +from pabutools.rules.mes.mes_details import MESAllocationDetails, MESIteration + +__all__ = [ + "method_of_equal_shares", + "method_of_equal_shares_scheme", + "mes_inner_algo", + "naive_mes", + "affordability_poor_rich", + "MESAllocationDetails", + "MESIteration", +] diff --git a/pabutools/rules/mes/mes_details.py b/pabutools/rules/mes/mes_details.py new file mode 100644 index 00000000..2557cf0e --- /dev/null +++ b/pabutools/rules/mes/mes_details.py @@ -0,0 +1,92 @@ +from pabutools.election.instance import Project +from pabutools.rules.budgetallocation import AllocationDetails +from pabutools.utils import Numeric + + +class MESAllocationDetails(AllocationDetails): + """Class representing the details of MES rule. + This class represents the MES details using an iteration approach: at each iteration of the MES rule some + crucial information is saved which allow for reconstruction of the whole run. Iteration happens whenever + a project got picked or discarded during a run. + + Parameters + ---------- + initial_budget_per_voter: :py:class:`~pabutools.utils.Numeric` + Describes the starting budget of each voter. + + + Attributes + ---------- + initial_budget_per_voter: :py:class:`~pabutools.utils.Numeric` + Describes the starting budget of each voter. + iterations: Iterable[:py:class:`~pabutools.rules.mes.MESIteration`] + A list of all iterations of a MES rule run. It is progressively populated during a MES rule run. + """ + + def __init__(self, initial_budget_per_voter: Numeric): + super().__init__() + self.initial_budget_per_voter: Numeric = initial_budget_per_voter + self.iterations: list[MESIteration] = [] + + def __str__(self): + return f"MESAllocationDetails[{self.iterations}]" + + def __repr__(self): + return f"MESAllocationDetails[{self.iterations}]" + + +class MESIteration: + """Class representing a single iteration of a MES rule run, solely used in + :py:class:`~pabutools.rules.mes.MESAllocationDetails`. Each iteration consist of information + necessary for reconstructing a MES rule run. This includes the project the iteration is about, its supporters, + whether it was picked or discarded, and the voters budgets after potential 'purchase' of the project. + + Parameters + ---------- + project: :py:class:`~pabutools.election.instance.Project` + The project that was considered by a MES iteration. + supporter_indices: list[int] + Stores all indices of voters which supported the aforementioned project. + Those indices match with indices of voters_budget attribute. + was_picked: bool + Indicates whether the aforementioned project was picked or discarded by MES. + voters_budget: list[int], optional + Describes the budget of each voter at the current iteration. If a project was picked, then the voters budgets + describe the state after the purchase. If the project wasn't picked, it stays the same (compared to previous iteration). + Defaults to `[]`. + + + Attributes + ---------- + project: :py:class:`~pabutools.election.instance.Project` + The project that was considered by a MES iteration. + supporter_indices: list[int] + Stores all indices of voters which supported the aforementioned project. + Those indices match with indices of voters_budget attribute. + was_picked: bool + Indicates whether the aforementioned project was picked or discarded by MES. + voters_budget: list[int], optional + Describes the budget of each voter at the current iteration. If a project was picked, then the voters budgets + describe the state after the purchase. If the project wasn't picked, it stays the same (compared to previous iteration). + Defaults to `[]`. + """ + + def __init__( + self, + project: Project, + supporter_indices: list[int], + was_picked: bool, + voters_budget=None, + ): + if voters_budget is None: + voters_budget = [] + self.project: Project = project + self.supporter_indices: list[int] = supporter_indices + self.was_picked: bool = was_picked + self.voters_budget: list[int] = voters_budget + + def __str__(self): + return f"MESIteration[Project: {self.project.name} {'was picked' if self.was_picked else 'was not picked'}]" + + def __repr__(self): + return f"MESIteration[Project: {self.project.name} {'was picked' if self.was_picked else 'was not picked'}]" diff --git a/pabutools/rules/mes.py b/pabutools/rules/mes/mes_rule.py similarity index 95% rename from pabutools/rules/mes.py rename to pabutools/rules/mes/mes_rule.py index 86043ea0..1e8021d3 100644 --- a/pabutools/rules/mes.py +++ b/pabutools/rules/mes/mes_rule.py @@ -1,16 +1,14 @@ """ The method of equal shares. """ + from __future__ import annotations from copy import copy, deepcopy -from collections.abc import Collection, Iterable +from collections.abc import Iterable -from pabutools.rules.budgetallocation import ( - BudgetAllocation, - MESAllocationDetails, - MESIteration -) +from pabutools.rules.budgetallocation import BudgetAllocation +from pabutools.rules.mes.mes_details import MESAllocationDetails, MESIteration from pabutools.utils import Numeric from pabutools.election import AbstractApprovalProfile @@ -172,7 +170,7 @@ def affordability_poor_rich(voters: list[MESVoter], project: MESProject) -> Nume """ rich = set(project.supporter_indices) - poor = set() + poor = {} while len(rich) > 0: poor_budget = sum(voters[i].total_budget() for i in poor) numerator = frac(project.cost - poor_budget) @@ -327,7 +325,7 @@ def mes_inner_algo( (`resoluteness = False`). """ - tied_projects = [] + tied_projects: list[MESProject] = [] if analytics: local_iterations = [] iteration_picked = [] @@ -403,8 +401,8 @@ def mes_inner_algo( if not tied_projects: if analytics: voters_budget = [voter.budget for voter in voters] - for iter in local_iterations: - iter.voters_budget = voters_budget + for iteration in local_iterations: + iteration.voters_budget = voters_budget current_alloc.details.iterations.extend(local_iterations) if resoluteness: all_allocs.append(current_alloc) @@ -442,11 +440,11 @@ def mes_inner_algo( ) if analytics: new_voters_budget = [voter.budget for voter in new_voters] - for iter_idx, iter in enumerate(local_iterations): + for iter_idx, iteration in enumerate(local_iterations): if iter_idx < iteration_picked[select_idx]: - iter.voters_budget = old_voters_budget + iteration.voters_budget = old_voters_budget else: - iter.voters_budget = new_voters_budget + iteration.voters_budget = new_voters_budget local_iterations.insert( iteration_picked[select_idx], MESIteration( @@ -513,9 +511,13 @@ def method_of_equal_shares_scheme( Uses the inner algorithm for binary satisfaction if set to `True`. Should typically be used with approval ballots to gain on the runtime. Automatically set to `True` if an approval profile is given. analytics: bool, optional - (De)Activate the calculation of analytics. + (De)Activate the computation of analytics. These are additional details that can be accessed from the + :py:class:`~pabutools.rules.budgetallocation.BudgetAllocation` object returned by the rule to perform + analyses. + Defaults to `False`. verbose : bool, optional (De)Activate the display of additional information. + Defaults to `False`. Returns ------- :py:class:`~pabutools.rules.budgetallocation.BudgetAllocation` | list[:py:class:`~pabutools.rules.budgetallocation.BudgetAllocation`] @@ -657,9 +659,13 @@ def method_of_equal_shares( Uses the inner algorithm for binary satisfaction if set to `True`. Should typically be used with approval ballots to gain on the runtime. Automatically set to `True` if an approval profile is given. analytics: bool, optional - (De)Activate the calculation of analytics. + (De)Activate the computation of analytics. These are additional details that can be accessed from the + :py:class:`~pabutools.rules.budgetallocation.BudgetAllocation` object returned by the rule to perform + analyses. + Defaults to `False`. verbose : bool, optional (De)Activate the display of additional information. + Defaults to `False`. Returns ------- diff --git a/pabutools/rules/phragmen.py b/pabutools/rules/phragmen.py index 4570d338..7fd05071 100644 --- a/pabutools/rules/phragmen.py +++ b/pabutools/rules/phragmen.py @@ -1,6 +1,7 @@ """ Phragmén's methods. """ + from __future__ import annotations from collections.abc import Collection diff --git a/pabutools/utils.py b/pabutools/utils.py index 046a347f..cbca0e46 100644 --- a/pabutools/utils.py +++ b/pabutools/utils.py @@ -1,6 +1,7 @@ """ Collection of util functions. """ + from __future__ import annotations from collections.abc import Iterable, Generator diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 648ee096..69844a68 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -21,11 +21,8 @@ Relative_Cardinality_Sat, ) from pabutools.fractions import frac -from pabutools.rules.budgetallocation import ( - BudgetAllocation, - MESAllocationDetails, - MESIteration, -) +from pabutools.rules.budgetallocation import BudgetAllocation +from pabutools.rules.mes import MESAllocationDetails, MESIteration class TestAnalysis(TestCase): @@ -278,13 +275,23 @@ def test_project_loss(self): initial_budget_per_voter = frac(1, 1) iterations = [ - MESIteration(projects[idx], supporters[idx], was_picked[idx], voters_budget[idx]) for idx in range(len(projects)) + MESIteration( + projects[idx], supporters[idx], was_picked[idx], voters_budget[idx] + ) + for idx in range(len(projects)) ] allocation_details = MESAllocationDetails(initial_budget_per_voter) allocation_details.iterations = iterations project_losses = calculate_project_loss(allocation_details) - expected_budgets = [frac(4, 1), frac(2, 1), frac(1, 2), frac(1, 1), frac(0, 1), frac(1, 1)] + expected_budgets = [ + frac(4, 1), + frac(2, 1), + frac(1, 2), + frac(1, 1), + frac(0, 1), + frac(1, 1), + ] expected_losses = [ {}, {projects[0]: frac(1, 1)}, diff --git a/tests/test_ballots.py b/tests/test_ballots.py index c8c7ce86..b01918c3 100644 --- a/tests/test_ballots.py +++ b/tests/test_ballots.py @@ -1,6 +1,7 @@ """ Module testing the ballots. """ + from unittest import TestCase from pabutools.election import ( diff --git a/tests/test_class_inheritence.py b/tests/test_class_inheritence.py index 8b94675e..59d88324 100644 --- a/tests/test_class_inheritence.py +++ b/tests/test_class_inheritence.py @@ -1,6 +1,7 @@ """ Module testing class inheritance for basic Python classes (list, tuple, etc...). """ + from copy import deepcopy from unittest import TestCase diff --git a/tests/test_fractions.py b/tests/test_fractions.py index 9dd478e9..8ef4d411 100644 --- a/tests/test_fractions.py +++ b/tests/test_fractions.py @@ -1,6 +1,7 @@ """ Module testing the custom fractions. """ + from unittest import TestCase from pabutools.fractions import * diff --git a/tests/test_pabulib.py b/tests/test_pabulib.py index eec178a8..0a885e15 100644 --- a/tests/test_pabulib.py +++ b/tests/test_pabulib.py @@ -5,7 +5,8 @@ parse_pabulib, parse_pabulib_from_string, parse_pabulib_from_url, - write_pabulib, election_as_pabulib_string, + write_pabulib, + election_as_pabulib_string, ) import os @@ -598,7 +599,9 @@ def test_legal_defaults(self): assert profile.legal_max_total_score == 100 def test_url_parse(self): - url = "http://pabulib.org/tiles/download/poland_warszawa_2018_pole-mokotowskie.pb" + url = ( + "http://pabulib.org/tiles/download/poland_warszawa_2018_pole-mokotowskie.pb" + ) url_inst, url_prof = parse_pabulib_from_url(url) assert url_inst.file_name == "poland_warszawa_2018_pole-mokotowskie.pb" assert url_inst.file_path == url diff --git a/tests/test_profile.py b/tests/test_profile.py index 9ab43952..19eb4470 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -1,6 +1,7 @@ """ Module for testing profiles. """ + from unittest import TestCase from pabutools.election import ( diff --git a/tests/test_rule.py b/tests/test_rule.py index afd57d2c..e97bbb00 100644 --- a/tests/test_rule.py +++ b/tests/test_rule.py @@ -109,9 +109,9 @@ def dummy_elections(): test_election.irr_results_sat[max_additive_utilitarian_welfare][Cost_Sat] = sorted( [[p[0], p[2]], [p[1]], [p[2], p[3]]] ) - test_election.irr_results_sat[max_additive_utilitarian_welfare][ - Cardinality_Sat - ] = sorted([[p[0], p[3]], [p[0], p[2]], [p[2], p[3]]]) + test_election.irr_results_sat[max_additive_utilitarian_welfare][Cardinality_Sat] = ( + sorted([[p[0], p[3]], [p[0], p[2]], [p[2], p[3]]]) + ) res.append(test_election) # Approval example 2 @@ -193,9 +193,9 @@ 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[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[mes_iterated][Cost_Sat] = sorted([[p[2]]]) test_election.irr_results_sat[method_of_equal_shares][Cardinality_Sat] = sorted( @@ -212,9 +212,9 @@ def dummy_elections(): prof = ApprovalProfile([ApprovalBallot()], instance=inst) test_election = DummyElection("EmptyProfile", p, inst, prof) for sat_class in ALL_SAT: - test_election.irr_results_sat[max_additive_utilitarian_welfare][ - sat_class - ] = sorted([sorted(list(b)) for b in inst.budget_allocations()]) + test_election.irr_results_sat[max_additive_utilitarian_welfare][sat_class] = ( + sorted([sorted(list(b)) for b in inst.budget_allocations()]) + ) test_election.irr_results_sat[greedy_utilitarian_welfare][sat_class] = sorted( [ sorted(list(b)) @@ -239,9 +239,9 @@ def dummy_elections(): initial_alloc = p[:1] test_election = DummyElection("EmptyProfile_Initial", p, inst, prof, initial_alloc) for sat_class in ALL_SAT: - test_election.irr_results_sat[max_additive_utilitarian_welfare][ - sat_class - ] = sorted([sorted(list(b)) for b in inst.budget_allocations() if p[0] in b]) + test_election.irr_results_sat[max_additive_utilitarian_welfare][sat_class] = ( + sorted([sorted(list(b)) for b in inst.budget_allocations() if p[0] in b]) + ) test_election.irr_results_sat[greedy_utilitarian_welfare][sat_class] = sorted( [ sorted(list(b)) @@ -345,19 +345,19 @@ def run_sat_rule(rule, verbose=False): f"{sorted(resolute_out) in test_election.irr_results_sat[rule][sat_class]} " f"({type(resolute_out)})" ) - irresolute_out = ( - rule( - test_election.instance, - profile, - sat_class=sat_class, - sat_profile=sat_profile, - resoluteness=False, - initial_budget_allocation=test_election.initial_alloc, - ) + irresolute_out = rule( + test_election.instance, + profile, + sat_class=sat_class, + sat_profile=sat_profile, + resoluteness=False, + initial_budget_allocation=test_election.initial_alloc, ) if verbose: - print(f"Irres outcome: {irresolute_out} " - f"({tuple(type(out) for out in irresolute_out)})") + print( + f"Irres outcome: {irresolute_out} " + f"({tuple(type(out) for out in irresolute_out)})" + ) print( f"Irres expected: {test_election.irr_results_sat[rule][sat_class]}" ) @@ -394,9 +394,8 @@ def run_sat_rule(rule, verbose=False): in test_election.irr_results_sat[rule][sat_class] ) assert sorted(resolute_out) == sorted(resolute_out_sat_profile) - assert ( - sorted(sorted(x) for x in irresolute_out) - == sorted(test_election.irr_results_sat[rule][sat_class]) + assert sorted(sorted(x) for x in irresolute_out) == sorted( + test_election.irr_results_sat[rule][sat_class] ) assert isinstance(resolute_out, BudgetAllocation)