Exemplo n.º 1
0
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
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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()))
Exemplo n.º 4
0
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]
Exemplo n.º 5
0
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]
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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
Exemplo n.º 10
0
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