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.
* 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.
@ -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.
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)
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>,
/// 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>,
/// Number of candidates who have been declared elected

View File

@ -281,7 +281,7 @@ where
if opts.meek_immediate_elect {
// 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());
break;
}

View File

@ -467,7 +467,7 @@ where
distribute_first_preferences(state, opts);
calculate_quota(state, opts);
elect_meeting_quota(state, opts)?;
elect_hopefuls(state, opts)?;
init_tiebreaks(state, opts);
return Ok(true);
@ -499,7 +499,7 @@ where
// Continue exclusions
if continue_exclusion(state, &opts)? {
calculate_quota(state, opts);
elect_meeting_quota(state, opts)?;
elect_hopefuls(state, opts)?;
update_tiebreaks(state, opts);
return Ok(false);
}
@ -507,7 +507,7 @@ where
// Exclude doomed candidates
if exclude_doomed(state, &opts)? {
calculate_quota(state, opts);
elect_meeting_quota(state, opts)?;
elect_hopefuls(state, opts)?;
update_tiebreaks(state, opts);
return Ok(false);
}
@ -515,7 +515,7 @@ where
// Distribute surpluses
if distribute_surpluses(state, &opts)? {
calculate_quota(state, opts);
elect_meeting_quota(state, opts)?;
elect_hopefuls(state, opts)?;
update_tiebreaks(state, opts);
return Ok(false);
}
@ -528,7 +528,7 @@ where
// Exclude lowest hopeful
if exclude_hopefuls(state, &opts)? {
calculate_quota(state, opts);
elect_meeting_quota(state, opts)?;
elect_hopefuls(state, opts)?;
update_tiebreaks(state, opts);
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
///
/// 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) {
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());
//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);
if &vote_req < state.quota.as_ref().unwrap() {
@ -742,16 +739,10 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
if opts.surplus == SurplusMethod::Meek {
// Update quota and so VRE every stage
state.vote_required_election = state.quota.clone();
} else {
if opts.early_bulk_elect && state.num_elected + 1 == state.election.seats {
// 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
}
}
}
}
/// Compare the candidate's votes with the specified target according to [STVOptions::quota_criterion]
@ -781,15 +772,94 @@ fn meets_vre<N: Number>(state: &CountState<N>, count_card: &CountCard<N>, opts:
}
}
/// Declare elected all candidates meeting the quota
fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> {
/// Declare elected the continuing candidates leading for remaining vacancies if they cannot be overtaken
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
.map(|c| (c, &state.candidates[c]))
.filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) })
.collect();
// 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 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]))
.filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) })
.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();
} else {
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
// FIXME: This may have unexpected behaviour if the tie occurs in the middle of a stage
#[cfg(not(target_arch = "wasm32"))]
fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
println!("Multiple tied candidates:");

View File

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

View File

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