Source code for pabutools.rules.maximin_support
"""
The maximin support rule.
"""
from __future__ import annotations
import logging
from collections.abc import Collection
from pulp import LpProblem, LpMinimize, LpVariable, lpSum, LpStatusOptimal, value, PULP_CBC_CMD
from pabutools.election import (
Instance,
Project,
total_cost,
AbstractApprovalProfile,
ApprovalMultiProfile,
)
from pabutools.rules.budgetallocation import BudgetAllocation
from pabutools.tiebreaking import TieBreakingRule, lexico_tie_breaking
logger = logging.getLogger(__name__)
[docs]
def maximin_support(
instance: Instance,
profile: AbstractApprovalProfile,
initial_budget_allocation: Collection[Project] | None = None,
tie_breaking: TieBreakingRule | None = None,
resoluteness: bool = True,
) -> BudgetAllocation:
"""
The maximin support rule (also introduced as "Generalised Sequential Phragmén" by Aziz, Lee and Talmon (2018)).
Parameters
----------
instance : :py:class:`~pabutools.election.instance.Instance`
The instance.
profile : :py:class:`~pabutools.election.profile.approvalprofile.AbstractApprovalProfile`
The profile.
initial_budget_allocation : Iterable[:py:class:`~pabutools.election.instance.Project`]
An initial budget allocation, typically empty.
tie_breaking : :py:class:`~pabutools.tiebreaking.TieBreakingRule`, optional
The tie-breaking rule used.
Defaults to the lexicographic tie-breaking.
resoluteness : bool, optional
Set to `False` to obtain an irresolute outcome, where all tied budget allocations are returned.
Defaults to True.
Returns
-------
:py:class:`~pabutools.rules.budgetallocation.BudgetAllocation`
The selected projects.
"""
if isinstance(profile, ApprovalMultiProfile):
raise NotImplementedError("The maximin support rule currently does not support multiprofiles.")
if not resoluteness:
raise NotImplementedError("The maximin support rule currently does not support irresolute outcomes.")
logging.info("Starting the maximin support rule")
if tie_breaking is None:
tie_breaking = lexico_tie_breaking
if initial_budget_allocation is None:
budget_allocation = BudgetAllocation()
else:
budget_allocation = BudgetAllocation(initial_budget_allocation)
remaining_budget = instance.budget_limit - total_cost(budget_allocation)
available_projects = set(
p
for p in instance
if p not in budget_allocation and 0 <= p.cost <= instance.budget_limit
)
approvers_map = {}
for p in available_projects:
approvers = [i for i, ballot in enumerate(profile) if p in ballot]
if approvers:
approvers_map[p] = approvers
while True:
available_projects = [p for p in available_projects if p.cost <= remaining_budget]
if len(available_projects) == 0:
break
min_new_maxload = None
arg_min_new_maxload = None
for p in available_projects:
new_maxload = _compute_optimal_load(budget_allocation + [p], profile)
if min_new_maxload is None or new_maxload < min_new_maxload:
min_new_maxload = new_maxload
arg_min_new_maxload = [p]
elif min_new_maxload == new_maxload:
arg_min_new_maxload.append(p)
chosen = tie_breaking.untie(instance, profile, arg_min_new_maxload)
budget_allocation.append(chosen)
remaining_budget -= chosen.cost
available_projects.remove(chosen)
return BudgetAllocation(budget_allocation)
def _compute_optimal_load(projects, profile):
"""
Solves the LP relaxation to minimize the max load.
Parameters
----------
projects : list of Project
The projects considered so far (W ∪ {c'}).
profile : AbstractApprovalProfile
The approval profile of the voters.
Returns
-------
float
The minimum max load (z) over voters given optimal distribution of costs.
"""
num_voters = profile.num_ballots()
voter_ids = range(num_voters)
prob = LpProblem("MinMaxLoad", LpMinimize)
# Decision variables: load each voter i takes for project p
x = {
(p, i): LpVariable(f"x_{p.name}_{i}", lowBound=0)
for p in projects for i in voter_ids
}
z = LpVariable("max_load", lowBound=0)
# Constraints
for p in projects:
for i in voter_ids:
if p not in profile[i]:
prob += x[p, i] == 0
for p in projects:
prob += lpSum(x[p, i] for i in voter_ids) == p.cost
for i in voter_ids:
prob += lpSum(x[p, i] for p in projects) <= z
prob += z # Objective: minimize max load
status = prob.solve(PULP_CBC_CMD(msg=False))
# This can mean a project that no one approves of for instance
if status != LpStatusOptimal:
return float("inf")
return value(z)