Expose advanced rounding options in count software

This commit is contained in:
RunasSudo 2021-01-03 01:23:11 +11:00
parent a3d79e993a
commit e219969711
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 145 additions and 87 deletions

View File

@ -29,12 +29,17 @@
tr.info td { tr.info td {
background-color: #edededff; background-color: #edededff;
} }
i.sep::after {
content: "•";
color: #ccc;
}
</style> </style>
</head> </head>
<body> <body>
<div> <div>
<input type="file" id="bltFile"> <input type="file" id="bltFile">
<button onclick="clickCount()">Count</button> <button onclick="clickCount()">Count</button>
<i class="sep"></i>
<label> <label>
Preset: Preset:
<select id="selPreset" onchange="changePreset()"> <select id="selPreset" onchange="changePreset()">
@ -66,10 +71,12 @@
<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"> <input type="checkbox" id="chkProgQuota">
Progressive quota Progressive quota
</label> </label>
<i class="sep"></i>
<label> <label>
<input type="checkbox" id="chkBulkElection" checked> <input type="checkbox" id="chkBulkElection" checked>
Bulk election Bulk election
@ -87,10 +94,29 @@
<option value="fixed" selected>Fixed</option> <option value="fixed" selected>Fixed</option>
</select> </select>
</label> </label>
<i class="sep"></i>
<label> <label>
Decimal places (if Numbers = Fixed): Decimal places (if Numbers = Fixed):
<input type="number" id="txtDP" value="5" style="width: 3em;"> <input type="number" id="txtDP" value="5" style="width: 3em;">
</label> </label>
<i class="sep"></i>
<label>
<input type="checkbox" id="chkRoundVotes">
Round votes to
</label>
<label>
<input type="number" id="txtRoundVotes" value="0" style="width: 3em;">
d.p.
</label>
<i class="sep"></i>
<label>
<input type="checkbox" id="chkRoundWeights">
Round ballot weights to
</label>
<label>
<input type="number" id="txtRoundWeights" value="0" style="width: 3em;">
d.p.
</label>
<br> <br>
<label> <label>
Surplus order: Surplus order:
@ -99,6 +125,7 @@
<option value="order">By order</option> <option value="order">By order</option>
</select> </select>
</label> </label>
<i class="sep"></i>
<label> <label>
Method: Method:
<select id="selTransfers"> <select id="selTransfers">
@ -131,6 +158,7 @@
<option value="prompt">Prompt</option> <option value="prompt">Prompt</option>
</select> </select>
</label> </label>
<i class="sep"></i>
<label> <label>
Random seed: Random seed:
<input type="text" id="txtSeed" value=""> <input type="text" id="txtSeed" value="">

View File

@ -35,6 +35,8 @@ function changePreset() {
document.getElementById('chkBulkExclusion').checked = false; document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5'; document.getElementById('txtDP').value = '5';
document.getElementById('chkRoundVotes').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';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -47,6 +49,8 @@ function changePreset() {
document.getElementById('chkBulkElection').checked = true; document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true; document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('selNumbers').value = 'rational'; document.getElementById('selNumbers').value = 'rational';
document.getElementById('chkRoundVotes').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';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -59,7 +63,10 @@ function changePreset() {
document.getElementById('chkBulkElection').checked = true; document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true; document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '0'; document.getElementById('txtDP').value = '5';
document.getElementById('chkRoundVotes').checked = true;
document.getElementById('txtRoundVotes').value = '0';
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';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -73,6 +80,8 @@ function changePreset() {
document.getElementById('chkBulkExclusion').checked = true; document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5'; document.getElementById('txtDP').value = '5';
document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSurplus').value = 'size'; document.getElementById('selSurplus').value = 'size';
document.getElementById('selTransfers').value = 'wright'; document.getElementById('selTransfers').value = 'wright';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -85,7 +94,10 @@ function changePreset() {
document.getElementById('chkBulkElection').checked = true; document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false; document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '0'; document.getElementById('txtDP').value = '5';
document.getElementById('chkRoundVotes').checked = true;
document.getElementById('txtRoundVotes').value = '0';
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';
@ -318,7 +330,9 @@ async function clickCount() {
'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_votes': document.getElementById('chkRoundVotes').checked ? parseInt(document.getElementById('txtRoundVotes').value) : null,
'round_weights': document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
}, },
'seed': document.getElementById('txtSeed').value, 'seed': document.getElementById('txtSeed').value,
'data': text 'data': text

View File

@ -17,7 +17,7 @@
__pragma__ = lambda x: None __pragma__ = lambda x: None
from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepResult from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepResult
from pyRCV2.numbers import Num, Rational from pyRCV2.numbers import Num
from pyRCV2.safedict import SafeDict from pyRCV2.safedict import SafeDict
import itertools import itertools
@ -33,8 +33,6 @@ class BaseSTVCounter:
def __init__(self, election, options=None): def __init__(self, election, options=None):
self.election = election self.election = election
self.cls_ballot_value = Num # Need to use Rational in unweighted inclusive Gregory
# Default options # Default options
self.options = { self.options = {
'prog_quota': False, # Progressively reducing quota? 'prog_quota': False, # Progressively reducing quota?
@ -44,7 +42,9 @@ class BaseSTVCounter:
'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' or 'by_value' 'exclusion': 'one_round', # 'one_round', 'parcels_by_order' or 'by_value'
'ties': [] 'ties': [], # List of tie strategies (e.g. TiesRandom)
'round_votes': None, # Number of decimal places or None
'round_weights': None, # Number of decimal places or None
} }
if options is not None: if options is not None:
@ -103,12 +103,12 @@ class BaseSTVCounter:
if candidate is not None: if candidate is not None:
self.candidates[candidate].transfers += ballot.value self.candidates[candidate].transfers += ballot.value
if len(self.candidates[candidate].parcels) == 0: if len(self.candidates[candidate].parcels) == 0:
self.candidates[candidate].parcels.append([(ballot, self.cls_ballot_value(ballot.value))]) self.candidates[candidate].parcels.append([(ballot, Num(ballot.value))])
else: else:
self.candidates[candidate].parcels[0].append((ballot, self.cls_ballot_value(ballot.value))) self.candidates[candidate].parcels[0].append((ballot, Num(ballot.value)))
else: else:
self.exhausted.transfers += ballot.value self.exhausted.transfers += ballot.value
#self.exhausted.parcels[0].append((ballot, self.cls_ballot_value(ballot.value))) #self.exhausted.parcels[0].append((ballot, Num(ballot.value)))
__pragma__('noopov') __pragma__('noopov')
def step(self): def step(self):
@ -250,39 +250,6 @@ class BaseSTVCounter:
""" """
raise NotImplementedError('Method not implemented') raise NotImplementedError('Method not implemented')
def next_preferences(self, parcels):
"""
Examine the specified parcels and group ballot papers by next available preference
"""
# SafeDict: Candidate -> [List[Ballot], ballots, votes]
next_preferences = SafeDict([(c, [[], Num('0'), Rational('0')]) for c, cc in self.candidates.items()])
total_ballots = Num('0')
total_votes = Rational('0')
next_exhausted = []
exhausted_ballots = Num('0')
exhausted_votes = Rational('0')
for parcel in parcels:
for ballot, ballot_value in parcel:
__pragma__('opov')
total_ballots += ballot.value
total_votes += ballot_value.to_rational()
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
next_preferences[candidate][0].append((ballot, ballot_value))
next_preferences[candidate][1] += ballot.value
next_preferences[candidate][2] += ballot_value.to_rational()
else:
next_exhausted.append((ballot, ballot_value))
exhausted_ballots += ballot.value
exhausted_votes += ballot_value.to_rational()
__pragma__('noopov')
return next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes
def before_exclusion(self): def before_exclusion(self):
""" """
Check before excluding a candidate Check before excluding a candidate
@ -428,6 +395,43 @@ class BaseSTVCounter:
meets_quota.remove(x) meets_quota.remove(x)
# -----------------
# UTILITY FUNCTIONS
# -----------------
def next_preferences(self, parcels):
"""
Examine the specified parcels 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()])
total_ballots = Num('0')
total_votes = Num('0')
next_exhausted = []
exhausted_ballots = Num('0')
exhausted_votes = Num('0')
for parcel in parcels:
for ballot, ballot_value in parcel:
__pragma__('opov')
total_ballots += ballot.value
total_votes += ballot_value
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None:
next_preferences[candidate][0].append((ballot, ballot_value))
next_preferences[candidate][1] += ballot.value
next_preferences[candidate][2] += ballot_value
else:
next_exhausted.append((ballot, ballot_value))
exhausted_ballots += ballot.value
exhausted_votes += ballot_value
__pragma__('noopov')
return next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes
def choose_lowest(self, l): def choose_lowest(self, l):
""" """
Provided a list of tuples (Candidate, CountCard), sorted in ASCENDING order of votes, choose the tuple with the fewest votes, breaking ties appropriately Provided a list of tuples (Candidate, CountCard), sorted in ASCENDING order of votes, choose the tuple with the fewest votes, breaking ties appropriately
@ -476,6 +480,16 @@ class BaseSTVCounter:
raise Exception('Unable to resolve tie') raise Exception('Unable to resolve tie')
def round_votes(self, num):
if self.options['round_votes'] is None:
return num
return num.round(self.options['round_votes'], num.ROUND_DOWN)
def round_weight(self, num):
if self.options['round_weights'] is None:
return num
return num.round(self.options['round_weights'], num.ROUND_DOWN)
class WIGSTVCounter(BaseSTVCounter): class WIGSTVCounter(BaseSTVCounter):
""" """
Basic weighted inclusive Gregory STV counter Basic weighted inclusive Gregory STV counter
@ -486,7 +500,7 @@ class WIGSTVCounter(BaseSTVCounter):
if self.options['papers'] == 'transferable': if self.options['papers'] == 'transferable':
__pragma__('opov') __pragma__('opov')
transferable_votes = total_votes.to_num() - exhausted_votes.to_num() transferable_votes = total_votes - exhausted_votes
__pragma__('noopov') __pragma__('noopov')
for candidate, x in next_preferences.items(): for candidate, x in next_preferences.items():
@ -503,11 +517,11 @@ class WIGSTVCounter(BaseSTVCounter):
__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 += (num_votes.to_num() * surplus) / transferable_votes self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / transferable_votes)
else: else:
self.candidates[candidate].transfers += num_votes.to_num() # 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 += (num_votes.to_num() * surplus) / total_votes.to_num() self.candidates[candidate].transfers += self.round_votes((num_votes * surplus) / total_votes)
__pragma__('noopov') __pragma__('noopov')
for ballot, ballot_value in cand_ballots: for ballot, ballot_value in cand_ballots:
@ -518,9 +532,9 @@ class WIGSTVCounter(BaseSTVCounter):
else: else:
new_value = ballot_value new_value = ballot_value
else: else:
new_value = (ballot_value * surplus) / total_votes.to_num() new_value = (ballot_value * surplus) / total_votes
new_parcel.append((ballot, new_value)) new_parcel.append((ballot, self.round_weight(new_value)))
__pragma__('noopov') __pragma__('noopov')
__pragma__('opov') __pragma__('opov')
@ -528,9 +542,9 @@ class WIGSTVCounter(BaseSTVCounter):
if transferable_votes > surplus: if transferable_votes > surplus:
pass # No ballots exhaust pass # No ballots exhaust
else: else:
self.exhausted.transfers += (surplus - transferable_votes) self.exhausted.transfers += self.round_votes((surplus - transferable_votes))
else: else:
self.exhausted.transfers += (exhausted_votes.to_num() * surplus) / total_votes.to_num() self.exhausted.transfers += self.round_votes((exhausted_votes * surplus) / total_votes)
__pragma__('noopov') __pragma__('noopov')
__pragma__('opov') __pragma__('opov')
@ -552,8 +566,10 @@ class WIGSTVCounter(BaseSTVCounter):
# Sort the ballots by value # Sort the ballots by value
ballots = [(b, bv) for p in count_card.parcels for b, bv in p] ballots = [(b, bv) for p in count_card.parcels for b, bv in p]
__pragma__('opov') __pragma__('opov')
ballots.sort(key=lambda x: x[1] / x[0].value.to_rational(), reverse=True) ballots.sort(key=lambda x: x[1] / x[0].value, reverse=True)
count_card.parcels = [list(g) for k, g in itertools.groupby(ballots, lambda x: x[1] / x[0].value.to_rational())] # Round to 8 decimal places to consider equality
# FIXME: Work out a better way of doing this
count_card.parcels = [list(g) for k, g in itertools.groupby(ballots, lambda x: (x[1] / x[0].value).round(8, x[1].ROUND_DOWN))]
__pragma__('noopov') __pragma__('noopov')
if len(count_card.parcels) > 0: if len(count_card.parcels) > 0:
@ -579,7 +595,7 @@ class WIGSTVCounter(BaseSTVCounter):
__pragma__('noopov') __pragma__('noopov')
__pragma__('opov') __pragma__('opov')
self.candidates[candidate].transfers += num_votes.to_num() self.candidates[candidate].transfers += self.round_votes(num_votes)
__pragma__('noopov') __pragma__('noopov')
for ballot, ballot_value in cand_ballots: for ballot, ballot_value in cand_ballots:
@ -588,11 +604,11 @@ class WIGSTVCounter(BaseSTVCounter):
__pragma__('noopov') __pragma__('noopov')
__pragma__('opov') __pragma__('opov')
self.exhausted.transfers += exhausted_votes.to_num() self.exhausted.transfers += self.round_votes(exhausted_votes)
__pragma__('noopov') __pragma__('noopov')
__pragma__('opov') __pragma__('opov')
count_card.transfers -= total_votes.to_num() count_card.transfers -= total_votes
__pragma__('noopov') __pragma__('noopov')
if len(count_card.parcels) == 0: if len(count_card.parcels) == 0:
@ -608,22 +624,20 @@ class UIGSTVCounter(WIGSTVCounter):
def __init__(self, *args): def __init__(self, *args):
WIGSTVCounter.__init__(self, *args) WIGSTVCounter.__init__(self, *args)
# Need to use Rational for ballot value internally, as Num may be set to integers only
self.cls_ballot_value = lambda x: x.to_rational()
def do_surplus(self, candidate_surplus, count_card, surplus): def do_surplus(self, candidate_surplus, count_card, surplus):
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels) next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels)
if self.options['papers'] == 'transferable': if self.options['papers'] == 'transferable':
__pragma__('opov') __pragma__('opov')
transferable_ballots = total_ballots - exhausted_ballots # Num transferable_ballots = total_ballots - exhausted_ballots
transferable_votes = total_votes - exhausted_votes # Rational transferable_votes = total_votes - exhausted_votes
__pragma__('noopov') __pragma__('noopov')
for candidate, x in next_preferences.items(): for candidate, x in next_preferences.items():
cand_ballots = x[0] cand_ballots = x[0]
num_ballots = x[1] # Num num_ballots = x[1]
num_votes = x[2] # Rational num_votes = x[2]
new_parcel = [] new_parcel = []
if len(cand_ballots) > 0: if len(cand_ballots) > 0:
@ -633,35 +647,35 @@ class UIGSTVCounter(WIGSTVCounter):
__pragma__('opov') __pragma__('opov')
if self.options['papers'] == 'transferable': if self.options['papers'] == 'transferable':
if transferable_votes.to_num() > surplus: if transferable_votes > surplus:
self.candidates[candidate].transfers += (num_ballots * surplus) / transferable_ballots self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots)
else: else:
self.candidates[candidate].transfers += num_votes.to_num() self.candidates[candidate].transfers += self.round_votes(num_votes)
else: else:
self.candidates[candidate].transfers += (num_ballots * surplus) / total_ballots self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots)
__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.to_num() > surplus: if transferable_votes > surplus:
new_value = (ballot.value * surplus).to_rational() / transferable_ballots.to_rational() new_value = (ballot.value * surplus) / transferable_ballots
else: else:
new_value = ballot_value new_value = ballot_value
else: else:
new_value = (ballot.value * surplus).to_rational() / total_ballots.to_rational() new_value = (ballot.value * surplus) / total_ballots
new_parcel.append((ballot, new_value)) new_parcel.append((ballot, self.round_weight(new_value)))
__pragma__('noopov') __pragma__('noopov')
__pragma__('opov') __pragma__('opov')
if self.options['papers'] == 'transferable': if self.options['papers'] == 'transferable':
if transferable_votes.to_num() > surplus: if transferable_votes > surplus:
pass # No ballots exhaust pass # No ballots exhaust
else: else:
self.exhausted.transfers += surplus - transferable_votes.to_num() self.exhausted.transfers += self.round_votes(surplus - transferable_votes)
else: else:
self.exhausted.transfers += (exhausted_ballots * surplus) / total_ballots self.exhausted.transfers += self.round_votes((exhausted_ballots * surplus) / total_ballots)
__pragma__('noopov') __pragma__('noopov')
__pragma__('opov') __pragma__('opov')
@ -684,7 +698,7 @@ class EGSTVCounter(UIGSTVCounter):
if self.options['papers'] == 'transferable': if self.options['papers'] == 'transferable':
__pragma__('opov') __pragma__('opov')
transferable_ballots = total_ballots - exhausted_ballots transferable_ballots = total_ballots - exhausted_ballots
transferable_votes = total_votes.to_num() - exhausted_votes.to_num() transferable_votes = total_votes - exhausted_votes
__pragma__('noopov') __pragma__('noopov')
for candidate, x in next_preferences.items(): for candidate, x in next_preferences.items():
@ -701,11 +715,11 @@ class EGSTVCounter(UIGSTVCounter):
__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 += (num_ballots * surplus) / transferable_ballots self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / transferable_ballots)
else: else:
self.candidates[candidate].transfers += num_votes.to_num() self.candidates[candidate].transfers += self.round_votes(num_votes)
else: else:
self.candidates[candidate].transfers += (num_ballots * surplus) / total_ballots self.candidates[candidate].transfers += self.round_votes((num_ballots * surplus) / total_ballots)
__pragma__('noopov') __pragma__('noopov')
for ballot, ballot_value in cand_ballots: for ballot, ballot_value in cand_ballots:
@ -718,7 +732,7 @@ class EGSTVCounter(UIGSTVCounter):
else: else:
new_value = (ballot.value * surplus) / total_ballots new_value = (ballot.value * surplus) / total_ballots
new_parcel.append((ballot, new_value)) new_parcel.append((ballot, self.round_weight(new_value)))
__pragma__('noopov') __pragma__('noopov')
__pragma__('opov') __pragma__('opov')
@ -726,9 +740,9 @@ class EGSTVCounter(UIGSTVCounter):
if transferable_votes > surplus: if transferable_votes > surplus:
pass # No ballots exhaust pass # No ballots exhaust
else: else:
self.exhausted.transfers += (surplus - transferable_votes) self.exhausted.transfers += self.round_votes((surplus - transferable_votes))
else: else:
self.exhausted.transfers += (exhausted_ballots * surplus) / total_ballots self.exhausted.transfers += self.round_votes((exhausted_ballots * surplus) / total_ballots)
__pragma__('noopov') __pragma__('noopov')
__pragma__('opov') __pragma__('opov')

View File

@ -14,6 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from pytest import approx
from pytest_steps import test_steps from pytest_steps import test_steps
import pyRCV2.blt import pyRCV2.blt
@ -36,7 +37,7 @@ candidates = [data[i][0] for i in range(2, len(data) - 2)]
def test_aec_tas19(): def test_aec_tas19():
"""Compare count of aec-senate-formalpreferences-24310-TAS.blt.gz with model result at https://results.aec.gov.au/24310/Website/External/SenateStateDop-24310-TAS.pdf""" """Compare count of aec-senate-formalpreferences-24310-TAS.blt.gz with model result at https://results.aec.gov.au/24310/Website/External/SenateStateDop-24310-TAS.pdf"""
pyRCV2.numbers.set_numclass(pyRCV2.numbers.NativeInt) pyRCV2.numbers.set_numclass(pyRCV2.numbers.Native)
with gzip.open('tests/data/aec-senate-formalpreferences-24310-TAS.blt.gz', 'rt') as f: with gzip.open('tests/data/aec-senate-formalpreferences-24310-TAS.blt.gz', 'rt') as f:
election = pyRCV2.blt.readBLT(f.read()) election = pyRCV2.blt.readBLT(f.read())
@ -45,7 +46,8 @@ def test_aec_tas19():
counter = UIGSTVCounter(election, { counter = UIGSTVCounter(election, {
'surplus_order': 'order', 'surplus_order': 'order',
'exclusion': 'by_value' 'exclusion': 'by_value',
'round_votes': 0,
}) })
result = counter.reset() result = counter.reset()
@ -56,21 +58,21 @@ def test_aec_tas19():
result = counter.step() result = counter.step()
comment = data[1][i] comment = data[1][i]
assert result.comment == comment, 'Failed to verify stage {} comment'.format(stage) assert result.comment == comment, 'Failed to verify comment'
for j, cand in enumerate(candidates): for j, cand in enumerate(candidates):
votes = pyRCV2.numbers.Num(data[j + 2][i]) votes = pyRCV2.numbers.Num(data[j + 2][i])
cc = next(cc for c, cc in result.candidates.items() if c.name == cand) cc = next(cc for c, cc in result.candidates.items() if c.name == cand)
assert cc.votes == votes, 'Failed to verify stage {} candidate "{}" votes, got {} expected {}'.format(stage, cand, cc.votes.pp(0), votes.pp(0)) assert cc.votes.impl == approx(votes.impl), 'Failed to verify candidate "{}" votes, got {} expected {}'.format(cand, cc.votes.pp(0), votes.pp(0))
state = data[j + 2][i + 1] if len(data[j + 2]) > (i + 1) else '' state = data[j + 2][i + 1] if len(data[j + 2]) > (i + 1) else ''
accept = {'': CandidateState.HOPEFUL, 'PEL': CandidateState.PROVISIONALLY_ELECTED, 'EL': CandidateState.ELECTED, 'EX': CandidateState.EXCLUDED, 'EXCLUDING': CandidateState.EXCLUDING} accept = {'': CandidateState.HOPEFUL, 'PEL': CandidateState.PROVISIONALLY_ELECTED, 'EL': CandidateState.ELECTED, 'EX': CandidateState.EXCLUDED, 'EXCLUDING': CandidateState.EXCLUDING}
assert cc.state == accept[state], 'Failed to verify stage {} candidate "{}" state'.format(stage, cand) assert cc.state == accept[state], 'Failed to verify candidate "{}" state'.format(cand)
exhausted = pyRCV2.numbers.Num(data[len(candidates) + 2][i]) exhausted = pyRCV2.numbers.Num(data[len(candidates) + 2][i])
assert result.exhausted.votes == exhausted, 'Failed to verify stage {} exhausted votes, got {} expected {}'.format(stage, result.exhausted.votes.pp(0), exhausted.pp(0)) assert result.exhausted.votes.impl == approx(exhausted.impl), 'Failed to verify exhausted votes, got {} expected {}'.format(result.exhausted.votes.pp(0), exhausted.pp(0))
loss_fraction = pyRCV2.numbers.Num(data[len(candidates) + 3][i]) loss_fraction = pyRCV2.numbers.Num(data[len(candidates) + 3][i])
assert result.loss_fraction.votes == loss_fraction, 'Failed to verify stage {} loss to fraction, got {} expected {}'.format(stage, result.loss_fraction.votes.pp(0), loss_fraction.pp(0)) assert result.loss_fraction.votes.impl == approx(loss_fraction.impl), 'Failed to verify loss to fraction, got {} expected {}'.format(result.loss_fraction.votes.pp(0), loss_fraction.pp(0))
yield 'Stage {}'.format(stage) yield 'Stage {}'.format(stage)