From 7bfdc5a66be3dd1061889d1c571f77f132ed1133 Mon Sep 17 00:00:00 2001 From: Simon Rey Date: Fri, 22 Mar 2024 18:18:18 +0100 Subject: [PATCH] Attempt to fix priaml/dual for Max Welfare --- pabutools/rules/budgetallocation.py | 3 - pabutools/rules/maxwelfare.py | 128 ++++++++++++++++------------ tests/test_rule.py | 8 +- tests/test_visualisation.py | 3 +- 4 files changed, 79 insertions(+), 63 deletions(-) diff --git a/pabutools/rules/budgetallocation.py b/pabutools/rules/budgetallocation.py index d1e0980c..27101c44 100644 --- a/pabutools/rules/budgetallocation.py +++ b/pabutools/rules/budgetallocation.py @@ -4,9 +4,6 @@ from pabutools.election.instance import Project -from pabutools.utils import Numeric - - class AllocationDetails: """ Class representing participatory budgeting rule run details. diff --git a/pabutools/rules/maxwelfare.py b/pabutools/rules/maxwelfare.py index ae546a74..a250638a 100644 --- a/pabutools/rules/maxwelfare.py +++ b/pabutools/rules/maxwelfare.py @@ -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 @@ -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 @@ -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() @@ -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 @@ -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 @@ -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)): @@ -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: @@ -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: @@ -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 \ No newline at end of file diff --git a/tests/test_rule.py b/tests/test_rule.py index 5635789d..071f5a88 100644 --- a/tests/test_rule.py +++ b/tests/test_rule.py @@ -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, @@ -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]}" @@ -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 ( @@ -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()) diff --git a/tests/test_visualisation.py b/tests/test_visualisation.py index 0dd71cc5..113a77ed 100644 --- a/tests/test_visualisation.py +++ b/tests/test_visualisation.py @@ -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: