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
# 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);
}
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();

View File

@ -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()

View File

@ -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,12 +314,61 @@ 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):
"""
Basic weighted inclusive Gregory STV counter

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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
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.method, pyRCV2.method.base_stv, pyRCV2.method.wright
import pyRCV2.numbers
import pyRCV2.ties
__pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};')