Skip to content

Commit

Permalink
Merge pull request #89 from mggg/ties_stv
Browse files Browse the repository at this point in the history
Ties stv
  • Loading branch information
jgibson517 authored Aug 29, 2023
2 parents 798fef5 + d793e3d commit 8045e2a
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 93 deletions.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ build-backend = "poetry.core.masonry.api"

[tool.ruff]
line-length = 100

[project.urls]
repository = "https://github.com/mggg/VoteKit"
3 changes: 3 additions & 0 deletions src/votekit/ballot.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ def __eq__(self, other):
return False

return True

def __hash__(self):
return hash(str(self.ranking))
146 changes: 64 additions & 82 deletions src/votekit/election_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Callable, Optional

from .ballot import Ballot
from .models import Election
from .election_state import ElectionState
from .graphs.pairwise_comparison_graph import PairwiseComparisonGraph
from .pref_profile import PreferenceProfile
Expand All @@ -18,7 +19,7 @@
)


class STV:
class STV(Election):
"""
Class for single-winner IRV and multi-winner STV elections
"""
Expand All @@ -29,66 +30,57 @@ def __init__(
transfer: Callable,
seats: int,
quota: str = "droop",
ties: bool = True,
):
"""
profile (PreferenceProfile): initial perference profile
transfer (function): vote transfer method such as fractional transfer
seats (int): number of winners/size of committee
"""
self.__profile = profile
# let parent class handle the og profile and election state
super().__init__(profile, ties)

self.transfer = transfer
self.seats = seats
self.election_state = ElectionState(
curr_round=0,
elected=[],
eliminated=[],
remaining=[
cand
for cand, votes in compute_votes(
profile.get_candidates(), profile.get_ballots()
)
],
profile=profile,
)
self.threshold = self.get_threshold(quota)

# can cache since it will not change throughout rounds
def get_threshold(self, quota: str) -> int:
quota = quota.lower()
if quota == "droop":
return int(self.__profile.num_ballots() / (self.seats + 1) + 1)
return int(self._profile.num_ballots() / (self.seats + 1) + 1)
elif quota == "hare":
return int(self.__profile.num_ballots() / self.seats)
return int(self._profile.num_ballots() / self.seats)
else:
raise ValueError("Misspelled or unknown quota type")

def next_round(self) -> bool:
"""
Determines if the number of seats has been met to call election
"""
return len(self.election_state.get_all_winners()) != self.seats
return len(self.state.get_all_winners()) != self.seats

def run_step(self) -> ElectionState:
"""
Simulates one round an STV election
"""
##TODO:must change the way we pass winner_votes
remaining: list[str] = self.election_state.remaining
ballots: list[Ballot] = self.election_state.profile.get_ballots()
remaining: list[str] = self.state.profile.get_candidates()
ballots: list[Ballot] = self.state.profile.get_ballots()
fp_votes = compute_votes(remaining, ballots) ##fp means first place
elected = []
eliminated = []

# if number of remaining candidates equals number of remaining seats,
# everyone is elected
if len(remaining) == self.seats - len(self.election_state.get_all_winners()):
if len(remaining) == self.seats - len(self.state.get_all_winners()):
elected = [cand for cand, votes in fp_votes]
remaining = []
ballots = []
# TODO: sort remaining candidates by vote share

# elect all candidates who crossed threshold
elif fp_votes[0][1] >= self.threshold:
elif fp_votes[0].votes >= self.threshold:
for candidate, votes in fp_votes:
if votes >= self.threshold:
elected.append(candidate)
Expand All @@ -112,15 +104,15 @@ def run_step(self) -> ElectionState:
ballots = remove_cand(lp_cand, ballots)
remaining.remove(lp_cand)

self.election_state = ElectionState(
curr_round=self.election_state.curr_round + 1,
self.state = ElectionState(
curr_round=self.state.curr_round + 1,
elected=elected,
eliminated=eliminated,
remaining=remaining,
profile=PreferenceProfile(ballots=ballots),
previous=self.election_state,
previous=self.state,
)
return self.election_state
return self.state

def run_election(self) -> ElectionState:
"""
Expand All @@ -134,12 +126,14 @@ def run_election(self) -> ElectionState:
while self.next_round():
self.run_step()

return self.election_state
return self.state


class Limited:
def __init__(self, profile: PreferenceProfile, seats: int, k: int):
self.state = ElectionState(curr_round=0, profile=profile)
class Limited(Election):
def __init__(
self, profile: PreferenceProfile, seats: int, k: int, ties: bool = True
):
super().__init__(profile, ties)
self.seats = seats
self.k = k

Expand Down Expand Up @@ -207,9 +201,9 @@ def run_election(self) -> ElectionState:
return outcome


class Bloc:
def __init__(self, profile: PreferenceProfile, seats: int):
self.state = ElectionState(curr_round=0, profile=profile)
class Bloc(Election):
def __init__(self, profile: PreferenceProfile, seats: int, ties: bool = True):
super().__init__(profile, ties)
self.seats = seats

"""Bloc: This rule returns the m candidates with the highest m-approval scores.
Expand All @@ -228,9 +222,9 @@ def run_election(self) -> ElectionState:
return outcome


class SNTV:
def __init__(self, profile: PreferenceProfile, seats: int):
self.state = ElectionState(curr_round=0, profile=profile)
class SNTV(Election):
def __init__(self, profile: PreferenceProfile, seats: int, ties: bool = True):
super().__init__(profile, ties)
self.seats = seats

"""Single nontransferable vote (SNTV): SNTV returns k candidates with the highest
Expand All @@ -246,11 +240,16 @@ def run_election(self) -> ElectionState:
return outcome


class SNTV_STV_Hybrid:
class SNTV_STV_Hybrid(Election):
def __init__(
self, profile: PreferenceProfile, transfer: Callable, r1_cutoff: int, seats: int
self,
profile: PreferenceProfile,
transfer: Callable,
r1_cutoff: int,
seats: int,
ties: bool = True,
):
self.state = ElectionState(curr_round=0, profile=profile)
super().__init__(profile, ties)
self.transfer = transfer
self.r1_cutoff = r1_cutoff
self.seats = seats
Expand Down Expand Up @@ -310,9 +309,9 @@ def run_election(self) -> ElectionState:
return outcome # type: ignore


class TopTwo:
def __init__(self, profile: PreferenceProfile):
self.state = ElectionState(curr_round=0, profile=profile)
class TopTwo(Election):
def __init__(self, profile: PreferenceProfile, ties: bool = True):
super().__init__(profile, ties)

"""Top Two: Top two eliminates all but the top two plurality vote getters,
and then conducts a runoff between them, reallocating other ballots."""
Expand All @@ -334,9 +333,9 @@ def run_election(self) -> ElectionState:
return outcome


class DominatingSets:
def __init__(self, profile: PreferenceProfile):
self.state = ElectionState(curr_round=0, profile=profile)
class DominatingSets(Election):
def __init__(self, profile: PreferenceProfile, ties: bool = True):
super().__init__(profile, ties)

"""Dominating sets: Return the tiers of candidates by dominating set,
which is a set of candidates such that every candidate in the set wins
Expand Down Expand Up @@ -370,9 +369,9 @@ def run_election(self) -> ElectionState:
return outcome


class CondoBorda:
def __init__(self, profile: PreferenceProfile, seats: int):
self.state = ElectionState(curr_round=0, profile=profile)
class CondoBorda(Election):
def __init__(self, profile: PreferenceProfile, seats: int, ties: bool = True):
super().__init__(profile, ties)
self.seats = seats

"""Condo-Borda: Condo-Borda returns candidates ordered by dominating set,
Expand Down Expand Up @@ -401,72 +400,54 @@ def run_election(self) -> ElectionState:
return outcome


class Plurality:
class Plurality(Election):
"""
Single or multi-winner plurality election
"""

def __init__(self, profile: PreferenceProfile, seats: int):
def __init__(self, profile: PreferenceProfile, seats: int, ties: bool = True):

super().__init__(profile, ties)
self.seats = seats
self.election_state = ElectionState(
curr_round=0,
elected=[],
eliminated=[],
remaining=[
cand
for cand, votes in compute_votes(
profile.get_candidates(), profile.get_ballots()
)
],
profile=profile,
)
self.__profile = profile

def run_step(self):
"""
Simulate 'step' of a plurarity election
"""
candidates = self.__profile.get_candidates()
ballots = self.__profile.get_ballots()
candidates = self._profile.get_candidates()
ballots = self._profile.get_ballots()
results = compute_votes(candidates, ballots)

return ElectionState(
curr_round=1,
elected=[result.cand for result in results[: self.seats]],
eliminated=[result.cand for result in results[self.seats :]],
remaining=[],
profile=self.__profile,
profile=self._profile,
)

run_election = run_step


class SequentialRCV:
class SequentialRCV(Election):
"""
class to run Sequential RCV election
"""

def __init__(self, profile: PreferenceProfile, seats: int):
def __init__(self, profile: PreferenceProfile, seats: int, ties: bool = True):
"""
profile (PreferenceProfile): initial perference profile
seats (int): number of winners/size of committee
"""
super().__init__(profile, ties)
self.seats = seats
self.profile = profile
self.election_state = ElectionState(
curr_round=0,
elected=[],
eliminated=[],
remaining=[],
profile=profile,
)

def run_step(self, old_profile: PreferenceProfile) -> ElectionState:
"""
Simulates a single step of the sequential RCV contest
which is a full IRV election run on the current set of candidates
"""
old_election_state = self.election_state
old_election_state = self.state

IRVrun = STV(old_profile, transfer=seqRCV_transfer, seats=1)
old_election = IRVrun.run_election()
Expand All @@ -478,23 +459,23 @@ def run_step(self, old_profile: PreferenceProfile) -> ElectionState:
# Updates profile with removed candidates
updated_profile = PreferenceProfile(ballots=updated_ballots)

self.election_state = ElectionState(
self.state = ElectionState(
curr_round=old_election_state.curr_round + 1,
elected=list(elected_cand),
profile=updated_profile,
previous=old_election_state,
remaining=old_election.remaining,
)
return self.election_state
return self.state

def run_election(self) -> ElectionState:
"""
Simulates a complete sequential RCV contest.
Will run rounds of elections until elected seats fill
"""
old_profile = self.profile
old_profile = self._profile
elected = [] # type: ignore
seqRCV_step = self.election_state
seqRCV_step = self.state

while len(elected) < self.seats:
seqRCV_step = self.run_step(old_profile)
Expand All @@ -503,14 +484,15 @@ def run_election(self) -> ElectionState:
return seqRCV_step


class Borda:
class Borda(Election):
def __init__(
self,
profile: PreferenceProfile,
seats: int,
score_vector: Optional[list[Fraction]],
ties: bool = True,
):
self.state = ElectionState(curr_round=0, profile=profile)
super().__init__(profile, ties)
self.seats = seats
self.score_vector = score_vector

Expand Down
Loading

0 comments on commit 8045e2a

Please sign in to comment.