Customisable options for STV count in backend
This commit is contained in:
parent
4d9846d8b7
commit
dcc3dcc5e7
@ -20,12 +20,26 @@ from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepRes
|
||||
from pyRCV2.numbers import Num
|
||||
from pyRCV2.safedict import SafeDict
|
||||
|
||||
class STVCCounter:
|
||||
"""Count an STV election using pyRCV STV-C rules"""
|
||||
class STVException(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, election):
|
||||
class BaseSTVCounter:
|
||||
"""Basic STV counter for various different variations"""
|
||||
|
||||
def __init__(self, election, options=None):
|
||||
self.election = election
|
||||
|
||||
# Default options
|
||||
self.options = {
|
||||
'prog_quota': False, # Progressively reducing quota?
|
||||
'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
|
||||
'quota_criterion': 'geq', # 'geq' or 'gt'
|
||||
'surplus_order': 'size', # 'size' or 'order'
|
||||
}
|
||||
|
||||
if options is not None:
|
||||
self.options.update(options)
|
||||
|
||||
def reset(self):
|
||||
self.candidates = SafeDict([(c, CountCard()) for c in self.election.candidates])
|
||||
self.exhausted = CountCard()
|
||||
@ -33,6 +47,8 @@ class STVCCounter:
|
||||
|
||||
self.total_orig = sum((b.value for b in self.election.ballots), Num('0'))
|
||||
|
||||
self.num_elected = 0
|
||||
|
||||
# Withdraw candidates
|
||||
for candidate in self.election.withdrawn:
|
||||
__pragma__('opov')
|
||||
@ -52,6 +68,7 @@ class STVCCounter:
|
||||
self.exhausted.ballots.append((ballot, ballot.value))
|
||||
__pragma__('noopov')
|
||||
|
||||
self.quota = None
|
||||
self.compute_quota()
|
||||
self.elect_meeting_quota()
|
||||
|
||||
@ -74,15 +91,19 @@ class STVCCounter:
|
||||
self.loss_fraction.step()
|
||||
|
||||
# Have sufficient candidates been elected?
|
||||
if sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED or cc.state == CandidateState.ELECTED) >= self.election.seats:
|
||||
if self.num_elected >= self.election.seats:
|
||||
return CountCompleted()
|
||||
|
||||
# Are there just enough candidates to fill all the seats
|
||||
if sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED or cc.state == CandidateState.ELECTED or cc.state == CandidateState.HOPEFUL) <= self.election.seats:
|
||||
if self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL) <= self.election.seats:
|
||||
# TODO: Sort by size
|
||||
|
||||
# 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')
|
||||
return CountStepResult(
|
||||
@ -101,8 +122,14 @@ class STVCCounter:
|
||||
__pragma__('noopov')
|
||||
|
||||
if len(has_surplus) > 0:
|
||||
# Distribute surpluses in order of size
|
||||
# Distribute surpluses in specified order
|
||||
if self.options['surplus_order'] == 'size':
|
||||
has_surplus.sort(lambda x: x[1].votes, True)
|
||||
elif self.options['surplus_order'] == 'order':
|
||||
has_surplus.sort(lambda x: x[1].order_elected)
|
||||
else:
|
||||
raise STVException('Invalid surplus order option')
|
||||
|
||||
candidate_surplus, count_card = has_surplus[0]
|
||||
count_card.state = CandidateState.ELECTED
|
||||
|
||||
@ -185,20 +212,43 @@ class STVCCounter:
|
||||
__pragma__('noopov')
|
||||
|
||||
def compute_quota(self):
|
||||
# Compute quota
|
||||
"""Recount total votes and (if applicable) recalculate the quota"""
|
||||
__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.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')
|
||||
__pragma__('noopov')
|
||||
|
||||
def meets_quota(self, count_card):
|
||||
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')
|
||||
|
||||
def elect_meeting_quota(self):
|
||||
# Does a candidate meet the quota?
|
||||
__pragma__('opov')
|
||||
meets_quota = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL and cc.votes > self.quota]
|
||||
__pragma__('noopov')
|
||||
meets_quota = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL and self.meets_quota(cc)]
|
||||
|
||||
if len(meets_quota) > 0:
|
||||
# Declare elected any candidate who meets the quota
|
||||
for candidate, count_card in meets_quota:
|
||||
count_card.state = CandidateState.PROVISIONALLY_ELECTED
|
||||
count_card.order_elected = self.num_elected
|
||||
self.num_elected += 1
|
@ -61,6 +61,7 @@ class CountCard:
|
||||
self.transfers = Num('0')
|
||||
self.ballots = []
|
||||
self.state = CandidateState.HOPEFUL
|
||||
self.order_elected = None
|
||||
|
||||
@property
|
||||
def votes(self):
|
||||
|
@ -48,3 +48,6 @@ class Fixed:
|
||||
return self.impl.lt(other.impl)
|
||||
def __le__(self, other):
|
||||
return self.impl.lte(other.impl)
|
||||
|
||||
def __floor__(self):
|
||||
return Fixed(Math.floor(self.impl))
|
||||
|
@ -15,6 +15,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from decimal import Decimal
|
||||
import math
|
||||
|
||||
_quantize_exp = 6
|
||||
|
||||
@ -51,3 +52,6 @@ class Fixed:
|
||||
return self.impl < other.impl
|
||||
def __le__(self, other):
|
||||
return self.impl <= other.impl
|
||||
|
||||
def __floor__(self):
|
||||
return Fixed(math.floor(self.impl))
|
||||
|
@ -43,3 +43,6 @@ class NativeInt:
|
||||
return self.impl < other.impl
|
||||
def __le__(self, other):
|
||||
return self.impl <= other.impl
|
||||
|
||||
def __floor__(self):
|
||||
return NativeInt(Math.floor(self.impl))
|
||||
|
@ -14,6 +14,8 @@
|
||||
# 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/>.
|
||||
|
||||
import math
|
||||
|
||||
class NativeInt:
|
||||
"""
|
||||
Wrapper for Python int
|
||||
@ -43,3 +45,6 @@ class NativeInt:
|
||||
return self.impl < other.impl
|
||||
def __le__(self, other):
|
||||
return self.impl <= other.impl
|
||||
|
||||
def __floor__(self):
|
||||
return NativeInt(math.floor(self.impl))
|
||||
|
@ -43,3 +43,6 @@ class Native:
|
||||
return self.impl < other.impl
|
||||
def __le__(self, other):
|
||||
return self.impl <= other.impl
|
||||
|
||||
def __floor__(self):
|
||||
return Native(Math.floor(self.impl))
|
||||
|
@ -14,6 +14,8 @@
|
||||
# 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/>.
|
||||
|
||||
import math
|
||||
|
||||
class Native:
|
||||
"""
|
||||
Wrapper for Python float (naive floating-point arithmetic)
|
||||
@ -43,3 +45,6 @@ class Native:
|
||||
return self.impl < other.impl
|
||||
def __le__(self, other):
|
||||
return self.impl <= other.impl
|
||||
|
||||
def __floor__(self):
|
||||
return Native(math.floor(self.impl))
|
||||
|
@ -43,3 +43,6 @@ class Rational:
|
||||
return self.impl.lesser(other.impl)
|
||||
def __le__(self, other):
|
||||
return self.impl.lesserOrEquals(other.impl)
|
||||
|
||||
def __floor__(self):
|
||||
return Rational(self.impl.floor())
|
||||
|
@ -15,6 +15,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from fractions import Fraction
|
||||
import math
|
||||
|
||||
class Rational:
|
||||
"""
|
||||
@ -45,3 +46,6 @@ class Rational:
|
||||
return self.impl < other.impl
|
||||
def __le__(self, other):
|
||||
return self.impl <= other.impl
|
||||
|
||||
def __floor__(self):
|
||||
return Rational(math.floor(self.impl))
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import pyRCV2.blt
|
||||
import pyRCV2.model
|
||||
import pyRCV2.method, pyRCV2.method.STVCCounter
|
||||
import pyRCV2.method, pyRCV2.method.base_stv
|
||||
import pyRCV2.numbers
|
||||
|
||||
__pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};')
|
||||
|
@ -167,7 +167,6 @@
|
||||
} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED) {
|
||||
elTd.classList.add('elected');
|
||||
elTd.innerText = countCard.votes;
|
||||
elTd.style.fontWeight = 'bold';
|
||||
|
||||
elTr1.querySelector('td:first-child').classList.add('elected');
|
||||
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) {
|
||||
|
Reference in New Issue
Block a user