-
Notifications
You must be signed in to change notification settings - Fork 25
Added BOS equal shares and fractional equal shares with tests #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
IvanGorbache
wants to merge
14
commits into
COMSOC-Community:main
Choose a base branch
from
IvanGorbache:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+564
−0
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
bdbc046
upload
IvanGorbache 363efa9
Added an additional test
IvanGorbache 4ef8011
removed the main function used for testing doctest
IvanGorbache 365de1b
added more tests
IvanGorbache 53927e3
first try
IvanGorbache b9d561a
fixed dict merging multiple entries into one
IvanGorbache cd3e3db
now working with CardinalBallots
IvanGorbache 40a6f6e
added fres impelmentation and tests
IvanGorbache 816594f
added EJR testing
IvanGorbache f085cc2
more tests
IvanGorbache 361cc99
added logging
IvanGorbache 7ea7a4c
updated logging
IvanGorbache 09b3200
made needed changes for pull
IvanGorbache c2d32c9
updated for pull request
IvanGorbache File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| """ | ||
| An implementation of the algorithms found in: | ||
| "Method of Equal Shares with Bounded Overspending" | ||
| https://www.ac.tuwien.ac.at/comsoc2025/comsoc2025-papers/50.pdf | ||
|
|
||
| Programmer: Ivan Gorbachev | ||
| Date: 17/04/2026 | ||
| """ | ||
| from __future__ import annotations | ||
| import logging | ||
| import math | ||
|
|
||
| from scipy.optimize import root_scalar | ||
|
|
||
| from pabutools.election import Project, Instance, AbstractProfile | ||
| from pabutools.rules.budgetallocation import BudgetAllocation, AllocationDetails | ||
|
|
||
|
|
||
| class FractionalAllocationDetails(AllocationDetails): | ||
| """ | ||
| Metadata container for tracking the funded fractions of projects | ||
| resulting from fractional_equal_shares. | ||
| """ | ||
| def __init__(self, fractions: dict[Project, float]): | ||
| super().__init__() | ||
| self.fractions = fractions | ||
|
|
||
|
|
||
| def get_utility(voter, project): | ||
| if hasattr(voter, "utility"): | ||
| return voter.utility(project) | ||
| return 1 if project in voter else 0 | ||
|
|
||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please type all the functions as done in the other files of the package.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be uniform with the rest of the package. |
||
| def bos_equal_shares(instance: Instance, profile: AbstractProfile) -> BudgetAllocation: | ||
| """ | ||
| Algorithm "BOS Equal Shares" - The algorithm selects a subset of projects such that the resulting subset is both | ||
| affordable under the budget while also exhausting it and guaranteeing fairness. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| instance : :py:class:`~pabutools.election.instance.Instance` | ||
| The instance. | ||
| profile : :py:class:`~pabutools.election.profile.profile.AbstractProfile` | ||
| The profile. | ||
|
|
||
| Returns | ||
| ------- | ||
| :py:class:`~pabutools.rules.budgetallocation.BudgetAllocation` | ||
| The selected projects packaged into a BudgetAllocation object. | ||
| """ | ||
| logger = logging.getLogger(__name__) | ||
| logger.info("\nBOS equal shares") | ||
|
|
||
|
Simon-Rey marked this conversation as resolved.
|
||
| voters = list(profile) | ||
| selected_projects = list() | ||
| cost_selected_projects = 0 | ||
|
|
||
| budget = instance.budget_limit | ||
| num_voters = profile.num_ballots() | ||
|
|
||
| if num_voters == 0: | ||
| return BudgetAllocation([]) | ||
|
|
||
| virtual_budgets = [budget / num_voters for _ in voters] | ||
| all_projects = list(instance) | ||
|
|
||
| logger.info(f"Budget: {budget}") | ||
| logger.info(f"Virtual budgets: {[{str(v): round(b, 2)} for v, b in zip(voters, virtual_budgets)]}") | ||
|
|
||
| budget_for_project = { | ||
| project: sum(virtual_budgets[i] * get_utility(voter, project) for i, voter in enumerate(voters)) for | ||
| project in all_projects} | ||
|
|
||
| available_projects = [project for project in all_projects if cost_selected_projects + project.cost <= budget and | ||
| budget_for_project[project] > 0 and project not in selected_projects] | ||
|
|
||
| while available_projects and cost_selected_projects < budget: | ||
| logger.info(f"Remaining budget: {budget - cost_selected_projects}") | ||
| best_alpha = 1 | ||
| best_rho = math.inf | ||
| best_project = None | ||
| for project in available_projects: | ||
| supporters = [(i, voter) for i, voter in enumerate(voters) if get_utility(voter, project) > 0] | ||
| if not supporters: | ||
| continue | ||
| supporters_budgets = [virtual_budgets[i] for i, voter in supporters] | ||
| supporters_utils = [get_utility(voter, project) for i, voter in supporters] | ||
| if sum(supporters_budgets) < project.cost: | ||
| lambda_prime = math.inf | ||
| else: | ||
| res = root_scalar( | ||
| lambda lmbda: sum(min(b, lmbda * project.cost * u) for b, u in | ||
| zip(supporters_budgets, supporters_utils)) - project.cost, | ||
| bracket=[0, 1.0] | ||
| ) | ||
| lambda_prime = res.root | ||
| lambdas = [virtual_budgets[i] / (project.cost * u) for i, u in | ||
| zip([s[0] for s in supporters], supporters_utils)] | ||
| lambdas.append(lambda_prime) | ||
| for lamb in lambdas: | ||
| total_collected = (sum( | ||
| min(virtual_budgets[i], lamb * project.cost * u) for i, u in | ||
| zip([s[0] for s in supporters], supporters_utils))) | ||
| alpha = min(total_collected / project.cost, 1) | ||
| if alpha <= 0: | ||
| continue | ||
| rho = lamb / alpha | ||
| if rho / alpha < best_rho / best_alpha: | ||
| best_rho = rho | ||
| best_alpha = alpha | ||
| best_project = project | ||
|
|
||
| if best_project is None: | ||
| break | ||
|
|
||
| logger.info(f"Selected project: {best_project}") | ||
| logger.info(f"alpha = {best_alpha:.4f}, rho = {best_rho:.4f}, rho/alpha = {best_rho / best_alpha:.4f}") | ||
|
|
||
| if best_project.cost + cost_selected_projects <= budget and best_project not in selected_projects: | ||
| selected_projects.append(best_project) | ||
|
|
||
| cost_selected_projects = sum(project.cost for project in selected_projects) | ||
|
|
||
| for i, voter in enumerate(voters): | ||
| u = get_utility(voter, best_project) | ||
| if u > 0: | ||
| virtual_budgets[i] = max(0, virtual_budgets[i] - best_rho * best_project.cost * u) | ||
| logger.info(f"Updated virtual budgets: {[{str(v): round(b, 2)} for v, b in zip(voters, virtual_budgets)]}") | ||
|
|
||
| budget_for_project = { | ||
| project: sum(virtual_budgets[i] * get_utility(voter, project) for i, voter in enumerate(voters)) for | ||
| project in all_projects} | ||
|
|
||
| available_projects = [project for project in all_projects if | ||
| cost_selected_projects + project.cost <= budget and budget_for_project[ | ||
| project] > 0 and project not in selected_projects] | ||
| logger.info(f"Selected projects: {selected_projects}\n") | ||
|
|
||
| return BudgetAllocation(selected_projects) | ||
|
|
||
|
|
||
| def fractional_equal_shares(instance: Instance, profile: AbstractProfile) -> BudgetAllocation: | ||
| """ | ||
| Algorithm "fractional equal shares" - The algorithm works much like equal shares with the exception that it | ||
| allows players to purchase fractional shares in the projects they support for fractional cost. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| instance : :py:class:`~pabutools.election.instance.Instance` | ||
| The instance. | ||
| profile : :py:class:`~pabutools.election.profile.profile.AbstractProfile` | ||
| The profile. | ||
|
|
||
| Returns | ||
| ------- | ||
| :py:class:`~pabutools.rules.budgetallocation.BudgetAllocation` | ||
| The funded projects packaged into a BudgetAllocation object, with an attached details map | ||
| tracking exact fractions. | ||
| """ | ||
| logger = logging.getLogger(__name__) | ||
| logger.info("\nFractional equal shares") | ||
|
|
||
| voters = sorted(list(profile), key=lambda v: str(v)) | ||
| cost_selected_projects = 0 | ||
| budget = instance.budget_limit | ||
| num_voters = profile.num_ballots() | ||
|
|
||
| if num_voters == 0: | ||
| return BudgetAllocation([], details=FractionalAllocationDetails({})) | ||
|
|
||
| virtual_budgets = [budget / num_voters for _ in voters] | ||
| all_projects = sorted(list(instance), key=lambda p: str(p)) | ||
| logger.info(f"Budget: {budget}") | ||
| logger.info(f"Virtual budgets: {[{str(v): round(b, 2)} for v, b in zip(voters, virtual_budgets)]}") | ||
|
|
||
| budget_for_project = { | ||
| project: sum(virtual_budgets[i] * get_utility(voter, project) for i, voter in enumerate(voters)) | ||
| for project in all_projects | ||
| } | ||
|
|
||
| project_part = {project: 0.0 for project in all_projects} | ||
|
|
||
| available_projects = [ | ||
| project for project in all_projects | ||
| if cost_selected_projects + project.cost * (1 - project_part[project]) <= budget and | ||
| budget_for_project[project] > 0 and | ||
| project_part[project] != 1 | ||
| ] | ||
|
|
||
| while available_projects and cost_selected_projects < budget: | ||
| logger.info(f"Remaining budget: {budget - cost_selected_projects}") | ||
| project_utilities = {c: sum(get_utility(voter, c) * c.cost for voter in voters) for c in available_projects} | ||
| valid_projects = [c for c in available_projects if project_utilities[c] > 0] | ||
|
|
||
| if not valid_projects: | ||
| break | ||
|
|
||
| c = min(valid_projects, key=lambda project: project.cost / project_utilities[project]) | ||
| p = c.cost / project_utilities[c] | ||
|
|
||
| fractions = [1 - project_part[c]] | ||
| for i, voter in enumerate(voters): | ||
| denom = p * get_utility(voter, c) * c.cost | ||
| if denom > 0: | ||
| fractions.append(virtual_budgets[i] / denom) | ||
|
|
||
| a = min(fractions) | ||
| logger.info(f"Selected project: {c}") | ||
| logger.info(f"alpha = {a:.4f}, rho = {p:.4f}") | ||
|
|
||
| project_part[c] += a | ||
| for i, voter in enumerate(voters): | ||
| virtual_budgets[i] = max(0, virtual_budgets[i] - a * p * get_utility(voter, c) * c.cost) | ||
|
|
||
| voters = [v for i, v in enumerate(voters) if virtual_budgets[i] > 0] | ||
| virtual_budgets = [b for b in virtual_budgets if b > 0] | ||
| logger.info(f"Updated virtual budgets: {[{str(v): round(b, 2)} for v, b in zip(voters, virtual_budgets)]}") | ||
| budget_for_project = { | ||
| project: sum(virtual_budgets[i] * get_utility(voter, project) for i, voter in enumerate(voters)) | ||
| for project in all_projects | ||
| } | ||
| cost_selected_projects = sum(project.cost * project_part[project] for project in all_projects) | ||
|
|
||
| available_projects = [ | ||
| project for project in all_projects | ||
| if cost_selected_projects + project.cost * (1 - project_part[project]) <= budget and | ||
| budget_for_project[project] > 0 and | ||
| project_part[project] != 1 | ||
| ] | ||
| logger.info(f"Selected project parts: {project_part}\n") | ||
|
|
||
| funded_projects = [proj for proj, frac_val in project_part.items() if frac_val > 0] | ||
| details = FractionalAllocationDetails(project_part) | ||
| return BudgetAllocation(funded_projects, details=details) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.