Begin implementation of tie handling

This commit is contained in:
RunasSudo 2020-12-23 22:36:49 +11:00
parent 8347a20675
commit e3edc80f2b
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
14 changed files with 174 additions and 6 deletions

View File

@ -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 rollup __target__/pyRCV2.transcrypt.js -o $OUTFILE --name py -f iife --no-treeshake || exit 1
# Patch incorrectly generated iterator functions # 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

View File

@ -49,6 +49,14 @@ onmessage = function(evt) {
counter = py.pyRCV2.method.base_stv.BaseWIGSTVCounter(election, evt.data.options); 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 // Reset
let result = counter.reset(); let result = counter.reset();

View File

@ -20,18 +20,20 @@ import pyRCV2.numbers
from pyRCV2.method.base_stv import BaseUIGSTVCounter, BaseWIGSTVCounter from pyRCV2.method.base_stv import BaseUIGSTVCounter, BaseWIGSTVCounter
from pyRCV2.method.wright import WrightSTVCounter from pyRCV2.method.wright import WrightSTVCounter
from pyRCV2.ties import TiesBackwards, TiesPrompt, TiesRandom
def add_parser(subparsers): def add_parser(subparsers):
parser = subparsers.add_parser('stv', help='single transferable vote') parser = subparsers.add_parser('stv', help='single transferable vote')
parser.add_argument('file', help='path to BLT file') 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', '-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('--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('--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('--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('--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('--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): def print_step(result):
print(result.comment) print(result.comment)
@ -80,6 +82,18 @@ def main(args):
else: else:
counter = BaseWIGSTVCounter(election, vars(args)) 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 # Reset
result = counter.reset() result = counter.reset()

View File

@ -19,6 +19,7 @@ __pragma__ = lambda x: None
from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepResult from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepResult
from pyRCV2.numbers import Num, Rational from pyRCV2.numbers import Num, Rational
from pyRCV2.safedict import SafeDict from pyRCV2.safedict import SafeDict
from pyRCV2.ties import TiesBackwards, TiesPrompt, TiesRandom
class STVException(Exception): class STVException(Exception):
pass pass
@ -39,6 +40,7 @@ class BaseSTVCounter:
'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact' 'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
'quota_criterion': 'geq', # 'geq' or 'gt' 'quota_criterion': 'geq', # 'geq' or 'gt'
'surplus_order': 'size', # 'size' or 'order' 'surplus_order': 'size', # 'size' or 'order'
'ties': [TiesBackwards(), TiesRandom()]
} }
if options is not None: if options is not None:
@ -174,12 +176,13 @@ class BaseSTVCounter:
# Distribute surpluses in specified order # Distribute surpluses in specified order
if self.options['surplus_order'] == 'size': if self.options['surplus_order'] == 'size':
has_surplus.sort(key=lambda x: x[1].votes, reverse=True) 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': elif self.options['surplus_order'] == 'order':
has_surplus.sort(key=lambda x: x[1].order_elected) 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: else:
raise STVException('Invalid surplus order option') raise STVException('Invalid surplus order option')
candidate_surplus, count_card = has_surplus[0]
count_card.state = CandidateState.ELECTED count_card.state = CandidateState.ELECTED
__pragma__('opov') __pragma__('opov')
@ -253,8 +256,7 @@ class BaseSTVCounter:
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL] hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
hopefuls.sort(key=lambda x: x[1].votes) hopefuls.sort(key=lambda x: x[1].votes)
# TODO: Handle ties candidate_excluded, count_card = self.choose_lowest(hopefuls)
candidate_excluded, count_card = hopefuls[0]
return candidate_excluded, count_card 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)] 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: if len(meets_quota) > 0:
meets_quota.sort(key=lambda x: x[1].votes, reverse=True)
# Declare elected any candidate who meets the quota # 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.state = CandidateState.PROVISIONALLY_ELECTED
count_card.order_elected = self.num_elected count_card.order_elected = self.num_elected
self.num_elected += 1 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): class BaseWIGSTVCounter(BaseSTVCounter):
""" """

View File

@ -48,6 +48,8 @@ class Fixed:
def __div__(self, other): def __div__(self, other):
return Fixed(self.impl.div(other.impl)) return Fixed(self.impl.div(other.impl))
def __eq__(self, other):
return self.impl.eq(other.impl)
def __gt__(self, other): def __gt__(self, other):
return self.impl.gt(other.impl) return self.impl.gt(other.impl)
def __ge__(self, other): def __ge__(self, other):

View File

@ -52,6 +52,8 @@ class Fixed:
def __truediv__(self, other): def __truediv__(self, other):
return Fixed(self.impl / other.impl) return Fixed(self.impl / other.impl)
def __eq__(self, other):
return self.impl == other.impl
def __gt__(self, other): def __gt__(self, other):
return self.impl > other.impl return self.impl > other.impl
def __ge__(self, other): def __ge__(self, other):

View File

@ -43,6 +43,8 @@ class NativeInt:
def __div__(self, other): def __div__(self, other):
return NativeInt(self.impl / other.impl) return NativeInt(self.impl / other.impl)
def __eq__(self, other):
return self.impl == other.impl
def __gt__(self, other): def __gt__(self, other):
return self.impl > other.impl return self.impl > other.impl
def __ge__(self, other): def __ge__(self, other):

View File

@ -45,6 +45,8 @@ class NativeInt:
def __truediv__(self, other): def __truediv__(self, other):
return NativeInt(self.impl / other.impl) return NativeInt(self.impl / other.impl)
def __eq__(self, other):
return self.impl == other.impl
def __gt__(self, other): def __gt__(self, other):
return self.impl > other.impl return self.impl > other.impl
def __ge__(self, other): def __ge__(self, other):

View File

@ -43,6 +43,8 @@ class Native:
def __div__(self, other): def __div__(self, other):
return Native(self.impl / other.impl) return Native(self.impl / other.impl)
def __eq__(self, other):
return self.impl == other.impl
def __gt__(self, other): def __gt__(self, other):
return self.impl > other.impl return self.impl > other.impl
def __ge__(self, other): def __ge__(self, other):

View File

@ -45,6 +45,8 @@ class Native:
def __truediv__(self, other): def __truediv__(self, other):
return Native(self.impl / other.impl) return Native(self.impl / other.impl)
def __eq__(self, other):
return self.impl == other.impl
def __gt__(self, other): def __gt__(self, other):
return self.impl > other.impl return self.impl > other.impl
def __ge__(self, other): def __ge__(self, other):

View File

@ -53,6 +53,8 @@ class Rational:
def __div__(self, other): def __div__(self, other):
return Rational(self.impl.divide(other.impl)) return Rational(self.impl.divide(other.impl))
def __eq__(self, other):
return self.impl.equals(other.impl)
def __gt__(self, other): def __gt__(self, other):
return self.impl.greater(other.impl) return self.impl.greater(other.impl)
def __ge__(self, other): def __ge__(self, other):

View File

@ -54,6 +54,8 @@ class Rational:
def __truediv__(self, other): def __truediv__(self, other):
return Rational(self.impl / other.impl) return Rational(self.impl / other.impl)
def __eq__(self, other):
return self.impl == other.impl
def __gt__(self, other): def __gt__(self, other):
return self.impl > other.impl return self.impl > other.impl
def __ge__(self, other): def __ge__(self, other):

78
pyRCV2/ties.py Normal file
View 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')

View File

@ -18,5 +18,6 @@ import pyRCV2.blt
import pyRCV2.model import pyRCV2.model
import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.wright import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.wright
import pyRCV2.numbers import pyRCV2.numbers
import pyRCV2.ties
__pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};') __pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};')