Extend early bulk election to multiple vacancies if the leading candidates cannot be overtaken
This commit is contained in:
parent
4690c32607
commit
3b8ccd097e
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
108
src/stv/mod.rs
108
src/stv/mod.rs
@ -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() {
|
||||
@ -743,13 +740,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||
// 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
|
||||
}
|
||||
// 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
|
||||
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());
|
||||
|
@ -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:");
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user