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.numbers import Num
|
||||||
from pyRCV2.safedict import SafeDict
|
from pyRCV2.safedict import SafeDict
|
||||||
|
|
||||||
class STVCCounter:
|
class STVException(Exception):
|
||||||
"""Count an STV election using pyRCV STV-C rules"""
|
pass
|
||||||
|
|
||||||
def __init__(self, election):
|
class BaseSTVCounter:
|
||||||
|
"""Basic STV counter for various different variations"""
|
||||||
|
|
||||||
|
def __init__(self, election, options=None):
|
||||||
self.election = election
|
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):
|
def reset(self):
|
||||||
self.candidates = SafeDict([(c, CountCard()) for c in self.election.candidates])
|
self.candidates = SafeDict([(c, CountCard()) for c in self.election.candidates])
|
||||||
self.exhausted = CountCard()
|
self.exhausted = CountCard()
|
||||||
@ -33,6 +47,8 @@ class STVCCounter:
|
|||||||
|
|
||||||
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.num_elected = 0
|
||||||
|
|
||||||
# Withdraw candidates
|
# Withdraw candidates
|
||||||
for candidate in self.election.withdrawn:
|
for candidate in self.election.withdrawn:
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
@ -52,6 +68,7 @@ class STVCCounter:
|
|||||||
self.exhausted.ballots.append((ballot, ballot.value))
|
self.exhausted.ballots.append((ballot, ballot.value))
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
|
self.quota = None
|
||||||
self.compute_quota()
|
self.compute_quota()
|
||||||
self.elect_meeting_quota()
|
self.elect_meeting_quota()
|
||||||
|
|
||||||
@ -74,15 +91,19 @@ class STVCCounter:
|
|||||||
self.loss_fraction.step()
|
self.loss_fraction.step()
|
||||||
|
|
||||||
# Have sufficient candidates been elected?
|
# 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()
|
return CountCompleted()
|
||||||
|
|
||||||
# Are there just enough candidates to fill all the seats
|
# 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
|
# Declare elected all remaining candidates
|
||||||
for candidate, count_card in self.candidates.items():
|
for candidate, count_card in self.candidates.items():
|
||||||
if count_card.state == CandidateState.HOPEFUL:
|
if count_card.state == CandidateState.HOPEFUL:
|
||||||
count_card.state = CandidateState.PROVISIONALLY_ELECTED
|
count_card.state = CandidateState.PROVISIONALLY_ELECTED
|
||||||
|
count_card.order_elected = self.num_elected
|
||||||
|
self.num_elected += 1
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
return CountStepResult(
|
return CountStepResult(
|
||||||
@ -101,8 +122,14 @@ class STVCCounter:
|
|||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
if len(has_surplus) > 0:
|
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)
|
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]
|
candidate_surplus, count_card = has_surplus[0]
|
||||||
count_card.state = CandidateState.ELECTED
|
count_card.state = CandidateState.ELECTED
|
||||||
|
|
||||||
@ -185,20 +212,43 @@ class STVCCounter:
|
|||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
def compute_quota(self):
|
def compute_quota(self):
|
||||||
# Compute quota
|
"""Recount total votes and (if applicable) recalculate the quota"""
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
self.total = sum((cc.votes for c, cc in self.candidates.items()), Num('0'))
|
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
|
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)
|
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')
|
__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):
|
def elect_meeting_quota(self):
|
||||||
# Does a candidate meet the quota?
|
# 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 self.meets_quota(cc)]
|
||||||
meets_quota = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL and cc.votes > self.quota]
|
|
||||||
__pragma__('noopov')
|
|
||||||
|
|
||||||
if len(meets_quota) > 0:
|
if len(meets_quota) > 0:
|
||||||
# Declare elected any candidate who meets the quota
|
# Declare elected any candidate who meets the quota
|
||||||
for candidate, count_card in meets_quota:
|
for candidate, count_card in meets_quota:
|
||||||
count_card.state = CandidateState.PROVISIONALLY_ELECTED
|
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.transfers = Num('0')
|
||||||
self.ballots = []
|
self.ballots = []
|
||||||
self.state = CandidateState.HOPEFUL
|
self.state = CandidateState.HOPEFUL
|
||||||
|
self.order_elected = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def votes(self):
|
def votes(self):
|
||||||
|
@ -48,3 +48,6 @@ class Fixed:
|
|||||||
return self.impl.lt(other.impl)
|
return self.impl.lt(other.impl)
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.impl.lte(other.impl)
|
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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
import math
|
||||||
|
|
||||||
_quantize_exp = 6
|
_quantize_exp = 6
|
||||||
|
|
||||||
@ -51,3 +52,6 @@ class Fixed:
|
|||||||
return self.impl < other.impl
|
return self.impl < other.impl
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.impl <= other.impl
|
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
|
return self.impl < other.impl
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.impl <= other.impl
|
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
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
class NativeInt:
|
class NativeInt:
|
||||||
"""
|
"""
|
||||||
Wrapper for Python int
|
Wrapper for Python int
|
||||||
@ -43,3 +45,6 @@ class NativeInt:
|
|||||||
return self.impl < other.impl
|
return self.impl < other.impl
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.impl <= other.impl
|
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
|
return self.impl < other.impl
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.impl <= other.impl
|
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
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
class Native:
|
class Native:
|
||||||
"""
|
"""
|
||||||
Wrapper for Python float (naive floating-point arithmetic)
|
Wrapper for Python float (naive floating-point arithmetic)
|
||||||
@ -43,3 +45,6 @@ class Native:
|
|||||||
return self.impl < other.impl
|
return self.impl < other.impl
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.impl <= other.impl
|
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)
|
return self.impl.lesser(other.impl)
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.impl.lesserOrEquals(other.impl)
|
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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
|
import math
|
||||||
|
|
||||||
class Rational:
|
class Rational:
|
||||||
"""
|
"""
|
||||||
@ -45,3 +46,6 @@ class Rational:
|
|||||||
return self.impl < other.impl
|
return self.impl < other.impl
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.impl <= other.impl
|
return self.impl <= other.impl
|
||||||
|
|
||||||
|
def __floor__(self):
|
||||||
|
return Rational(math.floor(self.impl))
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import pyRCV2.blt
|
import pyRCV2.blt
|
||||||
import pyRCV2.model
|
import pyRCV2.model
|
||||||
import pyRCV2.method, pyRCV2.method.STVCCounter
|
import pyRCV2.method, pyRCV2.method.base_stv
|
||||||
import pyRCV2.numbers
|
import pyRCV2.numbers
|
||||||
|
|
||||||
__pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};')
|
__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) {
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED) {
|
||||||
elTd.classList.add('elected');
|
elTd.classList.add('elected');
|
||||||
elTd.innerText = countCard.votes;
|
elTd.innerText = countCard.votes;
|
||||||
elTd.style.fontWeight = 'bold';
|
|
||||||
|
|
||||||
elTr1.querySelector('td:first-child').classList.add('elected');
|
elTr1.querySelector('td:first-child').classList.add('elected');
|
||||||
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) {
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) {
|
||||||
|
@ -24,7 +24,7 @@ onmessage = async function(evt) {
|
|||||||
}});
|
}});
|
||||||
|
|
||||||
// Create counter
|
// Create counter
|
||||||
let counter = py.pyRCV2.method.STVCCounter.STVCCounter(election);
|
let counter = py.pyRCV2.method.base_stv.BaseSTVCounter(election);
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
let result = counter.reset();
|
let result = counter.reset();
|
||||||
|
Reference in New Issue
Block a user