Skip to content
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
3 changes: 2 additions & 1 deletion src/vse_sim/methods/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from .borda import Borda, RankedMethod, RatedMethod
from .borda import Borda
from .bullety_approval import BulletyApprovalWith
from .irnr import IRNR
from .irv import Irv
from .irv_prime import IrvPrime
from .mav import Mav, toVote
from .mj import Mj
from .plurality import Plurality
from .ranked import RankedMethod, RatedMethod
from .ranked_pairs import Rp
from .schulze import Schulze
from .score import Score
Expand Down
71 changes: 2 additions & 69 deletions src/vse_sim/methods/borda.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,14 @@
from numpy import mean

from ..core import Method, rememberBallot
from ..voter_models import Voter # noqa: F401
from .ranked import RankedMethod


class Borda(Method):
class Borda(RankedMethod):
"""Implement Borda count with larger rank values representing preference.

Honest ballots assign consecutive scores from least to most preferred.
Ranked methods also inherit this class's ballot construction and strategy
helpers.
"""

candScore = staticmethod(mean)

nRanks = 999 # infinity

@staticmethod
def fillPrefOrder(voter, ballot,
whichCands=None, #None means "all"; otherwise, an iterable of cand indexes
lowSlot=0,
nSlots=None, #again, None means "all"
remainderScore=None #what to give candidates that don't fit in nSlots
):

venum = list(enumerate(voter))
if whichCands:
venum = [venum[c] for c in whichCands]
prefOrder = sorted(venum,key=lambda x:-x[1]) #high to low
Borda.fillCands(ballot, prefOrder, lowSlot, nSlots, remainderScore)
#modifies ballot argument, returns nothing.

@staticmethod
def fillCands(ballot,
whichCands, #list of tuples starting with cand id, in descending order
lowSlot=0,
nSlots=None, #again, None means "all"
remainderScore=None #what to give candidates that don't fit in nSlots
):
if nSlots is None:
nSlots = len(whichCands)
cur = lowSlot + nSlots - 1
for i in range(nSlots):
ballot[whichCands[i][0]] = cur
cur -= 1
if remainderScore is not None:
i += 1
while i < len(whichCands):
ballot[whichCands[i][0]] = remainderScore
i += 1
#modifies ballot argument, returns nothing.

@staticmethod #cls is provided explicitly, not through binding
@rememberBallot
def honBallot(cls, utils):
ballot = [0] * len(utils)
cls.fillPrefOrder(utils, ballot)
return ballot


@classmethod
def fillStratBallot(cls, voter, polls, places, n, stratGap, ballot,
frontId, frontResult, targId, targResult):
"""Mutates the `ballot` argument to be a strategic ballot.

>>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
[3, 0, 1, 2]
"""
nRanks = min(cls.nRanks,n)
if stratGap <= 0:
ballot[frontId], ballot[targId] = (nRanks - 1), 0
else:
ballot[frontId], ballot[targId] = 0, (nRanks - 1)
nRanks -= 2
if nRanks > 0:
cls.fillCands(ballot, places[2:][::-1],
lowSlot=1, nSlots=nRanks, remainderScore=0)

RankedMethod = Borda #alias
RatedMethod = RankedMethod #Should have same strategies available, plus more
2 changes: 1 addition & 1 deletion src/vse_sim/methods/irnr.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ..core import Method, rememberBallot
from .borda import RankedMethod
from .ranked import RankedMethod


class IRNR(RankedMethod):
Expand Down
5 changes: 4 additions & 1 deletion src/vse_sim/methods/plurality.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from numpy import mean

from ..core import rememberBallot
from ..voter_models import Voter # noqa: F401
from .borda import RankedMethod
from .ranked import RankedMethod


class Plurality(RankedMethod):
Expand All @@ -10,6 +12,7 @@ class Plurality(RankedMethod):
and every other candidate receives zero.
"""

candScore = staticmethod(mean)
nRanks = 2

@staticmethod
Expand Down
88 changes: 88 additions & 0 deletions src/vse_sim/methods/ranked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from ..core import Method, rememberBallot


class RankedMethod(Method):
"""Base class for methods that use candidate-aligned rank vectors.

Larger ballot values represent stronger preferences. The class provides
shared helpers for constructing complete or truncated ranked ballots and
the default ranked-method strategy used by Borda and plurality.
"""

@staticmethod
def fillPrefOrder(
voter,
ballot,
whichCands=None,
lowSlot=0,
nSlots=None,
remainderScore=None,
):
"""Fill ``ballot`` with candidates ordered by decreasing utility."""
venum = list(enumerate(voter))
if whichCands:
venum = [venum[c] for c in whichCands]
prefOrder = sorted(venum, key=lambda x: -x[1])
RankedMethod.fillCands(
ballot, prefOrder, lowSlot, nSlots, remainderScore
)

@staticmethod
def fillCands(
ballot,
whichCands,
lowSlot=0,
nSlots=None,
remainderScore=None,
):
"""Assign descending ranks to candidate tuples in ``whichCands``."""
if nSlots is None:
nSlots = len(whichCands)
cur = lowSlot + nSlots - 1
for i in range(nSlots):
ballot[whichCands[i][0]] = cur
cur -= 1
if remainderScore is not None:
for candidate, *_ in whichCands[nSlots:]:
ballot[candidate] = remainderScore

@staticmethod
@rememberBallot
def honBallot(cls, utils):
"""Return a complete rank vector ordered by utility."""
ballot = [0] * len(utils)
cls.fillPrefOrder(utils, ballot)
return ballot

@classmethod
def fillStratBallot(
cls,
voter,
polls,
places,
n,
stratGap,
ballot,
frontId,
frontResult,
targId,
targResult,
):
"""Mutate ``ballot`` with the default strategy for ranked methods."""
nRanks = min(cls.nRanks, n)
if stratGap <= 0:
ballot[frontId], ballot[targId] = (nRanks - 1), 0
else:
ballot[frontId], ballot[targId] = 0, (nRanks - 1)
nRanks -= 2
if nRanks > 0:
cls.fillCands(
ballot,
places[2:][::-1],
lowSlot=1,
nSlots=nRanks,
remainderScore=0,
)


RatedMethod = RankedMethod
2 changes: 1 addition & 1 deletion src/vse_sim/methods/schulze.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from numpy import sign

from ..voter_models import DeterministicModel # noqa: F401
from .borda import RankedMethod
from .ranked import RankedMethod


class Schulze(RankedMethod):
Expand Down
41 changes: 40 additions & 1 deletion tests/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,51 @@
from scripts.recalculate_irv_pages import recalculate
from vse_sim.core import SideTally
from vse_sim.diagnostics import TRACE, setDebug, trace
from vse_sim.methods import Irv, Mav, Schulze, Score
from vse_sim.methods import (
Borda,
Irv,
Mav,
Plurality,
RankedMethod,
RatedMethod,
Schulze,
Score,
)
from vse_sim.simulation import CsvBatch, seedRandomGenerators
from vse_sim.strategies import ProbChooser, beHon, beStrat
from vse_sim.voter_models import Electorate, Voter


def test_ranked_method_is_a_base_class_separate_from_borda():
assert RankedMethod is not Borda
assert RatedMethod is RankedMethod
assert issubclass(Borda, RankedMethod)
assert issubclass(Schulze, RankedMethod)
assert Borda.honBallot(Borda, Voter([4, 1, 6, 3])) == [2, 0, 3, 1]
assert Schulze.honBallot(Schulze, Voter([4, 1, 6, 3])) == [2, 0, 3, 1]
assert Borda().stratBallotFor([4, 5, 2, 1])(
Borda, Voter([-4, -5, -2, -1])
) == [3, 0, 1, 2]
assert Plurality().results([[1, 0], [1, 0], [0, 1]]) == [
pytest.approx(2 / 3),
pytest.approx(1 / 3),
]


def test_ranked_fill_candidates_handles_zero_slots():
ballot = [None, None]

RankedMethod.fillCands(
ballot,
[(0, 2), (1, 1)],
nSlots=0,
remainderScore=0,
)

assert ballot == [0, 0]
RankedMethod.fillCands([], [], nSlots=0, remainderScore=0)


def test_schulze_uses_independent_strongest_path_rows():
margins = [
[0, -3, 1],
Expand Down