def test_thiele_scores(scorefct_id, score, num_cand): profile = Profile(num_cand) approval_sets = [[0, 1], [1], [1, 3], [4], [1, 2, 3, 4, 5], [1, 5, 3], [0, 1, 2, 4, 5]] profile.add_voters(approval_sets) committee = [6, 7] assert scores.thiele_score(scorefct_id, profile, committee) == 0 committee = [1, 2, 3, 4] assert scores.thiele_score(scorefct_id, profile, committee) == score
def test_thiele_scores(scorefct_str, score, num_cand): profile = Profile(num_cand) preflist = [[0, 1], [1], [1, 3], [4], [1, 2, 3, 4, 5], [1, 5, 3], [0, 1, 2, 4, 5]] profile.add_preferences(preflist) committee = [6, 7] assert scores.thiele_score(scorefct_str, profile, committee) == 0 committee = [1, 2, 3, 4] assert scores.thiele_score(scorefct_str, profile, committee) == score
def __revseq_thiele_irresolute(profile, committeesize, scorefct_str): """Compute an *irresolute* sequential Thiele method Consider all possible ways to break ties between candidates (aka parallel universe tiebreaking) """ scorefct = scores.get_scorefct(scorefct_str, committeesize) allcandcomm = tuple(range(profile.num_cand)) comm_scores = {allcandcomm: scores.thiele_score( scorefct_str, profile, allcandcomm)} for _ in range(profile.num_cand - committeesize): comm_scores_next = {} for committee, score in comm_scores.items(): marg_util_cand = scores.marginal_thiele_scores_remove( scorefct, profile, committee) score_reduction = min(marg_util_cand) # find smallest elements in marg_util_cand and return indices cands_to_remove = [cand for cand in range(profile.num_cand) if marg_util_cand[cand] == min(marg_util_cand)] for c in cands_to_remove: next_comm = tuple(set(committee) - set([c])) comm_scores_next[next_comm] = score - score_reduction comm_scores = comm_scores_next return sort_committees(list(comm_scores.keys()))
def __revseq_thiele_resolute(profile, committeesize, scorefct_str, verbose): """Compute a *resolute* reverse sequential Thiele method Tiebreaking between candidates in favor of candidate with smaller number/index (candidates with smaller numbers are added first). """ scorefct = scores.get_scorefct(scorefct_str, committeesize) committee = set(range(profile.num_cand)) # optional output if verbose >= 2: output = "full committee (" + str(len(committee)) output += " candidates) has a total score of " output += str(scores.thiele_score( scorefct_str, profile, committee)) print(output + "\n") # end of optional output for _ in range(profile.num_cand - committeesize): marg_util_cand = scores.marginal_thiele_scores_remove( scorefct, profile, committee) score_reduction = min(marg_util_cand) # find smallest elements in marg_util_cand and return indices cands_to_remove = [cand for cand in range(profile.num_cand) if marg_util_cand[cand] == min(marg_util_cand)] committee.remove(cands_to_remove[-1]) # optional output if verbose >= 2: rem_cand = cands_to_remove[-1] output = "removing candidate number " output += str(profile.num_cand - len(committee)) + ": " output += profile.names[rem_cand] + "\n" output += " score decreases by " output += str(score_reduction) output += " to a total of " output += str(scores.thiele_score( scorefct_str, profile, committee)) if len(cands_to_remove) > 1: output += " (tie between candidates " output += str_candset(cands_to_remove) + ")\n" print(output + "\n") # end of optional output return [committee]
def __seq_thiele_resolute(profile, committeesize, scorefct_str, verbose): """Compute a *resolute* reverse sequential Thiele method Tiebreaking between candidates in favor of candidate with smaller number/index (candidates with larger numbers get deleted first). """ committee = [] scorefct = scores.get_scorefct(scorefct_str, committeesize) # optional output if verbose >= 2: output = "starting with the empty committee (score = " output += str(scores.thiele_score( scorefct_str, profile, committee)) + ")" print(output + "\n") # end of optional output # build a committee starting with the empty set for _ in range(committeesize): additional_score_cand = scores.marginal_thiele_scores_add( scorefct, profile, committee) next_cand = additional_score_cand.index(max(additional_score_cand)) committee.append(next_cand) # optional output if verbose >= 2: output = "adding candidate number " output += str(len(committee)) + ": " output += profile.names[next_cand] + "\n" output += " score increases by " output += str(max(additional_score_cand)) output += " to a total of " output += str(scores.thiele_score( scorefct_str, profile, committee)) tied_cands = [c for c in range(len(additional_score_cand)) if (c > next_cand and (additional_score_cand[c] == max(additional_score_cand)))] if len(tied_cands) > 0: output += " tie broken in favor of " + str(next_cand) output += " candidates " + str_candset(tied_cands) output += " would increase the score by the same amount (" output += str(max(additional_score_cand)) + ")" print(output + "\n") # end of optional output return [committee]
def __thiele_methods_branchandbound(profile, committeesize, scorefct_str, resolute): """Branch-and-bound algorithm to compute winning committees for Thiele methods""" enough_approved_candidates(profile, committeesize) scorefct = scores.get_scorefct(scorefct_str, committeesize) best_committees = [] init_com = compute_seq_thiele_method( profile, committeesize, scorefct_str, resolute=True)[0] best_score = scores.thiele_score(scorefct_str, profile, init_com) 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 = scores.thiele_score(scorefct_str, profile, part_com) 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 = scores.marginal_thiele_scores_add( scorefct, profile, part_com) upper_bound = ( sum(sorted(marg_util_cand[largest_cand + 1:])[-missing:]) + scores.thiele_score(scorefct_str, profile, part_com)) 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: committees = [committees[0]] 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_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
def _mip_lexcc(profile, committeesize, resolute, max_num_of_committees, solver_id): def set_opt_model_func(model, profile, in_committee, committeesize): # utility[(voter, x)] contains (intended binary) variables counting the number of approved # candidates in the selected committee by `voter`. This utility[(voter, x)] is true for # exactly the number of candidates in the committee approved by `voter` for all # x = 1...committeesize. utility = {} iteration = len(satisfaction_constraints) marginal_scorefcts = [ scores.get_marginal_scorefct(f"atleast{i + 1}") for i in range(iteration + 1) ] max_in_committee = {} for i, voter in enumerate(profile): # maximum number of approved candidates that this voter can have in a committee max_in_committee[voter] = min(len(voter.approved), committeesize) for x in range(1, max_in_committee[voter] + 1): utility[(voter, x)] = model.add_var(var_type=mip.BINARY, name=f"utility({i},{x})") # constraint: the committee has the required size model += mip.xsum(in_committee) == committeesize # constraint: utilities are consistent with actual committee for voter in profile: model += mip.xsum( utility[voter, x] for x in range(1, max_in_committee[voter] + 1)) == mip.xsum( in_committee[cand] for cand in voter.approved) # additional constraints from previous iterations for prev_iteration in range(iteration): model += (mip.xsum( float(marginal_scorefcts[prev_iteration](x)) * voter.weight * utility[(voter, x)] for voter in profile for x in range(1, max_in_committee[voter] + 1)) >= satisfaction_constraints[prev_iteration] - ACCURACY) # objective: the at-least-y score of the committee in iteration y model.objective = mip.maximize( mip.xsum( float(marginal_scorefcts[iteration](x)) * voter.weight * utility[(voter, x)] for voter in profile for x in range(1, max_in_committee[voter] + 1))) # proceed in `committeesize` many iterations to achieve lexicographic tie-breaking satisfaction_constraints = [] for iteration in range(1, committeesize): # in iteration x maximize the number of voters that have at least x approved candidates # in the committee committees = _optimize_rule_mip( set_opt_model_func=set_opt_model_func, profile=profile, committeesize=committeesize, resolute=resolute, max_num_of_committees=max_num_of_committees, solver_id=solver_id, name=f"lexcc-atleast{iteration}", committeescorefct=functools.partial(scores.thiele_score, f"atleast{iteration}"), reuse_model=False, # slower, but apparently necessary ) new_score = scores.thiele_score(f"atleast{iteration}", profile, committees[0]) if new_score == 0: satisfaction_constraints += [0] * (committeesize - 1 - len(satisfaction_constraints)) break satisfaction_constraints.append(new_score) iteration = committeesize committees = _optimize_rule_mip( set_opt_model_func=set_opt_model_func, profile=profile, committeesize=committeesize, resolute=resolute, max_num_of_committees=max_num_of_committees, solver_id=solver_id, name="lexcc-final", committeescorefct=functools.partial(scores.thiele_score, f"atleast{committeesize}"), reuse_model=False, # slower, but apparently necessary ) satisfaction_constraints.append( scores.thiele_score(f"atleast{iteration}", profile, committees[0])) detailed_info = {"opt_score_vector": satisfaction_constraints} return sorted_committees(committees), detailed_info
def _gurobi_lexcc(profile, committeesize, resolute, max_num_of_committees): def set_opt_model_func(model, in_committee): # utility[(voter, x)] contains (intended binary) variables counting the number of approved # candidates in the selected committee by `voter`. This utility[(voter, x)] is true for # exactly the number of candidates in the committee approved by `voter` for all # x = 1...committeesize. utility = {} iteration = len(satisfaction_constraints) scorefcts = [ scores.get_marginal_scorefct(f"atleast{i + 1}") for i in range(iteration + 1) ] max_in_committee = {} for i, voter in enumerate(profile): # maximum number of approved candidates that this voter can have in a committee max_in_committee[voter] = min(len(voter.approved), committeesize) for x in range(1, max_in_committee[voter] + 1): utility[(voter, x)] = model.addVar(vtype=gb.GRB.BINARY, name=f"utility({i, x})") # constraint: the committee has the required size model.addConstr(gb.quicksum(in_committee) == committeesize) # constraint: utilities are consistent with actual committee for voter in profile: model.addConstr( gb.quicksum(utility[voter, x] for x in range(1, max_in_committee[voter] + 1)) == gb.quicksum( in_committee[cand] for cand in voter.approved)) # additional constraints from previous iterations for prev_iteration in range(iteration): model.addConstr( gb.quicksum( float(scorefcts[prev_iteration](x)) * voter.weight * utility[(voter, x)] for voter in profile for x in range(1, max_in_committee[voter] + 1)) >= satisfaction_constraints[prev_iteration] - ACCURACY) # objective: the at-least-y score of the committee in iteration y model.setObjective( gb.quicksum( float(scorefcts[iteration](x)) * voter.weight * utility[(voter, x)] for voter in profile for x in range(1, max_in_committee[voter] + 1)), gb.GRB.MAXIMIZE, ) # proceed in `committeesize` many iterations to achieve lexicographic tie-breaking satisfaction_constraints = [] for iteration in range(1, committeesize): # in iteration x maximize the number of voters that have at least x approved candidates # in the committee committees, _ = _optimize_rule_gurobi( set_opt_model_func=set_opt_model_func, profile=profile, committeesize=committeesize, resolute=True, max_num_of_committees=None, name=f"lexcc-atleast{iteration}", committeescorefct=functools.partial(scores.thiele_score, f"atleast{iteration}"), ) satisfaction_constraints.append( scores.thiele_score(f"atleast{iteration}", profile, committees[0])) iteration = committeesize committees, _ = _optimize_rule_gurobi( set_opt_model_func=set_opt_model_func, profile=profile, committeesize=committeesize, resolute=resolute, max_num_of_committees=max_num_of_committees, name="lexcc-final", committeescorefct=functools.partial(scores.thiele_score, f"atleast{committeesize}"), ) satisfaction_constraints.append( scores.thiele_score(f"atleast{iteration}", profile, committees[0])) detailed_info = {"opt_score_vector": satisfaction_constraints} return sorted_committees(committees), detailed_info