Begin implementation of tie handling
This commit is contained in:
parent
8347a20675
commit
e3edc80f2b
2
build.sh
2
build.sh
@ -22,4 +22,4 @@ perl -0777 -i -pe 's#key \(a\) > key \(b\) \?#__gt__\(key \(a\), key \(b\)\) \?#
|
||||
rollup __target__/pyRCV2.transcrypt.js -o $OUTFILE --name py -f iife --no-treeshake || exit 1
|
||||
|
||||
# Patch incorrectly generated iterator functions
|
||||
perl -0777 -i -pe "s#'__index0__'#__index0__#g" $OUTFILE || exit 1
|
||||
#perl -0777 -i -pe "s#'__index0__'#__index0__#g" $OUTFILE || exit 1
|
||||
|
@ -49,6 +49,14 @@ onmessage = function(evt) {
|
||||
counter = py.pyRCV2.method.base_stv.BaseWIGSTVCounter(election, evt.data.options);
|
||||
}
|
||||
|
||||
if (evt.data.options['ties'] === 'backwards_random') {
|
||||
counter.options['ties'] = [py.pyRCV2.ties.TiesBackwards(), py.pyRCV2.ties.TiesRandom()];
|
||||
} else if (evt.data.options['ties'] === 'random') {
|
||||
counter.options['ties'] = [py.pyRCV2.ties.TiesRandom()];
|
||||
} //else if (evt.data.options['ties'] === 'prompt') {
|
||||
// counter.options['ties'] = [py.pyRCV2.ties.TiesPrompt()];
|
||||
//}
|
||||
|
||||
// Reset
|
||||
let result = counter.reset();
|
||||
|
||||
|
@ -20,18 +20,20 @@ import pyRCV2.numbers
|
||||
|
||||
from pyRCV2.method.base_stv import BaseUIGSTVCounter, BaseWIGSTVCounter
|
||||
from pyRCV2.method.wright import WrightSTVCounter
|
||||
from pyRCV2.ties import TiesBackwards, TiesPrompt, TiesRandom
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('stv', help='single transferable vote')
|
||||
parser.add_argument('file', help='path to BLT file')
|
||||
|
||||
parser.add_argument('--quota', '-q', choices=['droop', 'droop_exact', 'hare', 'hare_exact'], default='droop', help='quota calculation (default: droop)')
|
||||
parser.add_argument('--quota-criterion', '-qc', choices=['geq', 'gt'], default='geq', help='quota criterion (default: geq)')
|
||||
parser.add_argument('--quota-criterion', '-c', choices=['geq', 'gt'], default='geq', help='quota criterion (default: geq)')
|
||||
parser.add_argument('--prog-quota', action='store_true', help='progressively reducing quota')
|
||||
parser.add_argument('--numbers', '-n', choices=['fixed', 'rational', 'int', 'native'], default='fixed', help='numbers mode (default: fixed)')
|
||||
parser.add_argument('--decimals', type=int, default=5, help='decimal places if --numbers fixed (default: 5)')
|
||||
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', 'wright'], default='wig', help='method of surpluses and exclusions (default: wig - weighted inclusive Gregory)')
|
||||
parser.add_argument('--ties', '-t', action='append', choices=['backwards', 'prompt', 'random'], default=None, help='how to resolve ties (default: backwards then random)')
|
||||
|
||||
def print_step(result):
|
||||
print(result.comment)
|
||||
@ -80,6 +82,18 @@ def main(args):
|
||||
else:
|
||||
counter = BaseWIGSTVCounter(election, vars(args))
|
||||
|
||||
if args.ties is None:
|
||||
args.ties = ['backwards', 'random']
|
||||
|
||||
counter.options['ties'] = []
|
||||
for t in args.ties:
|
||||
if t == 'backwards':
|
||||
counter.options['ties'].append(TiesBackwards())
|
||||
elif t == 'prompt':
|
||||
counter.options['ties'].append(TiesPrompt())
|
||||
elif t == 'random':
|
||||
counter.options['ties'].append(TiesRandom())
|
||||
|
||||
# Reset
|
||||
result = counter.reset()
|
||||
|
||||
|
@ -19,6 +19,7 @@ __pragma__ = lambda x: None
|
||||
from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepResult
|
||||
from pyRCV2.numbers import Num, Rational
|
||||
from pyRCV2.safedict import SafeDict
|
||||
from pyRCV2.ties import TiesBackwards, TiesPrompt, TiesRandom
|
||||
|
||||
class STVException(Exception):
|
||||
pass
|
||||
@ -39,6 +40,7 @@ class BaseSTVCounter:
|
||||
'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
|
||||
'quota_criterion': 'geq', # 'geq' or 'gt'
|
||||
'surplus_order': 'size', # 'size' or 'order'
|
||||
'ties': [TiesBackwards(), TiesRandom()]
|
||||
}
|
||||
|
||||
if options is not None:
|
||||
@ -174,12 +176,13 @@ class BaseSTVCounter:
|
||||
# Distribute surpluses in specified order
|
||||
if self.options['surplus_order'] == 'size':
|
||||
has_surplus.sort(key=lambda x: x[1].votes, reverse=True)
|
||||
candidate_surplus, count_card = self.choose_highest(has_surplus) # May need to break ties
|
||||
elif self.options['surplus_order'] == 'order':
|
||||
has_surplus.sort(key=lambda x: x[1].order_elected)
|
||||
candidate_surplus, count_card = has_surplus[0] # Ties were already broken when these were assigned
|
||||
else:
|
||||
raise STVException('Invalid surplus order option')
|
||||
|
||||
candidate_surplus, count_card = has_surplus[0]
|
||||
count_card.state = CandidateState.ELECTED
|
||||
|
||||
__pragma__('opov')
|
||||
@ -253,8 +256,7 @@ class BaseSTVCounter:
|
||||
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
|
||||
hopefuls.sort(key=lambda x: x[1].votes)
|
||||
|
||||
# TODO: Handle ties
|
||||
candidate_excluded, count_card = hopefuls[0]
|
||||
candidate_excluded, count_card = self.choose_lowest(hopefuls)
|
||||
|
||||
return candidate_excluded, count_card
|
||||
|
||||
@ -312,11 +314,60 @@ class BaseSTVCounter:
|
||||
meets_quota = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL and self.meets_quota(cc)]
|
||||
|
||||
if len(meets_quota) > 0:
|
||||
meets_quota.sort(key=lambda x: x[1].votes, reverse=True)
|
||||
|
||||
# Declare elected any candidate who meets the quota
|
||||
for candidate, count_card in meets_quota:
|
||||
while len(meets_quota) > 0:
|
||||
x = self.choose_highest(meets_quota)
|
||||
candidate, count_card = x[0], x[1]
|
||||
|
||||
count_card.state = CandidateState.PROVISIONALLY_ELECTED
|
||||
count_card.order_elected = self.num_elected
|
||||
self.num_elected += 1
|
||||
|
||||
meets_quota.remove(x)
|
||||
|
||||
def choose_lowest(self, l):
|
||||
"""
|
||||
Provided a list of tuples (Candidate, CountCard), sorted in ASCENDING order of votes, choose the tuple with the fewest votes, breaking ties appropriately
|
||||
"""
|
||||
|
||||
if len(l) == 1:
|
||||
return l[0]
|
||||
|
||||
tied = [(c, cc) for c, cc in l if cc.votes == l[0][1].votes]
|
||||
|
||||
if len(tied) == 1:
|
||||
return tied[0]
|
||||
|
||||
# A tie exists
|
||||
for tie in self.options['ties']:
|
||||
result = tie.choose_lowest(tied)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
raise Exception('Unable to resolve tie')
|
||||
|
||||
def choose_highest(self, l):
|
||||
"""
|
||||
Provided a list of tuples (Candidate, CountCard), sorted in DESCENDING order of votes, choose the tuple with the most votes, breaking ties appropriately
|
||||
"""
|
||||
|
||||
if len(l) == 1:
|
||||
return l[0]
|
||||
|
||||
tied = [(c, cc) for c, cc in l if cc.votes == l[0][1].votes]
|
||||
|
||||
if len(tied) == 1:
|
||||
return tied[0]
|
||||
|
||||
# A tie exists
|
||||
for tie in self.options['ties']:
|
||||
result = tie.choose_highest(tied)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
raise Exception('Unable to resolve tie')
|
||||
|
||||
class BaseWIGSTVCounter(BaseSTVCounter):
|
||||
"""
|
||||
|
@ -48,6 +48,8 @@ class Fixed:
|
||||
def __div__(self, other):
|
||||
return Fixed(self.impl.div(other.impl))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.impl.eq(other.impl)
|
||||
def __gt__(self, other):
|
||||
return self.impl.gt(other.impl)
|
||||
def __ge__(self, other):
|
||||
|
@ -52,6 +52,8 @@ class Fixed:
|
||||
def __truediv__(self, other):
|
||||
return Fixed(self.impl / other.impl)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.impl == other.impl
|
||||
def __gt__(self, other):
|
||||
return self.impl > other.impl
|
||||
def __ge__(self, other):
|
||||
|
@ -43,6 +43,8 @@ class NativeInt:
|
||||
def __div__(self, other):
|
||||
return NativeInt(self.impl / other.impl)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.impl == other.impl
|
||||
def __gt__(self, other):
|
||||
return self.impl > other.impl
|
||||
def __ge__(self, other):
|
||||
|
@ -45,6 +45,8 @@ class NativeInt:
|
||||
def __truediv__(self, other):
|
||||
return NativeInt(self.impl / other.impl)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.impl == other.impl
|
||||
def __gt__(self, other):
|
||||
return self.impl > other.impl
|
||||
def __ge__(self, other):
|
||||
|
@ -43,6 +43,8 @@ class Native:
|
||||
def __div__(self, other):
|
||||
return Native(self.impl / other.impl)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.impl == other.impl
|
||||
def __gt__(self, other):
|
||||
return self.impl > other.impl
|
||||
def __ge__(self, other):
|
||||
|
@ -45,6 +45,8 @@ class Native:
|
||||
def __truediv__(self, other):
|
||||
return Native(self.impl / other.impl)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.impl == other.impl
|
||||
def __gt__(self, other):
|
||||
return self.impl > other.impl
|
||||
def __ge__(self, other):
|
||||
|
@ -53,6 +53,8 @@ class Rational:
|
||||
def __div__(self, other):
|
||||
return Rational(self.impl.divide(other.impl))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.impl.equals(other.impl)
|
||||
def __gt__(self, other):
|
||||
return self.impl.greater(other.impl)
|
||||
def __ge__(self, other):
|
||||
|
@ -54,6 +54,8 @@ class Rational:
|
||||
def __truediv__(self, other):
|
||||
return Rational(self.impl / other.impl)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.impl == other.impl
|
||||
def __gt__(self, other):
|
||||
return self.impl > other.impl
|
||||
def __ge__(self, other):
|
||||
|
78
pyRCV2/ties.py
Normal file
78
pyRCV2/ties.py
Normal file
@ -0,0 +1,78 @@
|
||||
# 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
__pragma__ = lambda x: None
|
||||
is_py = False
|
||||
__pragma__('skip')
|
||||
is_py = True
|
||||
__pragma__('noskip')
|
||||
|
||||
class TiesPrompt:
|
||||
"""Prompt the user to break ties"""
|
||||
|
||||
def choose_lowest(self, l):
|
||||
if is_py:
|
||||
print('Multiple tied candidates:')
|
||||
for i, x in enumerate(l):
|
||||
print('{}. {}'.format(i + 1, x[0].name))
|
||||
|
||||
while True:
|
||||
choice = input('Which candidate to select? ')
|
||||
try:
|
||||
choice = int(choice)
|
||||
if choice >= 1 and choice < len(l) + 1:
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
print()
|
||||
|
||||
return l[i - 1]
|
||||
else:
|
||||
# UNUSED IN JS - Cannot call window.prompt from Web Worker
|
||||
# TODO: Work this out
|
||||
message = ''
|
||||
for i, x in enumerate(l):
|
||||
message += (i + 1) + '. ' + x[0].name
|
||||
message += 'Which candidate to select?'
|
||||
|
||||
while True:
|
||||
choice = window.prompt(message)
|
||||
try:
|
||||
choice = int(choice)
|
||||
if choice >= 1 and choice < len(l) + 1:
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return l[i - 1]
|
||||
|
||||
def choose_highest(self, l):
|
||||
return self.choose_lowest(l)
|
||||
|
||||
class TiesBackwards:
|
||||
def choose_lowest(self, l):
|
||||
raise Exception('Not yet implemented')
|
||||
|
||||
def choose_highest(self, l):
|
||||
raise Exception('Not yet implemented')
|
||||
|
||||
class TiesRandom:
|
||||
def choose_lowest(self, l):
|
||||
raise Exception('Not yet implemented')
|
||||
|
||||
def choose_highest(self, l):
|
||||
raise Exception('Not yet implemented')
|
@ -18,5 +18,6 @@ import pyRCV2.blt
|
||||
import pyRCV2.model
|
||||
import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.wright
|
||||
import pyRCV2.numbers
|
||||
import pyRCV2.ties
|
||||
|
||||
__pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};')
|
||||
|
Reference in New Issue
Block a user