Summary
In RankedPairs._run_step, tied dominating tiers are resolved twice, inconsistently. The resolution stored in ElectionState.tiebreaks comes from tiebreak_set(tier, tiebreak=self.tiebreak), but the final ordering (and therefore who gets elected) is built separately with sort_candidates_pseudo_lexicographically, which ignores that resolution.
For tiebreak="lexicographic" (the default) the two happen to agree, so the bug is currently invisible.For any other setting, e.g. tiebreak="random", the reported resolution and the actual outcome can disagree.
Reproduction Code
from votekit.ballot import RankBallot
from votekit.pref_profile import RankProfile
from votekit.elections import RankedPairs
profile = RankProfile(
ballots=[
RankBallot(ranking=(frozenset({"A"}), frozenset({"B"})), weight=1),
RankBallot(ranking=(frozenset({"B"}), frozenset({"A"})), weight=1),
]
)
# A and B tie head to head, so the final digraph has them in one dominating tier.
# Repeat a few times since the random resolution lands on (A, B) half the time.
for _ in range(20):
e = RankedPairs(profile, tiebreak="random")
state = e.election_states[-1]
print(state.tiebreaks, state.elected)
The output then includes rounds like:
{frozenset({'A', 'B'}): (frozenset({'B'}), frozenset({'A'}))} (frozenset({'A'}),)
i.e. the state says the tie resolved with B first, but A was elected. Elected candidates are
always in lexicographic order regardless of tiebreak.
Summary
In RankedPairs._run_step, tied dominating tiers are resolved twice, inconsistently. The resolution stored in
ElectionState.tiebreakscomes fromtiebreak_set(tier, tiebreak=self.tiebreak), but the final ordering (and therefore who gets elected) is built separately withsort_candidates_pseudo_lexicographically, which ignores that resolution.For
tiebreak="lexicographic"(the default) the two happen to agree, so the bug is currently invisible.For any other setting, e.g. tiebreak="random", the reported resolution and the actual outcome can disagree.Reproduction Code
The output then includes rounds like:
{frozenset({'A', 'B'}): (frozenset({'B'}), frozenset({'A'}))} (frozenset({'A'}),)
i.e. the state says the tie resolved with B first, but A was elected. Elected candidates are
always in lexicographic order regardless of tiebreak.