Further aggressive early bulk election

This commit is contained in:
RunasSudo 2021-07-21 10:59:06 +10:00
parent ed4a86e699
commit b5ee76f159
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 74 additions and 40 deletions

View File

@ -154,6 +154,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.
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.

View File

@ -655,6 +655,42 @@ fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N
return total;
}
/// 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();
// Calculate total active vote
let total_active_vote = 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 } }
_ => { acc + &cc.votes }
}
});
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); // FIXME: This is incorrect
if &vote_req < state.quota.as_ref().unwrap() {
// VRE is less than the quota
if let Some(v) = &state.vote_required_election {
if &vote_req != v {
log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
state.vote_required_election = Some(vote_req);
state.logger.log_literal(log);
}
} else {
log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
state.vote_required_election = Some(vote_req);
state.logger.log_literal(log);
}
} else {
// VRE is not less than the quota, so use the quota
state.vote_required_election = state.quota.clone();
}
}
/// Calculate the quota according to [STVOptions::quota]
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
// Calculate quota
@ -676,9 +712,9 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
// ERS97/ERS76 rules
// -------------------------
// Reduce quota if allowable
// (ERS97) Reduce quota if allowable
if state.num_elected == 0 {
if opts.quota_mode == QuotaMode::ERS97 && state.num_elected == 0 {
let mut log = String::new();
// Calculate the total vote
@ -698,41 +734,23 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
// Calculate vote required for election
if state.num_elected < state.election.seats {
let mut log = String::new();
// Calculate total active vote
let total_active_vote = 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 } }
_ => { acc + &cc.votes }
}
});
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);
if &vote_req < state.quota.as_ref().unwrap() {
// VRE is less than the quota
if let Some(v) = &state.vote_required_election {
if &vote_req != v {
log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
state.vote_required_election = Some(vote_req);
state.logger.log_literal(log);
}
} else {
log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
state.vote_required_election = Some(vote_req);
state.logger.log_literal(log);
}
} else {
// VRE is not less than the quota, so use the quota
state.vote_required_election = state.quota.clone();
}
update_vre(state, opts);
}
} else {
// No ERS97/ERS76 rules
if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek {
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
update_vre(state, opts);
} else {
// VRE is quota
if state.vote_required_election.is_none() {
state.vote_required_election = state.quota.clone();
}
}
}
}
}
@ -777,11 +795,21 @@ fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVO
count_card.state = CandidateState::Elected;
state.num_elected += 1;
count_card.order_elected = state.num_elected as isize;
if meets_quota(state.quota.as_ref().unwrap(), count_card, opts) {
// Elected with a quota
state.logger.log_smart(
"{} meets the quota and is elected.",
"{} meet the quota and are elected.",
vec![&candidate.name]
);
} else {
// Elected with vote required
state.logger.log_smart(
"{} meets the vote required and is elected.",
"{} meet the vote required and are elected.",
vec![&candidate.name]
);
}
if constraints::update_constraints(state, opts) {
// Recheck as some candidates may have been doomed

View File

@ -285,6 +285,11 @@ fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &st
return result;
}
#[inline]
fn should_show_vre(opts: &stv::STVOptions) -> bool {
return opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 || opts.early_bulk_elect;
}
/// Generate the first column of the HTML results table
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
@ -292,7 +297,7 @@ fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions)
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
}
result.push_str(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr><tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr><tr class="info votes"></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
if opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 {
if should_show_vre(opts) {
result.push_str(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#);
}
return result;
@ -354,7 +359,7 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
if opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 {
if should_show_vre(opts) {
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into());
}