From bd5910e7f6c13d12239d40cd8c180a77cbd50fc1 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 8 Jan 2021 19:21:33 +1100 Subject: [PATCH] Refactor Gregory method classes into pyRCV2.method.gregory package --- html/worker.js | 6 +- pyRCV2/cli/stv.py | 2 +- pyRCV2/method/base_stv.py | 352 ---------------------------------- pyRCV2/method/gregory.py | 373 +++++++++++++++++++++++++++++++++++++ pyRCV2/transcrypt.py | 2 +- tests/test_aec.py | 2 +- tests/test_combinations.py | 10 +- tests/test_csm.py | 2 +- tests/test_ers97.py | 2 +- tests/test_prsa.py | 2 +- 10 files changed, 387 insertions(+), 366 deletions(-) create mode 100644 pyRCV2/method/gregory.py diff --git a/html/worker.js b/html/worker.js index 47fd592..20030c2 100644 --- a/html/worker.js +++ b/html/worker.js @@ -43,13 +43,13 @@ onmessage = function(evt) { // Create counter if (evt.data.data.transfers === 'uig') { - counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.data.options); + counter = py.pyRCV2.method.gregory.UIGSTVCounter(election, evt.data.data.options); } else if (evt.data.data.transfers === 'eg') { - counter = py.pyRCV2.method.base_stv.EGSTVCounter(election, evt.data.data.options); + counter = py.pyRCV2.method.gregory.EGSTVCounter(election, evt.data.data.options); } else if (evt.data.data.transfers === 'meek') { counter = py.pyRCV2.method.meek.MeekSTVCounter(election, evt.data.data.options); } else { - counter = py.pyRCV2.method.base_stv.WIGSTVCounter(election, evt.data.data.options); + counter = py.pyRCV2.method.gregory.WIGSTVCounter(election, evt.data.data.options); } if (evt.data.data.options['ties'] === 'backwards_random') { diff --git a/pyRCV2/cli/stv.py b/pyRCV2/cli/stv.py index 19f5321..27e8998 100644 --- a/pyRCV2/cli/stv.py +++ b/pyRCV2/cli/stv.py @@ -18,7 +18,7 @@ import pyRCV2.blt import pyRCV2.model import pyRCV2.numbers -from pyRCV2.method.base_stv import UIGSTVCounter, WIGSTVCounter, EGSTVCounter +from pyRCV2.method.gregory import UIGSTVCounter, WIGSTVCounter, EGSTVCounter from pyRCV2.method.meek import MeekSTVCounter from pyRCV2.ties import TiesBackwards, TiesForwards, TiesPrompt, TiesRandom diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py index 233851e..c6611e4 100644 --- a/pyRCV2/method/base_stv.py +++ b/pyRCV2/method/base_stv.py @@ -15,39 +15,11 @@ # along with this program. If not, see . __pragma__ = lambda x: None -is_py = False -__pragma__('skip') -is_py = True -__pragma__('noskip') from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepResult from pyRCV2.numbers import Num from pyRCV2.safedict import SafeDict -# Stubs for JS -def groupby(iterable, keyfunc): - if is_py: - __pragma__('skip') - import itertools - return [list(g) for k, g in itertools.groupby(iterable, keyfunc)] - __pragma__('noskip') - else: - groups = [] - group = [] - last_result = None - for i in iterable: - this_result = keyfunc(i) - __pragma__('opov') - if last_result is not None and this_result != last_result: - __pragma__('noopov') - groups.append(group) - group = [] - last_result = this_result - group.append(i) - if group: - groups.append(group) - return groups - class STVException(Exception): def __init__(self, message): Exception.__init__(self) @@ -699,327 +671,3 @@ class BaseSTVCounter: if self.options['round_tvs'] is None: return num return num.round(self.options['round_tvs'], num.ROUND_DOWN) - -class WIGSTVCounter(BaseSTVCounter): - """ - Basic weighted inclusive Gregory STV counter - """ - - def do_surplus(self, candidate_surplus, count_card, surplus): - next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels) - - if self.options['papers'] == 'transferable': - __pragma__('opov') - transferable_votes = total_votes - exhausted_votes - __pragma__('noopov') - - for candidate, x in next_preferences.items(): - cand_ballots = x[0] - num_ballots = x[1] - num_votes = x[2] - - new_parcel = [] - if len(cand_ballots) > 0: - __pragma__('opov') - self.candidates[candidate].parcels.append(new_parcel) - __pragma__('noopov') - - __pragma__('opov') - if self.options['papers'] == 'transferable': - if transferable_votes > surplus: - if self.options['round_tvs'] is None: - self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / transferable_votes) - else: - tv = self.round_tv(surplus / transferable_votes) - self.candidates[candidate].transfers += self.round_votes(num_votes * tv) - else: - self.candidates[candidate].transfers += self.round_votes(num_votes) # Do not allow weight to increase - else: - if self.options['round_tvs'] is None: - self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / total_votes) - else: - tv = self.round_tv(surplus / total_votes) - self.candidates[candidate].transfers += self.round_votes(num_votes * tv) - __pragma__('noopov') - - for ballot, ballot_value in cand_ballots: - __pragma__('opov') - if self.options['papers'] == 'transferable': - if transferable_votes > surplus: - if self.options['round_tvs'] is None: - new_value = (ballot_value * surplus) / transferable_votes - else: - tv = self.round_tv(surplus / transferable_votes) - new_value = ballot_value * tv - else: - new_value = ballot_value - else: - if self.options['round_tvs'] is None: - new_value = (ballot_value * surplus) / total_votes - else: - tv = self.round_tv(surplus / total_votes) - new_value = ballot_value * tv - - new_parcel.append((ballot, self.round_weight(new_value))) - __pragma__('noopov') - - __pragma__('opov') - if self.options['papers'] == 'transferable': - if transferable_votes > surplus: - pass # No ballots exhaust - else: - self.exhausted.transfers += self.round_votes((surplus - transferable_votes)) - else: - self.exhausted.transfers += self.round_votes((exhausted_votes * surplus) / total_votes) - __pragma__('noopov') - - __pragma__('opov') - count_card.transfers -= surplus - __pragma__('noopov') - - count_card.state = CandidateState.ELECTED - - def do_exclusion(self, candidates_excluded): - # Optimisation: Pre-sort exclusion ballots if applicable - # self._exclusion[1] -> list of ballots-per-stage, ballots-per-stage = List[Tuple[Candidate,List[Ballot+Value]]] - 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]]) - 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] - - # 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 - if self.options['round_tvs']: - ballots_by_value = groupby(ballots, lambda x: self.round_tv(x[2] / x[1].value)) - else: - ballots_by_value = groupby(ballots, lambda x: (x[2] / x[1].value).round(8, x[2].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]) - else: - raise STVException('Invalid exclusion mode') - - #print([[bv / b.value for c, bb in stage for b, bv in bb] for stage in self._exclusion[1]]) - - this_exclusion = self._exclusion[1][0] - self._exclusion[1].remove(this_exclusion) - - # Transfer votes - - next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([bb for c, bb in this_exclusion]) - for candidate, x in next_preferences.items(): - cand_ballots, num_ballots, num_votes = x[0], x[1], x[2] - - new_parcel = [] - if len(cand_ballots) > 0: - __pragma__('opov') - self.candidates[candidate].parcels.append(new_parcel) - __pragma__('noopov') - - __pragma__('opov') - self.candidates[candidate].transfers += self.round_votes(num_votes) - __pragma__('noopov') - - for ballot, ballot_value in cand_ballots: - __pragma__('opov') - new_parcel.append((ballot, ballot_value)) - __pragma__('noopov') - - # Subtract votes - - __pragma__('opov') - self.exhausted.transfers += self.round_votes(exhausted_votes) - __pragma__('noopov') - - for candidate, ballots in this_exclusion: - total_votes = Num(0) - for ballot, ballot_value in ballots: - __pragma__('opov') - total_votes += ballot_value - __pragma__('noopov') - - __pragma__('opov') - self.candidates[candidate].transfers -= total_votes - __pragma__('noopov') - - if len(self._exclusion[1]) == 0: - for candidate_excluded, count_card in candidates_excluded: - __pragma__('opov') - count_card.transfers -= count_card.votes - __pragma__('noopov') - count_card.state = CandidateState.EXCLUDED - self._exclusion = None - -class UIGSTVCounter(WIGSTVCounter): - """ - Basic unweighted inclusive Gregory STV counter - """ - - def __init__(self, *args): - WIGSTVCounter.__init__(self, *args) - - def do_surplus(self, candidate_surplus, count_card, surplus): - next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels) - - if self.options['papers'] == 'transferable': - __pragma__('opov') - transferable_ballots = total_ballots - exhausted_ballots - transferable_votes = total_votes - exhausted_votes - __pragma__('noopov') - - for candidate, x in next_preferences.items(): - cand_ballots = x[0] - num_ballots = x[1] - num_votes = x[2] - - new_parcel = [] - if len(cand_ballots) > 0: - __pragma__('opov') - self.candidates[candidate].parcels.append(new_parcel) - __pragma__('noopov') - - __pragma__('opov') - if self.options['papers'] == 'transferable': - if transferable_votes > surplus: - if self.options['round_tvs'] is None: - self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots) - else: - tv = self.round_tv(surplus / transferable_ballots) - self.candidates[candidate].transfers += self.round_votes(num_ballots * tv) - else: - self.candidates[candidate].transfers += self.round_votes(num_votes) - else: - if self.options['round_tvs'] is None: - self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots) - else: - tv = self.round_tv(surplus / total_ballots) - self.candidates[candidate].transfers += self.round_votes(num_ballots * tv) - __pragma__('noopov') - - for ballot, ballot_value in cand_ballots: - __pragma__('opov') - if self.options['papers'] == 'transferable': - if transferable_votes > surplus: - if self.options['round_tvs'] is None: - new_value = (ballot.value * surplus) / transferable_ballots - else: - tv = self.round_tv(surplus / transferable_ballots) - new_value = ballot.value * tv - else: - new_value = ballot_value - else: - if self.options['round_tvs'] is None: - new_value = (ballot.value * surplus) / total_ballots - else: - tv = self.round_tv(surplus / total_ballots) - new_value = ballot.value * tv - - new_parcel.append((ballot, self.round_weight(new_value))) - __pragma__('noopov') - - __pragma__('opov') - if self.options['papers'] == 'transferable': - if transferable_votes > surplus: - pass # No ballots exhaust - else: - self.exhausted.transfers += self.round_votes(surplus - transferable_votes) - else: - self.exhausted.transfers += self.round_votes((exhausted_ballots * surplus) / total_ballots) - __pragma__('noopov') - - __pragma__('opov') - count_card.transfers -= surplus - __pragma__('noopov') - - count_card.state = CandidateState.ELECTED - -class EGSTVCounter(UIGSTVCounter): - """ - Exclusive Gregory (last bundle) STV implementation - """ - - def do_surplus(self, candidate_surplus, count_card, surplus): - """Overrides UIGSTVCounter.do_surplus""" - - last_bundle = count_card.parcels[len(count_card.parcels)-1] - next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([last_bundle]) - - if self.options['papers'] == 'transferable': - __pragma__('opov') - transferable_ballots = total_ballots - exhausted_ballots - transferable_votes = total_votes - exhausted_votes - __pragma__('noopov') - - for candidate, x in next_preferences.items(): - cand_ballots = x[0] - num_ballots = x[1] - num_votes = x[2] - - new_parcel = [] - if len(cand_ballots) > 0: - __pragma__('opov') - self.candidates[candidate].parcels.append(new_parcel) - __pragma__('noopov') - - __pragma__('opov') - if self.options['papers'] == 'transferable': - if transferable_votes > surplus: - if self.options['round_tvs'] is None: - self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots) - else: - tv = self.round_tv(surplus / transferable_ballots) - self.candidates[candidate].transfers += self.round_votes(num_ballots * tv) - else: - self.candidates[candidate].transfers += self.round_votes(num_votes) - else: - if self.options['round_tvs'] is None: - self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots) - else: - tv = self.round_tv(surplus / total_ballots) - self.candidates[candidate].transfers += self.round_votes(num_ballots * tv) - __pragma__('noopov') - - for ballot, ballot_value in cand_ballots: - __pragma__('opov') - if self.options['papers'] == 'transferable': - if transferable_votes > surplus: - if self.options['round_tvs'] is None: - new_value = (ballot.value * surplus) / transferable_ballots - else: - tv = self.round_tv(surplus / transferable_ballots) - new_value = ballot.value * tv - else: - new_value = ballot_value - else: - if self.options['round_tvs'] is None: - new_value = (ballot.value * surplus) / total_ballots - else: - tv = self.round_tv(surplus / total_ballots) - new_value = ballot.value * tv - - new_parcel.append((ballot, self.round_weight(new_value))) - __pragma__('noopov') - - __pragma__('opov') - if self.options['papers'] == 'transferable': - if transferable_votes > surplus: - pass # No ballots exhaust - else: - self.exhausted.transfers += self.round_votes((surplus - transferable_votes)) - else: - self.exhausted.transfers += self.round_votes((exhausted_ballots * surplus) / total_ballots) - __pragma__('noopov') - - __pragma__('opov') - count_card.transfers -= surplus - __pragma__('noopov') - - count_card.state = CandidateState.ELECTED diff --git a/pyRCV2/method/gregory.py b/pyRCV2/method/gregory.py new file mode 100644 index 0000000..90c7ca5 --- /dev/null +++ b/pyRCV2/method/gregory.py @@ -0,0 +1,373 @@ +# pyRCV2: Preferential vote counting +# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +__pragma__ = lambda x: None +is_py = False +__pragma__('skip') +is_py = True +__pragma__('noskip') + +from pyRCV2.method.base_stv import BaseSTVCounter, STVException +from pyRCV2.model import CandidateState +from pyRCV2.numbers import Num + +# Stubs for JS +def groupby(iterable, keyfunc): + if is_py: + __pragma__('skip') + import itertools + return [list(g) for k, g in itertools.groupby(iterable, keyfunc)] + __pragma__('noskip') + else: + groups = [] + group = [] + last_result = None + for i in iterable: + this_result = keyfunc(i) + __pragma__('opov') + if last_result is not None and this_result != last_result: + __pragma__('noopov') + groups.append(group) + group = [] + last_result = this_result + group.append(i) + if group: + groups.append(group) + return groups + +class WIGSTVCounter(BaseSTVCounter): + """ + Basic weighted inclusive Gregory STV counter + """ + + def do_surplus(self, candidate_surplus, count_card, surplus): + next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels) + + if self.options['papers'] == 'transferable': + __pragma__('opov') + transferable_votes = total_votes - exhausted_votes + __pragma__('noopov') + + for candidate, x in next_preferences.items(): + cand_ballots = x[0] + num_ballots = x[1] + num_votes = x[2] + + new_parcel = [] + if len(cand_ballots) > 0: + __pragma__('opov') + self.candidates[candidate].parcels.append(new_parcel) + __pragma__('noopov') + + __pragma__('opov') + if self.options['papers'] == 'transferable': + if transferable_votes > surplus: + if self.options['round_tvs'] is None: + self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / transferable_votes) + else: + tv = self.round_tv(surplus / transferable_votes) + self.candidates[candidate].transfers += self.round_votes(num_votes * tv) + else: + self.candidates[candidate].transfers += self.round_votes(num_votes) # Do not allow weight to increase + else: + if self.options['round_tvs'] is None: + self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / total_votes) + else: + tv = self.round_tv(surplus / total_votes) + self.candidates[candidate].transfers += self.round_votes(num_votes * tv) + __pragma__('noopov') + + for ballot, ballot_value in cand_ballots: + __pragma__('opov') + if self.options['papers'] == 'transferable': + if transferable_votes > surplus: + if self.options['round_tvs'] is None: + new_value = (ballot_value * surplus) / transferable_votes + else: + tv = self.round_tv(surplus / transferable_votes) + new_value = ballot_value * tv + else: + new_value = ballot_value + else: + if self.options['round_tvs'] is None: + new_value = (ballot_value * surplus) / total_votes + else: + tv = self.round_tv(surplus / total_votes) + new_value = ballot_value * tv + + new_parcel.append((ballot, self.round_weight(new_value))) + __pragma__('noopov') + + __pragma__('opov') + if self.options['papers'] == 'transferable': + if transferable_votes > surplus: + pass # No ballots exhaust + else: + self.exhausted.transfers += self.round_votes((surplus - transferable_votes)) + else: + self.exhausted.transfers += self.round_votes((exhausted_votes * surplus) / total_votes) + __pragma__('noopov') + + __pragma__('opov') + count_card.transfers -= surplus + __pragma__('noopov') + + count_card.state = CandidateState.ELECTED + + def do_exclusion(self, candidates_excluded): + # Optimisation: Pre-sort exclusion ballots if applicable + # self._exclusion[1] -> list of ballots-per-stage, ballots-per-stage = List[Tuple[Candidate,List[Ballot+Value]]] + 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]]) + 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] + + # 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 + if self.options['round_tvs']: + ballots_by_value = groupby(ballots, lambda x: self.round_tv(x[2] / x[1].value)) + else: + ballots_by_value = groupby(ballots, lambda x: (x[2] / x[1].value).round(8, x[2].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]) + else: + raise STVException('Invalid exclusion mode') + + #print([[bv / b.value for c, bb in stage for b, bv in bb] for stage in self._exclusion[1]]) + + this_exclusion = self._exclusion[1][0] + self._exclusion[1].remove(this_exclusion) + + # Transfer votes + + next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([bb for c, bb in this_exclusion]) + for candidate, x in next_preferences.items(): + cand_ballots, num_ballots, num_votes = x[0], x[1], x[2] + + new_parcel = [] + if len(cand_ballots) > 0: + __pragma__('opov') + self.candidates[candidate].parcels.append(new_parcel) + __pragma__('noopov') + + __pragma__('opov') + self.candidates[candidate].transfers += self.round_votes(num_votes) + __pragma__('noopov') + + for ballot, ballot_value in cand_ballots: + __pragma__('opov') + new_parcel.append((ballot, ballot_value)) + __pragma__('noopov') + + # Subtract votes + + __pragma__('opov') + self.exhausted.transfers += self.round_votes(exhausted_votes) + __pragma__('noopov') + + for candidate, ballots in this_exclusion: + total_votes = Num(0) + for ballot, ballot_value in ballots: + __pragma__('opov') + total_votes += ballot_value + __pragma__('noopov') + + __pragma__('opov') + self.candidates[candidate].transfers -= total_votes + __pragma__('noopov') + + if len(self._exclusion[1]) == 0: + for candidate_excluded, count_card in candidates_excluded: + __pragma__('opov') + count_card.transfers -= count_card.votes + __pragma__('noopov') + count_card.state = CandidateState.EXCLUDED + self._exclusion = None + +class UIGSTVCounter(WIGSTVCounter): + """ + Basic unweighted inclusive Gregory STV counter + """ + + def __init__(self, *args): + WIGSTVCounter.__init__(self, *args) + + def do_surplus(self, candidate_surplus, count_card, surplus): + next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels) + + if self.options['papers'] == 'transferable': + __pragma__('opov') + transferable_ballots = total_ballots - exhausted_ballots + transferable_votes = total_votes - exhausted_votes + __pragma__('noopov') + + for candidate, x in next_preferences.items(): + cand_ballots = x[0] + num_ballots = x[1] + num_votes = x[2] + + new_parcel = [] + if len(cand_ballots) > 0: + __pragma__('opov') + self.candidates[candidate].parcels.append(new_parcel) + __pragma__('noopov') + + __pragma__('opov') + if self.options['papers'] == 'transferable': + if transferable_votes > surplus: + if self.options['round_tvs'] is None: + self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots) + else: + tv = self.round_tv(surplus / transferable_ballots) + self.candidates[candidate].transfers += self.round_votes(num_ballots * tv) + else: + self.candidates[candidate].transfers += self.round_votes(num_votes) + else: + if self.options['round_tvs'] is None: + self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots) + else: + tv = self.round_tv(surplus / total_ballots) + self.candidates[candidate].transfers += self.round_votes(num_ballots * tv) + __pragma__('noopov') + + for ballot, ballot_value in cand_ballots: + __pragma__('opov') + if self.options['papers'] == 'transferable': + if transferable_votes > surplus: + if self.options['round_tvs'] is None: + new_value = (ballot.value * surplus) / transferable_ballots + else: + tv = self.round_tv(surplus / transferable_ballots) + new_value = ballot.value * tv + else: + new_value = ballot_value + else: + if self.options['round_tvs'] is None: + new_value = (ballot.value * surplus) / total_ballots + else: + tv = self.round_tv(surplus / total_ballots) + new_value = ballot.value * tv + + new_parcel.append((ballot, self.round_weight(new_value))) + __pragma__('noopov') + + __pragma__('opov') + if self.options['papers'] == 'transferable': + if transferable_votes > surplus: + pass # No ballots exhaust + else: + self.exhausted.transfers += self.round_votes(surplus - transferable_votes) + else: + self.exhausted.transfers += self.round_votes((exhausted_ballots * surplus) / total_ballots) + __pragma__('noopov') + + __pragma__('opov') + count_card.transfers -= surplus + __pragma__('noopov') + + count_card.state = CandidateState.ELECTED + +class EGSTVCounter(UIGSTVCounter): + """ + Exclusive Gregory (last bundle) STV implementation + """ + + def do_surplus(self, candidate_surplus, count_card, surplus): + """Overrides UIGSTVCounter.do_surplus""" + + last_bundle = count_card.parcels[len(count_card.parcels)-1] + next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([last_bundle]) + + if self.options['papers'] == 'transferable': + __pragma__('opov') + transferable_ballots = total_ballots - exhausted_ballots + transferable_votes = total_votes - exhausted_votes + __pragma__('noopov') + + for candidate, x in next_preferences.items(): + cand_ballots = x[0] + num_ballots = x[1] + num_votes = x[2] + + new_parcel = [] + if len(cand_ballots) > 0: + __pragma__('opov') + self.candidates[candidate].parcels.append(new_parcel) + __pragma__('noopov') + + __pragma__('opov') + if self.options['papers'] == 'transferable': + if transferable_votes > surplus: + if self.options['round_tvs'] is None: + self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots) + else: + tv = self.round_tv(surplus / transferable_ballots) + self.candidates[candidate].transfers += self.round_votes(num_ballots * tv) + else: + self.candidates[candidate].transfers += self.round_votes(num_votes) + else: + if self.options['round_tvs'] is None: + self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots) + else: + tv = self.round_tv(surplus / total_ballots) + self.candidates[candidate].transfers += self.round_votes(num_ballots * tv) + __pragma__('noopov') + + for ballot, ballot_value in cand_ballots: + __pragma__('opov') + if self.options['papers'] == 'transferable': + if transferable_votes > surplus: + if self.options['round_tvs'] is None: + new_value = (ballot.value * surplus) / transferable_ballots + else: + tv = self.round_tv(surplus / transferable_ballots) + new_value = ballot.value * tv + else: + new_value = ballot_value + else: + if self.options['round_tvs'] is None: + new_value = (ballot.value * surplus) / total_ballots + else: + tv = self.round_tv(surplus / total_ballots) + new_value = ballot.value * tv + + new_parcel.append((ballot, self.round_weight(new_value))) + __pragma__('noopov') + + __pragma__('opov') + if self.options['papers'] == 'transferable': + if transferable_votes > surplus: + pass # No ballots exhaust + else: + self.exhausted.transfers += self.round_votes((surplus - transferable_votes)) + else: + self.exhausted.transfers += self.round_votes((exhausted_ballots * surplus) / total_ballots) + __pragma__('noopov') + + __pragma__('opov') + count_card.transfers -= surplus + __pragma__('noopov') + + count_card.state = CandidateState.ELECTED diff --git a/pyRCV2/transcrypt.py b/pyRCV2/transcrypt.py index 40024a5..860af14 100644 --- a/pyRCV2/transcrypt.py +++ b/pyRCV2/transcrypt.py @@ -16,7 +16,7 @@ import pyRCV2.blt import pyRCV2.model -import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.meek +import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.gregory, pyRCV2.method.meek import pyRCV2.numbers import pyRCV2.random import pyRCV2.ties diff --git a/tests/test_aec.py b/tests/test_aec.py index fdf580f..72ea757 100644 --- a/tests/test_aec.py +++ b/tests/test_aec.py @@ -19,7 +19,7 @@ from pytest_steps import test_steps import pyRCV2.blt import pyRCV2.numbers -from pyRCV2.method.base_stv import UIGSTVCounter +from pyRCV2.method.gregory import UIGSTVCounter from pyRCV2.model import CandidateState, CountCompleted import csv diff --git a/tests/test_combinations.py b/tests/test_combinations.py index 04d533c..e1145d1 100644 --- a/tests/test_combinations.py +++ b/tests/test_combinations.py @@ -16,7 +16,7 @@ import pyRCV2.blt import pyRCV2.numbers -import pyRCV2.method.base_stv +import pyRCV2.method.gregory from pyRCV2.model import CountCompleted import json @@ -59,7 +59,7 @@ def maketst(numbers, counter_cls, options): return t_py, t_js -test_prsa1_scottish_py, test_prsa1_scottish_js = maketst('Fixed', 'pyRCV2.method.base_stv.WIGSTVCounter', {}) -test_prsa1_stvc_py, test_prsa1_stvc_js = maketst('Rational', 'pyRCV2.method.base_stv.WIGSTVCounter', {'quota': 'droop_exact', 'quota_criterion': 'gt', 'prog_quota': True}) -test_prsa1_senate_py, test_prsa1_senate_js = maketst('Fixed', 'pyRCV2.method.base_stv.UIGSTVCounter', {'surplus_order': 'order', 'exclusion': 'by_value'}) -test_prsa1_wright_py, test_prsa1_wright_js = maketst('Fixed', 'pyRCV2.method.base_stv.WIGSTVCounter', {'exclusion': 'wright'}) +test_prsa1_scottish_py, test_prsa1_scottish_js = maketst('Fixed', 'pyRCV2.method.gregory.WIGSTVCounter', {}) +test_prsa1_stvc_py, test_prsa1_stvc_js = maketst('Rational', 'pyRCV2.method.gregory.WIGSTVCounter', {'quota': 'droop_exact', 'quota_criterion': 'gt', 'prog_quota': True}) +test_prsa1_senate_py, test_prsa1_senate_js = maketst('Fixed', 'pyRCV2.method.gregory.UIGSTVCounter', {'surplus_order': 'order', 'exclusion': 'by_value'}) +test_prsa1_wright_py, test_prsa1_wright_js = maketst('Fixed', 'pyRCV2.method.gregory.WIGSTVCounter', {'exclusion': 'wright'}) diff --git a/tests/test_csm.py b/tests/test_csm.py index 14200e0..ef199e2 100644 --- a/tests/test_csm.py +++ b/tests/test_csm.py @@ -19,7 +19,7 @@ from pytest_steps import test_steps import pyRCV2.blt import pyRCV2.numbers -from pyRCV2.method.base_stv import WIGSTVCounter +from pyRCV2.method.gregory import WIGSTVCounter from pyRCV2.model import CandidateState, CountCompleted def count_until_exclude(counter): diff --git a/tests/test_ers97.py b/tests/test_ers97.py index d167035..3cf8635 100644 --- a/tests/test_ers97.py +++ b/tests/test_ers97.py @@ -18,7 +18,7 @@ from pytest_steps import test_steps import pyRCV2.blt import pyRCV2.numbers -from pyRCV2.method.base_stv import EGSTVCounter +from pyRCV2.method.gregory import EGSTVCounter from pyRCV2.model import CandidateState, CountCompleted import csv diff --git a/tests/test_prsa.py b/tests/test_prsa.py index a2332ed..1552760 100644 --- a/tests/test_prsa.py +++ b/tests/test_prsa.py @@ -16,7 +16,7 @@ import pyRCV2.blt import pyRCV2.numbers -from pyRCV2.method.base_stv import EGSTVCounter +from pyRCV2.method.gregory import EGSTVCounter from pyRCV2.model import CandidateState, CountCompleted def isclose(x, y):