def compute_seq_thiele_methods(profile, committeesize, scorefct_str): enough_approved_candidates(profile, committeesize) scorefct = sf.get_scorefct(scorefct_str, committeesize) comm_scores = {(): 0} # build committees starting with the empty set for _ in range(0, committeesize): comm_scores_next = {} for committee, score in comm_scores.items(): # marginal utility gained by adding candidate to the committee additional_score_cand = sf.additional_thiele_scores( profile, committee, scorefct) for c in range(profile.num_cand): if additional_score_cand[c] >= max(additional_score_cand): next_comm = tuple(sorted(committee + (c, ))) comm_scores_next[next_comm] = (comm_scores[committee] + additional_score_cand[c]) # remove suboptimal committees comm_scores = {} cutoff = max(comm_scores_next.values()) for com, score in comm_scores_next.items(): if score >= cutoff: comm_scores[com] = score return sort_committees(list(comm_scores.keys()))
def compute_monroe_bruteforce(profile, committeesize, resolute=False, flowbased=True): """Returns the list of winning committees via brute-force Monroe's rule""" enough_approved_candidates(profile, committeesize) if not profile.has_unit_weights(): raise Exception("Monroe is only defined for unit weights (weight=1)") if profile.totalweight() % committeesize != 0 or flowbased: monroescore = sf.monroescore_flowbased else: monroescore = sf.monroescore_matching opt_committees = [] opt_monroescore = -1 for comm in combinations(list(range(profile.num_cand)), committeesize): score = monroescore(profile, comm) if score > opt_monroescore: opt_committees = [comm] opt_monroescore = score elif monroescore(profile, comm) == opt_monroescore: opt_committees.append(comm) opt_committees = sort_committees(opt_committees) if resolute: return [opt_committees[0]] else: return opt_committees
def compute_av(profile, committeesize, resolute=False, sav=False): """Returns the list of winning committees according to Approval Voting""" enough_approved_candidates(profile, committeesize) appr_scores = [0] * profile.num_cand for pref in profile.preferences: for cand in pref.approved: if sav: # Satisfaction Approval Voting appr_scores[cand] += Fraction(pref.weight, len(pref.approved)) else: # (Classic) Approval Voting appr_scores[cand] += pref.weight # smallest score to be in the committee cutoff = sorted(appr_scores)[-committeesize] certain_cand = [ c for c in range(profile.num_cand) if appr_scores[c] > cutoff ] possible_cand = [ c for c in range(profile.num_cand) if appr_scores[c] == cutoff ] missing = committeesize - len(certain_cand) if resolute: return sort_committees([(certain_cand + possible_cand[:missing])]) else: return sort_committees([ (certain_cand + list(selection)) for selection in combinations(possible_cand, missing) ])
def compute_seqphragmen(profile, committeesize, resolute=False): """Returns the list of winning committees according to sequential Phragmen""" enough_approved_candidates(profile, committeesize) load = {v: 0 for v in profile.preferences} comm_loads = {(): load} approvers_weight = {} for c in range(profile.num_cand): approvers_weight[c] = sum(v.weight for v in profile.preferences if c in v.approved) # build committees starting with the empty set for _ in range(0, committeesize): comm_loads_next = {} for committee, load in comm_loads.items(): approvers_load = {} for c in range(profile.num_cand): approvers_load[c] = sum(v.weight * load[v] for v in profile.preferences if c in v.approved) new_maxload = [ Fraction(approvers_load[c] + 1, approvers_weight[c]) if approvers_weight[c] > 0 else committeesize + 1 for c in range(profile.num_cand) ] for c in range(profile.num_cand): if c in committee: new_maxload[c] = sys.maxsize for c in range(profile.num_cand): if new_maxload[c] <= min(new_maxload): new_load = {} for v in profile.preferences: if c in v.approved: new_load[v] = new_maxload[c] else: new_load[v] = load[v] comm_loads_next[tuple(sorted(committee + (c, )))] = new_load # remove suboptimal committees comm_loads = {} cutoff = min([max(load.values()) for load in comm_loads_next.values()]) for com, load in comm_loads_next.items(): if max(load.values()) <= cutoff: comm_loads[com] = load if resolute: committees = sort_committees(list(comm_loads.keys())) comm = tuple(committees[0]) comm_loads = {comm: comm_loads[comm]} committees = sort_committees(list(comm_loads.keys())) if resolute: return [committees[0]] else: return committees
def compute_revseq_thiele_methods_resolute(profile, committeesize, scorefct_str): enough_approved_candidates(profile, committeesize) scorefct = sf.get_scorefct(scorefct_str, committeesize) committee = set(range(profile.num_cand)) for _ in range(0, profile.num_cand - committeesize): cands_to_remove, _ = __least_relevant_cands(profile, committee, scorefct) committee.remove(cands_to_remove[0]) return [sorted(list(committee))]
def compute_seq_thiele_resolute(profile, committeesize, scorefct_str): enough_approved_candidates(profile, committeesize) scorefct = sf.get_scorefct(scorefct_str, committeesize) committee = [] # build committees starting with the empty set for _ in range(0, committeesize): additional_score_cand = sf.additional_thiele_scores( profile, committee, scorefct) next_cand = additional_score_cand.index(max(additional_score_cand)) committee.append(next_cand) return [sorted(committee)]
def compute_greedy_monroe(profile, committeesize): """"Returns the winning committee of the greedy monroe. Always selects the candidate with the highest approval. Always removes the first n/k (rounding depends) voters that approve with the selected candidate. (voter sorted by their rankings) """ enough_approved_candidates(profile, committeesize) if not profile.has_unit_weights(): raise Exception("Greedy Monroe is only defined for unit weights" + " (weight=1)") v = list(enumerate(list(profile.preferences))) # list of tuples (nr, Preferences) # sorted by sorted approved list of preferences voters = sorted(v, key=lambda p: sorted(p[1].approved)) n = len(voters) # number of voters cands = set(range(profile.num_cand)) not_s, committee = (voters, set()) # not_s .. not satisfied voters for t in range(1, committeesize + 1): remaining_cands = cands - committee approval = {c: 0 for c in remaining_cands} for nr, voter in not_s: for c in voter.approved: if c in remaining_cands: approval[c] += 1 max_approval = max(approval.values()) winner = [c for c in remaining_cands if approval[c] == max_approval][0] # round how many are removed, either up or down if t <= n - committeesize * math.floor(n / committeesize): to_remove = math.ceil(float(n) / committeesize) else: to_remove = math.floor(n / committeesize) # not more than the voters that approve # the candidate can be removed to_remove = min(max_approval, to_remove) next_voters = [] for nr, voter in not_s: if to_remove > 0 and winner in voter.approved: to_remove -= 1 else: next_voters.append((nr, voter)) not_s = next_voters committee.add(winner) return sort_committees([committee])
def compute_thiele_methods_branchandbound(profile, committeesize, scorefct_str, resolute=False): enough_approved_candidates(profile, committeesize) scorefct = sf.get_scorefct(scorefct_str, committeesize) best_committees = [] init_com = compute_seq_thiele_resolute(profile, committeesize, scorefct_str) best_score = sf.thiele_score(profile, init_com[0], scorefct_str) part_coms = [[]] while part_coms: part_com = part_coms.pop(0) # potential committee, check if at least as good # as previous best committee if len(part_com) == committeesize: score = sf.thiele_score(profile, part_com, scorefct_str) if score == best_score: best_committees.append(part_com) elif score > best_score: best_committees = [part_com] best_score = score else: if len(part_com) > 0: largest_cand = part_com[-1] else: largest_cand = -1 missing = committeesize - len(part_com) marg_util_cand = sf.additional_thiele_scores( profile, part_com, scorefct) upper_bound = ( sum(sorted(marg_util_cand[largest_cand + 1:])[-missing:]) + sf.thiele_score(profile, part_com, scorefct_str)) if upper_bound >= best_score: for c in range(largest_cand + 1, profile.num_cand - missing + 1): part_coms.insert(0, part_com + [c]) committees = sort_committees(best_committees) if resolute: return [committees[0]] else: return committees
def compute_minimaxav(profile, committeesize, ilp=True, resolute=False): """Returns the list of winning committees according to Minimax AV""" if ilp: return compute_minimaxav_ilp(profile, committeesize, resolute) def hamming(a, b, elements): diffs = 0 for x in elements: if (x in a and x not in b) or (x in b and x not in a): diffs += 1 return diffs def mavscore(committee, profile): score = 0 for vote in profile.preferences: hamdistance = hamming(vote.approved, committee, list(range(profile.num_cand))) if hamdistance > score: score = hamdistance return score enough_approved_candidates(profile, committeesize) opt_committees = [] opt_mavscore = profile.num_cand + 1 for comm in combinations(list(range(profile.num_cand)), committeesize): score = mavscore(comm, profile) if score < opt_mavscore: opt_committees = [comm] opt_mavscore = score elif mavscore(comm, profile) == opt_mavscore: opt_committees.append(comm) opt_committees = sort_committees(opt_committees) if resolute: return [opt_committees[0]] else: return sort_committees(opt_committees)
def compute_revseq_thiele_methods(profile, committeesize, scorefct_str): enough_approved_candidates(profile, committeesize) scorefct = sf.get_scorefct(scorefct_str, committeesize) allcandcomm = tuple(range(profile.num_cand)) comm_scores = { allcandcomm: sf.thiele_score(profile, allcandcomm, scorefct_str) } for _ in range(0, profile.num_cand - committeesize): comm_scores_next = {} for committee, score in comm_scores.items(): cands_to_remove, score_reduction = \ __least_relevant_cands(profile, committee, scorefct) for c in cands_to_remove: next_comm = tuple(set(committee) - set([c])) comm_scores_next[next_comm] = score - score_reduction # remove suboptimal committees comm_scores = {} cutoff = max(comm_scores_next.values()) for com, score in comm_scores_next.items(): if score >= cutoff: comm_scores[com] = score return sort_committees(list(comm_scores.keys()))
def compute_monroe_ilp(profile, committeesize, resolute): enough_approved_candidates(profile, committeesize) # Monroe is only defined for unit weights if not profile.has_unit_weights(): raise Exception("Monroe is only defined for unit weights (weight=1)") num_voters = len(profile.preferences) cands = list(range(profile.num_cand)) # Alternative: split voters -> generate new profile with all weights = 1 # prof2 = Profile(profile.num_cand) # for v in profile: # for _ in range(v.weight): # prof2.add_preference(DichotomousPreference(v.approved, # profile.num_cand)) # total_weight = profile.voters_num() m = gb.Model() # optimization goal: variable "satisfaction" satisfaction = m.addVar(vtype=gb.GRB.INTEGER, name="satisfaction") # a list of committee members in_committee = m.addVars(profile.num_cand, vtype=gb.GRB.BINARY, name="in_comm") m.addConstr(gb.quicksum(in_committee[c] for c in cands) == committeesize) # a partition of voters into committeesize many sets partition = m.addVars(profile.num_cand, len(profile.preferences), vtype=gb.GRB.INTEGER, lb=0, name="partition") for i in range(len(profile.preferences)): # every voter has to be part of a voter partition set m.addConstr( gb.quicksum(partition[(j, i)] for j in cands) == profile.preferences[i].weight) for i in cands: # every voter set in the partition has to contain # at least (num_voters // committeesize) candidates m.addConstr( gb.quicksum(partition[(i, j)] for j in range(len(profile.preferences))) >= (num_voters // committeesize - num_voters * (1 - in_committee[i]))) # every voter set in the partition has to contain # at most ceil(num_voters/committeesize) candidates m.addConstr( gb.quicksum(partition[(i, j)] for j in range(len(profile.preferences))) <= (num_voters // committeesize + bool(num_voters % committeesize) + num_voters * (1 - in_committee[i]))) # if in_committee[i] = 0 then partition[(i,j) = 0 m.addConstr( gb.quicksum( partition[(i, j)] for j in range(len(profile.preferences))) <= num_voters * in_committee[i]) m.update() # constraint for objective variable "satisfaction" m.addConstr( gb.quicksum(partition[(i, j)] * (i in profile.preferences[j].approved) for j in range(len(profile.preferences)) for i in cands) >= satisfaction) # optimization objective m.setObjective(satisfaction, gb.GRB.MAXIMIZE) # m.setParam('OutputFlag', False) m.setParam('OutputFlag', False) if resolute: m.setParam('PoolSearchMode', 0) else: # output all optimal committees m.setParam('PoolSearchMode', 2) # abort after (roughly) 100 optimal solutions m.setParam('PoolSolutions', 100) # ignore suboptimal committees m.setParam('PoolGap', 0) m.optimize() if m.Status != 2: print "Warning (Monroe): solutions may be incomplete or not optimal." print "(Gurobi return code", m.Status, ")" # extract committees from model committees = [] if resolute: committees.append([c for c in cands if in_committee[c].Xn >= 0.99]) else: for sol in range(m.SolCount): m.setParam('SolutionNumber', sol) committees.append([c for c in cands if in_committee[c].Xn >= 0.99]) # if len(committees)>10: # print "Warning (Monroe): more than 10 committees found;", # print "returning first 10" # committees = committees[:10] committees = sort_committees(committees) return committees
def compute_thiele_methods_ilp(profile, committeesize, scorefct_str, resolute=False): enough_approved_candidates(profile, committeesize) scorefct = sf.get_scorefct(scorefct_str, committeesize) m = gb.Model() cands = list(range(profile.num_cand)) # a binary variable indicating whether c is in the committee in_committee = m.addVars(profile.num_cand, vtype=gb.GRB.BINARY, name="in_comm") # a (intended binary) variable indicating # whether v approves at least l candidates in the committee utility = {} for v in profile.preferences: for l in range(1, committeesize + 1): utility[(v, l)] = m.addVar(ub=1.0) # constraint: the committee has the required size m.addConstr(gb.quicksum(in_committee[c] for c in cands) == committeesize) # constraint: utilities are consistent with actual committee for v in profile.preferences: m.addConstr( gb.quicksum(utility[v, l] for l in range(1, committeesize + 1)) == gb.quicksum( in_committee[c] for c in v.approved)) # objective: the PAV score of the committee m.setObjective( gb.quicksum( float(scorefct(l)) * v.weight * utility[(v, l)] for v in profile.preferences for l in range(1, committeesize + 1)), gb.GRB.MAXIMIZE) m.setParam('OutputFlag', False) if resolute: m.setParam('PoolSearchMode', 0) else: # output all optimal committees m.setParam('PoolSearchMode', 2) # abort after (roughly) 100 optimal solutions m.setParam('PoolSolutions', 100) # ignore suboptimal committees m.setParam('PoolGap', 0) m.optimize() if m.Status != 2: print "Warning (" + scorefct_str + "):", print "solutions may be incomplete or not optimal." print "(Gurobi return code", m.Status, ")" # extract committees from model committees = [] if resolute: committees.append([c for c in cands if in_committee[c].Xn >= 0.99]) else: for sol in range(m.SolCount): m.setParam('SolutionNumber', sol) committees.append([c for c in cands if in_committee[c].Xn >= 0.99]) committees = sort_committees(committees) return committees
def compute_minimaxav_ilp(profile, committeesize, resolute=False): enough_approved_candidates(profile, committeesize) voters = profile.preferences num_voters = len(voters) cands = list(range(profile.num_cand)) m = gb.Model() # optimization goal: variable "sum_difference" max_hamdistance = m.addVar(vtype=gb.GRB.INTEGER, name="max_hamdistance") # a list of committee members in_committee = m.addVars(profile.num_cand, vtype=gb.GRB.BINARY, name="in_comm") m.addConstr(gb.quicksum(in_committee[c] for c in cands) == committeesize) # the single differences between the committee and the voters difference = m.addVars(profile.num_cand, num_voters, vtype=gb.GRB.INTEGER, name="diff") for i in cands: for j in range(num_voters): if i in voters[j].approved: # constraint for the case that the candidate is approved m.addConstr(difference[i, j] == 1 - in_committee[i]) else: # constraint for the case that the candidate isn't approved m.addConstr(difference[i, j] == in_committee[i]) for j in range(num_voters): # maximum hamming distance is greater of equal than any individual one m.addConstr(max_hamdistance >= gb.quicksum(difference[i, j] for i in cands)) # optimization objective m.setObjective(max_hamdistance, gb.GRB.MINIMIZE) # m.setParam('OutputFlag', False) m.setParam('OutputFlag', False) if resolute: m.setParam('PoolSearchMode', 0) else: # output all optimal committees m.setParam('PoolSearchMode', 2) # abort after (roughly) 100 optimal solutions m.setParam('PoolSolutions', 1000) # ignore suboptimal committees m.setParam('PoolGap', 0) m.optimize() if m.Status != 2: print( "Warning (Minimax AV): solutions may be incomplete or not optimal." ) print("(Gurobi return code", m.Status, ")") # extract committees from model committees = [] if resolute: committees.append([c for c in cands if in_committee[c].Xn >= 0.99]) else: for sol in range(m.SolCount): m.setParam('SolutionNumber', sol) committees.append([c for c in cands if in_committee[c].Xn >= 0.99]) committees = sort_committees(committees) return committees
def compute_optphragmen_ilp(profile, committeesize, resolute=False): enough_approved_candidates(profile, committeesize) cands = list(range(profile.num_cand)) m = gb.Model() m.setParam('OutputFlag', False) # a binary variable indicating whether c is in the committee in_committee = m.addVars(profile.num_cand, vtype=gb.GRB.BINARY, name="in_comm") load = {} for c in cands: for v in profile.preferences: load[(v, c)] = m.addVar(ub=1.0, lb=0.0) # constraint: the committee has the required size m.addConstr(gb.quicksum(in_committee[c] for c in cands) == committeesize) for c in cands: for v in profile.preferences: if c not in v.approved: m.addConstr(load[(v, c)] == 0) # a candidate's load is distributed among his approvers for c in cands: m.addConstr( gb.quicksum(v.weight * load[(v, c)] for v in profile.preferences if c in cands) == in_committee[c]) loadbound = m.addVar(name="loadbound") for v in profile.preferences: m.addConstr(gb.quicksum(load[(v, c)] for c in v.approved) <= loadbound) m.setObjective(loadbound, gb.GRB.MINIMIZE) m.setParam('OutputFlag', False) if resolute: m.setParam('PoolSearchMode', 0) else: # output all optimal committees m.setParam('PoolSearchMode', 2) # abort after (roughly) 100 optimal solutions m.setParam('PoolSolutions', 100) # ignore suboptimal committees m.setParam('PoolGap', 0) m.optimize() if m.Status != 2: print("Warning (opt-Phragmen): solutions may be " + "incomplete or not optimal.") print("(Gurobi return code", m.Status, ")") # extract committees from model committees = [] if resolute: committees.append([c for c in cands if in_committee[c].Xn >= 0.99]) else: for sol in range(m.SolCount): m.setParam('SolutionNumber', sol) committees.append([c for c in cands if in_committee[c].Xn >= 0.99]) committees = sort_committees(committees) return committees
def compute_phragmen_enestroem(profile, committeesize, resolute=False): """"Returns the winning committees with Phragmen's first method (Enestroem's method) – STV with unordered ballots In every step the candidate with the highest combined budget of their supporters gets into a committee. For equal voting power multiple committees are computed. Method from: https://arxiv.org/pdf/1611.08826.pdf (18.5, Page 59) """ enough_approved_candidates(profile, committeesize) num_voters = len(profile.preferences) start_budget = { v: Fraction(profile.preferences[v].weight) for v in range(num_voters) } price = Fraction(sum(start_budget.values()), committeesize) cands = range(profile.num_cand) committees = [(start_budget, set())] for i 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.preferences): voting_power = budget[nr] if voting_power <= 0: continue for cand in pref.approved: 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) # new 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.preferences): if cand in pref.approved: b[nr] *= multiplier c = comm.union([cand]) # new committee with candidate next_committees.append((b, c)) if resolute: # only one is requested if len(next_committees) > 0: committees = [next_committees[0]] else: # should not happen committees = [] raise Exception( "phragmen enestroem failed to find " + "next candidate for", committees) else: committees = next_committees committees = [comm for b, comm in committees] committees = sort_committees(committees) if resolute: if len(committees) > 0: return [committees[0]] else: return [] else: return committees
def compute_rule_x(profile, committeesize, resolute=False): """Returns the list of winning candidates according to rule x. But rule x does stop if not enough budget is there to finance a candidate. As this is not optimal the committee is filled with the candidates that have the most remaining budget as support. Rule from: https://arxiv.org/pdf/1911.11747.pdf (Page 7)""" enough_approved_candidates(profile, committeesize) if not profile.has_unit_weights(): raise Exception("Rule X is only defined \ for unit weights (weight=1)") num_voters = len(profile.preferences) price = Fraction(num_voters, committeesize) start_budget = {v: Fraction(1, 1) for v in range(num_voters)} cands = range(profile.num_cand) committees = [(start_budget, set())] final_committees = [] for _ in range(committeesize): next_committees = [] for committee in committees: budget = committee[0] q_affordability = {} curr_cands = set(cands) - committee[1] for c in curr_cands: approved_by = set() for v, vote in enumerate(profile.preferences): if c in vote.approved and budget[v] > 0.0: approved_by.add(v) too_poor = set() already_available = Fraction(0) rich = set(approved_by) q = 0.0 while already_available < price and q == 0.0 and len(rich) > 0: fair_split = Fraction(price - already_available, len(rich)) still_rich = set() for v in rich: if budget[v] <= fair_split: too_poor.add(v) already_available += budget[v] else: still_rich.add(v) if len(still_rich) == len(rich): q = fair_split q_affordability[c] = q elif already_available == price: q = fair_split q_affordability[c] = q else: rich = still_rich if len(q_affordability) > 0: min_q = min(q_affordability.values()) cheapest_split = [ c for c in q_affordability if q_affordability[c] == min_q ] for c in cheapest_split: b = dict(committee[0]) for v, vote in enumerate(profile.preferences): if c in vote.approved: b[v] -= min(budget[v], min_q) comm = set(committee[1]) comm.add(c) next_committees.append((b, comm)) else: # no affordable candidate remains comms = fill_remaining_committee(committee, curr_cands, committeesize, profile) # after filling the remaining spots these committees # have size committeesize for b, comm in comms: final_committees.append(comm) if resolute: if len(next_committees) > 0: committees = [next_committees[0]] else: committees = [] else: committees = next_committees # The committees that could be fully filled with Rule X: for b, comm in committees: # budget and committee final_committees.append(comm) committees = sort_committees(final_committees) if resolute: if len(committees) > 0: return [committees[0]] else: return [] else: return committees