Source code for pref_voting.other_axioms
"""
File: other_axioms.py
Date: April 29 2025
Authors: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
Other axioms
---------------------
"""
import numpy as np
from pref_voting.axiom import Axiom
from pref_voting.profiles import Profile
from pref_voting.profiles_with_ties import ProfileWithTies
from pref_voting.rankings import Ranking
from pref_voting.weighted_majority_graphs import MarginGraph
def _reverse_ranking(ballot, all_cands):
"""
Reverse a single ballot, treating *unranked* candidates as
tied for last before the reversal.
Parameters
----------
ballot : tuple | Ranking
• tuple – a strict linear order
• Ranking – weak order, possibly incomplete
all_cands : list | set
The full candidate set of the profile.
"""
if isinstance(ballot, tuple):
return tuple(reversed(ballot))
if isinstance(ballot, Ranking):
full_rmap = ballot.rmap.copy()
if full_rmap:
last_rank = max(full_rmap.values()) + 1
else:
last_rank = 1
for c in all_cands:
if c not in full_rmap:
full_rmap[c] = last_rank
max_rank = max(full_rmap.values())
rev_rmap = {c: max_rank + 1 - k for c, k in full_rmap.items()}
return Ranking(rev_rmap)
def _reverse_profile(P):
"""
Build the reversed profile ``Pᵣ``.
"""
all_cands = P.candidates
rev_ballots = [_reverse_ranking(b, all_cands) for b in P.rankings]
if isinstance(P, Profile):
return Profile(rev_ballots)
if isinstance(P, ProfileWithTies):
P_r = ProfileWithTies(rev_ballots, candidates=all_cands)
if P.using_extended_strict_preference:
P_r.use_extended_strict_preference()
return P_r
def _reverse_margin_graph(mg):
"""
Return the *edge-reversed* margin graph.
All positive-margin edges (u, v, w) become (v, u, w); weights are preserved.
"""
rev_edges = [(v, u, w) for (u, v, w) in mg.edges]
return MarginGraph(mg.candidates[:], rev_edges, cmap=mg.cmap)
[docs]
def has_reversal_symmetry_violation(edata, vm, verbose=False):
"""
Returns True iff ``vm`` violates reversal symmetry on *edata*.
Reversal Symmetry states that if x is a **unique** winner in ``edata``,
then x should not be among the winners in the reversal of ``edata``.
"""
if len(edata.candidates) <= 1:
return False
if isinstance(edata, MarginGraph):
mg = edata
winners = vm(mg)
if len(winners) != 1:
return False
x = winners[0]
mg_r = _reverse_margin_graph(mg)
rev_winners = vm(mg_r)
if x in rev_winners:
if verbose:
print(f"Reversal-symmetry violation for {vm.name} on a MarginGraph")
print(f"Unique winner {x} also wins after edge reversal.")
print("\nOriginal margin graph:")
mg.display()
print(mg.description())
vm.display(mg)
print("\nReversed margin graph:")
mg_r.display()
print(mg_r.description())
vm.display(mg_r)
return True
return False
winners = vm(edata)
if isinstance(winners, np.ndarray):
winners = winners.tolist()
if len(winners) != 1:
return False
x = winners[0]
P_r = _reverse_profile(edata)
rev_winners = vm(P_r)
if isinstance(rev_winners, np.ndarray):
rev_winners = rev_winners.tolist()
if x in rev_winners:
if verbose:
print(f"Reversal-symmetry violation for {vm.name}")
print(f"Unique winner {x} also wins after reversal.")
print("\nOriginal profile:")
edata.display()
print(edata.description())
vm.display(edata)
print("\nReversed profile:")
P_r.display()
print(P_r.description())
vm.display(P_r)
return True
return False
[docs]
def find_all_reversal_symmetry_violations(edata, vm, verbose=False):
"""
Returns a one-item list [(unique_winner, winners_after_reversal)] describing the violation on *edata*, or [].
"""
if not has_reversal_symmetry_violation(edata, vm, verbose):
return []
winners = vm(edata)
winners = winners.tolist() if isinstance(winners, np.ndarray) else winners
if isinstance(edata, MarginGraph):
rev_winners = vm(_reverse_margin_graph(edata))
else:
rev_winners = vm(_reverse_profile(edata))
rev_winners = (
rev_winners.tolist() if isinstance(rev_winners, np.ndarray) else rev_winners
)
return [(winners, rev_winners)]
reversal_symmetry = Axiom(
"Reversal Symmetry",
has_violation=has_reversal_symmetry_violation,
find_all_violations=find_all_reversal_symmetry_violations,
)