Extend early bulk election to multiple vacancies if the leading candidates cannot be overtaken

This commit is contained in:
RunasSudo 2021-07-23 01:21:29 +10:00
parent 4690c32607
commit 3b8ccd097e
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
7 changed files with 100 additions and 25 deletions

View File

@ -167,7 +167,7 @@ When early bulk election is enabled (default), the count terminates as soon as t
* 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. * 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 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.
* At the end of any stage, if only 1 vacancy remains and one continuing candidate has more votes than all other continuing candidates (plus votes awaiting transfer), that candidate is immediately declared elected. * At the end of any stage, if *n* vacancies remain and the *n*-th top continuing candidate has more votes than all lower continuing candidates (plus votes awaiting transfer), the *n* top continuing candidate are immediately declared elected.
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. 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.
@ -175,6 +175,10 @@ When early bulk election is disabled, surpluses continue to be distributed, and
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.
Note that the OpenTally rules for early bulk election are aggressive, and many STV rules do not implement all 3 (if any at all). It is not possible at this time to selectively apply only some of the rules. In order to reproduce the result of a count performed by others, where not all rules were implemented, consider disabling early bulk election and comparing the results at the time a bulk election would have been made.
Note also that early bulk election can conflict with constraints. If an election is to be run with constraints, it is recommend that early bulk election be disabled.
### Bulk exclusion (--bulk-exclude) ### Bulk exclusion (--bulk-exclude)
When bulk exclusion is disabled (default), only one candidate is ever excluded per stage. When bulk exclusion is disabled (default), only one candidate is ever excluded per stage.

View File

@ -165,7 +165,7 @@ pub struct CountState<'a, N: Number> {
pub quota: Option<N>, pub quota: Option<N>,
/// Vote required for election /// Vote required for election
/// ///
/// Only used in ERS97/ERS76, or if early bulk election is enabled and there is 1 vacancy remaining. /// Only used in ERS97/ERS76.
pub vote_required_election: Option<N>, pub vote_required_election: Option<N>,
/// Number of candidates who have been declared elected /// Number of candidates who have been declared elected

View File

@ -281,7 +281,7 @@ where
if opts.meek_immediate_elect { if opts.meek_immediate_elect {
// Try to elect candidates // Try to elect candidates
if super::elect_meeting_quota(state, opts)? { if super::elect_hopefuls(state, opts)? {
candidates_elected = Some(state.logger.entries.pop().unwrap()); candidates_elected = Some(state.logger.entries.pop().unwrap());
break; break;
} }

View File

@ -467,7 +467,7 @@ where
distribute_first_preferences(state, opts); distribute_first_preferences(state, opts);
calculate_quota(state, opts); calculate_quota(state, opts);
elect_meeting_quota(state, opts)?; elect_hopefuls(state, opts)?;
init_tiebreaks(state, opts); init_tiebreaks(state, opts);
return Ok(true); return Ok(true);
@ -499,7 +499,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_hopefuls(state, opts)?;
update_tiebreaks(state, opts); update_tiebreaks(state, opts);
return Ok(false); return Ok(false);
} }
@ -507,7 +507,7 @@ where
// Exclude doomed candidates // Exclude doomed candidates
if exclude_doomed(state, &opts)? { if exclude_doomed(state, &opts)? {
calculate_quota(state, opts); calculate_quota(state, opts);
elect_meeting_quota(state, opts)?; elect_hopefuls(state, opts)?;
update_tiebreaks(state, opts); update_tiebreaks(state, opts);
return Ok(false); return Ok(false);
} }
@ -515,7 +515,7 @@ where
// Distribute surpluses // Distribute surpluses
if distribute_surpluses(state, &opts)? { if distribute_surpluses(state, &opts)? {
calculate_quota(state, opts); calculate_quota(state, opts);
elect_meeting_quota(state, opts)?; elect_hopefuls(state, opts)?;
update_tiebreaks(state, opts); update_tiebreaks(state, opts);
return Ok(false); return Ok(false);
} }
@ -528,7 +528,7 @@ where
// Exclude lowest hopeful // Exclude lowest hopeful
if exclude_hopefuls(state, &opts)? { if exclude_hopefuls(state, &opts)? {
calculate_quota(state, opts); calculate_quota(state, opts);
elect_meeting_quota(state, opts)?; elect_hopefuls(state, opts)?;
update_tiebreaks(state, opts); update_tiebreaks(state, opts);
return Ok(false); return Ok(false);
} }
@ -656,8 +656,6 @@ fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N
} }
/// Update vote required for election according to ERS97 rules /// Update vote required for election according to ERS97 rules
///
/// This is also used to compute the vote required for election when early bulk election is enabled and 1 vacancy remains.
fn update_vre<N: Number>(state: &mut CountState<N>, opts: &STVOptions) { fn update_vre<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
let mut log = String::new(); let mut log = String::new();
@ -670,7 +668,6 @@ fn update_vre<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
}); });
log.push_str(format!("Total active vote is {:.dps$}, so the vote required for election is ", total_active_vote, dps=opts.pp_decimals).as_str()); log.push_str(format!("Total active vote is {:.dps$}, so the vote required for election is ", total_active_vote, dps=opts.pp_decimals).as_str());
//let vote_req = total_to_quota(total_active_vote, state.election.seats - state.num_elected, opts);
let vote_req = total_active_vote / N::from(state.election.seats - state.num_elected + 1); let vote_req = total_active_vote / N::from(state.election.seats - state.num_elected + 1);
if &vote_req < state.quota.as_ref().unwrap() { if &vote_req < state.quota.as_ref().unwrap() {
@ -743,13 +740,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
// Update quota and so VRE every stage // Update quota and so VRE every stage
state.vote_required_election = state.quota.clone(); state.vote_required_election = state.quota.clone();
} else { } else {
if opts.early_bulk_elect && state.num_elected + 1 == state.election.seats { // No use of VRE
// Early bulk election and one seat remains: VRE is majority of total active vote
// FIXME: This probably conflicts with constraints in some cases
update_vre(state, opts);
} else {
// No use of VRE
}
} }
} }
} }
@ -781,15 +772,94 @@ fn meets_vre<N: Number>(state: &CountState<N>, count_card: &CountCard<N>, opts:
} }
} }
/// Declare elected all candidates meeting the quota /// Declare elected the continuing candidates leading for remaining vacancies if they cannot be overtaken
fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> { fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> {
let num_vacancies = state.election.seats - state.num_elected;
if num_vacancies == 0 {
return Ok(false);
}
let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter()
.map(|c| (c, &state.candidates[c]))
.filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
.collect();
hopefuls.sort_unstable_by(|a, b| b.1.votes.cmp(&a.1.votes));
let mut total_trailing = N::new();
// For leading candidates, count only untransferred surpluses
total_trailing += hopefuls.iter().take(num_vacancies).fold(N::new(), |acc, (_, cc)| {
if &cc.votes > state.quota.as_ref().unwrap() {
acc + &cc.votes - state.quota.as_ref().unwrap()
} else {
acc
}
});
// For trailing candidates, count all votes
total_trailing += hopefuls.iter().skip(num_vacancies).fold(N::new(), |acc, (_, cc)| acc + &cc.votes);
// Add finally any votes awaiting transfer
total_trailing += state.candidates.values().fold(N::zero(), |acc, cc| {
match cc.state {
CandidateState::Elected => { if &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
CandidateState::Hopeful | CandidateState::Guarded | CandidateState::Withdrawn => { acc }
CandidateState::Excluded | CandidateState::Doomed => { acc + &cc.votes }
}
});
let last_winner = hopefuls[num_vacancies - 1].1;
if last_winner.votes <= total_trailing {
return Ok(false);
}
let mut hopefuls: Vec<&Candidate> = hopefuls.iter().map(|(c, _)| *c).collect();
// Bulk elect all remaining candidates
while state.num_elected < state.election.seats {
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(
"As they cannot now be overtaken, {} is elected to fill the remaining vacancy.",
"As they cannot now be overtaken, {} are elected to fill the remaining vacancies.",
vec![&candidate.name]
);
if constraints::update_constraints(state, opts) {
// FIXME: Work out interaction between early bulk election and constraints
panic!("Attempted early bulk election resulted in changes to constraint matrix");
} else {
hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap());
}
}
return Ok(true);
}
/// Declare elected all candidates meeting the quota, and (if enabled) any candidates who can be early bulk elected because they have sufficiently many votes
fn elect_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> {
// Determine if early bulk election can be effected
if opts.early_bulk_elect {
if elect_sure_winners(state, opts)? {
return Ok(true);
}
}
let mut cands_meeting_quota: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie let mut cands_meeting_quota: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
.map(|c| (c, &state.candidates[c])) .map(|c| (c, &state.candidates[c]))
.filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) }) .filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) })
.collect(); .collect();
// Sort by votes // Sort by votes
cands_meeting_quota.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); cands_meeting_quota.sort_unstable_by(|a, b| b.1.votes.cmp(&a.1.votes));
let mut cands_meeting_quota: Vec<&Candidate> = cands_meeting_quota.iter().map(|(c, _)| *c).collect(); let mut cands_meeting_quota: Vec<&Candidate> = cands_meeting_quota.iter().map(|(c, _)| *c).collect();
let elected = !cands_meeting_quota.is_empty(); let elected = !cands_meeting_quota.is_empty();
@ -829,7 +899,7 @@ fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVO
.map(|c| (c, &state.candidates[c])) .map(|c| (c, &state.candidates[c]))
.filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) }) .filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) })
.collect(); .collect();
cmq.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); cmq.sort_unstable_by(|a, b| b.1.votes.cmp(&a.1.votes));
cands_meeting_quota = cmq.iter().map(|(c, _)| *c).collect(); cands_meeting_quota = cmq.iter().map(|(c, _)| *c).collect();
} else { } else {
cands_meeting_quota.remove(cands_meeting_quota.iter().position(|c| *c == candidate).unwrap()); cands_meeting_quota.remove(cands_meeting_quota.iter().position(|c| *c == candidate).unwrap());

View File

@ -198,6 +198,7 @@ where
} }
/// Prompt the candidate for input, depending on CLI or WebAssembly target /// Prompt the candidate for input, depending on CLI or WebAssembly target
// FIXME: This may have unexpected behaviour if the tie occurs in the middle of a stage
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
println!("Multiple tied candidates:"); println!("Multiple tied candidates:");

View File

@ -39,7 +39,7 @@ fn ers97_rational() {
transferable_only: true, transferable_only: true,
exclusion: stv::ExclusionMethod::ByValue, exclusion: stv::ExclusionMethod::ByValue,
meek_nz_exclusion: false, meek_nz_exclusion: false,
early_bulk_elect: true, early_bulk_elect: false,
bulk_exclude: true, bulk_exclude: true,
defer_surpluses: true, defer_surpluses: true,
meek_immediate_elect: false, meek_immediate_elect: false,

View File

@ -46,7 +46,7 @@ fn scotland_linn07_fixed5() {
transferable_only: false, transferable_only: false,
exclusion: stv::ExclusionMethod::SingleStage, exclusion: stv::ExclusionMethod::SingleStage,
meek_nz_exclusion: false, meek_nz_exclusion: false,
early_bulk_elect: true, early_bulk_elect: false,
bulk_exclude: false, bulk_exclude: false,
defer_surpluses: false, defer_surpluses: false,
meek_immediate_elect: false, meek_immediate_elect: false,
@ -77,7 +77,7 @@ fn scotland_linn07_gfixed5() {
transferable_only: false, transferable_only: false,
exclusion: stv::ExclusionMethod::SingleStage, exclusion: stv::ExclusionMethod::SingleStage,
meek_nz_exclusion: false, meek_nz_exclusion: false,
early_bulk_elect: true, early_bulk_elect: false,
bulk_exclude: false, bulk_exclude: false,
defer_surpluses: false, defer_surpluses: false,
meek_immediate_elect: false, meek_immediate_elect: false,