Expose advanced rounding options in count software
This commit is contained in:
parent
a3d79e993a
commit
e219969711
@ -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="">
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
Reference in New Issue
Block a user