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.
* 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`).

View File

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

View File

@ -98,6 +98,13 @@ async function clickCount() {
let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota;
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') {
election = evt.data.election;
@ -285,20 +292,23 @@ async function clickCount() {
}
worker.postMessage({
'numbers': document.getElementById('selNumbers').value,
'fixedDPs': parseInt(document.getElementById('txtDP').value),
'transfers': document.getElementById('selTransfers').value,
'options': {
'quota_criterion': document.getElementById('selQuotaCriterion').value,
'quota': document.getElementById('selQuota').value,
'prog_quota': document.getElementById('chkProgQuota').checked,
'bulk_elect': document.getElementById('chkBulkElection').checked,
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
'surplus_order': document.getElementById('selSurplus').value,
'ties': document.getElementById('selTies').value
},
'seed': document.getElementById('txtSeed').value,
'data': text
'type': 'init',
'data': {
'numbers': document.getElementById('selNumbers').value,
'fixedDPs': parseInt(document.getElementById('txtDP').value),
'transfers': document.getElementById('selTransfers').value,
'options': {
'quota_criterion': document.getElementById('selQuotaCriterion').value,
'quota': document.getElementById('selQuota').value,
'prog_quota': document.getElementById('chkProgQuota').checked,
'bulk_elect': document.getElementById('chkBulkElection').checked,
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
'surplus_order': document.getElementById('selSurplus').value,
'ties': document.getElementById('selTies').value
},
'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');
let result, counter, ppDP, tiesPrompt;
onmessage = function(evt) {
// Set settings
if (evt.data.numbers === 'native') {
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Native);
if (evt.data.type === 'init') {
// Set settings
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);
}
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
}
function stepElection() {
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)) {
postMessage({'type': 'done'});
break;

View File

@ -22,9 +22,17 @@ __pragma__('noskip')
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:
"""Prompt the user to break ties"""
def __init__(self):
self.buffer = None
def choose_lowest(self, l):
if is_py:
print('Multiple tied candidates:')
@ -44,23 +52,25 @@ class TiesPrompt:
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)
if self.buffer is not None:
try:
choice = int(choice)
choice = int(self.buffer)
if choice >= 1 and choice < len(l) + 1:
break
self.buffer = None
return l[choice - 1]
except ValueError:
self.buffer = None
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):
return self.choose_lowest(l)