Use tree-packed ballots for Meek STV

Runtime on Senate data:
1m54s not tree packed
13s tree packed (!!!!!)
This commit is contained in:
RunasSudo 2021-01-16 02:21:38 +11:00
parent 37895d03ca
commit dbb80de0cb
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 94 additions and 33 deletions

View File

@ -39,6 +39,47 @@ class MeekCountCard(CountCard):
result.keep_value = keep_value result.keep_value = keep_value
return result return result
class BallotTree:
def __init__(self):
self.num = Num('0')
self.ballots = [] # List of tuples (ballot, idx)
self.next_preferences = None # SafeDict: Candidate -> BallotTree
self.next_exhausted = None # BallotTree
def descend_tree(self):
"""Expand one further level of the tree"""
if self.next_exhausted is not None:
raise Exception('Attempt to descend into already descended tree')
self.next_preferences = SafeDict()
self.next_exhausted = BallotTree()
for ballot, idx in self.ballots:
if idx is None:
idx = 0
else:
idx = idx + 1
if idx < len(ballot.preferences):
cand = ballot.preferences[idx]
__pragma__('opov')
if cand not in self.next_preferences:
np = BallotTree()
self.next_preferences[cand] = np
else:
np = self.next_preferences[cand]
np.num += ballot.value
__pragma__('noopov')
np.ballots.append((ballot, idx))
else:
__pragma__('opov')
self.next_exhausted.num += ballot.value
__pragma__('noopov')
self.next_exhausted.ballots.append((ballot, idx))
class MeekSTVCounter(BaseSTVCounter): class MeekSTVCounter(BaseSTVCounter):
def describe_options(self): def describe_options(self):
"""Overrides BaseSTVCounter.describe_options""" """Overrides BaseSTVCounter.describe_options"""
@ -46,9 +87,9 @@ class MeekSTVCounter(BaseSTVCounter):
def __init__(self, *args): def __init__(self, *args):
BaseSTVCounter.__init__(self, *args) BaseSTVCounter.__init__(self, *args)
self.candidates = SafeDict([(c, MeekCountCard()) for c in self.election.candidates])
# Withdraw candidates # Convert to MeekCountCard
self.candidates = SafeDict([(c, MeekCountCard()) for c in self.election.candidates])
for candidate in self.election.withdrawn: for candidate in self.election.withdrawn:
__pragma__('opov') __pragma__('opov')
self.candidates[candidate].state = CandidateState.WITHDRAWN self.candidates[candidate].state = CandidateState.WITHDRAWN
@ -56,6 +97,14 @@ class MeekSTVCounter(BaseSTVCounter):
self._quota_tolerance = Num('1.0001') self._quota_tolerance = Num('1.0001')
# For tree packing
self.ballots_tree = BallotTree()
for ballot in self.election.ballots:
__pragma__('opov')
self.ballots_tree.num += ballot.value
__pragma__('noopov')
self.ballots_tree.ballots.append((ballot, None))
def reset(self): def reset(self):
if self.options['quota_mode'] != 'progressive': if self.options['quota_mode'] != 'progressive':
raise STVException('Meek method requires --quota-mode progressive') raise STVException('Meek method requires --quota-mode progressive')
@ -87,6 +136,41 @@ class MeekSTVCounter(BaseSTVCounter):
return self.make_result('First preferences') return self.make_result('First preferences')
def distribute_recursively(self, tree, remaining_multiplier):
if tree.next_exhausted is None:
tree.descend_tree()
# Credit votes at this level
for candidate, cand_tree in tree.next_preferences.items():
__pragma__('opov')
count_card = self.candidates[candidate]
__pragma__('noopov')
if count_card.state == CandidateState.HOPEFUL:
# Hopeful candidate has keep value 1, so transfer entire remaining value
__pragma__('opov')
count_card.transfers += remaining_multiplier * cand_tree.num
__pragma__('noopov')
elif count_card.state == CandidateState.EXCLUDED or count_card.state == CandidateState.WITHDRAWN:
# Excluded candidate has keep value 0, so skip over this candidate
# Recurse
self.distribute_recursively(cand_tree, remaining_multiplier)
elif count_card.state == CandidateState.ELECTED:
# Transfer according to elected candidate's keep value
__pragma__('opov')
count_card.transfers += remaining_multiplier * cand_tree.num * count_card.keep_value
new_remaining_multiplier = remaining_multiplier * (Num(1) - count_card.keep_value)
__pragma__('noopov')
# Recurse
self.distribute_recursively(cand_tree, new_remaining_multiplier)
else:
raise STVException('Unexpected candidate state')
# Credit exhausted votes at this level
__pragma__('opov')
self.exhausted.transfers += remaining_multiplier * tree.next_exhausted.num
__pragma__('noopov')
def distribute_first_preferences(self): def distribute_first_preferences(self):
""" """
Overrides BaseSTVCounter.distribute_first_preferences Overrides BaseSTVCounter.distribute_first_preferences
@ -111,36 +195,7 @@ class MeekSTVCounter(BaseSTVCounter):
self.loss_fraction = CountCard() self.loss_fraction = CountCard()
# Distribute votes # Distribute votes
for ballot in self.election.ballots: self.distribute_recursively(self.ballots_tree, Num('1'))
remaining_value = Num(ballot.value) # Clone the value so we don't update ballot.value
for candidate in ballot.preferences:
__pragma__('opov')
count_card = self.candidates[candidate]
__pragma__('noopov')
if count_card.state == CandidateState.HOPEFUL:
# Hopeful candidate has keep value 1, so transfer entire remaining value
__pragma__('opov')
count_card.transfers += remaining_value
__pragma__('noopov')
remaining_value = Num(0)
break
elif count_card.state == CandidateState.EXCLUDED or count_card.state == CandidateState.WITHDRAWN:
# Excluded candidate has keep value 0, so skip over this candidate
pass
elif count_card.state == CandidateState.ELECTED:
# Transfer according to elected candidate's keep value
__pragma__('opov')
count_card.transfers += remaining_value * count_card.keep_value
remaining_value *= (Num(1) - count_card.keep_value)
__pragma__('noopov')
else:
raise STVException('Unexpected candidate state')
# Credit exhausted votes
__pragma__('opov')
self.exhausted.transfers += remaining_value
__pragma__('noopov')
# Recompute transfers # Recompute transfers
if len(self.step_results) > 0: if len(self.step_results) > 0:

View File

@ -1,5 +1,5 @@
# pyRCV2: Preferential vote counting # pyRCV2: Preferential vote counting
# Copyright © 2020 Lee Yingtong Li (RunasSudo) # Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -30,6 +30,9 @@ class SafeDict:
def __setitem__(self, key, value): def __setitem__(self, key, value):
self.impl.set(key, value) self.impl.set(key, value)
def __contains__(self, key):
return self.impl.has(key)
def items(self): def items(self):
entries = self.impl.entries() # Returns an Iterator entries = self.impl.entries() # Returns an Iterator
return __pragma__('js', 'Array.from(entries)') return __pragma__('js', 'Array.from(entries)')

View File

@ -29,5 +29,8 @@ class SafeDict:
def __setitem__(self, key, value): def __setitem__(self, key, value):
self.impl[key] = value self.impl[key] = value
def __contains__(self, key):
return key in self.impl
def items(self): def items(self):
return self.impl.items() return self.impl.items()