Add detailed comments per stage
This commit is contained in:
parent
bd5910e7f6
commit
ede1da73ad
@ -216,6 +216,8 @@
|
||||
|
||||
<table id="result"></table>
|
||||
|
||||
<div id="resultLogs"></div>
|
||||
|
||||
<script src="vendor/BigInt_BigRat-a5f89e2.min.js"></script>
|
||||
<script src="vendor/big-6.0.0.min.js"></script>
|
||||
<script src="vendor/sjcl-1.0.8.min.js"></script>
|
||||
|
@ -183,10 +183,14 @@ async function clickCount() {
|
||||
let tblResults = document.getElementById('result');
|
||||
tblResults.innerHTML = '';
|
||||
let candMap = {}; // candidate name -> rows
|
||||
let divResultLogs = document.getElementById('resultLogs');
|
||||
divResultLogs.innerHTML = '';
|
||||
|
||||
// Step election
|
||||
let worker = new Worker('worker.js');
|
||||
let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota, elVRE;
|
||||
let election;
|
||||
let trComment, trExhausted1, trExhausted2, trLTF1, trLTF2, trTotal, trQuota, trVRE;
|
||||
let olLogs;
|
||||
|
||||
worker.onmessage = function(evt) {
|
||||
if (evt.data.type === 'require_input') {
|
||||
@ -204,10 +208,10 @@ async function clickCount() {
|
||||
election = evt.data.election;
|
||||
|
||||
// Comment row
|
||||
elComment = document.createElement('tr');
|
||||
trComment = document.createElement('tr');
|
||||
let elTd = document.createElement('td');
|
||||
elComment.appendChild(elTd);
|
||||
tblResults.appendChild(elComment);
|
||||
trComment.appendChild(elTd);
|
||||
tblResults.appendChild(trComment);
|
||||
|
||||
// Candidates
|
||||
for (let candidate of election.candidates) {
|
||||
@ -227,65 +231,72 @@ async function clickCount() {
|
||||
}
|
||||
|
||||
// Exhausted votes row
|
||||
elExhausted1 = document.createElement('tr');
|
||||
elExhausted1.classList.add('info');
|
||||
elExhausted2 = document.createElement('tr');
|
||||
elExhausted2.classList.add('info');
|
||||
trExhausted1 = document.createElement('tr');
|
||||
trExhausted1.classList.add('info');
|
||||
trExhausted2 = document.createElement('tr');
|
||||
trExhausted2.classList.add('info');
|
||||
|
||||
elTd = document.createElement('td');
|
||||
elTd.setAttribute('rowspan', '2');
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.innerText = 'Exhausted';
|
||||
elExhausted1.appendChild(elTd);
|
||||
trExhausted1.appendChild(elTd);
|
||||
|
||||
tblResults.appendChild(elExhausted1);
|
||||
tblResults.appendChild(elExhausted2);
|
||||
tblResults.appendChild(trExhausted1);
|
||||
tblResults.appendChild(trExhausted2);
|
||||
|
||||
// Loss to fraction row
|
||||
elLTF1 = document.createElement('tr');
|
||||
elLTF1.classList.add('info');
|
||||
elLTF2 = document.createElement('tr');
|
||||
elLTF2.classList.add('info');
|
||||
trLTF1 = document.createElement('tr');
|
||||
trLTF1.classList.add('info');
|
||||
trLTF2 = document.createElement('tr');
|
||||
trLTF2.classList.add('info');
|
||||
|
||||
elTd = document.createElement('td');
|
||||
elTd.setAttribute('rowspan', '2');
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.innerText = 'Loss to fraction';
|
||||
elLTF1.appendChild(elTd);
|
||||
trLTF1.appendChild(elTd);
|
||||
|
||||
tblResults.appendChild(elLTF1);
|
||||
tblResults.appendChild(elLTF2);
|
||||
tblResults.appendChild(trLTF1);
|
||||
tblResults.appendChild(trLTF2);
|
||||
|
||||
// Total row
|
||||
elTotal = document.createElement('tr');
|
||||
elTotal.classList.add('info');
|
||||
trTotal = document.createElement('tr');
|
||||
trTotal.classList.add('info');
|
||||
elTd = document.createElement('td');
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.innerText = 'Total';
|
||||
elTotal.appendChild(elTd);
|
||||
tblResults.appendChild(elTotal);
|
||||
trTotal.appendChild(elTd);
|
||||
tblResults.appendChild(trTotal);
|
||||
|
||||
// Quota row
|
||||
elQuota = document.createElement('tr');
|
||||
elQuota.classList.add('info');
|
||||
trQuota = document.createElement('tr');
|
||||
trQuota.classList.add('info');
|
||||
elTd = document.createElement('td');
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.style.borderBottom = '1px solid black';
|
||||
elTd.innerText = 'Quota';
|
||||
elQuota.appendChild(elTd);
|
||||
tblResults.appendChild(elQuota);
|
||||
trQuota.appendChild(elTd);
|
||||
tblResults.appendChild(trQuota);
|
||||
|
||||
// Vote required for election row
|
||||
if (document.getElementById('selQuotaMode').value === 'ers97') {
|
||||
elVRE = document.createElement('tr');
|
||||
elVRE.classList.add('info');
|
||||
trVRE = document.createElement('tr');
|
||||
trVRE.classList.add('info');
|
||||
elTd = document.createElement('td');
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.style.borderBottom = '1px solid black';
|
||||
elTd.innerText = 'Vote required for election';
|
||||
elVRE.appendChild(elTd);
|
||||
tblResults.appendChild(elVRE);
|
||||
trVRE.appendChild(elTd);
|
||||
tblResults.appendChild(trVRE);
|
||||
}
|
||||
|
||||
// Result logs
|
||||
let elP = document.createElement('p');
|
||||
elP.innerText = 'Stage comments:';
|
||||
divResultLogs.appendChild(elP);
|
||||
olLogs = document.createElement('ol');
|
||||
divResultLogs.appendChild(olLogs);
|
||||
}
|
||||
|
||||
if (evt.data.type === 'result') {
|
||||
@ -294,7 +305,7 @@ async function clickCount() {
|
||||
// Display results
|
||||
elTd = document.createElement('td');
|
||||
elTd.innerText = result.stage + '. ' + result.comment;
|
||||
elComment.appendChild(elTd);
|
||||
trComment.appendChild(elTd);
|
||||
|
||||
for (let [candidate, countCard] of result.candidates) {
|
||||
[elTr1, elTr2] = candMap[candidate];
|
||||
@ -342,31 +353,31 @@ async function clickCount() {
|
||||
elTd.classList.add('count');
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.innerHTML = ppVotes(result.exhausted.transfers);
|
||||
elExhausted1.appendChild(elTd);
|
||||
trExhausted1.appendChild(elTd);
|
||||
|
||||
elTd = document.createElement('td');
|
||||
elTd.classList.add('count');
|
||||
elTd.innerHTML = ppVotes(result.exhausted.votes);
|
||||
elExhausted2.appendChild(elTd);
|
||||
trExhausted2.appendChild(elTd);
|
||||
|
||||
// Display loss to fraction
|
||||
elTd = document.createElement('td');
|
||||
elTd.classList.add('count');
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.innerHTML = ppVotes(result.loss_fraction.transfers);
|
||||
elLTF1.appendChild(elTd);
|
||||
trLTF1.appendChild(elTd);
|
||||
|
||||
elTd = document.createElement('td');
|
||||
elTd.classList.add('count');
|
||||
elTd.innerHTML = ppVotes(result.loss_fraction.votes);
|
||||
elLTF2.appendChild(elTd);
|
||||
trLTF2.appendChild(elTd);
|
||||
|
||||
// Display total
|
||||
elTd = document.createElement('td');
|
||||
elTd.classList.add('count');
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.innerHTML = ppVotes(result.total);
|
||||
elTotal.appendChild(elTd);
|
||||
trTotal.appendChild(elTd);
|
||||
|
||||
// Display quota
|
||||
elTd = document.createElement('td');
|
||||
@ -374,7 +385,7 @@ async function clickCount() {
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.style.borderBottom = '1px solid black';
|
||||
elTd.innerHTML = ppVotes(result.quota);
|
||||
elQuota.appendChild(elTd);
|
||||
trQuota.appendChild(elTd);
|
||||
|
||||
// Display vote required for election
|
||||
if (result.vote_required_election !== null) {
|
||||
@ -383,8 +394,13 @@ async function clickCount() {
|
||||
elTd.style.borderTop = '1px solid black';
|
||||
elTd.style.borderBottom = '1px solid black';
|
||||
elTd.innerHTML = ppVotes(result.vote_required_election);
|
||||
elVRE.appendChild(elTd);
|
||||
trVRE.appendChild(elTd);
|
||||
}
|
||||
|
||||
// Result logs
|
||||
let elLi = document.createElement('li');
|
||||
elLi.innerText = result.logs.join(' ');
|
||||
olLogs.appendChild(elLi);
|
||||
}
|
||||
|
||||
if (evt.data.type === 'done') {
|
||||
|
@ -124,6 +124,7 @@ function resultToJS(result) {
|
||||
return {
|
||||
'stage': stage,
|
||||
'comment': result.comment,
|
||||
'logs': result.logs,
|
||||
'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, {
|
||||
'transfers': cc.transfers.pp(ppDPs),
|
||||
'votes': cc.votes.pp(ppDPs),
|
||||
|
@ -54,6 +54,9 @@ def add_parser(subparsers):
|
||||
def print_step(args, stage, result):
|
||||
print('{}. {}'.format(stage, result.comment))
|
||||
|
||||
if result.logs:
|
||||
print(' '.join(result.logs))
|
||||
|
||||
results = list(result.candidates.items())
|
||||
if args.sort_votes:
|
||||
results.sort(key=lambda x: x[1].votes, reverse=True)
|
||||
@ -61,6 +64,9 @@ def print_step(args, stage, result):
|
||||
for candidate, count_card in results:
|
||||
state = None
|
||||
if count_card.state == pyRCV2.model.CandidateState.ELECTED or count_card.state == pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED or count_card.state == pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS:
|
||||
if args.method == 'meek':
|
||||
state = 'ELECTED {} (kv = {})'.format(count_card.order_elected, count_card.keep_value.pp(args.pp_decimals))
|
||||
else:
|
||||
state = 'ELECTED {}'.format(count_card.order_elected)
|
||||
elif count_card.state == pyRCV2.model.CandidateState.EXCLUDED or count_card.state == pyRCV2.model.CandidateState.EXCLUDING:
|
||||
state = 'Excluded {}'.format(-count_card.order_elected)
|
||||
|
@ -60,6 +60,7 @@ class BaseSTVCounter:
|
||||
|
||||
self.total_orig = sum((b.value for b in self.election.ballots), Num('0'))
|
||||
|
||||
self.logs = []
|
||||
self.step_results = []
|
||||
self.num_elected = 0
|
||||
self.num_excluded = 0
|
||||
@ -80,26 +81,14 @@ class BaseSTVCounter:
|
||||
self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots
|
||||
|
||||
self.distribute_first_preferences()
|
||||
self.logs.append('First preferences distributed.')
|
||||
|
||||
self.quota = None
|
||||
self.vote_required_election = None # For ERS97
|
||||
self.compute_quota()
|
||||
self.elect_meeting_quota()
|
||||
|
||||
__pragma__('opov')
|
||||
result = CountStepResult(
|
||||
'First preferences',
|
||||
self.candidates,
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
self.step_results = [result]
|
||||
return result
|
||||
return self.make_result('First preferences')
|
||||
|
||||
def distribute_first_preferences(self):
|
||||
"""
|
||||
@ -173,6 +162,7 @@ class BaseSTVCounter:
|
||||
__pragma__('opov')
|
||||
return CountCompleted(
|
||||
'Count complete',
|
||||
self.logs,
|
||||
self.candidates,
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
@ -187,26 +177,18 @@ class BaseSTVCounter:
|
||||
# Include EXCLUDING to avoid interrupting an exclusion
|
||||
if len(self.election.candidates) - self.num_excluded + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.EXCLUDING) <= self.election.seats:
|
||||
# Declare elected all remaining candidates
|
||||
for candidate, count_card in self.candidates.items():
|
||||
if count_card.state == CandidateState.HOPEFUL:
|
||||
candidates_elected = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
|
||||
if len(candidates_elected) == 1:
|
||||
self.logs.append(candidates_elected[0][0].name + ' is elected to fill the remaining vacancy.')
|
||||
else:
|
||||
self.logs.append(self.pretty_join([c.name for c, cc in candidates_elected]) + ' are elected to fill the remaining vacancies.')
|
||||
|
||||
for candidate, count_card in candidates_elected:
|
||||
count_card.state = CandidateState.PROVISIONALLY_ELECTED
|
||||
self.num_elected += 1
|
||||
count_card.order_elected = self.num_elected
|
||||
|
||||
__pragma__('opov')
|
||||
result = CountStepResult(
|
||||
'Bulk election',
|
||||
self.candidates,
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
self.step_results.append(result)
|
||||
return result
|
||||
return self.make_result('Bulk election')
|
||||
|
||||
def can_defer_surpluses(self, has_surplus):
|
||||
"""
|
||||
@ -235,6 +217,7 @@ class BaseSTVCounter:
|
||||
__pragma__('opov')
|
||||
|
||||
# Can defer surpluses
|
||||
self.logs.append('Distribution of surpluses totalling ' + total_surpluses.pp(2) + ' votes will be deferred.')
|
||||
return True
|
||||
|
||||
def distribute_surpluses(self):
|
||||
@ -287,6 +270,7 @@ class BaseSTVCounter:
|
||||
__pragma__('noopov')
|
||||
|
||||
# Transfer surplus
|
||||
self.logs.append('Surplus of ' + candidate_surplus.name + ' distributed.')
|
||||
self.do_surplus(candidate_surplus, count_card, surplus)
|
||||
|
||||
# Declare elected any candidates meeting the quota as a result of surpluses
|
||||
@ -294,20 +278,7 @@ class BaseSTVCounter:
|
||||
|
||||
self.elect_meeting_quota()
|
||||
|
||||
__pragma__('opov')
|
||||
result = CountStepResult(
|
||||
'Surplus of ' + candidate_surplus.name,
|
||||
self.candidates,
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
self.step_results.append(result)
|
||||
return result
|
||||
return self.make_result('Surplus of ' + candidate_surplus.name)
|
||||
|
||||
def do_surplus(self, candidate_surplus, count_card, surplus):
|
||||
"""
|
||||
@ -333,20 +304,7 @@ class BaseSTVCounter:
|
||||
self.num_elected += 1
|
||||
count_card.order_elected = self.num_elected
|
||||
|
||||
__pragma__('opov')
|
||||
result = CountStepResult(
|
||||
'Bulk election',
|
||||
self.candidates,
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
self.step_results.append(result)
|
||||
return result
|
||||
return self.make_result('Bulk election')
|
||||
|
||||
def exclude_candidates(self):
|
||||
"""
|
||||
@ -400,20 +358,7 @@ class BaseSTVCounter:
|
||||
|
||||
self.elect_meeting_quota()
|
||||
|
||||
__pragma__('opov')
|
||||
result = CountStepResult(
|
||||
'Exclusion of ' + ', '.join([c.name for c, cc in candidates_excluded]),
|
||||
self.candidates,
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
self.step_results.append(result)
|
||||
return result
|
||||
return self.make_result('Exclusion of ' + ', '.join([c.name for c, cc in candidates_excluded]))
|
||||
|
||||
def candidates_to_bulk_exclude(self, hopefuls):
|
||||
"""
|
||||
@ -460,6 +405,7 @@ class BaseSTVCounter:
|
||||
|
||||
# Continue current exclusion if applicable
|
||||
if self._exclusion is not None:
|
||||
self.logs.append('Continuing exclusion of ' + self.pretty_join([c.name for c, cc in self._exclusion[0]]) + '.')
|
||||
__pragma__('opov')
|
||||
return self._exclusion[0]
|
||||
__pragma__('noopov')
|
||||
@ -477,8 +423,14 @@ class BaseSTVCounter:
|
||||
|
||||
candidates_excluded = self.candidates_to_bulk_exclude(hopefuls)
|
||||
|
||||
if len(candidates_excluded) == 0:
|
||||
if len(candidates_excluded) > 0:
|
||||
if len(candidates_excluded) == 1:
|
||||
self.logs.append('No surpluses to distribute, so ' + candidates_excluded[0][0].name + ' is excluded.')
|
||||
else:
|
||||
self.logs.append('No surpluses to distribute, so ' + self.pretty_join([c.name for c, cc in candidates_excluded]) + ' are excluded.')
|
||||
else:
|
||||
candidates_excluded = [self.choose_lowest(hopefuls)]
|
||||
self.logs.append('No surpluses to distribute, so ' + candidates_excluded[0][0].name + ' is excluded.')
|
||||
|
||||
return candidates_excluded
|
||||
|
||||
@ -516,17 +468,22 @@ class BaseSTVCounter:
|
||||
else:
|
||||
# Round up (preserving the original quota if exact)
|
||||
self.quota = self.quota.round(self.options['round_quota'], self.quota.ROUND_UP)
|
||||
|
||||
self.logs.append(self.total.pp(2) + ' usable votes, so the quota is ' + self.quota.pp(2) + '.')
|
||||
__pragma__('noopov')
|
||||
|
||||
if self.options['quota_mode'] == 'ers97' and self.num_elected < self.election.seats:
|
||||
# Calculate the total active vote
|
||||
__pragma__('opov')
|
||||
orig_vre = self.vote_required_election
|
||||
total_active_vote = \
|
||||
sum((cc.votes for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL or cc.state == CandidateState.EXCLUDING), Num('0')) + \
|
||||
sum((cc.votes - self.quota for c, cc in self.candidates.items() if cc.votes > self.quota), Num('0'))
|
||||
self.vote_required_election = total_active_vote / Num(self.election.seats - self.num_elected + 1)
|
||||
if self.options['round_votes'] is not None:
|
||||
self.vote_required_election = self.vote_required_election.round(self.options['round_votes'], self.vote_required_election.ROUND_UP)
|
||||
if (orig_vre is None or self.vote_required_election != orig_vre) and self.vote_required_election < self.quota:
|
||||
self.logs.append('Total active vote is ' + total_active_vote.pp(2) + ', so the vote required for election is ' + self.vote_required_election.pp(2) + '.')
|
||||
__pragma__('noopov')
|
||||
|
||||
def meets_quota(self, count_card):
|
||||
@ -555,6 +512,10 @@ class BaseSTVCounter:
|
||||
|
||||
if len(meets_quota) > 0:
|
||||
meets_quota.sort(key=lambda x: x[1].votes, reverse=True)
|
||||
if len(meets_quota) == 1:
|
||||
self.logs.append(meets_quota[0][0].name + ' meets the quota and is elected.')
|
||||
else:
|
||||
self.logs.append(self.pretty_join([c.name for c, cc in meets_quota]) + ' meet the quota and are elected.')
|
||||
|
||||
# Declare elected any candidate who meets the quota
|
||||
while len(meets_quota) > 0:
|
||||
@ -671,3 +632,29 @@ class BaseSTVCounter:
|
||||
if self.options['round_tvs'] is None:
|
||||
return num
|
||||
return num.round(self.options['round_tvs'], num.ROUND_DOWN)
|
||||
|
||||
def make_result(self, comment):
|
||||
__pragma__('opov')
|
||||
result = CountStepResult(
|
||||
comment,
|
||||
self.logs,
|
||||
self.candidates,
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
self.logs = []
|
||||
self.step_results.append(result)
|
||||
return result
|
||||
|
||||
def pretty_join(self, strs):
|
||||
if len(strs) == 0:
|
||||
return ''
|
||||
if len(strs) == 1:
|
||||
return strs[0]
|
||||
if len(strs) == 2:
|
||||
return strs[0] + ' and ' + strs[1]
|
||||
return ', '.join(strs[0:-1]) + ' and ' + strs[len(strs)-1]
|
||||
|
@ -155,14 +155,18 @@ class WIGSTVCounter(BaseSTVCounter):
|
||||
else:
|
||||
raise STVException('Invalid exclusion mode')
|
||||
|
||||
#print([[bv / b.value for c, bb in stage for b, bv in bb] for stage in self._exclusion[1]])
|
||||
|
||||
this_exclusion = self._exclusion[1][0]
|
||||
self._exclusion[1].remove(this_exclusion)
|
||||
|
||||
# Transfer votes
|
||||
|
||||
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([bb for c, bb in this_exclusion])
|
||||
|
||||
if self.options['exclusion'] != 'one_round':
|
||||
__pragma__('opov')
|
||||
self.logs.append('Transferring ' + total_ballots.pp(0) + ' ballot papers, totalling ' + total_votes.pp(2) + ' votes, received at value ' + (this_exclusion[0][1][0][1] / this_exclusion[0][1][0][0].value).pp(2) + '.')
|
||||
__pragma__('noopov')
|
||||
|
||||
for candidate, x in next_preferences.items():
|
||||
cand_ballots, num_ballots, num_votes = x[0], x[1], x[2]
|
||||
|
||||
@ -199,6 +203,9 @@ class WIGSTVCounter(BaseSTVCounter):
|
||||
__pragma__('noopov')
|
||||
|
||||
if len(self._exclusion[1]) == 0:
|
||||
if self.options['exclusion'] != 'one_round':
|
||||
self.logs.append('Exclusion complete.')
|
||||
|
||||
for candidate_excluded, count_card in candidates_excluded:
|
||||
__pragma__('opov')
|
||||
count_card.transfers -= count_card.votes
|
||||
|
@ -17,7 +17,7 @@
|
||||
__pragma__ = lambda x: None
|
||||
|
||||
from pyRCV2.method.base_stv import BaseSTVCounter, STVException
|
||||
from pyRCV2.model import CandidateState, CountCard, CountStepResult
|
||||
from pyRCV2.model import CandidateState, CountCard
|
||||
from pyRCV2.numbers import Num
|
||||
from pyRCV2.safedict import SafeDict
|
||||
|
||||
@ -56,9 +56,25 @@ class MeekSTVCounter(BaseSTVCounter):
|
||||
raise STVException('Meek method is incompatible with --transferable-only')
|
||||
if self.options['exclusion'] != 'one_round':
|
||||
raise STVException('Meek method requires --exclusion one_round')
|
||||
if self.options['round_votes'] is not None:
|
||||
raise STVException('Meek method is incompatible with --round-votes')
|
||||
if self.options['round_tvs'] is not None:
|
||||
raise STVException('Meek method is incompatible with --round-tvs')
|
||||
return BaseSTVCounter.reset(self)
|
||||
if self.options['round_weights'] is not None:
|
||||
raise STVException('Meek method is incompatible with --round-weights')
|
||||
|
||||
self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots
|
||||
|
||||
self.distribute_first_preferences()
|
||||
self.logs.append('First preferences distributed.')
|
||||
|
||||
self.quota = None
|
||||
self.vote_required_election = None # For ERS97
|
||||
self.compute_quota()
|
||||
self.logs.append(self.total.pp(2) + ' usable votes, so the quota is ' + self.quota.pp(2) + '.')
|
||||
self.elect_meeting_quota()
|
||||
|
||||
return self.make_result('First preferences')
|
||||
|
||||
def distribute_first_preferences(self):
|
||||
"""
|
||||
@ -137,7 +153,11 @@ class MeekSTVCounter(BaseSTVCounter):
|
||||
__pragma__('noopov')
|
||||
|
||||
if len(has_surplus) > 0:
|
||||
num_iterations = 0
|
||||
orig_quota = self.quota
|
||||
while len(has_surplus) > 0:
|
||||
num_iterations += 1
|
||||
|
||||
# Recompute keep values
|
||||
for candidate, count_card in has_surplus:
|
||||
__pragma__('opov')
|
||||
@ -154,24 +174,21 @@ class MeekSTVCounter(BaseSTVCounter):
|
||||
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.ELECTED and cc.votes / self.quota > self._quota_tolerance_ub]
|
||||
__pragma__('noopov')
|
||||
|
||||
if num_iterations == 1:
|
||||
self.logs.append('Surpluses distributed, requiring 1 iteration.')
|
||||
else:
|
||||
self.logs.append('Surpluses distributed, requiring ' + str(num_iterations) + ' iterations.')
|
||||
|
||||
self.logs.append('Keep values of elected candidates are: ' + ', '.join([c.name + ' (' + cc.keep_value.pp(2) + ')' for c, cc in self.candidates.items() if cc.state == CandidateState.ELECTED]) + '.')
|
||||
|
||||
if self.quota != orig_quota:
|
||||
self.logs.append(self.total.pp(2) + ' usable votes, so the quota is ' + self.quota.pp(2) + '.')
|
||||
|
||||
# Declare elected any candidates meeting the quota as a result of surpluses
|
||||
# NB: We could do this earlier, but this shows the flow of the election more clearly in the count sheet
|
||||
self.elect_meeting_quota()
|
||||
|
||||
__pragma__('opov')
|
||||
result = CountStepResult(
|
||||
'Surpluses distributed',
|
||||
self.candidates,
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
self.step_results.append(result)
|
||||
return result
|
||||
return self.make_result('Surpluses distributed')
|
||||
|
||||
def do_exclusion(self, candidates_excluded):
|
||||
"""
|
||||
@ -194,6 +211,10 @@ class MeekSTVCounter(BaseSTVCounter):
|
||||
|
||||
if len(meets_quota) > 0:
|
||||
meets_quota.sort(key=lambda x: x[1].votes, reverse=True)
|
||||
if len(meets_quota) == 1:
|
||||
self.logs.append(meets_quota[0][0].name + ' meets the quota and is elected.')
|
||||
else:
|
||||
self.logs.append(self.pretty_join([c.name for c, cc in meets_quota]) + ' meet the quota and are elected.')
|
||||
|
||||
# Declare elected any candidate who meets the quota
|
||||
while len(meets_quota) > 0:
|
||||
@ -205,3 +226,33 @@ class MeekSTVCounter(BaseSTVCounter):
|
||||
count_card.order_elected = self.num_elected
|
||||
|
||||
meets_quota.remove(x)
|
||||
|
||||
def compute_quota(self):
|
||||
"""
|
||||
Overrides BaseSTVCounter.compute_quota
|
||||
Do not log quota changes
|
||||
"""
|
||||
|
||||
__pragma__('opov')
|
||||
self.total = sum((cc.votes for c, cc in self.candidates.items()), Num('0'))
|
||||
self.loss_fraction.transfers += (self.total_orig - self.total - self.exhausted.votes) - self.loss_fraction.votes
|
||||
|
||||
if self.options['quota'] == 'droop' or self.options['quota'] == 'droop_exact':
|
||||
self.quota = self.total / Num(self.election.seats + 1)
|
||||
elif self.options['quota'] == 'hare' or self.options['quota'] == 'hare_exact':
|
||||
self.quota = self.total / Num(self.election.seats)
|
||||
else:
|
||||
raise STVException('Invalid quota option')
|
||||
|
||||
if self.options['round_quota'] is not None:
|
||||
if self.options['quota'] == 'droop' or self.options['quota'] == 'hare':
|
||||
# Increment to next available increment
|
||||
factor = Num(10).__pow__(self.options['round_quota'])
|
||||
__pragma__('opov')
|
||||
self.quota = ((self.quota * factor).__floor__() + Num(1)) / factor
|
||||
__pragma__('noopov')
|
||||
else:
|
||||
# Round up (preserving the original quota if exact)
|
||||
self.quota = self.quota.round(self.options['round_quota'], self.quota.ROUND_UP)
|
||||
|
||||
__pragma__('noopov')
|
||||
|
@ -103,8 +103,9 @@ class CountCard:
|
||||
__pragma__('noopov')
|
||||
|
||||
class CountStepResult:
|
||||
def __init__(self, comment, candidates, exhausted, loss_fraction, total, quota, vote_required_election):
|
||||
def __init__(self, comment, logs, candidates, exhausted, loss_fraction, total, quota, vote_required_election):
|
||||
self.comment = comment
|
||||
self.logs = logs
|
||||
|
||||
self.candidates = candidates # SafeDict: Candidate -> CountCard
|
||||
self.exhausted = exhausted # CountCard
|
||||
|
Reference in New Issue
Block a user