def compute_optphragmen(profile, committeesize, algorithm="gurobi", resolute=False, verbose=0): enough_approved_candidates(profile, committeesize) # optional output if verbose: print(header(rules["optphrag"].longname)) if resolute: print("Computing only one winning committee (resolute=True)\n") if verbose >= 3: if algorithm == "gurobi": print("Using the Gurobi ILP solver") # end of optional output if algorithm != "gurobi": raise NotImplementedError("Algorithm " + str(algorithm) + " not specified for compute_optphragmen") committees = abcrules_gurobi.__gurobi_optphragmen( profile, committeesize, resolute=resolute, verbose=verbose) committees = sort_committees(committees) # optional output if verbose: print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) # end of optional output return committees
def compute_lexmav(profile, committeesize, algorithm="brute-force", resolute=False, verbose=0): """Lexicographic Minimax AV""" enough_approved_candidates(profile, committeesize) if not profile.has_unit_weights(): raise ValueError(rules["lexmav"].shortname + " is only defined for unit weights (weight=1)") if algorithm != "brute-force": raise NotImplementedError( "Algorithm " + str(algorithm) + " not specified for compute_lexmav") opt_committees = [] opt_distances = [profile.num_cand + 1] * len(profile) for comm in combinations(list(range(profile.num_cand)), committeesize): distances = sorted([hamming(pref, comm) for pref in profile], reverse=True) for i in range(len(distances)): if opt_distances[i] < distances[i]: break if opt_distances[i] > distances[i]: opt_distances = distances opt_committees = [comm] break else: opt_committees.append(comm) committees = sort_committees(opt_committees) if resolute: committees = [committees[0]] # optional output if verbose: print(header(rules["lexmav"].longname)) if resolute: print("Computing only one winning committee (resolute=True)\n") print("Minimum maximal distance: " + str(max(opt_distances))) print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) print("Corresponding distances to voters:") for comm in committees: print([hamming(pref, comm) for pref in profile]) print() # end of optional output return committees
def compute_thiele_method(scorefct_str, profile, committeesize, algorithm="gurobi", resolute=False, verbose=0): """Thiele methods Compute winning committees of the Thiele method specified by the score function (scorefct_str) """ enough_approved_candidates(profile, committeesize) scorefct = scores.get_scorefct(scorefct_str, committeesize) # optional output if verbose: print(header(rules[scorefct_str].longname)) if resolute: print("Computing only one winning committee (resolute=True)\n") if verbose >= 3: if algorithm == "gurobi": print("Using the Gurobi ILP solver\n") if algorithm == "branch-and-bound": print("Using a branch-and-bound algorithm\n") # end of optional output if algorithm == "gurobi": committees = abcrules_gurobi.__gurobi_thiele_methods( profile, committeesize, scorefct, resolute) committees = sort_committees(committees) elif algorithm == "branch-and-bound": committees = __thiele_methods_branchandbound( profile, committeesize, scorefct_str, resolute) else: raise NotImplementedError( "Algorithm " + str(algorithm) + " not specified for compute_thiele_method") # optional output if verbose >= 2: print("Optimal " + scorefct_str.upper() + "-score: " + str(scores.thiele_score(scorefct_str, profile, committees[0]))) print() if verbose: print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) # end of optional output return committees
def compute_mav(profile, committeesize, algorithm="brute-force", resolute=False, verbose=0): """Minimax AV (MAV)""" enough_approved_candidates(profile, committeesize) # optional output if verbose: print(header(rules["mav"].longname)) if resolute: print("Computing only one winning committee (resolute=True)\n") if verbose >= 3: if algorithm == "gurobi": print("Using the Gurobi ILP solver\n") if algorithm == "brute-force": print("Using a brute-force algorithm\n") # end of optional output if algorithm == "gurobi": committees = abcrules_gurobi.__gurobi_minimaxav( profile, committeesize, resolute) committees = sort_committees(committees) elif algorithm == "brute-force": committees = __minimaxav_bruteforce(profile, committeesize) if resolute: committees = [committees[0]] else: raise NotImplementedError("Algorithm " + str(algorithm) + " not specified for compute_mav") opt_mavscore = scores.mavscore(profile, committees[0]) # optional output if verbose: print("Minimum maximal distance: " + str(opt_mavscore)) print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) print("Corresponding distances to voters:") for comm in committees: print([hamming(pref, comm) for pref in profile]) print() # end of optional output return committees
def compute_monroe(profile, committeesize, algorithm="brute-force", resolute=False, verbose=0): """Monroe's rule""" enough_approved_candidates(profile, committeesize) # optional output if verbose: print(header(rules["monroe"].longname)) if resolute: print("Computing only one winning committee (resolute=True)\n") if verbose >= 3: if algorithm == "gurobi": print("Using the Gurobi ILP solver\n") if algorithm == "brute-force": print("Using a brute-force algorithm\n") # end of optional output if not profile.has_unit_weights(): raise ValueError(rules["monroe"].shortname + " is only defined for unit weights (weight=1)") if algorithm == "gurobi": committees = abcrules_gurobi.__gurobi_monroe( profile, committeesize, resolute) committees = sort_committees(committees) elif algorithm == "brute-force": committees = __monroe_bruteforce( profile, committeesize, resolute) else: raise NotImplementedError( "Algorithm " + str(algorithm) + " not specified for compute_monroe") # optional output if verbose: print("Optimal Monroe score: " + str(scores.monroescore(profile, committees[0])) + "\n") print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) # end of optional output return committees
def compute_seqphragmen(profile, committeesize, algorithm="standard", resolute=True, verbose=False): """Phragmen's sequential rule (seq-Phragmen)""" enough_approved_candidates(profile, committeesize) if algorithm != "standard": raise NotImplementedError( "Algorithm " + str(algorithm) + " not specified for compute_seqphragmen") # optional output if verbose: print(header(rules["seqphrag"].longname)) if resolute: print("Computing only one winning committee (resolute=True)\n") # end of optional output if resolute: committees, comm_loads = __seqphragmen_resolute( profile, committeesize, verbose) else: committees, comm_loads = __seqphragmen_irresolute( profile, committeesize) # optional output if verbose: print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) if verbose >= 2: if resolute or len(committees) == 1: print("corresponding load distribution:") else: print("corresponding load distributions:") for comm in committees: output = "(" for v, _ in enumerate(profile): output += str(comm_loads[tuple(comm)][v]) + ", " print(output[:-2] + ")") # end of optional output return committees
def compute_revseq_thiele_method(profile, committeesize, scorefct_str, algorithm="standard", resolute=True, verbose=0): """Reverse sequential Thiele methods""" enough_approved_candidates(profile, committeesize) if algorithm != "standard": raise NotImplementedError( "Algorithm " + str(algorithm) + " not specified for compute_revseq_thiele_method") # optional output if verbose: print(header(rules["revseq" + scorefct_str].longname)) if resolute: print("Computing only one winning committee (resolute=True)\n") # end of optional output if resolute: committees = __revseq_thiele_resolute( profile, committeesize, scorefct_str, verbose=verbose) else: committees = __revseq_thiele_irresolute( profile, committeesize, scorefct_str) # optional output if verbose: print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) if verbose >= 2: if resolute or len(committees) == 1: print("PAV-score of winning committee:", end="") else: print("PAV-score of winning committees:") for comm in committees: print(" " + str(scores.thiele_score(scorefct_str, profile, comm))) print() # end of optional output return committees
""" Proposition A.4. From "Multi-Winner Voting with Approval Preferences" by Martin Lackner and Piotr Skowron https://arxiv.org/abs/2007.01795 """ from abcvoting import abcrules from abcvoting.preferences import Profile from abcvoting import misc from collections import namedtuple print(misc.header("Proposition A.4", "*")) num_cand = 6 a, b, c, d, e, f = range(6) # a = 0, b = 1, c = 2, ... cand_names = "abcdef" ManipulationInstance = namedtuple( "ManipulationInstance", "rule_id committeesize approval_sets manipulated_vote committees_first committees_after", ) manipulations = [ ManipulationInstance( rule_id="cc", committeesize=2, approval_sets=[{a, b}] + [{a}] * 3 + [{c}], manipulated_vote={b}, committees_first=[{a, c}],
Proposition A.2. From "Multi-Winner Voting with Approval Preferences" by Martin Lackner and Piotr Skowron https://arxiv.org/abs/2007.01795 """ from abcvoting import abcrules from abcvoting.preferences import Profile from abcvoting import misc from abcvoting.output import output from abcvoting.output import DETAILS output.set_verbosity(DETAILS) print(misc.header("Proposition A.2", "*")) ### num_cand = 3 a, b, c = (0, 1, 2) approval_sets = [{a}] * 2 + [{a, c}] * 3 + [{b, c}] * 3 + [{b}] * 2 cand_names = "abcde" profile = Profile(num_cand, cand_names=cand_names) profile.add_voters(approval_sets) print(misc.header("1st profile:")) print(profile.str_compact()) print("winning committees for k=1 and k=2:") for rule_id in ["pav", "cc", "monroe", "minimaxphragmen", "minimaxav"]:
"""Proposition A.4 from the survey: "Approval-Based Multi-Winner Voting: Axioms, Algorithms, and Applications" by Martin Lackner and Piotr Skowron """ from abcvoting import abcrules from abcvoting.preferences import Profile from abcvoting import misc print(misc.header("Proposition A.4", "*")) num_cand = 6 a, b, c, d, e, f = range(6) # a = 0, b = 1, c = 2, ... cand_names = "abcdef" manipulations = [ ("cc", True, 2, [{a, b}] + [{a}] * 3 + [{c}], {b}, [{a, c}], [{a, b}]), ("sav", False, 1, [{a, b, c}, {d, e}], {a}, [{d}, {e}], [{a}]), ( "revseqpav", False, 2, [{a, b, c}, {b, d}, {c, b}, {a, d, e}, {b, e}], {a}, [{b, d}, {b, e}], [{a, b}], ), ( "seqphragmen", False,
def print_profile(): print(misc.header("Example 1", "*")) print(profile.str_compact()) print("desired committee size k = " + str(committeesize))
def test_output(capfd, rule_id, algorithm, resolute, verbosity): if algorithm == "fastest": return # not necessary, output for "fastest" is the same as # whatever algorithm is selected as fastest # (and "fastest" depends on the available solvers) if algorithm == "cvxpy_glpk_mi": # TODO unfortunately GLPK_MI prints "Long-step dual simplex will be used" to stderr and it # would be very complicated to capture this on all platforms reliably, changing # sys.stderr doesn't help. # This seems to be fixed in GLPK 5.0 but not in GLPK 4.65. For some weird reason this # test succeeds and does not need to be skipped when using conda-forge, although the # version from conda-forge is given as glpk 4.65 he80fd80_1002. # This could help to introduce a workaround: https://github.com/xolox/python-capturer # Sage math is fighting the same problem: https://trac.sagemath.org/ticket/24824 pytest.skip("GLPK_MI prints something to stderr, not easy to capture") output.set_verbosity(verbosity=verbosity) try: profile = Profile(2) profile.add_voters([[0]]) committeesize = 1 committees = abcrules.compute( rule_id, profile, committeesize, algorithm=algorithm, resolute=resolute ) out = str(capfd.readouterr().out) # remove unwanted solver output out = remove_solver_output(out) if verbosity >= WARNING: assert out == "" else: assert len(out) > 0 rule = abcrules.get_rule(rule_id) start_output = misc.header(rule.longname) + "\n" if resolute and rule.resolute_values[0] == False: # only if irresolute is default but resolute is chosen start_output += "Computing only one winning committee (resolute=True)\n\n" if not resolute and rule.resolute_values[0] == True: # only if resolute is default but resolute=False is chosen start_output += ( "Computing all possible winning committees for any tiebreaking order\n" " (aka parallel universes tiebreaking) (resolute=False)\n\n" ) if verbosity <= DETAILS: start_output += "Algorithm: " + abcrules.ALGORITHM_NAMES[algorithm] + "\n" if verbosity <= DEBUG: assert start_output in out else: assert out.startswith(start_output) end_output = ( f"{misc.str_committees_header(committees, winning=True)}\n" f"{misc.str_sets_of_candidates(committees, cand_names=profile.cand_names)}\n" ) if verbosity == INFO: assert out.endswith(end_output) else: assert end_output in out finally: output.set_verbosity(verbosity=WARNING)
"""Example 1 (approval profile for running example) from the survey: "Approval-Based Multi-Winner Voting: Axioms, Algorithms, and Applications" by Martin Lackner and Piotr Skowron """ from __future__ import print_function import sys sys.path.insert(0, '..') from abcvoting.preferences import Profile from abcvoting import misc num_cand = 8 a, b, c, d, e, f, g = list(range(7)) # a = 0, b = 1, c = 2, ... apprsets = [[a, b], [a, b], [a, b], [a, c], [a, c], [a, c], [a, d], [a, d], [b, c, f], [e], [f], [g]] names = "abcdefgh" committeesize = 4 profile = Profile(num_cand, names=names) profile.add_preferences(apprsets) if __name__ == "__main__": print(misc.header("Example 1", "*")) print(profile.str_compact()) print("desired committee size k = " + str(committeesize))
def compute_phragmen_enestroem(profile, committeesize, algorithm="standard", resolute=True, verbose=0): """"Phragmen-Enestroem (aka Phragmen's first method, Enestroem's method) In every round the candidate with the highest combined budget of their supporters is put in the committee. Method described in: https://arxiv.org/pdf/1611.08826.pdf (Section 18.5, Page 59) """ enough_approved_candidates(profile, committeesize) if not profile.has_unit_weights(): raise ValueError(rules["phrag-enestr"].shortname + " is only defined for unit weights (weight=1)") if algorithm != "standard": raise NotImplementedError( "Algorithm " + str(algorithm) + " not specified for compute_phragmen_enestroem") num_voters = len(profile) start_budget = {i: Fraction(profile[i].weight) for i in range(num_voters)} price = Fraction(sum(start_budget.values()), committeesize) cands = range(profile.num_cand) committees = [(start_budget, set())] for _ in range(committeesize): # here the committees with i+1 candidates are # stored (together with budget) next_committees = [] # loop in case multiple possible committees # with i filled candidates for committee in committees: budget, comm = committee curr_cands = set(cands) - comm support = {c: 0 for c in curr_cands} for nr, pref in enumerate(profile): voting_power = budget[nr] if voting_power <= 0: continue for cand in pref: if cand in curr_cands: support[cand] += voting_power max_support = max(support.values()) winners = [c for c, s in support.items() if s == max_support] for cand in winners: b = dict(budget) # copy of budget if max_support > price: # supporters can afford it # (voting_power - price) / voting_power multiplier = Fraction(max_support - price, max_support) else: # set supporters to 0 multiplier = 0 for nr, pref in enumerate(profile): if cand in pref: b[nr] *= multiplier c = comm.union([cand]) # new committee with candidate next_committees.append((b, c)) if resolute: committees = [next_committees[0]] else: committees = next_committees committees = [comm for b, comm in committees] committees = sort_committees(committees) if resolute: committees = [committees[0]] # optional output if verbose: print(header(rules["phrag-enestr"].longname)) print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) # end of optional output return committees
""" Example 2.6 (PAV, seq-PAV, revseq-PAV). From "Multi-Winner Voting with Approval Preferences" by Martin Lackner and Piotr Skowron https://arxiv.org/abs/2007.01795 """ from abcvoting import abcrules from abcvoting.preferences import Profile from abcvoting import misc from abcvoting.output import output, DETAILS output.set_verbosity(DETAILS) print(misc.header("Example 6", "*")) # Approval profile num_cand = 4 a, b, c, d = range(4) # a = 0, b = 1, c = 2, ... cand_names = "abcd" profile = Profile(num_cand, cand_names=cand_names) profile.add_voters([{a, b}, {a, b, c}, {a, b, d}, {a, c, d}, {a, c, d}, {b}, {c}, {d}]) print(misc.header("Input:")) print(profile.str_compact()) committees_av = abcrules.compute_av(profile, 1)
from __future__ import print_function import sys sys.path.insert(0, '..') from abcvoting import abcrules from abcvoting.preferences import Profile from abcvoting import misc print("Remark 2:\n*********\n") # Approval profile num_cand = 3 a, b, c = list(range(3)) # a = 0, b = 1, c = 2 apprsets = [[a]] * 99 + [[a, b, c]] names = "abc" profile = Profile(num_cand, names=names) profile.add_preferences(apprsets) print(misc.header("Input:")) print(profile.str_compact()) committees_mav = abcrules.compute_mav(profile, 1, verbose=2) committees_lexmav = abcrules.compute_lexmav(profile, 1, verbose=2) # verify correctness assert committees_mav == [[a], [b], [c]] assert committees_lexmav == [[a]]
def __separable(rule_id, profile, committeesize, resolute, verbose): enough_approved_candidates(profile, committeesize) appr_scores = [0] * profile.num_cand for pref in profile: for cand in pref: if rule_id == "sav": # Satisfaction Approval Voting appr_scores[cand] += Fraction(pref.weight, len(pref)) elif rule_id == "av": # (Classic) Approval Voting appr_scores[cand] += pref.weight else: raise UnknownRuleIDError(rule_id) # smallest score to be in the committee cutoff = sorted(appr_scores)[-committeesize] certain_cands = [c for c in range(profile.num_cand) if appr_scores[c] > cutoff] possible_cands = [c for c in range(profile.num_cand) if appr_scores[c] == cutoff] missing = committeesize - len(certain_cands) if len(possible_cands) == missing: # candidates with appr_scores[c] == cutoff # are also certain candidates because all these candidates # are required to fill the committee certain_cands = sorted(certain_cands + possible_cands) possible_cands = [] missing = 0 if resolute: committees = sort_committees( [(certain_cands + possible_cands[:missing])]) else: committees = sort_committees( [(certain_cands + list(selection)) for selection in combinations(possible_cands, missing)]) # optional output if verbose: print(header(rules[rule_id].longname)) if resolute: print("Computing only one winning committee (resolute=True)\n") if verbose >= 2: print("Scores of candidates:") for c in range(profile.num_cand): print(profile.names[c] + ": " + str(appr_scores[c])) print("\nCandidates are contained in winning committees") print("if their score is >= " + str(cutoff) + ".") if len(certain_cands) > 0: print("\nThe following candidates are contained in") print("every winning committee:") namedset = [profile.names[c] for c in certain_cands] print(" " + ", ".join(map(str, namedset))) print() if len(possible_cands) > 0: print("The following candidates are contained in") print("some of the winning committees:") namedset = [profile.names[c] for c in possible_cands] print(" " + ", ".join(map(str, namedset))) print("(" + str(missing) + " of those candidates is contained\n" + " in every winning committee.)\n") if verbose: print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) # end of optional output return committees
"""Example 7 (Greedy Monroe) from the survey: "Approval-Based Multi-Winner Voting: Axioms, Algorithms, and Applications" by Martin Lackner and Piotr Skowron """ from __future__ import print_function import sys sys.path.insert(0, '..') from abcvoting import abcrules from survey import example01 as ex1 from abcvoting.scores import monroescore from abcvoting import misc print(misc.header("Example 7", "*")) print(misc.header("Input (election instance from Example 1):")) print(ex1.profile.str_compact()) committees = abcrules.compute_greedy_monroe(ex1.profile, 4, verbose=2) # verify correctness a, b, c, d, e, f, g = list(range(7)) # a = 0, b = 1, c = 2, ... assert committees == [[a, c, d, f]] assert monroescore(ex1.profile, committees[0]) == 10
def compute_greedy_monroe(profile, committeesize, algorithm="standard", resolute=True, verbose=0): """"Greedy Monroe""" enough_approved_candidates(profile, committeesize) if not profile.has_unit_weights(): raise ValueError(rules["greedy-monroe"].shortname + " is only defined for unit weights (weight=1)") if not resolute: raise NotImplementedError( "compute_greedy_monroe does not support resolute=False.") if algorithm != "standard": raise NotImplementedError( "Algorithm " + str(algorithm) + " not specified for compute_greedy_monroe") num_voters = len(profile) committee = [] # remaining voters remaining_voters = list(range(num_voters)) remaining_cands = set(range(profile.num_cand)) assignment = [] for t in range(committeesize): maxapprovals = -1 selected = None for c in remaining_cands: approvals = len([i for i in remaining_voters if c in profile[i]]) if approvals > maxapprovals: maxapprovals = approvals selected = c # determine how many voters are removed (at most) if t < num_voters - committeesize * (num_voters // committeesize): num_remove = num_voters // committeesize + 1 else: num_remove = num_voters // committeesize # only voters that approve the chosen candidate # are removed to_remove = [i for i in remaining_voters if selected in profile[i]] if len(to_remove) > num_remove: to_remove = to_remove[:num_remove] assignment.append((selected, to_remove)) remaining_voters = [i for i in remaining_voters if i not in to_remove] committee.append(selected) remaining_cands.remove(selected) committees = sort_committees([committee]) # optional output if verbose: print(header(rules["greedy-monroe"].longname)) if verbose >= 2: score1 = scores.monroescore(profile, committees[0]) score2 = len(profile) - len(remaining_voters) print("The Monroe assignment computed by Greedy Monroe") print("has a Monroe score of " + str(score2) + ".") if score1 > score2: print("Monroe assignment found by Greedy Monroe is not " + "optimal for the winning committee,") print("i.e., by redistributing voters to candidates a higher " + "satisfaction is possible " + "(without changing the committee).") print("Optimal Monroe score of the winning committee is " + str(score1) + ".") # build actual Monroe assignment for winning committee for t, district in enumerate(assignment): cand, voters = district if t < num_voters - committeesize * (num_voters // committeesize): missing = num_voters // committeesize + 1 - len(voters) else: missing = num_voters // committeesize - len(voters) for _ in range(missing): v = remaining_voters.pop() voters.append(v) print("Assignment (unsatisfatied voters marked with *):\n") for cand, voters in assignment: print(" candidate " + profile.names[cand] + " assigned to: ", end="") output = "" for v in sorted(voters): output += str(v) if cand not in profile[v].approved: output += "*" output += ", " print(output[:-2]) print() if verbose: print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) # end of optional output return committees
""" Example 2.5 (PAV, seq-PAV, revseq-PAV). From "Multi-Winner Voting with Approval Preferences" by Martin Lackner and Piotr Skowron https://arxiv.org/abs/2007.01795 """ from abcvoting import abcrules from abcvoting.preferences import Profile, Voter from abcvoting import misc from abcvoting.output import output, DETAILS output.set_verbosity(DETAILS) print(misc.header("Example 5", "*")) # Approval profile num_cand = 4 a, b, c, d = range(4) # a = 0, b = 1, c = 2, ... cand_names = "abcd" approval_sets = [[a, b]] * 3 + [[a, d]] * 6 + [[b]] * 4 + [[c]] * 5 + [[c, d] ] * 5 profile = Profile(num_cand, cand_names=cand_names) profile.add_voters(approval_sets) print(misc.header("Input:")) print(profile.str_compact()) committees_pav = abcrules.compute_pav(profile, 2)
def compute_rule_x(profile, committeesize, algorithm="standard", resolute=True, verbose=0): """Rule X See https://arxiv.org/pdf/1911.11747.pdf, page 7 """ enough_approved_candidates(profile, committeesize) if not profile.has_unit_weights(): raise ValueError(rules["rule-x"].shortname + " is only defined for unit weights (weight=1)") if algorithm != "standard": raise NotImplementedError( "Algorithm " + str(algorithm) + " not specified for compute_rule_x") # optional output if verbose: print(header(rules["rule-x"].longname)) if resolute: print("Computing only one winning committee (resolute=True)\n") # end of optional output start_budget = {v: Fraction(committeesize, len(profile)) for v, _ in enumerate(profile)} cands = range(profile.num_cand) commbugdets = [(set(), start_budget)] final_committees = set() # optional output if resolute and verbose >= 2: print("Phase 1:\n") print("starting budget:") output = " (" for v, _ in enumerate(profile): output += str(start_budget[v]) + ", " print(output[:-2] + ")\n") # end of optional output for _ in range(committeesize): next_commbudgets = [] for committee, budget in commbugdets: curr_cands = set(cands) - committee min_q = {} for c in curr_cands: q = __rule_x_get_min_q(profile, budget, c) if q is not None: min_q[c] = q if len(min_q) > 0: # one or more candidates are affordable next_cands = [c for c in min_q.keys() if min_q[c] == min(min_q.values())] for next_cand in next_cands: new_budget = dict(budget) for v, pref in enumerate(profile): if next_cand in pref: new_budget[v] -= min(budget[v], min_q[next_cand]) new_comm = set(committee) new_comm.add(next_cand) next_commbudgets.append((new_comm, new_budget)) # optional output if resolute and verbose >= 2: output = "adding candidate number " output += str(len(committee)) + ": " output += profile.names[next_cand] + "\n" output += " with maxmimum cost per voter q = " output += str(min(min_q.values())) print(output) print(" remaining budget:") output = " (" for v, _ in enumerate(profile): output += str(new_budget[v]) + ", " print(output[:-2] + ")") if len(next_cands) > 1: output = " tie broken in favor of " output += profile.names[next_cand] + "," output += "\n candidates " output += str_candset(next_cands[1:]) output += " are tied" print(output) print() # end of optional output if resolute: break else: # no affordable candidates remain # fill committee via seq-Phragmen # optional output if resolute and verbose >= 2: print("Phase 2 (seq-Phragmén):\n") # end of optional output start_load = {} # translate budget to loads for v in range(len(profile)): start_load[v] = (Fraction(committeesize, len(profile)) - budget[v]) # optional output if resolute and verbose >= 2: print("starting loads (= budget spent):") output = " (" for v, _ in enumerate(profile): output += str(start_load[v]) + ", " print(output[:-2] + ")\n") # end of optional output if resolute: committees, _ = __seqphragmen_resolute( profile, committeesize, verbose=verbose, partial_committee=list(committee), start_load=start_load) else: committees, _ = __seqphragmen_irresolute( profile, committeesize, partial_committee=list(committee), start_load=start_load) final_committees.update([tuple(comm) for comm in committees]) # after filling the remaining spots these committees # have size committeesize commbugdets = next_commbudgets final_committees.update([tuple(comm) for comm, _ in commbugdets]) committees = sort_committees(final_committees) if resolute: committees = committees[:1] # optional output if verbose: print(str_committees_header(committees, winning=True)) print(str_candsets(committees, names=profile.names)) # end of optional output return committees