/* OpenTally: Open-source election vote counting * Copyright © 2021 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 . */ #![allow(mutable_borrow_reservation_conflict)] /// Gregory method of surplus distributions pub mod gregory; /// Meek method of surplus distributions, etc. pub mod meek; /// WebAssembly wrappers //#[cfg(target_arch = "wasm32")] pub mod wasm; use crate::numbers::Number; use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, Vote}; use crate::sharandom::SHARandom; use crate::ties::TieStrategy; use itertools::Itertools; use ndarray::Dimension; use wasm_bindgen::prelude::wasm_bindgen; use std::collections::HashMap; use std::ops; /// Options for conducting an STV count pub struct STVOptions { /// Round transfer values to specified decimal places pub round_tvs: Option, /// Round ballot weights to specified decimal places pub round_weights: Option, /// Round votes to specified decimal places pub round_votes: Option, /// Round quota to specified decimal places pub round_quota: Option, /// How to calculate votes to credit to candidates in surplus transfers pub sum_surplus_transfers: SumSurplusTransfersMode, /// (Meek STV) Limit for stopping iteration of surplus distribution pub meek_surplus_tolerance: String, /// Convert ballots with value >1 to multiple ballots of value 1 (used only for [STVOptions::describe]) pub normalise_ballots: bool, /// Quota type pub quota: QuotaType, /// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota pub quota_criterion: QuotaCriterion, /// Whether to apply a form of progressive quota pub quota_mode: QuotaMode, /// Tie-breaking method pub ties: Vec, /// Method of surplus distributions pub surplus: SurplusMethod, /// Order to distribute surpluses pub surplus_order: SurplusOrder, /// Examine only transferable papers during surplus distributions pub transferable_only: bool, /// Method of exclusions pub exclusion: ExclusionMethod, /// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion pub meek_nz_exclusion: bool, /// Bulk elect as soon as continuing candidates fill all remaining vacancies pub early_bulk_elect: bool, /// Use bulk exclusion pub bulk_exclude: bool, /// Defer surplus distributions if possible pub defer_surpluses: bool, /// (Meek STV) Immediately elect candidates even if keep values have not converged pub meek_immediate_elect: bool, /// Path to constraints file (used only for [STVOptions::describe]) pub constraints_path: Option, /// Mode of handling constraints pub constraint_mode: ConstraintMode, /// Print votes to specified decimal places in results report pub pp_decimals: usize, } impl STVOptions { /// Returns a new [STVOptions] based on arguments given as strings pub fn new( round_tvs: Option, round_weights: Option, round_votes: Option, round_quota: Option, sum_surplus_transfers: &str, meek_surplus_tolerance: &str, normalise_ballots: bool, quota: &str, quota_criterion: &str, quota_mode: &str, ties: &Vec, random_seed: &Option, surplus: &str, surplus_order: &str, transferable_only: bool, exclusion: &str, meek_nz_exclusion: bool, early_bulk_elect: bool, bulk_exclude: bool, defer_surpluses: bool, meek_immediate_elect: bool, constraints_path: Option<&str>, constraint_mode: &str, pp_decimals: usize, ) -> Self { return STVOptions { round_tvs, round_weights, round_votes, round_quota, sum_surplus_transfers: match sum_surplus_transfers { "single_step" => SumSurplusTransfersMode::SingleStep, "by_value" => SumSurplusTransfersMode::ByValue, "per_ballot" => SumSurplusTransfersMode::PerBallot, _ => panic!("Invalid --sum-transfers"), }, meek_surplus_tolerance: meek_surplus_tolerance.to_string(), normalise_ballots, quota: match quota { "droop" => QuotaType::Droop, "hare" => QuotaType::Hare, "droop_exact" => QuotaType::DroopExact, "hare_exact" => QuotaType::HareExact, _ => panic!("Invalid --quota"), }, quota_criterion: match quota_criterion { "geq" => QuotaCriterion::GreaterOrEqual, "gt" => QuotaCriterion::Greater, _ => panic!("Invalid --quota-criterion"), }, quota_mode: match quota_mode { "static" => QuotaMode::Static, "ers97" => QuotaMode::ERS97, _ => panic!("Invalid --quota-mode"), }, ties: ties.into_iter().map(|t| match t.as_str() { "forwards" => TieStrategy::Forwards, "backwards" => TieStrategy::Backwards, "random" => TieStrategy::Random(random_seed.as_ref().expect("Must provide a --random-seed if using --ties random").clone()), "prompt" => TieStrategy::Prompt, _ => panic!("Invalid --ties"), }).collect(), surplus: match surplus { "wig" => SurplusMethod::WIG, "uig" => SurplusMethod::UIG, "eg" => SurplusMethod::EG, "meek" => SurplusMethod::Meek, _ => panic!("Invalid --surplus"), }, surplus_order: match surplus_order { "by_size" => SurplusOrder::BySize, "by_order" => SurplusOrder::ByOrder, _ => panic!("Invalid --surplus-order"), }, transferable_only, exclusion: match exclusion { "single_stage" => ExclusionMethod::SingleStage, "by_value" => ExclusionMethod::ByValue, "parcels_by_order" => ExclusionMethod::ParcelsByOrder, "wright" => ExclusionMethod::Wright, _ => panic!("Invalid --exclusion"), }, meek_nz_exclusion, early_bulk_elect, bulk_exclude, defer_surpluses, meek_immediate_elect, constraints_path: match constraints_path { Some(p) => Some(p.to_string()), None => None, }, constraint_mode: match constraint_mode { "guard_doom" => ConstraintMode::GuardDoom, "rollback" => ConstraintMode::Rollback, _ => panic!("Invalid --constraint-mode"), }, pp_decimals, }; } /// Converts the [STVOptions] into CLI argument representation pub fn describe(&self) -> String { let mut flags = Vec::new(); let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) }; if let Some(dps) = self.round_tvs { flags.push(format!("--round-tvs {}", dps)); } if let Some(dps) = self.round_weights { flags.push(format!("--round-weights {}", dps)); } if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); } if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); } if self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.describe()); } if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); } if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); } if self.quota != QuotaType::DroopExact { flags.push(self.quota.describe()); } if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); } if self.surplus != SurplusMethod::Meek && self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); } let ties_str = self.ties.iter().map(|t| t.describe()).join(" "); if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); } for t in self.ties.iter() { if let TieStrategy::Random(seed) = t { flags.push(format!("--random-seed {}", seed)); } } if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); } if self.surplus != SurplusMethod::Meek && self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); } if self.surplus != SurplusMethod::Meek && self.transferable_only { flags.push("--transferable-only".to_string()); } if self.surplus != SurplusMethod::Meek && self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); } if self.surplus == SurplusMethod::Meek && self.meek_nz_exclusion { flags.push("--meek-nz-exclusion".to_string()); } if !self.early_bulk_elect { flags.push("--no-early-bulk-elect".to_string()); } if self.bulk_exclude { flags.push("--bulk-exclude".to_string()); } if self.defer_surpluses { flags.push("--defer-surpluses".to_string()); } if self.surplus == SurplusMethod::Meek && self.meek_immediate_elect { flags.push("--meek-immediate-elect".to_string()); } if let Some(path) = &self.constraints_path { flags.push(format!("--constraints {}", path)); if self.constraint_mode != ConstraintMode::GuardDoom { flags.push(self.constraint_mode.describe()); } } if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); } return flags.join(" "); } /// Validate the combination of [STVOptions] and panic if invalid pub fn validate(&self) { if self.surplus == SurplusMethod::Meek { if self.transferable_only { panic!("--surplus meek is incompatible with --transferable-only"); } if self.exclusion != ExclusionMethod::SingleStage { panic!("--surplus meek requires --exclusion single_stage"); } } } } /// Enum of options for [STVOptions::sum_surplus_transfers] #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SumSurplusTransfersMode { /// Sum and round all surplus transfers for a candidate in a single step SingleStep, /// Sum and round a candidate's surplus transfers separately for ballot papers received at each particular value ByValue, /// Sum and round a candidate's surplus transfers individually for each ballot paper PerBallot, } impl SumSurplusTransfersMode { /// Convert to CLI argument representation fn describe(self) -> String { match self { SumSurplusTransfersMode::SingleStep => "--sum-surplus-transfers single_step", SumSurplusTransfersMode::ByValue => "--sum-surplus-transfers by_value", SumSurplusTransfersMode::PerBallot => "--sum-surplus-transfers per_ballot", }.to_string() } } /// Enum of options for [STVOptions::quota] #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum QuotaType { /// Droop quota Droop, /// Hare quota Hare, /// Exact Droop quota (Newland–Britton/Hagenbach-Bischoff quota) DroopExact, /// Exact Hare quota HareExact, } impl QuotaType { /// Convert to CLI argument representation fn describe(self) -> String { match self { QuotaType::Droop => "--quota droop", QuotaType::Hare => "--quota hare", QuotaType::DroopExact => "--quota droop_exact", QuotaType::HareExact => "--quota hare_exact", }.to_string() } } /// Enum of options for [STVOptions::quota_criterion] #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum QuotaCriterion { /// Elect candidates on equalling or exceeding the quota GreaterOrEqual, /// Elect candidates on strictly exceeding the quota Greater, } impl QuotaCriterion { /// Convert to CLI argument representation fn describe(self) -> String { match self { QuotaCriterion::GreaterOrEqual => "--quota-criterion geq", QuotaCriterion::Greater => "--quota-criterion gt", }.to_string() } } /// Enum of options for [STVOptions::quota_mode] #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum QuotaMode { /// Static quota Static, /// Static quota with ERS97 rules ERS97, } impl QuotaMode { /// Convert to CLI argument representation fn describe(self) -> String { match self { QuotaMode::Static => "--quota-mode static", QuotaMode::ERS97 => "--quota-mode ers97", }.to_string() } } /// Enum of options for [STVOptions::surplus] #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SurplusMethod { /// Weighted inclusive Gregory method WIG, /// Unweighted inclusive Gregory method UIG, /// Exclusive Gregory method (last bundle) EG, /// Meek method Meek, } impl SurplusMethod { /// Convert to CLI argument representation fn describe(self) -> String { match self { SurplusMethod::WIG => "--surplus wig", SurplusMethod::UIG => "--surplus uig", SurplusMethod::EG => "--surplus eg", SurplusMethod::Meek => "--surplus meek", }.to_string() } } /// Enum of options for [STVOptions::surplus_order] #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SurplusOrder { /// Transfer the largest surplus first, even if it arose at a later stage of the count BySize, /// Transfer the surplus of the candidate elected first, even if it is smaller than another ByOrder, } impl SurplusOrder { /// Convert to CLI argument representation fn describe(self) -> String { match self { SurplusOrder::BySize => "--surplus-order by_size", SurplusOrder::ByOrder => "--surplus-order by_order", }.to_string() } } /// Enum of options for [STVOptions::exclusion] #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum ExclusionMethod { /// Transfer all ballot papers of an excluded candidate in one stage SingleStage, /// Transfer the ballot papers of an excluded candidate in descending order of accumulated transfer value ByValue, /// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received ParcelsByOrder, /// Wright method (re-iterate) Wright, } impl ExclusionMethod { /// Convert to CLI argument representation fn describe(self) -> String { match self { ExclusionMethod::SingleStage => "--exclusion single_stage", ExclusionMethod::ByValue => "--exclusion by_value", ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order", ExclusionMethod::Wright => "--exclusion wright", }.to_string() } } /// Enum of options for [STVOptions::constraint_mode] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum ConstraintMode { /// Guard or doom candidates as soon as required to secure a conformant result GuardDoom, /// TODO: NYI Rollback, } impl ConstraintMode { /// Convert to CLI argument representation fn describe(self) -> String { match self { ConstraintMode::GuardDoom => "--constraint-mode guard_doom", ConstraintMode::Rollback => "--constraint-mode rollback", }.to_string() } } /// An error during the STV count #[wasm_bindgen] #[derive(Debug)] pub enum STVError { /// User input is required RequireInput, /// Tie could not be resolved UnresolvedTie, } /// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions) 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)); } } distribute_first_preferences(&mut state, opts); calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); init_tiebreaks(&mut state, opts); } /// Perform a single stage of the STV count pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result 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>, for<'r> &'r N: ops::Neg, { state.logger.entries.clear(); state.step_all(); // Finish count if finished_before_stage(&state) { return Ok(true); } // Attempt early bulk election if opts.early_bulk_elect { if bulk_elect(&mut state, &opts)? { return Ok(false); } } // Continue exclusions if continue_exclusion(&mut state, &opts) { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); update_tiebreaks(&mut state, opts); return Ok(false); } // Exclude doomed candidates if exclude_doomed(&mut state, &opts)? { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); update_tiebreaks(&mut state, opts); return Ok(false); } // Distribute surpluses if distribute_surpluses(&mut state, &opts)? { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); update_tiebreaks(&mut state, opts); return Ok(false); } // Attempt late bulk election if bulk_elect(&mut state, &opts)? { return Ok(false); } // Exclude lowest hopeful if exclude_hopefuls(&mut state, &opts)? { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); update_tiebreaks(&mut state, opts); return Ok(false); } panic!("Count incomplete but unable to proceed"); } /// See [next_preferences] struct NextPreferencesResult<'a, N> { candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, exhausted: NextPreferencesEntry<'a, N>, total_ballots: N, total_votes: N, } /// See [next_preferences] struct NextPreferencesEntry<'a, N> { //count_card: Option<&'a CountCard<'a, N>>, votes: Vec>, num_ballots: N, num_votes: N, } /// Count the given votes, grouping according to next available preference fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec>) -> NextPreferencesResult<'a, N> { let mut result = NextPreferencesResult { candidates: HashMap::new(), exhausted: NextPreferencesEntry { votes: Vec::new(), num_ballots: N::new(), num_votes: N::new(), }, total_ballots: N::new(), total_votes: N::new(), }; for mut vote in votes.into_iter() { result.total_ballots += &vote.ballot.orig_value; result.total_votes += &vote.value; let mut next_candidate = None; for (i, preference) in vote.ballot.preferences.iter().enumerate().skip(vote.up_to_pref) { let candidate = &state.election.candidates[*preference]; let count_card = state.candidates.get(candidate).unwrap(); if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state { next_candidate = Some(candidate); vote.up_to_pref = i + 1; break; } } // Have to structure like this to satisfy Rust's borrow checker if let Some(candidate) = next_candidate { if result.candidates.contains_key(candidate) { let entry = result.candidates.get_mut(candidate).unwrap(); entry.num_ballots += &vote.ballot.orig_value; entry.num_votes += &vote.value; entry.votes.push(vote); } else { let entry = NextPreferencesEntry { num_ballots: vote.ballot.orig_value.clone(), num_votes: vote.value.clone(), votes: vec![vote], }; result.candidates.insert(candidate, entry); } } else { result.exhausted.num_ballots += &vote.ballot.orig_value; result.exhausted.num_votes += &vote.value; result.exhausted.votes.push(vote); } } return result; } /// Distribute first preference votes fn distribute_first_preferences(state: &mut CountState, 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 => { gregory::distribute_first_preferences(state); } SurplusMethod::Meek => { meek::distribute_first_preferences(state, opts); } } } /// Calculate the quota, given the total vote, according to [STVOptions::quota] fn total_to_quota(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; } /// Calculate the quota according to [STVOptions::quota] fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // Calculate quota if state.quota.is_none() || opts.surplus == SurplusMethod::Meek { let mut log = String::new(); // Calculate the total vote let total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); 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); } if let QuotaMode::ERS97 = opts.quota_mode { // ERS97 rules // ------------------------- // Reduce quota if allowable if state.num_elected == 0 { let mut log = String::new(); // Calculate the total vote let total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); 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 "a < 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 { 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(); } } } else { // No ERS97 rules if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek { state.vote_required_election = state.quota.clone(); } } } /// Determine if the given candidate meets the quota, according to [STVOptions::quota_criterion] fn meets_quota(quota: &N, count_card: &CountCard, opts: &STVOptions) -> bool { match opts.quota_criterion { QuotaCriterion::GreaterOrEqual => { return count_card.votes >= *quota; } QuotaCriterion::Greater => { return count_card.votes > *quota; } } } /// Declare elected all candidates meeting the quota fn elect_meeting_quota(state: &mut CountState, opts: &STVOptions) -> bool { let vote_req = state.vote_required_election.as_ref().unwrap().clone(); // Have to do this or else the borrow checker gets confused let mut cands_meeting_quota: Vec<&Candidate> = state.election.candidates.iter() .filter(|c| { let cc = state.candidates.get(c).unwrap(); return (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_quota(&vote_req, cc, opts); }) .collect(); // Sort by votes cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.cmp(&state.candidates.get(b).unwrap().votes)); let elected = !cands_meeting_quota.is_empty(); while !cands_meeting_quota.is_empty() { // Declare elected in descending order of votes let candidate = cands_meeting_quota.pop().unwrap(); 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( "{} meets the quota and is elected.", "{} meet the quota and are elected.", vec![&candidate.name] ); if update_constraints(state, opts) { // Recheck as some candidates may have been doomed cands_meeting_quota = state.election.candidates.iter() .filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::Hopeful && meets_quota(&vote_req, cc, opts) }) .collect(); cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.cmp(&state.candidates.get(b).unwrap().votes)); } if opts.quota_mode == QuotaMode::ERS97 { // Vote required for election may have changed calculate_quota(state, opts); } if opts.quota_mode == QuotaMode::ERS97 { // Repeat in case vote required for election has changed elect_meeting_quota(state, opts); } } return elected; } fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election, candidates: &HashMap<&Candidate, CountCard>, idx: &[usize]) -> Vec<&'a Candidate> { let mut result: Vec<&Candidate> = Vec::new(); for (i, candidate) in election.candidates.iter().enumerate() { let cc = candidates.get(candidate).unwrap(); if cc.state != CandidateState::Hopeful { continue; } // Is this candidate within this constraint cell? let mut matches = true; for (coord, constraint) in idx.iter().zip(election.constraints.as_ref().unwrap().0.iter()) { let group = &constraint.groups[coord - 1]; // The group referred to by this constraint cell if !group.candidates.contains(&i) { matches = false; break; } } if matches { result.push(candidate); } } return result; } fn update_constraints(state: &mut CountState, opts: &STVOptions) -> bool { if state.constraint_matrix.is_none() { return false; } let cm = state.constraint_matrix.as_mut().unwrap(); // Update cands/elected cm.update_from_state(&state.election, &state.candidates); cm.recount_cands(); // Iterate for stable state //println!("{}", cm); while !cm.step().expect("No conformant result") { //println!("{}", cm); } //println!("{}", cm); // TODO: Refactor and move this to constraints module? match opts.constraint_mode { ConstraintMode::GuardDoom => { // Check for guarded or doomed candidates let mut guarded_or_doomed = false; for idx in ndarray::indices(cm.0.shape()) { if (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc }) != 0 { continue; } let cell = &cm.0[&idx]; if cell.elected == cell.max { // Doom remaining candidates in this cell let doomed = candidates_in_constraint_cell(state.election, &state.candidates, idx.slice()); if !doomed.is_empty() { for candidate in doomed.iter() { state.candidates.get_mut(candidate).unwrap().state = CandidateState::Doomed; } state.logger.log_smart( "{} must be doomed to comply with constraints.", "{} must be doomed to comply with constraints.", doomed.iter().map(|c| c.name.as_str()).collect() ); guarded_or_doomed = true; } } if cell.cands == cell.min { // Guard remaining candidates in this cell let guarded = candidates_in_constraint_cell(state.election, &state.candidates, idx.slice()); if !guarded.is_empty() { for candidate in guarded.iter() { state.candidates.get_mut(candidate).unwrap().state = CandidateState::Guarded; } state.logger.log_smart( "{} must be guarded to comply with constraints.", "{} must be guarded to comply with constraints.", guarded.iter().map(|c| c.name.as_str()).collect() ); guarded_or_doomed = true; } } } return guarded_or_doomed; } _ => { todo!() } } //return false; } /// 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(state: &CountState, opts: &STVOptions, total_surpluses: &N) -> bool where for<'r> &'r N: ops::Sub<&'r N, Output=N> { // Do not defer if this could change the last 2 candidates let mut hopefuls: Vec<(&&Candidate, &CountCard)> = 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)); if total_surpluses > &(&hopefuls[1].1.votes - &hopefuls[0].1.votes) { return false; } // 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 = to_exclude.into_iter() .fold(N::new(), |acc, c| acc + &state.candidates.get(c).unwrap().votes); if total_surpluses > &(&hopefuls[num_to_exclude + 1].1.votes - &total_excluded) { return false; } } } return true; } /// Distribute surpluses according to [STVOptions::surplus] fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> Result 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>, for<'r> &'r N: ops::Neg, { match opts.surplus { SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { return gregory::distribute_surpluses(state, opts); } SurplusMethod::Meek => { return meek::distribute_surpluses(state, opts); } } } /// Declare all continuing candidates elected, if the number equals the number of remaining vacancies fn bulk_elect(state: &mut CountState, opts: &STVOptions) -> Result { if state.election.candidates.len() - state.num_excluded <= state.election.seats { state.kind = None; state.title = "Bulk election".to_string(); // Bulk elect all remaining candidates let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter() .filter(|c| { let cc = state.candidates.get(c).unwrap(); return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded; }) .collect(); while !hopefuls.is_empty() { let max_votes = hopefuls.iter() .max_by(|a, b| state.candidates.get(**a).unwrap().votes.cmp(&state.candidates.get(**b).unwrap().votes)) .unwrap(); let max_votes = &state.candidates.get(max_votes).unwrap().votes; let max_hopefuls: Vec<&Candidate> = hopefuls.iter() .filter(|c| &state.candidates.get(**c).unwrap().votes == max_votes) .map(|c| *c) .collect(); let candidate; if max_hopefuls.len() > 1 { // Handle ties candidate = choose_highest(state, opts, max_hopefuls)?; } else { candidate = max_hopefuls[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( "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.", vec![&candidate.name] ); hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap()); update_constraints(state, opts); } return Ok(true); } return Ok(false); } fn exclude_doomed<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result 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 doomed: Vec<(&Candidate, &CountCard)> = state.election.candidates.iter() // Present in order in case of tie .map(|c| (c, state.candidates.get(c).unwrap())) .filter(|(_, cc)| cc.state == CandidateState::Doomed) .collect(); if !doomed.is_empty() { let excluded_candidates; if opts.bulk_exclude { excluded_candidates = doomed.into_iter().map(|(c, _)| c).collect(); } else { // Exclude only the lowest-ranked doomed candidate // Sort by votes doomed.sort_by(|a, b| a.1.votes.cmp(&b.1.votes)); // Handle ties if doomed.len() > 1 && doomed[0].1.votes == doomed[1].1.votes { let min_votes = &doomed[0].1.votes; let doomed = doomed.into_iter().filter_map(|(c, cc)| if &cc.votes == min_votes { Some(c) } else { None }).collect(); excluded_candidates = vec![choose_lowest(state, opts, doomed)?]; } else { excluded_candidates = vec![&doomed[0].0]; } } let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect(); state.kind = Some("Exclusion of"); state.title = names.join(", "); state.logger.log_smart( "Doomed candidate, {}, is excluded.", "Doomed candidates, {}, are excluded.", names ); exclude_candidates(state, opts, excluded_candidates); return Ok(true); } return Ok(false); } /// 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)> = 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.candidates.iter() .filter(|(_, cc)| &cc.votes > state.quota.as_ref().unwrap()) .fold(N::new(), |agg, (_, cc)| agg + &cc.votes - state.quota.as_ref().unwrap()); // 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 = try_exclude.into_iter().fold(N::new(), |agg, (_, cc)| agg + &cc.votes); if i != 0 && total_votes + &total_surpluses >= hopefuls[hopefuls.len()-i].1.votes { continue; } for (c, _) in try_exclude.into_iter() { excluded_candidates.push(**c); } 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 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(); // Attempt a bulk exclusion if opts.bulk_exclude { excluded_candidates = hopefuls_to_bulk_exclude(state, opts); } // Exclude lowest ranked candidate if excluded_candidates.is_empty() { let mut hopefuls: Vec<(&Candidate, &CountCard)> = state.election.candidates.iter() // Present in order in case of tie .map(|c| (c, state.candidates.get(c).unwrap())) .filter(|(_, cc)| cc.state == CandidateState::Hopeful) .collect(); // Sort by votes hopefuls.sort_by(|a, b| a.1.votes.cmp(&b.1.votes)); // Handle ties if hopefuls.len() > 1 && hopefuls[0].1.votes == hopefuls[1].1.votes { let min_votes = &hopefuls[0].1.votes; let hopefuls = hopefuls.into_iter().filter_map(|(c, cc)| if &cc.votes == min_votes { Some(c) } else { None }).collect(); excluded_candidates = vec![choose_lowest(state, opts, hopefuls)?]; } else { excluded_candidates = vec![&hopefuls[0].0]; } } let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect(); state.kind = Some("Exclusion of"); state.title = names.join(", "); state.logger.log_smart( "No surpluses to distribute, so {} is excluded.", "No surpluses to distribute, so {} are excluded.", names ); exclude_candidates(state, opts, excluded_candidates); return Ok(true); } /// Continue the exclusion of a candidate who is being excluded fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool 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)> = state.candidates.iter() .filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| !p.is_empty())) .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 mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect(); names.sort(); state.kind = Some("Exclusion of"); state.title = names.join(", "); state.logger.log_smart( "Continuing exclusion of {}.", "Continuing exclusion of {}.", names ); exclude_candidates(state, opts, excluded_candidates); return true; } return false; } /// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion] fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) 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); } SurplusMethod::Meek => { meek::exclude_candidates(state, opts, excluded_candidates); } } } ExclusionMethod::ByValue | ExclusionMethod::ParcelsByOrder => { // Exclusion in parts compatible only with Gregory method gregory::exclude_candidates(state, opts, excluded_candidates); } ExclusionMethod::Wright => { gregory::wright_exclude_candidates(state, opts, excluded_candidates); } } } /// Determine if the count is complete because the number of elected candidates equals the number of vacancies fn finished_before_stage(state: &CountState) -> 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 fn choose_highest<'c, N: Number>(state: &mut CountState, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { for strategy in opts.ties.iter() { match strategy.choose_highest(state, &candidates) { Ok(c) => { return Ok(c); } Err(e) => { if let STVError::UnresolvedTie = e { continue; } else { return Err(e); } } } } panic!("Unable to resolve tie"); } /// 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 fn choose_lowest<'c, N: Number>(state: &mut CountState, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { for strategy in opts.ties.iter() { match strategy.choose_lowest(state, &candidates) { Ok(c) => { return Ok(c); } Err(e) => { if let STVError::UnresolvedTie = e { continue; } else { return Err(e); } } } } panic!("Unable to resolve tie"); } /// If required, initialise the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties] fn init_tiebreaks(state: &mut CountState, 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)> = state.candidates.iter().collect(); sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); let sorted_candidates: Vec)>> = 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: HashMap<&Candidate, usize> = HashMap::new(); 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: HashMap<&Candidate, usize> = HashMap::new(); 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(state: &mut CountState, _opts: &STVOptions) { if let None = state.forwards_tiebreak { if let None = state.backwards_tiebreak { return; } } // Sort candidates in this stage by votes, grouping by ties let mut sorted_candidates: Vec<(&&Candidate, &CountCard)> = state.candidates.iter().collect(); sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); let sorted_candidates: Vec> = 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> = 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.into_iter().map(|c| *c).collect(); tied_this_round.sort_unstable_by(|a, b| hm_orig.get(a).unwrap().cmp(hm_orig.get(b).unwrap())); let tied_this_round = tied_this_round.into_iter() .group_by(|c| hm_orig.get(c).unwrap()); for (_, group2) in tied_this_round.into_iter() { for candidate in group2 { hm.insert(candidate, i); } i += 1; } } } } }