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 mavscore(profile, committee): score = 0 for pref in profile: hamdistance = hamming(pref, committee) if hamdistance > score: score = hamdistance return score
def num_voters_with_upper_bounded_hamming_distance(upperbound, profile, committee): """ Return the number of voters having a Hamming distance <= `upperbound` to the given committee. Parameters ---------- upperbound : int The Hamming distance upper bound. profile : abcvoting.preferences.Profile A profile. committee : set A committee. Returns ------- int The number of voters having a Hamming distance <= `upperbound`. """ return len([ voter for voter in profile if hamming(voter.approved, committee) <= upperbound ])
def mavscore(profile, committee): """Return the Minimax AV (MAV) score of a committee.""" score = 0 for voter in profile: hamdistance = hamming(voter.approved, committee) if hamdistance > score: score = hamdistance return score
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 minimaxav_score(profile, committee): """ Return the Minimax AV (MAV) score of a committee. Parameters ---------- profile : abcvoting.preferences.Profile A profile. committee : iterable of int A committee. Returns ------- int The Minimax AV score of `committee`. """ score = 0 for voter in profile: hamdistance = hamming(voter.approved, committee) if hamdistance > score: score = hamdistance return score
def test_hamming(a, b, dist): assert misc.hamming(a, b) == dist
def _gurobi_lexminimaxav(profile, committeesize, resolute, max_num_of_committees): def set_opt_model_func(model, in_committee): voteratmostdistances = {} for i, voter in enumerate(profile): for dist in range(profile.num_cand + 1): voteratmostdistances[(i, dist)] = model.addVar( vtype=gb.GRB.BINARY, name=f"atmostdistance({i, dist})") if dist >= len(voter.approved) + committeesize: # distances are always <= len(voter.approved) + committeesize voteratmostdistances[(i, dist)] = 1 if dist < abs(len(voter.approved) - committeesize): # distances are never < abs(len(voter.approved) - committeesize) voteratmostdistances[(i, dist)] = 0 # constraint: the committee has the required size model.addConstr(gb.quicksum(in_committee) == committeesize) # constraint: distances are consistent with actual committee for i, voter in enumerate(profile): not_approved = [ cand for cand in profile.candidates if cand not in voter.approved ] for dist in range(profile.num_cand + 1): if isinstance(voteratmostdistances[(i, dist)], int): # trivially satisfied continue model.addConstr((voteratmostdistances[(i, dist)] == 1) >> ( gb.quicksum(1 - in_committee[cand] for cand in voter.approved) + gb.quicksum(in_committee[cand] for cand in not_approved) <= dist)) # additional constraints from previous iterations for dist, num_voters_achieving_distance in hammingdistance_constraints.items( ): model.addConstr( gb.quicksum(voteratmostdistances[(i, dist)] for i, _ in enumerate(profile)) >= num_voters_achieving_distance - ACCURACY) new_distance = min(hammingdistance_constraints.keys()) - 1 # objective: maximize number of voters achieving at most distance `new_distance` model.setObjective( gb.quicksum(voteratmostdistances[(i, new_distance)] for i, _ in enumerate(profile)), gb.GRB.MAXIMIZE, ) # compute minimaxav as baseline and then improve on it committees = _gurobi_minimaxav(profile, committeesize, resolute=True, max_num_of_committees=None) maxdistance = scores.minimaxav_score(profile, committees[0]) # all voters have at most this distance hammingdistance_constraints = {maxdistance: len(profile)} for distance in range(maxdistance - 1, -1, -1): # in iteration `distance` we maximize the number of voters that have at # most a Hamming distance of `distance` to the committee if distance == 0: # last iteration _resolute = resolute _max_num_of_committees = max_num_of_committees else: _resolute = True _max_num_of_committees = None 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=f"lexminimaxav-atmostdistance{distance}", committeescorefct=functools.partial( scores.num_voters_with_upper_bounded_hamming_distance, distance), ) num_voters_achieving_distance = scores.num_voters_with_upper_bounded_hamming_distance( distance, profile, committees[0]) hammingdistance_constraints[distance] = num_voters_achieving_distance committees = sorted_committees(committees) detailed_info = { "hammingdistance_constraints": hammingdistance_constraints, "opt_distances": [misc.hamming(voter.approved, committees[0]) for voter in profile], } return committees, detailed_info