Implement un/weighted inclusive Gregory transfers

This commit is contained in:
RunasSudo 2020-10-18 20:41:48 +11:00
parent 643e23b6ec
commit e0c56c5b1d
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 208 additions and 69 deletions

View File

@ -39,7 +39,7 @@
<select id="selPreset" onchange="changePreset()"> <select id="selPreset" onchange="changePreset()">
<option value="scottish" selected>Scottish STV</option> <option value="scottish" selected>Scottish STV</option>
<option value="stvc">pyRCV STV-C</option> <option value="stvc">pyRCV STV-C</option>
<option value="prsa77">PRSA 1977</option> <option value="senate">Australian Senate STV</option>
</select> </select>
</label> </label>
<button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button> <button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button>
@ -92,7 +92,7 @@
</select> </select>
</label> </label>
<label> <label>
Transfer value (NYI): Transfer value:
<select id="selTransfers"> <select id="selTransfers">
<option value="wig" selected>Weighted inclusive Gregory</option> <option value="wig" selected>Weighted inclusive Gregory</option>
<option value="uig">Unweighted inclusive Gregory</option> <option value="uig">Unweighted inclusive Gregory</option>

View File

@ -46,7 +46,7 @@ function changePreset() {
document.getElementById('selSurplus').value = 'size'; document.getElementById('selSurplus').value = 'size';
document.getElementById('selTransfers').value = 'wig'; document.getElementById('selTransfers').value = 'wig';
document.getElementById('selTies').value = 'backwards_random'; document.getElementById('selTies').value = 'backwards_random';
} else if (document.getElementById('selPreset').value === 'prsa77') { } else if (document.getElementById('selPreset').value === 'senate') {
document.getElementById('selQuotaCriterion').value = 'geq'; document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop'; document.getElementById('selQuota').value = 'droop';
document.getElementById('chkProgQuota').checked = false; document.getElementById('chkProgQuota').checked = false;
@ -126,10 +126,8 @@ async function clickCount() {
elTd.innerText = 'Loss to fraction'; elTd.innerText = 'Loss to fraction';
elLTF1.appendChild(elTd); elLTF1.appendChild(elTd);
if (document.getElementById('selNumbers').value === 'int') { tblResults.appendChild(elLTF1);
tblResults.appendChild(elLTF1); tblResults.appendChild(elLTF2);
tblResults.appendChild(elLTF2);
}
// Total row // Total row
elTotal = document.createElement('tr'); elTotal = document.createElement('tr');
@ -170,7 +168,7 @@ async function clickCount() {
elTd.classList.add('elected'); elTd.classList.add('elected');
} }
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
if (countCard.transfers != '0.00') { if (countCard.transfers != '0.00' && countCard.transfers != '0') {
elTd.innerText = countCard.transfers; elTd.innerText = countCard.transfers;
} }
elTr1.appendChild(elTd); elTr1.appendChild(elTd);
@ -198,7 +196,7 @@ async function clickCount() {
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.classList.add('count'); elTd.classList.add('count');
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
if (result.exhausted.transfers != '0.00') { if (result.exhausted.transfers != '0.00' && result.exhausted.transfers != '0') {
elTd.innerText = result.exhausted.transfers; elTd.innerText = result.exhausted.transfers;
} }
elExhausted1.appendChild(elTd); elExhausted1.appendChild(elTd);
@ -212,14 +210,18 @@ async function clickCount() {
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.classList.add('count'); elTd.classList.add('count');
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
if (result.loss_fraction.transfers != '0.00') { if (result.loss_fraction.transfers != '0.00' && result.loss_fraction.transfers != '-0.00' && result.loss_fraction.transfers != '0') {
elTd.innerText = result.loss_fraction.transfers; elTd.innerText = result.loss_fraction.transfers;
} }
elLTF1.appendChild(elTd); elLTF1.appendChild(elTd);
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.classList.add('count'); elTd.classList.add('count');
elTd.innerText = result.loss_fraction.votes; if (result.loss_fraction.votes == '-0.00') {
elTd.innerText = '0.00';
} else {
elTd.innerText = result.loss_fraction.votes;
}
elLTF2.appendChild(elTd); elLTF2.appendChild(elTd);
// Display total // Display total
@ -246,13 +248,13 @@ async function clickCount() {
worker.postMessage({ worker.postMessage({
'numbers': document.getElementById('selNumbers').value, 'numbers': document.getElementById('selNumbers').value,
'fixedDPs': parseInt(document.getElementById('txtDP').value), 'fixedDPs': parseInt(document.getElementById('txtDP').value),
'transfers': document.getElementById('selTransfers').value,
'options': { 'options': {
'quota_criterion': document.getElementById('selQuotaCriterion').value, 'quota_criterion': document.getElementById('selQuotaCriterion').value,
'quota': document.getElementById('selQuota').value, 'quota': document.getElementById('selQuota').value,
'prog_quota': document.getElementById('chkProgQuota').checked, 'prog_quota': document.getElementById('chkProgQuota').checked,
'bulk_exclude': document.getElementById('chkBulkExclusion').checked, 'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
'surplus_order': document.getElementById('selSurplus').value, 'surplus_order': document.getElementById('selSurplus').value,
'transfer_value': document.getElementById('selTransfers').value,
'ties': document.getElementById('selTies').value 'ties': document.getElementById('selTies').value
}, },
'data': text 'data': text

View File

@ -40,7 +40,12 @@ onmessage = function(evt) {
}}); }});
// Create counter // Create counter
let counter = py.pyRCV2.method.base_stv.BaseSTVCounter(election, evt.data.options); let counter;
if (evt.data.transfers === 'wig') {
counter = py.pyRCV2.method.base_stv.BaseWIGSTVCounter(election, evt.data.options);
} else {
counter = py.pyRCV2.method.base_stv.BaseUIGSTVCounter(election, evt.data.options);
}
// Reset // Reset
let result = counter.reset(); let result = counter.reset();

View File

@ -17,14 +17,16 @@
__pragma__ = lambda x: None __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 from pyRCV2.numbers import Num, Rational
from pyRCV2.safedict import SafeDict from pyRCV2.safedict import SafeDict
class STVException(Exception): class STVException(Exception):
pass pass
class BaseSTVCounter: class BaseSTVCounter:
"""Basic STV counter for various different variations""" """
Basic STV counter for various different variations
"""
def __init__(self, election, options=None): def __init__(self, election, options=None):
self.election = election self.election = election
@ -54,34 +56,6 @@ class BaseSTVCounter:
__pragma__('opov') __pragma__('opov')
self.candidates[candidate].state = CandidateState.WITHDRAWN self.candidates[candidate].state = CandidateState.WITHDRAWN
__pragma__('noopov') __pragma__('noopov')
# Distribute first preferences
for ballot in self.election.ballots:
__pragma__('opov')
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
self.candidates[candidate].transfers += ballot.value
self.candidates[candidate].ballots.append((ballot, ballot.value))
else:
self.exhausted.transfers += ballot.value
self.exhausted.ballots.append((ballot, ballot.value))
__pragma__('noopov')
self.quota = None
self.compute_quota()
self.elect_meeting_quota()
__pragma__('opov')
return CountStepResult(
'First preferences',
self.candidates,
self.exhausted,
self.loss_fraction,
self.total + self.exhausted.votes + self.loss_fraction.votes,
self.quota
)
__pragma__('noopov')
def step(self): def step(self):
# Step count cards # Step count cards
@ -135,22 +109,10 @@ class BaseSTVCounter:
__pragma__('opov') __pragma__('opov')
surplus = count_card.votes - self.quota surplus = count_card.votes - self.quota
#transfer_value = surplus / count_card.votes # Do not do this yet to avoid rounding errors
__pragma__('noopov') __pragma__('noopov')
# Transfer surplus # Transfer surplus
for ballot, ballot_value in count_card.ballots: self.transfer_surplus(candidate_surplus, count_card, surplus)
__pragma__('opov')
new_value = (ballot_value * surplus) / count_card.votes
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
self.candidates[candidate].transfers += new_value
self.candidates[candidate].ballots.append((ballot, new_value))
else:
self.exhausted.transfers += new_value
self.exhausted.ballots.append((ballot, new_value))
__pragma__('noopov')
__pragma__('opov') __pragma__('opov')
count_card.transfers -= surplus count_card.transfers -= surplus
@ -179,23 +141,14 @@ class BaseSTVCounter:
# TODO: Handle ties # TODO: Handle ties
candidate_excluded, count_card = hopefuls[0] candidate_excluded, count_card = hopefuls[0]
count_card.state = CandidateState.EXCLUDED count_card.state = CandidateState.EXCLUDED
# Exclude this candidate
self.exclude_candidate(candidate_excluded, count_card)
__pragma__('opov') __pragma__('opov')
count_card.transfers -= count_card.votes count_card.transfers -= count_card.votes
__pragma__('noopov') __pragma__('noopov')
# Exclude this candidate
for ballot, ballot_value in count_card.ballots:
__pragma__('opov')
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
self.candidates[candidate].transfers += ballot_value
self.candidates[candidate].ballots.append((ballot, ballot_value))
else:
self.exhausted.transfers += ballot_value
self.exhausted.ballots.append((ballot, ballot_value))
__pragma__('noopov')
# Declare any candidates meeting the quota as a result of exclusion # Declare any candidates meeting the quota as a result of exclusion
self.compute_quota() self.compute_quota()
self.elect_meeting_quota() self.elect_meeting_quota()
@ -252,3 +205,182 @@ class BaseSTVCounter:
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
class BaseWIGSTVCounter(BaseSTVCounter):
"""
Basic weighted inclusive Gregory STV counter
"""
def reset(self):
BaseSTVCounter.reset(self)
# Distribute first preferences
for ballot in self.election.ballots:
__pragma__('opov')
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
self.candidates[candidate].transfers += ballot.value
self.candidates[candidate].ballots.append((ballot, ballot.value))
else:
self.exhausted.transfers += ballot.value
self.exhausted.ballots.append((ballot, ballot.value))
__pragma__('noopov')
self.quota = None
self.compute_quota()
self.elect_meeting_quota()
__pragma__('opov')
return CountStepResult(
'First preferences',
self.candidates,
self.exhausted,
self.loss_fraction,
self.total + self.exhausted.votes + self.loss_fraction.votes,
self.quota
)
__pragma__('noopov')
def transfer_surplus(self, candidate_surplus, count_card, surplus):
for ballot, ballot_value in count_card.ballots:
__pragma__('opov')
new_value = (ballot_value * surplus) / count_card.votes # Multiply first to avoid rounding errors
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
self.candidates[candidate].transfers += new_value
self.candidates[candidate].ballots.append((ballot, new_value))
else:
self.exhausted.transfers += new_value
self.exhausted.ballots.append((ballot, new_value))
__pragma__('noopov')
def exclude_candidate(self, candidate_excluded, count_card):
for ballot, ballot_value in count_card.ballots:
__pragma__('opov')
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
self.candidates[candidate].transfers += ballot_value
self.candidates[candidate].ballots.append((ballot, ballot_value))
else:
self.exhausted.transfers += ballot_value
self.exhausted.ballots.append((ballot, ballot_value))
__pragma__('noopov')
class BaseUIGSTVCounter(BaseSTVCounter):
"""
Basic unweighted inclusive Gregory STV counter
"""
def reset(self):
BaseSTVCounter.reset(self)
# Distribute first preferences
for ballot in self.election.ballots:
__pragma__('opov')
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
self.candidates[candidate].transfers += ballot.value
self.candidates[candidate].ballots.append((ballot, Rational(ballot.value.pp(0))))
else:
self.exhausted.transfers += ballot.value
self.exhausted.ballots.append((ballot, Rational(ballot.value.pp(0))))
__pragma__('noopov')
self.quota = None
self.compute_quota()
self.elect_meeting_quota()
__pragma__('opov')
return CountStepResult(
'First preferences',
self.candidates,
self.exhausted,
self.loss_fraction,
self.total + self.exhausted.votes + self.loss_fraction.votes,
self.quota
)
__pragma__('noopov')
def transfer_surplus(self, candidate_surplus, count_card, surplus):
# FIXME: Is it okay to use native int's here?
next_preferences = SafeDict([(c, []) for c, cc in self.candidates.items()])
next_exhausted = []
total_ballots = Num('0')
# Count next preferences
for ballot, ballot_value in count_card.ballots:
__pragma__('opov')
total_ballots += ballot.value
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
__pragma__('noopov')
if candidate is not None:
__pragma__('opov')
next_preferences[candidate].append(ballot)
__pragma__('noopov')
else:
next_exhausted.append(ballot)
# Make transfers
for candidate, cand_ballots in next_preferences.items():
num_ballots = sum((b.value for b in cand_ballots), Num('0'))
__pragma__('opov')
self.candidates[candidate].transfers += (num_ballots * surplus) / total_ballots # Multiply first to avoid rounding errors
__pragma__('noopov')
for ballot in cand_ballots:
__pragma__('opov')
new_value = (Rational(ballot.value.pp(0)) * Rational(surplus.pp(0))) / Rational(total_ballots.pp(0))
self.candidates[candidate].ballots.append((ballot, new_value))
__pragma__('noopov')
num_exhausted = sum((b.value for b in next_exhausted), Num('0'))
__pragma__('opov')
self.exhausted.transfers += (num_exhausted * surplus) / total_ballots
__pragma__('noopov')
for ballot in next_exhausted:
__pragma__('opov')
new_value = (Rational(ballot.value.pp(0)) * Rational(surplus.pp(0))) / Rational(total_ballots.pp(0))
__pragma__('noopov')
self.exhausted.ballots.append((ballot, new_value))
def exclude_candidate(self, candidate_excluded, count_card):
next_preferences = SafeDict([(c, Rational('0')) for c, cc in self.candidates.items()])
next_exhausted = Rational('0')
total_votes = Rational('0')
# Count and transfer next preferences
for ballot, ballot_value in count_card.ballots:
__pragma__('opov')
total_votes += ballot_value
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
__pragma__('noopov')
if candidate is not None:
__pragma__('opov')
next_preferences[candidate] += ballot_value
self.candidates[candidate].ballots.append((ballot, ballot_value))
__pragma__('noopov')
else:
__pragma__('opov')
next_exhausted += ballot_value
__pragma__('noopov')
self.exhausted.ballots.append((ballot, ballot_value))
# Credit votes
for candidate, cand_votes in next_preferences.items():
__pragma__('opov')
self.candidates[candidate].transfers += Num(cand_votes.__floor__().pp(0))
__pragma__('noopov')
__pragma__('opov')
self.exhausted.transfers += Num(next_exhausted.__floor__().pp(0))
__pragma__('noopov')