diff --git a/html/index.html b/html/index.html index 0efc28d..60d7f84 100644 --- a/html/index.html +++ b/html/index.html @@ -39,7 +39,7 @@ Scottish STV pyRCV STV-C - PRSA 1977 + Australian Senate STV Show advanced options @@ -92,7 +92,7 @@ - Transfer value (NYI): + Transfer value: Weighted inclusive Gregory Unweighted inclusive Gregory diff --git a/html/index.js b/html/index.js index d982955..8c797d6 100644 --- a/html/index.js +++ b/html/index.js @@ -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 diff --git a/html/worker.js b/html/worker.js index 40a3337..7c688aa 100644 --- a/html/worker.js +++ b/html/worker.js @@ -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(); diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py index 2623bab..08eed46 100644 --- a/pyRCV2/method/base_stv.py +++ b/pyRCV2/method/base_stv.py @@ -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')