From 61601f05cc356ebb617fde278fbea24be438ceaf Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 3 Jan 2021 18:57:56 +1100 Subject: [PATCH] Make Wright method a variant of regular STV as an exclusion method --- docs/options.md | 4 +-- html/index.html | 1 + html/index.js | 4 +-- html/worker.js | 2 -- pyRCV2/cli/stv.py | 9 ++---- pyRCV2/method/base_stv.py | 35 ++++++++++++++++++++-- pyRCV2/method/wright.py | 61 -------------------------------------- pyRCV2/transcrypt.py | 2 +- tests/test_combinations.py | 5 ++-- tests/test_csm.py | 6 ++-- 10 files changed, 47 insertions(+), 82 deletions(-) delete mode 100644 pyRCV2/method/wright.py diff --git a/docs/options.md b/docs/options.md index 72b7659..2a43e7a 100644 --- a/docs/options.md +++ b/docs/options.md @@ -66,10 +66,9 @@ Some STV counting rules provide, for example, that ‘no surplus shall be transf ## Method (-m/--method) -This dropdown allows you to select how ballots are transferred during surplus transfers or exclusions. The 2 recommended methods are: +This dropdown allows you to select how ballots are transferred during surplus transfers or exclusions. The recommended method is: * Weighted inclusive Gregory: During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are weighted according to the weights of the ballot papers. -* Wright STV: Same as weighted inclusive Gregory, but when a candidate is excluded, the count is reset from the beginning (minus the excluded candidate). Other methods are supported, but not recommended: @@ -88,6 +87,7 @@ Other surplus transfer methods, such as non-fractional transfers (e.g. random sa * Exclude in one round (default): When excluding a candidate, transfer all their ballot papers in one round. * Exclude by parcel (by order): When excluding a candidate, transfer their ballot papers one parcel at a time, in their order each was received. Each parcel forms a separate round, i.e. if a transfer allows another candidate to meet the quota criterion, no further papers are transferred to that candidate. * Exclude by value: When excluding a candidate, transfer their ballot papers in descending order of accumulated transfer value. Each transfer of all ballots of a certain transfer value forms a separate round. +* Wright method (re-iterate): When a candidate is excluded, the count is reset from the beginning (minus the excluded candidate). ## Ties (-t/--ties) diff --git a/html/index.html b/html/index.html index dd59484..d43f7c1 100644 --- a/html/index.html +++ b/html/index.html @@ -147,6 +147,7 @@ +
diff --git a/html/index.js b/html/index.js index b03dbcf..7f404ad 100644 --- a/html/index.js +++ b/html/index.js @@ -83,9 +83,9 @@ function changePreset() { document.getElementById('chkRoundVotes').checked = false; document.getElementById('chkRoundWeights').checked = false; document.getElementById('selSurplus').value = 'size'; - document.getElementById('selTransfers').value = 'wright'; + document.getElementById('selTransfers').value = 'wig'; document.getElementById('selPapers').value = 'both'; - document.getElementById('selExclusion').value = 'one_round'; + document.getElementById('selExclusion').value = 'wright'; document.getElementById('selTies').value = 'backwards_random'; } else if (document.getElementById('selPreset').value === 'prsa77') { document.getElementById('selQuotaCriterion').value = 'geq'; diff --git a/html/worker.js b/html/worker.js index 8fdcd7e..afd1b15 100644 --- a/html/worker.js +++ b/html/worker.js @@ -46,8 +46,6 @@ onmessage = function(evt) { counter = py.pyRCV2.method.base_stv.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); - } else if (evt.data.data.transfers === 'wright') { - counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.data.options); } else { counter = py.pyRCV2.method.base_stv.WIGSTVCounter(election, evt.data.data.options); } diff --git a/pyRCV2/cli/stv.py b/pyRCV2/cli/stv.py index f746653..ccc669f 100644 --- a/pyRCV2/cli/stv.py +++ b/pyRCV2/cli/stv.py @@ -1,5 +1,5 @@ # pyRCV2: Preferential vote counting -# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# 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 @@ -19,7 +19,6 @@ import pyRCV2.model import pyRCV2.numbers from pyRCV2.method.base_stv import UIGSTVCounter, WIGSTVCounter, EGSTVCounter -from pyRCV2.method.wright import WrightSTVCounter from pyRCV2.ties import TiesBackwards, TiesPrompt, TiesRandom import sys @@ -37,9 +36,9 @@ def add_parser(subparsers): parser.add_argument('--round-votes', type=int, help='round votes to specified decimal places') parser.add_argument('--round-weights', type=int, help='round ballot weights to specified decimal places') parser.add_argument('--surplus-order', '-s', choices=['size', 'order'], default='size', help='whether to distribute surpluses by size or by order of election (default: size)') - parser.add_argument('--method', '-m', choices=['wig', 'uig', 'eg', 'wright'], default='wig', help='method of surpluses and exclusions (default: wig - weighted inclusive Gregory)') + parser.add_argument('--method', '-m', choices=['wig', 'uig', 'eg'], default='wig', help='method of transferring surpluses (default: wig - weighted inclusive Gregory)') parser.add_argument('--transferable-only', action='store_true', help='examine only transferable papers during surplus distributions') - parser.add_argument('--exclusion', choices=['one_round', 'parcels_by_order', 'by_value'], default='one_round', help='whether to perform exclusions in one round or parcel by parcel (default: one_round)') + parser.add_argument('--exclusion', choices=['one_round', 'parcels_by_order', 'by_value', 'wright'], default='one_round', help='how to perform exclusions (default: one_round)') parser.add_argument('--ties', '-t', action='append', choices=['backwards', 'prompt', 'random'], default=None, help='how to resolve ties (default: backwards then random)') parser.add_argument('--random-seed', default=None, help='arbitrary string used to seed the RNG for random tie breaking') @@ -85,8 +84,6 @@ def main(args): counter = UIGSTVCounter(election, vars(args)) elif args.method == 'eg': counter = EGSTVCounter(election, vars(args)) - elif args.method == 'wright': - counter = WrightSTVCounter(election, vars(args)) else: counter = WIGSTVCounter(election, vars(args)) diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py index 97a8eb9..2a6d57c 100644 --- a/pyRCV2/method/base_stv.py +++ b/pyRCV2/method/base_stv.py @@ -1,5 +1,5 @@ # pyRCV2: Preferential vote counting -# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# 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 @@ -67,7 +67,7 @@ class BaseSTVCounter: 'quota_criterion': 'geq', # 'geq' or 'gt' 'surplus_order': 'size', # 'size' or 'order' 'papers': 'both', # 'both' or 'transferable' - 'exclusion': 'one_round', # 'one_round', 'parcels_by_order' or 'by_value' + 'exclusion': 'one_round', # 'one_round', 'parcels_by_order', 'by_value' or 'wright' 'ties': [], # List of tie strategies (e.g. TiesRandom) 'round_votes': None, # Number of decimal places or None 'round_weights': None, # Number of decimal places or None @@ -315,6 +315,37 @@ class BaseSTVCounter: candidate_excluded, count_card = self.candidate_to_exclude() count_card.state = CandidateState.EXCLUDING + # Handle Wright STV + if self.options['exclusion'] == 'wright': + count_card.state = CandidateState.EXCLUDED + + # Reset the count + # Carry over certain candidate states + new_candidates = SafeDict() + for candidate, count_card in self.candidates.items(): + new_count_card = CountCard() + + if count_card.state == CandidateState.WITHDRAWN: + new_count_card.state = CandidateState.WITHDRAWN + elif count_card.state == CandidateState.EXCLUDED: + new_count_card.state = CandidateState.EXCLUDED + + __pragma__('opov') + new_candidates[candidate] = new_count_card + __pragma__('noopov') + + self.candidates = new_candidates + self.exhausted = CountCard() + self.loss_fraction = CountCard() + self.num_elected = 0 + + step_results = self.step_results # Carry over step results + result = self.reset() + self.step_results = step_results + result.comment = 'Exclusion of ' + candidate_excluded.name + + return result + # Exclude this candidate self.do_exclusion(candidate_excluded, count_card) diff --git a/pyRCV2/method/wright.py b/pyRCV2/method/wright.py deleted file mode 100644 index bb0ccda..0000000 --- a/pyRCV2/method/wright.py +++ /dev/null @@ -1,61 +0,0 @@ -# pyRCV2: Preferential vote counting -# Copyright © 2020 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 - -from pyRCV2.method.base_stv import WIGSTVCounter -from pyRCV2.model import CandidateState, CountCard -from pyRCV2.safedict import SafeDict - -class WrightSTVCounter(WIGSTVCounter): - """ - Wright STV implementation - """ - - def exclude_candidate(self): - """ - Overrides BaseSTVCounter.exclude_candidate per Wright STV - """ - - candidate_excluded, count_card = self.candidate_to_exclude() - count_card.state = CandidateState.EXCLUDED - - # Reset the count - # Carry over certain candidate states - new_candidates = SafeDict() - for candidate, count_card in self.candidates.items(): - new_count_card = CountCard() - - if count_card.state == CandidateState.WITHDRAWN: - new_count_card.state = CandidateState.WITHDRAWN - elif count_card.state == CandidateState.EXCLUDED: - new_count_card.state = CandidateState.EXCLUDED - - __pragma__('opov') - new_candidates[candidate] = new_count_card - __pragma__('noopov') - - self.candidates = new_candidates - self.exhausted = CountCard() - self.loss_fraction = CountCard() - self.num_elected = 0 - - step_results = self.step_results # Carry over step results - result = self.reset() - self.step_results = step_results - result.comment = 'Exclusion of ' + candidate_excluded.name - - return result diff --git a/pyRCV2/transcrypt.py b/pyRCV2/transcrypt.py index 752be3a..af92cab 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.wright +import pyRCV2.method, pyRCV2.method.base_stv import pyRCV2.numbers import pyRCV2.random import pyRCV2.ties diff --git a/tests/test_combinations.py b/tests/test_combinations.py index 7091001..04d533c 100644 --- a/tests/test_combinations.py +++ b/tests/test_combinations.py @@ -1,5 +1,5 @@ # pyRCV2: Preferential vote counting -# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# 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 @@ -17,7 +17,6 @@ import pyRCV2.blt import pyRCV2.numbers import pyRCV2.method.base_stv -import pyRCV2.method.wright from pyRCV2.model import CountCompleted import json @@ -63,4 +62,4 @@ def maketst(numbers, counter_cls, options): 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.wright.WrightSTVCounter', {}) +test_prsa1_wright_py, test_prsa1_wright_js = maketst('Fixed', 'pyRCV2.method.base_stv.WIGSTVCounter', {'exclusion': 'wright'}) diff --git a/tests/test_csm.py b/tests/test_csm.py index 5b49381..547c2b9 100644 --- a/tests/test_csm.py +++ b/tests/test_csm.py @@ -1,5 +1,5 @@ # pyRCV2: Preferential vote counting -# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# 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 @@ -19,7 +19,7 @@ from pytest_steps import test_steps import pyRCV2.blt import pyRCV2.numbers -from pyRCV2.method.wright import WrightSTVCounter +from pyRCV2.method.base_stv import WIGSTVCounter from pyRCV2.model import CandidateState, CountCompleted def count_until_exclude(counter): @@ -43,7 +43,7 @@ def test_csm15(): cands = {c.name: c for c in election.candidates} - counter = WrightSTVCounter(election) + counter = WIGSTVCounter(election, {'exclusion': 'wright'}) # Round 1 result = counter.reset()