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>
<br>
<label>
Ties (NYI):
Ties:
<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>
</select>
</label>
<label>
Random seed:
<input type="text" id="txtSeed" value="Not yet implemented">
<input type="text" id="txtSeed" value="">
</label>
</div>
@ -119,6 +119,7 @@
<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/sjcl@1.0.8/sjcl.min.js"></script>
<script src="bundle.js"></script>
<script src="index.js"></script>
</body>

View File

@ -278,6 +278,14 @@ async function clickCount() {
'surplus_order': document.getElementById('selSurplus').value,
'ties': document.getElementById('selTies').value
},
'seed': document.getElementById('txtSeed').value,
'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/>.
*/
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) {
// Set settings
@ -50,12 +50,10 @@ onmessage = function(evt) {
}
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') {
counter.options['ties'] = [py.pyRCV2.ties.TiesRandom()];
} //else if (evt.data.options['ties'] === 'prompt') {
// counter.options['ties'] = [py.pyRCV2.ties.TiesPrompt()];
//}
counter.options['ties'] = [py.pyRCV2.ties.TiesRandom(evt.data.seed)];
}
// 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.ties import TiesBackwards, TiesPrompt, TiesRandom
import sys
def add_parser(subparsers):
parser = subparsers.add_parser('stv', help='single transferable vote')
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('--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('--random-seed', default=None, help='arbitrary string used to seed the RNG for random tie breaking')
def print_step(result):
print(result.comment)
@ -92,7 +95,10 @@ def main(args):
elif t == 'prompt':
counter.options['ties'].append(TiesPrompt())
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
result = counter.reset()

View File

@ -19,7 +19,6 @@ __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
@ -40,7 +39,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()]
'ties': []
}
if options is not None:
@ -335,7 +334,10 @@ class BaseSTVCounter:
if len(l) == 1:
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:
return tied[0]
@ -356,7 +358,10 @@ class BaseSTVCounter:
if len(l) == 1:
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:
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
__pragma__('noskip')
from pyRCV2.random import SHARandom
class TiesPrompt:
"""Prompt the user to break ties"""
@ -71,8 +73,13 @@ class TiesBackwards:
raise Exception('Not yet implemented')
class TiesRandom:
def __init__(self, seed):
self.random = SHARandom(seed)
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):
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.method, pyRCV2.method.base_stv, pyRCV2.method.wright
import pyRCV2.numbers
import pyRCV2.random
import pyRCV2.ties
__pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};')