Implement random breaking of ties

This commit is contained in:
RunasSudo 2020-12-24 00:04:30 +11:00
parent e3edc80f2b
commit 06ab133615
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
10 changed files with 138 additions and 16 deletions

View File

@ -103,15 +103,15 @@
</label> </label>
<br> <br>
<label> <label>
Ties (NYI): Ties:
<select id="selTies"> <select id="selTies">
<option value="backwards_random" selected>Backwards then random</option> <option value="backwards_random" selected>Backwards then random (NYI)</option>
<option value="random">Random</option> <option value="random">Random</option>
</select> </select>
</label> </label>
<label> <label>
Random seed: Random seed:
<input type="text" id="txtSeed" value="Not yet implemented"> <input type="text" id="txtSeed" value="">
</label> </label>
</div> </div>
@ -119,6 +119,7 @@
<script src="http://peterolson.github.com/BigRational.js/BigInt_BigRat.min.js"></script> <script src="http://peterolson.github.com/BigRational.js/BigInt_BigRat.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/big.js@6.0.0/big.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/big.js@6.0.0/big.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sjcl@1.0.8/sjcl.min.js"></script>
<script src="bundle.js"></script> <script src="bundle.js"></script>
<script src="index.js"></script> <script src="index.js"></script>
</body> </body>

View File

@ -278,6 +278,14 @@ async function clickCount() {
'surplus_order': document.getElementById('selSurplus').value, 'surplus_order': document.getElementById('selSurplus').value,
'ties': document.getElementById('selTies').value 'ties': document.getElementById('selTies').value
}, },
'seed': document.getElementById('txtSeed').value,
'data': text 'data': text
}); });
} }
// Provide a default seed
if (document.getElementById('txtSeed').value === '') {
function pad(x) { if (x < 10) { return '0' + x; } return '' + x; }
let d = new Date();
document.getElementById('txtSeed').value = d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate());
}

View File

@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
importScripts('http://peterolson.github.com/BigRational.js/BigInt_BigRat.min.js', 'https://cdn.jsdelivr.net/npm/big.js@6.0.0/big.min.js', 'bundle.js'); importScripts('http://peterolson.github.com/BigRational.js/BigInt_BigRat.min.js', 'https://cdn.jsdelivr.net/npm/big.js@6.0.0/big.min.js', 'https://cdn.jsdelivr.net/npm/sjcl@1.0.8/sjcl.min.js', 'bundle.js');
onmessage = function(evt) { onmessage = function(evt) {
// Set settings // Set settings
@ -50,12 +50,10 @@ onmessage = function(evt) {
} }
if (evt.data.options['ties'] === 'backwards_random') { if (evt.data.options['ties'] === 'backwards_random') {
counter.options['ties'] = [py.pyRCV2.ties.TiesBackwards(), py.pyRCV2.ties.TiesRandom()]; counter.options['ties'] = [py.pyRCV2.ties.TiesBackwards(), py.pyRCV2.ties.TiesRandom(evt.data.seed)];
} else if (evt.data.options['ties'] === 'random') { } else if (evt.data.options['ties'] === 'random') {
counter.options['ties'] = [py.pyRCV2.ties.TiesRandom()]; counter.options['ties'] = [py.pyRCV2.ties.TiesRandom(evt.data.seed)];
} //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

@ -22,6 +22,8 @@ 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 from pyRCV2.ties import TiesBackwards, TiesPrompt, TiesRandom
import sys
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')
@ -34,6 +36,7 @@ def add_parser(subparsers):
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)') 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')
def print_step(result): def print_step(result):
print(result.comment) print(result.comment)
@ -92,7 +95,10 @@ def main(args):
elif t == 'prompt': elif t == 'prompt':
counter.options['ties'].append(TiesPrompt()) counter.options['ties'].append(TiesPrompt())
elif t == 'random': elif t == 'random':
counter.options['ties'].append(TiesRandom()) if args.random_seed is None:
print('A --random-seed is required to use random tie breaking')
sys.exit(1)
counter.options['ties'].append(TiesRandom(args.random_seed))
# Reset # Reset
result = counter.reset() result = counter.reset()

View File

@ -19,7 +19,6 @@ __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
@ -40,7 +39,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()] 'ties': []
} }
if options is not None: if options is not None:
@ -335,7 +334,10 @@ class BaseSTVCounter:
if len(l) == 1: if len(l) == 1:
return l[0] return l[0]
tied = [(c, cc) for c, cc in l if cc.votes == l[0][1].votes] __pragma__('opov')
# Do not use (c, cc) for c, cc in ... as this will break equality in JS
tied = [x for x in l if x[1].votes == l[0][1].votes]
__pragma__('noopov')
if len(tied) == 1: if len(tied) == 1:
return tied[0] return tied[0]
@ -356,7 +358,10 @@ class BaseSTVCounter:
if len(l) == 1: if len(l) == 1:
return l[0] return l[0]
tied = [(c, cc) for c, cc in l if cc.votes == l[0][1].votes] __pragma__('opov')
# Do not use (c, cc) for c, cc in ... as this will break equality in JS
tied = [x for x in l if x[1].votes == l[0][1].votes]
__pragma__('noopov')
if len(tied) == 1: if len(tied) == 1:
return tied[0] return tied[0]

28
pyRCV2/random/__init__.py Normal file
View File

@ -0,0 +1,28 @@
# 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')
if is_py:
__pragma__('skip')
from pyRCV2.random.sharandom_py import SHARandom
__pragma__('noskip')
else:
from pyRCV2.random.sharandom_js import SHARandom

View File

@ -0,0 +1,32 @@
# 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/>.
class SHARandom:
MAX_VAL = bigInt(2).pow(256).subtract(1)
def __init__(self, seed):
self.seed = seed
self.ctr = 0
def next(self, modulus):
val = bigInt(sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(self.seed + ',' + str(self.ctr))), 16)
self.ctr += 1
if val.greaterOrEquals(SHARandom.MAX_VAL.divide(modulus).multiply(modulus)):
# Discard this value to avoid bias
return self.next(modulus)
return val.mod(modulus)

View File

@ -0,0 +1,36 @@
# 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/>.
import hashlib
class SHARandom:
MAX_VAL = 2 ** 256 - 1
def __init__(self, seed):
self.seed = seed
self.ctr = 0
def next(self, modulus):
c = hashlib.sha256()
c.update((self.seed + ',' + str(self.ctr)).encode('utf-8'))
self.ctr += 1
val = int.from_bytes(c.digest(), byteorder='big', signed=False)
if val >= (SHARandom.MAX_VAL // modulus) * modulus:
# Discard this value to avoid bias
return self.next(modulus)
return val % modulus

View File

@ -20,6 +20,8 @@ __pragma__('skip')
is_py = True is_py = True
__pragma__('noskip') __pragma__('noskip')
from pyRCV2.random import SHARandom
class TiesPrompt: class TiesPrompt:
"""Prompt the user to break ties""" """Prompt the user to break ties"""
@ -71,8 +73,13 @@ class TiesBackwards:
raise Exception('Not yet implemented') raise Exception('Not yet implemented')
class TiesRandom: class TiesRandom:
def __init__(self, seed):
self.random = SHARandom(seed)
def choose_lowest(self, l): def choose_lowest(self, l):
raise Exception('Not yet implemented') l.sort(key=lambda x: x[0].name)
return l[self.random.next(len(l))]
def choose_highest(self, l): def choose_highest(self, l):
raise Exception('Not yet implemented') l.sort(key=lambda x: x[0].name)
return l[self.random.next(len(l))]

View File

@ -18,6 +18,7 @@ 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.random
import pyRCV2.ties import pyRCV2.ties
__pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};') __pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};')