OpenTally/src/election.rs

480 lines
16 KiB
Rust

/* 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 <https://www.gnu.org/licenses/>.
*/
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<N> {
/// 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<Candidate>,
/// Indexes of withdrawn candidates
pub withdrawn_candidates: Vec<usize>,
/// [Vec] of [Ballot]s cast in the election
pub ballots: Vec<Ballot<N>>,
/// Constraints on candidates
pub constraints: Option<Constraints>,
}
impl<N: Number> Election<N> {
/// 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<N>,
/// [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<crate::stv::meek::BallotTree<'a, N>>,
/// Values used to break ties, based on forwards tie-breaking
pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
/// Values used to break ties, based on backwards tie-breaking
pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
/// [SHARandom] for random tie-breaking
pub random: Option<SHARandom<'a>>,
/// Quota for election
pub quota: Option<N>,
/// Vote required for election
///
/// Only used in ERS97/ERS76.
pub vote_required_election: Option<N>,
/// 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<ConstraintMatrix>,
/// 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<N>) -> 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<usize> = 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<N>)>;
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<Parcel<'a, N>>,
/// Candidate's keep value (Meek STV)
pub keep_value: Option<N>,
}
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<Vote<'a, N>> {
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<Vote<'a, N>>,
/// 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<N>,
/// 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<usize> {
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<N> {
/// 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<Vec<usize>>,
}
impl<N: Number> Ballot<N> {
/// Convert ballot with equal rankings to strict-preference "minivoters"
pub fn realise_equal_rankings(&self) -> Vec<Ballot<N>> {
// 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<N: Number> ArchiveWith<N> for SerializedNum {
type Archived = Archived<String>;
type Resolver = Resolver<String>;
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<N: Number, S: Fallible + ?Sized> SerializeWith<N, S> for SerializedNum where String: Serialize<S> {
fn serialize_with(field: &N, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
return field.to_string().serialize(serializer);
}
}
#[cfg(not(target_arch = "wasm32"))]
impl<N: Number, D: Fallible + ?Sized> DeserializeWith<Archived<String>, N, D> for SerializedNum where Archived<String>: Deserialize<String, D> {
fn deserialize_with(field: &Archived<String>, deserializer: &mut D) -> Result<N, D::Error> {
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,
}