Implement option to disable bulk election
This commit is contained in:
parent
ae6bb3e09f
commit
d1528ee987
@ -33,6 +33,12 @@ When this option is disabled (default), the quota is calculated once after all f
|
|||||||
|
|
||||||
When this option is enabled, the quota is recalculated at the end of every stage, and may decrease throughout the count as votes are exhausted. OpaVote calls this a ‘dynamic threshold’.
|
When this option is enabled, the quota is recalculated at the end of every stage, and may decrease throughout the count as votes are exhausted. OpaVote calls this a ‘dynamic threshold’.
|
||||||
|
|
||||||
|
## Bulk election (--no-bulk-election)
|
||||||
|
|
||||||
|
When bulk election is enabled (default), all remaining candidates are declared elected in a single round once the number of not-excluded candidates exactly equals the number of vacancies to fill. Further surplus distributions are not performed. This is typical of most STV rules.
|
||||||
|
|
||||||
|
When bulk election is disabled, surpluses continue to be distributed even once the number of not-excluded candidates exactly equals the number of vacancies to fill. Once there are no more surpluses to distribute, the candidates are declared elected, one by one, in descending order of votes. This ensures that only one candidate is ever elected in each round and the order of election is well-defined, which is required e.g. for some affirmative action rules.
|
||||||
|
|
||||||
## Numbers (-n/--numbers), Decimal places (--decimals)
|
## Numbers (-n/--numbers), Decimal places (--decimals)
|
||||||
|
|
||||||
This dropdown allows you to select how numbers (vote totals, etc.) are represented internally in memory. The options are:
|
This dropdown allows you to select how numbers (vote totals, etc.) are represented internally in memory. The options are:
|
||||||
|
@ -68,6 +68,10 @@
|
|||||||
<input type="checkbox" id="chkProgQuota">
|
<input type="checkbox" id="chkProgQuota">
|
||||||
Progressive quota
|
Progressive quota
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="chkBulkElection" checked>
|
||||||
|
Bulk election
|
||||||
|
</label>
|
||||||
<label style="display: none;">
|
<label style="display: none;">
|
||||||
<input type="checkbox" id="chkBulkExclusion">
|
<input type="checkbox" id="chkBulkExclusion">
|
||||||
Bulk exclusion (NYI)
|
Bulk exclusion (NYI)
|
||||||
|
@ -31,6 +31,7 @@ function changePreset() {
|
|||||||
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('chkBulkExclusion').checked = false;
|
document.getElementById('chkBulkExclusion').checked = false;
|
||||||
document.getElementById('selNumbers').value = 'fixed';
|
document.getElementById('selNumbers').value = 'fixed';
|
||||||
document.getElementById('txtDP').value = '5';
|
document.getElementById('txtDP').value = '5';
|
||||||
@ -41,24 +42,27 @@ function changePreset() {
|
|||||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||||
document.getElementById('selQuota').value = 'droop_exact';
|
document.getElementById('selQuota').value = 'droop_exact';
|
||||||
document.getElementById('chkProgQuota').checked = true;
|
document.getElementById('chkProgQuota').checked = true;
|
||||||
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = true;
|
document.getElementById('chkBulkExclusion').checked = true;
|
||||||
document.getElementById('selNumbers').value = 'rational';
|
document.getElementById('selNumbers').value = 'rational';
|
||||||
document.getElementById('selSurplus').value = 'size';
|
document.getElementById('selSurplus').value = 'size';
|
||||||
document.getElementById('selTransfers').value = 'wig';
|
document.getElementById('selTransfers').value = 'wig';
|
||||||
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('chkBulkExclusion').checked = true;
|
document.getElementById('chkBulkExclusion').checked = true;
|
||||||
document.getElementById('selNumbers').value = 'int';
|
document.getElementById('selNumbers').value = 'int';
|
||||||
document.getElementById('selSurplus').value = 'order';
|
document.getElementById('selSurplus').value = 'order';
|
||||||
document.getElementById('selTransfers').value = 'uig';
|
document.getElementById('selTransfers').value = 'uig';
|
||||||
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;
|
||||||
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = true;
|
document.getElementById('chkBulkExclusion').checked = true;
|
||||||
document.getElementById('selNumbers').value = 'fixed';
|
document.getElementById('selNumbers').value = 'fixed';
|
||||||
document.getElementById('txtDP').value = '5';
|
document.getElementById('txtDP').value = '5';
|
||||||
@ -274,6 +278,7 @@ async function clickCount() {
|
|||||||
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
||||||
'quota': document.getElementById('selQuota').value,
|
'quota': document.getElementById('selQuota').value,
|
||||||
'prog_quota': document.getElementById('chkProgQuota').checked,
|
'prog_quota': document.getElementById('chkProgQuota').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,
|
||||||
'ties': document.getElementById('selTies').value
|
'ties': document.getElementById('selTies').value
|
||||||
|
@ -31,6 +31,7 @@ def add_parser(subparsers):
|
|||||||
parser.add_argument('--quota', '-q', choices=['droop', 'droop_exact', 'hare', 'hare_exact'], default='droop', help='quota calculation (default: droop)')
|
parser.add_argument('--quota', '-q', choices=['droop', 'droop_exact', 'hare', 'hare_exact'], default='droop', help='quota calculation (default: droop)')
|
||||||
parser.add_argument('--quota-criterion', '-c', choices=['geq', 'gt'], default='geq', help='quota criterion (default: geq)')
|
parser.add_argument('--quota-criterion', '-c', choices=['geq', 'gt'], default='geq', help='quota criterion (default: geq)')
|
||||||
parser.add_argument('--prog-quota', action='store_true', help='progressively reducing quota')
|
parser.add_argument('--prog-quota', action='store_true', help='progressively reducing quota')
|
||||||
|
parser.add_argument('--no-bulk-election', action='store_true', help='disable bulk election unless absolutely required')
|
||||||
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)')
|
||||||
@ -100,6 +101,8 @@ def main(args):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
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
|
||||||
|
|
||||||
# Reset
|
# Reset
|
||||||
result = counter.reset()
|
result = counter.reset()
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ 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?
|
||||||
'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'
|
||||||
@ -107,7 +108,7 @@ class BaseSTVCounter:
|
|||||||
self.step_count_cards()
|
self.step_count_cards()
|
||||||
|
|
||||||
# Check if done
|
# Check if done
|
||||||
result = self.check_if_done()
|
result = self.before_surpluses()
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -116,6 +117,11 @@ class BaseSTVCounter:
|
|||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# Check if done (2)
|
||||||
|
result = self.before_exclusion()
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
# Insufficient winners and no surpluses to distribute
|
# Insufficient winners and no surpluses to distribute
|
||||||
# Exclude the lowest ranked hopeful
|
# Exclude the lowest ranked hopeful
|
||||||
result = self.exclude_candidate()
|
result = self.exclude_candidate()
|
||||||
@ -134,39 +140,40 @@ class BaseSTVCounter:
|
|||||||
self.exhausted.step()
|
self.exhausted.step()
|
||||||
self.loss_fraction.step()
|
self.loss_fraction.step()
|
||||||
|
|
||||||
def check_if_done(self):
|
def before_surpluses(self):
|
||||||
"""
|
"""
|
||||||
Check if the count can be completed
|
Check if the count can be completed before distributing surpluses
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Have sufficient candidates been elected?
|
# Have sufficient candidates been elected?
|
||||||
if self.num_elected >= self.election.seats:
|
if self.num_elected >= self.election.seats:
|
||||||
return CountCompleted()
|
return CountCompleted()
|
||||||
|
|
||||||
# Are there just enough candidates to fill all the seats
|
# Are there just enough candidates to fill all the seats?
|
||||||
if self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL) <= self.election.seats:
|
if self.options['bulk_elect']:
|
||||||
# TODO: Sort by size
|
if self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL) <= self.election.seats:
|
||||||
|
# TODO: Sort by size
|
||||||
|
|
||||||
# Declare elected all remaining candidates
|
# Declare elected all remaining candidates
|
||||||
for candidate, count_card in self.candidates.items():
|
for candidate, count_card in self.candidates.items():
|
||||||
if count_card.state == CandidateState.HOPEFUL:
|
if count_card.state == CandidateState.HOPEFUL:
|
||||||
count_card.state = CandidateState.PROVISIONALLY_ELECTED
|
count_card.state = CandidateState.PROVISIONALLY_ELECTED
|
||||||
count_card.order_elected = self.num_elected
|
count_card.order_elected = self.num_elected
|
||||||
self.num_elected += 1
|
self.num_elected += 1
|
||||||
|
|
||||||
__pragma__('opov')
|
__pragma__('opov')
|
||||||
result = CountStepResult(
|
result = CountStepResult(
|
||||||
'Bulk election',
|
'Bulk election',
|
||||||
self.candidates,
|
self.candidates,
|
||||||
self.exhausted,
|
self.exhausted,
|
||||||
self.loss_fraction,
|
self.loss_fraction,
|
||||||
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||||
self.quota
|
self.quota
|
||||||
)
|
)
|
||||||
__pragma__('noopov')
|
__pragma__('noopov')
|
||||||
|
|
||||||
self.step_results.append(result)
|
self.step_results.append(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def distribute_surpluses(self):
|
def distribute_surpluses(self):
|
||||||
"""
|
"""
|
||||||
@ -227,6 +234,37 @@ class BaseSTVCounter:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError('Method not implemented')
|
raise NotImplementedError('Method not implemented')
|
||||||
|
|
||||||
|
def before_exclusion(self):
|
||||||
|
"""
|
||||||
|
Check before excluding a candidate
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If we did not perform bulk election in before_surpluses: Are there just enough candidates to fill all the seats?
|
||||||
|
if not self.options['bulk_elect']:
|
||||||
|
if self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL) <= self.election.seats:
|
||||||
|
# Declare elected one remaining candidate at a time
|
||||||
|
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
|
||||||
|
hopefuls.sort(key=lambda x: x[1].votes, reverse=True)
|
||||||
|
candidate_elected, count_card = self.choose_highest(hopefuls)
|
||||||
|
|
||||||
|
count_card.state = CandidateState.PROVISIONALLY_ELECTED
|
||||||
|
count_card.order_elected = self.num_elected
|
||||||
|
self.num_elected += 1
|
||||||
|
|
||||||
|
__pragma__('opov')
|
||||||
|
result = CountStepResult(
|
||||||
|
'Bulk election',
|
||||||
|
self.candidates,
|
||||||
|
self.exhausted,
|
||||||
|
self.loss_fraction,
|
||||||
|
self.total + self.exhausted.votes + self.loss_fraction.votes,
|
||||||
|
self.quota
|
||||||
|
)
|
||||||
|
__pragma__('noopov')
|
||||||
|
|
||||||
|
self.step_results.append(result)
|
||||||
|
return result
|
||||||
|
|
||||||
def exclude_candidate(self):
|
def exclude_candidate(self):
|
||||||
"""
|
"""
|
||||||
Exclude the lowest ranked hopeful
|
Exclude the lowest ranked hopeful
|
||||||
|
Reference in New Issue
Block a user