Implement un/weighted inclusive Gregory transfers
This commit is contained in:
parent
643e23b6ec
commit
e0c56c5b1d
@ -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>
|
||||||
|
@ -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');
|
||||||
|
if (result.loss_fraction.votes == '-0.00') {
|
||||||
|
elTd.innerText = '0.00';
|
||||||
|
} else {
|
||||||
elTd.innerText = result.loss_fraction.votes;
|
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
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
@ -55,34 +57,6 @@ class BaseSTVCounter:
|
|||||||
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
|
||||||
for candidate, count_card in self.candidates.items():
|
for candidate, count_card in self.candidates.items():
|
||||||
@ -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,21 +141,12 @@ 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
|
||||||
__pragma__('opov')
|
|
||||||
count_card.transfers -= count_card.votes
|
|
||||||
__pragma__('noopov')
|
|
||||||
|
|
||||||
# Exclude this candidate
|
# Exclude this candidate
|
||||||
for ballot, ballot_value in count_card.ballots:
|
self.exclude_candidate(candidate_excluded, count_card)
|
||||||
__pragma__('opov')
|
|
||||||
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
|
|
||||||
|
|
||||||
if candidate is not None:
|
__pragma__('opov')
|
||||||
self.candidates[candidate].transfers += ballot_value
|
count_card.transfers -= count_card.votes
|
||||||
self.candidates[candidate].ballots.append((ballot, ballot_value))
|
|
||||||
else:
|
|
||||||
self.exhausted.transfers += ballot_value
|
|
||||||
self.exhausted.ballots.append((ballot, ballot_value))
|
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
# Declare any candidates meeting the quota as a result of exclusion
|
# Declare any candidates meeting the quota as a result of exclusion
|
||||||
@ -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')
|
||||||
|
Reference in New Issue
Block a user