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()">
<option value="scottish" selected>Scottish STV</option>
<option value="stvc">pyRCV STV-C</option>
<option value="prsa77">PRSA 1977</option>
<option value="senate">Australian Senate STV</option>
</select>
</label>
<button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button>
@ -92,7 +92,7 @@
</select>
</label>
<label>
Transfer value (NYI):
Transfer value:
<select id="selTransfers">
<option value="wig" selected>Weighted 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('selTransfers').value = 'wig';
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('selQuota').value = 'droop';
document.getElementById('chkProgQuota').checked = false;
@ -126,10 +126,8 @@ async function clickCount() {
elTd.innerText = 'Loss to fraction';
elLTF1.appendChild(elTd);
if (document.getElementById('selNumbers').value === 'int') {
tblResults.appendChild(elLTF1);
tblResults.appendChild(elLTF2);
}
tblResults.appendChild(elLTF1);
tblResults.appendChild(elLTF2);
// Total row
elTotal = document.createElement('tr');
@ -170,7 +168,7 @@ async function clickCount() {
elTd.classList.add('elected');
}
elTd.style.borderTop = '1px solid black';
if (countCard.transfers != '0.00') {
if (countCard.transfers != '0.00' && countCard.transfers != '0') {
elTd.innerText = countCard.transfers;
}
elTr1.appendChild(elTd);
@ -198,7 +196,7 @@ async function clickCount() {
elTd = document.createElement('td');
elTd.classList.add('count');
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;
}
elExhausted1.appendChild(elTd);
@ -212,14 +210,18 @@ async function clickCount() {
elTd = document.createElement('td');
elTd.classList.add('count');
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;
}
elLTF1.appendChild(elTd);
elTd = document.createElement('td');
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);
// Display total
@ -246,13 +248,13 @@ 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_exclude': document.getElementById('chkBulkExclusion').checked,
'surplus_order': document.getElementById('selSurplus').value,
'transfer_value': document.getElementById('selTransfers').value,
'ties': document.getElementById('selTies').value
},
'data': text

View File

@ -40,7 +40,12 @@ onmessage = function(evt) {
}});
// 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
let result = counter.reset();

View File

@ -17,14 +17,16 @@
__pragma__ = lambda x: None
from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepResult
from pyRCV2.numbers import Num
from pyRCV2.numbers import Num, Rational
from pyRCV2.safedict import SafeDict
class STVException(Exception):
pass
class BaseSTVCounter:
"""Basic STV counter for various different variations"""
"""
Basic STV counter for various different variations
"""
def __init__(self, election, options=None):
self.election = election
@ -55,34 +57,6 @@ class BaseSTVCounter:
self.candidates[candidate].state = CandidateState.WITHDRAWN
__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):
# Step count cards
for candidate, count_card in self.candidates.items():
@ -135,22 +109,10 @@ class BaseSTVCounter:
__pragma__('opov')
surplus = count_card.votes - self.quota
#transfer_value = surplus / count_card.votes # Do not do this yet to avoid rounding errors
__pragma__('noopov')
# Transfer surplus
for ballot, ballot_value in count_card.ballots:
__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')
self.transfer_surplus(candidate_surplus, count_card, surplus)
__pragma__('opov')
count_card.transfers -= surplus
@ -179,23 +141,14 @@ class BaseSTVCounter:
# TODO: Handle ties
candidate_excluded, count_card = hopefuls[0]
count_card.state = CandidateState.EXCLUDED
# Exclude this candidate
self.exclude_candidate(candidate_excluded, count_card)
__pragma__('opov')
count_card.transfers -= count_card.votes
__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
self.compute_quota()
self.elect_meeting_quota()
@ -252,3 +205,182 @@ class BaseSTVCounter:
count_card.state = CandidateState.PROVISIONALLY_ELECTED
count_card.order_elected = self.num_elected
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')