/* 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 . */ use crate::constraints::{Constraints, ConstraintMatrix}; use crate::logger::Logger; use crate::numbers::Number; use crate::sharandom::SHARandom; use crate::stv::{QuotaMode, STVOptions}; #[cfg(not(target_arch = "wasm32"))] use rkyv::{Archive, Archived, Deserialize, Fallible, Resolver, Serialize, with::{ArchiveWith, DeserializeWith, SerializeWith}}; use std::cmp::max; use std::collections::HashMap; /// An election to be counted #[derive(Clone)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct Election { /// Name of the election pub name: String, /// Number of candidates to be elected pub seats: usize, /// [Vec] of [Candidate]s in the election pub candidates: Vec, /// Indexes of withdrawn candidates pub withdrawn_candidates: Vec, /// [Vec] of [Ballot]s cast in the election pub ballots: Vec>, /// Constraints on candidates pub constraints: Option, } impl Election { /// Convert ballots with weight >1 to multiple ballots of weight 1 /// /// Assumes ballots have integer weight. pub fn normalise_ballots(&mut self) { let mut normalised_ballots = Vec::new(); for ballot in self.ballots.iter() { let mut n = N::new(); let one = N::one(); while n < ballot.orig_value { let new_ballot = Ballot { orig_value: N::one(), preferences: ballot.preferences.clone(), }; normalised_ballots.push(new_ballot); n += &one; } } self.ballots = normalised_ballots; } /// Convert ballots with equal rankings to strict-preference "minivoters" pub fn realise_equal_rankings(&mut self) { let mut realised_ballots = Vec::new(); for ballot in self.ballots.iter() { let mut b = ballot.realise_equal_rankings(); realised_ballots.append(&mut b); } self.ballots = realised_ballots; } } /// A candidate in an [Election] #[derive(Clone, Eq, Hash, PartialEq)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct Candidate { /// Name of the candidate pub name: String, } /// The current state of counting an [Election] pub struct CountState<'a, N: Number> { /// Pointer to the [Election] being counted pub election: &'a Election, /// [HashMap] of [CountCard]s for each [Candidate] in the election pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>, /// [CountCard] representing the exhausted pile pub exhausted: CountCard<'a, N>, /// [CountCard] representing loss by fraction pub loss_fraction: CountCard<'a, N>, /// [crate::stv::meek::BallotTree] for Meek STV pub ballot_tree: Option>, /// Values used to break ties, based on forwards tie-breaking pub forwards_tiebreak: Option>, /// Values used to break ties, based on backwards tie-breaking pub backwards_tiebreak: Option>, /// [SHARandom] for random tie-breaking pub random: Option>, /// Quota for election pub quota: Option, /// Vote required for election /// /// Only used in ERS97/ERS76. pub vote_required_election: Option, /// Number of candidates who have been declared elected pub num_elected: usize, /// Number of candidates who have been declared excluded pub num_excluded: usize, /// [ConstraintMatrix] for constrained elections pub constraint_matrix: Option, /// The type of stage being counted /// /// For example, "Surplus of", "Exclusion of" pub kind: Option<&'a str>, /// The description of the stage being counted, excluding [CountState::kind] pub title: String, /// [Logger] for this stage of the count pub logger: Logger<'a>, } impl<'a, N: Number> CountState<'a, N> { /// Construct a new blank [CountState] for the given [Election] pub fn new(election: &'a Election) -> Self { let mut state = CountState { election: &election, candidates: HashMap::new(), exhausted: CountCard::new(), loss_fraction: CountCard::new(), ballot_tree: None, forwards_tiebreak: None, backwards_tiebreak: None, random: None, quota: None, vote_required_election: None, num_elected: 0, num_excluded: 0, constraint_matrix: None, kind: None, title: String::new(), logger: Logger { entries: Vec::new() }, }; for candidate in election.candidates.iter() { state.candidates.insert(candidate, CountCard::new()); } for withdrawn_idx in election.withdrawn_candidates.iter() { state.candidates.get_mut(&election.candidates[*withdrawn_idx]).unwrap().state = CandidateState::Withdrawn; } if let Some(constraints) = &election.constraints { let mut num_groups: Vec = constraints.0.iter().map(|c| c.groups.len()).collect(); let mut cm = ConstraintMatrix::new(&mut num_groups[..]); // Init constraint matrix total cells min/max for (i, constraint) in constraints.0.iter().enumerate() { for (j, group) in constraint.groups.iter().enumerate() { let mut idx = vec![0; constraints.0.len()]; idx[i] = j + 1; let mut cell = &mut cm[&idx]; cell.min = group.min; cell.max = group.max; } } // Fill in grand total, etc. cm.update_from_state(&state.election, &state.candidates); cm.init(); //println!("{}", cm); // Require correct number of candidates to be elected let idx = vec![0; constraints.0.len()]; cm[&idx].min = election.seats; cm[&idx].max = election.seats; state.constraint_matrix = Some(cm); } return state; } /// [Step](CountCard::step) every [CountCard] to prepare for the next stage pub fn step_all(&mut self) { for (_, count_card) in self.candidates.iter_mut() { count_card.step(); } self.exhausted.step(); self.loss_fraction.step(); } /// List the candidates, and their current state, votes and transfers pub fn describe_candidates(&self, opts: &STVOptions) -> String { let mut candidates: Vec<(&Candidate, &CountCard)>; if opts.sort_votes { // Sort by votes if requested candidates = self.candidates.iter() .map(|(c, cc)| (*c, cc)).collect(); // First sort by order of election (as a tie-breaker, if votes are equal) candidates.sort_unstable_by(|a, b| b.1.order_elected.cmp(&a.1.order_elected)); // Then sort by votes candidates.sort_by(|a, b| a.1.votes.cmp(&b.1.votes)); candidates.reverse(); } else { candidates = self.election.candidates.iter() .map(|c| (c, &self.candidates[c])) .collect(); } let mut result = String::new(); for (candidate, count_card) in candidates { match count_card.state { CandidateState::Hopeful => { result.push_str(&format!("- {}: {:.dps$} ({:.dps$})\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals)); } CandidateState::Guarded => { result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Guarded\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals)); } CandidateState::Elected => { if let Some(kv) = &count_card.keep_value { result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - ELECTED {} (kv = {:.dps2$})\n", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, kv, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } else { result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}\n", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=opts.pp_decimals)); } } CandidateState::Doomed => { result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Doomed\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals)); } CandidateState::Withdrawn => { if !opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() { result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Withdrawn\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals)); } } CandidateState::Excluded => { // If --hide-excluded, hide unless nonzero votes or nonzero transfers if !opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() { result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Excluded {}\n", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=opts.pp_decimals)); } } } } return result; } /// Produce summary rows for the current stage pub fn describe_summary(&self, opts: &STVOptions) -> String { let mut result = String::new(); result.push_str(&format!("Exhausted: {:.dps$} ({:.dps$})\n", self.exhausted.votes, self.exhausted.transfers, dps=opts.pp_decimals)); result.push_str(&format!("Loss by fraction: {:.dps$} ({:.dps$})\n", self.loss_fraction.votes, self.loss_fraction.transfers, dps=opts.pp_decimals)); let mut total_vote = self.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); total_vote += &self.exhausted.votes; total_vote += &self.loss_fraction.votes; result.push_str(&format!("Total votes: {:.dps$}\n", total_vote, dps=opts.pp_decimals)); result.push_str(&format!("Quota: {:.dps$}\n", self.quota.as_ref().unwrap(), dps=opts.pp_decimals)); if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 { result.push_str(&format!("Vote required for election: {:.dps$}\n", self.vote_required_election.as_ref().unwrap(), dps=opts.pp_decimals)); } return result; } } /// Current state of a [Candidate] during an election count #[derive(Clone)] pub struct CountCard<'a, N> { /// State of the candidate pub state: CandidateState, /// Order of election or exclusion /// /// Positive integers represent order of election; negative integers represent order of exclusion. pub order_elected: isize, /// Whether distribution of this candidate's surpluses/transfer of excluded candidate's votes is complete pub finalised: bool, /// Net votes transferred to this candidate in this stage pub transfers: N, /// Votes of the candidate at the end of this stage pub votes: N, /// Net ballots transferred to this candidate in this stage pub ballot_transfers: N, /// Parcels of ballots assigned to this candidate pub parcels: Vec>, /// Candidate's keep value (Meek STV) pub keep_value: Option, } impl<'a, N: Number> CountCard<'a, N> { /// Returns a new blank [CountCard] pub fn new() -> Self { return CountCard { state: CandidateState::Hopeful, order_elected: 0, finalised: false, transfers: N::new(), votes: N::new(), ballot_transfers: N::new(), parcels: Vec::new(), keep_value: None, }; } /// Transfer the given number of votes to this [CountCard], incrementing [transfers](CountCard::transfers) and [votes](CountCard::votes) pub fn transfer(&mut self, transfer: &'_ N) { self.transfers += transfer; self.votes += transfer; } /// Set [transfers](CountCard::transfers) to 0 pub fn step(&mut self) { self.transfers = N::new(); self.ballot_transfers = N::new(); } /// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty pub fn concat_parcels(&mut self) -> Vec> { let mut result = Vec::new(); for parcel in self.parcels.iter_mut() { result.append(&mut parcel.votes); } return result; } /// Return the number of ballots across all parcels pub fn num_ballots(&self) -> N { return self.parcels.iter().fold(N::new(), |acc, p| acc + p.num_ballots()); } } /// Parcel of [Vote]s during a count #[derive(Clone)] pub struct Parcel<'a, N> { /// [Vote]s in this parcel pub votes: Vec>, /// Accumulated relative value of each [Vote] in this parcel pub value_fraction: N, /// Order for sorting with [crate::stv::ExclusionMethod::BySource] pub source_order: usize, } impl<'a, N: Number> Parcel<'a, N> { /// Return the number of ballots in this parcel pub fn num_ballots(&self) -> N { return self.votes.iter().fold(N::new(), |acc, v| acc + &v.ballot.orig_value); } /// Return the value of the votes in this parcel pub fn num_votes(&self) -> N { return self.num_ballots() * &self.value_fraction; } } /// Represents a [Ballot] with an associated value #[derive(Clone)] pub struct Vote<'a, N> { /// Ballot from which the vote is derived pub ballot: &'a Ballot, /// Index of the next preference to examine pub up_to_pref: usize, } impl<'a, N> Vote<'a, N> { /// Get the next preference and increment `up_to_pref` /// /// Assumes that each preference level contains only one preference. pub fn next_preference(&mut self) -> Option { if self.up_to_pref >= self.ballot.preferences.len() { return None; } let preference = &self.ballot.preferences[self.up_to_pref]; self.up_to_pref += 1; return Some(*preference.first().unwrap()); } } /// A record of a voter's preferences #[derive(Clone)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct Ballot { /// Original value/weight of the ballot #[cfg_attr(not(target_arch = "wasm32"), with(SerializedNum))] pub orig_value: N, /// Indexes of candidates preferenced at each level on the ballot pub preferences: Vec>, } impl Ballot { /// Convert ballot with equal rankings to strict-preference "minivoters" pub fn realise_equal_rankings(&self) -> Vec> { // Preferences for each minivoter let mut minivoters = vec![Vec::new()]; for preference in self.preferences.iter() { if preference.len() == 1 { for minivoter in minivoters.iter_mut() { minivoter.push(preference.clone()); } } else { todo!(); } } let weight_each = self.orig_value.clone() / N::from(minivoters.len()); let ballots = minivoters.into_iter() .map(|p| Ballot { orig_value: weight_each.clone(), preferences: p }) .collect(); return ballots; } } /// rkyv-serialized representation of [Number] #[cfg(not(target_arch = "wasm32"))] pub struct SerializedNum; #[cfg(not(target_arch = "wasm32"))] impl ArchiveWith for SerializedNum { type Archived = Archived; type Resolver = Resolver; unsafe fn resolve_with(field: &N, pos: usize, resolver: Self::Resolver, out: *mut Self::Archived) { field.to_string().resolve(pos, resolver, out); } } #[cfg(not(target_arch = "wasm32"))] impl SerializeWith for SerializedNum where String: Serialize { fn serialize_with(field: &N, serializer: &mut S) -> Result { return field.to_string().serialize(serializer); } } #[cfg(not(target_arch = "wasm32"))] impl DeserializeWith, N, D> for SerializedNum where Archived: Deserialize { fn deserialize_with(field: &Archived, deserializer: &mut D) -> Result { return Ok(N::parse(&field.deserialize(deserializer)?)); } } /// State of a [Candidate] during a count #[allow(dead_code)] #[derive(PartialEq)] #[derive(Clone)] #[derive(Debug)] pub enum CandidateState { /// Hopeful (continuing candidate) Hopeful, /// Required by constraints to be guarded from exclusion Guarded, /// Declared elected Elected, /// Required by constraints to be doomed to be excluded Doomed, /// Withdrawn candidate Withdrawn, /// Declared excluded Excluded, }