Source code for mapof.elections.features.abc_features

try:
    from abcvoting.preferences import Profile
    from abcvoting import abcrules, properties
    from abcvoting.output import output, INFO
except ImportError:
    pass

try:
    import gurobipy as gb
except ImportError:
    pass

from mapof.elections.features.register import register_approval_election_feature

ACCURACY = 1e-8  # 1e-9 causes problems (some unit tests fail)
CMP_ACCURACY = 10 * ACCURACY  # when comparing float numbers obtained from a MIP


def _set_gurobi_model_parameters(model):
    model.setParam("OutputFlag", False)
    model.setParam("FeasibilityTol", ACCURACY)
    model.setParam("OptimalityTol", ACCURACY)
    model.setParam("IntFeasTol", ACCURACY)
    model.setParam("MIPGap", ACCURACY)
    model.setParam("PoolSearchMode", 0)
    model.setParam("MIPFocus", 2)  # focus more attention on proving optimality
    model.setParam("IntegralityFocus", 1)


def _check_core_gurobi(profile, committee, committeesize):

    if not gb:
        raise ImportError("Gurobi (gurobipy) not available.")

    model = gb.Model()

    set_of_voter = model.addVars(range(len(profile)), vtype=gb.GRB.BINARY)
    set_of_candidates = model.addVars(range(profile.num_cand), vtype=gb.GRB.BINARY)

    model.addConstr(
        gb.quicksum(set_of_candidates) * len(profile) <= gb.quicksum(set_of_voter) * committeesize)
    model.addConstr(gb.quicksum(set_of_voter) >= 1)
    for i, voter in enumerate(profile):
        approved = [(c in voter.approved) * set_of_candidates[i] for i, c in
                    enumerate(profile.candidates)]
        model.addConstr(
            (set_of_voter[i] == 1) >>
            (gb.quicksum(approved) >= len(voter.approved & committee) + 1)
        )

    _set_gurobi_model_parameters(model)
    model.optimize()

    if model.Status == gb.GRB.OPTIMAL:
        return False
    elif model.Status == gb.GRB.INFEASIBLE:
        return True
    else:
        raise RuntimeError(f"Gurobi returned an unexpected status code: {model.Status}")


def _check_priceability_gurobi(profile, committee, stable=False):

    if len(
            [cand for cand in profile.candidates if
             any(cand in voter.approved for voter in profile)]
    ) < len(committee):
        return True

    if not gb:
        raise ImportError("Gurobi (gurobipy) not available.")

    model = gb.Model()

    budget = model.addVar(vtype=gb.GRB.CONTINUOUS)
    payment = {}
    for voter in profile:
        payment[voter] = {}
        for candidate in profile.candidates:
            payment[voter][candidate] = model.addVar(vtype=gb.GRB.CONTINUOUS)

    # condition 1
    for voter in profile:
        model.addConstr(
            gb.quicksum(payment[voter][candidate] for candidate in profile.candidates) <= budget)

    # condition 2
    for voter in profile:
        for candidate in profile.candidates:
            if candidate not in voter.approved:
                model.addConstr(payment[voter][candidate] == 0)

    # condition 3
    for candidate in profile.candidates:
        if candidate in committee:
            model.addConstr(gb.quicksum(payment[voter][candidate] for voter in profile) == 1)
        else:
            model.addConstr(gb.quicksum(payment[voter][candidate] for voter in profile) == 0)

    if stable:
        # condition 4*
        for candidate in profile.candidates:
            if candidate not in committee:
                extrema = []
                for voter in profile:
                    if candidate in voter.approved:
                        extremum = model.addVar(vtype=gb.GRB.CONTINUOUS)
                        extrema.append(extremum)
                        r = model.addVar(vtype=gb.GRB.CONTINUOUS)
                        max_Payment = model.addVar(vtype=gb.GRB.CONTINUOUS)
                        model.addConstr(r == budget - gb.quicksum(
                            payment[voter][committee_member] for committee_member in committee))
                        model.addGenConstrMax(max_Payment,
                                              [payment[voter][committee_member] for committee_member
                                               in committee])
                        model.addGenConstrMax(extremum, [max_Payment, r])
                model.addConstr(
                    gb.quicksum(extrema) <= 1
                )
    else:
        # condition 4
        for candidate in profile.candidates:
            if candidate not in committee:
                model.addConstr(
                    gb.quicksum(
                        budget - gb.quicksum(
                            payment[voter][committee_member] for committee_member in committee)
                        for voter in profile if candidate in voter.approved
                    ) <= 1)

    model.setObjective(budget)
    _set_gurobi_model_parameters(model)
    model.optimize()

    if model.Status == gb.GRB.OPTIMAL:
        output.details(f"Budget: {budget.X}")

        column_widths = {candidate: max(len(str(payment[voter][candidate].X)) for voter in payment)
                         for candidate in profile.candidates}
        column_widths["voter"] = len(str(len(profile)))
        output.details(" " * column_widths["voter"] + " | " + " | ".join(
            str(i).rjust(column_widths[candidate]) for i, candidate in
            enumerate(profile.candidates)))
        for i, voter in enumerate(profile):
            output.details(str(i).rjust(column_widths["voter"]) + " | " + " | ".join(
                str(pay.X).rjust(column_widths[candidate]) for candidate, pay in
                payment[voter].items()))

        return True
    elif model.Status == gb.GRB.INFEASIBLE:
        output.details(f"No feasible budget and payment function")
        return False
    else:
        raise RuntimeError(f"Gurobi returned an unexpected status code: {model.Status}")


[docs] @register_approval_election_feature("priceability", has_params=True, is_rule_related=True) def get_priceability(election, feature_params): """ Computes priceability using ABC Python package. """ rule = feature_params['rule'] profile = Profile(election.num_candidates) profile.add_voters(election.votes) committee = election.winning_committee[rule] return {'value': int(_check_priceability_gurobi(profile, committee))}
[docs] @register_approval_election_feature("core", has_params=True, is_rule_related=True) def get_core(election, feature_params): """ Computes the core using ABC Python package. """ rule = feature_params['rule'] profile = Profile(election.num_candidates) profile.add_voters(election.votes) committee = election.winning_committee[rule] return {'value': int(_check_core_gurobi(profile, committee, feature_params['committee_size']))}