OpenTally/src/stv/mod.rs

1276 lines
43 KiB
Rust

/* OpenTally: Open-source election vote counting
* Copyright © 2021–2023 Lee Yingtong Li (RunasSudo)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/// Gregory methods of surplus distributions
pub mod gregory;
/// Meek method of surplus distributions, etc.
pub mod meek;
/// Helper functions for HTML reporting output
pub mod html;
/// Random sample methods of surplus distributions
pub mod sample;
/// WebAssembly wrappers
//#[cfg(target_arch = "wasm32")]
pub mod wasm;
mod options;
pub use options::*;
use crate::candmap::CandidateMap;
use crate::constraints;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, RollbackState, StageKind, Vote};
use crate::numbers::Number;
use crate::sharandom::SHARandom;
use crate::ties::{self, TieStrategy};
use itertools::Itertools;
use std::fmt;
use std::ops;
/// An error during the STV count
#[derive(Debug, Eq, PartialEq)]
pub enum STVError {
/// Options for the count are invalid
InvalidOptions(&'static str),
/// Tie could not be resolved
UnresolvedTie,
/// Unrecoverable error during the count
CannotCompleteCount(&'static str),
}
impl STVError {
/// Describe the error
pub fn describe(&self) -> &'static str {
match self {
STVError::InvalidOptions(s) => s,
STVError::UnresolvedTie => "Unable to resolve tie",
STVError::CannotCompleteCount(s) => s,
}
}
}
impl fmt::Display for STVError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.describe())?;
return Ok(());
}
}
/// Preprocess the given election
pub fn preprocess_election<N: Number>(election: &mut Election<N>, opts: &STVOptions) {
// Normalise ballots if required
if opts.surplus == SurplusMethod::IHare || opts.surplus == SurplusMethod::Hare {
election.normalise_ballots();
}
// Process equal rankings
election.realise_equal_rankings();
}
/// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules
pub fn count_init<'a, N: Number>(state: &mut CountState<'a, N>, opts: &'a STVOptions) -> Result<(), STVError>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
{
// Initialise RNG
for t in opts.ties.iter() {
if let TieStrategy::Random(seed) = t {
state.random = Some(SHARandom::new(seed));
}
}
constraints::update_constraints(state, opts);
distribute_first_preferences(state, opts);
calculate_quota(state, opts);
elect_hopefuls(state, opts, true)?;
init_tiebreaks(state, opts);
return Ok(());
}
/// Perform a single stage of the STV count
///
/// Returns `true` if the count is complete, otherwise `false`.
pub fn count_one_stage<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where
for<'r> &'r N: ops::Add<&'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::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>,
{
state.transfer_table = None;
state.logger.entries.clear();
state.step_all();
// Finish count
if finished_before_stage(state) {
return Ok(true);
}
if let RollbackState::Normal = state.rollback_state {
} else {
constraints::rollback_one_stage(state, opts)?;
elect_hopefuls(state, opts, true)?;
update_tiebreaks(state, opts);
return Ok(false);
}
// Attempt early bulk election
if opts.early_bulk_elect {
if bulk_elect(state, opts)? {
return Ok(false);
}
}
// Continue exclusions
if continue_exclusion(state, opts)? {
calculate_quota(state, opts);
elect_hopefuls(state, opts, true)?;
update_tiebreaks(state, opts);
return Ok(false);
}
// Exclude doomed candidates
if exclude_doomed(state, opts)? {
calculate_quota(state, opts);
elect_hopefuls(state, opts, true)?;
update_tiebreaks(state, opts);
return Ok(false);
}
// Distribute surpluses
if distribute_surpluses(state, opts)? {
calculate_quota(state, opts);
elect_hopefuls(state, opts, true)?;
update_tiebreaks(state, opts);
return Ok(false);
}
// Attempt late bulk election
if bulk_elect(state, opts)? {
return Ok(false);
}
// Sanity check
let num_hopefuls = state.candidates.values()
.filter(|cc| cc.state == CandidateState::Hopeful)
.count();
if num_hopefuls == 0 {
return Err(STVError::CannotCompleteCount("Insufficient continuing candidates to complete count"));
}
// Exclude lowest hopeful
exclude_hopefuls(state, opts)?; // Cannot fail
calculate_quota(state, opts);
elect_hopefuls(state, opts, true)?;
update_tiebreaks(state, opts);
return Ok(false);
}
/// See [next_preferences]
pub struct NextPreferencesResult<'a, N> {
/// [NextPreferencesEntry] for each [Candidate]
pub candidates: CandidateMap<'a, NextPreferencesEntry<'a, N>>,
/// [NextPreferencesEntry] for exhausted ballots
pub exhausted: NextPreferencesEntry<'a, N>,
/// Total weight of ballots examined
pub total_ballots: N,
}
/// See [next_preferences]
pub struct NextPreferencesEntry<'a, N> {
/// Votes recording a next preference for the candidate
pub votes: Vec<Vote<'a, N>>,
/// Weight of such ballots
pub num_ballots: N,
}
/// Count the given votes, grouping according to next available preference
pub fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a, N>>) -> NextPreferencesResult<'a, N> {
let mut result = NextPreferencesResult {
candidates: CandidateMap::with_capacity(state.election.candidates.len()),
exhausted: NextPreferencesEntry {
votes: Vec::new(),
num_ballots: N::new(),
},
total_ballots: N::new(),
};
for mut vote in votes.into_iter() {
result.total_ballots += &vote.ballot.orig_value;
let mut next_candidate = None;
while let Some(preference) = vote.next_preference() {
let candidate = &state.election.candidates[preference];
let count_card = &state.candidates[candidate];
if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
next_candidate = Some(candidate);
break;
}
}
// Have to structure like this to satisfy Rust's borrow checker
if let Some(candidate) = next_candidate {
match result.candidates.get_mut(candidate) {
Some(entry) => {
entry.num_ballots += &vote.ballot.orig_value;
entry.votes.push(vote);
}
None => {
let entry = NextPreferencesEntry {
num_ballots: vote.ballot.orig_value.clone(),
votes: vec![vote],
};
result.candidates.insert(candidate, entry);
}
}
} else {
result.exhausted.num_ballots += &vote.ballot.orig_value;
result.exhausted.votes.push(vote);
}
}
return result;
}
/// Distribute first preference votes
fn distribute_first_preferences<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
{
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG | SurplusMethod::IHare | SurplusMethod::Hare => {
gregory::distribute_first_preferences(state, opts);
}
SurplusMethod::Meek => {
meek::distribute_first_preferences(state, opts);
}
}
}
/// Calculate the quota, given the total vote, according to [STVOptions::quota]
fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N {
match opts.quota {
QuotaType::Droop | QuotaType::DroopExact => {
total /= N::from(seats + 1);
}
QuotaType::Hare | QuotaType::HareExact => {
total /= N::from(seats);
}
}
if let Some(dps) = opts.round_quota {
match opts.quota {
QuotaType::Droop | QuotaType::Hare => {
// Increment to next available increment
let mut factor = N::from(10);
factor.pow_assign(dps as i32);
total *= &factor;
total.floor_mut(0);
total += N::one();
total /= factor;
}
QuotaType::DroopExact | QuotaType::HareExact => {
// Round up to next available increment if necessary
total.ceil_mut(dps);
}
}
}
return total;
}
/// Update vote required for election according to ERS97/ERS76 rules
fn update_vre_ers<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
if opts.quota_mode == QuotaMode::ERS76 && state.num_excluded == 0 && (state.num_elected == 0 || state.candidates.values().all(|cc| !cc.finalised)) {
// ERS76 rules: Do not update VRE until a surplus is distributed or candidate is excluded
state.vote_required_election = state.quota.clone();
return;
}
let mut log = String::new();
// Calculate active vote
let active_vote = state.active_vote();
log.push_str(format!("Active vote is {:.dps$}, so the vote required for election is ", active_vote, dps=opts.pp_decimals).as_str());
let vote_req = active_vote / N::from(state.election.seats - state.num_elected + 1);
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 vote required for election if only one candidate remains, used in early bulk election
///
/// Assumes early bulk election is enabled.
fn update_vre_bulk<N: Number>(state: &mut CountState<N>, _opts: &STVOptions) {
// If --early-bulk-elect and one candidate remains, VRE is half of the active vote
// For display purposes only
if state.election.seats - state.num_elected == 1 {
//let mut log = String::new();
// Calculate active vote
let active_vote = state.active_vote();
//log.push_str(format!("Active vote is {:.dps$}, so the vote required for election is ", active_vote, dps=opts.pp_decimals).as_str());
let vote_req = active_vote / N::from(state.election.seats - state.num_elected + 1);
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);
//}
}
}
}
/// Calculate the quota according to [STVOptions::quota]
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
if state.quota.is_none() || opts.quota_mode == QuotaMode::DynamicByTotal {
// Calculate quota by total vote
let mut log = String::new();
// Calculate the total vote
let total_vote = state.total_vote();
if state.election.seats == 1 {
log.push_str(format!("{:.dps$} usable votes, so a majority is ", total_vote, dps=opts.pp_decimals).as_str());
} else {
log.push_str(format!("{:.dps$} usable votes, so the quota is ", total_vote, dps=opts.pp_decimals).as_str());
}
let quota = total_to_quota(total_vote, state.election.seats, opts);
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
state.quota = Some(quota);
state.logger.log_literal(log);
} else if opts.quota_mode == QuotaMode::DynamicByActive {
// Calculate quota by active vote
let mut log = String::new();
// Calculate the active vote
let active_vote = state.active_vote();
if state.election.seats == 1 {
log.push_str(format!("Active vote is {:.dps$}, so a majority is ", active_vote, dps=opts.pp_decimals).as_str());
} else {
log.push_str(format!("Active vote is {:.dps$}, so the quota is ", active_vote, dps=opts.pp_decimals).as_str());
}
// TODO: Calculate according to --quota ?
let quota = active_vote / N::from(state.election.seats - state.num_elected + 1);
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
state.quota = Some(quota);
state.logger.log_literal(log);
}
if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 {
// ERS97/ERS76 rules
// -------------------------
// (ERS97) Reduce quota if allowable
if opts.quota_mode == QuotaMode::ERS97 && state.num_elected == 0 {
let mut log = String::new();
// Calculate the total vote
let total_vote = state.total_vote();
if state.election.seats == 1 {
log.push_str(format!("{:.dps$} usable votes, so a majority is ", total_vote, dps=opts.pp_decimals).as_str());
} else {
log.push_str(format!("{:.dps$} usable votes, so the quota is reduced to ", total_vote, dps=opts.pp_decimals).as_str());
}
let quota = total_to_quota(total_vote, state.election.seats, opts);
if &quota < state.quota.as_ref().unwrap() {
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
state.quota = Some(quota);
state.logger.log_literal(log);
}
}
// ------------------------------------
// Calculate vote required for election
if state.num_elected < state.election.seats {
update_vre_ers(state, opts);
}
} else {
// No ERS97/ERS76 rules
if opts.early_bulk_elect {
update_vre_bulk(state, opts);
}
}
}
/// Compare the candidate's votes with the specified target according to [STVOptions::quota_criterion]
fn cmp_quota_criterion<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
match opts.quota_criterion {
QuotaCriterion::GreaterOrEqual => {
return count_card.votes >= *quota;
}
QuotaCriterion::Greater => {
return count_card.votes > *quota;
}
}
}
/// Determine if the given candidate meets the vote required to be elected, according to [STVOptions::quota_criterion] and [STVOptions::quota_mode]
fn meets_vre<N: Number>(state: &CountState<N>, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 {
// VRE is set because ERS97/ERS76 rules
return cmp_quota_criterion(state.vote_required_election.as_ref().unwrap(), count_card, opts);
} else {
// VRE is set (if at all) for display purposes only so ignore it here
return cmp_quota_criterion(state.quota.as_ref().unwrap(), count_card, opts);
}
}
/// Declare elected the continuing candidates leading for remaining vacancies if they cannot be overtaken
///
/// Returns `true` if any candidates were elected.
fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> {
// Do not interrupt rolling back!!
if let RollbackState::Normal = state.rollback_state {
} else {
return Ok(false);
}
if state.num_elected >= state.election.seats {
return Ok(false);
}
let num_vacancies = state.election.seats - state.num_elected;
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 += state.total_surplus_of(hopefuls.iter().take(num_vacancies).map(|(_, cc)| *cc));
// For trailing candidates, count all votes
total_trailing += state.total_votes_of(hopefuls.iter().skip(num_vacancies).map(|(_, cc)| *cc));
// Add finally any votes awaiting transfer
total_trailing += state.candidates.values().fold(N::new(), |mut acc, cc| {
match cc.state {
CandidateState::Elected => {
if !cc.finalised && &cc.votes > state.quota.as_ref().unwrap() {
acc += &cc.votes;
acc -= state.quota.as_ref().unwrap();
}
}
CandidateState::Hopeful | CandidateState::Guarded | CandidateState::Withdrawn => {}
CandidateState::Excluded | CandidateState::Doomed => {
acc += &cc.votes;
}
}
acc
});
if num_vacancies - 1 < hopefuls.len() {
let last_winner = hopefuls[num_vacancies - 1].1;
if last_winner.votes <= total_trailing {
return Ok(false);
}
}
let mut leading_hopefuls: Vec<&Candidate> = hopefuls.iter().take(num_vacancies).map(|(c, _)| *c).collect();
match constraints::test_constraints_any_time(state, &leading_hopefuls, CandidateState::Elected) {
Ok(_) => {}
Err(_) => { return Ok(false); } // Bulk election conflicts with constraints
}
// Bulk election is possible!
// Elect all leading candidates
if num_vacancies > 1 {
// Update VRE
// (If num_vacancies == 1, this has already been done in calculate_quota)
state.vote_required_election = Some(total_trailing);
}
while !leading_hopefuls.is_empty() && state.num_elected < state.election.seats {
let max_cands = ties::multiple_max_by(&leading_hopefuls, |c| &state.candidates[c].votes);
let candidate = if max_cands.len() > 1 {
choose_highest(state, opts, &max_cands, "Which candidate to elect?")?
} 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.as_str()] // rust-analyzer doesn't understand &String -> &str
);
leading_hopefuls.remove(leading_hopefuls.iter().position(|c| *c == candidate).unwrap());
}
constraints::update_constraints(state, opts);
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
///
/// Returns `true` if any candidates were elected.
fn elect_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, if_immediate: bool) -> Result<bool, STVError> {
if opts.immediate_elect != if_immediate && opts.surplus != SurplusMethod::Meek {
// For --no-immediate-elect
return Ok(false);
}
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| 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();
while !cands_meeting_quota.is_empty() && state.num_elected < state.election.seats {
// Declare elected in descending order of votes
let max_cands = ties::multiple_max_by(&cands_meeting_quota, |c| &state.candidates[c].votes);
let candidate = if max_cands.len() > 1 {
choose_highest(state, opts, &max_cands, "Which candidate to elect?")?
} else {
max_cands[0]
};
if opts.constraint_mode == ConstraintMode::RepeatCount && state.election.constraints.is_some() {
if let Err((constraint, group)) = constraints::test_constraints_immediate(state, &[candidate], CandidateState::Elected) {
// This election would violate a constraint, so stop here
state.logger.log_smart(
"The election of {} now would violate constraints.",
"The election of {} now would violate constraints.",
vec![candidate.name.as_str()]
);
// Trigger rollback
constraints::init_repeat_count_rollback(state, constraint, group);
return Ok(elected);
}
}
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;
let elected_on_quota;
if cmp_quota_criterion(state.quota.as_ref().unwrap(), count_card, opts) {
// Elected with a quota
elected_on_quota = true;
if state.election.seats == 1 {
state.logger.log_smart(
"{} reaches a majority and is elected.",
"{} reach a majority and are elected.", // This one is impossible
vec![candidate.name.as_str()]
);
} else {
state.logger.log_smart(
"{} meets the quota and is elected.",
"{} meet the quota and are elected.",
vec![candidate.name.as_str()]
);
}
} else {
// Elected with vote required
elected_on_quota = false;
state.logger.log_smart(
"{} meets the vote required and is elected.",
"{} meet the vote required and are elected.",
vec![candidate.name.as_str()]
);
}
if constraints::update_constraints(state, opts) {
// Recheck as some candidates may have been doomed
let mut cmq: 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();
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());
}
if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 || opts.quota_mode == QuotaMode::DynamicByActive {
// Vote required for election may have changed
// ERS97: Check this after every elected candidate (cf. model election)
// ERS76: Check this after every candidate elected on a quota, but all at once for candidates elected on VRE (cf. model election)
if opts.quota_mode == QuotaMode::ERS97 || (opts.quota_mode == QuotaMode::ERS76 && elected_on_quota) || opts.quota_mode == QuotaMode::DynamicByActive {
calculate_quota(state, opts);
// Repeat in case vote required for election has changed
match elect_hopefuls(state, opts, true) {
Ok(_) => { break; }
Err(e) => { return Err(e); }
}
}
} else if opts.early_bulk_elect {
// Vote required for election may have changed for display purposes
update_vre_bulk(state, opts);
}
}
// Determine if early bulk election can be effected
if opts.early_bulk_elect {
if elect_sure_winners(state, opts)? {
return Ok(true);
}
}
return Ok(elected);
}
/// Determine whether the transfer of all surpluses can be deferred
///
/// The value of [STVOptions::defer_surpluses] is not taken into account and must be handled by the caller.
fn can_defer_surpluses<N: Number>(state: &CountState<N>, opts: &STVOptions, total_surpluses: &N) -> bool
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>
{
let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
.collect();
hopefuls.sort_unstable_by(|(_, cc1), (_, cc2)| cc1.votes.cmp(&cc2.votes));
// Surpluses can always be deferred if there is only 1 continuing candidate
if hopefuls.len() <= 1 {
return true;
}
// Do not defer if this could affect a bulk exclusion
if opts.bulk_exclude {
let to_exclude = hopefuls_to_bulk_exclude(state, opts);
let num_to_exclude = to_exclude.len();
if num_to_exclude > 0 {
let total_excluded = state.total_votes_of(to_exclude.into_iter().map(|c| &state.candidates[c]));
if total_surpluses >= &(&hopefuls[num_to_exclude].1.votes - &total_excluded) {
return false;
} else {
return true;
}
}
}
// Do not defer if this could change the last 2 candidates
if total_surpluses >= &(&hopefuls[1].1.votes - &hopefuls[0].1.votes) {
return false;
}
return true;
}
/// Distribute surpluses according to [STVOptions::surplus]
///
/// Returns `true` if any surpluses were distributed.
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
where
for<'r> &'r N: ops::Add<&'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::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>,
{
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG | SurplusMethod::IHare | SurplusMethod::Hare => {
return gregory::distribute_surpluses(state, opts);
}
SurplusMethod::Meek => {
return meek::distribute_surpluses(state, opts);
}
}
}
/// Determine if, with the proposed exclusion of num_to_exclude candidates (if any), a bulk election can be made
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];
// Include doomed candidates here as these are included in num_to_exclude and so will later be subtracted
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded || cc.state == CandidateState::Doomed;
})
.count();
if num_hopefuls - num_to_exclude > 0 && 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<(), STVError> {
let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter()
.filter(|c| {
let cc = &state.candidates[c];
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
})
.collect();
// 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, "Which candidate to elect?")?
} 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.as_str()]
);
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(());
}
/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies
///
/// Returns `true` if any candidates were elected.
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> {
if can_bulk_elect(state, 0) {
state.title = StageKind::BulkElection;
do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.")?;
return Ok(true);
}
return Ok(false);
}
/// Declare all doomed candidates excluded
///
/// Returns `true` if any candidates were excluded.
fn exclude_doomed<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where
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::Div<&'r N, Output=N>,
{
let doomed: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
.filter(|c| state.candidates[c].state == CandidateState::Doomed)
.collect();
if !doomed.is_empty() {
let excluded_candidates;
if opts.bulk_exclude {
excluded_candidates = doomed;
} else {
// Exclude only the lowest-ranked doomed candidate
let min_cands = ties::multiple_min_by(&doomed, |c| &state.candidates[c].votes);
excluded_candidates = if min_cands.len() > 1 {
vec![choose_lowest(state, opts, &min_cands, "Which candidate to exclude?")?]
} else {
vec![min_cands[0]]
};
}
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
state.title = StageKind::ExclusionOf(excluded_candidates.clone());
state.logger.log_smart(
"Doomed candidate, {}, is excluded.",
"Doomed candidates, {}, are excluded.",
names
);
if opts.early_bulk_elect {
// Determine if the proposed exclusion would enable a bulk election
// See comment in exclude_hopefuls as to constraints
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);
}
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);
}
}
exclude_candidates(state, opts, excluded_candidates, "Exclusion")?;
return Ok(true);
}
return Ok(false);
}
/// Determine which continuing candidates have votes equal to or below the minimum threshold
fn hopefuls_below_threshold<'a, N: Number>(state: &CountState<'a, N>, opts: &STVOptions) -> Vec<&'a Candidate> {
let min_threshold = N::parse(&opts.min_threshold);
let excluded_candidates: Vec<&Candidate> = state.candidates.iter()
.filter_map(|(c, cc)|
if cc.state == CandidateState::Hopeful && cc.votes <= min_threshold {
Some(c)
} else {
None
})
.collect();
// Do not exclude if this violates constraints
match constraints::test_constraints_any_time(state, &excluded_candidates, CandidateState::Excluded) {
Ok(_) => { return excluded_candidates; }
Err(_) => { return Vec::new(); } // Bulk exclusion conflicts with constraints
}
}
/// Determine which continuing candidates could be excluded in a bulk exclusion
///
/// The value of [STVOptions::bulk_exclude] is not taken into account and must be handled by the caller.
fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> {
let mut excluded_candidates = Vec::new();
let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
.collect();
// Sort by votes
// NB: Unnecessary to handle ties, as ties will be rejected at "Do not exclude if this could change the order of exclusion"
hopefuls.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
let total_surpluses = state.total_surplus();
// Attempt to exclude as many candidates as possible
for i in 0..hopefuls.len() {
let try_exclude = &hopefuls[0..hopefuls.len()-i];
// Do not exclude if this leaves insufficient candidates
if state.num_elected + hopefuls.len() - try_exclude.len() < state.election.seats {
continue;
}
// Do not exclude if this could change the order of exclusion
let total_votes = state.total_votes_of(try_exclude.iter().map(|(_, cc)| *cc));
if i != 0 && total_votes + &total_surpluses >= hopefuls[hopefuls.len()-i].1.votes {
continue;
}
let try_exclude: Vec<&Candidate> = try_exclude.iter().map(|(c, _)| *c).collect();
// Do not exclude if this violates constraints
match constraints::test_constraints_any_time(state, &try_exclude, CandidateState::Excluded) {
Ok(_) => {}
Err(_) => { break; } // Bulk exclusion conflicts with constraints
}
excluded_candidates.extend(try_exclude);
break;
}
return excluded_candidates;
}
/// Exclude the lowest-ranked hopeful candidate(s)
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<(), STVError>
where
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::Div<&'r N, Output=N>,
{
let mut excluded_candidates: Vec<&Candidate> = Vec::new();
if state.num_excluded == 0 {
if opts.bulk_exclude && opts.min_threshold == "0" {
// Proceed directly to bulk exclusion, as candidates with 0 votes will necessarily be included
} else {
// Exclude candidates below min threshold
excluded_candidates = hopefuls_below_threshold(state, opts);
}
}
// Attempt a bulk exclusion
if excluded_candidates.is_empty() && opts.bulk_exclude {
excluded_candidates = hopefuls_to_bulk_exclude(state, opts);
}
// Exclude lowest ranked candidate
if excluded_candidates.is_empty() {
let hopefuls: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
.filter(|c| state.candidates[c].state == CandidateState::Hopeful)
.collect();
let min_cands = ties::multiple_min_by(&hopefuls, |c| &state.candidates[c].votes);
excluded_candidates = if min_cands.len() > 1 {
vec![choose_lowest(state, opts, &min_cands, "Which candidate to exclude?")?]
} else {
vec![min_cands[0]]
};
}
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
state.title = StageKind::ExclusionOf(excluded_candidates.clone());
if state.election.seats == 1 {
state.logger.log_smart(
"No candidate has a majority, so {} is excluded.",
"No candidate has a majority, so {} are excluded.",
names
);
} else {
state.logger.log_smart(
"No surpluses to distribute, so {} is excluded.",
"No surpluses to distribute, so {} are excluded.",
names
);
}
if opts.early_bulk_elect {
// Determine if the proposed exclusion would enable a bulk election
// This should be OK for constraints, as if the election of the remaining candidates would be invalid, the excluded candidate must necessarily have be guarded already
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);
}
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(());
}
}
exclude_candidates(state, opts, excluded_candidates, "Exclusion")?;
return Ok(());
}
/// Continue the exclusion of a candidate who is being excluded
///
/// Returns `true` if an exclusion was continued.
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where
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::Div<&'r N, Output=N>,
{
// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
let mut excluded_with_votes: Vec<(&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::Excluded && !cc.finalised)
.collect();
if !excluded_with_votes.is_empty() {
excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
let order_excluded = excluded_with_votes[0].1.order_elected;
let excluded_candidates: Vec<&Candidate> = excluded_with_votes.into_iter()
.filter(|(_, cc)| cc.order_elected == order_excluded)
.map(|(c, _)| c)
.collect();
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
state.title = StageKind::ExclusionOf(excluded_candidates.clone());
state.logger.log_smart(
"Continuing exclusion of {}.",
"Continuing exclusion of {}.",
names
);
exclude_candidates(state, opts, excluded_candidates, "Exclusion")?;
return Ok(true);
}
return Ok(false);
}
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>, complete_type: &'static str) -> Result<(), STVError>
where
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::Div<&'r N, Output=N>,
{
match opts.exclusion {
ExclusionMethod::SingleStage => {
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
gregory::exclude_candidates(state, opts, excluded_candidates, complete_type);
}
SurplusMethod::Meek => {
meek::exclude_candidates(state, opts, excluded_candidates);
}
SurplusMethod::IHare | SurplusMethod::Hare => {
sample::exclude_candidates(state, opts, excluded_candidates)?;
}
}
}
ExclusionMethod::ByValue | ExclusionMethod::FirstPreferencesThenByValue | ExclusionMethod::BySource | ExclusionMethod::ParcelsByOrder => {
// Segmented exclusion compatible only with Gregory method
gregory::exclude_candidates(state, opts, excluded_candidates, complete_type);
}
ExclusionMethod::ResetAndReiterate => {
gregory::exclude_candidates_and_reset(state, opts, excluded_candidates);
}
}
return Ok(());
}
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
if state.num_elected >= state.election.seats {
return true;
}
return false;
}
/// Break a tie between the given candidates according to [STVOptions::ties], selecting the highest candidate
///
/// The given candidates are assumed to be tied in this round.
pub fn choose_highest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
for strategy in opts.ties.iter() {
match strategy.choose_highest(state, opts, candidates, prompt_text) {
Ok(c) => {
return Ok(c);
}
Err(e) => {
if let STVError::UnresolvedTie = e {
continue;
} else {
return Err(e);
}
}
}
}
return Err(STVError::UnresolvedTie);
}
/// Break a tie between the given candidates according to [STVOptions::ties], selecting the lowest candidate
///
/// The given candidates are assumed to be tied in this round.
pub fn choose_lowest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
for strategy in opts.ties.iter() {
match strategy.choose_lowest(state, opts, candidates, prompt_text) {
Ok(c) => {
return Ok(c);
}
Err(e) => {
if let STVError::UnresolvedTie = e {
continue;
} else {
return Err(e);
}
}
}
}
return Err(STVError::UnresolvedTie);
}
/// If required, initialise the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
fn init_tiebreaks<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
if !opts.ties.iter().any(|t| t == &TieStrategy::Forwards) && !opts.ties.iter().any(|t| t == &TieStrategy::Backwards) {
return;
}
// Sort candidates in this stage by votes, grouping by ties
let mut sorted_candidates: Vec<(&Candidate, &CountCard<N>)> = state.candidates.iter().collect();
sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
let sorted_candidates: Vec<Vec<(&Candidate, &CountCard<N>)>> = sorted_candidates.into_iter()
.group_by(|(_, cc)| &cc.votes)
.into_iter()
.map(|(_, candidates)| candidates.collect())
.collect();
// Update forwards tie-breaking order
if opts.ties.iter().any(|t| t == &TieStrategy::Forwards) {
let mut hm = CandidateMap::with_capacity(state.candidates.len());
for (i, group) in sorted_candidates.iter().enumerate() {
for (candidate, _) in group.iter() {
hm.insert(candidate, i);
}
}
state.forwards_tiebreak = Some(hm);
}
// Update backwards tie-breaking order
if opts.ties.iter().any(|t| t == &TieStrategy::Backwards) {
let mut hm = CandidateMap::with_capacity(state.candidates.len());
for (i, group) in sorted_candidates.iter().enumerate() {
for (candidate, _) in group.iter() {
hm.insert(candidate, i);
}
}
state.backwards_tiebreak = Some(hm);
}
}
/// If required, update the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
fn update_tiebreaks<N: Number>(state: &mut CountState<N>, _opts: &STVOptions) {
if state.forwards_tiebreak.is_none() && state.backwards_tiebreak.is_none() {
return;
}
// Sort candidates in this stage by votes, grouping by ties
let mut sorted_candidates: Vec<(&Candidate, &CountCard<N>)> = state.candidates.iter().collect();
sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
let sorted_candidates: Vec<Vec<&Candidate>> = sorted_candidates.into_iter()
.group_by(|(_, cc)| &cc.votes)
.into_iter()
.map(|(_, candidates)| candidates.map(|(c, _)| c).collect())
.collect();
// Update forwards tie-breaking order
if let Some(hm) = state.forwards_tiebreak.as_mut() {
// TODO: Check if already completely sorted
let mut sorted_last_round: Vec<(&Candidate, &usize)> = hm.iter().collect();
sorted_last_round.sort_unstable_by(|a, b| a.1.cmp(b.1));
let sorted_last_round: Vec<Vec<&Candidate>> = sorted_last_round.into_iter()
.group_by(|(_, v)| **v)
.into_iter()
.map(|(_, group)| group.map(|(c, _)| c).collect())
.collect();
let mut i: usize = 0;
for mut group in sorted_last_round.into_iter() {
if group.len() == 1 {
hm.insert(group[0], i);
i += 1;
continue;
} else {
// Tied in last round - refer to this round
group.sort_unstable_by(|a, b|
sorted_candidates.iter().position(|x| x.contains(a)).unwrap()
.cmp(&sorted_candidates.iter().position(|x| x.contains(b)).unwrap())
);
let tied_last_round = group.into_iter()
.group_by(|c| sorted_candidates.iter().position(|x| x.contains(c)).unwrap());
for (_, group2) in tied_last_round.into_iter() {
for candidate in group2 {
hm.insert(candidate, i);
}
i += 1;
}
}
}
}
// Update backwards tie-breaking order
if let Some(hm) = state.backwards_tiebreak.as_mut() {
let hm_orig = hm.clone();
let mut i: usize = 0;
for group in sorted_candidates.iter() {
if group.len() == 1 {
hm.insert(group[0], i);
i += 1;
continue;
} else {
// Tied in this round - refer to last round
let mut tied_this_round: Vec<&Candidate> = group.iter().copied().collect();
tied_this_round.sort_unstable_by(|a, b| hm_orig[a].cmp(&hm_orig[b]));
let tied_this_round = tied_this_round.into_iter()
.group_by(|c| hm_orig[c]);
for (_, group2) in tied_this_round.into_iter() {
for candidate in group2 {
hm.insert(candidate, i);
}
i += 1;
}
}
}
}
}
/// Returns `true` if the votes required for election should be displayed, based on the given [STVOptions]
pub fn should_show_vre(opts: &STVOptions) -> bool {
if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 {
return true;
}
if opts.surplus == SurplusMethod::Meek && opts.quota_mode == QuotaMode::DynamicByTotal {
// Meek method ensures that, if the quota is recalculated, every candidate will be elected with a quota
return false;
}
if opts.early_bulk_elect {
return true;
}
return false;
}