Skip to content

Commit

Permalink
Attempt to fix priaml/dual for Max Welfare
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon-Rey committed Mar 22, 2024
1 parent 41f0bd6 commit 7bfdc5a
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 63 deletions.
3 changes: 0 additions & 3 deletions pabutools/rules/budgetallocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@

from pabutools.election.instance import Project

from pabutools.utils import Numeric


class AllocationDetails:
"""
Class representing participatory budgeting rule run details.
Expand Down
128 changes: 73 additions & 55 deletions pabutools/rules/maxwelfare.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from __future__ import annotations

from collections.abc import Collection
from collections.abc import Collection, Iterable

import mip
from mip import Model, xsum, maximize, BINARY
Expand All @@ -21,11 +21,12 @@
)
from pabutools.rules.budgetallocation import BudgetAllocation

def max_additive_utilitarian_welfare_scheme(
instance: Instance,
sat_profile: GroupSatisfactionMeasure,
initial_budget_allocation: Collection[Project],
resoluteness: bool = True,

def max_additive_utilitarian_welfare_ilp_scheme(
instance: Instance,
sat_profile: GroupSatisfactionMeasure,
initial_budget_allocation: Collection[Project],
resoluteness: bool = True,
) -> BudgetAllocation | list[BudgetAllocation]:
"""
The inner algorithm for the welfare maximizing rule. It generates the corresponding budget allocations using a
Expand Down Expand Up @@ -78,14 +79,14 @@ def max_additive_utilitarian_welfare_scheme(
while True:
# See http://yetanothermathprogrammingconsultant.blogspot.com/2011/10/integer-cuts.html
mip_model += (
xsum(1 - p_vars[p] for p in previous_partial_alloc)
+ xsum(p_vars[p] for p in p_vars if p not in previous_partial_alloc)
>= 1
xsum(1 - p_vars[p] for p in previous_partial_alloc)
+ xsum(p_vars[p] for p in p_vars if p not in previous_partial_alloc)
>= 1
)
mip_model += (
xsum(p_vars[p] for p in previous_partial_alloc)
- xsum(p_vars[p] for p in p_vars if p not in previous_partial_alloc)
<= len(previous_partial_alloc) - 1
xsum(p_vars[p] for p in previous_partial_alloc)
- xsum(p_vars[p] for p in p_vars if p not in previous_partial_alloc)
<= len(previous_partial_alloc) - 1
)

opt_status = mip_model.optimize()
Expand All @@ -102,12 +103,12 @@ def max_additive_utilitarian_welfare_scheme(


def max_additive_utilitarian_welfare(
instance: Instance,
profile: AbstractProfile,
sat_class: type[SatisfactionMeasure] | None = None,
sat_profile: GroupSatisfactionMeasure | None = None,
resoluteness: bool = True,
initial_budget_allocation: Collection[Project] | None = None,
instance: Instance,
profile: AbstractProfile,
sat_class: type[SatisfactionMeasure] | None = None,
sat_profile: GroupSatisfactionMeasure | None = None,
resoluteness: bool = True,
initial_budget_allocation: Collection[Project] | None = None,
) -> BudgetAllocation | list[BudgetAllocation]:
"""
Rule returning the budget allocation(s) maximizing the utilitarian social welfare. The utilitarian social welfare is
Expand Down Expand Up @@ -152,30 +153,51 @@ 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_scheme2(
instance, sat_profile, budget_allocation, resoluteness=resoluteness
)

def max_additive_utilitarian_welfare_scheme2(
instance: Instance,
sat_profile: GroupSatisfactionMeasure,
initial_budget_allocation: Collection[Project],
resoluteness: bool = True,
) -> BudgetAllocation | list[BudgetAllocation]:
kvps = {p: sat_profile.total_satisfaction_project(p) for p in instance}
items = []
if resoluteness:
return max_additive_utilitarian_welfare_primal_dual_scheme(
instance, sat_profile, budget_allocation
)
else:
return max_additive_utilitarian_welfare_ilp_scheme(
instance, sat_profile, budget_allocation, resoluteness
)


def max_additive_utilitarian_welfare_primal_dual_scheme(
instance: Instance,
sat_profile: GroupSatisfactionMeasure,
initial_budget_allocation: Collection[Project],
) -> BudgetAllocation:
budget_allocation = BudgetAllocation(initial_budget_allocation)

items = []
for p in instance:
profit = kvps[p]
tmp = KnapsackItem(p.name, p.cost, profit)
items.append(tmp)
if p not in budget_allocation:
profit = sat_profile.total_satisfaction_project(p)
if p.cost == 0:
if profit > 0:
budget_allocation.append(p)
else:
items.append(KnapsackItem(p, p.cost, profit))

current_budget_limit = instance.budget_limit - total_cost(budget_allocation)
result = primal_dual_branch(items, current_budget_limit)
budget_allocation.extend(p.project for p in result)
return budget_allocation


class KnapsackItem:
def __init__(self, project, weight, profit):
self.project = project
self.weight = weight
self.profit = profit

result = primal_dual_branch(items, instance.budget_limit)
resultNames = [p.id for p in result]
res = [p for p in instance if p.name in resultNames]
return BudgetAllocation(res)
@property
def efficiency(self):
return self.profit / self.weight if self.weight != 0 else 0

def primal_dual_branch(items, capacity):

def primal_dual_branch(items: list[KnapsackItem], capacity: float):
items.sort(key=lambda x: x.efficiency, reverse=True)

tmp_capacity = capacity
Expand All @@ -195,7 +217,8 @@ def primal_dual_branch(items, capacity):
lower_bound = [0]
a_star = [-1]
b_star = [-1]
primal_dual_branch_impl(split_idx - 1, split_idx, split_profit, split_weight, items, capacity, solution, lower_bound, a_star, b_star)
primal_dual_branch_impl(split_idx - 1, split_idx, split_profit, split_weight, items, capacity,
solution, lower_bound, a_star, b_star)

result = []
for i in range(len(items)):
Expand All @@ -208,7 +231,9 @@ def primal_dual_branch(items, capacity):

return result

def primal_dual_branch_impl(a, b, profit_sum, weight_sum, items, capacity, x, lower_bound, a_star, b_star):

def primal_dual_branch_impl(a, b, profit_sum, weight_sum, items, capacity, x, lower_bound, a_star,
b_star):
improved = False

if weight_sum <= capacity:
Expand All @@ -227,11 +252,13 @@ def primal_dual_branch_impl(a, b, profit_sum, weight_sum, items, capacity, x, lo

pb = items[b].profit
wb = items[b].weight
if primal_dual_branch_impl(a, b + 1, profit_sum + pb, weight_sum + wb, items, capacity, x, lower_bound, a_star, b_star):
if primal_dual_branch_impl(a, b + 1, profit_sum + pb, weight_sum + wb, items, capacity, x,
lower_bound, a_star, b_star):
x[b] = 1
improved = True

if primal_dual_branch_impl(a, b + 1, profit_sum, weight_sum, items, capacity, x, lower_bound, a_star, b_star):
if primal_dual_branch_impl(a, b + 1, profit_sum, weight_sum, items, capacity, x,
lower_bound, a_star, b_star):
x[b] = 0
improved = True
else:
Expand All @@ -244,23 +271,14 @@ def primal_dual_branch_impl(a, b, profit_sum, weight_sum, items, capacity, x, lo

pa = items[a].profit
wa = items[a].weight
if primal_dual_branch_impl(a - 1, b, profit_sum - pa, weight_sum - wa, items, capacity, x, lower_bound, a_star, b_star):
if primal_dual_branch_impl(a - 1, b, profit_sum - pa, weight_sum - wa, items, capacity, x,
lower_bound, a_star, b_star):
x[a] = 0
improved = True

if primal_dual_branch_impl(a - 1, b, profit_sum, weight_sum, items, capacity, x, lower_bound, a_star, b_star):
if primal_dual_branch_impl(a - 1, b, profit_sum, weight_sum, items, capacity, x,
lower_bound, a_star, b_star):
x[a] = 1
improved = True

return improved


class KnapsackItem:
def __init__(self, id, weight, profit):
self.id = id
self.weight = weight
self.profit = profit

@property
def efficiency(self):
return self.profit / self.weight if self.weight != 0 else 0
8 changes: 4 additions & 4 deletions tests/test_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ def run_sat_rule(rule, verbose=False):
print(
f"Res outcome: {resolute_out} -- In irres: "
f"{sorted(resolute_out) in test_election.irr_results_sat[rule][sat_class]} "
f"({type(resolute_out)})"
f"(type is {type(resolute_out)})"
)
irresolute_out = rule(
test_election.instance,
Expand All @@ -356,7 +356,7 @@ def run_sat_rule(rule, verbose=False):
if verbose:
print(
f"Irres outcome: {irresolute_out} "
f"({tuple(type(out) for out in irresolute_out)})"
f"(types are {tuple(type(out) for out in irresolute_out)})"
)
print(
f"Irres expected: {test_election.irr_results_sat[rule][sat_class]}"
Expand All @@ -374,7 +374,7 @@ def run_sat_rule(rule, verbose=False):
print(
f"Res outcome with sat_profile: {resolute_out_sat_profile} -- Same: "
f"{resolute_out == resolute_out_sat_profile} "
f"({type(resolute_out_sat_profile)})"
f"(type is {type(resolute_out_sat_profile)})"
)

assert (
Expand Down Expand Up @@ -491,7 +491,7 @@ def test_greedy_multisat(self):
assert outcome1 == outcome2

def test_max_welfare(self):
run_sat_rule(max_additive_utilitarian_welfare, verbose=False)
run_sat_rule(max_additive_utilitarian_welfare, verbose=True)
with self.assertRaises(ValueError):
max_additive_utilitarian_welfare(Instance(), ApprovalProfile())

Expand Down
3 changes: 2 additions & 1 deletion tests/test_visualisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

class TestUtils(TestCase):
def test_mes_visualisation(self):
instance, profile = election.parse_pabulib("tests/PaBuLib/All_10/poland_czestochowa_2020_grabowka.pb")
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PaBuLib", "All_10", "poland_czestochowa_2020_grabowka.pb")
instance, profile = election.parse_pabulib(file_path)
outcome = method_of_equal_shares(instance, profile, sat_class=Cost_Sat, analytics=True)
vis = MESVisualiser(profile, instance, outcome.details)
with tempfile.TemporaryDirectory() as temp_dir:
Expand Down

0 comments on commit 7bfdc5a

Please sign in to comment.