More aggressive early bulk election

This commit is contained in:
RunasSudo 2021-07-21 00:45:10 +10:00
parent a97ee591e5
commit ed4a86e699
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 108 additions and 53 deletions

View File

@ -4,10 +4,10 @@
The preset dropdown allows you to choose from a hardcoded list of preloaded STV counting rules. These are: The preset dropdown allows you to choose from a hardcoded list of preloaded STV counting rules. These are:
* *Recommended WIGM*: A recommended set of simple STV rules designed for computer counting, using the weighted inclusive Gregory method and rational arithmetic. * *OpenTally WIGM*: A recommended set of simple STV rules designed for computer counting, using the weighted inclusive Gregory method and rational arithmetic.
* *Scottish STV*: Rules from the [*Scottish Local Government Elections Order 2011*](https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made), using the weighted inclusive Gregory method. Validated against the [2007 Scottish local government election result for Linn ward](https://web.archive.org/web/20121004213938/http://www.glasgow.gov.uk/en/YourCouncil/Elections_Voting/Election_Results/ElectionScotland2007/LGWardResults.htm?ward=1&wardname=1%20-%20Linn). * *Scottish STV*: Rules from the [*Scottish Local Government Elections Order 2011*](https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made), using the weighted inclusive Gregory method. Validated against the [2007 Scottish local government election result for Linn ward](https://web.archive.org/web/20121004213938/http://www.glasgow.gov.uk/en/YourCouncil/Elections_Voting/Election_Results/ElectionScotland2007/LGWardResults.htm?ward=1&wardname=1%20-%20Linn).
* [*Meek STV*](http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf): Advanced STV rules designed for computer counting, recognised by the Proportional Representation Society of Australia (Victoria–Tasmania) as the superior STV system. * [*Meek STV*](http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf): Advanced STV rules designed for computer counting, recognised by the Proportional Representation Society of Australia (Victoria–Tasmania) as the superior STV system.
* *Meek STV (1987)* operates according to the original [Hill–Wichmann–Woodall specification](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) of Meek STV, with the modifications, relevant only in exceptional cases, that (a) fixed-point arithmetic with 5 decimal places is used, and (b) candidates are elected on strictly exceeding the quota. Validated against the Hill–Wichmann–Woodall implementation for the [ERS97 model election](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/#sub-section-24). * *Meek STV (1987)* operates according to the original Hill–Wichmann–Woodall [‘Algorithm 123’](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) specification of Meek STV, with the modifications, relevant only in exceptional cases, that (a) fixed-point arithmetic with 5 decimal places is used, and (b) candidates are elected on strictly exceeding the quota. Validated against the Hill–Wichmann–Woodall implementation for ballot papers [derived from the ERS97 model election](https://yingtongli.me/blog/2021/01/04/ers97.html).
* *Meek STV (2006)* operates according to [Hill's 2006 revisions](http://www.votingmatters.org.uk/ISSUE22/I22P2.pdf). This is the algorithm referred to in OpenSTV/OpaVote as ‘Meek STV’, and forms the basis of New Zealand's Meek STV rules. Validated against OpenSTV 1.7 for the ERS97 model election. * *Meek STV (2006)* operates according to [Hill's 2006 revisions](http://www.votingmatters.org.uk/ISSUE22/I22P2.pdf). This is the algorithm referred to in OpenSTV/OpaVote as ‘Meek STV’, and forms the basis of New Zealand's Meek STV rules. Validated against OpenSTV 1.7 for the ERS97 model election.
* *Meek STV (New Zealand)* operates according to Schedule 1A of the [*Local Electoral Regulations 2001*](https://www.legislation.govt.nz/regulation/public/2001/0145/latest/DLM57125.html). Validated against OpenSTV 1.7, and Hill's nzmeek version 6.7.7, for the ERS97 model election. * *Meek STV (New Zealand)* operates according to Schedule 1A of the [*Local Electoral Regulations 2001*](https://www.legislation.govt.nz/regulation/public/2001/0145/latest/DLM57125.html). Validated against OpenSTV 1.7, and Hill's nzmeek version 6.7.7, for the ERS97 model election.
* *Australian Senate STV*: Rules from the [*Commonwealth Electoral Act 1918*](https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700), using the unweighted inclusive Gregory method. Validated against the [2019 Australian Senate election result for Tasmania](https://results.aec.gov.au/24310/Website/SenateDownloadsMenu-24310-Csv.htm). * *Australian Senate STV*: Rules from the [*Commonwealth Electoral Act 1918*](https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700), using the unweighted inclusive Gregory method. Validated against the [2019 Australian Senate election result for Tasmania](https://results.aec.gov.au/24310/Website/SenateDownloadsMenu-24310-Csv.htm).
@ -150,9 +150,14 @@ When ballots are normalised, a set of preferences with weight *n* > 1 is instead
### Early bulk election (--no-early-bulk-elect) ### Early bulk election (--no-early-bulk-elect)
When early bulk election is enabled (default), all remaining candidates are declared elected in a single stage as soon as the number of not-excluded candidates exactly equals the number of vacancies to fill. Further surplus distributions are not performed, and outstanding exclusions, if any, are not completed. This is typical of most STV rules. When early bulk election is enabled (default), the count terminates as soon as the set of winning candidates is known. Specifically:
When early bulk election is disabled, surpluses continue to be distributed, and outstanding exclusions continue to be completed, even once the number of not-excluded candidates exactly equals the number of vacancies to fill. Bulk election is performed only once there are no more surpluses to distribute, and no exclusions to complete. * At the beginning of each stage, if the number of not-excluded candidates exactly equals the number of vacancies to fill, all remaining candidates are declared elected in a single stage. This is typical of most STV rules.
* If a proposed exclusion would cause the number of not-excluded candidates to exactly equal the number of vacancies, all remaining candidates are declared elected without transfers arising from the proposed exclusion being performed.
If an early bulk election is performed, further surplus distributions are not performed, and outstanding exclusions, if any, are not completed, even if they could change the order of election.
When early bulk election is disabled, surpluses continue to be distributed, and outstanding exclusions continue to be completed, even once the number of not-excluded candidates exactly equals the number of vacancies to fill. Bulk election is performed only as a final measure once there are no more surpluses to distribute, and no exclusions to complete.
In either case, candidates are declared elected in descending order of votes. This ensures that only one candidate is ever elected at a time and the order of election is well-defined, which is required e.g. for some affirmative action rules. In either case, candidates are declared elected in descending order of votes. This ensures that only one candidate is ever elected at a time and the order of election is well-defined, which is required e.g. for some affirmative action rules.

View File

@ -497,7 +497,7 @@ where
} }
// Continue exclusions // Continue exclusions
if continue_exclusion(state, &opts) { if continue_exclusion(state, &opts)? {
calculate_quota(state, opts); calculate_quota(state, opts);
elect_meeting_quota(state, opts)?; elect_meeting_quota(state, opts)?;
update_tiebreaks(state, opts); update_tiebreaks(state, opts);
@ -860,8 +860,23 @@ where
} }
} }
/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies /// Determine if, with the proposed exclusion of num_to_exclude candidates (if any), a bulk election can be made
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> { fn can_bulk_elect<N: Number>(state: &CountState<N>, num_to_exclude: usize) -> bool {
let num_hopefuls = state.election.candidates.iter()
.filter(|c| {
let cc = &state.candidates[c];
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
})
.count();
if state.num_elected + num_hopefuls - num_to_exclude <= state.election.seats {
return true;
}
return false;
}
/// Declare all continuing candidates to be elected
fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, template1: &'static str, template2: &'static str) -> Result<bool, STVError> {
let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter() let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter()
.filter(|c| { .filter(|c| {
let cc = &state.candidates[c]; let cc = &state.candidates[c];
@ -869,44 +884,49 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result
}) })
.collect(); .collect();
if state.num_elected + hopefuls.len() <= state.election.seats { // Bulk elect all remaining candidates
while !hopefuls.is_empty() {
let max_cands = ties::multiple_max_by(&hopefuls, |c| &state.candidates[c].votes);
let candidate = if max_cands.len() > 1 {
choose_highest(state, opts, max_cands)?
} else {
max_cands[0]
};
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.state = CandidateState::Elected;
state.num_elected += 1;
count_card.order_elected = state.num_elected as isize;
state.logger.log_smart(
template1,
template2,
vec![&candidate.name]
);
if constraints::update_constraints(state, opts) {
// Recheck as some candidates may have been doomed
hopefuls = state.election.candidates.iter()
.filter(|c| {
let cc = &state.candidates[c];
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
})
.collect();
} else {
hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap());
}
}
return Ok(true);
}
/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> {
if can_bulk_elect(state, 0) {
state.kind = None; state.kind = None;
state.title = "Bulk election".to_string(); state.title = "Bulk election".to_string();
// Bulk elect all remaining candidates return do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.");
while !hopefuls.is_empty() {
let max_cands = ties::multiple_max_by(&hopefuls, |c| &state.candidates[c].votes);
let candidate = if max_cands.len() > 1 {
choose_highest(state, opts, max_cands)?
} else {
max_cands[0]
};
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.state = CandidateState::Elected;
state.num_elected += 1;
count_card.order_elected = state.num_elected as isize;
state.logger.log_smart(
"{} is elected to fill the remaining vacancy.",
"{} are elected to fill the remaining vacancies.",
vec![&candidate.name]
);
if constraints::update_constraints(state, opts) {
// Recheck as some candidates may have been doomed
hopefuls = state.election.candidates.iter()
.filter(|c| {
let cc = &state.candidates[c];
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
})
.collect();
} else {
hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap());
}
}
return Ok(true);
} }
return Ok(false); return Ok(false);
} }
@ -944,8 +964,23 @@ where
names names
); );
exclude_candidates(state, opts, excluded_candidates); if opts.early_bulk_elect {
return Ok(true); // Determine if the proposed exclusion would enable a bulk election
if can_bulk_elect(state, excluded_candidates.len()) {
// Exclude candidates without further transfers
let order_excluded = state.num_excluded + 1;
for candidate in excluded_candidates {
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.state = CandidateState::Excluded;
state.num_excluded += 1;
count_card.order_elected = -(order_excluded as isize);
}
return do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.");
}
}
return exclude_candidates(state, opts, excluded_candidates);
} }
return Ok(false); return Ok(false);
@ -1030,13 +1065,27 @@ where
names names
); );
exclude_candidates(state, opts, excluded_candidates); if opts.early_bulk_elect {
// Determine if the proposed exclusion would enable a bulk election
if can_bulk_elect(state, excluded_candidates.len()) {
// Exclude candidates without further transfers
let order_excluded = state.num_excluded + 1;
for candidate in excluded_candidates {
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.state = CandidateState::Excluded;
state.num_excluded += 1;
count_card.order_elected = -(order_excluded as isize);
}
return do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.");
}
}
return Ok(true); return exclude_candidates(state, opts, excluded_candidates);
} }
/// Continue the exclusion of a candidate who is being excluded /// Continue the exclusion of a candidate who is being excluded
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where where
for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Mul<&'r N, Output=N>,
@ -1065,15 +1114,14 @@ where
names names
); );
exclude_candidates(state, opts, excluded_candidates); return exclude_candidates(state, opts, excluded_candidates);
return true;
} }
return false; return Ok(false);
} }
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion] /// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<bool, STVError>
where where
for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Mul<&'r N, Output=N>,
@ -1098,6 +1146,8 @@ where
gregory::wright_exclude_candidates(state, opts, excluded_candidates); gregory::wright_exclude_candidates(state, opts, excluded_candidates);
} }
} }
return Ok(true);
} }
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies /// Determine if the count is complete because the number of elected candidates equals the number of vacancies

View File

@ -39,7 +39,7 @@ fn csm15_float64() {
transferable_only: false, transferable_only: false,
exclusion: stv::ExclusionMethod::Wright, exclusion: stv::ExclusionMethod::Wright,
meek_nz_exclusion: false, meek_nz_exclusion: false,
early_bulk_elect: true, early_bulk_elect: false, // Required for validation
bulk_exclude: true, bulk_exclude: true,
defer_surpluses: false, defer_surpluses: false,
meek_immediate_elect: false, meek_immediate_elect: false,

View File

@ -39,7 +39,7 @@ fn prsa1_rational() {
transferable_only: true, transferable_only: true,
exclusion: stv::ExclusionMethod::ParcelsByOrder, exclusion: stv::ExclusionMethod::ParcelsByOrder,
meek_nz_exclusion: false, meek_nz_exclusion: false,
early_bulk_elect: false, early_bulk_elect: false, // Required for validation
bulk_exclude: false, bulk_exclude: false,
defer_surpluses: false, defer_surpluses: false,
meek_immediate_elect: false, meek_immediate_elect: false,