Implement un/weighted inclusive Gregory transfers
This commit is contained in:
parent
643e23b6ec
commit
e0c56c5b1d
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
@ -54,34 +56,6 @@ class BaseSTVCounter:
|
||||
__pragma__('opov')
|
||||
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
|
||||
@ -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')
|
||||
|
Reference in New Issue
Block a user