This repository has been archived on 2021-05-25. You can view files and clone it, but cannot push or open issues or pull requests.
pyRCV2/pyRCV2/method/base_stv.py

816 lines
25 KiB
Python
Raw Normal View History

2020-10-17 22:20:13 +11:00
# pyRCV2: Preferential vote counting
# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
2020-10-17 22:20:13 +11:00
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
__pragma__ = lambda x: None
is_py = False
__pragma__('skip')
is_py = True
__pragma__('noskip')
2020-10-17 22:20:13 +11:00
from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepResult
from pyRCV2.numbers import Num
2020-10-17 22:20:13 +11:00
from pyRCV2.safedict import SafeDict
# Stubs for JS
def groupby(iterable, keyfunc):
if is_py:
__pragma__('skip')
import itertools
return [list(g) for k, g in itertools.groupby(iterable, keyfunc)]
__pragma__('noskip')
else:
groups = []
group = []
last_result = None
for i in iterable:
this_result = keyfunc(i)
__pragma__('opov')
if last_result is not None and this_result != last_result:
__pragma__('noopov')
groups.append(group)
group = []
last_result = this_result
group.append(i)
if group:
groups.append(group)
return groups
class STVException(Exception):
pass
class BaseSTVCounter:
"""
Basic STV counter for various different variations
"""
2020-10-17 22:20:13 +11:00
def __init__(self, election, options=None):
2020-10-17 22:20:13 +11:00
self.election = election
# Default options
self.options = {
2020-12-29 02:43:37 +11:00
'prog_quota': False, # Progressively reducing quota?
'bulk_elect': True, # Bulk election?
'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
'quota_criterion': 'geq', # 'geq' or 'gt'
2020-12-29 02:43:37 +11:00
'surplus_order': 'size', # 'size' or 'order'
'papers': 'both', # 'both' or 'transferable'
'exclusion': 'one_round', # 'one_round', 'parcels_by_order', 'by_value' or 'wright'
'ties': [], # List of tie strategies (e.g. TiesRandom)
'round_votes': None, # Number of decimal places or None
'round_weights': None, # Number of decimal places or None
}
if options is not None:
self.options.update(options)
2020-10-18 21:24:12 +11:00
2020-10-17 22:20:13 +11:00
self.candidates = SafeDict([(c, CountCard()) for c in self.election.candidates])
self.exhausted = CountCard()
2020-10-18 03:48:00 +11:00
self.loss_fraction = CountCard()
self.total_orig = sum((b.value for b in self.election.ballots), Num('0'))
2020-10-17 22:20:13 +11:00
self.num_elected = 0
2020-10-17 22:20:13 +11:00
# Withdraw candidates
for candidate in self.election.withdrawn:
__pragma__('opov')
self.candidates[candidate].state = CandidateState.WITHDRAWN
__pragma__('noopov')
2020-10-18 21:24:12 +11:00
def reset(self):
"""
Public function:
2020-12-24 01:36:39 +11:00
Perform the first step (distribute first preferences)
Does not reset the states of candidates, etc.
2020-10-18 21:24:12 +11:00
"""
self.distribute_first_preferences()
self.quota = None
self.compute_quota()
self.elect_meeting_quota()
__pragma__('opov')
2020-12-24 01:36:39 +11:00
result = CountStepResult(
'First preferences',
self.candidates,
self.exhausted,
self.loss_fraction,
self.total + self.exhausted.votes + self.loss_fraction.votes,
self.quota
)
__pragma__('noopov')
2020-12-24 01:36:39 +11:00
self.step_results = [result]
return result
2020-10-18 21:24:12 +11:00
def distribute_first_preferences(self):
"""
Distribute first preferences (called as part of the reset() step)
"""
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
2020-12-29 02:43:37 +11:00
if len(self.candidates[candidate].parcels) == 0:
self.candidates[candidate].parcels.append([(ballot, Num(ballot.value))])
2020-12-29 02:43:37 +11:00
else:
self.candidates[candidate].parcels[0].append((ballot, Num(ballot.value)))
else:
self.exhausted.transfers += ballot.value
#self.exhausted.parcels[0].append((ballot, Num(ballot.value)))
__pragma__('noopov')
2020-10-17 22:20:13 +11:00
def step(self):
2020-10-18 21:24:12 +11:00
"""
Public function:
Perform one step of the STV count
"""
2020-10-17 22:20:13 +11:00
# Step count cards
2020-10-18 21:24:12 +11:00
self.step_count_cards()
# Check if done
result = self.before_surpluses()
2020-10-18 21:24:12 +11:00
if result:
return result
# Distribute surpluses
result = self.distribute_surpluses()
if result:
return result
# Check if done (2)
result = self.before_exclusion()
if result:
return result
2020-10-18 21:24:12 +11:00
# Insufficient winners and no surpluses to distribute
# Exclude the lowest ranked hopeful
result = self.exclude_candidate()
if result:
return result
raise STVException('Unable to complete step')
def step_count_cards(self):
"""
Reset the count cards for the beginning of a new step
"""
2020-10-17 22:20:13 +11:00
for candidate, count_card in self.candidates.items():
count_card.step()
self.exhausted.step()
2020-10-18 03:48:00 +11:00
self.loss_fraction.step()
2020-10-18 21:24:12 +11:00
def before_surpluses(self):
2020-10-18 21:24:12 +11:00
"""
Check if the count can be completed before distributing surpluses
2020-10-18 21:24:12 +11:00
"""
2020-10-17 22:20:13 +11:00
# Have sufficient candidates been elected?
if self.num_elected >= self.election.seats:
2020-10-17 22:20:13 +11:00
return CountCompleted()
# Are there just enough candidates to fill all the seats?
if self.options['bulk_elect']:
if self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL or 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
count_card.order_elected = self.num_elected
self.num_elected += 1
__pragma__('opov')
result = CountStepResult(
'Bulk election',
self.candidates,
self.exhausted,
self.loss_fraction,
self.total + self.exhausted.votes + self.loss_fraction.votes,
self.quota
)
__pragma__('noopov')
self.step_results.append(result)
return result
2020-10-18 21:24:12 +11:00
def distribute_surpluses(self):
"""
Distribute surpluses, if any
"""
2020-10-17 22:20:13 +11:00
# Do not interrupt an exclusion
if any(cc.state == CandidateState.EXCLUDING for c, cc in self.candidates.items()):
return
# Are we distributing a surplus?
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.DISTRIBUTING_SURPLUS]
2020-10-17 22:20:13 +11:00
# Do surpluses need to be distributed?
if len(has_surplus) == 0:
__pragma__('opov')
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED and cc.votes > self.quota]
__pragma__('noopov')
2020-10-17 22:20:13 +11:00
if len(has_surplus) > 0:
# Distribute surpluses in specified order
if self.options['surplus_order'] == 'size':
2020-12-23 21:39:43 +11:00
has_surplus.sort(key=lambda x: x[1].votes, reverse=True)
2020-12-23 22:36:49 +11:00
candidate_surplus, count_card = self.choose_highest(has_surplus) # May need to break ties
elif self.options['surplus_order'] == 'order':
2020-12-23 21:39:43 +11:00
has_surplus.sort(key=lambda x: x[1].order_elected)
2020-12-23 22:36:49 +11:00
candidate_surplus, count_card = has_surplus[0] # Ties were already broken when these were assigned
else:
raise STVException('Invalid surplus order option')
count_card.state = CandidateState.DISTRIBUTING_SURPLUS
2020-10-17 22:20:13 +11:00
__pragma__('opov')
surplus = count_card.votes - self.quota
__pragma__('noopov')
# Transfer surplus
2020-12-29 02:43:37 +11:00
self.do_surplus(candidate_surplus, count_card, surplus)
2020-10-17 22:20:13 +11:00
2020-12-29 02:43:37 +11:00
# Declare elected any candidates meeting the quota as a result of surpluses
self.compute_quota()
2020-10-18 03:25:41 +11:00
2020-10-17 22:20:13 +11:00
self.elect_meeting_quota()
2020-10-18 02:54:51 +11:00
__pragma__('opov')
2020-12-24 01:36:39 +11:00
result = CountStepResult(
2020-10-17 23:09:29 +11:00
'Surplus of ' + candidate_surplus.name,
2020-10-17 22:20:13 +11:00
self.candidates,
self.exhausted,
2020-10-18 03:48:00 +11:00
self.loss_fraction,
self.total + self.exhausted.votes + self.loss_fraction.votes,
2020-10-17 22:20:13 +11:00
self.quota
)
2020-10-18 02:54:51 +11:00
__pragma__('noopov')
2020-12-24 01:36:39 +11:00
self.step_results.append(result)
return result
2020-10-18 21:24:12 +11:00
def do_surplus(self, candidate_surplus, count_card, surplus):
"""
Transfer the surplus of the given candidate
Subclasses must override this function
"""
raise NotImplementedError('Method not implemented')
def before_exclusion(self):
"""
Check before excluding a candidate
"""
# If we did not perform bulk election in before_surpluses: Are there just enough candidates to fill all the seats?
if not self.options['bulk_elect']:
if self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL) <= self.election.seats:
# Declare elected one remaining candidate at a time
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
hopefuls.sort(key=lambda x: x[1].votes, reverse=True)
candidate_elected, count_card = self.choose_highest(hopefuls)
count_card.state = CandidateState.PROVISIONALLY_ELECTED
count_card.order_elected = self.num_elected
self.num_elected += 1
__pragma__('opov')
result = CountStepResult(
'Bulk election',
self.candidates,
self.exhausted,
self.loss_fraction,
self.total + self.exhausted.votes + self.loss_fraction.votes,
self.quota
)
__pragma__('noopov')
self.step_results.append(result)
return result
2020-10-18 21:24:12 +11:00
def exclude_candidate(self):
"""
Exclude the lowest ranked hopeful
"""
2020-10-17 22:20:13 +11:00
2020-10-18 21:47:59 +11:00
candidate_excluded, count_card = self.candidate_to_exclude()
count_card.state = CandidateState.EXCLUDING
# Handle Wright STV
if self.options['exclusion'] == 'wright':
count_card.state = CandidateState.EXCLUDED
# Reset the count
# Carry over certain candidate states
new_candidates = SafeDict()
for candidate, count_card in self.candidates.items():
new_count_card = CountCard()
if count_card.state == CandidateState.WITHDRAWN:
new_count_card.state = CandidateState.WITHDRAWN
elif count_card.state == CandidateState.EXCLUDED:
new_count_card.state = CandidateState.EXCLUDED
__pragma__('opov')
new_candidates[candidate] = new_count_card
__pragma__('noopov')
self.candidates = new_candidates
self.exhausted = CountCard()
self.loss_fraction = CountCard()
self.num_elected = 0
step_results = self.step_results # Carry over step results
result = self.reset()
self.step_results = step_results
result.comment = 'Exclusion of ' + candidate_excluded.name
return result
# Exclude this candidate
2020-12-29 02:43:37 +11:00
self.do_exclusion(candidate_excluded, count_card)
2020-10-17 22:20:13 +11:00
# Declare any candidates meeting the quota as a result of exclusion
self.compute_quota()
2020-10-17 22:20:13 +11:00
self.elect_meeting_quota()
2020-10-18 02:54:51 +11:00
__pragma__('opov')
2020-12-24 01:36:39 +11:00
result = CountStepResult(
2020-10-17 23:09:29 +11:00
'Exclusion of ' + candidate_excluded.name,
2020-10-17 22:20:13 +11:00
self.candidates,
self.exhausted,
2020-10-18 03:48:00 +11:00
self.loss_fraction,
self.total + self.exhausted.votes + self.loss_fraction.votes,
2020-10-17 22:20:13 +11:00
self.quota
)
2020-10-18 02:54:51 +11:00
__pragma__('noopov')
2020-12-24 01:36:39 +11:00
self.step_results.append(result)
return result
2020-10-17 22:20:13 +11:00
2020-10-18 21:47:59 +11:00
def candidate_to_exclude(self):
"""
Determine the candidate to exclude
"""
# Continue current exclusion if applicable
excluding = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.EXCLUDING]
if len(excluding) > 0:
return excluding[0][0], excluding[0][1]
2020-10-18 21:47:59 +11:00
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
2020-12-23 21:39:43 +11:00
hopefuls.sort(key=lambda x: x[1].votes)
2020-10-18 21:47:59 +11:00
2020-12-23 22:36:49 +11:00
candidate_excluded, count_card = self.choose_lowest(hopefuls)
2020-10-18 21:47:59 +11:00
return candidate_excluded, count_card
2020-10-18 21:24:12 +11:00
def do_exclusion(self, candidate_excluded, count_card):
"""
Exclude the given candidate and transfer the votes
Subclasses must override this function
"""
raise NotImplementedError('Method not implemented')
2020-10-17 22:20:13 +11:00
def compute_quota(self):
2020-10-18 21:24:12 +11:00
"""
Recount total votes and (if applicable) recalculate the quota
"""
2020-10-17 22:20:13 +11:00
__pragma__('opov')
2020-10-18 02:54:51 +11:00
self.total = sum((cc.votes for c, cc in self.candidates.items()), Num('0'))
2020-10-18 03:48:00 +11:00
self.loss_fraction.transfers += (self.total_orig - self.total - self.exhausted.votes) - self.loss_fraction.votes
if self.quota is None or self.options['prog_quota']:
if self.options['quota'] == 'droop':
self.quota = (self.total / Num(self.election.seats + 1)).__floor__() + Num('1')
elif self.options['quota'] == 'droop_exact':
self.quota = self.total / Num(self.election.seats + 1)
elif self.options['quota'] == 'hare':
self.quota = (self.total / Num(self.election.seats)).__floor__() + Num('1')
elif self.options['quota'] == 'hare_exact':
self.quota = self.total / Num(self.election.seats)
else:
raise STVException('Invalid quota option')
2020-10-17 22:20:13 +11:00
__pragma__('noopov')
def meets_quota(self, count_card):
2020-10-18 21:24:12 +11:00
"""
Determine if the given candidate meets the quota
"""
if self.options['quota_criterion'] == 'geq':
__pragma__('opov')
return count_card.votes >= self.quota
__pragma__('noopov')
elif self.options['quota_criterion'] == 'gt':
__pragma__('opov')
return count_card.votes > self.quota
__pragma__('noopov')
else:
raise STVException('Invalid quota criterion')
2020-10-17 22:20:13 +11:00
def elect_meeting_quota(self):
2020-10-18 21:24:12 +11:00
"""
Elect all candidates meeting the quota
"""
2020-10-17 22:20:13 +11:00
# Does a candidate meet the quota?
meets_quota = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL and self.meets_quota(cc)]
2020-10-17 22:20:13 +11:00
if len(meets_quota) > 0:
2020-12-23 22:36:49 +11:00
meets_quota.sort(key=lambda x: x[1].votes, reverse=True)
2020-10-17 22:20:13 +11:00
# Declare elected any candidate who meets the quota
2020-12-23 22:36:49 +11:00
while len(meets_quota) > 0:
x = self.choose_highest(meets_quota)
candidate, count_card = x[0], x[1]
2020-10-17 22:20:13 +11:00
count_card.state = CandidateState.PROVISIONALLY_ELECTED
count_card.order_elected = self.num_elected
self.num_elected += 1
2020-12-23 22:36:49 +11:00
meets_quota.remove(x)
# -----------------
# UTILITY FUNCTIONS
# -----------------
def next_preferences(self, parcels):
"""
Examine the specified parcels and group ballot papers by next available preference
"""
# SafeDict: Candidate -> [List[Ballot], ballots, votes]
next_preferences = SafeDict([(c, [[], Num('0'), Num('0')]) for c, cc in self.candidates.items()])
total_ballots = Num('0')
total_votes = Num('0')
next_exhausted = []
exhausted_ballots = Num('0')
exhausted_votes = Num('0')
for parcel in parcels:
for ballot, ballot_value in parcel:
__pragma__('opov')
total_ballots += ballot.value
total_votes += ballot_value
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
next_preferences[candidate][0].append((ballot, ballot_value))
next_preferences[candidate][1] += ballot.value
next_preferences[candidate][2] += ballot_value
else:
next_exhausted.append((ballot, ballot_value))
exhausted_ballots += ballot.value
exhausted_votes += ballot_value
__pragma__('noopov')
return next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes
2020-12-23 22:36:49 +11:00
def choose_lowest(self, l):
"""
Provided a list of tuples (Candidate, CountCard), sorted in ASCENDING order of votes, choose the tuple with the fewest votes, breaking ties appropriately
"""
if len(l) == 1:
return l[0]
2020-12-24 00:04:30 +11:00
__pragma__('opov')
# Do not use (c, cc) for c, cc in ... as this will break equality in JS
tied = [x for x in l if x[1].votes == l[0][1].votes]
__pragma__('noopov')
2020-12-23 22:36:49 +11:00
if len(tied) == 1:
return tied[0]
# A tie exists
for tie in self.options['ties']:
result = tie.choose_lowest(tied)
if result is not None:
return result
raise Exception('Unable to resolve tie')
def choose_highest(self, l):
"""
Provided a list of tuples (Candidate, CountCard), sorted in DESCENDING order of votes, choose the tuple with the most votes, breaking ties appropriately
"""
if len(l) == 1:
return l[0]
2020-12-24 00:04:30 +11:00
__pragma__('opov')
# Do not use (c, cc) for c, cc in ... as this will break equality in JS
tied = [x for x in l if x[1].votes == l[0][1].votes]
__pragma__('noopov')
2020-12-23 22:36:49 +11:00
if len(tied) == 1:
return tied[0]
# A tie exists
for tie in self.options['ties']:
result = tie.choose_highest(tied)
if result is not None:
return result
raise Exception('Unable to resolve tie')
def round_votes(self, num):
if self.options['round_votes'] is None:
return num
return num.round(self.options['round_votes'], num.ROUND_DOWN)
def round_weight(self, num):
if self.options['round_weights'] is None:
return num
return num.round(self.options['round_weights'], num.ROUND_DOWN)
2020-12-27 18:57:36 +11:00
class WIGSTVCounter(BaseSTVCounter):
"""
Basic weighted inclusive Gregory STV counter
"""
2020-10-18 21:24:12 +11:00
def do_surplus(self, candidate_surplus, count_card, surplus):
2020-12-29 02:43:37 +11:00
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels)
if self.options['papers'] == 'transferable':
__pragma__('opov')
transferable_votes = total_votes - exhausted_votes
2020-12-29 02:43:37 +11:00
__pragma__('noopov')
for candidate, x in next_preferences.items():
cand_ballots = x[0]
num_ballots = x[1]
num_votes = x[2]
2020-12-29 02:43:37 +11:00
new_parcel = []
if len(cand_ballots) > 0:
__pragma__('opov')
self.candidates[candidate].parcels.append(new_parcel)
self.candidates[candidate]._parcels_sorted = False
2020-12-29 02:43:37 +11:00
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
__pragma__('opov')
if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / transferable_votes)
2020-12-29 02:43:37 +11:00
else:
self.candidates[candidate].transfers += self.round_votes(num_votes) # Do not allow weight to increase
else:
self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / total_votes)
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
for ballot, ballot_value in cand_ballots:
__pragma__('opov')
if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
new_value = (ballot_value * surplus) / transferable_votes
else:
new_value = ballot_value
else:
new_value = (ballot_value * surplus) / total_votes
2020-12-29 02:43:37 +11:00
new_parcel.append((ballot, self.round_weight(new_value)))
2020-12-29 02:43:37 +11:00
__pragma__('noopov')
__pragma__('opov')
if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
pass # No ballots exhaust
else:
self.exhausted.transfers += self.round_votes((surplus - transferable_votes))
2020-12-29 02:43:37 +11:00
else:
self.exhausted.transfers += self.round_votes((exhausted_votes * surplus) / total_votes)
2020-12-29 02:43:37 +11:00
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
__pragma__('opov')
count_card.transfers -= surplus
__pragma__('noopov')
count_card.state = CandidateState.ELECTED
2020-10-18 21:24:12 +11:00
def do_exclusion(self, candidate_excluded, count_card):
2020-12-29 02:43:37 +11:00
if self.options['exclusion'] == 'parcels_by_order':
if len(count_card.parcels) > 0:
parcel = count_card.parcels[0]
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([parcel])
else:
# TODO: Skip this entirely if this is the case
parcel = []
2020-12-29 02:43:37 +11:00
count_card.parcels.remove(parcel)
elif self.options['exclusion'] == 'by_value':
# Sort the ballots by value
if not count_card._parcels_sorted:
ballots = [(b, bv) for p in count_card.parcels for b, bv in p]
__pragma__('opov')
ballots.sort(key=lambda x: x[1] / x[0].value, reverse=True)
# Round to 8 decimal places to consider equality
# FIXME: Work out a better way of doing this
count_card.parcels = groupby(ballots, lambda x: (x[1] / x[0].value).round(8, x[1].ROUND_DOWN))
count_card._parcels_sorted = True
__pragma__('noopov')
if len(count_card.parcels) > 0:
parcel = count_card.parcels[0]
count_card.parcels.remove(parcel)
else:
parcel = []
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([parcel])
2020-12-29 02:43:37 +11:00
else: # one_round
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels)
count_card.parcels = []
for candidate, x in next_preferences.items():
cand_ballots = x[0]
num_ballots = x[1]
num_votes = x[2]
2020-12-29 02:43:37 +11:00
new_parcel = []
if len(cand_ballots) > 0:
__pragma__('opov')
self.candidates[candidate].parcels.append(new_parcel)
self.candidates[candidate]._parcels_sorted = False
2020-12-29 02:43:37 +11:00
__pragma__('noopov')
__pragma__('opov')
self.candidates[candidate].transfers += self.round_votes(num_votes)
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
for ballot, ballot_value in cand_ballots:
__pragma__('opov')
new_parcel.append((ballot, ballot_value))
__pragma__('noopov')
__pragma__('opov')
self.exhausted.transfers += self.round_votes(exhausted_votes)
2020-12-29 02:43:37 +11:00
__pragma__('noopov')
__pragma__('opov')
count_card.transfers -= total_votes
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
if len(count_card.parcels) == 0:
__pragma__('opov')
count_card.transfers -= count_card.votes
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
count_card.state = CandidateState.EXCLUDED
2020-12-29 02:43:37 +11:00
class UIGSTVCounter(WIGSTVCounter):
"""
Basic unweighted inclusive Gregory STV counter
"""
def __init__(self, *args):
2020-12-29 02:43:37 +11:00
WIGSTVCounter.__init__(self, *args)
2020-10-18 21:24:12 +11:00
def do_surplus(self, candidate_surplus, count_card, surplus):
2020-12-29 02:43:37 +11:00
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels)
2020-12-29 02:43:37 +11:00
if self.options['papers'] == 'transferable':
__pragma__('opov')
transferable_ballots = total_ballots - exhausted_ballots
transferable_votes = total_votes - exhausted_votes
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
for candidate, x in next_preferences.items():
cand_ballots = x[0]
num_ballots = x[1]
num_votes = x[2]
2020-12-29 02:43:37 +11:00
new_parcel = []
if len(cand_ballots) > 0:
__pragma__('opov')
2020-12-29 02:43:37 +11:00
self.candidates[candidate].parcels.append(new_parcel)
self.candidates[candidate]._parcels_sorted = False
__pragma__('noopov')
__pragma__('opov')
2020-12-29 02:43:37 +11:00
if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots)
2020-12-29 02:43:37 +11:00
else:
self.candidates[candidate].transfers += self.round_votes(num_votes)
2020-12-29 02:43:37 +11:00
else:
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots)
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
for ballot, ballot_value in cand_ballots:
__pragma__('opov')
2020-12-29 02:43:37 +11:00
if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
new_value = (ballot.value * surplus) / transferable_ballots
2020-12-29 02:43:37 +11:00
else:
new_value = ballot_value
else:
new_value = (ballot.value * surplus) / total_ballots
2020-12-29 02:43:37 +11:00
new_parcel.append((ballot, self.round_weight(new_value)))
__pragma__('noopov')
__pragma__('opov')
2020-12-29 02:43:37 +11:00
if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
2020-12-29 02:43:37 +11:00
pass # No ballots exhaust
else:
self.exhausted.transfers += self.round_votes(surplus - transferable_votes)
2020-12-29 02:43:37 +11:00
else:
self.exhausted.transfers += self.round_votes((exhausted_ballots * surplus) / total_ballots)
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
__pragma__('opov')
count_card.transfers -= surplus
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
count_card.state = CandidateState.ELECTED
class EGSTVCounter(UIGSTVCounter):
"""
Exclusive Gregory (last bundle) STV implementation
"""
2020-12-29 02:43:37 +11:00
def do_surplus(self, candidate_surplus, count_card, surplus):
"""Overrides UIGSTVCounter.do_surplus"""
2020-12-29 02:43:37 +11:00
last_bundle = count_card.parcels[len(count_card.parcels)-1]
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([last_bundle])
if self.options['papers'] == 'transferable':
__pragma__('opov')
2020-12-29 02:43:37 +11:00
transferable_ballots = total_ballots - exhausted_ballots
transferable_votes = total_votes - exhausted_votes
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
for candidate, x in next_preferences.items():
cand_ballots = x[0]
num_ballots = x[1]
num_votes = x[2]
2020-12-29 02:43:37 +11:00
new_parcel = []
if len(cand_ballots) > 0:
__pragma__('opov')
2020-12-29 02:43:37 +11:00
self.candidates[candidate].parcels.append(new_parcel)
self.candidates[candidate]._parcels_sorted = False
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
__pragma__('opov')
if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots)
2020-12-29 02:43:37 +11:00
else:
self.candidates[candidate].transfers += self.round_votes(num_votes)
else:
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots)
2020-12-29 02:43:37 +11:00
__pragma__('noopov')
for ballot, ballot_value in cand_ballots:
__pragma__('opov')
2020-12-29 02:43:37 +11:00
if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
new_value = (ballot.value * surplus) / transferable_ballots
else:
new_value = ballot_value
else:
new_value = (ballot.value * surplus) / total_ballots
new_parcel.append((ballot, self.round_weight(new_value)))
__pragma__('noopov')
__pragma__('opov')
2020-12-29 02:43:37 +11:00
if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
pass # No ballots exhaust
else:
self.exhausted.transfers += self.round_votes((surplus - transferable_votes))
2020-12-29 02:43:37 +11:00
else:
self.exhausted.transfers += self.round_votes((exhausted_ballots * surplus) / total_ballots)
2020-12-29 02:43:37 +11:00
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
__pragma__('opov')
count_card.transfers -= surplus
__pragma__('noopov')
2020-12-29 02:43:37 +11:00
count_card.state = CandidateState.ELECTED