diff --git a/docs/options.md b/docs/options.md index 345c68b..9553729 100644 --- a/docs/options.md +++ b/docs/options.md @@ -73,10 +73,12 @@ When deferred surpluses is enabled, the transfer of all surpluses is deferred if When rounding is enabled, the specified values are rounded to the specified number of decimal places. This enables, for example, votes to be counted only in integers, while ballot weights and transfer values are calculated to higher precision (according to the *Numbers*) option. -In relation to *Round transfer values to [n] d.p.* specifically: +The quota is incremented or rounded up (according to the *Quota* option), whereas votes, transfer values and weights are always rounded down. -* When this option is disabled (default), the numerator of the transfer value is multiplied by the applicable multiplicand before being divided by the denominator of the transfer value. For example, in the weighted inclusive Gregory method with non-transferable papers included, to determine the number of votes to transfer to a candidate, the surplus is multiplied by the number of votes for that candidate, before being divided by the total votes of the transferring candidate. This minimises rounding errors. -* When this option is enabled, the transfer value is calculated separately and rounded to the specified precision, before being multiplied by the applicable multiplicand. +In relation to *Round transfer values to [n] d.p.* – note that transfer values are used in STV in calculations of the form *A* × (*B*/*C*), where (*B*/*C*) is the transfer value. The order of operations depends on this setting: + +* When this option is disabled (default), (*A* × *B*) is calculated first, then divided by *C*. This minimises rounding errors. +* When this option is enabled, (*B*/*C*) is calculated separately first and rounded to the specified precision, before being multiplied by *A*. ## Surplus order (-s/--surplus-order) diff --git a/html/index.js b/html/index.js index 5b4f726..3b49f5f 100644 --- a/html/index.js +++ b/html/index.js @@ -112,12 +112,13 @@ function changePreset() { document.getElementById('selNumbers').value = 'fixed'; document.getElementById('txtDP').value = '5'; document.getElementById('chkRoundQuota').checked = true; - document.getElementById('txtRoundQuota').value = '0'; + document.getElementById('txtRoundQuota').value = '3'; document.getElementById('chkRoundVotes').checked = true; - document.getElementById('txtRoundVotes').value = '0'; + document.getElementById('txtRoundVotes').value = '3'; document.getElementById('chkRoundTVs').checked = true; - document.getElementById('txtRoundTVs').value = '0'; - document.getElementById('chkRoundWeights').checked = false; + document.getElementById('txtRoundTVs').value = '3'; + document.getElementById('chkRoundWeights').checked = true; + document.getElementById('txtRoundWeights').value = '3'; document.getElementById('selSurplus').value = 'order'; document.getElementById('selTransfers').value = 'eg'; document.getElementById('selPapers').value = 'transferable'; diff --git a/pyRCV2/cli/stv.py b/pyRCV2/cli/stv.py index 9c5f56b..ef8a7c9 100644 --- a/pyRCV2/cli/stv.py +++ b/pyRCV2/cli/stv.py @@ -48,6 +48,7 @@ def add_parser(subparsers): parser.add_argument('--random-seed', default=None, help='arbitrary string used to seed the RNG for random tie breaking') parser.add_argument('--hide-excluded', action='store_true', help='hide excluded candidates from results report') parser.add_argument('--sort-votes', action='store_true', help='sort candidates by votes in results report') + parser.add_argument('--pp-decimals', type=int, default=2, help='print votes to specified decimal places in results report (default: 2)') def print_step(args, stage, result): print('{}. {}'.format(stage, result.comment)) @@ -65,23 +66,23 @@ def print_step(args, stage, result): elif count_card.state == pyRCV2.model.CandidateState.WITHDRAWN: state = 'Withdrawn' - ppVotes = count_card.votes.pp(2) - ppTransfers = count_card.transfers.pp(2) + ppVotes = count_card.votes.pp(args.pp_decimals) + ppTransfers = count_card.transfers.pp(args.pp_decimals) if state is None: print('- {}: {} ({})'.format(candidate.name, ppVotes, ppTransfers)) else: - if not (args.hide_excluded and state == 'Excluded' and (ppVotes == '0.00' or ppVotes == '-0.00') and (ppTransfers == '0.00' or ppTransfers == '-0.00')): + if not (args.hide_excluded and state == 'Excluded' and float(ppVotes) == 0 and float(ppTransfers) == 0): print('- {}: {} ({}) - {}'.format(candidate.name, ppVotes, ppTransfers, state)) - 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))) + print('Exhausted: {} ({})'.format(result.exhausted.votes.pp(args.pp_decimals), result.exhausted.transfers.pp(args.pp_decimals))) + print('Loss to fraction: {} ({})'.format(result.loss_fraction.votes.pp(args.pp_decimals), result.loss_fraction.transfers.pp(args.pp_decimals))) + print('Total votes: {}'.format(result.total.pp(args.pp_decimals))) if args.quota_mode == 'ers97' and result.vote_required_election < result.quota: - print('Vote required for election: {}'.format(result.vote_required_election.pp(2))) + print('Vote required for election: {}'.format(result.vote_required_election.pp(args.pp_decimals))) else: - print('Quota: {}'.format(result.quota.pp(2))) + print('Quota: {}'.format(result.quota.pp(args.pp_decimals))) print() diff --git a/tests/data/prsa1.blt b/tests/data/prsa1.blt index eb36fc5..fbc54ae 100644 --- a/tests/data/prsa1.blt +++ b/tests/data/prsa1.blt @@ -1,69 +1,69 @@ 7 4 -1000 1 3 0 -1000 2 4 7 0 -1000 2 6 1 7 0 -1000 2 6 5 3 0 -1000 3 0 -1000 4 0 -1000 3 0 -1000 4 0 -1000 4 0 -1000 3 0 -1000 5 7 0 -1000 4 1 0 -1000 6 0 -1000 4 0 -1000 2 3 0 -1000 3 0 -1000 7 0 -1000 4 0 -1000 2 4 7 0 -1000 2 4 7 0 -1000 4 0 -1000 2 3 0 -1000 4 0 -1000 7 0 -1000 2 6 0 -1000 2 1 0 -1000 7 0 -1000 2 4 1 3 0 -1000 4 0 -1000 6 0 -1000 6 0 -1000 2 4 3 7 0 -1000 6 0 -1000 2 7 0 -1000 2 7 0 -1000 2 6 1 7 0 -1000 3 0 -1000 2 4 7 0 -1000 2 6 0 -1000 2 4 3 7 0 -1000 2 6 0 -1000 2 4 7 0 -1000 4 0 -1000 2 6 7 0 -1000 2 6 1 0 -1000 2 4 7 0 -1000 6 0 -1000 2 4 7 0 -1000 2 3 0 -1000 6 0 -1000 2 6 5 3 0 -1000 2 4 1 7 0 -1000 6 0 -1000 6 0 -1000 2 1 0 -1000 2 4 7 0 -1000 2 6 5 1 7 0 -1000 2 7 0 -1000 2 4 3 7 0 -1000 6 0 -1000 2 3 0 -1000 2 5 7 0 -1000 2 4 5 1 3 0 -1000 6 0 -1000 6 0 +1 1 3 0 +1 2 4 7 0 +1 2 6 1 7 0 +1 2 6 5 3 0 +1 3 0 +1 4 0 +1 3 0 +1 4 0 +1 4 0 +1 3 0 +1 5 7 0 +1 4 1 0 +1 6 0 +1 4 0 +1 2 3 0 +1 3 0 +1 7 0 +1 4 0 +1 2 4 7 0 +1 2 4 7 0 +1 4 0 +1 2 3 0 +1 4 0 +1 7 0 +1 2 6 0 +1 2 1 0 +1 7 0 +1 2 4 1 3 0 +1 4 0 +1 6 0 +1 6 0 +1 2 4 3 7 0 +1 6 0 +1 2 7 0 +1 2 7 0 +1 2 6 1 7 0 +1 3 0 +1 2 4 7 0 +1 2 6 0 +1 2 4 3 7 0 +1 2 6 0 +1 2 4 7 0 +1 4 0 +1 2 6 7 0 +1 2 6 1 0 +1 2 4 7 0 +1 6 0 +1 2 4 7 0 +1 2 3 0 +1 6 0 +1 2 6 5 3 0 +1 2 4 1 7 0 +1 6 0 +1 6 0 +1 2 1 0 +1 2 4 7 0 +1 2 6 5 1 7 0 +1 2 7 0 +1 2 4 3 7 0 +1 6 0 +1 2 3 0 +1 2 5 7 0 +1 2 4 5 1 3 0 +1 6 0 +1 6 0 0 "Evans" "Grey" diff --git a/tests/test_prsa.py b/tests/test_prsa.py index 8db421a..a2332ed 100644 --- a/tests/test_prsa.py +++ b/tests/test_prsa.py @@ -20,8 +20,8 @@ from pyRCV2.method.base_stv import EGSTVCounter from pyRCV2.model import CandidateState, CountCompleted def isclose(x, y): - # There will be some discrepancy due to how the PRSA count rounds transfer values, so accept up to 0.1 votes difference - return abs(int(x.impl) - y) < 100 + # Misnomer: Require exact match + return x == pyRCV2.numbers.Num(y) / pyRCV2.numbers.Num(1000) def test_prsa1(): """Compare count of prsa1.blt with model result at http://www.prsa.org.au/example1.pdf""" @@ -46,14 +46,17 @@ def test_prsa1(): 'surplus_order': 'order', 'papers': 'transferable', 'exclusion': 'parcels_by_order', - 'round_quota': 0, + 'round_quota': 3, + 'round_votes': 3, + 'round_tvs': 3, + 'round_weights': 3, }) # Stage 1 result = counter.reset() assert len(result.candidates.impl) == 7 - assert int(result.total.impl) == 65000 - assert int(result.quota.impl) == 13001 + assert result.total == pyRCV2.numbers.Num('65') + assert result.quota == pyRCV2.numbers.Num('13.001') assert result.comment == 'First preferences' assert isclose(result.candidates[c_evans].votes, 1000) assert isclose(result.candidates[c_grey].votes, 34000)