Implement ERS97
This commit is contained in:
parent
27c0638f98
commit
fb33a96d37
@ -48,6 +48,7 @@
|
|||||||
<option value="senate">Australian Senate STV</option>
|
<option value="senate">Australian Senate STV</option>
|
||||||
<option value="wright">Wright STV</option>
|
<option value="wright">Wright STV</option>
|
||||||
<option value="prsa77">PRSA 1977</option>
|
<option value="prsa77">PRSA 1977</option>
|
||||||
|
<option value="ers97">ERS97</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<i class="sep"></i>
|
<i class="sep"></i>
|
||||||
@ -58,6 +59,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="divAdvancedOptions" style="display: none;">
|
<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>
|
<label>
|
||||||
Quota:
|
Quota:
|
||||||
<select id="selQuotaCriterion">
|
<select id="selQuotaCriterion">
|
||||||
@ -73,33 +88,36 @@
|
|||||||
<option value="hare_exact">Hare (exact)</option>
|
<option value="hare_exact">Hare (exact)</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<i class="sep"></i>
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="chkProgQuota">
|
<select id="selQuotaMode">
|
||||||
Progressive quota
|
<option value="static" selected>Static quota</option>
|
||||||
|
<option value="progressive">Progressive quota</option>
|
||||||
|
<option value="ers97">Static with ERS97 rules</option>
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<i class="sep"></i>
|
<i class="sep"></i>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="chkBulkElection" checked>
|
<input type="checkbox" id="chkBulkElection" checked>
|
||||||
Bulk election
|
Bulk election
|
||||||
</label>
|
</label>
|
||||||
<label style="display: none;">
|
<i class="sep"></i>
|
||||||
<input type="checkbox" id="chkBulkExclusion">
|
|
||||||
Bulk exclusion (NYI)
|
|
||||||
</label>
|
|
||||||
<br>
|
|
||||||
<label>
|
<label>
|
||||||
Numbers:
|
<input type="checkbox" id="chkBulkExclusion">
|
||||||
<select id="selNumbers">
|
Bulk exclusion
|
||||||
<option value="native">Native</option>
|
|
||||||
<option value="rational">Rational</option>
|
|
||||||
<option value="fixed" selected>Fixed</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
</label>
|
||||||
<i class="sep"></i>
|
<i class="sep"></i>
|
||||||
<label>
|
<label>
|
||||||
Decimal places (if Numbers = Fixed):
|
<input type="checkbox" id="chkDeferSurpluses">
|
||||||
<input type="number" id="txtDP" value="5" style="width: 3em;">
|
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>
|
</label>
|
||||||
<i class="sep"></i>
|
<i class="sep"></i>
|
||||||
<label>
|
<label>
|
||||||
@ -111,6 +129,15 @@
|
|||||||
d.p.
|
d.p.
|
||||||
</label>
|
</label>
|
||||||
<i class="sep"></i>
|
<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>
|
<label>
|
||||||
<input type="checkbox" id="chkRoundWeights">
|
<input type="checkbox" id="chkRoundWeights">
|
||||||
Round ballot weights to
|
Round ballot weights to
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
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,12 +30,16 @@ function changePreset() {
|
|||||||
if (document.getElementById('selPreset').value === 'scottish') {
|
if (document.getElementById('selPreset').value === 'scottish') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
document.getElementById('selQuota').value = 'droop';
|
document.getElementById('selQuota').value = 'droop';
|
||||||
document.getElementById('chkProgQuota').checked = false;
|
document.getElementById('selQuotaMode').value = 'static';
|
||||||
document.getElementById('chkBulkElection').checked = true;
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = false;
|
document.getElementById('chkBulkExclusion').checked = false;
|
||||||
|
document.getElementById('chkDeferSurpluses').checked = false;
|
||||||
document.getElementById('selNumbers').value = 'fixed';
|
document.getElementById('selNumbers').value = 'fixed';
|
||||||
document.getElementById('txtDP').value = '5';
|
document.getElementById('txtDP').value = '5';
|
||||||
|
document.getElementById('chkRoundQuota').checked = true;
|
||||||
|
document.getElementById('txtRoundQuota').value = '0';
|
||||||
document.getElementById('chkRoundVotes').checked = false;
|
document.getElementById('chkRoundVotes').checked = false;
|
||||||
|
document.getElementById('chkRoundTVs').checked = false;
|
||||||
document.getElementById('chkRoundWeights').checked = false;
|
document.getElementById('chkRoundWeights').checked = false;
|
||||||
document.getElementById('selSurplus').value = 'size';
|
document.getElementById('selSurplus').value = 'size';
|
||||||
document.getElementById('selTransfers').value = 'wig';
|
document.getElementById('selTransfers').value = 'wig';
|
||||||
@ -45,11 +49,14 @@ function changePreset() {
|
|||||||
} else if (document.getElementById('selPreset').value === 'stvc') {
|
} else if (document.getElementById('selPreset').value === 'stvc') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||||
document.getElementById('selQuota').value = 'droop_exact';
|
document.getElementById('selQuota').value = 'droop_exact';
|
||||||
document.getElementById('chkProgQuota').checked = true;
|
document.getElementById('selQuotaMode').value = 'progressive';
|
||||||
document.getElementById('chkBulkElection').checked = true;
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = true;
|
document.getElementById('chkBulkExclusion').checked = true;
|
||||||
|
document.getElementById('chkDeferSurpluses').checked = false;
|
||||||
document.getElementById('selNumbers').value = 'rational';
|
document.getElementById('selNumbers').value = 'rational';
|
||||||
|
document.getElementById('chkRoundQuota').checked = false;
|
||||||
document.getElementById('chkRoundVotes').checked = false;
|
document.getElementById('chkRoundVotes').checked = false;
|
||||||
|
document.getElementById('chkRoundTVs').checked = false;
|
||||||
document.getElementById('chkRoundWeights').checked = false;
|
document.getElementById('chkRoundWeights').checked = false;
|
||||||
document.getElementById('selSurplus').value = 'size';
|
document.getElementById('selSurplus').value = 'size';
|
||||||
document.getElementById('selTransfers').value = 'wig';
|
document.getElementById('selTransfers').value = 'wig';
|
||||||
@ -59,13 +66,17 @@ function changePreset() {
|
|||||||
} else if (document.getElementById('selPreset').value === 'senate') {
|
} else if (document.getElementById('selPreset').value === 'senate') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
document.getElementById('selQuota').value = 'droop';
|
document.getElementById('selQuota').value = 'droop';
|
||||||
document.getElementById('chkProgQuota').checked = false;
|
document.getElementById('selQuotaMode').value = 'static';
|
||||||
document.getElementById('chkBulkElection').checked = true;
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = true;
|
document.getElementById('chkBulkExclusion').checked = true;
|
||||||
|
document.getElementById('chkDeferSurpluses').checked = false;
|
||||||
document.getElementById('selNumbers').value = 'fixed';
|
document.getElementById('selNumbers').value = 'fixed';
|
||||||
document.getElementById('txtDP').value = '5';
|
document.getElementById('txtDP').value = '5';
|
||||||
|
document.getElementById('chkRoundQuota').checked = true;
|
||||||
|
document.getElementById('txtRoundQuota').value = '0';
|
||||||
document.getElementById('chkRoundVotes').checked = true;
|
document.getElementById('chkRoundVotes').checked = true;
|
||||||
document.getElementById('txtRoundVotes').value = '0';
|
document.getElementById('txtRoundVotes').value = '0';
|
||||||
|
document.getElementById('chkRoundTVs').checked = false;
|
||||||
document.getElementById('chkRoundWeights').checked = false;
|
document.getElementById('chkRoundWeights').checked = false;
|
||||||
document.getElementById('selSurplus').value = 'order';
|
document.getElementById('selSurplus').value = 'order';
|
||||||
document.getElementById('selTransfers').value = 'uig';
|
document.getElementById('selTransfers').value = 'uig';
|
||||||
@ -75,12 +86,16 @@ function changePreset() {
|
|||||||
} else if (document.getElementById('selPreset').value === 'wright') {
|
} else if (document.getElementById('selPreset').value === 'wright') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
document.getElementById('selQuota').value = 'droop';
|
document.getElementById('selQuota').value = 'droop';
|
||||||
document.getElementById('chkProgQuota').checked = false;
|
document.getElementById('selQuotaMode').value = 'static';
|
||||||
document.getElementById('chkBulkElection').checked = true;
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = true;
|
document.getElementById('chkBulkExclusion').checked = true;
|
||||||
|
document.getElementById('chkDeferSurpluses').checked = false;
|
||||||
document.getElementById('selNumbers').value = 'fixed';
|
document.getElementById('selNumbers').value = 'fixed';
|
||||||
document.getElementById('txtDP').value = '5';
|
document.getElementById('txtDP').value = '5';
|
||||||
|
document.getElementById('chkRoundQuota').checked = true;
|
||||||
|
document.getElementById('txtRoundQuota').value = '0';
|
||||||
document.getElementById('chkRoundVotes').checked = false;
|
document.getElementById('chkRoundVotes').checked = false;
|
||||||
|
document.getElementById('chkRoundTVs').checked = false;
|
||||||
document.getElementById('chkRoundWeights').checked = false;
|
document.getElementById('chkRoundWeights').checked = false;
|
||||||
document.getElementById('selSurplus').value = 'size';
|
document.getElementById('selSurplus').value = 'size';
|
||||||
document.getElementById('selTransfers').value = 'wig';
|
document.getElementById('selTransfers').value = 'wig';
|
||||||
@ -90,19 +105,46 @@ function changePreset() {
|
|||||||
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
document.getElementById('selQuota').value = 'droop';
|
document.getElementById('selQuota').value = 'droop';
|
||||||
document.getElementById('chkProgQuota').checked = false;
|
document.getElementById('selQuotaMode').value = 'static';
|
||||||
document.getElementById('chkBulkElection').checked = true;
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = false;
|
document.getElementById('chkBulkExclusion').checked = false;
|
||||||
|
document.getElementById('chkDeferSurpluses').checked = true;
|
||||||
document.getElementById('selNumbers').value = 'fixed';
|
document.getElementById('selNumbers').value = 'fixed';
|
||||||
document.getElementById('txtDP').value = '5';
|
document.getElementById('txtDP').value = '5';
|
||||||
|
document.getElementById('chkRoundQuota').checked = true;
|
||||||
|
document.getElementById('txtRoundQuota').value = '0';
|
||||||
document.getElementById('chkRoundVotes').checked = true;
|
document.getElementById('chkRoundVotes').checked = true;
|
||||||
document.getElementById('txtRoundVotes').value = '0';
|
document.getElementById('txtRoundVotes').value = '0';
|
||||||
|
document.getElementById('chkRoundTVs').checked = true;
|
||||||
|
document.getElementById('txtRoundTVs').value = '0';
|
||||||
document.getElementById('chkRoundWeights').checked = false;
|
document.getElementById('chkRoundWeights').checked = false;
|
||||||
document.getElementById('selSurplus').value = 'order';
|
document.getElementById('selSurplus').value = 'order';
|
||||||
document.getElementById('selTransfers').value = 'eg';
|
document.getElementById('selTransfers').value = 'eg';
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
document.getElementById('selExclusion').value = 'parcels_by_order';
|
document.getElementById('selExclusion').value = 'parcels_by_order';
|
||||||
document.getElementById('selTies').value = 'backwards_random';
|
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': {
|
'options': {
|
||||||
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
||||||
'quota': document.getElementById('selQuota').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_elect': document.getElementById('chkBulkElection').checked,
|
||||||
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
||||||
|
'defer_surpluses': document.getElementById('chkDeferSurpluses').checked,
|
||||||
'surplus_order': document.getElementById('selSurplus').value,
|
'surplus_order': document.getElementById('selSurplus').value,
|
||||||
'papers': document.getElementById('selPapers').value,
|
'papers': document.getElementById('selPapers').value,
|
||||||
'exclusion': document.getElementById('selExclusion').value,
|
'exclusion': document.getElementById('selExclusion').value,
|
||||||
'ties': document.getElementById('selTies').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_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,
|
'round_weights': document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
|
||||||
},
|
},
|
||||||
'seed': document.getElementById('txtSeed').value,
|
'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', '-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('--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('--quota-mode', choices=['static', 'progressive', 'ers97'], default='static', help='whether to apply a form of progressive quota (default: static)')
|
||||||
parser.add_argument('--no-bulk-election', action='store_true', help='disable bulk election unless absolutely required')
|
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('--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('--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-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('--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('--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)')
|
parser.add_argument('--method', '-m', choices=['wig', 'uig', 'eg'], default='wig', help='method of transferring surpluses (default: wig - weighted inclusive Gregory)')
|
||||||
@ -72,7 +77,11 @@ def print_step(args, stage, result):
|
|||||||
print('Exhausted: {} ({})'.format(result.exhausted.votes.pp(2), result.exhausted.transfers.pp(2)))
|
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('Loss to fraction: {} ({})'.format(result.loss_fraction.votes.pp(2), result.loss_fraction.transfers.pp(2)))
|
||||||
print('Total votes: {}'.format(result.total.pp(2)))
|
print('Total votes: {}'.format(result.total.pp(2)))
|
||||||
print('Quota: {}'.format(result.quota.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()
|
print()
|
||||||
|
|
||||||
@ -97,6 +106,9 @@ def main(args):
|
|||||||
else:
|
else:
|
||||||
counter = WIGSTVCounter(election, vars(args))
|
counter = WIGSTVCounter(election, vars(args))
|
||||||
|
|
||||||
|
if args.no_round_quota:
|
||||||
|
counter.options['round_quota'] = None
|
||||||
|
|
||||||
if args.ties is None:
|
if args.ties is None:
|
||||||
args.ties = ['prompt']
|
args.ties = ['prompt']
|
||||||
|
|
||||||
@ -108,11 +120,11 @@ def main(args):
|
|||||||
counter.options['ties'].append(TiesPrompt())
|
counter.options['ties'].append(TiesPrompt())
|
||||||
elif t == 'random':
|
elif t == 'random':
|
||||||
if args.random_seed is None:
|
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)
|
sys.exit(1)
|
||||||
counter.options['ties'].append(TiesRandom(args.random_seed))
|
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'
|
counter.options['papers'] = 'transferable' if args.transferable_only else 'both'
|
||||||
|
|
||||||
# Reset
|
# Reset
|
||||||
|
@ -61,15 +61,19 @@ class BaseSTVCounter:
|
|||||||
|
|
||||||
# Default options
|
# Default options
|
||||||
self.options = {
|
self.options = {
|
||||||
'prog_quota': False, # Progressively reducing quota?
|
|
||||||
'bulk_elect': True, # Bulk election?
|
'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': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
|
||||||
'quota_criterion': 'geq', # 'geq' or 'gt'
|
'quota_criterion': 'geq', # 'geq' or 'gt'
|
||||||
|
'quota_mode': 'static', # 'static', 'progressive' or 'ers97'
|
||||||
'surplus_order': 'size', # 'size' or 'order'
|
'surplus_order': 'size', # 'size' or 'order'
|
||||||
'papers': 'both', # 'both' or 'transferable'
|
'papers': 'both', # 'both' or 'transferable'
|
||||||
'exclusion': 'one_round', # 'one_round', 'parcels_by_order', 'by_value' or 'wright'
|
'exclusion': 'one_round', # 'one_round', 'parcels_by_order', 'by_value' or 'wright'
|
||||||
'ties': [], # List of tie strategies (e.g. TiesRandom)
|
'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_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
|
'round_weights': None, # Number of decimal places or None
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,9 +101,12 @@ class BaseSTVCounter:
|
|||||||
Does not reset the states of candidates, etc.
|
Does not reset the states of candidates, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots
|
||||||
|
|
||||||
self.distribute_first_preferences()
|
self.distribute_first_preferences()
|
||||||
|
|
||||||
self.quota = None
|
self.quota = None
|
||||||
|
self.vote_required_election = None # For ERS97
|
||||||
self.compute_quota()
|
self.compute_quota()
|
||||||
self.elect_meeting_quota()
|
self.elect_meeting_quota()
|
||||||
|
|
||||||
@ -110,7 +117,8 @@ class BaseSTVCounter:
|
|||||||
self.exhausted,
|
self.exhausted,
|
||||||
self.loss_fraction,
|
self.loss_fraction,
|
||||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||||
self.quota
|
self.quota,
|
||||||
|
self.vote_required_election,
|
||||||
)
|
)
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
@ -162,8 +170,8 @@ class BaseSTVCounter:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
# Insufficient winners and no surpluses to distribute
|
# Insufficient winners and no surpluses to distribute
|
||||||
# Exclude the lowest ranked hopeful
|
# Exclude the lowest ranked hopeful(s)
|
||||||
result = self.exclude_candidate()
|
result = self.exclude_candidates()
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -205,13 +213,43 @@ class BaseSTVCounter:
|
|||||||
self.exhausted,
|
self.exhausted,
|
||||||
self.loss_fraction,
|
self.loss_fraction,
|
||||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||||
self.quota
|
self.quota,
|
||||||
|
self.vote_required_election,
|
||||||
)
|
)
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
self.step_results.append(result)
|
self.step_results.append(result)
|
||||||
return 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):
|
def distribute_surpluses(self):
|
||||||
"""
|
"""
|
||||||
Distribute surpluses, if any
|
Distribute surpluses, if any
|
||||||
@ -221,26 +259,40 @@ class BaseSTVCounter:
|
|||||||
if any(cc.state == CandidateState.EXCLUDING for c, cc in self.candidates.items()):
|
if any(cc.state == CandidateState.EXCLUDING for c, cc in self.candidates.items()):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
candidate_surplus, count_card = None, None
|
||||||
|
|
||||||
# Are we distributing a surplus?
|
# Are we distributing a surplus?
|
||||||
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.DISTRIBUTING_SURPLUS]
|
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.DISTRIBUTING_SURPLUS]
|
||||||
|
if len(has_surplus) > 0:
|
||||||
# Do surpluses need to be distributed?
|
candidate_surplus, count_card = has_surplus[0]
|
||||||
if len(has_surplus) == 0:
|
else:
|
||||||
|
# Do surpluses need to be distributed?
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED and cc.votes > self.quota]
|
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED and cc.votes > self.quota]
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
if len(has_surplus) > 0:
|
|
||||||
# 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')
|
|
||||||
|
|
||||||
|
if len(has_surplus) > 0:
|
||||||
|
# Distribute surpluses in specified order
|
||||||
|
if self.options['surplus_order'] == 'size':
|
||||||
|
has_surplus.sort(key=lambda x: x[1].votes, reverse=True)
|
||||||
|
elif self.options['surplus_order'] == 'order':
|
||||||
|
has_surplus.sort(key=lambda x: x[1].order_elected)
|
||||||
|
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
|
count_card.state = CandidateState.DISTRIBUTING_SURPLUS
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
@ -262,7 +314,8 @@ class BaseSTVCounter:
|
|||||||
self.exhausted,
|
self.exhausted,
|
||||||
self.loss_fraction,
|
self.loss_fraction,
|
||||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||||
self.quota
|
self.quota,
|
||||||
|
self.vote_required_election,
|
||||||
)
|
)
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
@ -300,24 +353,27 @@ class BaseSTVCounter:
|
|||||||
self.exhausted,
|
self.exhausted,
|
||||||
self.loss_fraction,
|
self.loss_fraction,
|
||||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||||
self.quota
|
self.quota,
|
||||||
|
self.vote_required_election,
|
||||||
)
|
)
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
self.step_results.append(result)
|
self.step_results.append(result)
|
||||||
return 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()
|
||||||
count_card.state = CandidateState.EXCLUDING
|
for candidate, count_card in candidates_excluded:
|
||||||
|
count_card.state = CandidateState.EXCLUDING
|
||||||
|
|
||||||
# Handle Wright STV
|
# Handle Wright STV
|
||||||
if self.options['exclusion'] == 'wright':
|
if self.options['exclusion'] == 'wright':
|
||||||
count_card.state = CandidateState.EXCLUDED
|
for candidate, count_card in candidates_excluded:
|
||||||
|
count_card.state = CandidateState.EXCLUDED
|
||||||
|
|
||||||
# Reset the count
|
# Reset the count
|
||||||
# Carry over certain candidate states
|
# Carry over certain candidate states
|
||||||
@ -342,12 +398,12 @@ class BaseSTVCounter:
|
|||||||
step_results = self.step_results # Carry over step results
|
step_results = self.step_results # Carry over step results
|
||||||
result = self.reset()
|
result = self.reset()
|
||||||
self.step_results = step_results
|
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
|
return result
|
||||||
|
|
||||||
# Exclude this candidate
|
# 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
|
# Declare any candidates meeting the quota as a result of exclusion
|
||||||
self.compute_quota()
|
self.compute_quota()
|
||||||
@ -356,36 +412,87 @@ class BaseSTVCounter:
|
|||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
result = CountStepResult(
|
result = CountStepResult(
|
||||||
'Exclusion of ' + candidate_excluded.name,
|
'Exclusion of ' + ', '.join([c.name for c, cc in candidates_excluded]),
|
||||||
self.candidates,
|
self.candidates,
|
||||||
self.exhausted,
|
self.exhausted,
|
||||||
self.loss_fraction,
|
self.loss_fraction,
|
||||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||||
self.quota
|
self.quota,
|
||||||
|
self.vote_required_election,
|
||||||
)
|
)
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
self.step_results.append(result)
|
self.step_results.append(result)
|
||||||
return 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
|
# Continue current exclusion if applicable
|
||||||
excluding = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.EXCLUDING]
|
if self._exclusion is not None:
|
||||||
if len(excluding) > 0:
|
__pragma__('opov')
|
||||||
return excluding[0][0], excluding[0][1]
|
return self._exclusion[0]
|
||||||
|
__pragma__('noopov')
|
||||||
|
|
||||||
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
|
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
|
||||||
hopefuls.sort(key=lambda x: x[1].votes)
|
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')
|
||||||
|
|
||||||
|
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, candidate_excluded, count_card):
|
def do_exclusion(self, candidates_excluded):
|
||||||
"""
|
"""
|
||||||
Exclude the given candidate and transfer the votes
|
Exclude the given candidate and transfer the votes
|
||||||
Subclasses must override this function
|
Subclasses must override this function
|
||||||
@ -401,18 +508,32 @@ class BaseSTVCounter:
|
|||||||
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.quota is None or self.options['quota_mode'] == 'progressive':
|
||||||
if self.options['quota'] == 'droop':
|
if self.options['quota'] == 'droop' or self.options['quota'] == 'droop_exact':
|
||||||
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':
|
elif self.options['quota'] == 'hare' or self.options['quota'] == 'hare_exact':
|
||||||
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)
|
self.quota = self.total / Num(self.election.seats)
|
||||||
else:
|
else:
|
||||||
raise STVException('Invalid quota option')
|
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')
|
__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):
|
def meets_quota(self, count_card):
|
||||||
"""
|
"""
|
||||||
@ -421,11 +542,11 @@ class BaseSTVCounter:
|
|||||||
|
|
||||||
if self.options['quota_criterion'] == 'geq':
|
if self.options['quota_criterion'] == 'geq':
|
||||||
__pragma__('opov')
|
__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')
|
__pragma__('noopov')
|
||||||
elif self.options['quota_criterion'] == 'gt':
|
elif self.options['quota_criterion'] == 'gt':
|
||||||
__pragma__('opov')
|
__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')
|
__pragma__('noopov')
|
||||||
else:
|
else:
|
||||||
raise STVException('Invalid quota criterion')
|
raise STVException('Invalid quota criterion')
|
||||||
@ -451,6 +572,9 @@ class BaseSTVCounter:
|
|||||||
self.num_elected += 1
|
self.num_elected += 1
|
||||||
|
|
||||||
meets_quota.remove(x)
|
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
|
# UTILITY FUNCTIONS
|
||||||
@ -458,7 +582,7 @@ class BaseSTVCounter:
|
|||||||
|
|
||||||
def next_preferences(self, parcels):
|
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]
|
# SafeDict: Candidate -> [List[Ballot], ballots, votes]
|
||||||
next_preferences = SafeDict([(c, [[], Num('0'), Num('0')]) for c, cc in self.candidates.items()])
|
next_preferences = SafeDict([(c, [[], Num('0'), Num('0')]) for c, cc in self.candidates.items()])
|
||||||
@ -546,6 +670,11 @@ class BaseSTVCounter:
|
|||||||
if self.options['round_weights'] is None:
|
if self.options['round_weights'] is None:
|
||||||
return num
|
return num
|
||||||
return num.round(self.options['round_weights'], num.ROUND_DOWN)
|
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):
|
class WIGSTVCounter(BaseSTVCounter):
|
||||||
"""
|
"""
|
||||||
@ -569,28 +698,43 @@ class WIGSTVCounter(BaseSTVCounter):
|
|||||||
if len(cand_ballots) > 0:
|
if len(cand_ballots) > 0:
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
self.candidates[candidate].parcels.append(new_parcel)
|
self.candidates[candidate].parcels.append(new_parcel)
|
||||||
self.candidates[candidate]._parcels_sorted = False
|
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
if self.options['papers'] == 'transferable':
|
if self.options['papers'] == 'transferable':
|
||||||
if transferable_votes > surplus:
|
if transferable_votes > surplus:
|
||||||
self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / transferable_votes)
|
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:
|
else:
|
||||||
self.candidates[candidate].transfers += self.round_votes(num_votes) # Do not allow weight to increase
|
self.candidates[candidate].transfers += self.round_votes(num_votes) # Do not allow weight to increase
|
||||||
else:
|
else:
|
||||||
self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / total_votes)
|
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')
|
__pragma__('noopov')
|
||||||
|
|
||||||
for ballot, ballot_value in cand_ballots:
|
for ballot, ballot_value in cand_ballots:
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
if self.options['papers'] == 'transferable':
|
if self.options['papers'] == 'transferable':
|
||||||
if transferable_votes > surplus:
|
if transferable_votes > surplus:
|
||||||
new_value = (ballot_value * surplus) / transferable_votes
|
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:
|
else:
|
||||||
new_value = ballot_value
|
new_value = ballot_value
|
||||||
else:
|
else:
|
||||||
new_value = (ballot_value * surplus) / total_votes
|
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)))
|
new_parcel.append((ballot, self.round_weight(new_value)))
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
@ -611,48 +755,49 @@ class WIGSTVCounter(BaseSTVCounter):
|
|||||||
|
|
||||||
count_card.state = CandidateState.ELECTED
|
count_card.state = CandidateState.ELECTED
|
||||||
|
|
||||||
def do_exclusion(self, candidate_excluded, count_card):
|
def do_exclusion(self, candidates_excluded):
|
||||||
if self.options['exclusion'] == 'parcels_by_order':
|
# Optimisation: Pre-sort exclusion ballots if applicable
|
||||||
if len(count_card.parcels) > 0:
|
# self._exclusion[1] -> list of ballots-per-stage, ballots-per-stage = List[Tuple[Candidate,List[Ballot+Value]]]
|
||||||
parcel = count_card.parcels[0]
|
if self._exclusion is None:
|
||||||
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([parcel])
|
if self.options['exclusion'] == 'one_round':
|
||||||
else:
|
self._exclusion = (candidates_excluded, [[(c, [b for p in cc.parcels for b in p]) for c, cc in candidates_excluded]])
|
||||||
# TODO: Skip this entirely if this is the case
|
elif self.options['exclusion'] == 'parcels_by_order':
|
||||||
parcel = []
|
c, cc = candidates_excluded[0]
|
||||||
count_card.parcels.remove(parcel)
|
self._exclusion = (candidates_excluded, [[(c, p)] for p in cc.parcels])
|
||||||
elif self.options['exclusion'] == 'by_value':
|
elif self.options['exclusion'] == 'by_value':
|
||||||
# Sort the ballots by value
|
ballots = [(c, b, bv) for c, cc in candidates_excluded for p in cc.parcels for b, bv in p]
|
||||||
if not count_card._parcels_sorted:
|
|
||||||
ballots = [(b, bv) for p in count_card.parcels for b, bv in p]
|
# Sort ballots by value
|
||||||
__pragma__('opov')
|
__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
|
# Round to 8 decimal places to consider equality
|
||||||
# FIXME: Work out a better way of doing this
|
# 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))
|
if self.options['round_tvs']:
|
||||||
count_card._parcels_sorted = True
|
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')
|
__pragma__('noopov')
|
||||||
|
|
||||||
if len(count_card.parcels) > 0:
|
# TODO: Can we combine ballots for each candidate within each stage?
|
||||||
parcel = count_card.parcels[0]
|
self._exclusion = (candidates_excluded, [[(c, [(b, bv)]) for c, b, bv in x] for x in ballots_by_value])
|
||||||
count_card.parcels.remove(parcel)
|
|
||||||
else:
|
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():
|
for candidate, x in next_preferences.items():
|
||||||
cand_ballots = x[0]
|
cand_ballots, num_ballots, num_votes = x[0], x[1], x[2]
|
||||||
num_ballots = x[1]
|
|
||||||
num_votes = x[2]
|
|
||||||
|
|
||||||
new_parcel = []
|
new_parcel = []
|
||||||
if len(cand_ballots) > 0:
|
if len(cand_ballots) > 0:
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
self.candidates[candidate].parcels.append(new_parcel)
|
self.candidates[candidate].parcels.append(new_parcel)
|
||||||
self.candidates[candidate]._parcels_sorted = False
|
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
@ -664,19 +809,30 @@ class WIGSTVCounter(BaseSTVCounter):
|
|||||||
new_parcel.append((ballot, ballot_value))
|
new_parcel.append((ballot, ballot_value))
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
|
# Subtract votes
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
self.exhausted.transfers += self.round_votes(exhausted_votes)
|
self.exhausted.transfers += self.round_votes(exhausted_votes)
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
__pragma__('opov')
|
for candidate, ballots in this_exclusion:
|
||||||
count_card.transfers -= total_votes
|
total_votes = Num(0)
|
||||||
__pragma__('noopov')
|
for ballot, ballot_value in ballots:
|
||||||
|
__pragma__('opov')
|
||||||
if len(count_card.parcels) == 0:
|
total_votes += ballot_value
|
||||||
|
__pragma__('noopov')
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
count_card.transfers -= count_card.votes
|
self.candidates[candidate].transfers -= total_votes
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
count_card.state = CandidateState.EXCLUDED
|
|
||||||
|
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):
|
class UIGSTVCounter(WIGSTVCounter):
|
||||||
"""
|
"""
|
||||||
@ -704,28 +860,43 @@ class UIGSTVCounter(WIGSTVCounter):
|
|||||||
if len(cand_ballots) > 0:
|
if len(cand_ballots) > 0:
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
self.candidates[candidate].parcels.append(new_parcel)
|
self.candidates[candidate].parcels.append(new_parcel)
|
||||||
self.candidates[candidate]._parcels_sorted = False
|
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
if self.options['papers'] == 'transferable':
|
if self.options['papers'] == 'transferable':
|
||||||
if transferable_votes > surplus:
|
if transferable_votes > surplus:
|
||||||
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots)
|
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:
|
else:
|
||||||
self.candidates[candidate].transfers += self.round_votes(num_votes)
|
self.candidates[candidate].transfers += self.round_votes(num_votes)
|
||||||
else:
|
else:
|
||||||
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots)
|
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')
|
__pragma__('noopov')
|
||||||
|
|
||||||
for ballot, ballot_value in cand_ballots:
|
for ballot, ballot_value in cand_ballots:
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
if self.options['papers'] == 'transferable':
|
if self.options['papers'] == 'transferable':
|
||||||
if transferable_votes > surplus:
|
if transferable_votes > surplus:
|
||||||
new_value = (ballot.value * surplus) / transferable_ballots
|
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:
|
else:
|
||||||
new_value = ballot_value
|
new_value = ballot_value
|
||||||
else:
|
else:
|
||||||
new_value = (ballot.value * surplus) / total_ballots
|
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)))
|
new_parcel.append((ballot, self.round_weight(new_value)))
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
@ -772,28 +943,43 @@ class EGSTVCounter(UIGSTVCounter):
|
|||||||
if len(cand_ballots) > 0:
|
if len(cand_ballots) > 0:
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
self.candidates[candidate].parcels.append(new_parcel)
|
self.candidates[candidate].parcels.append(new_parcel)
|
||||||
self.candidates[candidate]._parcels_sorted = False
|
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
if self.options['papers'] == 'transferable':
|
if self.options['papers'] == 'transferable':
|
||||||
if transferable_votes > surplus:
|
if transferable_votes > surplus:
|
||||||
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots)
|
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:
|
else:
|
||||||
self.candidates[candidate].transfers += self.round_votes(num_votes)
|
self.candidates[candidate].transfers += self.round_votes(num_votes)
|
||||||
else:
|
else:
|
||||||
self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots)
|
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')
|
__pragma__('noopov')
|
||||||
|
|
||||||
for ballot, ballot_value in cand_ballots:
|
for ballot, ballot_value in cand_ballots:
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
if self.options['papers'] == 'transferable':
|
if self.options['papers'] == 'transferable':
|
||||||
if transferable_votes > surplus:
|
if transferable_votes > surplus:
|
||||||
new_value = (ballot.value * surplus) / transferable_ballots
|
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:
|
else:
|
||||||
new_value = ballot_value
|
new_value = ballot_value
|
||||||
else:
|
else:
|
||||||
new_value = (ballot.value * surplus) / total_ballots
|
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)))
|
new_parcel.append((ballot, self.round_weight(new_value)))
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
@ -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
|
||||||
@ -98,7 +98,7 @@ class CountCompleted:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class CountStepResult:
|
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.comment = comment
|
||||||
|
|
||||||
self.candidates = candidates # SafeDict: Candidate -> CountCard
|
self.candidates = candidates # SafeDict: Candidate -> CountCard
|
||||||
@ -107,6 +107,7 @@ class CountStepResult:
|
|||||||
|
|
||||||
self.total = total
|
self.total = total
|
||||||
self.quota = quota
|
self.quota = quota
|
||||||
|
self.vote_required_election = vote_required_election
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
"""Return a clone of this result as a record of this stage"""
|
"""Return a clone of this result as a record of this stage"""
|
||||||
|
@ -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
|
||||||
@ -22,12 +22,12 @@ __pragma__('noskip')
|
|||||||
|
|
||||||
if is_py:
|
if is_py:
|
||||||
__pragma__('skip')
|
__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.native_py import Native
|
||||||
from pyRCV2.numbers.rational_py import Rational
|
from pyRCV2.numbers.rational_py import Rational
|
||||||
__pragma__('noskip')
|
__pragma__('noskip')
|
||||||
else:
|
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.native_js import Native
|
||||||
from pyRCV2.numbers.rational_js import Rational
|
from pyRCV2.numbers.rational_js import Rational
|
||||||
|
|
||||||
|
@ -116,6 +116,9 @@ class BaseNum:
|
|||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
raise NotImplementedError('Method not implemented')
|
raise NotImplementedError('Method not implemented')
|
||||||
|
|
||||||
|
def __pow__(self, power):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
|
||||||
def round(self, dps, mode):
|
def round(self, dps, mode):
|
||||||
"""
|
"""
|
||||||
Round to the specified number of decimal places, using the ROUND_* mode specified
|
Round to the specified number of decimal places, using the ROUND_* mode specified
|
||||||
@ -214,6 +217,10 @@ class BasePyNum(BaseNum):
|
|||||||
"""Implements BaseNum.__le__"""
|
"""Implements BaseNum.__le__"""
|
||||||
return self.impl <= other.impl
|
return self.impl <= other.impl
|
||||||
|
|
||||||
|
def __pow__(self, power):
|
||||||
|
"""Implements BaseNum.__pow__"""
|
||||||
|
return self._from_impl(self.impl ** power)
|
||||||
|
|
||||||
@compatible_types
|
@compatible_types
|
||||||
def __iadd__(self, other):
|
def __iadd__(self, other):
|
||||||
"""Overrides BaseNum.__iadd__"""
|
"""Overrides BaseNum.__iadd__"""
|
||||||
|
@ -21,6 +21,9 @@ Big.DP = 6
|
|||||||
def set_dps(dps):
|
def set_dps(dps):
|
||||||
Big.DP = dps
|
Big.DP = dps
|
||||||
|
|
||||||
|
def get_dps():
|
||||||
|
return Big.DP
|
||||||
|
|
||||||
class Fixed(BaseNum):
|
class Fixed(BaseNum):
|
||||||
"""
|
"""
|
||||||
Wrapper for big.js (fixed-point arithmetic)
|
Wrapper for big.js (fixed-point arithmetic)
|
||||||
@ -73,6 +76,10 @@ class Fixed(BaseNum):
|
|||||||
"""Implements BaseNum.__le__"""
|
"""Implements BaseNum.__le__"""
|
||||||
return self.impl.lte(other.impl)
|
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):
|
def round(self, dps, mode):
|
||||||
"""Implements BaseNum.round"""
|
"""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
|
import decimal
|
||||||
|
|
||||||
_quantize_exp = 6
|
_dps = 6
|
||||||
|
_quantize_exp = decimal.Decimal('10') ** -_dps
|
||||||
|
|
||||||
def set_dps(dps):
|
def set_dps(dps):
|
||||||
global _quantize_exp
|
global _dps, _quantize_exp
|
||||||
|
_dps = dps
|
||||||
_quantize_exp = decimal.Decimal('10') ** -dps
|
_quantize_exp = decimal.Decimal('10') ** -dps
|
||||||
|
|
||||||
|
def get_dps():
|
||||||
|
return _dps
|
||||||
|
|
||||||
class Fixed(BasePyNum):
|
class Fixed(BasePyNum):
|
||||||
"""
|
"""
|
||||||
Wrapper for Python Decimal (for fixed-point arithmetic)
|
Wrapper for Python Decimal (for fixed-point arithmetic)
|
||||||
@ -43,4 +48,4 @@ class Fixed(BasePyNum):
|
|||||||
|
|
||||||
def round(self, dps, mode):
|
def round(self, dps, mode):
|
||||||
"""Implements BaseNum.round"""
|
"""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):
|
def __floor__(self):
|
||||||
"""Overrides BaseNum.__floor__"""
|
"""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):
|
def round(self, dps, mode):
|
||||||
"""Implements BaseNum.round"""
|
"""Implements BaseNum.round"""
|
||||||
if mode == Native.ROUND_DOWN:
|
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:
|
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:
|
elif mode == Native.ROUND_HALF_EVEN:
|
||||||
raise NotImplementedError('ROUND_HALF_EVEN is not implemented in JS Native context')
|
raise NotImplementedError('ROUND_HALF_EVEN is not implemented in JS Native context')
|
||||||
elif mode == Native.ROUND_UP:
|
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:
|
else:
|
||||||
raise ValueError('Invalid rounding mode')
|
raise ValueError('Invalid rounding mode')
|
||||||
|
@ -29,12 +29,12 @@ class Native(BasePyNum):
|
|||||||
"""Implements BaseNum.round"""
|
"""Implements BaseNum.round"""
|
||||||
factor = 10 ** dps
|
factor = 10 ** dps
|
||||||
if mode == Native.ROUND_DOWN:
|
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:
|
elif mode == Native.ROUND_HALF_UP:
|
||||||
raise NotImplementedError('ROUND_HALF_UP is not implemented in Python Native context')
|
raise NotImplementedError('ROUND_HALF_UP is not implemented in Python Native context')
|
||||||
elif mode == Native.ROUND_HALF_EVEN:
|
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:
|
elif mode == Native.ROUND_UP:
|
||||||
return Native(math.ceil(self.impl * factor) / factor)
|
return Native._from_impl(math.ceil(self.impl * factor) / factor)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Invalid rounding mode')
|
raise ValueError('Invalid rounding mode')
|
||||||
|
@ -71,18 +71,22 @@ class Rational(BaseNum):
|
|||||||
|
|
||||||
def __floor__(self):
|
def __floor__(self):
|
||||||
"""Overrides BaseNum.__floor__"""
|
"""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):
|
def round(self, dps, mode):
|
||||||
"""Implements BaseNum.round"""
|
"""Implements BaseNum.round"""
|
||||||
factor = bigRat(10).pow(dps)
|
factor = bigRat(10).pow(dps)
|
||||||
if mode == Rational.ROUND_DOWN:
|
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:
|
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:
|
elif mode == Rational.ROUND_HALF_EVEN:
|
||||||
raise NotImplementedError('ROUND_HALF_EVEN is not implemented in JS Native context')
|
raise NotImplementedError('ROUND_HALF_EVEN is not implemented in JS Native context')
|
||||||
elif mode == Rational.ROUND_UP:
|
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:
|
else:
|
||||||
raise ValueError('Invalid rounding mode')
|
raise ValueError('Invalid rounding mode')
|
||||||
|
@ -35,12 +35,12 @@ class Rational(BasePyNum):
|
|||||||
"""Implements BaseNum.round"""
|
"""Implements BaseNum.round"""
|
||||||
factor = Fraction(10) ** dps
|
factor = Fraction(10) ** dps
|
||||||
if mode == Rational.ROUND_DOWN:
|
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:
|
elif mode == Rational.ROUND_HALF_UP:
|
||||||
raise NotImplementedError('ROUND_HALF_UP is not implemented in Python Rational context')
|
raise NotImplementedError('ROUND_HALF_UP is not implemented in Python Rational context')
|
||||||
elif mode == Rational.ROUND_HALF_EVEN:
|
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:
|
elif mode == Rational.ROUND_UP:
|
||||||
return Rational(math.ceil(self.impl * factor) / factor)
|
return Rational._from_impl(math.ceil(self.impl * factor) / factor)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Invalid rounding mode')
|
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
|
# 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
|
||||||
@ -47,6 +47,7 @@ def test_aec_tas19():
|
|||||||
counter = UIGSTVCounter(election, {
|
counter = UIGSTVCounter(election, {
|
||||||
'surplus_order': 'order',
|
'surplus_order': 'order',
|
||||||
'exclusion': 'by_value',
|
'exclusion': 'by_value',
|
||||||
|
'round_quota': 0,
|
||||||
'round_votes': 0,
|
'round_votes': 0,
|
||||||
})
|
})
|
||||||
result = counter.reset()
|
result = counter.reset()
|
||||||
|
@ -43,7 +43,10 @@ def test_csm15():
|
|||||||
|
|
||||||
cands = {c.name: c for c in election.candidates}
|
cands = {c.name: c for c in election.candidates}
|
||||||
|
|
||||||
counter = WIGSTVCounter(election, {'exclusion': 'wright'})
|
counter = WIGSTVCounter(election, {
|
||||||
|
'exclusion': 'wright',
|
||||||
|
'round_quota': 0,
|
||||||
|
})
|
||||||
|
|
||||||
# Round 1
|
# Round 1
|
||||||
result = counter.reset()
|
result = counter.reset()
|
||||||
|
@ -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
|
||||||
@ -45,7 +45,8 @@ def test_prsa1():
|
|||||||
counter = EGSTVCounter(election, {
|
counter = EGSTVCounter(election, {
|
||||||
'surplus_order': 'order',
|
'surplus_order': 'order',
|
||||||
'papers': 'transferable',
|
'papers': 'transferable',
|
||||||
'exclusion': 'parcels_by_order'
|
'exclusion': 'parcels_by_order',
|
||||||
|
'round_quota': 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Stage 1
|
# Stage 1
|
||||||
|
Reference in New Issue
Block a user