Separate out parcel handling logic
This commit is contained in:
parent
044b9fb3d3
commit
509768c071
@ -49,7 +49,7 @@ This dropdown allows you to select how numbers (vote totals, etc.) are represent
|
||||
* Native float: Numbers are represented as floating-point numbers. This is fast, but not recommended as unexpectedly large rounding errors may be introduced in some circumstances.
|
||||
* Native int: Numbers are represented as integers.
|
||||
|
||||
Note that, when unweighted inclusive Gregory surplus transfers are used (see *Method*), ballot weights are always represented internally as fractions, while candidate totals are represented according to this option.
|
||||
Note that, during some calculations, in order to reduce rounding errors, ballot weights and intermediate may be represented internally as fractions irrespective of this option.
|
||||
|
||||
## Surplus order (-s/--surplus-order)
|
||||
|
||||
@ -64,18 +64,26 @@ Some STV counting rules provide, for example, that ‘no surplus shall be transf
|
||||
|
||||
This dropdown allows you to select how ballots are transferred during surplus transfers or exclusions. The 2 recommended methods are:
|
||||
|
||||
* Weighted inclusive Gregory: During surplus transfers, a transfer value is calculated as the candidate's excess votes divided by the candidate's votes. Ballot weights are multiplied by the transfer value.
|
||||
* Weighted inclusive Gregory: During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are weighted according to the weights of the ballot papers.
|
||||
* Wright STV: Same as weighted inclusive Gregory, but when a candidate is excluded, the count is reset from the beginning (minus the excluded candidate).
|
||||
|
||||
Other methods are supported, but not recommended:
|
||||
|
||||
* Unweighted inclusive Gregory: During surplus transfers, a transfer value is calculated as the candidate's excess votes divided by the candidate's ballot papers. Ballot weights are set to the transfer value.
|
||||
* Exclusive Gregory (by parcel): During surplus transfers, only transferable papers in the ‘last bundle’ are examined. During exclusions, transfers are performed ‘parcel by parcel’, and each parcel forms a separate stage, i.e. if a parcel allows another candidate to be elected, no further transfers are made to that candidate.
|
||||
|
||||
Note that, when unweighted inclusive Gregory or exclusive Gregory surplus transfers are used, ballot weights are always represented internally as fractions, while candidate totals are represented according to the *Numbers* option.
|
||||
* Unweighted inclusive Gregory: During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are not weighted, and each ballot paper has equal value in the calculation.
|
||||
* Exclusive Gregory (last bundle): During surplus transfers, only the ballot papers received in the last transfer are examined. Transfers are not weighted.
|
||||
|
||||
Other surplus transfer methods, such as non-fractional transfers (e.g. random sample) are not supported at this time.
|
||||
|
||||
### Papers to examine in surplus transfer (--transferable-only)
|
||||
|
||||
* Include non-transferable papers (default): When this option is selected, all ballot papers of the transferring candidate are examined. Non-transferable papers are always exhausted at the relevant transfer values.
|
||||
* Use transferable papers only (CLI: --transferable-only): When this option is selected, only transferable papers of the transferring candidate are examined. Non-transferable papers are exhausted only if the value of the transferable papers is less than the surplus.
|
||||
|
||||
### Exclusion method (--exclusion)
|
||||
|
||||
* Exclude in one round (default): When excluding a candidate, transfer all their ballot papers in one round.
|
||||
* Exclude by parcel (by order): When excluding a candidate, transfer their ballot papers one parcel at a time, in their order each was received. Each parcel forms a separate round, i.e. if a transfer allows another candidate to meet the quota criterion, no further papers are transferred to that candidate.
|
||||
|
||||
## Ties (-t/--ties)
|
||||
|
||||
This dropdown allows you to select how ties (in surplus transfer or exclusion) are broken. The options are:
|
||||
|
@ -105,10 +105,23 @@
|
||||
<select id="selTransfers">
|
||||
<option value="wig" selected>Weighted inclusive Gregory</option>
|
||||
<option value="uig">Unweighted inclusive Gregory</option>
|
||||
<option value="parcelled_eg">Exclusive Gregory (by parcel)</option>
|
||||
<option value="eg">Exclusive Gregory (last bundle)</option>
|
||||
<option value="wright">Wright STV</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<select id="selPapers">
|
||||
<option value="both" selected>Include non-transferable papers</option>
|
||||
<option value="transferable">Use transferable papers only</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<select id="selExclusion">
|
||||
<option value="one_round" selected>Exclude in one round</option>
|
||||
<option value="parcels_by_order">Exclude by parcel (by order)</option>
|
||||
<!--<option value="parcels_by_value">Exclude by parcel (by value)</option>-->
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Ties:
|
||||
|
@ -37,6 +37,8 @@ function changePreset() {
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('selSurplus').value = 'size';
|
||||
document.getElementById('selTransfers').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'one_round';
|
||||
document.getElementById('selTies').value = 'backwards_random';
|
||||
} else if (document.getElementById('selPreset').value === 'stvc') {
|
||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||
@ -47,18 +49,23 @@ function changePreset() {
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('selSurplus').value = 'size';
|
||||
document.getElementById('selTransfers').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'one_round';
|
||||
document.getElementById('selTies').value = 'backwards_random';
|
||||
} /* else if (document.getElementById('selPreset').value === 'senate') {
|
||||
} else if (document.getElementById('selPreset').value === 'senate') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('chkProgQuota').checked = false;
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('selNumbers').value = 'int';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '0';
|
||||
document.getElementById('selSurplus').value = 'order';
|
||||
document.getElementById('selTransfers').value = 'uig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'parcels_by_value';
|
||||
document.getElementById('selTies').value = 'backwards_random';
|
||||
} */ else if (document.getElementById('selPreset').value === 'wright') {
|
||||
} else if (document.getElementById('selPreset').value === 'wright') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('chkProgQuota').checked = false;
|
||||
@ -68,6 +75,8 @@ function changePreset() {
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('selSurplus').value = 'size';
|
||||
document.getElementById('selTransfers').value = 'wright';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'one_round';
|
||||
document.getElementById('selTies').value = 'backwards_random';
|
||||
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
@ -78,7 +87,9 @@ function changePreset() {
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '0';
|
||||
document.getElementById('selSurplus').value = 'order';
|
||||
document.getElementById('selTransfers').value = 'parcelled_eg';
|
||||
document.getElementById('selTransfers').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'parcels_by_order';
|
||||
document.getElementById('selTies').value = 'backwards_random';
|
||||
}
|
||||
}
|
||||
@ -305,6 +316,8 @@ async function clickCount() {
|
||||
'bulk_elect': document.getElementById('chkBulkElection').checked,
|
||||
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
||||
'surplus_order': document.getElementById('selSurplus').value,
|
||||
'papers': document.getElementById('selPapers').value,
|
||||
'exclusion': document.getElementById('selExclusion').value,
|
||||
'ties': document.getElementById('selTies').value
|
||||
},
|
||||
'seed': document.getElementById('txtSeed').value,
|
||||
|
@ -47,8 +47,8 @@ onmessage = function(evt) {
|
||||
// Create counter
|
||||
if (evt.data.data.transfers === 'uig') {
|
||||
counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.data.options);
|
||||
} else if (evt.data.data.transfers === 'parcelled_eg') {
|
||||
counter = py.pyRCV2.method.parcels.ParcelledEGSTVCounter(election, evt.data.data.options);
|
||||
} else if (evt.data.data.transfers === 'eg') {
|
||||
counter = py.pyRCV2.method.base_stv.EGSTVCounter(election, evt.data.data.options);
|
||||
} else if (evt.data.data.transfers === 'wright') {
|
||||
counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.data.options);
|
||||
} else {
|
||||
|
@ -18,8 +18,7 @@ import pyRCV2.blt
|
||||
import pyRCV2.model
|
||||
import pyRCV2.numbers
|
||||
|
||||
from pyRCV2.method.base_stv import UIGSTVCounter, WIGSTVCounter
|
||||
from pyRCV2.method.parcels import ParcelledEGSTVCounter
|
||||
from pyRCV2.method.base_stv import UIGSTVCounter, WIGSTVCounter, EGSTVCounter
|
||||
from pyRCV2.method.wright import WrightSTVCounter
|
||||
from pyRCV2.ties import TiesBackwards, TiesPrompt, TiesRandom
|
||||
|
||||
@ -36,7 +35,9 @@ def add_parser(subparsers):
|
||||
parser.add_argument('--numbers', '-n', choices=['fixed', 'rational', 'int', '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('--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', 'parcelled_eg', 'wright'], default='wig', help='method of surpluses and exclusions (default: wig - weighted inclusive Gregory)')
|
||||
parser.add_argument('--method', '-m', choices=['wig', 'uig', 'eg', 'wright'], default='wig', help='method of surpluses and exclusions (default: wig - weighted inclusive Gregory)')
|
||||
parser.add_argument('--transferable-only', action='store_true', help='examine only transferable papers during surplus distributions')
|
||||
parser.add_argument('--exclusion', choices=['one_round', 'parcels_by_order', 'parcels_by_value'], default='one_round', help='whether to perform exclusions in one round or parcel by parcel (default: one_round)')
|
||||
parser.add_argument('--ties', '-t', action='append', choices=['backwards', 'prompt', 'random'], default=None, help='how to resolve ties (default: backwards then random)')
|
||||
parser.add_argument('--random-seed', default=None, help='arbitrary string used to seed the RNG for random tie breaking')
|
||||
|
||||
@ -45,9 +46,9 @@ def print_step(result):
|
||||
|
||||
for candidate, count_card in result.candidates.items():
|
||||
state = None
|
||||
if count_card.state == pyRCV2.model.CandidateState.ELECTED or count_card.state == pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED:
|
||||
if count_card.state == pyRCV2.model.CandidateState.ELECTED or count_card.state == pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED or count_card.state == pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS:
|
||||
state = 'ELECTED'
|
||||
elif count_card.state == pyRCV2.model.CandidateState.EXCLUDED:
|
||||
elif count_card.state == pyRCV2.model.CandidateState.EXCLUDED or count_card.state == pyRCV2.model.CandidateState.EXCLUDING:
|
||||
state = 'Excluded'
|
||||
elif count_card.state == pyRCV2.model.CandidateState.WITHDRAWN:
|
||||
state = 'Withdrawn'
|
||||
@ -82,8 +83,8 @@ def main(args):
|
||||
# Create counter
|
||||
if args.method == 'uig':
|
||||
counter = UIGSTVCounter(election, vars(args))
|
||||
elif args.method == 'parcelled_eg':
|
||||
counter = ParcelledEGSTVCounter(election, vars(args))
|
||||
elif args.method == 'eg':
|
||||
counter = EGSTVCounter(election, vars(args))
|
||||
elif args.method == 'wright':
|
||||
counter = WrightSTVCounter(election, vars(args))
|
||||
else:
|
||||
@ -105,6 +106,7 @@ def main(args):
|
||||
counter.options['ties'].append(TiesRandom(args.random_seed))
|
||||
|
||||
counter.options['bulk_elect'] = not args.no_bulk_election
|
||||
counter.options['papers'] = 'transferable' if args.transferable_only else 'both'
|
||||
|
||||
# Reset
|
||||
result = counter.reset()
|
||||
|
@ -40,6 +40,8 @@ class BaseSTVCounter:
|
||||
'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
|
||||
'quota_criterion': 'geq', # 'geq' or 'gt'
|
||||
'surplus_order': 'size', # 'size' or 'order'
|
||||
'papers': 'both', # 'both' or 'transferable'
|
||||
'exclusion': 'one_round', # 'one_round', 'parcels_by_order' or 'parcels_by_value'
|
||||
'ties': []
|
||||
}
|
||||
|
||||
@ -98,10 +100,13 @@ class BaseSTVCounter:
|
||||
|
||||
if candidate is not None:
|
||||
self.candidates[candidate].transfers += ballot.value
|
||||
self.candidates[candidate].ballots.append((ballot, self.cls_ballot_value(ballot.value)))
|
||||
if len(self.candidates[candidate].parcels) == 0:
|
||||
self.candidates[candidate].parcels.append([(ballot, self.cls_ballot_value(ballot.value))])
|
||||
else:
|
||||
self.candidates[candidate].parcels[0].append((ballot, self.cls_ballot_value(ballot.value)))
|
||||
else:
|
||||
self.exhausted.transfers += ballot.value
|
||||
self.exhausted.ballots.append((ballot, self.cls_ballot_value(ballot.value)))
|
||||
#self.exhausted.parcels[0].append((ballot, self.cls_ballot_value(ballot.value)))
|
||||
__pragma__('noopov')
|
||||
|
||||
def step(self):
|
||||
@ -211,14 +216,7 @@ class BaseSTVCounter:
|
||||
__pragma__('noopov')
|
||||
|
||||
# Transfer surplus
|
||||
is_complete = self.do_surplus(candidate_surplus, count_card, surplus)
|
||||
|
||||
if is_complete:
|
||||
__pragma__('opov')
|
||||
count_card.transfers -= surplus
|
||||
__pragma__('noopov')
|
||||
|
||||
count_card.state = CandidateState.ELECTED
|
||||
self.do_surplus(candidate_surplus, count_card, surplus)
|
||||
|
||||
# Declare elected any candidates meeting the quota as a result of surpluses
|
||||
self.compute_quota()
|
||||
@ -246,6 +244,39 @@ class BaseSTVCounter:
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Check before excluding a candidate
|
||||
@ -286,7 +317,7 @@ class BaseSTVCounter:
|
||||
count_card.state = CandidateState.EXCLUDING
|
||||
|
||||
# Exclude this candidate
|
||||
is_complete = self.do_exclusion(candidate_excluded, count_card)
|
||||
self.do_exclusion(candidate_excluded, count_card)
|
||||
|
||||
# Declare any candidates meeting the quota as a result of exclusion
|
||||
self.compute_quota()
|
||||
@ -445,133 +476,237 @@ class WIGSTVCounter(BaseSTVCounter):
|
||||
"""
|
||||
|
||||
def do_surplus(self, candidate_surplus, count_card, surplus):
|
||||
for ballot, ballot_value in count_card.ballots:
|
||||
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels)
|
||||
|
||||
if self.options['papers'] == 'transferable':
|
||||
__pragma__('opov')
|
||||
new_value = (ballot_value * surplus) / count_card.votes # Multiply first to avoid rounding errors
|
||||
|
||||
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
|
||||
|
||||
if candidate is not None:
|
||||
self.candidates[candidate].transfers += new_value
|
||||
self.candidates[candidate].ballots.append((ballot, new_value))
|
||||
else:
|
||||
self.exhausted.transfers += new_value
|
||||
self.exhausted.ballots.append((ballot, new_value))
|
||||
transferable_votes = total_votes.to_num() - exhausted_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
return True
|
||||
for candidate, x in next_preferences.items():
|
||||
cand_ballots = x[0]
|
||||
num_ballots = x[1]
|
||||
num_votes = x[2]
|
||||
|
||||
new_parcel = []
|
||||
if len(cand_ballots) > 0:
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
self.candidates[candidate].transfers += (num_votes.to_num() * surplus) / transferable_votes
|
||||
else:
|
||||
self.candidates[candidate].transfers += num_votes.to_num() # Do not allow weight to increase
|
||||
else:
|
||||
self.candidates[candidate].transfers += (num_votes.to_num() * surplus) / total_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
new_value = (ballot_value * surplus) / transferable_votes
|
||||
else:
|
||||
new_value = ballot_value
|
||||
else:
|
||||
new_value = (ballot_value * surplus) / total_votes.to_num()
|
||||
|
||||
new_parcel.append((ballot, new_value))
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
pass # No ballots exhaust
|
||||
else:
|
||||
self.exhausted.transfers += (surplus - transferable_votes)
|
||||
else:
|
||||
self.exhausted.transfers += (exhausted_votes.to_num() * surplus) / total_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
count_card.transfers -= surplus
|
||||
__pragma__('noopov')
|
||||
|
||||
count_card.state = CandidateState.ELECTED
|
||||
|
||||
def do_exclusion(self, candidate_excluded, count_card):
|
||||
for ballot, ballot_value in count_card.ballots:
|
||||
__pragma__('opov')
|
||||
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
|
||||
if self.options['exclusion'] == 'parcels_by_order':
|
||||
parcel = count_card.parcels[0]
|
||||
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([parcel])
|
||||
count_card.parcels.remove(parcel)
|
||||
elif self.options['exclusion'] == 'parcels_by_value':
|
||||
raise Exception('Not implemented')
|
||||
else: # one_round
|
||||
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels)
|
||||
count_card.parcels = []
|
||||
|
||||
if candidate is not None:
|
||||
self.candidates[candidate].transfers += ballot_value
|
||||
self.candidates[candidate].ballots.append((ballot, ballot_value))
|
||||
else:
|
||||
self.exhausted.transfers += ballot_value
|
||||
self.exhausted.ballots.append((ballot, ballot_value))
|
||||
for candidate, x in next_preferences.items():
|
||||
cand_ballots = x[0]
|
||||
num_ballots = x[1]
|
||||
num_votes = x[2]
|
||||
|
||||
new_parcel = []
|
||||
if len(cand_ballots) > 0:
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
count_card.transfers -= count_card.votes
|
||||
self.candidates[candidate].transfers += num_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
__pragma__('opov')
|
||||
new_parcel.append((ballot, ballot_value))
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
self.exhausted.transfers += exhausted_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
count_card.transfers -= total_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
if len(count_card.parcels) == 0:
|
||||
count_card.state = CandidateState.EXCLUDED
|
||||
|
||||
class UIGSTVCounter(BaseSTVCounter):
|
||||
class UIGSTVCounter(WIGSTVCounter):
|
||||
"""
|
||||
Basic unweighted inclusive Gregory STV counter
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
BaseSTVCounter.__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 = Rational
|
||||
self.cls_ballot_value = lambda x: x.to_rational()
|
||||
|
||||
def do_surplus(self, candidate_surplus, count_card, surplus):
|
||||
# FIXME: Is it okay to use native int's here?
|
||||
next_preferences = SafeDict([(c, []) for c, cc in self.candidates.items()])
|
||||
next_exhausted = []
|
||||
total_ballots = Num('0')
|
||||
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels)
|
||||
|
||||
# Count next preferences
|
||||
for ballot, ballot_value in count_card.ballots:
|
||||
if self.options['papers'] == 'transferable':
|
||||
__pragma__('opov')
|
||||
total_ballots += ballot.value
|
||||
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
|
||||
transferable_ballots = total_ballots - exhausted_ballots
|
||||
transferable_votes = total_votes.to_num() - exhausted_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
if candidate is not None:
|
||||
for candidate, x in next_preferences.items():
|
||||
cand_ballots = x[0]
|
||||
num_ballots = x[1]
|
||||
num_votes = x[2]
|
||||
|
||||
new_parcel = []
|
||||
if len(cand_ballots) > 0:
|
||||
__pragma__('opov')
|
||||
next_preferences[candidate].append(ballot)
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
self.candidates[candidate].transfers += (num_ballots * surplus) / transferable_ballots
|
||||
else:
|
||||
next_exhausted.append(ballot)
|
||||
|
||||
# Make transfers
|
||||
for candidate, cand_ballots in next_preferences.items():
|
||||
num_ballots = sum((b.value for b in cand_ballots), Num('0'))
|
||||
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].transfers += (num_ballots * surplus) / total_ballots # Multiply first to avoid rounding errors
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot in cand_ballots:
|
||||
__pragma__('opov')
|
||||
new_value = (ballot.value.to_rational() * surplus.to_rational()) / total_ballots.to_rational()
|
||||
self.candidates[candidate].ballots.append((ballot, new_value))
|
||||
__pragma__('noopov')
|
||||
|
||||
num_exhausted = sum((b.value for b in next_exhausted), Num('0'))
|
||||
|
||||
__pragma__('opov')
|
||||
self.exhausted.transfers += (num_exhausted * surplus) / total_ballots
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot in next_exhausted:
|
||||
__pragma__('opov')
|
||||
new_value = (ballot.value.to_rational() * surplus.to_rational()) / total_ballots.to_rational()
|
||||
__pragma__('noopov')
|
||||
self.exhausted.ballots.append((ballot, new_value))
|
||||
|
||||
return True
|
||||
|
||||
def do_exclusion(self, candidate_excluded, count_card):
|
||||
next_preferences = SafeDict([(c, Rational('0')) for c, cc in self.candidates.items()])
|
||||
next_exhausted = Rational('0')
|
||||
total_votes = Rational('0')
|
||||
|
||||
# Count and transfer next preferences
|
||||
for ballot, ballot_value in count_card.ballots:
|
||||
__pragma__('opov')
|
||||
total_votes += ballot_value
|
||||
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
|
||||
__pragma__('noopov')
|
||||
|
||||
if candidate is not None:
|
||||
__pragma__('opov')
|
||||
next_preferences[candidate] += ballot_value
|
||||
self.candidates[candidate].ballots.append((ballot, ballot_value))
|
||||
__pragma__('noopov')
|
||||
self.candidates[candidate].transfers += num_votes.to_num()
|
||||
else:
|
||||
__pragma__('opov')
|
||||
next_exhausted += ballot_value
|
||||
self.candidates[candidate].transfers += (num_ballots * surplus) / total_ballots
|
||||
__pragma__('noopov')
|
||||
self.exhausted.ballots.append((ballot, ballot_value))
|
||||
|
||||
# Credit votes
|
||||
for candidate, cand_votes in next_preferences.items():
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].transfers += cand_votes.to_num()
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
new_value = (ballot.value * surplus) / transferable_ballots
|
||||
else:
|
||||
new_value = ballot_value
|
||||
else:
|
||||
new_value = (ballot.value * surplus) / total_ballots
|
||||
|
||||
new_parcel.append((ballot, new_value))
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
self.exhausted.transfers += next_exhausted.to_num()
|
||||
|
||||
count_card.transfers -= count_card.votes
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
pass # No ballots exhaust
|
||||
else:
|
||||
self.exhausted.transfers += (surplus - transferable_votes)
|
||||
else:
|
||||
self.exhausted.transfers += (exhausted_ballots * surplus) / total_ballots
|
||||
__pragma__('noopov')
|
||||
|
||||
count_card.state = CandidateState.EXCLUDED
|
||||
__pragma__('opov')
|
||||
count_card.transfers -= surplus
|
||||
__pragma__('noopov')
|
||||
|
||||
count_card.state = CandidateState.ELECTED
|
||||
|
||||
class EGSTVCounter(UIGSTVCounter):
|
||||
"""
|
||||
Exclusive Gregory (last bundle) STV implementation
|
||||
"""
|
||||
|
||||
def do_surplus(self, candidate_surplus, count_card, surplus):
|
||||
"""Overrides UIGSTVCounter.do_surplus"""
|
||||
|
||||
last_bundle = count_card.parcels[len(count_card.parcels)-1]
|
||||
next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([last_bundle])
|
||||
|
||||
if self.options['papers'] == 'transferable':
|
||||
__pragma__('opov')
|
||||
transferable_ballots = total_ballots - exhausted_ballots
|
||||
transferable_votes = total_votes.to_num() - exhausted_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
for candidate, x in next_preferences.items():
|
||||
cand_ballots = x[0]
|
||||
num_ballots = x[1]
|
||||
num_votes = x[2]
|
||||
|
||||
new_parcel = []
|
||||
if len(cand_ballots) > 0:
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
self.candidates[candidate].transfers += (num_ballots * surplus) / transferable_ballots
|
||||
else:
|
||||
self.candidates[candidate].transfers += num_votes.to_num()
|
||||
else:
|
||||
self.candidates[candidate].transfers += (num_ballots * surplus) / total_ballots
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
new_value = (ballot.value * surplus) / transferable_ballots
|
||||
else:
|
||||
new_value = ballot_value
|
||||
else:
|
||||
new_value = (ballot.value * surplus) / total_ballots
|
||||
|
||||
new_parcel.append((ballot, new_value))
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
if self.options['papers'] == 'transferable':
|
||||
if transferable_votes > surplus:
|
||||
pass # No ballots exhaust
|
||||
else:
|
||||
self.exhausted.transfers += (surplus - transferable_votes)
|
||||
else:
|
||||
self.exhausted.transfers += (exhausted_ballots * surplus) / total_ballots
|
||||
__pragma__('noopov')
|
||||
|
||||
__pragma__('opov')
|
||||
count_card.transfers -= surplus
|
||||
__pragma__('noopov')
|
||||
|
||||
count_card.state = CandidateState.ELECTED
|
||||
|
@ -1,177 +0,0 @@
|
||||
# pyRCV2: Preferential vote counting
|
||||
# Copyright © 2020 Lee Yingtong Li (RunasSudo)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
__pragma__ = lambda x: None
|
||||
|
||||
from pyRCV2.method.base_stv import UIGSTVCounter
|
||||
from pyRCV2.model import CandidateState, CountCard
|
||||
from pyRCV2.numbers import Num, Rational
|
||||
from pyRCV2.safedict import SafeDict
|
||||
|
||||
class ParcelledEGCountCard(CountCard):
|
||||
def __init__(self, *args):
|
||||
CountCard.__init__(self, *args)
|
||||
del self.ballots
|
||||
self.parcels = []
|
||||
|
||||
#@property
|
||||
#def ballots(self):
|
||||
# raise Exception('Attempted to access ballots attribute of ParcelledUIGCountCard')
|
||||
|
||||
def clone(self):
|
||||
"""Overrides CountCard.clone"""
|
||||
result = ParcelledUIGCountCard()
|
||||
result.orig_votes = self.orig_votes
|
||||
result.transfers = self.transfers
|
||||
result.parcels = [[(b[0].clone(), b[1]) for b in p] for p in self.parcels]
|
||||
result.state = self.state
|
||||
result.order_elected = self.order_elected
|
||||
return result
|
||||
|
||||
class ParcelledEGSTVCounter(UIGSTVCounter):
|
||||
"""
|
||||
Exclusive Gregory (parcel by parcel) STV implementation
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
UIGSTVCounter.__init__(self, *args)
|
||||
self.candidates = SafeDict([(c, ParcelledEGCountCard()) for c in self.election.candidates])
|
||||
|
||||
def distribute_first_preferences(self):
|
||||
"""Overrides UIGSTVCounter.distribute_first_preferences"""
|
||||
|
||||
for ballot in self.election.ballots:
|
||||
__pragma__('opov')
|
||||
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
|
||||
|
||||
if candidate is not None:
|
||||
self.candidates[candidate].transfers += ballot.value
|
||||
if len(self.candidates[candidate].parcels) == 0:
|
||||
self.candidates[candidate].parcels.append([(ballot, self.cls_ballot_value(ballot.value))])
|
||||
else:
|
||||
self.candidates[candidate].parcels[0].append((ballot, self.cls_ballot_value(ballot.value)))
|
||||
else:
|
||||
self.exhausted.transfers += ballot.value
|
||||
self.exhausted.ballots.append((ballot, self.cls_ballot_value(ballot.value)))
|
||||
__pragma__('noopov')
|
||||
|
||||
def do_surplus(self, candidate_surplus, count_card, surplus):
|
||||
"""Overrides UIGSTVCounter.do_surplus"""
|
||||
|
||||
next_preferences = SafeDict([(c, []) for c, cc in self.candidates.items()])
|
||||
total_ballots = Num('0')
|
||||
total_votes = Rational('0')
|
||||
|
||||
parcel = count_card.parcels[len(count_card.parcels)-1] # Last bundle only
|
||||
|
||||
__pragma__('opov')
|
||||
|
||||
# Count next preferences
|
||||
for ballot, ballot_value in parcel:
|
||||
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
|
||||
|
||||
if candidate is not None:
|
||||
# Examine only transferable ballot papers
|
||||
total_ballots += ballot.value
|
||||
total_votes += ballot_value
|
||||
next_preferences[candidate].append((ballot, ballot_value))
|
||||
|
||||
# Make transfers
|
||||
for candidate, cand_ballots in next_preferences.items():
|
||||
if len(cand_ballots) == 0:
|
||||
continue
|
||||
|
||||
new_parcel = []
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
|
||||
if total_votes.to_num() > surplus:
|
||||
# Reweight according to transfer value
|
||||
num_ballots = sum((b.value for b, bv in cand_ballots), Num('0'))
|
||||
self.candidates[candidate].transfers += (num_ballots * surplus) / total_ballots # Multiply first to avoid rounding errors
|
||||
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
new_value = (ballot.value.to_rational() * surplus.to_rational()) / total_ballots.to_rational()
|
||||
new_parcel.append((ballot, new_value))
|
||||
else:
|
||||
# Do not reweight
|
||||
num_votes = sum((bv for b, bv in cand_ballots), Rational('0'))
|
||||
self.candidates[candidate].transfers += num_votes.to_num()
|
||||
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
new_parcel.append((ballot, ballot_value))
|
||||
|
||||
# Credit exhausted votes (only if not reweighting)
|
||||
if total_votes.to_num() <= surplus:
|
||||
self.exhausted.transfers += surplus - total_votes.to_num()
|
||||
|
||||
__pragma__('noopov')
|
||||
|
||||
return True
|
||||
|
||||
def do_exclusion(self, candidate_excluded, count_card):
|
||||
"""Overrides UIGSTVCounter.do_exclusion"""
|
||||
|
||||
next_preferences = SafeDict([(c, []) for c, cc in self.candidates.items()])
|
||||
next_exhausted = []
|
||||
|
||||
if len(count_card.parcels) > 0:
|
||||
parcel = count_card.parcels[0]
|
||||
count_card.parcels.remove(parcel)
|
||||
|
||||
# Count next preferences
|
||||
for ballot, ballot_value in parcel:
|
||||
__pragma__('opov')
|
||||
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
|
||||
__pragma__('noopov')
|
||||
|
||||
if candidate is not None:
|
||||
__pragma__('opov')
|
||||
next_preferences[candidate].append((ballot, ballot_value))
|
||||
__pragma__('noopov')
|
||||
else:
|
||||
next_exhausted.append((ballot, ballot_value))
|
||||
|
||||
# Make transfers
|
||||
for candidate, cand_ballots in next_preferences.items():
|
||||
if len(cand_ballots) == 0:
|
||||
continue
|
||||
|
||||
new_parcel = []
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].parcels.append(new_parcel)
|
||||
__pragma__('noopov')
|
||||
|
||||
num_votes = sum((bv for b, bv in cand_ballots), Rational('0'))
|
||||
__pragma__('opov')
|
||||
self.candidates[candidate].transfers += num_votes.to_num()
|
||||
count_card.transfers -= num_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot, ballot_value in cand_ballots:
|
||||
new_parcel.append((ballot, ballot_value))
|
||||
|
||||
# Credit exhausted votes
|
||||
num_votes = sum((bv for b, bv in next_exhausted), Rational('0'))
|
||||
__pragma__('opov')
|
||||
self.exhausted.transfers += num_votes.to_num()
|
||||
count_card.transfers -= num_votes.to_num()
|
||||
__pragma__('noopov')
|
||||
|
||||
for ballot, ballot_value in next_exhausted:
|
||||
self.exhausted.ballots.append((ballot, ballot_value))
|
||||
|
||||
if len(count_card.parcels) == 0:
|
||||
count_card.state = CandidateState.EXCLUDED
|
@ -64,10 +64,14 @@ class CountCard:
|
||||
def __init__(self):
|
||||
self.orig_votes = Num('0')
|
||||
self.transfers = Num('0')
|
||||
self.ballots = []
|
||||
self.state = CandidateState.HOPEFUL
|
||||
self.order_elected = None
|
||||
|
||||
# self.parcels = List[Parcel]
|
||||
# Parcel = List[Tuple[Ballot, Num]]
|
||||
# The exhausted/loss to fraction piles will have only one parcel
|
||||
self.parcels = []
|
||||
|
||||
@property
|
||||
def votes(self):
|
||||
__pragma__('opov')
|
||||
@ -84,7 +88,7 @@ class CountCard:
|
||||
result = CountCard()
|
||||
result.orig_votes = self.orig_votes
|
||||
result.transfers = self.transfers
|
||||
result.ballots = [(b[0].clone(), b[1]) for b in self.ballots]
|
||||
result.parcels = [[(b[0].clone(), b[1]) for b in p] for p in self.parcels]
|
||||
result.state = self.state
|
||||
result.order_elected = self.order_elected
|
||||
return result
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import pyRCV2.blt
|
||||
import pyRCV2.model
|
||||
import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.parcels, pyRCV2.method.wright
|
||||
import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.wright
|
||||
import pyRCV2.numbers
|
||||
import pyRCV2.random
|
||||
import pyRCV2.ties
|
||||
|
Reference in New Issue
Block a user