OpenTally/src/stv/mod.rs

1884 lines
65 KiB
Rust

/* OpenTally: Open-source election vote counting
* Copyright © 2021–2022 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;
/// Random sample methods of surplus distributions
pub mod sample;
/// WebAssembly wrappers
//#[cfg(target_arch = "wasm32")]
pub mod wasm;
use crate::constraints;
use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, RollbackState, StageKind, Vote};
use crate::sharandom::SHARandom;
use crate::ties::{self, TieStrategy};
use derive_builder::Builder;
use derive_more::Constructor;
use itertools::Itertools;
use nohash_hasher::BuildNoHashHasher;
#[allow(unused_imports)]
use wasm_bindgen::prelude::wasm_bindgen;
use std::collections::HashMap;
use std::fmt;
use std::ops;
/// Options for conducting an STV count
#[derive(Builder, Constructor)]
pub struct STVOptions {
/// Round surplus fractions to specified decimal places
#[builder(default="None")]
pub round_surplus_fractions: Option<usize>,
/// Round ballot values to specified decimal places
#[builder(default="None")]
pub round_values: Option<usize>,
/// Round votes to specified decimal places
#[builder(default="None")]
pub round_votes: Option<usize>,
/// Round quota to specified decimal places
#[builder(default="None")]
pub round_quota: Option<usize>,
/// How to round votes in transfer table
#[builder(default="RoundSubtransfersMode::SingleStep")]
pub round_subtransfers: RoundSubtransfersMode,
/// (Meek STV) Limit for stopping iteration of surplus distribution
#[builder(default=r#"String::from("0.001%")"#)]
pub meek_surplus_tolerance: String,
/// Quota type
#[builder(default="QuotaType::Droop")]
pub quota: QuotaType,
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
#[builder(default="QuotaCriterion::Greater")]
pub quota_criterion: QuotaCriterion,
/// Whether to apply a form of progressive quota
#[builder(default="QuotaMode::Static")]
pub quota_mode: QuotaMode,
/// Tie-breaking method
#[builder(default="vec![TieStrategy::Prompt]")]
pub ties: Vec<TieStrategy>,
/// Method of surplus distributions
#[builder(default="SurplusMethod::WIG")]
pub surplus: SurplusMethod,
/// (Gregory STV) Order to distribute surpluses
#[builder(default="SurplusOrder::BySize")]
pub surplus_order: SurplusOrder,
/// (Gregory STV) Examine only transferable papers during surplus distributions
#[builder(default="false")]
pub transferable_only: bool,
/// (Gregory STV) If --transferable-only, calculate value of transferable papers by subtracting value of non-transferable papers
#[builder(default="false")]
pub subtract_nontransferable: bool,
/// (Gregory STV) Method of exclusions
#[builder(default="ExclusionMethod::SingleStage")]
pub exclusion: ExclusionMethod,
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
#[builder(default="false")]
pub meek_nz_exclusion: bool,
/// (Hare) Method of drawing a sample
#[builder(default="SampleMethod::StratifyLR")]
pub sample: SampleMethod,
/// (Hare) Sample-based methods: Check for candidate election after each individual ballot paper transfer
#[builder(default="false")]
pub sample_per_ballot: bool,
/// Bulk elect as soon as continuing candidates fill all remaining vacancies
#[builder(default="true")]
pub early_bulk_elect: bool,
/// Use bulk exclusion
#[builder(default="false")]
pub bulk_exclude: bool,
/// Defer surplus distributions if possible
#[builder(default="false")]
pub defer_surpluses: bool,
/// Elect candidates on meeting the quota, rather than on surpluses being distributed; (Meek STV) Immediately elect candidates even if keep values have not converged
#[builder(default="true")]
pub immediate_elect: bool,
/// On exclusion, exclude any candidate with this many votes or fewer
#[builder(default="\"0\".to_string()")]
pub min_threshold: String,
/// Path to constraints file (used only for [STVOptions::describe])
#[builder(default="None")]
pub constraints_path: Option<String>,
/// Mode of handling constraints
#[builder(default="ConstraintMode::GuardDoom")]
pub constraint_mode: ConstraintMode,
/// (CLI) Hide excluded candidates from results report
#[builder(default="false")]
pub hide_excluded: bool,
/// (CLI) Sort candidates by votes in results report
#[builder(default="false")]
pub sort_votes: bool,
/// (CLI) Show details of transfers to candidates during surplus distributions/candidate exclusions
#[builder(default="false")]
pub transfers_detail: bool,
/// Print votes to specified decimal places in results report
#[builder(default="2")]
pub pp_decimals: usize,
}
impl STVOptions {
/// Converts the [STVOptions] into CLI argument representation
pub fn describe<N: Number>(&self) -> String {
let mut flags = Vec::new();
let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) };
if self.surplus != SurplusMethod::IHare && self.surplus != SurplusMethod::Hare {
if let Some(dps) = self.round_surplus_fractions { flags.push(format!("--round-surplus-fractions {}", dps)); }
if let Some(dps) = self.round_values { flags.push(format!("--round-values {}", 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.round_subtransfers != RoundSubtransfersMode::SingleStep { flags.push(self.round_subtransfers.describe()); }
if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); }
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); }
if 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 {
if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
if self.transferable_only { flags.push("--transferable-only".to_string()); }
if self.subtract_nontransferable { flags.push("--subtract-nontransferable".to_string()); }
if 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.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare) && self.sample != SampleMethod::StratifyLR { flags.push(self.sample.describe()); }
if (self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare) && self.sample_per_ballot { flags.push("--sample-per-ballot".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.immediate_elect { flags.push("--no-immediate-elect".to_string()); }
if self.min_threshold != "0" { flags.push(format!("--min-threshold {}", self.min_threshold)); }
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.hide_excluded { flags.push("--hide-excluded".to_string()); }
if self.sort_votes { flags.push("--sort-votes".to_string()); }
if self.transfers_detail { flags.push("--transfers-detail".to_string()); }
if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); }
return flags.join(" ");
}
/// Validate the combination of [STVOptions] and error if invalid
pub fn validate(&self) -> Result<(), STVError> {
if self.surplus == SurplusMethod::Meek {
if self.quota_mode == QuotaMode::ERS97 {
// Invalid because keep values cannot be calculated for a candidate elected with less than a surplus
return Err(STVError::InvalidOptions("--surplus meek is incompatible with --quota-mode ers97"));
}
if self.quota_mode == QuotaMode::ERS76 {
// Invalid because keep values cannot be calculated for a candidate elected with less than a surplus
return Err(STVError::InvalidOptions("--surplus meek is incompatible with --quota-mode ers76"));
}
if self.quota_mode == QuotaMode::DynamicByActive {
// Invalid because all votes are "active" in Meek STV
return Err(STVError::InvalidOptions("--surplus meek is incompatible with --quota-mode dynamic_by_active"));
}
if self.transferable_only {
// Invalid because this would imply a different keep value applies to nontransferable ballots (?)
// TODO: NYI?
return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only"));
}
if self.exclusion != ExclusionMethod::SingleStage {
// Invalid because Meek STV is independent of order of exclusion, so segmented exclusion has no impact
return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage"));
}
if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount {
// TODO: NYI?
return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus"));
}
}
if self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare {
if self.round_quota != Some(0) {
// Invalid because votes are counted only in whole numbers
return Err(STVError::InvalidOptions("--surplus ihare and --surplus hare require --round-quota 0"));
}
if self.sample == SampleMethod::StratifyLR && self.sample_per_ballot {
// Invalid because a stratification cannot be made until all relevant ballots are transferred
return Err(STVError::InvalidOptions("--sample stratify is incompatible with --sample-per-ballot"));
}
if self.sample_per_ballot && !self.immediate_elect {
// Invalid because otherwise --sample-per-ballot would be ineffectual
return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect"));
}
if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount {
// TODO: NYI?
return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus"));
}
}
if self.subtract_nontransferable {
if self.surplus != SurplusMethod::WIG {
// Invalid because other methods do not distinguish between ballots of different value during surplus transfer
return Err(STVError::InvalidOptions("--subtract-nontransferable requires --surplus wig"));
}
if !self.transferable_only {
// Invalid because nontransferables are only subtracted with --transferable-only
return Err(STVError::InvalidOptions("--subtract-nontransferable requires --transferable-only"));
}
}
if !self.immediate_elect && self.surplus_order != SurplusOrder::BySize {
// Invalid because there is no other metric to determine which surplus to distribute
return Err(STVError::InvalidOptions("--no-immediate-elect requires --surplus-order by_size"));
}
if self.min_threshold != "0" && self.defer_surpluses {
// TODO: NYI
return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)"));
}
if self.round_subtransfers == RoundSubtransfersMode::ByValueAndSource && self.bulk_exclude {
// TODO: NYI
return Err(STVError::InvalidOptions("--round-subtransfers by_value_and_source is incompatible with --bulk-exclude (not yet implemented)"));
}
return Ok(());
}
}
/// Enum of options for [STVOptions::round_subtransfers]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum RoundSubtransfersMode {
/// Do not round subtransfers (only round final number of votes credited)
SingleStep,
/// Round in subtransfers according to the value when received
ByValue,
/// Round in subtransfers according to the candidate from who each vote was received, and the value when received
ByValueAndSource,
/// Round in subtransfers according to parcel
ByParcel,
/// Sum and round transfers individually for each ballot paper
PerBallot,
}
impl RoundSubtransfersMode {
/// Convert to CLI argument representation
fn describe(self) -> String {
match self {
RoundSubtransfersMode::SingleStep => "--round-subtransfers single_step",
RoundSubtransfersMode::ByValue => "--round-subtransfers by_value",
RoundSubtransfersMode::ByValueAndSource => "--round-subtransfers by_value_and_source",
RoundSubtransfersMode::ByParcel => "--round-subtransfers by_parcel",
RoundSubtransfersMode::PerBallot => "--round-subtransfers per_ballot",
}.to_string()
}
}
impl<S: AsRef<str>> From<S> for RoundSubtransfersMode {
fn from(s: S) -> Self {
match s.as_ref() {
"single_step" => RoundSubtransfersMode::SingleStep,
"by_value" => RoundSubtransfersMode::ByValue,
"by_value_and_source" => RoundSubtransfersMode::ByValueAndSource,
"by_parcel" => RoundSubtransfersMode::ByParcel,
"per_ballot" => RoundSubtransfersMode::PerBallot,
_ => panic!("Invalid --round-subtransfers"),
}
}
}
/// Enum of options for [STVOptions::quota]
#[cfg_attr(feature = "wasm", 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()
}
}
impl<S: AsRef<str>> From<S> for QuotaType {
fn from(s: S) -> Self {
match s.as_ref() {
"droop" => QuotaType::Droop,
"hare" => QuotaType::Hare,
"droop_exact" => QuotaType::DroopExact,
"hare_exact" => QuotaType::HareExact,
_ => panic!("Invalid --quota"),
}
}
}
/// Enum of options for [STVOptions::quota_criterion]
#[cfg_attr(feature = "wasm", 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()
}
}
impl<S: AsRef<str>> From<S> for QuotaCriterion {
fn from(s: S) -> Self {
match s.as_ref() {
"geq" => QuotaCriterion::GreaterOrEqual,
"gt" => QuotaCriterion::Greater,
_ => panic!("Invalid --quota-criterion"),
}
}
}
/// Enum of options for [STVOptions::quota_mode]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum QuotaMode {
/// Static quota
Static,
/// Static quota with ERS97 rules
ERS97,
/// Static quota with ERS76 rules
ERS76,
/// Dynamic quota by total vote
DynamicByTotal,
/// Dynamic quota by active vote
DynamicByActive,
}
impl QuotaMode {
/// Convert to CLI argument representation
fn describe(self) -> String {
match self {
QuotaMode::Static => "--quota-mode static",
QuotaMode::ERS97 => "--quota-mode ers97",
QuotaMode::ERS76 => "--quota-mode ers76",
QuotaMode::DynamicByTotal => "--quota-mode dynamic_by_total",
QuotaMode::DynamicByActive => "--quota-mode dynamic_by_active",
}.to_string()
}
}
impl<S: AsRef<str>> From<S> for QuotaMode {
fn from(s: S) -> Self {
match s.as_ref() {
"static" => QuotaMode::Static,
"ers97" => QuotaMode::ERS97,
"ers76" => QuotaMode::ERS76,
"dynamic_by_total" => QuotaMode::DynamicByTotal,
"dynamic_by_active" => QuotaMode::DynamicByActive,
_ => panic!("Invalid --quota-mode"),
}
}
}
/// Enum of options for [STVOptions::surplus]
#[cfg_attr(feature = "wasm", 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,
/// Inclusive Hare method (random subset)
IHare,
/// (Exclusive) Hare method (random subset)
Hare,
}
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",
SurplusMethod::IHare => "--surplus ihare",
SurplusMethod::Hare => "--surplus hare",
}.to_string()
}
/// Returns `true` if this is a weighted method
pub fn is_weighted(&self) -> bool {
return match self {
SurplusMethod::WIG => { true }
SurplusMethod::UIG | SurplusMethod::EG => { false }
_ => unreachable!()
};
}
}
impl<S: AsRef<str>> From<S> for SurplusMethod {
fn from(s: S) -> Self {
match s.as_ref() {
"wig" => SurplusMethod::WIG,
"uig" => SurplusMethod::UIG,
"eg" => SurplusMethod::EG,
"meek" => SurplusMethod::Meek,
"ihare" | "ih" | "cincinnati" => SurplusMethod::IHare, // Inclusive Hare method used to be erroneously referred to as "Cincinnati" method - accept for backwards compatibility
"hare" | "eh" => SurplusMethod::Hare,
_ => panic!("Invalid --surplus"),
}
}
}
/// Enum of options for [STVOptions::surplus_order]
#[cfg_attr(feature = "wasm", 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()
}
}
impl<S: AsRef<str>> From<S> for SurplusOrder {
fn from(s: S) -> Self {
match s.as_ref() {
"by_size" => SurplusOrder::BySize,
"by_order" => SurplusOrder::ByOrder,
_ => panic!("Invalid --surplus-order"),
}
}
}
/// Enum of options for [STVOptions::exclusion]
#[cfg_attr(feature = "wasm", 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 according to the candidate who transferred the papers to the excluded candidate, in the order the transferring candidates were elected or excluded
BySource,
/// 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::BySource => "--exclusion by_source",
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
ExclusionMethod::Wright => "--exclusion wright",
}.to_string()
}
}
impl<S: AsRef<str>> From<S> for ExclusionMethod {
fn from(s: S) -> Self {
match s.as_ref() {
"single_stage" => ExclusionMethod::SingleStage,
"by_value" => ExclusionMethod::ByValue,
"by_source" => ExclusionMethod::BySource,
"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
"wright" => ExclusionMethod::Wright,
_ => panic!("Invalid --exclusion"),
}
}
}
/// Enum of options for [STVOptions::sample]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum SampleMethod {
/// Stratify the ballots into parcels according to next available preference and transfer the last ballots from each parcel; round fractions according to largest remainders
StratifyLR,
// Stratify the ballots into parcels according to next available preference and transfer the last ballots from each parcel; disregard fractions
//StratifyFloor,
/// Transfer the last ballots
ByOrder,
/// Transfer every n-th ballot, Cincinnati style
Cincinnati,
}
impl SampleMethod {
/// Convert to CLI argument representation
fn describe(self) -> String {
match self {
SampleMethod::StratifyLR => "--sample stratify",
//SampleMethod::StratifyFloor => "--sample stratify_floor",
SampleMethod::ByOrder => "--sample by_order",
SampleMethod::Cincinnati => "--sample cincinnati",
}.to_string()
}
}
impl<S: AsRef<str>> From<S> for SampleMethod {
fn from(s: S) -> Self {
match s.as_ref() {
"stratify" | "stratify_lr" => SampleMethod::StratifyLR,
//"stratify_floor" => SampleMethod::StratifyFloor,
"by_order" => SampleMethod::ByOrder,
"cincinnati" | "nth_ballot" => SampleMethod::Cincinnati,
_ => panic!("Invalid --sample-method"),
}
}
}
/// 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,
/// If constraints violated, exclude/reintroduce candidates as required and redistribute ballot papers
RepeatCount,
}
impl ConstraintMode {
/// Convert to CLI argument representation
fn describe(self) -> String {
match self {
ConstraintMode::GuardDoom => "--constraint-mode guard_doom",
ConstraintMode::RepeatCount => "--constraint-mode repeat_count",
}.to_string()
}
}
impl<S: AsRef<str>> From<S> for ConstraintMode {
fn from(s: S) -> Self {
match s.as_ref() {
"guard_doom" => ConstraintMode::GuardDoom,
"repeat_count" => ConstraintMode::RepeatCount,
_ => panic!("Invalid --constraint-mode"),
}
}
}
/// 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: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>, BuildNoHashHasher<Candidate>>,
/// [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: HashMap::with_capacity_and_hasher(state.election.candidates.len(), BuildNoHashHasher::default()),
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.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();
}
}
_ => {
acc += &cc.votes;
}
}
acc
});
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.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();
}
}
_ => {
acc += &cc.votes;
}
}
acc
});
//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.candidates.values().fold(N::new(), |mut acc, cc| { acc += &cc.votes; acc });
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.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();
}
}
_ => {
acc += &cc.votes;
}
}
acc
});
log.push_str(format!("Active vote is {:.dps$}, so the quota is 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.candidates.values().fold(N::new(), |mut acc, cc| { acc += &cc.votes; acc });
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 += hopefuls.iter().take(num_vacancies).fold(N::new(), |mut acc, (_, cc)| {
if &cc.votes > state.quota.as_ref().unwrap() {
acc += &cc.votes; acc -= state.quota.as_ref().unwrap();
}
acc
});
// For trailing candidates, count all votes
total_trailing += hopefuls.iter().skip(num_vacancies).fold(N::new(), |mut acc, (_, cc)| { acc += &cc.votes; acc });
// 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;
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>
{
// Do not defer if this could change the last 2 candidates
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
.collect();
if hopefuls.len() < 2 {
return true;
}
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(), |mut acc, c| { acc += &state.candidates[c].votes; acc });
if total_surpluses >= &(&hopefuls[num_to_exclude].1.votes - &total_excluded) {
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.candidates.iter()
.filter(|(_, cc)| !cc.finalised && &cc.votes > state.quota.as_ref().unwrap())
.fold(N::new(), |mut acc, (_, cc)| { acc += &cc.votes; acc -= state.quota.as_ref().unwrap(); acc });
// 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.iter().fold(N::new(), |mut acc, (_, cc)| { acc += &cc.votes; acc });
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());
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::BySource | ExclusionMethod::ParcelsByOrder => {
// Exclusion in parts compatible only with Gregory method
gregory::exclude_candidates(state, opts, excluded_candidates, complete_type);
}
ExclusionMethod::Wright => {
gregory::wright_exclude_candidates(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: HashMap<&Candidate, usize, BuildNoHashHasher<Candidate>> = HashMap::with_capacity_and_hasher(state.candidates.len(), BuildNoHashHasher::default());
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, BuildNoHashHasher<Candidate>> = HashMap::with_capacity_and_hasher(state.candidates.len(), BuildNoHashHasher::default());
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;
}