From ede1da73add83f6a4171f2c13818182980c45fb2 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 9 Jan 2021 04:33:13 +1100 Subject: [PATCH] Add detailed comments per stage --- html/index.html | 2 + html/index.js | 92 ++++++++++++++---------- html/worker.js | 1 + pyRCV2/cli/stv.py | 8 ++- pyRCV2/method/base_stv.py | 147 +++++++++++++++++--------------------- pyRCV2/method/gregory.py | 11 ++- pyRCV2/method/meek.py | 83 ++++++++++++++++----- pyRCV2/model.py | 3 +- 8 files changed, 209 insertions(+), 138 deletions(-) diff --git a/html/index.html b/html/index.html index 4d13131..4978ba9 100644 --- a/html/index.html +++ b/html/index.html @@ -216,6 +216,8 @@
+
+ diff --git a/html/index.js b/html/index.js index c1d3cd6..0ab3341 100644 --- a/html/index.js +++ b/html/index.js @@ -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') { diff --git a/html/worker.js b/html/worker.js index 20030c2..5e1ac8b 100644 --- a/html/worker.js +++ b/html/worker.js @@ -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), diff --git a/pyRCV2/cli/stv.py b/pyRCV2/cli/stv.py index 27e8998..9bc6d65 100644 --- a/pyRCV2/cli/stv.py +++ b/pyRCV2/cli/stv.py @@ -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,7 +64,10 @@ 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: - 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: state = 'Excluded {}'.format(-count_card.order_elected) elif count_card.state == pyRCV2.model.CandidateState.WITHDRAWN: diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py index c6611e4..c11af83 100644 --- a/pyRCV2/method/base_stv.py +++ b/pyRCV2/method/base_stv.py @@ -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: - count_card.state = CandidateState.PROVISIONALLY_ELECTED - self.num_elected += 1 - count_card.order_elected = self.num_elected + 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.') - __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') + for candidate, count_card in candidates_elected: + count_card.state = CandidateState.PROVISIONALLY_ELECTED + self.num_elected += 1 + count_card.order_elected = self.num_elected - 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: @@ -566,11 +527,11 @@ class BaseSTVCounter: count_card.order_elected = self.num_elected meets_quota.remove(x) - - if self.options['quota_mode'] == 'ers97': - self.compute_quota() - self.elect_meeting_quota() # Repeat as the vote required for election may have changed - return + + if self.options['quota_mode'] == 'ers97': + self.compute_quota() + self.elect_meeting_quota() # Repeat as the vote required for election may have changed + return # ----------------- # UTILITY FUNCTIONS @@ -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] diff --git a/pyRCV2/method/gregory.py b/pyRCV2/method/gregory.py index 90c7ca5..32de54d 100644 --- a/pyRCV2/method/gregory.py +++ b/pyRCV2/method/gregory.py @@ -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 diff --git a/pyRCV2/method/meek.py b/pyRCV2/method/meek.py index e6d9669..e27fa60 100644 --- a/pyRCV2/method/meek.py +++ b/pyRCV2/method/meek.py @@ -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') diff --git a/pyRCV2/model.py b/pyRCV2/model.py index 7a70c68..80935d5 100644 --- a/pyRCV2/model.py +++ b/pyRCV2/model.py @@ -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