Separate out parcel handling logic

This commit is contained in:
RunasSudo 2020-12-29 02:43:37 +11:00
parent 044b9fb3d3
commit 509768c071
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 298 additions and 300 deletions

View File

@ -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 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. * 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) ## 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: 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). * 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: 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. * 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 (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. * Exclusive Gregory (last bundle): During surplus transfers, only the ballot papers received in the last transfer are examined. Transfers are not weighted.
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.
Other surplus transfer methods, such as non-fractional transfers (e.g. random sample) are not supported at this time. 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) ## Ties (-t/--ties)
This dropdown allows you to select how ties (in surplus transfer or exclusion) are broken. The options are: This dropdown allows you to select how ties (in surplus transfer or exclusion) are broken. The options are:

View File

@ -105,10 +105,23 @@
<select id="selTransfers"> <select id="selTransfers">
<option value="wig" selected>Weighted inclusive Gregory</option> <option value="wig" selected>Weighted inclusive Gregory</option>
<option value="uig">Unweighted 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> <option value="wright">Wright STV</option>
</select> </select>
</label> </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> <br>
<label> <label>
Ties: Ties:

View File

@ -37,6 +37,8 @@ function changePreset() {
document.getElementById('txtDP').value = '5'; document.getElementById('txtDP').value = '5';
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('selExclusion').value = 'one_round';
document.getElementById('selTies').value = 'backwards_random'; document.getElementById('selTies').value = 'backwards_random';
} else if (document.getElementById('selPreset').value === 'stvc') { } else if (document.getElementById('selPreset').value === 'stvc') {
document.getElementById('selQuotaCriterion').value = 'gt'; document.getElementById('selQuotaCriterion').value = 'gt';
@ -47,18 +49,23 @@ function changePreset() {
document.getElementById('selNumbers').value = 'rational'; document.getElementById('selNumbers').value = 'rational';
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('selExclusion').value = 'one_round';
document.getElementById('selTies').value = 'backwards_random'; 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('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop'; document.getElementById('selQuota').value = 'droop';
document.getElementById('chkProgQuota').checked = false; document.getElementById('chkProgQuota').checked = false;
document.getElementById('chkBulkElection').checked = true; document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').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('selSurplus').value = 'order';
document.getElementById('selTransfers').value = 'uig'; document.getElementById('selTransfers').value = 'uig';
document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'parcels_by_value';
document.getElementById('selTies').value = 'backwards_random'; 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('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop'; document.getElementById('selQuota').value = 'droop';
document.getElementById('chkProgQuota').checked = false; document.getElementById('chkProgQuota').checked = false;
@ -68,6 +75,8 @@ function changePreset() {
document.getElementById('txtDP').value = '5'; document.getElementById('txtDP').value = '5';
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('selExclusion').value = 'one_round';
document.getElementById('selTies').value = 'backwards_random'; document.getElementById('selTies').value = 'backwards_random';
} else if (document.getElementById('selPreset').value === 'prsa77') { } else if (document.getElementById('selPreset').value === 'prsa77') {
document.getElementById('selQuotaCriterion').value = 'geq'; document.getElementById('selQuotaCriterion').value = 'geq';
@ -78,7 +87,9 @@ function changePreset() {
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '0'; document.getElementById('txtDP').value = '0';
document.getElementById('selSurplus').value = 'order'; 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'; document.getElementById('selTies').value = 'backwards_random';
} }
} }
@ -305,6 +316,8 @@ async function clickCount() {
'bulk_elect': document.getElementById('chkBulkElection').checked, 'bulk_elect': document.getElementById('chkBulkElection').checked,
'bulk_exclude': document.getElementById('chkBulkExclusion').checked, 'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
'surplus_order': document.getElementById('selSurplus').value, 'surplus_order': document.getElementById('selSurplus').value,
'papers': document.getElementById('selPapers').value,
'exclusion': document.getElementById('selExclusion').value,
'ties': document.getElementById('selTies').value 'ties': document.getElementById('selTies').value
}, },
'seed': document.getElementById('txtSeed').value, 'seed': document.getElementById('txtSeed').value,

View File

@ -47,8 +47,8 @@ onmessage = function(evt) {
// Create counter // Create counter
if (evt.data.data.transfers === 'uig') { if (evt.data.data.transfers === 'uig') {
counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.data.options); counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.data.options);
} else if (evt.data.data.transfers === 'parcelled_eg') { } else if (evt.data.data.transfers === 'eg') {
counter = py.pyRCV2.method.parcels.ParcelledEGSTVCounter(election, evt.data.data.options); counter = py.pyRCV2.method.base_stv.EGSTVCounter(election, evt.data.data.options);
} else if (evt.data.data.transfers === 'wright') { } else if (evt.data.data.transfers === 'wright') {
counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.data.options); counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.data.options);
} else { } else {

View File

@ -18,8 +18,7 @@ import pyRCV2.blt
import pyRCV2.model import pyRCV2.model
import pyRCV2.numbers import pyRCV2.numbers
from pyRCV2.method.base_stv import UIGSTVCounter, WIGSTVCounter from pyRCV2.method.base_stv import UIGSTVCounter, WIGSTVCounter, EGSTVCounter
from pyRCV2.method.parcels import ParcelledEGSTVCounter
from pyRCV2.method.wright import WrightSTVCounter from pyRCV2.method.wright import WrightSTVCounter
from pyRCV2.ties import TiesBackwards, TiesPrompt, TiesRandom 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('--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('--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('--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('--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') 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(): for candidate, count_card in result.candidates.items():
state = None 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' 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' state = 'Excluded'
elif count_card.state == pyRCV2.model.CandidateState.WITHDRAWN: elif count_card.state == pyRCV2.model.CandidateState.WITHDRAWN:
state = 'Withdrawn' state = 'Withdrawn'
@ -82,8 +83,8 @@ def main(args):
# Create counter # Create counter
if args.method == 'uig': if args.method == 'uig':
counter = UIGSTVCounter(election, vars(args)) counter = UIGSTVCounter(election, vars(args))
elif args.method == 'parcelled_eg': elif args.method == 'eg':
counter = ParcelledEGSTVCounter(election, vars(args)) counter = EGSTVCounter(election, vars(args))
elif args.method == 'wright': elif args.method == 'wright':
counter = WrightSTVCounter(election, vars(args)) counter = WrightSTVCounter(election, vars(args))
else: else:
@ -105,6 +106,7 @@ def main(args):
counter.options['ties'].append(TiesRandom(args.random_seed)) counter.options['ties'].append(TiesRandom(args.random_seed))
counter.options['bulk_elect'] = not args.no_bulk_election counter.options['bulk_elect'] = not args.no_bulk_election
counter.options['papers'] = 'transferable' if args.transferable_only else 'both'
# Reset # Reset
result = counter.reset() result = counter.reset()

View File

@ -35,11 +35,13 @@ class BaseSTVCounter:
# Default options # Default options
self.options = { self.options = {
'prog_quota': False, # Progressively reducing quota? 'prog_quota': False, # Progressively reducing quota?
'bulk_elect': True, # Bulk election? 'bulk_elect': True, # Bulk election?
'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact' 'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
'quota_criterion': 'geq', # 'geq' or 'gt' 'quota_criterion': 'geq', # 'geq' or 'gt'
'surplus_order': 'size', # 'size' or 'order' 'surplus_order': 'size', # 'size' or 'order'
'papers': 'both', # 'both' or 'transferable'
'exclusion': 'one_round', # 'one_round', 'parcels_by_order' or 'parcels_by_value'
'ties': [] 'ties': []
} }
@ -98,10 +100,13 @@ class BaseSTVCounter:
if candidate is not None: if candidate is not None:
self.candidates[candidate].transfers += ballot.value 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: else:
self.exhausted.transfers += ballot.value 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') __pragma__('noopov')
def step(self): def step(self):
@ -211,17 +216,10 @@ class BaseSTVCounter:
__pragma__('noopov') __pragma__('noopov')
# Transfer surplus # Transfer surplus
is_complete = self.do_surplus(candidate_surplus, count_card, surplus) self.do_surplus(candidate_surplus, count_card, surplus)
if is_complete: # Declare elected any candidates meeting the quota as a result of surpluses
__pragma__('opov') self.compute_quota()
count_card.transfers -= surplus
__pragma__('noopov')
count_card.state = CandidateState.ELECTED
# Declare elected any candidates meeting the quota as a result of surpluses
self.compute_quota()
self.elect_meeting_quota() self.elect_meeting_quota()
@ -246,6 +244,39 @@ 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
@ -286,7 +317,7 @@ class BaseSTVCounter:
count_card.state = CandidateState.EXCLUDING count_card.state = CandidateState.EXCLUDING
# Exclude this candidate # 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 # Declare any candidates meeting the quota as a result of exclusion
self.compute_quota() self.compute_quota()
@ -445,133 +476,237 @@ class WIGSTVCounter(BaseSTVCounter):
""" """
def do_surplus(self, candidate_surplus, count_card, surplus): 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') __pragma__('opov')
new_value = (ballot_value * surplus) / count_card.votes # Multiply first to avoid rounding errors transferable_votes = total_votes.to_num() - exhausted_votes.to_num()
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))
__pragma__('noopov') __pragma__('noopov')
return True for candidate, x in next_preferences.items():
cand_ballots = x[0]
def do_exclusion(self, candidate_excluded, count_card): num_ballots = x[1]
for ballot, ballot_value in count_card.ballots: num_votes = x[2]
__pragma__('opov')
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
if candidate is not None: new_parcel = []
self.candidates[candidate].transfers += ballot_value if len(cand_ballots) > 0:
self.candidates[candidate].ballots.append((ballot, ballot_value)) __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: else:
self.exhausted.transfers += ballot_value self.candidates[candidate].transfers += (num_votes.to_num() * surplus) / total_votes.to_num()
self.exhausted.ballots.append((ballot, ballot_value))
__pragma__('noopov') __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') __pragma__('opov')
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_votes.to_num() * surplus) / total_votes.to_num()
__pragma__('noopov') __pragma__('noopov')
count_card.state = CandidateState.EXCLUDED __pragma__('opov')
count_card.transfers -= surplus
__pragma__('noopov')
count_card.state = CandidateState.ELECTED
def do_exclusion(self, candidate_excluded, count_card):
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 = []
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')
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 Basic unweighted inclusive Gregory STV counter
""" """
def __init__(self, *args): 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 # 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() 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):
# FIXME: Is it okay to use native int's here? next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences(count_card.parcels)
next_preferences = SafeDict([(c, []) for c, cc in self.candidates.items()])
next_exhausted = []
total_ballots = Num('0')
# Count next preferences if self.options['papers'] == 'transferable':
for ballot, ballot_value in count_card.ballots:
__pragma__('opov') __pragma__('opov')
total_ballots += ballot.value transferable_ballots = total_ballots - exhausted_ballots
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None) transferable_votes = total_votes.to_num() - exhausted_votes.to_num()
__pragma__('noopov') __pragma__('noopov')
for candidate, x in next_preferences.items():
cand_ballots = x[0]
num_ballots = x[1]
num_votes = x[2]
if candidate is not None: new_parcel = []
if len(cand_ballots) > 0:
__pragma__('opov') __pragma__('opov')
next_preferences[candidate].append(ballot) self.candidates[candidate].parcels.append(new_parcel)
__pragma__('noopov') __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: else:
next_exhausted.append(ballot) self.candidates[candidate].transfers += (num_ballots * surplus) / total_ballots
# 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') __pragma__('noopov')
for ballot in cand_ballots: for ballot, ballot_value in cand_ballots:
__pragma__('opov') __pragma__('opov')
new_value = (ballot.value.to_rational() * surplus.to_rational()) / total_ballots.to_rational() if self.options['papers'] == 'transferable':
self.candidates[candidate].ballots.append((ballot, new_value)) 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__('noopov')
num_exhausted = sum((b.value for b in next_exhausted), Num('0'))
__pragma__('opov') __pragma__('opov')
self.exhausted.transfers += (num_exhausted * surplus) / total_ballots 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__('noopov')
for ballot in next_exhausted: __pragma__('opov')
__pragma__('opov') count_card.transfers -= surplus
new_value = (ballot.value.to_rational() * surplus.to_rational()) / total_ballots.to_rational() __pragma__('noopov')
__pragma__('noopov')
self.exhausted.ballots.append((ballot, new_value))
return True count_card.state = CandidateState.ELECTED
class EGSTVCounter(UIGSTVCounter):
"""
Exclusive Gregory (last bundle) STV implementation
"""
def do_exclusion(self, candidate_excluded, count_card): def do_surplus(self, candidate_surplus, count_card, surplus):
next_preferences = SafeDict([(c, Rational('0')) for c, cc in self.candidates.items()]) """Overrides UIGSTVCounter.do_surplus"""
next_exhausted = Rational('0')
total_votes = Rational('0')
# Count and transfer next preferences last_bundle = count_card.parcels[len(count_card.parcels)-1]
for ballot, ballot_value in count_card.ballots: next_preferences, total_ballots, total_votes, next_exhausted, exhausted_ballots, exhausted_votes = self.next_preferences([last_bundle])
if self.options['papers'] == 'transferable':
__pragma__('opov') __pragma__('opov')
total_votes += ballot_value transferable_ballots = total_ballots - exhausted_ballots
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None) 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') __pragma__('noopov')
if candidate is not None: for ballot, ballot_value in cand_ballots:
__pragma__('opov') __pragma__('opov')
next_preferences[candidate] += ballot_value if self.options['papers'] == 'transferable':
self.candidates[candidate].ballots.append((ballot, ballot_value)) 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__('noopov')
else:
__pragma__('opov')
next_exhausted += ballot_value
__pragma__('noopov')
self.exhausted.ballots.append((ballot, ballot_value))
# Credit votes
for candidate, cand_votes in next_preferences.items():
__pragma__('opov')
self.candidates[candidate].transfers += cand_votes.to_num()
__pragma__('noopov')
__pragma__('opov') __pragma__('opov')
self.exhausted.transfers += next_exhausted.to_num() if self.options['papers'] == 'transferable':
if transferable_votes > surplus:
count_card.transfers -= count_card.votes pass # No ballots exhaust
else:
self.exhausted.transfers += (surplus - transferable_votes)
else:
self.exhausted.transfers += (exhausted_ballots * surplus) / total_ballots
__pragma__('noopov') __pragma__('noopov')
count_card.state = CandidateState.EXCLUDED __pragma__('opov')
count_card.transfers -= surplus
__pragma__('noopov')
count_card.state = CandidateState.ELECTED

View File

@ -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

View File

@ -64,9 +64,13 @@ class CountCard:
def __init__(self): def __init__(self):
self.orig_votes = Num('0') self.orig_votes = Num('0')
self.transfers = Num('0') self.transfers = Num('0')
self.ballots = []
self.state = CandidateState.HOPEFUL self.state = CandidateState.HOPEFUL
self.order_elected = None 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 @property
def votes(self): def votes(self):
@ -84,7 +88,7 @@ class CountCard:
result = CountCard() result = CountCard()
result.orig_votes = self.orig_votes result.orig_votes = self.orig_votes
result.transfers = self.transfers 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.state = self.state
result.order_elected = self.order_elected result.order_elected = self.order_elected
return result return result

View File

@ -16,7 +16,7 @@
import pyRCV2.blt import pyRCV2.blt
import pyRCV2.model 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.numbers
import pyRCV2.random import pyRCV2.random
import pyRCV2.ties import pyRCV2.ties