Implement prompting for ties

This commit is contained in:
RunasSudo 2020-12-27 23:28:16 +11:00
parent 4e06dfe068
commit 7ad142f19d
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
5 changed files with 135 additions and 90 deletions

View File

@ -82,7 +82,7 @@ This dropdown allows you to select how ties (in surplus transfer or exclusion) a
* Backwards: Ties are broken according to which tied candidate had the most/fewest votes at the end of the last stage where one tied candidate had more/fewer votes than the others, if such a stage exists. * Backwards: Ties are broken according to which tied candidate had the most/fewest votes at the end of the last stage where one tied candidate had more/fewer votes than the others, if such a stage exists.
* Random: Ties are broken at random (see *Random seed*). * Random: Ties are broken at random (see *Random seed*).
* Prompt: The user is prompted to break the tie. This option is not available in the web application. * Prompt: The user is prompted to break the tie.
Multiple tie breaking methods can be specified. If the first method cannot resolve the tie, the next is tried, and so on. In the web application, 2 options are available (‘Backwards then random’ and ‘Random’). On the Python command line, the `--ties` option can be specified multiple times (e.g. `--ties backwards --ties random`). Multiple tie breaking methods can be specified. If the first method cannot resolve the tie, the next is tried, and so on. In the web application, 2 options are available (‘Backwards then random’ and ‘Random’). On the Python command line, the `--ties` option can be specified multiple times (e.g. `--ties backwards --ties random`).

View File

@ -115,6 +115,7 @@
<select id="selTies"> <select id="selTies">
<option value="backwards_random" selected>Backwards then random</option> <option value="backwards_random" selected>Backwards then random</option>
<option value="random">Random</option> <option value="random">Random</option>
<option value="prompt">Prompt</option>
</select> </select>
</label> </label>
<label> <label>

View File

@ -98,6 +98,13 @@ async function clickCount() {
let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota; let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota;
worker.onmessage = function(evt) { worker.onmessage = function(evt) {
if (evt.data.type === 'require_input') {
let input = window.prompt(evt.data.message);
if (input !== null) {
worker.postMessage({'type': 'require_input', 'input': input});
}
}
if (evt.data.type === 'init') { if (evt.data.type === 'init') {
election = evt.data.election; election = evt.data.election;
@ -285,20 +292,23 @@ async function clickCount() {
} }
worker.postMessage({ worker.postMessage({
'numbers': document.getElementById('selNumbers').value, 'type': 'init',
'fixedDPs': parseInt(document.getElementById('txtDP').value), 'data': {
'transfers': document.getElementById('selTransfers').value, 'numbers': document.getElementById('selNumbers').value,
'options': { 'fixedDPs': parseInt(document.getElementById('txtDP').value),
'quota_criterion': document.getElementById('selQuotaCriterion').value, 'transfers': document.getElementById('selTransfers').value,
'quota': document.getElementById('selQuota').value, 'options': {
'prog_quota': document.getElementById('chkProgQuota').checked, 'quota_criterion': document.getElementById('selQuotaCriterion').value,
'bulk_elect': document.getElementById('chkBulkElection').checked, 'quota': document.getElementById('selQuota').value,
'bulk_exclude': document.getElementById('chkBulkExclusion').checked, 'prog_quota': document.getElementById('chkProgQuota').checked,
'surplus_order': document.getElementById('selSurplus').value, 'bulk_elect': document.getElementById('chkBulkElection').checked,
'ties': document.getElementById('selTies').value 'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
}, 'surplus_order': document.getElementById('selSurplus').value,
'seed': document.getElementById('txtSeed').value, 'ties': document.getElementById('selTies').value
'data': text },
'seed': document.getElementById('txtSeed').value,
'data': text
}
}); });
} }

View File

@ -18,72 +18,96 @@
importScripts('vendor/BigInt_BigRat-a5f89e2.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'); importScripts('vendor/BigInt_BigRat-a5f89e2.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');
let result, counter, ppDP, tiesPrompt;
onmessage = function(evt) { onmessage = function(evt) {
// Set settings if (evt.data.type === 'init') {
if (evt.data.numbers === 'native') { // Set settings
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Native); if (evt.data.data.numbers === 'native') {
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Native);
}
if (evt.data.data.numbers === 'int') {
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.NativeInt);
}
if (evt.data.data.numbers === 'rational') {
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Rational);
}
if (evt.data.data.numbers === 'fixed') {
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Fixed);
py.pyRCV2.numbers.set_dps(evt.data.data.fixedDPs);
}
ppDP = evt.data.data.fixedDPs > 2 ? 2 : evt.data.data.fixedDPs;
let election = py.pyRCV2.blt.readBLT(evt.data.data.data);
postMessage({'type': 'init', 'election': {
'candidates': election.candidates.map(c => c.py_name)
}});
// Create counter
if (evt.data.data.transfers === 'uig') {
counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.data.options);
} else if (evt.data.data.transfers === 'parcelled_eg') {
counter = py.pyRCV2.method.parcels.ParcelledEGSTVCounter(election, evt.data.data.options);
} else if (evt.data.data.transfers === 'wright') {
counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.data.options);
} else {
counter = py.pyRCV2.method.base_stv.WIGSTVCounter(election, evt.data.data.options);
}
if (evt.data.data.options['ties'] === 'backwards_random') {
counter.options['ties'] = [py.pyRCV2.ties.TiesBackwards(counter), py.pyRCV2.ties.TiesRandom(evt.data.data.seed)];
} else if (evt.data.data.options['ties'] === 'random') {
counter.options['ties'] = [py.pyRCV2.ties.TiesRandom(evt.data.data.seed)];
} else if (evt.data.data.options['ties'] === 'prompt') {
tiesPrompt = py.pyRCV2.ties.TiesPrompt();
counter.options['ties'] = [tiesPrompt];
}
// Reset
result = counter.reset();
postMessage({'type': 'result', 'result': {
'comment': result.comment,
'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, {
'transfers': cc.transfers.pp(ppDP),
'votes': cc.votes.pp(ppDP),
'state': cc.state
}]),
'exhausted': {
'transfers': result.exhausted.transfers.pp(ppDP),
'votes': result.exhausted.votes.pp(ppDP)
},
'loss_fraction': {
'transfers': result.loss_fraction.transfers.pp(ppDP),
'votes': result.loss_fraction.votes.pp(ppDP)
},
'total': result.total.pp(ppDP),
'quota': result.quota.pp(ppDP)
}});
stepElection();
} else if (evt.data.type === 'require_input') {
// Data received from the user
tiesPrompt.buffer = evt.data.input;
stepElection();
} }
if (evt.data.numbers === 'int') { }
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.NativeInt);
} function stepElection() {
if (evt.data.numbers === 'rational') {
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Rational);
}
if (evt.data.numbers === 'fixed') {
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Fixed);
py.pyRCV2.numbers.set_dps(evt.data.fixedDPs);
}
let ppDP = evt.data.fixedDPs > 2 ? 2 : evt.data.fixedDPs;
let election = py.pyRCV2.blt.readBLT(evt.data.data);
postMessage({'type': 'init', 'election': {
'candidates': election.candidates.map(c => c.py_name)
}});
// Create counter
let counter;
if (evt.data.transfers === 'uig') {
counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.options);
} else if (evt.data.transfers === 'parcelled_eg') {
counter = py.pyRCV2.method.parcels.ParcelledEGSTVCounter(election, evt.data.options);
} else if (evt.data.transfers === 'wright') {
counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.options);
} else {
counter = py.pyRCV2.method.base_stv.WIGSTVCounter(election, evt.data.options);
}
if (evt.data.options['ties'] === 'backwards_random') {
counter.options['ties'] = [py.pyRCV2.ties.TiesBackwards(counter), py.pyRCV2.ties.TiesRandom(evt.data.seed)];
} else if (evt.data.options['ties'] === 'random') {
counter.options['ties'] = [py.pyRCV2.ties.TiesRandom(evt.data.seed)];
}
// Reset
let result = counter.reset();
postMessage({'type': 'result', 'result': {
'comment': result.comment,
'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, {
'transfers': cc.transfers.pp(ppDP),
'votes': cc.votes.pp(ppDP),
'state': cc.state
}]),
'exhausted': {
'transfers': result.exhausted.transfers.pp(ppDP),
'votes': result.exhausted.votes.pp(ppDP)
},
'loss_fraction': {
'transfers': result.loss_fraction.transfers.pp(ppDP),
'votes': result.loss_fraction.votes.pp(ppDP)
},
'total': result.total.pp(ppDP),
'quota': result.quota.pp(ppDP)
}});
// Step election
while (true) { while (true) {
result = counter.step(); try {
result = counter.step();
} catch (ex) {
if (py.isinstance(ex, py.pyRCV2.ties.RequireInput)) {
// Signals we require input to break a tie
postMessage({'type': 'require_input', 'message': ex.message});
break;
} else {
throw ex;
}
}
if (py.isinstance(result, py.pyRCV2.model.CountCompleted)) { if (py.isinstance(result, py.pyRCV2.model.CountCompleted)) {
postMessage({'type': 'done'}); postMessage({'type': 'done'});
break; break;

View File

@ -22,9 +22,17 @@ __pragma__('noskip')
from pyRCV2.random import SHARandom from pyRCV2.random import SHARandom
class RequireInput(Exception):
# Exceptions for control flow? In my code? It's more likely thank you think!
def __init__(self, message):
self.message = message
class TiesPrompt: class TiesPrompt:
"""Prompt the user to break ties""" """Prompt the user to break ties"""
def __init__(self):
self.buffer = None
def choose_lowest(self, l): def choose_lowest(self, l):
if is_py: if is_py:
print('Multiple tied candidates:') print('Multiple tied candidates:')
@ -44,23 +52,25 @@ class TiesPrompt:
return l[i - 1] return l[i - 1]
else: else:
# UNUSED IN JS - Cannot call window.prompt from Web Worker if self.buffer is not None:
# 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: try:
choice = int(choice) choice = int(self.buffer)
if choice >= 1 and choice < len(l) + 1: if choice >= 1 and choice < len(l) + 1:
break self.buffer = None
return l[choice - 1]
except ValueError: except ValueError:
self.buffer = None
pass pass
return l[i - 1] self.buffer = None
# Require prompting
message = 'Multiple tied candidates:\n'
for i, x in enumerate(l):
message += (i + 1) + '. ' + x[0].name + '\n'
message += 'Which candidate to select?'
raise RequireInput(message)
def choose_highest(self, l): def choose_highest(self, l):
return self.choose_lowest(l) return self.choose_lowest(l)