Implement ERS97
This commit is contained in:
parent
27c0638f98
commit
fb33a96d37
@ -48,6 +48,7 @@
|
||||
<option value="senate">Australian Senate STV</option>
|
||||
<option value="wright">Wright STV</option>
|
||||
<option value="prsa77">PRSA 1977</option>
|
||||
<option value="ers97">ERS97</option>
|
||||
</select>
|
||||
</label>
|
||||
<i class="sep"></i>
|
||||
@ -58,6 +59,20 @@
|
||||
</div>
|
||||
|
||||
<div id="divAdvancedOptions" style="display: none;">
|
||||
<label>
|
||||
Numbers:
|
||||
<select id="selNumbers">
|
||||
<option value="native">Native</option>
|
||||
<option value="rational">Rational</option>
|
||||
<option value="fixed" selected>Fixed</option>
|
||||
</select>
|
||||
</label>
|
||||
<i class="sep"></i>
|
||||
<label>
|
||||
Decimal places (if Numbers = Fixed):
|
||||
<input type="number" id="txtDP" value="5" style="width: 3em;">
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Quota:
|
||||
<select id="selQuotaCriterion">
|
||||
@ -73,33 +88,36 @@
|
||||
<option value="hare_exact">Hare (exact)</option>
|
||||
</select>
|
||||
</label>
|
||||
<i class="sep"></i>
|
||||
<label>
|
||||
<input type="checkbox" id="chkProgQuota">
|
||||
Progressive quota
|
||||
<select id="selQuotaMode">
|
||||
<option value="static" selected>Static quota</option>
|
||||
<option value="progressive">Progressive quota</option>
|
||||
<option value="ers97">Static with ERS97 rules</option>
|
||||
</select>
|
||||
</label>
|
||||
<i class="sep"></i>
|
||||
<label>
|
||||
<input type="checkbox" id="chkBulkElection" checked>
|
||||
Bulk election
|
||||
</label>
|
||||
<label style="display: none;">
|
||||
<input type="checkbox" id="chkBulkExclusion">
|
||||
Bulk exclusion (NYI)
|
||||
</label>
|
||||
<br>
|
||||
<i class="sep"></i>
|
||||
<label>
|
||||
Numbers:
|
||||
<select id="selNumbers">
|
||||
<option value="native">Native</option>
|
||||
<option value="rational">Rational</option>
|
||||
<option value="fixed" selected>Fixed</option>
|
||||
</select>
|
||||
<input type="checkbox" id="chkBulkExclusion">
|
||||
Bulk exclusion
|
||||
</label>
|
||||
<i class="sep"></i>
|
||||
<label>
|
||||
Decimal places (if Numbers = Fixed):
|
||||
<input type="number" id="txtDP" value="5" style="width: 3em;">
|
||||
<input type="checkbox" id="chkDeferSurpluses">
|
||||
Defer surpluses
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
<input type="checkbox" id="chkRoundQuota" checked>
|
||||
Round quota to
|
||||
</label>
|
||||
<label>
|
||||
<input type="number" id="txtRoundQuota" value="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
<i class="sep"></i>
|
||||
<label>
|
||||
@ -111,6 +129,15 @@
|
||||
d.p.
|
||||
</label>
|
||||
<i class="sep"></i>
|
||||
<label>
|
||||
<input type="checkbox" id="chkRoundTVs">
|
||||
Round transfer values to
|
||||
</label>
|
||||
<label>
|
||||
<input type="number" id="txtRoundTVs" value="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
<i class="sep"></i>
|
||||
<label>
|
||||
<input type="checkbox" id="chkRoundWeights">
|
||||
Round ballot weights to
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
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
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
@ -30,12 +30,16 @@ function changePreset() {
|
||||
if (document.getElementById('selPreset').value === 'scottish') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('chkProgQuota').checked = false;
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundTVs').checked = false;
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSurplus').value = 'size';
|
||||
document.getElementById('selTransfers').value = 'wig';
|
||||
@ -45,11 +49,14 @@ function changePreset() {
|
||||
} else if (document.getElementById('selPreset').value === 'stvc') {
|
||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('chkProgQuota').checked = true;
|
||||
document.getElementById('selQuotaMode').value = 'progressive';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('chkRoundQuota').checked = false;
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundTVs').checked = false;
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSurplus').value = 'size';
|
||||
document.getElementById('selTransfers').value = 'wig';
|
||||
@ -59,13 +66,17 @@ function changePreset() {
|
||||
} else if (document.getElementById('selPreset').value === 'senate') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('chkProgQuota').checked = false;
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '0';
|
||||
document.getElementById('chkRoundTVs').checked = false;
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSurplus').value = 'order';
|
||||
document.getElementById('selTransfers').value = 'uig';
|
||||
@ -75,12 +86,16 @@ function changePreset() {
|
||||
} else if (document.getElementById('selPreset').value === 'wright') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('chkProgQuota').checked = false;
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundTVs').checked = false;
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSurplus').value = 'size';
|
||||
document.getElementById('selTransfers').value = 'wig';
|
||||
@ -90,19 +105,46 @@ function changePreset() {
|
||||
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('chkProgQuota').checked = false;
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '0';
|
||||
document.getElementById('chkRoundTVs').checked = true;
|
||||
document.getElementById('txtRoundTVs').value = '0';
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSurplus').value = 'order';
|
||||
document.getElementById('selTransfers').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'parcels_by_order';
|
||||
document.getElementById('selTies').value = 'backwards_random';
|
||||
} else if (document.getElementById('selPreset').value === 'ers97') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'ers97';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '2';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '2';
|
||||
document.getElementById('chkRoundTVs').checked = true;
|
||||
document.getElementById('txtRoundTVs').value = '2';
|
||||
document.getElementById('chkRoundWeights').checked = true;
|
||||
document.getElementById('txtRoundTVs').value = '2';
|
||||
document.getElementById('selSurplus').value = 'size';
|
||||
document.getElementById('selTransfers').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'backwards_random';
|
||||
}
|
||||
}
|
||||
|
||||
@ -308,14 +350,17 @@ async function clickCount() {
|
||||
'options': {
|
||||
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
||||
'quota': document.getElementById('selQuota').value,
|
||||
'prog_quota': document.getElementById('chkProgQuota').checked,
|
||||
'quota_mode': document.getElementById('selQuotaMode').value,
|
||||
'bulk_elect': document.getElementById('chkBulkElection').checked,
|
||||
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
||||
'defer_surpluses': document.getElementById('chkDeferSurpluses').checked,
|
||||
'surplus_order': document.getElementById('selSurplus').value,
|
||||
'papers': document.getElementById('selPapers').value,
|
||||
'exclusion': document.getElementById('selExclusion').value,
|
||||
'ties': document.getElementById('selTies').value,
|
||||
'round_quota': document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
|
||||
'round_votes': document.getElementById('chkRoundVotes').checked ? parseInt(document.getElementById('txtRoundVotes').value) : null,
|
||||
'round_tvs': document.getElementById('chkRoundTVs').checked ? parseInt(document.getElementById('txtRoundTVs').value) : null,
|
||||
'round_weights': document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
|
||||
},
|
||||
'seed': document.getElementById('txtSeed').value,
|
||||
|
@ -29,11 +29,16 @@ def add_parser(subparsers):
|
||||
|
||||
parser.add_argument('--quota', '-q', choices=['droop', 'droop_exact', 'hare', 'hare_exact'], default='droop', help='quota calculation (default: droop)')
|
||||
parser.add_argument('--quota-criterion', '-c', choices=['geq', 'gt'], default='geq', help='quota criterion (default: geq)')
|
||||
parser.add_argument('--prog-quota', action='store_true', help='progressively reducing quota')
|
||||
parser.add_argument('--no-bulk-election', action='store_true', help='disable bulk election unless absolutely required')
|
||||
parser.add_argument('--quota-mode', choices=['static', 'progressive', 'ers97'], default='static', help='whether to apply a form of progressive quota (default: static)')
|
||||
parser.add_argument('--no-bulk-elect', action='store_true', help='disable bulk election unless absolutely required')
|
||||
parser.add_argument('--bulk-exclude', action='store_true', help='use bulk exclusion')
|
||||
parser.add_argument('--defer-surpluses', action='store_true', help='defer surplus transfers if possible')
|
||||
parser.add_argument('--numbers', '-n', choices=['fixed', 'rational', 'native'], default='fixed', help='numbers mode (default: fixed)')
|
||||
parser.add_argument('--decimals', type=int, default=5, help='decimal places if --numbers fixed (default: 5)')
|
||||
parser.add_argument('--no-round-quota', action='store_true', help='do not round the quota')
|
||||
parser.add_argument('--round-quota', type=int, help='round quota to specified decimal places')
|
||||
parser.add_argument('--round-votes', type=int, help='round votes to specified decimal places')
|
||||
parser.add_argument('--round-tvs', type=int, help='round transfer values to specified decimal places')
|
||||
parser.add_argument('--round-weights', type=int, help='round ballot weights to specified decimal places')
|
||||
parser.add_argument('--surplus-order', '-s', choices=['size', 'order'], default='size', help='whether to distribute surpluses by size or by order of election (default: size)')
|
||||
parser.add_argument('--method', '-m', choices=['wig', 'uig', 'eg'], default='wig', help='method of transferring surpluses (default: wig - weighted inclusive Gregory)')
|
||||
@ -72,6 +77,10 @@ def print_step(args, stage, result):
|
||||
print('Exhausted: {} ({})'.format(result.exhausted.votes.pp(2), result.exhausted.transfers.pp(2)))
|
||||
print('Loss to fraction: {} ({})'.format(result.loss_fraction.votes.pp(2), result.loss_fraction.transfers.pp(2)))
|
||||
print('Total votes: {}'.format(result.total.pp(2)))
|
||||
|
||||
if args.quota_mode == 'ers97' and result.vote_required_election < result.quota:
|
||||
print('Vote required for election: {}'.format(result.vote_required_election.pp(2)))
|
||||
else:
|
||||
print('Quota: {}'.format(result.quota.pp(2)))
|
||||
|
||||
print()
|
||||
@ -97,6 +106,9 @@ def main(args):
|
||||
else:
|
||||
counter = WIGSTVCounter(election, vars(args))
|
||||
|
||||
if args.no_round_quota:
|
||||
counter.options['round_quota'] = None
|
||||
|
||||
if args.ties is None:
|
||||
args.ties = ['prompt']
|
||||
|
||||
@ -108,11 +120,11 @@ def main(args):
|
||||
counter.options['ties'].append(TiesPrompt())
|
||||
elif t == 'random':
|
||||
if args.random_seed is None:
|
||||
print('A --random-seed is required to use random tie breaking')
|
||||
print('A --random-seed is required to use random tie breaking.')
|
||||
sys.exit(1)
|
||||
counter.options['ties'].append(TiesRandom(args.random_seed))
|
||||
|
||||
counter.options['bulk_elect'] = not args.no_bulk_election
|
||||
counter.options['bulk_elect'] = not args.no_bulk_elect
|
||||
counter.options['papers'] = 'transferable' if args.transferable_only else 'both'
|
||||
|
||||
# Reset
|
||||
|
@ -61,15 +61,19 @@ class BaseSTVCounter:
|
||||
|
||||
# Default options
|
||||
self.options = {
|
||||
'prog_quota': False, # Progressively reducing quota?
|
||||
'bulk_elect': True, # Bulk election?
|
||||
'bulk_exclude': False, # Bulk exclusion?
|
||||
'defer_surpluses': False, # Defer surpluses?
|
||||
'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
|
||||
'quota_criterion': 'geq', # 'geq' or 'gt'
|
||||
'quota_mode': 'static', # 'static', 'progressive' or 'ers97'
|
||||
'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_quota': None, # Number of decimal places or None
|
||||
'round_votes': None, # Number of decimal places or None
|
||||
'round_tvs': None, # Number of decimal places or None
|
||||
'round_weights': None, # Number of decimal places or None
|
||||
}
|
||||
|
||||
@ -97,9 +101,12 @@ class BaseSTVCounter:
|
||||
Does not reset the states of candidates, etc.
|
||||
"""
|
||||
|
||||
self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots
|
||||
|
||||
self.distribute_first_preferences()
|
||||
|
||||
self.quota = None
|
||||
self.vote_required_election = None # For ERS97
|
||||
self.compute_quota()
|
||||
self.elect_meeting_quota()
|
||||
|
||||
@ -110,7 +117,8 @@ class BaseSTVCounter:
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
@ -162,8 +170,8 @@ class BaseSTVCounter:
|
||||
return result
|
||||
|
||||
# Insufficient winners and no surpluses to distribute
|
||||
# Exclude the lowest ranked hopeful
|
||||
result = self.exclude_candidate()
|
||||
# Exclude the lowest ranked hopeful(s)
|
||||
result = self.exclude_candidates()
|
||||
if result:
|
||||
return result
|
||||
|
||||
@ -205,13 +213,43 @@ class BaseSTVCounter:
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
self.step_results.append(result)
|
||||
return result
|
||||
|
||||
def can_defer_surpluses(self, has_surplus):
|
||||
"""
|
||||
Determine if the specified surpluses can be deferred
|
||||
"""
|
||||
|
||||
# Do not defer if this could change the last 2 candidates
|
||||
__pragma__('opov')
|
||||
total_surpluses = sum((cc.votes - self.quota for c, cc in has_surplus), Num(0))
|
||||
__pragma__('noopov')
|
||||
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
|
||||
hopefuls.sort(key=lambda x: x[1].votes)
|
||||
__pragma__('opov')
|
||||
if total_surpluses > hopefuls[1][1].votes - hopefuls[0][1].votes:
|
||||
return False
|
||||
__pragma__('noopov')
|
||||
|
||||
# Do not defer if this could affect a bulk exclusion
|
||||
if self.options['bulk_exclude']:
|
||||
to_bulk_exclude = self.candidates_to_bulk_exclude(hopefuls)
|
||||
if len(to_bulk_exclude) > 0:
|
||||
total_excluded = sum((cc.votes for c, cc in to_bulk_exclude), Num(0))
|
||||
__pragma__('opov')
|
||||
if total_surpluses > hopefuls[len(to_bulk_exclude) + 1][1].votes - total_excluded:
|
||||
return False
|
||||
__pragma__('opov')
|
||||
|
||||
# Can defer surpluses
|
||||
return True
|
||||
|
||||
def distribute_surpluses(self):
|
||||
"""
|
||||
Distribute surpluses, if any
|
||||
@ -221,11 +259,14 @@ class BaseSTVCounter:
|
||||
if any(cc.state == CandidateState.EXCLUDING for c, cc in self.candidates.items()):
|
||||
return
|
||||
|
||||
candidate_surplus, count_card = None, None
|
||||
|
||||
# Are we distributing a surplus?
|
||||
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.DISTRIBUTING_SURPLUS]
|
||||
|
||||
if len(has_surplus) > 0:
|
||||
candidate_surplus, count_card = has_surplus[0]
|
||||
else:
|
||||
# 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')
|
||||
@ -234,13 +275,24 @@ class BaseSTVCounter:
|
||||
# Distribute surpluses in specified order
|
||||
if self.options['surplus_order'] == 'size':
|
||||
has_surplus.sort(key=lambda x: x[1].votes, reverse=True)
|
||||
candidate_surplus, count_card = self.choose_highest(has_surplus) # May need to break ties
|
||||
elif self.options['surplus_order'] == 'order':
|
||||
has_surplus.sort(key=lambda x: x[1].order_elected)
|
||||
candidate_surplus, count_card = has_surplus[0] # Ties were already broken when these were assigned
|
||||
else:
|
||||
raise STVException('Invalid surplus order option')
|
||||
|
||||
# Attempt to defer all remaining surpluses if possible
|
||||
if self.options['defer_surpluses']:
|
||||
if self.can_defer_surpluses(has_surplus):
|
||||
has_surplus = []
|
||||
|
||||
if len(has_surplus) > 0:
|
||||
# Cannot defer any surpluses
|
||||
if self.options['surplus_order'] == 'size':
|
||||
candidate_surplus, count_card = self.choose_highest(has_surplus) # May need to break ties
|
||||
elif self.options['surplus_order'] == 'order':
|
||||
candidate_surplus, count_card = has_surplus[0] # Ties were already broken when these were assigned
|
||||
|
||||
if candidate_surplus is not None:
|
||||
count_card.state = CandidateState.DISTRIBUTING_SURPLUS
|
||||
|
||||
__pragma__('opov')
|
||||
@ -262,7 +314,8 @@ class BaseSTVCounter:
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
@ -300,23 +353,26 @@ class BaseSTVCounter:
|
||||
self.exhausted,
|
||||
self.loss_fraction,
|
||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||
self.quota
|
||||
self.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
self.step_results.append(result)
|
||||
return result
|
||||
|
||||
def exclude_candidate(self):
|
||||
def exclude_candidates(self):
|
||||
"""
|
||||
Exclude the lowest ranked hopeful
|
||||
Exclude the lowest ranked hopeful(s)
|
||||
"""
|
||||
|
||||
candidate_excluded, count_card = self.candidate_to_exclude()
|
||||
candidates_excluded = self.candidates_to_exclude()
|
||||
for candidate, count_card in candidates_excluded:
|
||||
count_card.state = CandidateState.EXCLUDING
|
||||
|
||||
# Handle Wright STV
|
||||
if self.options['exclusion'] == 'wright':
|
||||
for candidate, count_card in candidates_excluded:
|
||||
count_card.state = CandidateState.EXCLUDED
|
||||
|
||||
# Reset the count
|
||||
@ -342,12 +398,12 @@ class BaseSTVCounter:
|
||||
step_results = self.step_results # Carry over step results
|
||||
result = self.reset()
|
||||
self.step_results = step_results
|
||||
result.comment = 'Exclusion of ' + candidate_excluded.name
|
||||
result.comment = 'Exclusion of ' + ', '.join([c.name for c, cc in candidates_excluded])
|
||||
|
||||
return result
|
||||
|
||||
# Exclude this candidate
|
||||
self.do_exclusion(candidate_excluded, count_card)
|
||||
self.do_exclusion(candidates_excluded)
|
||||
|
||||
# Declare any candidates meeting the quota as a result of exclusion
|
||||
self.compute_quota()
|
||||
@ -356,36 +412,87 @@ class BaseSTVCounter:
|
||||
|
||||
__pragma__('opov')
|
||||
result = CountStepResult(
|
||||
'Exclusion of ' + candidate_excluded.name,
|
||||
'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.quota,
|
||||
self.vote_required_election,
|
||||
)
|
||||
__pragma__('noopov')
|
||||
|
||||
self.step_results.append(result)
|
||||
return result
|
||||
|
||||
def candidate_to_exclude(self):
|
||||
def candidates_to_bulk_exclude(self, hopefuls):
|
||||
"""
|
||||
Determine the candidate to exclude
|
||||
Determine which candidates can be bulk excluded
|
||||
Returns List[Tuple[Candidate, CountCard]]
|
||||
"""
|
||||
|
||||
remaining_candidates = self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL)
|
||||
__pragma__('opov')
|
||||
total_surpluses = sum((cc.votes - self.quota for c, cc in self.candidates.items() if cc.votes > self.quota), Num(0))
|
||||
__pragma__('noopov')
|
||||
|
||||
# Attempt to exclude as many candidates as possible
|
||||
for i in range(0, len(hopefuls)):
|
||||
try_exclude = hopefuls[0:len(hopefuls)-i]
|
||||
|
||||
# Do not exclude if this splits tied candidates
|
||||
__pragma__('opov')
|
||||
if i != 0 and try_exclude[len(hopefuls)-i-1][1].votes == hopefuls[len(hopefuls)-i][1].votes:
|
||||
continue
|
||||
__pragma__('noopov')
|
||||
|
||||
# Do not exclude if this leaves insufficient candidates
|
||||
if remaining_candidates - len(try_exclude) < self.election.seats:
|
||||
continue
|
||||
|
||||
# Do not exclude if this could change the order of exclusion
|
||||
total_votes = sum((cc.votes for c, cc in try_exclude), Num(0))
|
||||
__pragma__('opov')
|
||||
if i != 0 and total_votes + total_surpluses > hopefuls[len(hopefuls)-i][1].votes:
|
||||
continue
|
||||
__pragma__('noopov')
|
||||
|
||||
# Can bulk exclude
|
||||
return try_exclude
|
||||
|
||||
return []
|
||||
|
||||
def candidates_to_exclude(self):
|
||||
"""
|
||||
Determine the candidate(s) to exclude
|
||||
Returns List[Tuple[Candidate, CountCard]]
|
||||
"""
|
||||
|
||||
# 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]
|
||||
if self._exclusion is not None:
|
||||
__pragma__('opov')
|
||||
return self._exclusion[0]
|
||||
__pragma__('noopov')
|
||||
|
||||
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
|
||||
hopefuls.sort(key=lambda x: x[1].votes)
|
||||
|
||||
candidate_excluded, count_card = self.choose_lowest(hopefuls)
|
||||
candidates_excluded = []
|
||||
|
||||
return candidate_excluded, count_card
|
||||
# Bulk exclusion
|
||||
if self.options['bulk_exclude']:
|
||||
if self.options['exclusion'] == 'parcels_by_order':
|
||||
# Ordering of parcels is not defined in this case
|
||||
raise STVException('Cannot use bulk_exclude with parcels_by_order')
|
||||
|
||||
def do_exclusion(self, candidate_excluded, count_card):
|
||||
candidates_excluded = self.candidates_to_bulk_exclude(hopefuls)
|
||||
|
||||
if len(candidates_excluded) == 0:
|
||||
candidates_excluded = [self.choose_lowest(hopefuls)]
|
||||
|
||||
return candidates_excluded
|
||||
|
||||
def do_exclusion(self, candidates_excluded):
|
||||
"""
|
||||
Exclude the given candidate and transfer the votes
|
||||
Subclasses must override this function
|
||||
@ -401,17 +508,31 @@ class BaseSTVCounter:
|
||||
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':
|
||||
if self.quota is None or self.options['quota_mode'] == 'progressive':
|
||||
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':
|
||||
self.quota = (self.total / Num(self.election.seats)).__floor__() + Num('1')
|
||||
elif self.options['quota'] == 'hare_exact':
|
||||
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')
|
||||
|
||||
if self.options['quota_mode'] == 'ers97':
|
||||
# Calculate the total active vote
|
||||
__pragma__('opov')
|
||||
total_active_vote = sum((cc.votes for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL), 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)
|
||||
__pragma__('noopov')
|
||||
|
||||
def meets_quota(self, count_card):
|
||||
@ -421,11 +542,11 @@ class BaseSTVCounter:
|
||||
|
||||
if self.options['quota_criterion'] == 'geq':
|
||||
__pragma__('opov')
|
||||
return count_card.votes >= self.quota
|
||||
return count_card.votes >= self.quota or (self.options['quota_mode'] == 'ers97' and count_card.votes >= self.vote_required_election)
|
||||
__pragma__('noopov')
|
||||
elif self.options['quota_criterion'] == 'gt':
|
||||
__pragma__('opov')
|
||||
return count_card.votes > self.quota
|
||||
return count_card.votes > self.quota or (self.options['quota_mode'] == 'ers97' and count_card.votes > self.vote_required_election)
|
||||
__pragma__('noopov')
|
||||
else:
|
||||
raise STVException('Invalid quota criterion')
|
||||
@ -452,13 +573,16 @@ class BaseSTVCounter:
|
||||
|
||||
meets_quota.remove(x)
|
||||
|
||||
if self.options['quota_mode'] == 'ers97':
|
||||
self.elect_meeting_quota() # Repeat as the vote required for election may have changed
|
||||
|
||||
# -----------------
|
||||
# UTILITY FUNCTIONS
|
||||
# -----------------
|
||||
|
||||
def next_preferences(self, parcels):
|
||||
"""
|
||||
Examine the specified parcels and group ballot papers by next available preference
|
||||
Examine the specified ballots 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()])
|
||||
@ -547,6 +671,11 @@ class BaseSTVCounter:
|
||||
return num
|
||||
return num.round(self.options['round_weights'], num.ROUND_DOWN)
|
||||
|
||||
def round_tv(self, num):
|
||||
if self.options['round_tvs'] is None:
|
||||
return num
|
||||
return num.round(self.options['round_tvs'], num.ROUND_DOWN)
|
||||
|
||||
class WIGSTVCounter(BaseSTVCounter):
|
||||
"""
|
||||
Basic weighted inclusive Gregory STV counter
|
||||
@ -569,28 +698,43 @@ class WIGSTVCounter(BaseSTVCounter):
|
||||
if len(cand_ballots) > 0:
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
self.candidates[candidate]._parcels_sorted = False
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
if self.options['round_tvs'] is None:
|
||||
self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / transferable_votes)
|
||||
else:
|
||||
tv = self.round_tv(surplus / transferable_votes)
|
||||
self.candidates[candidate].transfers += self.round_votes(num_votes * tv)
|
||||
else:
|
||||
self.candidates[candidate].transfers += self.round_votes(num_votes) # Do not allow weight to increase
|
||||
else:
|
||||
if self.options['round_tvs'] is None:
|
||||
self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / total_votes)
|
||||
else:
|
||||
tv = self.round_tv(surplus / total_votes)
|
||||
self.candidates[candidate].transfers += self.round_votes(num_votes * tv)
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
if self.options['round_tvs'] is None:
|
||||
new_value = (ballot_value * surplus) / transferable_votes
|
||||
else:
|
||||
tv = self.round_tv(surplus / transferable_votes)
|
||||
new_value = ballot_value * tv
|
||||
else:
|
||||
new_value = ballot_value
|
||||
else:
|
||||
if self.options['round_tvs'] is None:
|
||||
new_value = (ballot_value * surplus) / total_votes
|
||||
else:
|
||||
tv = self.round_tv(surplus / total_votes)
|
||||
new_value = ballot_value * tv
|
||||
|
||||
new_parcel.append((ballot, self.round_weight(new_value)))
|
||||
__pragma__('noopov')
|
||||
@ -611,48 +755,49 @@ class WIGSTVCounter(BaseSTVCounter):
|
||||
|
||||
count_card.state = CandidateState.ELECTED
|
||||
|
||||
def do_exclusion(self, candidate_excluded, count_card):
|
||||
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 = []
|
||||
count_card.parcels.remove(parcel)
|
||||
def do_exclusion(self, candidates_excluded):
|
||||
# Optimisation: Pre-sort exclusion ballots if applicable
|
||||
# self._exclusion[1] -> list of ballots-per-stage, ballots-per-stage = List[Tuple[Candidate,List[Ballot+Value]]]
|
||||
if self._exclusion is None:
|
||||
if self.options['exclusion'] == 'one_round':
|
||||
self._exclusion = (candidates_excluded, [[(c, [b for p in cc.parcels for b in p]) for c, cc in candidates_excluded]])
|
||||
elif self.options['exclusion'] == 'parcels_by_order':
|
||||
c, cc = candidates_excluded[0]
|
||||
self._exclusion = (candidates_excluded, [[(c, p)] for p in cc.parcels])
|
||||
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]
|
||||
ballots = [(c, b, bv) for c, cc in candidates_excluded for p in cc.parcels for b, bv in p]
|
||||
|
||||
# Sort ballots by value
|
||||
__pragma__('opov')
|
||||
ballots.sort(key=lambda x: x[1] / x[0].value, reverse=True)
|
||||
ballots.sort(key=lambda x: x[2] / x[1].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
|
||||
if self.options['round_tvs']:
|
||||
ballots_by_value = groupby(ballots, lambda x: self.round_tv(x[2] / x[1].value))
|
||||
else:
|
||||
ballots_by_value = groupby(ballots, lambda x: (x[2] / x[1].value).round(8, x[2].ROUND_DOWN))
|
||||
__pragma__('noopov')
|
||||
|
||||
if len(count_card.parcels) > 0:
|
||||
parcel = count_card.parcels[0]
|
||||
count_card.parcels.remove(parcel)
|
||||
# TODO: Can we combine ballots for each candidate within each stage?
|
||||
self._exclusion = (candidates_excluded, [[(c, [(b, bv)]) for c, b, bv in x] for x in ballots_by_value])
|
||||
else:
|
||||
parcel = []
|
||||
raise STVException('Invalid exclusion mode')
|
||||
|
||||
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([parcel])
|
||||
else: # one_round
|
||||
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels)
|
||||
count_card.parcels = []
|
||||
#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])
|
||||
for candidate, x in next_preferences.items():
|
||||
cand_ballots = x[0]
|
||||
num_ballots = x[1]
|
||||
num_votes = x[2]
|
||||
cand_ballots, num_ballots, num_votes = x[0], x[1], x[2]
|
||||
|
||||
new_parcel = []
|
||||
if len(cand_ballots) > 0:
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
self.candidates[candidate]._parcels_sorted = False
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
@ -664,19 +809,30 @@ class WIGSTVCounter(BaseSTVCounter):
|
||||
new_parcel.append((ballot, ballot_value))
|
||||
__pragma__('noopov')
|
||||
|
||||
# Subtract votes
|
||||
|
||||
__pragma__('opov')
|
||||
self.exhausted.transfers += self.round_votes(exhausted_votes)
|
||||
__pragma__('noopov')
|
||||
|
||||
for candidate, ballots in this_exclusion:
|
||||
total_votes = Num(0)
|
||||
for ballot, ballot_value in ballots:
|
||||
__pragma__('opov')
|
||||
count_card.transfers -= total_votes
|
||||
total_votes += ballot_value
|
||||
__pragma__('noopov')
|
||||
|
||||
if len(count_card.parcels) == 0:
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].transfers -= total_votes
|
||||
__pragma__('noopov')
|
||||
|
||||
if len(self._exclusion[1]) == 0:
|
||||
for candidate_excluded, count_card in candidates_excluded:
|
||||
__pragma__('opov')
|
||||
count_card.transfers -= count_card.votes
|
||||
__pragma__('noopov')
|
||||
count_card.state = CandidateState.EXCLUDED
|
||||
self._exclusion = None
|
||||
|
||||
class UIGSTVCounter(WIGSTVCounter):
|
||||
"""
|
||||
@ -704,28 +860,43 @@ class UIGSTVCounter(WIGSTVCounter):
|
||||
if len(cand_ballots) > 0:
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
self.candidates[candidate]._parcels_sorted = False
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
if self.options['round_tvs'] is None:
|
||||
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots)
|
||||
else:
|
||||
tv = self.round_tv(surplus / transferable_ballots)
|
||||
self.candidates[candidate].transfers += self.round_votes(num_ballots * tv)
|
||||
else:
|
||||
self.candidates[candidate].transfers += self.round_votes(num_votes)
|
||||
else:
|
||||
if self.options['round_tvs'] is None:
|
||||
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots)
|
||||
else:
|
||||
tv = self.round_tv(surplus / total_ballots)
|
||||
self.candidates[candidate].transfers += self.round_votes(num_ballots * tv)
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
if self.options['round_tvs'] is None:
|
||||
new_value = (ballot.value * surplus) / transferable_ballots
|
||||
else:
|
||||
tv = self.round_tv(surplus / transferable_ballots)
|
||||
new_value = ballot.value * tv
|
||||
else:
|
||||
new_value = ballot_value
|
||||
else:
|
||||
if self.options['round_tvs'] is None:
|
||||
new_value = (ballot.value * surplus) / total_ballots
|
||||
else:
|
||||
tv = self.round_tv(surplus / total_ballots)
|
||||
new_value = ballot.value * tv
|
||||
|
||||
new_parcel.append((ballot, self.round_weight(new_value)))
|
||||
__pragma__('noopov')
|
||||
@ -772,28 +943,43 @@ class EGSTVCounter(UIGSTVCounter):
|
||||
if len(cand_ballots) > 0:
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
self.candidates[candidate]._parcels_sorted = False
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
if self.options['round_tvs'] is None:
|
||||
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots)
|
||||
else:
|
||||
tv = self.round_tv(surplus / transferable_ballots)
|
||||
self.candidates[candidate].transfers += self.round_votes(num_ballots * tv)
|
||||
else:
|
||||
self.candidates[candidate].transfers += self.round_votes(num_votes)
|
||||
else:
|
||||
if self.options['round_tvs'] is None:
|
||||
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots)
|
||||
else:
|
||||
tv = self.round_tv(surplus / total_ballots)
|
||||
self.candidates[candidate].transfers += self.round_votes(num_ballots * tv)
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
if self.options['round_tvs'] is None:
|
||||
new_value = (ballot.value * surplus) / transferable_ballots
|
||||
else:
|
||||
tv = self.round_tv(surplus / transferable_ballots)
|
||||
new_value = ballot.value * tv
|
||||
else:
|
||||
new_value = ballot_value
|
||||
else:
|
||||
if self.options['round_tvs'] is None:
|
||||
new_value = (ballot.value * surplus) / total_ballots
|
||||
else:
|
||||
tv = self.round_tv(surplus / total_ballots)
|
||||
new_value = ballot.value * tv
|
||||
|
||||
new_parcel.append((ballot, self.round_weight(new_value)))
|
||||
__pragma__('noopov')
|
||||
|
@ -1,5 +1,5 @@
|
||||
# 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
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
@ -98,7 +98,7 @@ class CountCompleted:
|
||||
pass
|
||||
|
||||
class CountStepResult:
|
||||
def __init__(self, comment, candidates, exhausted, loss_fraction, total, quota):
|
||||
def __init__(self, comment, candidates, exhausted, loss_fraction, total, quota, vote_required_election):
|
||||
self.comment = comment
|
||||
|
||||
self.candidates = candidates # SafeDict: Candidate -> CountCard
|
||||
@ -107,6 +107,7 @@ class CountStepResult:
|
||||
|
||||
self.total = total
|
||||
self.quota = quota
|
||||
self.vote_required_election = vote_required_election
|
||||
|
||||
def clone(self):
|
||||
"""Return a clone of this result as a record of this stage"""
|
||||
|
@ -1,5 +1,5 @@
|
||||
# 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
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
@ -22,12 +22,12 @@ __pragma__('noskip')
|
||||
|
||||
if is_py:
|
||||
__pragma__('skip')
|
||||
from pyRCV2.numbers.fixed_py import Fixed, set_dps
|
||||
from pyRCV2.numbers.fixed_py import Fixed, set_dps, get_dps
|
||||
from pyRCV2.numbers.native_py import Native
|
||||
from pyRCV2.numbers.rational_py import Rational
|
||||
__pragma__('noskip')
|
||||
else:
|
||||
from pyRCV2.numbers.fixed_js import Fixed, set_dps
|
||||
from pyRCV2.numbers.fixed_js import Fixed, set_dps, get_dps
|
||||
from pyRCV2.numbers.native_js import Native
|
||||
from pyRCV2.numbers.rational_js import Rational
|
||||
|
||||
|
@ -116,6 +116,9 @@ class BaseNum:
|
||||
def __le__(self, other):
|
||||
raise NotImplementedError('Method not implemented')
|
||||
|
||||
def __pow__(self, power):
|
||||
raise NotImplementedError('Method not implemented')
|
||||
|
||||
def round(self, dps, mode):
|
||||
"""
|
||||
Round to the specified number of decimal places, using the ROUND_* mode specified
|
||||
@ -214,6 +217,10 @@ class BasePyNum(BaseNum):
|
||||
"""Implements BaseNum.__le__"""
|
||||
return self.impl <= other.impl
|
||||
|
||||
def __pow__(self, power):
|
||||
"""Implements BaseNum.__pow__"""
|
||||
return self._from_impl(self.impl ** power)
|
||||
|
||||
@compatible_types
|
||||
def __iadd__(self, other):
|
||||
"""Overrides BaseNum.__iadd__"""
|
||||
|
@ -21,6 +21,9 @@ Big.DP = 6
|
||||
def set_dps(dps):
|
||||
Big.DP = dps
|
||||
|
||||
def get_dps():
|
||||
return Big.DP
|
||||
|
||||
class Fixed(BaseNum):
|
||||
"""
|
||||
Wrapper for big.js (fixed-point arithmetic)
|
||||
@ -73,6 +76,10 @@ class Fixed(BaseNum):
|
||||
"""Implements BaseNum.__le__"""
|
||||
return self.impl.lte(other.impl)
|
||||
|
||||
def __pow__(self, power):
|
||||
"""Implements BaseNum.__pow__"""
|
||||
return Fixed._from_impl(self.impl.pow(power))
|
||||
|
||||
def round(self, dps, mode):
|
||||
"""Implements BaseNum.round"""
|
||||
return Fixed(self.impl.round(dps, mode))
|
||||
return Fixed._from_impl(self.impl.round(dps, mode))
|
||||
|
@ -18,12 +18,17 @@ from pyRCV2.numbers.base import BasePyNum, compatible_types
|
||||
|
||||
import decimal
|
||||
|
||||
_quantize_exp = 6
|
||||
_dps = 6
|
||||
_quantize_exp = decimal.Decimal('10') ** -_dps
|
||||
|
||||
def set_dps(dps):
|
||||
global _quantize_exp
|
||||
global _dps, _quantize_exp
|
||||
_dps = dps
|
||||
_quantize_exp = decimal.Decimal('10') ** -dps
|
||||
|
||||
def get_dps():
|
||||
return _dps
|
||||
|
||||
class Fixed(BasePyNum):
|
||||
"""
|
||||
Wrapper for Python Decimal (for fixed-point arithmetic)
|
||||
@ -43,4 +48,4 @@ class Fixed(BasePyNum):
|
||||
|
||||
def round(self, dps, mode):
|
||||
"""Implements BaseNum.round"""
|
||||
return Fixed(self.impl.quantize(decimal.Decimal('10') ** -dps, mode))
|
||||
return Fixed._from_impl(self.impl.quantize(decimal.Decimal('10') ** -dps, mode))
|
||||
|
@ -74,17 +74,21 @@ class Native(BaseNum):
|
||||
|
||||
def __floor__(self):
|
||||
"""Overrides BaseNum.__floor__"""
|
||||
return Native(Math.floor(self.impl))
|
||||
return Native._from_impl(Math.floor(self.impl))
|
||||
|
||||
def __pow__(self, power):
|
||||
"""Implements BaseNum.__pow__"""
|
||||
return Native._from_impl(Math.pow(self.impl, power))
|
||||
|
||||
def round(self, dps, mode):
|
||||
"""Implements BaseNum.round"""
|
||||
if mode == Native.ROUND_DOWN:
|
||||
return Native(Math.floor(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
||||
return Native._from_impl(Math.floor(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
||||
elif mode == Native.ROUND_HALF_UP:
|
||||
return Native(Math.round(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
||||
return Native._from_impl(Math.round(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
||||
elif mode == Native.ROUND_HALF_EVEN:
|
||||
raise NotImplementedError('ROUND_HALF_EVEN is not implemented in JS Native context')
|
||||
elif mode == Native.ROUND_UP:
|
||||
return Native(Math.ceil(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
||||
return Native._from_impl(Math.ceil(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
||||
else:
|
||||
raise ValueError('Invalid rounding mode')
|
||||
|
@ -29,12 +29,12 @@ class Native(BasePyNum):
|
||||
"""Implements BaseNum.round"""
|
||||
factor = 10 ** dps
|
||||
if mode == Native.ROUND_DOWN:
|
||||
return Native(math.floor(self.impl * factor) / factor)
|
||||
return Native._from_impl(math.floor(self.impl * factor) / factor)
|
||||
elif mode == Native.ROUND_HALF_UP:
|
||||
raise NotImplementedError('ROUND_HALF_UP is not implemented in Python Native context')
|
||||
elif mode == Native.ROUND_HALF_EVEN:
|
||||
return Native(round(self.impl * factor) / factor)
|
||||
return Native._from_impl(round(self.impl * factor) / factor)
|
||||
elif mode == Native.ROUND_UP:
|
||||
return Native(math.ceil(self.impl * factor) / factor)
|
||||
return Native._from_impl(math.ceil(self.impl * factor) / factor)
|
||||
else:
|
||||
raise ValueError('Invalid rounding mode')
|
||||
|
@ -71,18 +71,22 @@ class Rational(BaseNum):
|
||||
|
||||
def __floor__(self):
|
||||
"""Overrides BaseNum.__floor__"""
|
||||
return Rational(self.impl.floor())
|
||||
return Rational._from_impl(self.impl.floor())
|
||||
|
||||
def __pow__(self, power):
|
||||
"""Implements BaseNum.__pow__"""
|
||||
return Rational._from_impl(self.impl.pow(power))
|
||||
|
||||
def round(self, dps, mode):
|
||||
"""Implements BaseNum.round"""
|
||||
factor = bigRat(10).pow(dps)
|
||||
if mode == Rational.ROUND_DOWN:
|
||||
return Rational(self.impl.multiply(factor).floor().divide(factor))
|
||||
return Rational._from_impl(self.impl.multiply(factor).floor().divide(factor))
|
||||
elif mode == Rational.ROUND_HALF_UP:
|
||||
return Rational(self.impl.multiply(factor).round().divide(factor))
|
||||
return Rational._from_impl(self.impl.multiply(factor).round().divide(factor))
|
||||
elif mode == Rational.ROUND_HALF_EVEN:
|
||||
raise NotImplementedError('ROUND_HALF_EVEN is not implemented in JS Native context')
|
||||
elif mode == Rational.ROUND_UP:
|
||||
return Rational(self.impl.multiply(factor).ceil().divide(factor))
|
||||
return Rational._from_impl(self.impl.multiply(factor).ceil().divide(factor))
|
||||
else:
|
||||
raise ValueError('Invalid rounding mode')
|
||||
|
@ -35,12 +35,12 @@ class Rational(BasePyNum):
|
||||
"""Implements BaseNum.round"""
|
||||
factor = Fraction(10) ** dps
|
||||
if mode == Rational.ROUND_DOWN:
|
||||
return Rational(math.floor(self.impl * factor) / factor)
|
||||
return Rational._from_impl(math.floor(self.impl * factor) / factor)
|
||||
elif mode == Rational.ROUND_HALF_UP:
|
||||
raise NotImplementedError('ROUND_HALF_UP is not implemented in Python Rational context')
|
||||
elif mode == Rational.ROUND_HALF_EVEN:
|
||||
return Rational(round(self.impl * factor) / factor)
|
||||
return Rational._from_impl(round(self.impl * factor) / factor)
|
||||
elif mode == Rational.ROUND_UP:
|
||||
return Rational(math.ceil(self.impl * factor) / factor)
|
||||
return Rational._from_impl(math.ceil(self.impl * factor) / factor)
|
||||
else:
|
||||
raise ValueError('Invalid rounding mode')
|
||||
|
51
tests/data/ers97.blt
Normal file
51
tests/data/ers97.blt
Normal file
@ -0,0 +1,51 @@
|
||||
11 6
|
||||
35 1 2
|
||||
3 1 3 9
|
||||
11 1 3 6
|
||||
5 1 3 7
|
||||
6 1 3 8
|
||||
20 1 4 0
|
||||
7 1 4 9
|
||||
4 1 4 11
|
||||
8 1 5
|
||||
4 1 8
|
||||
1 1 10 5 7
|
||||
1 1 10 9
|
||||
18 1 11
|
||||
11 1 0
|
||||
81 2
|
||||
23 3 1 2 0
|
||||
2 3 1 2 7
|
||||
1 3 1 2 8
|
||||
1 3 1 0
|
||||
3 4 1 3 7
|
||||
1 4 8
|
||||
11 4 5 6
|
||||
3 4 2 0
|
||||
5 4 2 11
|
||||
1 4 3 0
|
||||
105 5
|
||||
91 6
|
||||
64 7
|
||||
5 8
|
||||
42 8 9
|
||||
12 8 7
|
||||
55 9
|
||||
2 10 5
|
||||
5 10 8
|
||||
14 10 9
|
||||
2 10 1 11
|
||||
90 11
|
||||
0
|
||||
"Smith"
|
||||
"Carpenter"
|
||||
"Wright"
|
||||
"Glazier"
|
||||
"Duke"
|
||||
"Prince"
|
||||
"Baron"
|
||||
"Abbot"
|
||||
"Vicar"
|
||||
"Monk"
|
||||
"Freeman"
|
||||
"ERS97 Model Election"
|
@ -1,5 +1,5 @@
|
||||
# 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
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
@ -47,6 +47,7 @@ def test_aec_tas19():
|
||||
counter = UIGSTVCounter(election, {
|
||||
'surplus_order': 'order',
|
||||
'exclusion': 'by_value',
|
||||
'round_quota': 0,
|
||||
'round_votes': 0,
|
||||
})
|
||||
result = counter.reset()
|
||||
|
@ -43,7 +43,10 @@ def test_csm15():
|
||||
|
||||
cands = {c.name: c for c in election.candidates}
|
||||
|
||||
counter = WIGSTVCounter(election, {'exclusion': 'wright'})
|
||||
counter = WIGSTVCounter(election, {
|
||||
'exclusion': 'wright',
|
||||
'round_quota': 0,
|
||||
})
|
||||
|
||||
# Round 1
|
||||
result = counter.reset()
|
||||
|
@ -1,5 +1,5 @@
|
||||
# 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
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
@ -45,7 +45,8 @@ def test_prsa1():
|
||||
counter = EGSTVCounter(election, {
|
||||
'surplus_order': 'order',
|
||||
'papers': 'transferable',
|
||||
'exclusion': 'parcels_by_order'
|
||||
'exclusion': 'parcels_by_order',
|
||||
'round_quota': 0,
|
||||
})
|
||||
|
||||
# Stage 1
|
||||
|
Reference in New Issue
Block a user