From fc0d600dee76a669136c14180ded1f2df9d86245 Mon Sep 17 00:00:00 2001 From: Kapiszon2343 Date: Wed, 12 Mar 2025 22:40:59 +0100 Subject: [PATCH 1/8] big cstv fixes & 2 more combinations --- pabutools/rules/cstv.py | 130 +++++++++++++++++++++++++++++++++------- tests/test_cstv.py | 52 ++++++++++++++-- 2 files changed, 155 insertions(+), 27 deletions(-) diff --git a/pabutools/rules/cstv.py b/pabutools/rules/cstv.py index 4e807543..805d2308 100644 --- a/pabutools/rules/cstv.py +++ b/pabutools/rules/cstv.py @@ -1,8 +1,12 @@ """ + + + An implementation of the algorithms in: "Participatory Budgeting with Cumulative Votes", by Piotr Skowron, Arkadii Slinko, Stanisaw Szufa, Nimrod Talmon (2020), https://arxiv.org/pdf/2009.02690 Programmer: Achiya Ben Natan +Changes: Kacper Harasimowicz Date: 2024/05/16. """ @@ -15,7 +19,8 @@ from enum import Enum from pabutools.election.instance import Instance, Project -from pabutools.election.profile.cumulativeprofile import AbstractCumulativeProfile +from pabutools.election.ballot import CumulativeBallot +from pabutools.election.profile.cumulativeprofile import ( CumulativeProfile, AbstractCumulativeProfile ) from pabutools.fractions import frac from pabutools.rules.budgetallocation import BudgetAllocation from pabutools.tiebreaking import TieBreakingRule, lexico_tie_breaking @@ -56,6 +61,17 @@ class CSTV_Combination(Enum): minimal transfer used if no eligible projects; and acceptance of under-supported projects as post-processing method. """ + EWTS = 5 + """ + Project selection via greedy-by-support; eligible projects selected via greedy-by-support; + elimination with transfer used if no eligible projects; and reverse elimination as post-processing method. + """ + + MTS = 6 + """ + Project selection via greedy-by-support; eligible projects selected via greedy-by-support; + minimal transfer used if no eligible projects; and acceptance of under-supported projects as post-processing method. + """ def cstv( instance: Instance, @@ -127,16 +143,26 @@ def cstv( eligible_projects_func = is_eligible_gsc no_eligible_project_func = elimination_with_transfers exhaustiveness_postprocess_func = reverse_eliminations - elif CSTV_Combination.MT: + elif combination == CSTV_Combination.MT: select_project_to_fund_func = select_project_ge eligible_projects_func = is_eligible_ge no_eligible_project_func = minimal_transfer exhaustiveness_postprocess_func = acceptance_of_under_supported_projects - elif CSTV_Combination.MTC: + elif combination == CSTV_Combination.MTC: select_project_to_fund_func = select_project_gsc eligible_projects_func = is_eligible_gsc no_eligible_project_func = minimal_transfer exhaustiveness_postprocess_func = acceptance_of_under_supported_projects + elif combination == CSTV_Combination.EWTS: + select_project_to_fund_func = select_project_gs + eligible_projects_func = is_eligible_gs + no_eligible_project_func = elimination_with_transfers + exhaustiveness_postprocess_func = reverse_eliminations + elif combination == CSTV_Combination.MTS: + select_project_to_fund_func = select_project_gs + eligible_projects_func = is_eligible_gs + no_eligible_project_func = minimal_transfer + exhaustiveness_postprocess_func = acceptance_of_under_supported_projects else: raise ValueError( f"Invalid combination {combination}. Please select an element of the " @@ -191,10 +217,10 @@ def cstv( ] current_projects = set(instance) + budget = sum(sum(donor.values()) for donor in donations) # Loop until a halting condition is met while True: # Calculate the total budget - budget = sum(sum(donor.values()) for donor in donations) if verbose: print(f"Budget is: {budget}") @@ -266,23 +292,23 @@ def cstv( # If the project has enough or excess support if excess_support >= 0: + # Add the project to the selected set and remove it from further consideration + selected_projects.append(p) + current_projects.remove(p) + if verbose: + print(f"Updated selected projects: {selected_projects}") + budget -= p.cost + if excess_support > 0.01: # Perform the excess redistribution procedure gama = frac(p.cost, excess_support + p.cost) - excess_redistribution_procedure(donations, p, gama) + excess_redistribution_procedure(current_projects, donations, p, gama) else: # Reset donations for the eliminated project if verbose: print(f"Resetting donations for eliminated project: {p}") for donor in donations: donor[p] = 0 - - # Add the project to the selected set and remove it from further consideration - selected_projects.append(p) - current_projects.remove(p) - if verbose: - print(f"Updated selected projects: {selected_projects}") - budget -= p.cost continue @@ -294,6 +320,7 @@ def cstv( def excess_redistribution_procedure( + current_projects: set[Project], donors: list[dict[Project, Numeric]], selected_project: Project, gama: Numeric, @@ -320,13 +347,39 @@ def excess_redistribution_procedure( donor[selected_project] = to_distribute donor_copy[selected_project] = 0 total = sum(donor_copy.values()) - for key, donation in donor_copy.items(): - if donation != selected_project: - if total != 0: + if total != 0: + for key, donation in donor_copy.items(): + if donation != selected_project: part = frac(donation, total) donor[key] = donation + to_distribute * part - donor[selected_project] = 0 + donor[selected_project] = 0 + + + + +def is_eligible_gs( + projects: Iterable[Project], donors: list[dict[Project, Numeric]] +) -> list[Project]: + """ + Determines the eligible projects based on the Greedy-by-Support (GS) rule. + + Parameters + ---------- + projects : Iterable[Project] + The list of projects. + donors : list[dict[Project, Numeric]] + The list of donor ballots. + Returns + ------- + list[Project] + The list of eligible projects. + """ + return [ + project + for project in projects + if (sum(donor.get(project, 0) for donor in donors)) >= project.cost + ] def is_eligible_ge( projects: Iterable[Project], donors: list[dict[Project, Numeric]] @@ -377,6 +430,36 @@ def is_eligible_gsc( if frac(sum(donor.get(project, 0) for donor in donors), project.cost) >= 1 ] +def select_project_gs( + projects: Iterable[Project], + donors: list[dict[Project, Numeric]], +) -> list[Project]: + """ + Selects the project with the maximum support using the Greedy-by-Support (GS) rule. + + Parameters + ---------- + projects : Iterable[Project] + The list of projects. + donors : list[dict[Project, Numeric]] + The list of donor ballots. + + Returns + ------- + list[Project] + The tied selected projects. + """ + support = { + project: sum(donor.get(project, 0) for donor in donors) + for project in projects + } + max_support_value = max(support.values()) + max_support_projects = [ + project + for project, supp in support.items() + if supp == max_support_value + ] + return max_support_projects def select_project_ge( projects: Iterable[Project], @@ -547,6 +630,8 @@ def minimal_transfer( sum_of_don += sum(d) if sum_of_don >= project.cost: projects_with_chance.append(project) + else: + eliminated_projects.add(project) if not projects_with_chance: return False chosen_project = project_to_fund_selection_procedure(projects_with_chance, donors)[ @@ -667,14 +752,15 @@ def acceptance_of_under_supported_projects( None """ while len(eliminated_projects) != 0: - selected_project = project_to_fund_selection_procedure( - eliminated_projects, donors, tie_breaking, True - )[ - 0 - ] # TODO: tie-breaking here + selected_project = tie_breaking.untie( + eliminated_projects, + CumulativeProfile([CumulativeBallot(donor) for donor in donors]), + project_to_fund_selection_procedure( + eliminated_projects, donors + )) if selected_project.cost <= budget: selected_projects.append(selected_project) eliminated_projects.remove(selected_project) budget -= selected_project.cost else: - eliminated_projects.remove(selected_project) + eliminated_projects.remove(selected_project) \ No newline at end of file diff --git a/tests/test_cstv.py b/tests/test_cstv.py index ddd846e2..c4f11ebc 100644 --- a/tests/test_cstv.py +++ b/tests/test_cstv.py @@ -18,6 +18,7 @@ class TestFunctions(unittest.TestCase): def setUp(self): + self.do_verbose = [False] self.p1 = Project("A", 27) self.p2 = Project("B", 30) self.p3 = Project("C", 40) @@ -38,7 +39,7 @@ def test_cstv_budgeting_with_zero_budget(self): for key in donor.keys(): donor[key] = 0 for combination in CSTV_Combination: - for verbose in [True, False]: + for verbose in self.do_verbose: with self.subTest(combination=combination): selected_projects = cstv(self.instance, self.donors, combination, verbose=verbose) self.assertEqual( @@ -52,7 +53,7 @@ def test_cstv_budgeting_with_budget_less_than_min_project_cost(self): donor[self.p2] = 1 donor[self.p3] = 1 for combination in CSTV_Combination: - for verbose in [True, False]: + for verbose in self.do_verbose: with self.subTest(combination=combination): selected_projects = cstv(self.instance, self.donors, combination, verbose=verbose) self.assertEqual( @@ -66,7 +67,7 @@ def test_cstv_budgeting_with_budget_greater_than_max_total_needed_support(self): for key in donor.keys(): donor[key] = 100 for combination in CSTV_Combination: - for verbose in [True, False]: + for verbose in self.do_verbose: with self.subTest(combination=combination): selected_projects = cstv(self.instance, donors, combination, verbose=verbose) self.assertEqual( @@ -75,8 +76,12 @@ def test_cstv_budgeting_with_budget_greater_than_max_total_needed_support(self): def test_cstv_budgeting_with_budget_between_min_and_max(self): # Ensure the number of selected projects is 2 when total budget is between the minimum and maximum costs + for donor in self.donors: + donor[self.p1] = 5 + donor[self.p2] = 5 + donor[self.p3] = 5 for combination in CSTV_Combination: - for verbose in [True, False]: + for verbose in self.do_verbose: with self.subTest(combination=combination): selected_projects = cstv(self.instance, self.donors, combination, verbose=verbose) self.assertEqual( @@ -90,13 +95,50 @@ def test_cstv_budgeting_with_budget_exactly_matching_required_support(self): donor[self.p1] = frac(self.p1.cost, len(self.donors)) donor[self.p2] = frac(self.p2.cost, len(self.donors)) donor[self.p3] = frac(self.p3.cost, len(self.donors)) - for verbose in [True, False]: + for verbose in self.do_verbose: with self.subTest(combination=combination): selected_projects = cstv(self.instance, self.donors, combination, verbose=verbose) self.assertEqual( len(selected_projects), 3 ) + def test_cstv_budgeting_with_single_project_consuming_most_support(self): + # Ensure all projects are selected when the total budget matches the required support exactly + for combination in CSTV_Combination: + self.donors = CumulativeProfile( + [ + CumulativeBallot({self.p1: 20, self.p2: 0, self.p3: 0}), + CumulativeBallot({self.p1: 20, self.p2: 0, self.p3: 0}), + CumulativeBallot({self.p1: 0, self.p2: 20, self.p3: 0}), + ] + ) + for verbose in self.do_verbose: + with self.subTest(combination=combination): + selected_projects = cstv(self.instance, self.donors, combination, verbose=verbose) + self.assertEqual( + len(selected_projects), 2 + ) + + def test_cstv_budgeting_maximality_is_achieved_with_low_support(self): + for combination in CSTV_Combination: + self.p1 = Project("A", 10) + self.p2 = Project("B", 10) + self.p3 = Project("C", 10) + self.p4 = Project("D", 10) + self.instance = Instance([self.p1, self.p2, self.p3, self.p4]) + self.donors = CumulativeProfile( + [ + CumulativeBallot({self.p1: 10, self.p2: 5, self.p3: 0, self.p4: 0}), + CumulativeBallot({self.p1: 0, self.p2: 0, self.p3: 5, self.p4: 10}), + ] + ) + for verbose in self.do_verbose: + with self.subTest(combination=combination): + selected_projects = cstv(self.instance, self.donors, combination, verbose=verbose) + self.assertEqual( + len(selected_projects), 3 + ) + def test_cstv_budgeting_large_input(self): # Ensure the number of selected projects does not exceed the total number of projects for combination in CSTV_Combination: From 25c39c065b12df16350fa743f7d75bdcdcdec815 Mon Sep 17 00:00:00 2001 From: Kapiszon2343 Date: Fri, 11 Jul 2025 14:23:43 +0200 Subject: [PATCH 2/8] cstv optimization --- pabutools/rules/cstv.py | 343 ++++++++++++++++++++++++---------------- 1 file changed, 211 insertions(+), 132 deletions(-) diff --git a/pabutools/rules/cstv.py b/pabutools/rules/cstv.py index 805d2308..a93f8f18 100644 --- a/pabutools/rules/cstv.py +++ b/pabutools/rules/cstv.py @@ -128,6 +128,7 @@ def cstv( BudgetAllocation The list of selected projects. """ + epsilon = 1e-10 if tie_breaking is None: tie_breaking = lexico_tie_breaking @@ -195,20 +196,23 @@ def cstv( 'The "resoluteness = False" feature is not yet implemented' ) - # Check if all donors donate the same amount - if not len(set([sum(donor.values()) for donor in profile])) == 1: - raise ValueError( - "Not all donors donate the same amount. Change the donations and try again." - ) - if initial_budget_allocation is None: initial_budget_allocation = BudgetAllocation() else: initial_budget_allocation = BudgetAllocation(initial_budget_allocation) + # Check if all donors donate the same amount + donor_sums = set([sum(donor.values()) for donor in profile]) + if (max(donor_sums) == 0): + return initial_budget_allocation + if frac((max(donor_sums) - min(donor_sums)), max(donor_sums)) > epsilon: + raise ValueError( + "Not all donors donate the same amount. Change the donations and try again." + ) + # Initialize the set of selected projects and eliminated projects selected_projects = initial_budget_allocation - eliminated_projects = set() + eliminated_projects = [] # The donations to avoid to mutate the profile passed as argument donations = [ @@ -217,7 +221,10 @@ def cstv( ] current_projects = set(instance) - budget = sum(sum(donor.values()) for donor in donations) + if instance.budget_limit > 0: + budget = instance.budget_limit + else: + budget = sum(sum(donor.values()) for donor in donations) # Loop until a halting condition is met while True: # Calculate the total budget @@ -290,26 +297,24 @@ def cstv( if verbose: print(f"Excess support for {p}: {excess_support}") - # If the project has enough or excess support - if excess_support >= 0: - # Add the project to the selected set and remove it from further consideration - selected_projects.append(p) - current_projects.remove(p) + # Add the project to the selected set and remove it from further consideration + selected_projects.append(p) + current_projects.remove(p) + if verbose: + print(f"Updated selected projects: {selected_projects}") + budget -= p.cost + + if excess_support > 0.01: + # Perform the excess redistribution procedure + gama = frac(p.cost, excess_support + p.cost) + excess_redistribution_procedure(current_projects, donations, p, gama) + else: + # Reset donations for the eliminated project if verbose: - print(f"Updated selected projects: {selected_projects}") - budget -= p.cost - - if excess_support > 0.01: - # Perform the excess redistribution procedure - gama = frac(p.cost, excess_support + p.cost) - excess_redistribution_procedure(current_projects, donations, p, gama) - else: - # Reset donations for the eliminated project - if verbose: - print(f"Resetting donations for eliminated project: {p}") - for donor in donations: - donor[p] = 0 - continue + print(f"Resetting donations for eliminated project: {p}") + for donor in donations: + donor[p] = 0 + continue ################################################################### @@ -323,7 +328,7 @@ def excess_redistribution_procedure( current_projects: set[Project], donors: list[dict[Project, Numeric]], selected_project: Project, - gama: Numeric, + gama: Numeric ) -> None: """ Distributes the excess support of a selected project to the remaining projects. @@ -341,27 +346,32 @@ def excess_redistribution_procedure( ------- None """ + project_support = sum(donor.get(selected_project.name, 0) for donor in donors) + cost = selected_project.cost for donor in donors: - donor_copy = donor.copy() - to_distribute = donor_copy[selected_project] * (1 - gama) - donor[selected_project] = to_distribute - donor_copy[selected_project] = 0 - total = sum(donor_copy.values()) - if total != 0: - for key, donation in donor_copy.items(): - if donation != selected_project: + contribution = donor[selected_project] + total = sum(donor.values()) - contribution + if total == 0: + project_support -= contribution + cost -= contribution + if cost > 0: + gama = frac(cost, project_support) + for donor in donors: + contribution = donor[selected_project] + donor.pop(selected_project) + to_distribute = contribution * (1 - gama) + total = sum(donor.values()) + if total != 0: + for key, donation in donor.items(): part = frac(donation, total) donor[key] = donation + to_distribute * part - donor[selected_project] = 0 - - -def is_eligible_gs( +def is_eligible_greedy( projects: Iterable[Project], donors: list[dict[Project, Numeric]] ) -> list[Project]: """ - Determines the eligible projects based on the Greedy-by-Support (GS) rule. + Determines the eligible projects based on the Greedy rules Parameters ---------- @@ -375,12 +385,37 @@ def is_eligible_gs( list[Project] The list of eligible projects. """ + epsilon = 1e-5 + support = { + project: sum([donor.get(project, 0) for donor in donors]) + for project in projects + } return [ project for project in projects - if (sum(donor.get(project, 0) for donor in donors)) >= project.cost + if support.get(project, 0) * (1+epsilon) >= project.cost ] +def is_eligible_gs( + projects: Iterable[Project], donors: list[dict[Project, Numeric]] +) -> list[Project]: + """ + Determines the eligible projects based on the Greedy-by-Support (GS) rule. + + Parameters + ---------- + projects : Iterable[Project] + The list of projects. + donors : list[dict[Project, Numeric]] + The list of donor ballots. + + Returns + ------- + list[Project] + The list of eligible projects. + """ + return is_eligible_greedy(projects, donors) + def is_eligible_ge( projects: Iterable[Project], donors: list[dict[Project, Numeric]] ) -> list[Project]: @@ -399,11 +434,7 @@ def is_eligible_ge( list[Project] The list of eligible projects. """ - return [ - project - for project in projects - if (sum(donor.get(project, 0) for donor in donors) - project.cost) >= 0 - ] + return is_eligible_greedy(projects, donors) def is_eligible_gsc( @@ -424,15 +455,12 @@ def is_eligible_gsc( list[Project] The list of eligible projects. """ - return [ - project - for project in projects - if frac(sum(donor.get(project, 0) for donor in donors), project.cost) >= 1 - ] + return is_eligible_greedy(projects, donors) def select_project_gs( projects: Iterable[Project], donors: list[dict[Project, Numeric]], + find_best: bool = True ) -> list[Project]: """ Selects the project with the maximum support using the Greedy-by-Support (GS) rule. @@ -443,27 +471,34 @@ def select_project_gs( The list of projects. donors : list[dict[Project, Numeric]] The list of donor ballots. - + find_best: bool, optional + Set to `True` to select best project, or `False` for worst project + defaults to `True` + Returns ------- list[Project] The tied selected projects. """ support = { - project: sum(donor.get(project, 0) for donor in donors) + project: sum([donor.get(project, 0) for donor in donors]) for project in projects } - max_support_value = max(support.values()) - max_support_projects = [ + if find_best: + target_support_value = max(support.values()) + else: + target_support_value = min(support.values()) + target_support_projects = [ project for project, supp in support.items() - if supp == max_support_value + if supp == target_support_value ] - return max_support_projects + return target_support_projects def select_project_ge( projects: Iterable[Project], donors: list[dict[Project, Numeric]], + find_best: bool = True ) -> list[Project]: """ Selects the project with the maximum excess support using the General Election (GE) rule. @@ -474,28 +509,35 @@ def select_project_ge( The list of projects. donors : list[dict[Project, Numeric]] The list of donor ballots. - + find_best: bool, optional + Set to `True` to select best project, or `False` for worst project + defaults to `True` + Returns ------- list[Project] The tied selected projects. """ excess_support = { - project: sum(donor.get(project, 0) for donor in donors) - project.cost + project: sum([donor.get(project, 0) for donor in donors]) - project.cost for project in projects } - max_excess_value = max(excess_support.values()) - max_excess_projects = [ + if find_best: + target_excess_value = max(excess_support.values()) + else: + target_excess_value = min(excess_support.values()) + target_excess_projects = [ project for project, excess in excess_support.items() - if excess == max_excess_value + if excess == target_excess_value ] - return max_excess_projects + return target_excess_projects def select_project_gsc( projects: Iterable[Project], donors: list[dict[Project, Numeric]], + find_best: bool = True ) -> list[Project]: """ Selects the project with the maximum excess support using the General Election (GSC) rule. @@ -506,29 +548,35 @@ def select_project_gsc( The list of projects. donors : list[dict[Project, Numeric]] The list of donor ballots. + find_best: bool, optional + Set to `True` to select best project, or `False` for worst project + defaults to `True` Returns ------- list[Project] The tied selected projects. """ - excess_support = { - project: frac(sum(donor.get(project, 0) for donor in donors), project.cost) + support_over_cost = { + project: frac(sum([donor.get(project, 0) for donor in donors]), project.cost) for project in projects } - max_excess_value = max(excess_support.values()) - max_excess_projects = [ + if find_best: + target_SOC_value = max(support_over_cost.values()) + else: + target_SOC_value = min(support_over_cost.values()) + target_SOC_projects = [ project - for project, excess in excess_support.items() - if excess == max_excess_value + for project, SOC in support_over_cost.items() + if SOC == target_SOC_value ] - return max_excess_projects + return target_SOC_projects def elimination_with_transfers( - projects: list[Project], + projects: set[Project], donors: list[dict[Project, Numeric]], - eliminated_projects: set[Project], + eliminated_projects: list[Project], project_to_fund_selection_procedure: Callable, tie_breaking: TieBreakingRule, ) -> bool: @@ -564,32 +612,39 @@ def distribute_project_support( """ for donor in all_donors: to_distribute = donor[eliminated_project] - total = sum(donor.values()) - to_distribute + donor.pop(eliminated_project) + total = sum(donor.values()) if total == 0: continue for key, donation in donor.items(): - if key != eliminated_project: - part = frac(donation, total) - donor[key] = donation + to_distribute * part - donor[eliminated_project] = 0 + part = frac(donation, total) + donor[key] = donation + to_distribute * part if len(projects) < 2: if len(projects) == 1: - eliminated_projects.add(projects.pop()) + eliminated_projects.append(projects.pop()) return False - min_project = min( - projects, key=lambda p: sum(donor.get(p.name, 0) for donor in donors) - p.cost + min_projects = project_to_fund_selection_procedure( + projects, donors, False ) + if len(min_projects) > 1: + min_project = tie_breaking.untie( + projects, + CumulativeProfile([CumulativeBallot(donor) for donor in donors]), + min_projects + ) + else: + min_project = min_projects[0] distribute_project_support(donors, min_project) projects.remove(min_project) - eliminated_projects.add(min_project) + eliminated_projects.append(min_project) return True def minimal_transfer( - projects: Iterable[Project], + projects: set[Project], donors: list[dict[Project, Numeric]], - eliminated_projects: set[Project], + eliminated_projects: list[Project], project_to_fund_selection_procedure: Callable, tie_breaking: TieBreakingRule = lexico_tie_breaking, ) -> bool: @@ -618,8 +673,7 @@ def minimal_transfer( """ if pabutools.fractions.FRACTION != pabutools.fractions.FLOAT_FRAC: warnings.warn("You are using minimal transfers with exact fractions, this may never end...") - projects_with_chance = [] - for project in projects: + for project in projects.copy(): donors_of_selected_project = [ donor.values() for _, donor in enumerate(donors) @@ -628,15 +682,20 @@ def minimal_transfer( sum_of_don = 0 for d in donors_of_selected_project: sum_of_don += sum(d) - if sum_of_don >= project.cost: - projects_with_chance.append(project) - else: - eliminated_projects.add(project) - if not projects_with_chance: + if sum_of_don < project.cost: + eliminated_projects.append(project) + projects.remove(project) + if not projects: return False - chosen_project = project_to_fund_selection_procedure(projects_with_chance, donors)[ - 0 - ] # TODO: there should be a tie-breaking here + tied_projects = project_to_fund_selection_procedure(projects, donors) + if len(tied_projects) > 1: + chosen_project = tie_breaking.untie( + projects, + CumulativeProfile([CumulativeBallot(donor) for donor in donors]), + tied_projects + ) + else: + chosen_project = tied_projects[0] donors_of_selected_project = [ i for i, donor in enumerate(donors) if donor.get(chosen_project.name, 0) > 0 ] @@ -644,51 +703,73 @@ def minimal_transfer( project_cost = chosen_project.cost # Calculate initial support ratio - total_support = sum(donor.get(chosen_project, 0) for donor in donors) + total_support = sum(donors[i].get(chosen_project, 0) for i in donors_of_selected_project) r = frac(total_support, project_cost) - # Loop until the required support is achieved + # Loop until all donors can afford ratio num_loop_run = 0 - while r < 1: - num_loop_run += 1 - # Check if all donors have their entire donation on the chosen project - all_on_chosen_project = all( - sum(donors[i].values()) == donors[i].get(chosen_project, 0) - for i in donors_of_selected_project - ) - if all_on_chosen_project: - for project in projects: - eliminated_projects.add(project) - return False - - for i in donors_of_selected_project: + do_continue = True + while do_continue: + do_continue = False + + for i in list(donors_of_selected_project): donor = donors[i] donation = donor.get(chosen_project, 0) - total = sum(donor.values()) - donation - if total > 0: - to_distribute = min(total, frac(donation, r) - donation) + total = sum(donor.values()) + if frac(donation, r) > total: + do_continue = True for proj_name, proj_donation in donor.items(): - if proj_name != chosen_project and proj_donation > 0: - change = frac(to_distribute * proj_donation, total) - if 1 - change < 1e-14: - change = 1 - donor[proj_name] -= change - donor[chosen_project] += frac(math.ceil(change * 100000000000000), 100000000000000) - - # Recalculate the support ratio - total_support = sum(donor.get(chosen_project, 0) for donor in donors) - r = frac(total_support, project_cost) - - if num_loop_run > 10000: - raise RuntimeError("The while loop of the minimal_transfer function ran for too long. This can be due to" - " issues with floating point arithmetic.") + donor[proj_name] = 0 + donor[chosen_project] = total + donors_of_selected_project.remove(i) + total_support -= total + project_cost -= total + r = frac(total_support, project_cost) + + # Loop until the required support is achieved + if donors_of_selected_project: + num_loop_run = 0 + while r < 1: + num_loop_run += 1 + # Check if all donors have their entire donation on the chosen project + all_on_chosen_project = all( + sum(donors[i].values()) == donors[i].get(chosen_project, 0) + for i in donors_of_selected_project + ) + if all_on_chosen_project: + for project in projects: + eliminated_projects.append(project) + return False + + for i in donors_of_selected_project: + donor = donors[i] + donation = donor.get(chosen_project, 0) + total = sum(donor.values()) - donation + if total > 0: + to_distribute = min(total, frac(donation, r) - donation) + for proj_name, proj_donation in donor.items(): + if proj_name != chosen_project and proj_donation > 0: + change = frac(to_distribute * proj_donation, total) + if to_distribute - change < 1e-14: + change = to_distribute + donor[proj_name] -= change + donor[chosen_project] += frac(math.ceil(change * 100000000000000), 100000000000000) + + # Recalculate the support ratio + total_support = sum(donors[i].get(chosen_project, 0) for i in donors_of_selected_project) + r = frac(total_support, project_cost) + + if num_loop_run > 10000: + #raise RuntimeError("The while loop of the minimal_transfer function ran for too long. This can be due to" + # " issues with floating point arithmetic.") + break return True def reverse_eliminations( selected_projects: BudgetAllocation, donors: list[dict[Project, Numeric]], - eliminated_projects: set[Project], + eliminated_projects: list[Project], project_to_fund_selection_procedure: Callable, budget: Numeric, tie_breaking: TieBreakingRule = lexico_tie_breaking, @@ -715,7 +796,7 @@ def reverse_eliminations( ------- None """ - for project in eliminated_projects: + for project in reversed(eliminated_projects): if project.cost <= budget: selected_projects.append(project) budget -= project.cost @@ -751,7 +832,7 @@ def acceptance_of_under_supported_projects( ------- None """ - while len(eliminated_projects) != 0: + while len(eliminated_projects) > 0: selected_project = tie_breaking.untie( eliminated_projects, CumulativeProfile([CumulativeBallot(donor) for donor in donors]), @@ -760,7 +841,5 @@ def acceptance_of_under_supported_projects( )) if selected_project.cost <= budget: selected_projects.append(selected_project) - eliminated_projects.remove(selected_project) budget -= selected_project.cost - else: - eliminated_projects.remove(selected_project) \ No newline at end of file + eliminated_projects.remove(selected_project) \ No newline at end of file From 714a615d39e8c9b4fb735509e329cdd29e949ba6 Mon Sep 17 00:00:00 2001 From: Kapiszon2343 Date: Sat, 12 Jul 2025 17:10:22 +0200 Subject: [PATCH 3/8] acceptance_of_under_supported_projects consumes support --- pabutools/rules/cstv.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pabutools/rules/cstv.py b/pabutools/rules/cstv.py index a93f8f18..6a5674a0 100644 --- a/pabutools/rules/cstv.py +++ b/pabutools/rules/cstv.py @@ -842,4 +842,8 @@ def acceptance_of_under_supported_projects( if selected_project.cost <= budget: selected_projects.append(selected_project) budget -= selected_project.cost + for donor in donors: + if donor.get(selected_project, 0) > 0: + for project in donor.keys(): + donor[project] = 0 eliminated_projects.remove(selected_project) \ No newline at end of file From 5608ad599ef36f9a2cd455bae9835999412f6b11 Mon Sep 17 00:00:00 2001 From: Kapiszon2343 Date: Sat, 12 Jul 2025 20:32:11 +0200 Subject: [PATCH 4/8] cstv exact result tests --- tests/test_cstv.py | 507 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 507 insertions(+) diff --git a/tests/test_cstv.py b/tests/test_cstv.py index c4f11ebc..dbe7ad9c 100644 --- a/tests/test_cstv.py +++ b/tests/test_cstv.py @@ -203,3 +203,510 @@ def generate_donations(total_donation, m): self.assertGreaterEqual(len(selected_projects), positive_excess) # Ensure the total initial support from donors is at least the total cost of the selected projects self.assertGreaterEqual(support, total_cost) + + def test_cstv_party_split(self): + for combination in CSTV_Combination: + projects = [ + Project('A1', 200), + Project('A2', 120), + Project('A3', 80), + Project('B1', 150), + Project('B2', 100), + Project('B3', 70), + Project('C1', 160), + Project('C2', 90), + Project('C3', 80), + ] + instance = Instance(projects) + donors = CumulativeProfile( + [ + CumulativeBallot({projects[i]: 0 for i in range(len(projects))}) + for _ in range(8) + ] + ) + donors[0][projects[0]] = 50 + donors[0][projects[1]] = 30 + donors[0][projects[2]] = 20 + donors[1][projects[0]] = 30 + donors[1][projects[1]] = 40 + donors[1][projects[2]] = 30 + donors[2][projects[0]] = 50 + donors[2][projects[1]] = 45 + donors[2][projects[2]] = 5 + + donors[3][projects[3]] = 50 + donors[3][projects[4]] = 40 + donors[3][projects[5]] = 10 + donors[4][projects[3]] = 30 + donors[4][projects[4]] = 40 + donors[4][projects[5]] = 30 + donors[5][projects[3]] = 50 + donors[5][projects[4]] = 25 + donors[5][projects[5]] = 25 + + donors[6][projects[6]] = 70 + donors[6][projects[7]] = 20 + donors[6][projects[8]] = 10 + donors[7][projects[6]] = 50 + donors[7][projects[7]] = 40 + donors[7][projects[8]] = 10 + + with self.subTest(combination=combination): + selected_projects = cstv(instance, donors, combination) + match combination: + case CSTV_Combination.EWT | CSTV_Combination.EWTC: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[1], + projects[2], + projects[7], + projects[5], + projects[3], + projects[6] + }) + case CSTV_Combination.EWTS: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[1], + projects[6], + projects[3], + projects[5], + projects[0] + }) + case CSTV_Combination.MT: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[5], + projects[1], + projects[2], + projects[7], + projects[8], + projects[3] + }) + case CSTV_Combination.MTC: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[5], + projects[1], + projects[2], + projects[6], + projects[3], + projects[7] + }) + case CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[0], + projects[3], + projects[6], + projects[2], + projects[5] + }) + + def test_cstv_laminal1(self): + for combination in CSTV_Combination: + projects = [ + Project('L1', 200), + Project('L2', 200), + Project('A1', 150), + Project('A2', 120), + Project('A3', 100), + Project('A4', 90), + Project('A5', 80), + Project('B1', 110), + Project('B2', 100), + Project('B3', 70), + Project('B4', 60), + Project('C1', 130), + Project('C2', 110), + Project('C3', 90), + Project('C4', 70), + ] + instance = Instance(projects) + donors = CumulativeProfile( + [ + CumulativeBallot({projects[i]: 0 for i in range(len(projects))}) + for _ in range(9) + ] + ) + donors[0][projects[0]] = 20 + donors[0][projects[1]] = 10 + donors[0][projects[2]] = 30 + donors[0][projects[3]] = 20 + donors[0][projects[4]] = 10 + donors[0][projects[5]] = 5 + donors[0][projects[6]] = 5 + donors[1][projects[0]] = 40 + donors[1][projects[1]] = 10 + donors[1][projects[2]] = 10 + donors[1][projects[3]] = 10 + donors[1][projects[4]] = 10 + donors[1][projects[5]] = 10 + donors[1][projects[6]] = 10 + donors[2][projects[0]] = 25 + donors[2][projects[1]] = 5 + donors[2][projects[2]] = 15 + donors[2][projects[3]] = 5 + donors[2][projects[4]] = 25 + donors[2][projects[5]] = 15 + donors[2][projects[6]] = 10 + donors[3][projects[0]] = 5 + donors[3][projects[1]] = 25 + donors[3][projects[2]] = 20 + donors[3][projects[3]] = 5 + donors[3][projects[4]] = 5 + donors[3][projects[5]] = 5 + donors[3][projects[6]] = 35 + + donors[4][projects[0]] = 35 + donors[4][projects[1]] = 5 + donors[4][projects[7]] = 25 + donors[4][projects[8]] = 25 + donors[4][projects[9]] = 5 + donors[4][projects[10]] = 5 + donors[5][projects[0]] = 5 + donors[5][projects[1]] = 15 + donors[5][projects[7]] = 35 + donors[5][projects[8]] = 15 + donors[5][projects[9]] = 25 + donors[5][projects[10]] = 5 + donors[6][projects[0]] = 15 + donors[6][projects[1]] = 15 + donors[6][projects[7]] = 25 + donors[6][projects[8]] = 15 + donors[6][projects[9]] = 15 + donors[6][projects[10]] = 15 + + donors[7][projects[0]] = 25 + donors[7][projects[1]] = 5 + donors[7][projects[11]] = 25 + donors[7][projects[12]] = 15 + donors[7][projects[13]] = 25 + donors[7][projects[14]] = 5 + donors[8][projects[0]] = 15 + donors[8][projects[1]] = 15 + donors[8][projects[11]] = 35 + donors[8][projects[12]] = 15 + donors[8][projects[13]] = 5 + donors[8][projects[14]] = 15 + + + with self.subTest(combination=combination): + selected_projects = cstv(instance, donors, combination) + match combination: + case CSTV_Combination.EWT: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[6], + projects[7], + projects[9], + projects[10], + projects[11], + projects[4], + projects[2], + }) + case CSTV_Combination.EWTC: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[7], + projects[9], + projects[6], + projects[4], + projects[1], + projects[11], + }) + case CSTV_Combination.EWTS: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[7], + projects[6], + projects[1], + projects[2], + projects[11], + }) + case CSTV_Combination.MT: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[6], + projects[9], + projects[7], + projects[10], + projects[14], + projects[4], + projects[5], + projects[13] + }) + case CSTV_Combination.MTC: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[7], + projects[6], + projects[9], + projects[4], + projects[11], + projects[1], + }) + case CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[1], + projects[7], + projects[2], + projects[13], + projects[10], + projects[6], + }) + + def test_cstv_laminal2(self): + for combination in CSTV_Combination: + projects = [ + Project('L1', 200), + Project('L2', 200), + Project('ABC', 170), + Project('AB1', 150), + Project('AB2', 130), + Project('AB3', 110), + Project('A1', 80), + Project('A2', 60), + Project('B', 70), + Project('C1', 90), + Project('C2', 80), + Project('C3', 60), + Project('D1', 110), + Project('D2', 90), + Project('D3', 90), + ] + instance = Instance(projects) + donors = CumulativeProfile( + [ + CumulativeBallot({projects[i]: 0 for i in range(len(projects))}) + for _ in range(9) + ] + ) + donors[0][projects[0]] = 20 + donors[0][projects[1]] = 10 + donors[0][projects[2]] = 15 + donors[0][projects[3]] = 15 + donors[0][projects[4]] = 15 + donors[0][projects[5]] = 10 + donors[0][projects[6]] = 5 + donors[0][projects[7]] = 10 + donors[1][projects[0]] = 40 + donors[1][projects[1]] = 10 + donors[1][projects[2]] = 20 + donors[1][projects[3]] = 5 + donors[1][projects[4]] = 5 + donors[1][projects[5]] = 5 + donors[1][projects[6]] = 10 + donors[1][projects[7]] = 5 + donors[2][projects[0]] = 25 + donors[2][projects[1]] = 5 + donors[2][projects[2]] = 15 + donors[2][projects[3]] = 10 + donors[2][projects[4]] = 20 + donors[2][projects[5]] = 5 + donors[2][projects[8]] = 20 + donors[3][projects[0]] = 5 + donors[3][projects[1]] = 25 + donors[3][projects[2]] = 15 + donors[3][projects[3]] = 15 + donors[3][projects[4]] = 5 + donors[3][projects[5]] = 10 + donors[3][projects[8]] = 25 + + donors[4][projects[0]] = 35 + donors[4][projects[1]] = 5 + donors[4][projects[2]] = 15 + donors[4][projects[9]] = 15 + donors[4][projects[10]] = 20 + donors[4][projects[11]] = 10 + donors[5][projects[0]] = 5 + donors[5][projects[1]] = 15 + donors[5][projects[2]] = 25 + donors[5][projects[9]] = 25 + donors[5][projects[10]] = 15 + donors[5][projects[11]] = 15 + + donors[6][projects[0]] = 15 + donors[6][projects[1]] = 15 + donors[6][projects[12]] = 35 + donors[6][projects[13]] = 25 + donors[6][projects[14]] = 10 + donors[7][projects[0]] = 25 + donors[7][projects[1]] = 5 + donors[7][projects[12]] = 25 + donors[7][projects[13]] = 20 + donors[7][projects[14]] = 25 + donors[8][projects[0]] = 15 + donors[8][projects[1]] = 15 + donors[8][projects[12]] = 50 + donors[8][projects[13]] = 15 + donors[8][projects[14]] = 5 + + + with self.subTest(combination=combination): + selected_projects = cstv(instance, donors, combination) + match combination: + case CSTV_Combination.EWT: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[8], + projects[2], + projects[7], + projects[13], + projects[11], + projects[10], + }) + case CSTV_Combination.EWTC: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[8], + projects[2], + projects[1], + projects[13], + projects[11], + }) + case CSTV_Combination.EWTS: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[2], + projects[1], + projects[13], + projects[4], + }) + case CSTV_Combination.MT: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[8], + projects[13], + projects[11], + projects[7], + projects[10], + projects[5], + projects[14], + }) + case CSTV_Combination.MTC: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[13], + projects[8], + projects[2], + projects[1], + projects[11], + }) + case CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[1], + projects[2], + projects[13], + projects[4], + }) + + def test_cstv_EWT_v_MT(self): + for combination in CSTV_Combination: + projects = [ + Project('A', 20), + Project('B', 26), + Project('C', 30), + Project('D', 30), + ] + instance = Instance(projects) + donors = CumulativeProfile( + [ + CumulativeBallot({projects[0]: 15, projects[1]: 7, projects[2]: 0, projects[3]: 0}), + CumulativeBallot({projects[0]: 1, projects[1]: 7, projects[2]: 10, projects[3]: 4}), + ] + ) + with self.subTest(combination=combination): + selected_projects = cstv(instance, donors, combination) + match combination: + case CSTV_Combination.EWT | CSTV_Combination.EWTC | CSTV_Combination.EWTS: + self.assertSetEqual(set(selected_projects), { + projects[1], + }) + case CSTV_Combination.MT | CSTV_Combination.MTC | CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[0], + }) + + def test_cstv_Greedy_difference(self): + for combination in CSTV_Combination: + projects = [ + Project('A', 2200), + Project('B', 1720), + Project('C', 2800), + Project('D', 400), + Project('E', 400), + Project('F', 400), + ] + instance = Instance(projects) + donors = CumulativeProfile( + [ + CumulativeBallot({projects[0]: 2000, projects[1]: 10, projects[2]: 500, projects[3]: 5, projects[4]: 1, projects[5]: 1}), + CumulativeBallot({projects[0]: 10, projects[1]: 2000, projects[2]: 500, projects[3]: 1, projects[4]: 5, projects[5]: 1}), + CumulativeBallot({projects[0]: 500, projects[1]: 10, projects[2]: 2000, projects[3]: 1, projects[4]: 1, projects[5]: 5}), + ] + ) + with self.subTest(combination=combination): + selected_projects = cstv(instance, donors, combination) + match combination: + case CSTV_Combination.EWT | CSTV_Combination.MT: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[2], + projects[1], + projects[4], + projects[5], + }) + case CSTV_Combination.EWTC | CSTV_Combination.MTC: + self.assertSetEqual(set(selected_projects), { + projects[1], + projects[2], + projects[0], + projects[3], + projects[5], + }) + case CSTV_Combination.EWTS | CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[2], + projects[0], + projects[1], + projects[4], + projects[3], + }) + + def test_cstv_small(self): + for combination in CSTV_Combination: + projects = [ + Project('A', 20), + Project('B', 35), + Project('C', 40), + Project('D', 40), + Project('E', 20), + ] + instance = Instance(projects) + donors = CumulativeProfile( + [ + CumulativeBallot({projects[0]: 10, projects[1]: 7, projects[2]: 3, projects[3]: 0, projects[4]: 0}), + CumulativeBallot({projects[0]: 10, projects[1]: 8, projects[2]: 1, projects[3]: 1, projects[4]: 0}), + CumulativeBallot({projects[0]: 10, projects[1]: 0, projects[2]: 5, projects[3]: 3, projects[4]: 2}), + CumulativeBallot({projects[0]: 10, projects[1]: 0, projects[2]: 5, projects[3]: 5, projects[4]: 0}), + ] + ) + with self.subTest(combination=combination): + selected_projects = cstv(instance, donors, combination) + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[2], + projects[4], + }) + + + From f9d5b89fce9945d429904bfe0cf8be41718267b4 Mon Sep 17 00:00:00 2001 From: Kapiszon2343 Date: Thu, 17 Jul 2025 18:12:14 +0200 Subject: [PATCH 5/8] minimal transfers float fix --- pabutools/rules/cstv.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/pabutools/rules/cstv.py b/pabutools/rules/cstv.py index 6a5674a0..8ecc4db3 100644 --- a/pabutools/rules/cstv.py +++ b/pabutools/rules/cstv.py @@ -272,6 +272,12 @@ def cstv( ) if not flag: # Perform the inclusive maximality postprocedure + if verbose: + print( + f"Beginning exhaustiveness postprocess\n \ + Remaining projects: {eligible_projects}\n \ + Eliminated projects: {eliminated_projects}", + ) exhaustiveness_postprocess_func( selected_projects, donations, @@ -671,8 +677,6 @@ def minimal_transfer( eliminated_projects. """ - if pabutools.fractions.FRACTION != pabutools.fractions.FLOAT_FRAC: - warnings.warn("You are using minimal transfers with exact fractions, this may never end...") for project in projects.copy(): donors_of_selected_project = [ donor.values() @@ -699,7 +703,6 @@ def minimal_transfer( donors_of_selected_project = [ i for i, donor in enumerate(donors) if donor.get(chosen_project.name, 0) > 0 ] - project_cost = chosen_project.cost # Calculate initial support ratio @@ -725,7 +728,6 @@ def minimal_transfer( total_support -= total project_cost -= total r = frac(total_support, project_cost) - # Loop until the required support is achieved if donors_of_selected_project: num_loop_run = 0 @@ -763,6 +765,18 @@ def minimal_transfer( #raise RuntimeError("The while loop of the minimal_transfer function ran for too long. This can be due to" # " issues with floating point arithmetic.") break + + diff = project_cost - sum(donor.get(chosen_project.name, 0) for donor in donors) + if diff > 0: + mn_supp = project_cost + mn_i = 0 + for idx, donor in enumerate(donors): + supp = donor.get(chosen_project.name, 0) + if supp > 0 and mn_supp > supp: + mn_supp = supp + mn_i = idx + donors[mn_i][chosen_project.name] += diff + return True From acc7794509c29b705f87d3d1c3720843a6d0a37d Mon Sep 17 00:00:00 2001 From: harasimowiczk Date: Sat, 18 Oct 2025 00:40:56 +0200 Subject: [PATCH 6/8] test global vars fix --- tests/rules/test_cstv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rules/test_cstv.py b/tests/rules/test_cstv.py index dbe7ad9c..3e561f46 100644 --- a/tests/rules/test_cstv.py +++ b/tests/rules/test_cstv.py @@ -709,4 +709,4 @@ def test_cstv_small(self): }) - + pabutools.fractions.FRACTION = "gmpy2" From 9d34a0f2edc3520af1fca482d793194ea3b31ec2 Mon Sep 17 00:00:00 2001 From: harasimowiczk Date: Sat, 18 Oct 2025 01:33:52 +0200 Subject: [PATCH 7/8] better comments cstv --- pabutools/rules/cstv.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pabutools/rules/cstv.py b/pabutools/rules/cstv.py index bc309cf6..4e278d75 100644 --- a/pabutools/rules/cstv.py +++ b/pabutools/rules/cstv.py @@ -3,9 +3,10 @@ An implementation of the algorithms in: -"Participatory Budgeting with Cumulative Votes", by Piotr Skowron, Arkadii Slinko, Stanisaw Szufa, +"Participatory Budgeting with Cumulative Votes", +by Piotr Skowron, Arkadii Slinko, Stanisaw Szufa, Nimrod Talmon (2020), https://arxiv.org/pdf/2009.02690 -Programmer: Achiya Ben Natan +Original implementation: Achiya Ben Natan Changes: Kacper Harasimowicz Date: 2024/05/16. """ @@ -745,6 +746,7 @@ def minimal_transfer( eliminated_projects.append(project) return False + # Redistribute all support when current ratio would need to consume all for i in donors_of_selected_project: donor = donors[i] donation = donor.get(chosen_project, 0) @@ -768,6 +770,7 @@ def minimal_transfer( # " issues with floating point arithmetic.") break + # Correction for (potentail) fraction inaccuracy diff = project_cost - sum(donor.get(chosen_project.name, 0) for donor in donors) if diff > 0: mn_supp = project_cost From c93c87f5fcc1bffa08291a2ae0211afc8d34366d Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 29 Oct 2025 20:42:25 +0100 Subject: [PATCH 8/8] remove match case --- tests/rules/test_cstv.py | 411 +++++++++++++++++++-------------------- 1 file changed, 203 insertions(+), 208 deletions(-) diff --git a/tests/rules/test_cstv.py b/tests/rules/test_cstv.py index 3e561f46..57913be8 100644 --- a/tests/rules/test_cstv.py +++ b/tests/rules/test_cstv.py @@ -253,55 +253,54 @@ def test_cstv_party_split(self): with self.subTest(combination=combination): selected_projects = cstv(instance, donors, combination) - match combination: - case CSTV_Combination.EWT | CSTV_Combination.EWTC: - self.assertSetEqual(set(selected_projects), { - projects[4], - projects[1], - projects[2], - projects[7], - projects[5], - projects[3], - projects[6] - }) - case CSTV_Combination.EWTS: - self.assertSetEqual(set(selected_projects), { - projects[4], - projects[1], - projects[6], - projects[3], - projects[5], - projects[0] - }) - case CSTV_Combination.MT: - self.assertSetEqual(set(selected_projects), { - projects[4], - projects[5], - projects[1], - projects[2], - projects[7], - projects[8], - projects[3] - }) - case CSTV_Combination.MTC: - self.assertSetEqual(set(selected_projects), { - projects[4], - projects[5], - projects[1], - projects[2], - projects[6], - projects[3], - projects[7] - }) - case CSTV_Combination.MTS: - self.assertSetEqual(set(selected_projects), { - projects[4], - projects[0], - projects[3], - projects[6], - projects[2], - projects[5] - }) + if combination == CSTV_Combination.EWT or combination == CSTV_Combination.EWTC: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[1], + projects[2], + projects[7], + projects[5], + projects[3], + projects[6] + }) + elif combination == CSTV_Combination.EWTS: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[1], + projects[6], + projects[3], + projects[5], + projects[0] + }) + elif combination == CSTV_Combination.MT: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[5], + projects[1], + projects[2], + projects[7], + projects[8], + projects[3] + }) + elif combination == CSTV_Combination.MTC: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[5], + projects[1], + projects[2], + projects[6], + projects[3], + projects[7] + }) + elif combination == CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[4], + projects[0], + projects[3], + projects[6], + projects[2], + projects[5] + }) def test_cstv_laminal1(self): for combination in CSTV_Combination: @@ -393,69 +392,68 @@ def test_cstv_laminal1(self): with self.subTest(combination=combination): selected_projects = cstv(instance, donors, combination) - match combination: - case CSTV_Combination.EWT: - self.assertSetEqual(set(selected_projects), { - projects[0], - projects[6], - projects[7], - projects[9], - projects[10], - projects[11], - projects[4], - projects[2], - }) - case CSTV_Combination.EWTC: - self.assertSetEqual(set(selected_projects), { - projects[0], - projects[7], - projects[9], - projects[6], - projects[4], - projects[1], - projects[11], - }) - case CSTV_Combination.EWTS: - self.assertSetEqual(set(selected_projects), { - projects[0], - projects[7], - projects[6], - projects[1], - projects[2], - projects[11], - }) - case CSTV_Combination.MT: - self.assertSetEqual(set(selected_projects), { - projects[0], - projects[6], - projects[9], - projects[7], - projects[10], - projects[14], - projects[4], - projects[5], - projects[13] - }) - case CSTV_Combination.MTC: - self.assertSetEqual(set(selected_projects), { - projects[0], - projects[7], - projects[6], - projects[9], - projects[4], - projects[11], - projects[1], - }) - case CSTV_Combination.MTS: - self.assertSetEqual(set(selected_projects), { - projects[0], - projects[1], - projects[7], - projects[2], - projects[13], - projects[10], - projects[6], - }) + if combination == CSTV_Combination.EWT: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[6], + projects[7], + projects[9], + projects[10], + projects[11], + projects[4], + projects[2], + }) + elif combination == CSTV_Combination.EWTC: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[7], + projects[9], + projects[6], + projects[4], + projects[1], + projects[11], + }) + elif combination == CSTV_Combination.EWTS: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[7], + projects[6], + projects[1], + projects[2], + projects[11], + }) + elif combination == CSTV_Combination.MT: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[6], + projects[9], + projects[7], + projects[10], + projects[14], + projects[4], + projects[5], + projects[13] + }) + elif combination == CSTV_Combination.MTC: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[7], + projects[6], + projects[9], + projects[4], + projects[11], + projects[1], + }) + elif combination == CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[1], + projects[7], + projects[2], + projects[13], + projects[10], + projects[6], + }) def test_cstv_laminal2(self): for combination in CSTV_Combination: @@ -546,68 +544,67 @@ def test_cstv_laminal2(self): with self.subTest(combination=combination): selected_projects = cstv(instance, donors, combination) - match combination: - case CSTV_Combination.EWT: - self.assertSetEqual(set(selected_projects), { - projects[12], - projects[0], - projects[8], - projects[2], - projects[7], - projects[13], - projects[11], - projects[10], - }) - case CSTV_Combination.EWTC: - self.assertSetEqual(set(selected_projects), { - projects[12], - projects[0], - projects[8], - projects[2], - projects[1], - projects[13], - projects[11], - }) - case CSTV_Combination.EWTS: - self.assertSetEqual(set(selected_projects), { - projects[12], - projects[0], - projects[2], - projects[1], - projects[13], - projects[4], - }) - case CSTV_Combination.MT: - self.assertSetEqual(set(selected_projects), { - projects[12], - projects[0], - projects[8], - projects[13], - projects[11], - projects[7], - projects[10], - projects[5], - projects[14], - }) - case CSTV_Combination.MTC: - self.assertSetEqual(set(selected_projects), { - projects[12], - projects[0], - projects[13], - projects[8], - projects[2], - projects[1], - projects[11], - }) - case CSTV_Combination.MTS: - self.assertSetEqual(set(selected_projects), { - projects[12], - projects[0], - projects[1], - projects[2], - projects[13], - projects[4], - }) + if combination == CSTV_Combination.EWT: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[8], + projects[2], + projects[7], + projects[13], + projects[11], + projects[10], + }) + elif combination == CSTV_Combination.EWTC: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[8], + projects[2], + projects[1], + projects[13], + projects[11], + }) + elif combination == CSTV_Combination.EWTS: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[2], + projects[1], + projects[13], + projects[4], + }) + elif combination == CSTV_Combination.MT: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[8], + projects[13], + projects[11], + projects[7], + projects[10], + projects[5], + projects[14], + }) + elif combination == CSTV_Combination.MTC: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[13], + projects[8], + projects[2], + projects[1], + projects[11], + }) + elif combination == CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[12], + projects[0], + projects[1], + projects[2], + projects[13], + projects[4], + }) def test_cstv_EWT_v_MT(self): for combination in CSTV_Combination: @@ -626,15 +623,14 @@ def test_cstv_EWT_v_MT(self): ) with self.subTest(combination=combination): selected_projects = cstv(instance, donors, combination) - match combination: - case CSTV_Combination.EWT | CSTV_Combination.EWTC | CSTV_Combination.EWTS: - self.assertSetEqual(set(selected_projects), { - projects[1], - }) - case CSTV_Combination.MT | CSTV_Combination.MTC | CSTV_Combination.MTS: - self.assertSetEqual(set(selected_projects), { - projects[0], - }) + if combination == CSTV_Combination.EWT or combination == CSTV_Combination.EWTC or combination == CSTV_Combination.EWTS: + self.assertSetEqual(set(selected_projects), { + projects[1], + }) + elif combination == CSTV_Combination.MT or combination == CSTV_Combination.MTC or combination == CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[0], + }) def test_cstv_Greedy_difference(self): for combination in CSTV_Combination: @@ -656,31 +652,30 @@ def test_cstv_Greedy_difference(self): ) with self.subTest(combination=combination): selected_projects = cstv(instance, donors, combination) - match combination: - case CSTV_Combination.EWT | CSTV_Combination.MT: - self.assertSetEqual(set(selected_projects), { - projects[0], - projects[2], - projects[1], - projects[4], - projects[5], - }) - case CSTV_Combination.EWTC | CSTV_Combination.MTC: - self.assertSetEqual(set(selected_projects), { - projects[1], - projects[2], - projects[0], - projects[3], - projects[5], - }) - case CSTV_Combination.EWTS | CSTV_Combination.MTS: - self.assertSetEqual(set(selected_projects), { - projects[2], - projects[0], - projects[1], - projects[4], - projects[3], - }) + if combination == CSTV_Combination.EWT or combination == CSTV_Combination.MT: + self.assertSetEqual(set(selected_projects), { + projects[0], + projects[2], + projects[1], + projects[4], + projects[5], + }) + elif combination == CSTV_Combination.EWTC or combination == CSTV_Combination.MTC: + self.assertSetEqual(set(selected_projects), { + projects[1], + projects[2], + projects[0], + projects[3], + projects[5], + }) + elif combination == CSTV_Combination.EWTS or combination == CSTV_Combination.MTS: + self.assertSetEqual(set(selected_projects), { + projects[2], + projects[0], + projects[1], + projects[4], + projects[3], + }) def test_cstv_small(self): for combination in CSTV_Combination: