diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py index d69dfa2..6e0a2f4 100644 --- a/pyRCV2/method/base_stv.py +++ b/pyRCV2/method/base_stv.py @@ -16,7 +16,7 @@ __pragma__ = lambda x: None -from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepResult +from pyRCV2.model import BallotInCount, CandidateState, CountCard, CountCompleted, CountStepResult import pyRCV2.numbers from pyRCV2.numbers import Num from pyRCV2.safedict import SafeDict @@ -101,15 +101,18 @@ class BaseSTVCounter: for ballot in self.election.ballots: __pragma__('opov') - candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None) - - if candidate is not None: - self.candidates[candidate].transfers += ballot.value - if len(self.candidates[candidate].parcels) == 0: - self.candidates[candidate].parcels.append([(ballot, Num(ballot.value))]) - else: - self.candidates[candidate].parcels[0].append((ballot, Num(ballot.value))) + for i, candidate in enumerate(ballot.preferences): + count_card = self.candidates[candidate] + + if count_card.state == CandidateState.HOPEFUL: + count_card.transfers += ballot.value + if len(count_card.parcels) == 0: + count_card.parcels.append([BallotInCount(ballot, Num(ballot.value), i)]) + else: + count_card.parcels[0].append(BallotInCount(ballot, Num(ballot.value), i)) + break else: + # No available preference self.exhausted.transfers += ballot.value #self.exhausted.parcels[0].append((ballot, Num(ballot.value))) __pragma__('noopov') @@ -601,7 +604,7 @@ class BaseSTVCounter: """ Examine the specified ballots and group ballot papers by next available preference """ - # SafeDict: Candidate -> [List[Ballot], ballots, votes] + # SafeDict: Candidate -> [List[BallotInCount], ballots, votes] next_preferences = SafeDict([(c, [[], Num('0'), Num('0')]) for c, cc in self.candidates.items()]) total_ballots = Num('0') total_votes = Num('0') @@ -611,21 +614,25 @@ class BaseSTVCounter: exhausted_votes = Num('0') for parcel in parcels: - for ballot, ballot_value in parcel: + for bc in parcel: __pragma__('opov') - total_ballots += ballot.value - total_votes += ballot_value - candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None) + total_ballots += bc.ballot.value + total_votes += bc.ballot_value - if candidate is not None: - next_preferences[candidate][0].append((ballot, ballot_value)) - next_preferences[candidate][1] += ballot.value - next_preferences[candidate][2] += ballot_value + for i in range(bc.last_preference + 1, len(bc.ballot.preferences)): + candidate = bc.ballot.preferences[i] + count_card = self.candidates[candidate] + + if count_card.state == CandidateState.HOPEFUL: + next_preferences[candidate][0].append(BallotInCount(bc.ballot, bc.ballot_value, i)) + next_preferences[candidate][1] += bc.ballot.value + next_preferences[candidate][2] += bc.ballot_value + break else: - next_exhausted.append((ballot, ballot_value)) - exhausted_ballots += ballot.value - exhausted_votes += ballot_value - + # No next available preference + next_exhausted.append(bc) + exhausted_ballots += bc.ballot.value + exhausted_votes += bc.ballot_value __pragma__('noopov') return next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes diff --git a/pyRCV2/method/gregory.py b/pyRCV2/method/gregory.py index 740173c..35b1c71 100644 --- a/pyRCV2/method/gregory.py +++ b/pyRCV2/method/gregory.py @@ -119,37 +119,38 @@ class BaseGregorySTVCounter(BaseSTVCounter): self.candidates[candidate].transfers += self.round_votes(num_units * tv) __pragma__('noopov') - for ballot, ballot_value in cand_ballots: + for bc in cand_ballots: __pragma__('opov') if self.options['papers'] == 'transferable': if transferable_votes > surplus: if self.options['round_tvs'] is None: if is_weighted: - new_value = (ballot_value * surplus) / transferable_units + new_value = (bc.ballot_value * surplus) / transferable_units else: - new_value = (ballot.value * surplus) / transferable_units + new_value = (bc.ballot.value * surplus) / transferable_units else: tv = self.round_tv(surplus / transferable_units) if is_weighted: - new_value = ballot_value * tv + new_value = bc.ballot_value * tv else: - new_value = ballot.value * tv + new_value = bc.ballot.value * tv else: - new_value = ballot_value + new_value = bc.ballot_value else: if self.options['round_tvs'] is None: if is_weighted: - new_value = (ballot_value * surplus) / total_units + new_value = (bc.ballot_value * surplus) / total_units else: - new_value = (ballot.value * surplus) / total_units + new_value = (bc.ballot.value * surplus) / total_units else: tv = self.round_tv(surplus / total_units) if is_weighted: - new_value = ballot_value * tv + new_value = bc.ballot_value * tv else: - new_value = ballot.value * tv + new_value = bc.ballot.value * tv - new_parcel.append((ballot, self.round_weight(new_value))) + bc.ballot_value = self.round_weight(new_value) + new_parcel.append(bc) __pragma__('noopov') __pragma__('opov') @@ -174,29 +175,29 @@ class BaseGregorySTVCounter(BaseSTVCounter): def do_exclusion(self, candidates_excluded): """Implements BaseSTVCounter.do_exclusion""" # Optimisation: Pre-sort exclusion ballots if applicable - # self._exclusion[1] -> list of ballots-per-stage, ballots-per-stage = List[Tuple[Candidate,List[Ballot+Value]]] + # self._exclusion[1] -> list of ballots-per-stage ; ballots-per-stage = List[Tuple[Candidate,List[BallotInCount]]] if self._exclusion is None: if self.options['exclusion'] == 'one_round': - self._exclusion = (candidates_excluded, [[(c, [b for p in cc.parcels for b in p]) for c, cc in candidates_excluded]]) + self._exclusion = (candidates_excluded, [[(c, [bc for p in cc.parcels for bc in p]) for c, cc in candidates_excluded]]) elif self.options['exclusion'] == 'parcels_by_order': c, cc = candidates_excluded[0] self._exclusion = (candidates_excluded, [[(c, p)] for p in cc.parcels]) elif self.options['exclusion'] == 'by_value': - ballots = [(c, b, bv) for c, cc in candidates_excluded for p in cc.parcels for b, bv in p] + ballots = [(c, bc) for c, cc in candidates_excluded for p in cc.parcels for bc in p] # Sort ballots by value __pragma__('opov') - ballots.sort(key=lambda x: x[2] / x[1].value, reverse=True) - # Round to 8 decimal places to consider equality - # FIXME: Work out a better way of doing this + ballots.sort(key=lambda x: x[1].ballot_value / x[1].ballot.value, reverse=True) if self.options['round_tvs']: - ballots_by_value = groupby(ballots, lambda x: self.round_tv(x[2] / x[1].value)) + ballots_by_value = groupby(ballots, lambda x: self.round_tv(x[1].ballot_value / x[1].ballot.value)) else: - ballots_by_value = groupby(ballots, lambda x: (x[2] / x[1].value).round(8, x[2].ROUND_DOWN)) + # Round to 8 decimal places to consider equality + # FIXME: Work out a better way of doing this + ballots_by_value = groupby(ballots, lambda x: (x[1].ballot_value / x[1].ballot.value).round(8, x[1].ballot_value.ROUND_DOWN)) __pragma__('noopov') # TODO: Can we combine ballots for each candidate within each stage? - self._exclusion = (candidates_excluded, [[(c, [(b, bv)]) for c, b, bv in x] for x in ballots_by_value]) + self._exclusion = (candidates_excluded, [[(c, [bc]) for c, bc in x] for x in ballots_by_value]) else: raise STVException('Invalid exclusion mode') # pragma: no cover @@ -205,11 +206,11 @@ class BaseGregorySTVCounter(BaseSTVCounter): # Transfer votes - next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([bb for c, bb in this_exclusion]) + next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([bc for c, bc in this_exclusion]) if self.options['exclusion'] != 'one_round': __pragma__('opov') - self.logs.append('Transferring ' + total_ballots.pp(0) + ' ballot papers, totalling ' + total_votes.pp(2) + ' votes, received at value ' + (this_exclusion[0][1][0][1] / this_exclusion[0][1][0][0].value).pp(2) + '.') + self.logs.append('Transferring ' + total_ballots.pp(0) + ' ballot papers, totalling ' + total_votes.pp(2) + ' votes, received at value ' + (this_exclusion[0][1][0].ballot_value / this_exclusion[0][1][0].ballot.value).pp(2) + '.') __pragma__('noopov') for candidate, x in next_preferences.items(): @@ -225,9 +226,9 @@ class BaseGregorySTVCounter(BaseSTVCounter): self.candidates[candidate].transfers += self.round_votes(num_votes) __pragma__('noopov') - for ballot, ballot_value in cand_ballots: + for bc in cand_ballots: __pragma__('opov') - new_parcel.append((ballot, ballot_value)) + new_parcel.append(bc) __pragma__('noopov') # Subtract votes @@ -238,9 +239,9 @@ class BaseGregorySTVCounter(BaseSTVCounter): for candidate, ballots in this_exclusion: total_votes = Num(0) - for ballot, ballot_value in ballots: + for bc in ballots: __pragma__('opov') - total_votes += ballot_value + total_votes += bc.ballot_value __pragma__('noopov') __pragma__('opov') diff --git a/pyRCV2/method/meek.py b/pyRCV2/method/meek.py index 79f859f..1af62b4 100644 --- a/pyRCV2/method/meek.py +++ b/pyRCV2/method/meek.py @@ -33,7 +33,7 @@ class MeekCountCard(CountCard): result = MeekCountCard() result.orig_votes = self.orig_votes result.transfers = self.transfers - result.parcels = [[(b[0].clone(), b[1]) for b in p] for p in self.parcels] + #result.parcels = [[(b[0].clone(), b[1]) for b in p] for p in self.parcels] result.state = self.state result.order_elected = self.order_elected result.keep_value = keep_value diff --git a/pyRCV2/model.py b/pyRCV2/model.py index 22dd454..fdc7ccb 100644 --- a/pyRCV2/model.py +++ b/pyRCV2/model.py @@ -38,6 +38,10 @@ class CandidateState: WITHDRAWN = 60 class Ballot: + """ + Represents a voter's (or if weighted, multiple voters') preferences + """ + def __init__(self, value, preferences): self.value = value self.preferences = preferences @@ -45,6 +49,20 @@ class Ballot: def clone(self): return Ballot(self.value, self.preferences) +class BallotInCount: + """ + Represents a Ballot held by a candidate at a particular value during the count + """ + + __slots__ = ['ballot', 'ballot_value', 'last_preference'] + + def __init__(self, ballot, ballot_value, last_preference): + self.ballot = ballot + self.ballot_value = ballot_value + + # Optimisation: Record the most-recently used preference so earlier preferences do not need to be later examined + self.last_preference = last_preference + class Election: """ Represents a BLT election