diff --git a/docs/options.md b/docs/options.md
index 3965293..4072891 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -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:
diff --git a/html/index.html b/html/index.html
index 8a3783d..3629a9e 100644
--- a/html/index.html
+++ b/html/index.html
@@ -105,10 +105,23 @@
Weighted inclusive Gregory
Unweighted inclusive Gregory
- Exclusive Gregory (by parcel)
+ Exclusive Gregory (last bundle)
Wright STV
+
+
+ Include non-transferable papers
+ Use transferable papers only
+
+
+
+
+ Exclude in one round
+ Exclude by parcel (by order)
+
+
+
Ties:
diff --git a/html/index.js b/html/index.js
index 18732ee..7ae2d45 100644
--- a/html/index.js
+++ b/html/index.js
@@ -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,
diff --git a/html/worker.js b/html/worker.js
index 3959d0a..a37b7de 100644
--- a/html/worker.js
+++ b/html/worker.js
@@ -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 {
diff --git a/pyRCV2/cli/stv.py b/pyRCV2/cli/stv.py
index 63a5cc0..8cac26d 100644
--- a/pyRCV2/cli/stv.py
+++ b/pyRCV2/cli/stv.py
@@ -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()
diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py
index 36b90aa..6bf866b 100644
--- a/pyRCV2/method/base_stv.py
+++ b/pyRCV2/method/base_stv.py
@@ -35,11 +35,13 @@ class BaseSTVCounter:
# Default options
self.options = {
- 'prog_quota': False, # Progressively reducing quota?
- 'bulk_elect': True, # Bulk election?
- 'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
+ 'prog_quota': False, # Progressively reducing quota?
+ 'bulk_elect': True, # Bulk election?
+ 'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
'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': []
}
@@ -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,17 +216,10 @@ class BaseSTVCounter:
__pragma__('noopov')
# Transfer surplus
- is_complete = self.do_surplus(candidate_surplus, count_card, surplus)
+ self.do_surplus(candidate_surplus, count_card, surplus)
- if is_complete:
- __pragma__('opov')
- 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()
+ # Declare elected any candidates meeting the quota as a result of surpluses
+ self.compute_quota()
self.elect_meeting_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
-
- 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)
+ for candidate, x in next_preferences.items():
+ cand_ballots = x[0]
+ num_ballots = x[1]
+ num_votes = x[2]
- if candidate is not None:
- self.candidates[candidate].transfers += ballot_value
- self.candidates[candidate].ballots.append((ballot, ballot_value))
+ 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.exhausted.transfers += ballot_value
- self.exhausted.ballots.append((ballot, ballot_value))
+ 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')
- 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')
- 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
"""
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')
+
+ 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')
- 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:
+ self.candidates[candidate].transfers += num_votes.to_num()
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
+ self.candidates[candidate].transfers += (num_ballots * surplus) / total_ballots
__pragma__('noopov')
- for ballot in cand_ballots:
+ for ballot, ballot_value 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))
+ 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')
- num_exhausted = sum((b.value for b in next_exhausted), Num('0'))
-
__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')
- 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))
+ __pragma__('opov')
+ count_card.transfers -= surplus
+ __pragma__('noopov')
- return True
+ count_card.state = CandidateState.ELECTED
+
+class EGSTVCounter(UIGSTVCounter):
+ """
+ Exclusive Gregory (last bundle) STV implementation
+ """
- 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')
+ def do_surplus(self, candidate_surplus, count_card, surplus):
+ """Overrides UIGSTVCounter.do_surplus"""
- # Count and transfer next preferences
- for ballot, ballot_value in count_card.ballots:
+ 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')
- total_votes += 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')
+
+ 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')
- if candidate is not None:
+ for ballot, ballot_value in cand_ballots:
__pragma__('opov')
- next_preferences[candidate] += ballot_value
- self.candidates[candidate].ballots.append((ballot, ballot_value))
+ 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')
- 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')
- 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
diff --git a/pyRCV2/method/parcels.py b/pyRCV2/method/parcels.py
deleted file mode 100644
index acab4c4..0000000
--- a/pyRCV2/method/parcels.py
+++ /dev/null
@@ -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 .
-
-__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
diff --git a/pyRCV2/model.py b/pyRCV2/model.py
index fb60f07..520692f 100644
--- a/pyRCV2/model.py
+++ b/pyRCV2/model.py
@@ -64,9 +64,13 @@ 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):
@@ -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
diff --git a/pyRCV2/transcrypt.py b/pyRCV2/transcrypt.py
index 5f928c9..752be3a 100644
--- a/pyRCV2/transcrypt.py
+++ b/pyRCV2/transcrypt.py
@@ -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