Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement EJR+ #62

Merged
merged 4 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 118 additions & 13 deletions abcvoting/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@


ACCURACY = 1e-8 # 1e-9 causes problems (some unit tests fail)
PROPERTY_NAMES = ["pareto", "jr", "pjr", "ejr", "priceability", "stable-priceability", "core"]
PROPERTY_NAMES = [
"pareto",
"jr",
"pjr",
"ejr",
"ejr+",
"priceability",
"stable-priceability",
"core",
]


def _set_gurobi_model_parameters(model):
Expand Down Expand Up @@ -57,6 +66,7 @@ def full_analysis(profile, committee):
"jr": "Justified representation (JR)",
"pjr": "Proportional justified representation (PJR)",
"ejr": "Extended justified representation (EJR)",
"ejr+": "EJR+",
"priceability": "Priceability",
"stable-priceability": "Stable Priceability",
"core": "The core",
Expand Down Expand Up @@ -102,6 +112,8 @@ def check(property_name, profile, committee, algorithm="fastest"):
return check_PJR(profile, committee, algorithm=algorithm)
elif property_name == "ejr":
return check_EJR(profile, committee, algorithm=algorithm)
elif property_name == "ejr+":
return check_EJR_plus(profile, committee)
elif property_name == "priceability":
return check_priceability(profile, committee, algorithm=algorithm)
elif property_name == "stable-priceability":
Expand Down Expand Up @@ -444,6 +456,42 @@ def check_JR(profile, committee):
return result


def _check_JR(profile, committee):
"""
Test whether a committee satisfies JR.

Uses the polynomial-time algorithm proposed by Aziz et.al (2017).

Parameters
----------
profile : abcvoting.preferences.Profile
A profile.
committee : iterable of int
A committee.

Returns
-------
bool
"""

for cand in profile.candidates:
group = set()
for vi, voter in enumerate(profile):
# if current candidate appears in this voter's ballot AND
# this voter's approval ballot does NOT intersect with input committee
if (cand in voter.approved) and (len(voter.approved & committee) == 0):
group.add(vi)

if len(group) * len(committee) >= len(profile): # |group| >= num_voters / |committee|
detailed_information = {"cohesive_group": group, "joint_candidate": cand}
return False, detailed_information

# if function has not yet returned by now, then this means no such candidate
# exists. Then input committee must satisfy JR wrt the input profile
detailed_information = {}
return True, detailed_information


def _check_pareto_optimality_brute_force(profile, committee):
"""
Test using brute-force whether a committee is Pareto optimal.
Expand Down Expand Up @@ -910,11 +958,55 @@ def _check_PJR_gurobi(profile, committee):
raise RuntimeError(f"Gurobi returned an unexpected status code: {model.Status}")


def _check_JR(profile, committee):
def check_EJR_plus(profile, committee):
"""
Test whether a committee satisfies JR.
Test whether a committee satisfies EJR+.

Uses the polynomial-time algorithm proposed by Aziz et.al (2017).
Parameters
----------
profile : abcvoting.preferences.Profile
A profile.
committee : iterable of int
A committee.

Returns
-------
bool

References
----------
Brill, M., & Peters, J. (2023).
Robust and Verifiable Proportionality Axioms for Multiwinner Voting.
https://arxiv.org/abs/2302.01989
"""

# check that `committee` is a valid input
committee = CandidateSet(committee, num_cand=profile.num_cand)

result, detailed_information = _check_EJR_plus(profile, committee)

if result:
output.info(f"Committee {str_set_of_candidates(committee)} satisfies EJR+.")
else:
output.info(f"Committee {str_set_of_candidates(committee)} does not satisfy EJR+.")
cand = detailed_information["joint_candidate"]
cohesive_group = detailed_information["cohesive_group"]
ell = detailed_information["ell"]
output.details(
f"(The group of voters {str_set_of_candidates(cohesive_group)}"
f" ({len(cohesive_group)/len(profile)*100:.1f}% of all voters) deserves {ell} candidates,"
f" and jointly approve candidate {profile.cand_names[cand]} which is not part of the committee,"
f" but no member approves at least {ell} members of the committee.)"
)

return result


def _check_EJR_plus(profile, committee):
"""
Test whether a committee satisfies EJR+.

Uses the polynomial-time algorithm proposed by Brill and Peters (2023).

Parameters
----------
Expand All @@ -929,19 +1021,32 @@ def _check_JR(profile, committee):
"""

for cand in profile.candidates:
group = set()
if cand in committee:
continue
supporters_by_utility = {ell: set() for ell in range(len(committee) + 1)}
for vi, voter in enumerate(profile):
# if current candidate appears in this voter's ballot AND
# this voter's approval ballot does NOT intersect with input committee
if (cand in voter.approved) and (len(voter.approved & committee) == 0):
group.add(vi)
if cand in voter.approved:
utility = len(voter.approved & committee)
supporters_by_utility[utility].add(vi)

if len(group) * len(committee) >= len(profile): # |group| >= num_voters / |committee|
detailed_information = {"cohesive_group": group, "joint_candidate": cand}
return False, detailed_information
group = set()
for ell in range(len(committee)):
# group of supporters of cand with utility <= ell
group |= supporters_by_utility[ell]
if len(group) * len(committee) >= (ell + 1) * len(
profile
): # |group| >= (ell + 1) * num_voters / |committee|
# EJR+ requires someone to get utility at least ell + 1, but no one does
detailed_information = {
"cohesive_group": group,
"joint_candidate": cand,
"ell": ell + 1,
}
return False, detailed_information

# if function has not yet returned by now, then this means no such candidate
# exists. Then input committee must satisfy JR wrt the input profile
# has sufficiently many sufficiently unsatisfied supporters.
# Then input committee must satisfy EJR+
detailed_information = {}
return True, detailed_information

Expand Down
2 changes: 2 additions & 0 deletions tests/expected_output/test_axiomatic_properties
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Pareto optimality : True
Justified representation (JR) : True
Proportional justified representation (PJR) : True
Extended justified representation (EJR) : True
EJR+ : True
Priceability : False
Stable Priceability : False
The core : False
Expand All @@ -36,6 +37,7 @@ Pareto optimality : True
Justified representation (JR) : True
Proportional justified representation (PJR) : True
Extended justified representation (EJR) : True
EJR+ : True
Priceability : True
Stable Priceability : True
The core : True
Expand Down
45 changes: 44 additions & 1 deletion tests/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,42 @@ def _create_handcrafted_instances():
expected_result = False
handcrafted_instances.append(("ejr", profile, committee, expected_result))

# EJR+
# Brill and Peters, 2023, "Robust and Verifiable Proportionality Axioms for Multiwinner Voting", Example 3 left
profile = Profile(7)
profile.add_voters(
[[0, 1, 2]]
+ [[0, 1, 2, 3]] * 2
+ [[0, 1, 2, 3, 4]]
+ [[2, 3, 4]]
+ [[2, 3, 4, 5]]
+ [[2, 3, 4, 5]]
+ [[3, 4, 5, 6]]
)
committee = {0, 2, 4, 6}
expected_result = True
handcrafted_instances.append(("ejr", profile, committee, expected_result))
expected_result = False
handcrafted_instances.append(("ejr+", profile, committee, expected_result))

# Brill and Peters, 2023, "Robust and Verifiable Proportionality Axioms for Multiwinner Voting", Example 3 right
profile = Profile(7)
profile.add_voters([[0]] + [[0, 1]] + [[0, 1, 2, 3]] + [[2, 3, 4]] * 3 + [[4, 5]] + [[5, 6]])
committee = {0, 1, 2, 6}
expected_result = True
handcrafted_instances.append(("ejr", profile, committee, expected_result))
expected_result = False
handcrafted_instances.append(("ejr+", profile, committee, expected_result))

# Brill and Peters, 2023, "Robust and Verifiable Proportionality Axioms for Multiwinner Voting", Remark 2 (core does not imply EJR+)
profile = Profile(3)
profile.add_voters([[0, 1]] + [[0, 2]])
committee = {1, 2}
expected_result = True
handcrafted_instances.append(("core", profile, committee, expected_result))
expected_result = False
handcrafted_instances.append(("ejr+", profile, committee, expected_result))

# add an instance from
# Sanchez-Fernandez et al, 2017, "Proportional Justified Representation", Example 1
profile = Profile(8)
Expand Down Expand Up @@ -202,6 +238,11 @@ def _create_handcrafted_instances():
expected_result = False
handcrafted_instances.append(("jr", profile, committee, expected_result))

# EJR+ implies EJR
for property_name, profile, committee, expected_result in handcrafted_instances:
if property_name == "ejr+" and expected_result:
handcrafted_instances.append(("ejr", profile, committee, expected_result))

# EJR implies PJR
for property_name, profile, committee, expected_result in handcrafted_instances:
if property_name == "ejr" and expected_result:
Expand Down Expand Up @@ -239,7 +280,7 @@ def test_property_functions_with_handcrafted_instances(
property_name, algorithm, profile, committee, expected_result
):
if algorithm == "nonsense":
if property_name == "jr":
if property_name in ["jr", "ejr+"]:
return # no `algorithm` parameter
with pytest.raises(NotImplementedError):
properties.check(property_name, profile, committee, algorithm=algorithm)
Expand Down Expand Up @@ -298,6 +339,7 @@ def test_matching_output_different_approaches(abc_yaml_instance):
("pav", "jr"),
("pav", "pjr"),
("pav", "ejr"),
("pav", "ejr+"),
("slav", "pareto"),
("cc", "jr"),
("geom2", "pareto"),
Expand All @@ -311,6 +353,7 @@ def test_matching_output_different_approaches(abc_yaml_instance):
("equal-shares", "jr"),
("equal-shares", "pjr"),
("equal-shares", "ejr"),
("equal-shares", "ejr+"),
("equal-shares", "priceability"),
("phragmen-enestroem", "jr"),
("phragmen-enestroem", "pjr"),
Expand Down