Implement exclusive Gregory and PRSA 1977 rules
This commit is contained in:
parent
8307ebf86c
commit
f6596bd8e8
@ -7,6 +7,7 @@ The preset dropdown allows you to choose from a hardcoded list of preloaded STV
|
|||||||
* [Scottish STV](https://www.opavote.com/methods/scottish-stv-rules)
|
* [Scottish STV](https://www.opavote.com/methods/scottish-stv-rules)
|
||||||
* pyRCV STV-C: Our recommended rules for a computerised STV count
|
* pyRCV STV-C: Our recommended rules for a computerised STV count
|
||||||
* [Wright STV](https://www.aph.gov.au/Parliamentary_Business/Committees/House_of_Representatives_Committees?url=em/elect07/subs/sub051.1.pdf)
|
* [Wright STV](https://www.aph.gov.au/Parliamentary_Business/Committees/House_of_Representatives_Committees?url=em/elect07/subs/sub051.1.pdf)
|
||||||
|
* [PRSA 1977](https://www.prsa.org.au/rule1977.htm)
|
||||||
|
|
||||||
This functionality is not available on the Python command line.
|
This functionality is not available on the Python command line.
|
||||||
|
|
||||||
@ -57,21 +58,23 @@ This dropdown allows you to select in what order surpluses are distributed:
|
|||||||
* By size (default): When multiple candidates exceed the quota, the largest surplus is transferred (even if it arose at a later stage of the count).
|
* By size (default): When multiple candidates exceed the quota, the largest surplus is transferred (even if it arose at a later stage of the count).
|
||||||
* By order: When multiple candidates exceed the quota, the surplus of the candidate elected first is transferred (even if it is smaller than another). Candidates are always declared elected in descending order of number of votes.
|
* By order: When multiple candidates exceed the quota, the surplus of the candidate elected first is transferred (even if it is smaller than another). Candidates are always declared elected in descending order of number of votes.
|
||||||
|
|
||||||
Some STV counting rules provide, for example, that ‘no surplus shall be transferred before a surplus that arose earlier in the counting whether larger or not’ ([PRSA 1977 rules](https://www.prsa.org.au/rule1977.htm)). In this case, the option ‘By order’ should be selected.
|
Some STV counting rules provide, for example, that ‘no surplus shall be transferred before a surplus that arose earlier in the counting whether larger or not’ (PRSA 1977). In this case, the option ‘By order’ should be selected.
|
||||||
|
|
||||||
## Method (-m/--method)
|
## Method (-m/--method)
|
||||||
|
|
||||||
This dropdown allows you to select how ballots are transferred during surplus transfers or exclusions:
|
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, 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.
|
||||||
* 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. Transferred votes are credited in parcels to avoid excessive rounding.
|
|
||||||
* 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).
|
||||||
|
|
||||||
Note that, in each of these options, all a candidate's ballot papers are examined during a surplus transfer. The practice used in some rules (e.g. ERS97) where ballot papers are divided into transferable and non-transferable papers, and only transferable papers further examined, is not currently supported.
|
Other methods are supported, but not recommended:
|
||||||
|
|
||||||
Note that, when unweighted inclusive 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, 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.
|
||||||
|
|
||||||
Other surplus transfer methods, such as exclusive Gregory (‘last bundle’) or non-fractional transfers (e.g. random sample) are not recommended and not supported.
|
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.
|
||||||
|
|
||||||
## Ties (-t/--ties)
|
## Ties (-t/--ties)
|
||||||
|
|
||||||
|
@ -42,9 +42,11 @@
|
|||||||
<option value="stvc">pyRCV STV-C</option>
|
<option value="stvc">pyRCV STV-C</option>
|
||||||
<!--<option value="senate">Australian Senate STV</option>-->
|
<!--<option value="senate">Australian Senate STV</option>-->
|
||||||
<option value="wright">Wright STV</option>
|
<option value="wright">Wright STV</option>
|
||||||
|
<option value="prsa77">PRSA 1977</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button>
|
<button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button>
|
||||||
|
<!--GITREV-->
|
||||||
<a href="https://yingtongli.me/blog/2020/12/24/pyrcv2.html">Information and instructions</a>
|
<a href="https://yingtongli.me/blog/2020/12/24/pyrcv2.html">Information and instructions</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -103,6 +105,7 @@
|
|||||||
<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="wright">Wright STV</option>
|
<option value="wright">Wright STV</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
@ -69,6 +69,17 @@ function changePreset() {
|
|||||||
document.getElementById('selSurplus').value = 'size';
|
document.getElementById('selSurplus').value = 'size';
|
||||||
document.getElementById('selTransfers').value = 'wright';
|
document.getElementById('selTransfers').value = 'wright';
|
||||||
document.getElementById('selTies').value = 'backwards_random';
|
document.getElementById('selTies').value = 'backwards_random';
|
||||||
|
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
||||||
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
|
document.getElementById('selQuota').value = 'droop';
|
||||||
|
document.getElementById('chkProgQuota').checked = false;
|
||||||
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
|
document.getElementById('chkBulkExclusion').checked = false;
|
||||||
|
document.getElementById('selNumbers').value = 'fixed';
|
||||||
|
document.getElementById('txtDP').value = '0';
|
||||||
|
document.getElementById('selSurplus').value = 'order';
|
||||||
|
document.getElementById('selTransfers').value = 'parcelled_eg';
|
||||||
|
document.getElementById('selTies').value = 'backwards_random';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,9 +187,9 @@ async function clickCount() {
|
|||||||
|
|
||||||
elTd = document.createElement('td');
|
elTd = document.createElement('td');
|
||||||
elTd.classList.add('count');
|
elTd.classList.add('count');
|
||||||
if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) {
|
if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
|
||||||
elTd.classList.add('excluded');
|
elTd.classList.add('excluded');
|
||||||
} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED) {
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
|
||||||
elTd.classList.add('elected');
|
elTd.classList.add('elected');
|
||||||
}
|
}
|
||||||
elTd.style.borderTop = '1px solid black';
|
elTd.style.borderTop = '1px solid black';
|
||||||
@ -192,7 +203,7 @@ async function clickCount() {
|
|||||||
elTd = document.createElement('td');
|
elTd = document.createElement('td');
|
||||||
elTd.classList.add('count');
|
elTd.classList.add('count');
|
||||||
|
|
||||||
if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED) {
|
if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
|
||||||
elTd.classList.add('elected');
|
elTd.classList.add('elected');
|
||||||
elTd.innerText = countCard.votes;
|
elTd.innerText = countCard.votes;
|
||||||
elTr1.querySelector('td:first-child').classList.add('elected');
|
elTr1.querySelector('td:first-child').classList.add('elected');
|
||||||
@ -202,7 +213,7 @@ async function clickCount() {
|
|||||||
if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN) {
|
if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN) {
|
||||||
elTd.classList.add('excluded');
|
elTd.classList.add('excluded');
|
||||||
elTd.innerText = 'WD';
|
elTd.innerText = 'WD';
|
||||||
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) {
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
|
||||||
elTd.classList.add('excluded');
|
elTd.classList.add('excluded');
|
||||||
elTd.innerText = 'Ex';
|
elTd.innerText = 'Ex';
|
||||||
} else {
|
} else {
|
||||||
|
@ -43,6 +43,8 @@ onmessage = function(evt) {
|
|||||||
let counter;
|
let counter;
|
||||||
if (evt.data.transfers === 'uig') {
|
if (evt.data.transfers === 'uig') {
|
||||||
counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.options);
|
counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.options);
|
||||||
|
} else if (evt.data.transfers === 'parcelled_eg') {
|
||||||
|
counter = py.pyRCV2.method.parcels.ParcelledEGSTVCounter(election, evt.data.options);
|
||||||
} else if (evt.data.transfers === 'wright') {
|
} else if (evt.data.transfers === 'wright') {
|
||||||
counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.options);
|
counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.options);
|
||||||
} else {
|
} else {
|
||||||
|
@ -19,6 +19,7 @@ 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
|
||||||
|
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
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ 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', 'wright'], default='wig', help='method of surpluses and exclusions (default: wig - weighted inclusive Gregory)')
|
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('--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')
|
||||||
|
|
||||||
@ -81,6 +82,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':
|
||||||
|
counter = ParcelledEGSTVCounter(election, vars(args))
|
||||||
elif args.method == 'wright':
|
elif args.method == 'wright':
|
||||||
counter = WrightSTVCounter(election, vars(args))
|
counter = WrightSTVCounter(election, vars(args))
|
||||||
else:
|
else:
|
||||||
|
@ -67,18 +67,7 @@ class BaseSTVCounter:
|
|||||||
Does not reset the states of candidates, etc.
|
Does not reset the states of candidates, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Distribute first preferences
|
self.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
|
|
||||||
self.candidates[candidate].ballots.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')
|
|
||||||
|
|
||||||
self.quota = None
|
self.quota = None
|
||||||
self.compute_quota()
|
self.compute_quota()
|
||||||
@ -98,6 +87,23 @@ class BaseSTVCounter:
|
|||||||
self.step_results = [result]
|
self.step_results = [result]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def distribute_first_preferences(self):
|
||||||
|
"""
|
||||||
|
Distribute first preferences (called as part of the reset() step)
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
self.candidates[candidate].ballots.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 step(self):
|
def step(self):
|
||||||
"""
|
"""
|
||||||
Public function:
|
Public function:
|
||||||
@ -180,10 +186,14 @@ class BaseSTVCounter:
|
|||||||
Distribute surpluses, if any
|
Distribute surpluses, if any
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Are we distributing a surplus?
|
||||||
|
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.DISTRIBUTING_SURPLUS]
|
||||||
|
|
||||||
# Do surpluses need to be distributed?
|
# Do surpluses need to be distributed?
|
||||||
__pragma__('opov')
|
if len(has_surplus) == 0:
|
||||||
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED and cc.votes > self.quota]
|
__pragma__('opov')
|
||||||
__pragma__('noopov')
|
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED and cc.votes > self.quota]
|
||||||
|
__pragma__('noopov')
|
||||||
|
|
||||||
if len(has_surplus) > 0:
|
if len(has_surplus) > 0:
|
||||||
# Distribute surpluses in specified order
|
# Distribute surpluses in specified order
|
||||||
@ -196,21 +206,25 @@ class BaseSTVCounter:
|
|||||||
else:
|
else:
|
||||||
raise STVException('Invalid surplus order option')
|
raise STVException('Invalid surplus order option')
|
||||||
|
|
||||||
count_card.state = CandidateState.ELECTED
|
count_card.state = CandidateState.DISTRIBUTING_SURPLUS
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
surplus = count_card.votes - self.quota
|
surplus = count_card.votes - self.quota
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
# Transfer surplus
|
# Transfer surplus
|
||||||
self.do_surplus(candidate_surplus, count_card, surplus)
|
is_complete = self.do_surplus(candidate_surplus, count_card, surplus)
|
||||||
|
|
||||||
__pragma__('opov')
|
if is_complete:
|
||||||
count_card.transfers -= surplus
|
__pragma__('opov')
|
||||||
__pragma__('noopov')
|
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()
|
self.elect_meeting_quota()
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
@ -271,17 +285,14 @@ class BaseSTVCounter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
candidate_excluded, count_card = self.candidate_to_exclude()
|
candidate_excluded, count_card = self.candidate_to_exclude()
|
||||||
count_card.state = CandidateState.EXCLUDED
|
count_card.state = CandidateState.EXCLUDING
|
||||||
|
|
||||||
# Exclude this candidate
|
# Exclude this candidate
|
||||||
self.do_exclusion(candidate_excluded, count_card)
|
is_complete = self.do_exclusion(candidate_excluded, count_card)
|
||||||
|
|
||||||
__pragma__('opov')
|
|
||||||
count_card.transfers -= count_card.votes
|
|
||||||
__pragma__('noopov')
|
|
||||||
|
|
||||||
# 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()
|
||||||
|
|
||||||
self.elect_meeting_quota()
|
self.elect_meeting_quota()
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
@ -303,6 +314,11 @@ class BaseSTVCounter:
|
|||||||
Determine the candidate to exclude
|
Determine the candidate to exclude
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Continue current exclusion if applicable
|
||||||
|
excluding = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.EXCLUDING]
|
||||||
|
if len(excluding) > 0:
|
||||||
|
return excluding[0][0], excluding[0][1]
|
||||||
|
|
||||||
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
|
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
|
||||||
hopefuls.sort(key=lambda x: x[1].votes)
|
hopefuls.sort(key=lambda x: x[1].votes)
|
||||||
|
|
||||||
@ -445,6 +461,8 @@ class WIGSTVCounter(BaseSTVCounter):
|
|||||||
self.exhausted.ballots.append((ballot, new_value))
|
self.exhausted.ballots.append((ballot, new_value))
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def do_exclusion(self, candidate_excluded, count_card):
|
def do_exclusion(self, candidate_excluded, count_card):
|
||||||
for ballot, ballot_value in count_card.ballots:
|
for ballot, ballot_value in count_card.ballots:
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
@ -458,6 +476,12 @@ class WIGSTVCounter(BaseSTVCounter):
|
|||||||
self.exhausted.ballots.append((ballot, ballot_value))
|
self.exhausted.ballots.append((ballot, ballot_value))
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
|
__pragma__('opov')
|
||||||
|
count_card.transfers -= count_card.votes
|
||||||
|
__pragma__('noopov')
|
||||||
|
|
||||||
|
count_card.state = CandidateState.EXCLUDED
|
||||||
|
|
||||||
class UIGSTVCounter(BaseSTVCounter):
|
class UIGSTVCounter(BaseSTVCounter):
|
||||||
"""
|
"""
|
||||||
Basic unweighted inclusive Gregory STV counter
|
Basic unweighted inclusive Gregory STV counter
|
||||||
@ -515,6 +539,8 @@ class UIGSTVCounter(BaseSTVCounter):
|
|||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
self.exhausted.ballots.append((ballot, new_value))
|
self.exhausted.ballots.append((ballot, new_value))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def do_exclusion(self, candidate_excluded, count_card):
|
def do_exclusion(self, candidate_excluded, count_card):
|
||||||
next_preferences = SafeDict([(c, Rational('0')) for c, cc in self.candidates.items()])
|
next_preferences = SafeDict([(c, Rational('0')) for c, cc in self.candidates.items()])
|
||||||
next_exhausted = Rational('0')
|
next_exhausted = Rational('0')
|
||||||
@ -546,4 +572,8 @@ class UIGSTVCounter(BaseSTVCounter):
|
|||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
self.exhausted.transfers += next_exhausted.to_num()
|
self.exhausted.transfers += next_exhausted.to_num()
|
||||||
|
|
||||||
|
count_card.transfers -= count_card.votes
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
|
count_card.state = CandidateState.EXCLUDED
|
||||||
|
176
pyRCV2/method/parcels.py
Normal file
176
pyRCV2/method/parcels.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
# 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 = []
|
||||||
|
|
||||||
|
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
|
@ -30,10 +30,12 @@ class Candidate:
|
|||||||
|
|
||||||
class CandidateState:
|
class CandidateState:
|
||||||
HOPEFUL = 0
|
HOPEFUL = 0
|
||||||
PROVISIONALLY_ELECTED = 1
|
PROVISIONALLY_ELECTED = 10
|
||||||
ELECTED = 2
|
DISTRIBUTING_SURPLUS = 20
|
||||||
EXCLUDED = 3
|
ELECTED = 30
|
||||||
WITHDRAWN = 4
|
EXCLUDING = 40
|
||||||
|
EXCLUDED = 50
|
||||||
|
WITHDRAWN = 60
|
||||||
|
|
||||||
class Ballot:
|
class Ballot:
|
||||||
def __init__(self, value, preferences):
|
def __init__(self, value, preferences):
|
||||||
@ -82,7 +84,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.clone() for b in self.ballots]
|
result.ballots = [(b[0].clone(), b[1]) for b in self.ballots]
|
||||||
result.state = self.state
|
result.state = self.state
|
||||||
result.order_elected = self.order_elected
|
result.order_elected = self.order_elected
|
||||||
return result
|
return result
|
||||||
|
@ -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.wright
|
import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.parcels, pyRCV2.method.wright
|
||||||
import pyRCV2.numbers
|
import pyRCV2.numbers
|
||||||
import pyRCV2.random
|
import pyRCV2.random
|
||||||
import pyRCV2.ties
|
import pyRCV2.ties
|
||||||
|
Reference in New Issue
Block a user