Add detailed comments per stage

This commit is contained in:
RunasSudo 2021-01-09 04:33:13 +11:00
parent bd5910e7f6
commit ede1da73ad
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
8 changed files with 209 additions and 138 deletions

View File

@ -216,6 +216,8 @@
<table id="result"></table> <table id="result"></table>
<div id="resultLogs"></div>
<script src="vendor/BigInt_BigRat-a5f89e2.min.js"></script> <script src="vendor/BigInt_BigRat-a5f89e2.min.js"></script>
<script src="vendor/big-6.0.0.min.js"></script> <script src="vendor/big-6.0.0.min.js"></script>
<script src="vendor/sjcl-1.0.8.min.js"></script> <script src="vendor/sjcl-1.0.8.min.js"></script>

View File

@ -183,10 +183,14 @@ async function clickCount() {
let tblResults = document.getElementById('result'); let tblResults = document.getElementById('result');
tblResults.innerHTML = ''; tblResults.innerHTML = '';
let candMap = {}; // candidate name -> rows let candMap = {}; // candidate name -> rows
let divResultLogs = document.getElementById('resultLogs');
divResultLogs.innerHTML = '';
// Step election // Step election
let worker = new Worker('worker.js'); 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) { worker.onmessage = function(evt) {
if (evt.data.type === 'require_input') { if (evt.data.type === 'require_input') {
@ -204,10 +208,10 @@ async function clickCount() {
election = evt.data.election; election = evt.data.election;
// Comment row // Comment row
elComment = document.createElement('tr'); trComment = document.createElement('tr');
let elTd = document.createElement('td'); let elTd = document.createElement('td');
elComment.appendChild(elTd); trComment.appendChild(elTd);
tblResults.appendChild(elComment); tblResults.appendChild(trComment);
// Candidates // Candidates
for (let candidate of election.candidates) { for (let candidate of election.candidates) {
@ -227,65 +231,72 @@ async function clickCount() {
} }
// Exhausted votes row // Exhausted votes row
elExhausted1 = document.createElement('tr'); trExhausted1 = document.createElement('tr');
elExhausted1.classList.add('info'); trExhausted1.classList.add('info');
elExhausted2 = document.createElement('tr'); trExhausted2 = document.createElement('tr');
elExhausted2.classList.add('info'); trExhausted2.classList.add('info');
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.setAttribute('rowspan', '2'); elTd.setAttribute('rowspan', '2');
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
elTd.innerText = 'Exhausted'; elTd.innerText = 'Exhausted';
elExhausted1.appendChild(elTd); trExhausted1.appendChild(elTd);
tblResults.appendChild(elExhausted1); tblResults.appendChild(trExhausted1);
tblResults.appendChild(elExhausted2); tblResults.appendChild(trExhausted2);
// Loss to fraction row // Loss to fraction row
elLTF1 = document.createElement('tr'); trLTF1 = document.createElement('tr');
elLTF1.classList.add('info'); trLTF1.classList.add('info');
elLTF2 = document.createElement('tr'); trLTF2 = document.createElement('tr');
elLTF2.classList.add('info'); trLTF2.classList.add('info');
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.setAttribute('rowspan', '2'); elTd.setAttribute('rowspan', '2');
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
elTd.innerText = 'Loss to fraction'; elTd.innerText = 'Loss to fraction';
elLTF1.appendChild(elTd); trLTF1.appendChild(elTd);
tblResults.appendChild(elLTF1); tblResults.appendChild(trLTF1);
tblResults.appendChild(elLTF2); tblResults.appendChild(trLTF2);
// Total row // Total row
elTotal = document.createElement('tr'); trTotal = document.createElement('tr');
elTotal.classList.add('info'); trTotal.classList.add('info');
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
elTd.innerText = 'Total'; elTd.innerText = 'Total';
elTotal.appendChild(elTd); trTotal.appendChild(elTd);
tblResults.appendChild(elTotal); tblResults.appendChild(trTotal);
// Quota row // Quota row
elQuota = document.createElement('tr'); trQuota = document.createElement('tr');
elQuota.classList.add('info'); trQuota.classList.add('info');
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
elTd.style.borderBottom = '1px solid black'; elTd.style.borderBottom = '1px solid black';
elTd.innerText = 'Quota'; elTd.innerText = 'Quota';
elQuota.appendChild(elTd); trQuota.appendChild(elTd);
tblResults.appendChild(elQuota); tblResults.appendChild(trQuota);
// Vote required for election row // Vote required for election row
if (document.getElementById('selQuotaMode').value === 'ers97') { if (document.getElementById('selQuotaMode').value === 'ers97') {
elVRE = document.createElement('tr'); trVRE = document.createElement('tr');
elVRE.classList.add('info'); trVRE.classList.add('info');
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
elTd.style.borderBottom = '1px solid black'; elTd.style.borderBottom = '1px solid black';
elTd.innerText = 'Vote required for election'; elTd.innerText = 'Vote required for election';
elVRE.appendChild(elTd); trVRE.appendChild(elTd);
tblResults.appendChild(elVRE); 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') { if (evt.data.type === 'result') {
@ -294,7 +305,7 @@ async function clickCount() {
// Display results // Display results
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.innerText = result.stage + '. ' + result.comment; elTd.innerText = result.stage + '. ' + result.comment;
elComment.appendChild(elTd); trComment.appendChild(elTd);
for (let [candidate, countCard] of result.candidates) { for (let [candidate, countCard] of result.candidates) {
[elTr1, elTr2] = candMap[candidate]; [elTr1, elTr2] = candMap[candidate];
@ -342,31 +353,31 @@ async function clickCount() {
elTd.classList.add('count'); elTd.classList.add('count');
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
elTd.innerHTML = ppVotes(result.exhausted.transfers); elTd.innerHTML = ppVotes(result.exhausted.transfers);
elExhausted1.appendChild(elTd); trExhausted1.appendChild(elTd);
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.classList.add('count'); elTd.classList.add('count');
elTd.innerHTML = ppVotes(result.exhausted.votes); elTd.innerHTML = ppVotes(result.exhausted.votes);
elExhausted2.appendChild(elTd); trExhausted2.appendChild(elTd);
// Display loss to fraction // Display loss to fraction
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';
elTd.innerHTML = ppVotes(result.loss_fraction.transfers); elTd.innerHTML = ppVotes(result.loss_fraction.transfers);
elLTF1.appendChild(elTd); trLTF1.appendChild(elTd);
elTd = document.createElement('td'); elTd = document.createElement('td');
elTd.classList.add('count'); elTd.classList.add('count');
elTd.innerHTML = ppVotes(result.loss_fraction.votes); elTd.innerHTML = ppVotes(result.loss_fraction.votes);
elLTF2.appendChild(elTd); trLTF2.appendChild(elTd);
// Display total // Display total
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';
elTd.innerHTML = ppVotes(result.total); elTd.innerHTML = ppVotes(result.total);
elTotal.appendChild(elTd); trTotal.appendChild(elTd);
// Display quota // Display quota
elTd = document.createElement('td'); elTd = document.createElement('td');
@ -374,7 +385,7 @@ async function clickCount() {
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
elTd.style.borderBottom = '1px solid black'; elTd.style.borderBottom = '1px solid black';
elTd.innerHTML = ppVotes(result.quota); elTd.innerHTML = ppVotes(result.quota);
elQuota.appendChild(elTd); trQuota.appendChild(elTd);
// Display vote required for election // Display vote required for election
if (result.vote_required_election !== null) { if (result.vote_required_election !== null) {
@ -383,8 +394,13 @@ async function clickCount() {
elTd.style.borderTop = '1px solid black'; elTd.style.borderTop = '1px solid black';
elTd.style.borderBottom = '1px solid black'; elTd.style.borderBottom = '1px solid black';
elTd.innerHTML = ppVotes(result.vote_required_election); 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') { if (evt.data.type === 'done') {

View File

@ -124,6 +124,7 @@ function resultToJS(result) {
return { return {
'stage': stage, 'stage': stage,
'comment': result.comment, 'comment': result.comment,
'logs': result.logs,
'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, { 'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, {
'transfers': cc.transfers.pp(ppDPs), 'transfers': cc.transfers.pp(ppDPs),
'votes': cc.votes.pp(ppDPs), 'votes': cc.votes.pp(ppDPs),

View File

@ -54,6 +54,9 @@ def add_parser(subparsers):
def print_step(args, stage, result): def print_step(args, stage, result):
print('{}. {}'.format(stage, result.comment)) print('{}. {}'.format(stage, result.comment))
if result.logs:
print(' '.join(result.logs))
results = list(result.candidates.items()) results = list(result.candidates.items())
if args.sort_votes: if args.sort_votes:
results.sort(key=lambda x: x[1].votes, reverse=True) results.sort(key=lambda x: x[1].votes, reverse=True)
@ -61,7 +64,10 @@ def print_step(args, stage, result):
for candidate, count_card in results: for candidate, count_card in results:
state = None 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 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:
state = 'ELECTED {}'.format(count_card.order_elected) 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: elif count_card.state == pyRCV2.model.CandidateState.EXCLUDED or count_card.state == pyRCV2.model.CandidateState.EXCLUDING:
state = 'Excluded {}'.format(-count_card.order_elected) state = 'Excluded {}'.format(-count_card.order_elected)
elif count_card.state == pyRCV2.model.CandidateState.WITHDRAWN: elif count_card.state == pyRCV2.model.CandidateState.WITHDRAWN:

View File

@ -60,6 +60,7 @@ class BaseSTVCounter:
self.total_orig = sum((b.value for b in self.election.ballots), Num('0')) self.total_orig = sum((b.value for b in self.election.ballots), Num('0'))
self.logs = []
self.step_results = [] self.step_results = []
self.num_elected = 0 self.num_elected = 0
self.num_excluded = 0 self.num_excluded = 0
@ -80,26 +81,14 @@ class BaseSTVCounter:
self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots
self.distribute_first_preferences() self.distribute_first_preferences()
self.logs.append('First preferences distributed.')
self.quota = None self.quota = None
self.vote_required_election = None # For ERS97 self.vote_required_election = None # For ERS97
self.compute_quota() self.compute_quota()
self.elect_meeting_quota() self.elect_meeting_quota()
__pragma__('opov') return self.make_result('First preferences')
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
def distribute_first_preferences(self): def distribute_first_preferences(self):
""" """
@ -173,6 +162,7 @@ class BaseSTVCounter:
__pragma__('opov') __pragma__('opov')
return CountCompleted( return CountCompleted(
'Count complete', 'Count complete',
self.logs,
self.candidates, self.candidates,
self.exhausted, self.exhausted,
self.loss_fraction, self.loss_fraction,
@ -187,26 +177,18 @@ class BaseSTVCounter:
# Include EXCLUDING to avoid interrupting an exclusion # 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: 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 # Declare elected all remaining candidates
for candidate, count_card in self.candidates.items(): candidates_elected = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
if count_card.state == CandidateState.HOPEFUL: if len(candidates_elected) == 1:
count_card.state = CandidateState.PROVISIONALLY_ELECTED self.logs.append(candidates_elected[0][0].name + ' is elected to fill the remaining vacancy.')
self.num_elected += 1 else:
count_card.order_elected = self.num_elected self.logs.append(self.pretty_join([c.name for c, cc in candidates_elected]) + ' are elected to fill the remaining vacancies.')
__pragma__('opov') for candidate, count_card in candidates_elected:
result = CountStepResult( count_card.state = CandidateState.PROVISIONALLY_ELECTED
'Bulk election', self.num_elected += 1
self.candidates, count_card.order_elected = self.num_elected
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 self.make_result('Bulk election')
return result
def can_defer_surpluses(self, has_surplus): def can_defer_surpluses(self, has_surplus):
""" """
@ -235,6 +217,7 @@ class BaseSTVCounter:
__pragma__('opov') __pragma__('opov')
# Can defer surpluses # Can defer surpluses
self.logs.append('Distribution of surpluses totalling ' + total_surpluses.pp(2) + ' votes will be deferred.')
return True return True
def distribute_surpluses(self): def distribute_surpluses(self):
@ -287,6 +270,7 @@ class BaseSTVCounter:
__pragma__('noopov') __pragma__('noopov')
# Transfer surplus # Transfer surplus
self.logs.append('Surplus of ' + candidate_surplus.name + ' distributed.')
self.do_surplus(candidate_surplus, count_card, surplus) self.do_surplus(candidate_surplus, count_card, surplus)
# Declare elected any candidates meeting the quota as a result of surpluses # Declare elected any candidates meeting the quota as a result of surpluses
@ -294,20 +278,7 @@ class BaseSTVCounter:
self.elect_meeting_quota() self.elect_meeting_quota()
__pragma__('opov') return self.make_result('Surplus of ' + candidate_surplus.name)
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
def do_surplus(self, candidate_surplus, count_card, surplus): def do_surplus(self, candidate_surplus, count_card, surplus):
""" """
@ -333,20 +304,7 @@ class BaseSTVCounter:
self.num_elected += 1 self.num_elected += 1
count_card.order_elected = self.num_elected count_card.order_elected = self.num_elected
__pragma__('opov') return self.make_result('Bulk election')
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
def exclude_candidates(self): def exclude_candidates(self):
""" """
@ -400,20 +358,7 @@ class BaseSTVCounter:
self.elect_meeting_quota() self.elect_meeting_quota()
__pragma__('opov') return self.make_result('Exclusion of ' + ', '.join([c.name for c, cc in candidates_excluded]))
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
def candidates_to_bulk_exclude(self, hopefuls): def candidates_to_bulk_exclude(self, hopefuls):
""" """
@ -460,6 +405,7 @@ class BaseSTVCounter:
# Continue current exclusion if applicable # Continue current exclusion if applicable
if self._exclusion is not None: 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') __pragma__('opov')
return self._exclusion[0] return self._exclusion[0]
__pragma__('noopov') __pragma__('noopov')
@ -477,8 +423,14 @@ class BaseSTVCounter:
candidates_excluded = self.candidates_to_bulk_exclude(hopefuls) 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)] candidates_excluded = [self.choose_lowest(hopefuls)]
self.logs.append('No surpluses to distribute, so ' + candidates_excluded[0][0].name + ' is excluded.')
return candidates_excluded return candidates_excluded
@ -516,17 +468,22 @@ class BaseSTVCounter:
else: else:
# Round up (preserving the original quota if exact) # Round up (preserving the original quota if exact)
self.quota = self.quota.round(self.options['round_quota'], self.quota.ROUND_UP) 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') __pragma__('noopov')
if self.options['quota_mode'] == 'ers97' and self.num_elected < self.election.seats: if self.options['quota_mode'] == 'ers97' and self.num_elected < self.election.seats:
# Calculate the total active vote # Calculate the total active vote
__pragma__('opov') __pragma__('opov')
orig_vre = self.vote_required_election
total_active_vote = \ 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 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')) 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) self.vote_required_election = total_active_vote / Num(self.election.seats - self.num_elected + 1)
if self.options['round_votes'] is not None: 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) 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') __pragma__('noopov')
def meets_quota(self, count_card): def meets_quota(self, count_card):
@ -555,6 +512,10 @@ class BaseSTVCounter:
if len(meets_quota) > 0: if len(meets_quota) > 0:
meets_quota.sort(key=lambda x: x[1].votes, reverse=True) 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 # Declare elected any candidate who meets the quota
while len(meets_quota) > 0: while len(meets_quota) > 0:
@ -566,11 +527,11 @@ class BaseSTVCounter:
count_card.order_elected = self.num_elected count_card.order_elected = self.num_elected
meets_quota.remove(x) meets_quota.remove(x)
if self.options['quota_mode'] == 'ers97': if self.options['quota_mode'] == 'ers97':
self.compute_quota() self.compute_quota()
self.elect_meeting_quota() # Repeat as the vote required for election may have changed self.elect_meeting_quota() # Repeat as the vote required for election may have changed
return return
# ----------------- # -----------------
# UTILITY FUNCTIONS # UTILITY FUNCTIONS
@ -671,3 +632,29 @@ class BaseSTVCounter:
if self.options['round_tvs'] is None: if self.options['round_tvs'] is None:
return num return num
return num.round(self.options['round_tvs'], num.ROUND_DOWN) 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]

View File

@ -155,14 +155,18 @@ class WIGSTVCounter(BaseSTVCounter):
else: else:
raise STVException('Invalid exclusion mode') 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] this_exclusion = self._exclusion[1][0]
self._exclusion[1].remove(this_exclusion) self._exclusion[1].remove(this_exclusion)
# Transfer votes # Transfer votes
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([bb for c, bb in this_exclusion]) 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(): for candidate, x in next_preferences.items():
cand_ballots, num_ballots, num_votes = x[0], x[1], x[2] cand_ballots, num_ballots, num_votes = x[0], x[1], x[2]
@ -199,6 +203,9 @@ class WIGSTVCounter(BaseSTVCounter):
__pragma__('noopov') __pragma__('noopov')
if len(self._exclusion[1]) == 0: 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: for candidate_excluded, count_card in candidates_excluded:
__pragma__('opov') __pragma__('opov')
count_card.transfers -= count_card.votes count_card.transfers -= count_card.votes

View File

@ -17,7 +17,7 @@
__pragma__ = lambda x: None __pragma__ = lambda x: None
from pyRCV2.method.base_stv import BaseSTVCounter, STVException 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.numbers import Num
from pyRCV2.safedict import SafeDict from pyRCV2.safedict import SafeDict
@ -56,9 +56,25 @@ class MeekSTVCounter(BaseSTVCounter):
raise STVException('Meek method is incompatible with --transferable-only') raise STVException('Meek method is incompatible with --transferable-only')
if self.options['exclusion'] != 'one_round': if self.options['exclusion'] != 'one_round':
raise STVException('Meek method requires --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: if self.options['round_tvs'] is not None:
raise STVException('Meek method is incompatible with --round-tvs') 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): def distribute_first_preferences(self):
""" """
@ -137,7 +153,11 @@ class MeekSTVCounter(BaseSTVCounter):
__pragma__('noopov') __pragma__('noopov')
if len(has_surplus) > 0: if len(has_surplus) > 0:
num_iterations = 0
orig_quota = self.quota
while len(has_surplus) > 0: while len(has_surplus) > 0:
num_iterations += 1
# Recompute keep values # Recompute keep values
for candidate, count_card in has_surplus: for candidate, count_card in has_surplus:
__pragma__('opov') __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] 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') __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 # 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 # NB: We could do this earlier, but this shows the flow of the election more clearly in the count sheet
self.elect_meeting_quota() self.elect_meeting_quota()
__pragma__('opov') return self.make_result('Surpluses distributed')
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
def do_exclusion(self, candidates_excluded): def do_exclusion(self, candidates_excluded):
""" """
@ -194,6 +211,10 @@ class MeekSTVCounter(BaseSTVCounter):
if len(meets_quota) > 0: if len(meets_quota) > 0:
meets_quota.sort(key=lambda x: x[1].votes, reverse=True) 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 # Declare elected any candidate who meets the quota
while len(meets_quota) > 0: while len(meets_quota) > 0:
@ -205,3 +226,33 @@ class MeekSTVCounter(BaseSTVCounter):
count_card.order_elected = self.num_elected count_card.order_elected = self.num_elected
meets_quota.remove(x) 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')

View File

@ -103,8 +103,9 @@ class CountCard:
__pragma__('noopov') __pragma__('noopov')
class CountStepResult: 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.comment = comment
self.logs = logs
self.candidates = candidates # SafeDict: Candidate -> CountCard self.candidates = candidates # SafeDict: Candidate -> CountCard
self.exhausted = exhausted # CountCard self.exhausted = exhausted # CountCard