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
|
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
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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
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.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};')
|
||||||
|
Reference in New Issue
Block a user