2021-05-28 19:58:40 +10:00
|
|
|
/* OpenTally: Open-source election vote counting
|
2023-02-01 18:06:22 +11:00
|
|
|
* Copyright © 2021–2023 Lee Yingtong Li (RunasSudo)
|
2021-05-28 19:58:40 +10:00
|
|
|
*
|
|
|
|
* 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/>.
|
|
|
|
*/
|
|
|
|
|
2022-08-21 03:37:12 +10:00
|
|
|
use crate::candmap::CandidateMap;
|
2022-04-16 02:27:59 +10:00
|
|
|
use crate::constraints::{Constraint, Constraints, ConstrainedGroup, ConstraintMatrix};
|
2021-05-29 02:13:47 +10:00
|
|
|
use crate::logger::Logger;
|
2021-05-28 19:58:40 +10:00
|
|
|
use crate::numbers::Number;
|
2021-06-13 03:15:15 +10:00
|
|
|
use crate::sharandom::SHARandom;
|
2021-09-10 02:41:40 +10:00
|
|
|
use crate::stv::{self, STVOptions};
|
2021-09-11 18:42:15 +10:00
|
|
|
use crate::stv::gregory::TransferTable;
|
2021-09-11 01:19:38 +10:00
|
|
|
use crate::stv::meek::BallotTree;
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-09-04 02:26:30 +10:00
|
|
|
use itertools::Itertools;
|
|
|
|
|
2021-09-05 00:04:09 +10:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
use rkyv::{Archive, Deserialize, Serialize};
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
use crate::numbers::{SerializedNumber, SerializedOptionNumber};
|
|
|
|
|
2021-07-31 17:41:28 +10:00
|
|
|
use std::cmp::max;
|
2021-09-06 02:43:33 +10:00
|
|
|
use std::fmt;
|
2022-08-20 23:20:42 +10:00
|
|
|
use std::hash::{Hash, Hasher};
|
2021-07-31 15:24:23 +10:00
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// An election to be counted
|
2021-09-02 17:17:45 +10:00
|
|
|
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
|
2022-04-16 02:27:59 +10:00
|
|
|
#[derive(Clone)]
|
2021-05-28 19:58:40 +10:00
|
|
|
pub struct Election<N> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Name of the election
|
2021-06-03 21:35:25 +10:00
|
|
|
pub name: String,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Number of candidates to be elected
|
2021-05-28 19:58:40 +10:00
|
|
|
pub seats: usize,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [Vec] of [Candidate]s in the election
|
2021-05-28 19:58:40 +10:00
|
|
|
pub candidates: Vec<Candidate>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Indexes of withdrawn candidates
|
2021-06-12 00:50:01 +10:00
|
|
|
pub withdrawn_candidates: Vec<usize>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [Vec] of [Ballot]s cast in the election
|
2021-05-28 19:58:40 +10:00
|
|
|
pub ballots: Vec<Ballot<N>>,
|
2021-09-05 00:04:09 +10:00
|
|
|
/// Total value of [Ballot]s cast in the election
|
|
|
|
///
|
|
|
|
/// Used for [Election::realise_equal_rankings].
|
|
|
|
#[cfg_attr(not(target_arch = "wasm32"), with(SerializedOptionNumber))]
|
|
|
|
pub total_votes: Option<N>,
|
2021-06-27 21:57:24 +10:00
|
|
|
/// Constraints on candidates
|
|
|
|
pub constraints: Option<Constraints>,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<N: Number> Election<N> {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Convert ballots with weight >1 to multiple ballots of weight 1
|
2021-09-03 23:53:15 +10:00
|
|
|
///
|
|
|
|
/// Assumes ballots have integer weight.
|
2021-06-11 21:23:08 +10:00
|
|
|
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(),
|
2022-08-25 21:49:50 +10:00
|
|
|
has_equal_rankings: ballot.has_equal_rankings,
|
2021-06-11 21:23:08 +10:00
|
|
|
};
|
|
|
|
normalised_ballots.push(new_ballot);
|
|
|
|
n += &one;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self.ballots = normalised_ballots;
|
|
|
|
}
|
2021-09-03 23:53:15 +10:00
|
|
|
|
|
|
|
/// Convert ballots with equal rankings to strict-preference "minivoters"
|
|
|
|
pub fn realise_equal_rankings(&mut self) {
|
2022-08-25 21:49:50 +10:00
|
|
|
if !self.ballots.iter().any(|b| b.has_equal_rankings) {
|
2022-08-18 00:15:44 +10:00
|
|
|
// No equal rankings
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-09-05 00:04:09 +10:00
|
|
|
// Record total_votes so loss by fraction can be calculated
|
2022-08-18 00:15:44 +10:00
|
|
|
// See crate::stv::gregory::distribute_first_preferences, etc.
|
|
|
|
self.total_votes = Some(self.ballots.iter().fold(N::new(), |mut acc, b| { acc += &b.orig_value; acc }));
|
2021-09-05 00:04:09 +10:00
|
|
|
|
2022-08-18 00:15:44 +10:00
|
|
|
let mut realised_ballots = Vec::with_capacity(self.ballots.len());
|
|
|
|
for ballot in self.ballots.drain(..) {
|
|
|
|
ballot.realise_equal_rankings_into(&mut realised_ballots);
|
2021-09-03 23:53:15 +10:00
|
|
|
}
|
|
|
|
self.ballots = realised_ballots;
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// A candidate in an [Election]
|
2022-08-20 23:39:15 +10:00
|
|
|
#[derive(Clone, Eq)]
|
2021-09-02 17:17:45 +10:00
|
|
|
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
|
2021-05-28 19:58:40 +10:00
|
|
|
pub struct Candidate {
|
2022-08-20 23:20:42 +10:00
|
|
|
/// Index of the candidate
|
|
|
|
pub index: usize,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Name of the candidate
|
2021-05-28 19:58:40 +10:00
|
|
|
pub name: String,
|
2022-04-16 02:27:59 +10:00
|
|
|
/// If this candidate is a dummy candidate (e.g. for --constraint-mode repeat_count)
|
|
|
|
pub is_dummy: bool,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2022-08-20 23:39:15 +10:00
|
|
|
impl PartialEq for Candidate {
|
|
|
|
// Custom implementation of eq for HashMap purposes, to improve performance
|
|
|
|
//
|
|
|
|
// SAFETY: Results in undefined behaviour if multiple Candidates are allowed to have the same index
|
|
|
|
fn eq(&self, other: &Candidate) -> bool {
|
|
|
|
return self.index == other.index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-20 23:20:42 +10:00
|
|
|
impl Hash for Candidate {
|
|
|
|
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
|
|
|
// Custom implementation of hash for use with NoHashHasher, to improve performance
|
|
|
|
hasher.write_usize(self.index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
impl nohash_hasher::IsEnabled for Candidate {}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// The current state of counting an [Election]
|
2022-04-16 02:27:59 +10:00
|
|
|
#[derive(Clone)]
|
2021-06-16 13:00:54 +10:00
|
|
|
pub struct CountState<'a, N: Number> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Pointer to the [Election] being counted
|
2021-05-28 19:58:40 +10:00
|
|
|
pub election: &'a Election<N>,
|
|
|
|
|
2022-08-21 03:37:12 +10:00
|
|
|
/// [CandidateMap] of [CountCard]s for each [Candidate] in the election
|
|
|
|
pub candidates: CandidateMap<'a, CountCard<'a, N>>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [CountCard] representing the exhausted pile
|
2021-05-28 19:58:40 +10:00
|
|
|
pub exhausted: CountCard<'a, N>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [CountCard] representing loss by fraction
|
2021-05-29 00:43:58 +10:00
|
|
|
pub loss_fraction: CountCard<'a, N>,
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-09-11 01:19:38 +10:00
|
|
|
/// [BallotTree] for Meek STV
|
|
|
|
pub ballot_tree: Option<BallotTree<'a, N>>,
|
2021-06-16 13:00:54 +10:00
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Values used to break ties, based on forwards tie-breaking
|
2022-08-21 03:37:12 +10:00
|
|
|
pub forwards_tiebreak: Option<CandidateMap<'a, usize>>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Values used to break ties, based on backwards tie-breaking
|
2022-08-21 03:37:12 +10:00
|
|
|
pub backwards_tiebreak: Option<CandidateMap<'a, usize>>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [SHARandom] for random tie-breaking
|
2021-06-13 03:15:15 +10:00
|
|
|
pub random: Option<SHARandom<'a>>,
|
2021-06-13 00:15:14 +10:00
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Quota for election
|
2021-06-07 20:52:18 +10:00
|
|
|
pub quota: Option<N>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Vote required for election
|
|
|
|
///
|
2021-07-23 01:21:29 +10:00
|
|
|
/// Only used in ERS97/ERS76.
|
2021-06-07 20:52:18 +10:00
|
|
|
pub vote_required_election: Option<N>,
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Number of candidates who have been declared elected
|
2021-05-28 19:58:40 +10:00
|
|
|
pub num_elected: usize,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Number of candidates who have been declared excluded
|
2021-05-28 19:58:40 +10:00
|
|
|
pub num_excluded: usize,
|
2021-05-29 01:22:46 +10:00
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
/// [ConstraintMatrix] for constrained elections
|
|
|
|
pub constraint_matrix: Option<ConstraintMatrix>,
|
2022-08-22 11:35:20 +10:00
|
|
|
/// [RollbackState] when using [ConstraintMode::RepeatCount](crate::stv::ConstraintMode::RepeatCount)
|
2022-04-16 02:27:59 +10:00
|
|
|
pub rollback_state: RollbackState<'a, N>,
|
2021-06-27 21:57:24 +10:00
|
|
|
|
2021-09-11 01:19:38 +10:00
|
|
|
/// Transfer table for this surplus/exclusion
|
|
|
|
pub transfer_table: Option<TransferTable<'a, N>>,
|
|
|
|
|
2021-09-06 02:43:33 +10:00
|
|
|
/// The type of stage being counted, etc.
|
|
|
|
pub title: StageKind<'a>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [Logger] for this stage of the count
|
2021-05-29 02:13:47 +10:00
|
|
|
pub logger: Logger<'a>,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, N: Number> CountState<'a, N> {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Construct a new blank [CountState] for the given [Election]
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn new(election: &'a Election<N>) -> Self {
|
|
|
|
let mut state = CountState {
|
2021-10-27 19:52:51 +11:00
|
|
|
election,
|
2022-08-21 03:37:12 +10:00
|
|
|
candidates: CandidateMap::with_capacity(election.candidates.len()),
|
2021-05-28 19:58:40 +10:00
|
|
|
exhausted: CountCard::new(),
|
2021-05-29 00:43:58 +10:00
|
|
|
loss_fraction: CountCard::new(),
|
2021-06-16 13:00:54 +10:00
|
|
|
ballot_tree: None,
|
2021-06-13 00:15:14 +10:00
|
|
|
forwards_tiebreak: None,
|
|
|
|
backwards_tiebreak: None,
|
2021-06-13 03:15:15 +10:00
|
|
|
random: None,
|
2021-06-07 20:52:18 +10:00
|
|
|
quota: None,
|
|
|
|
vote_required_election: None,
|
2021-05-28 19:58:40 +10:00
|
|
|
num_elected: 0,
|
|
|
|
num_excluded: 0,
|
2021-06-27 21:57:24 +10:00
|
|
|
constraint_matrix: None,
|
2022-04-16 02:27:59 +10:00
|
|
|
rollback_state: RollbackState::Normal,
|
2021-09-11 01:19:38 +10:00
|
|
|
transfer_table: None,
|
2021-09-06 02:43:33 +10:00
|
|
|
title: StageKind::FirstPreferences,
|
2021-05-29 02:13:47 +10:00
|
|
|
logger: Logger { entries: Vec::new() },
|
2021-05-28 19:58:40 +10:00
|
|
|
};
|
|
|
|
|
2021-09-04 22:46:29 +10:00
|
|
|
// Init candidate count cards
|
2021-05-28 19:58:40 +10:00
|
|
|
for candidate in election.candidates.iter() {
|
2022-04-16 02:27:59 +10:00
|
|
|
let mut count_card = CountCard::new();
|
|
|
|
if candidate.is_dummy {
|
|
|
|
count_card.state = CandidateState::Withdrawn;
|
|
|
|
}
|
|
|
|
state.candidates.insert(candidate, count_card);
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-09-04 22:46:29 +10:00
|
|
|
// Set withdrawn candidates state
|
2021-06-12 00:50:01 +10:00
|
|
|
for withdrawn_idx in election.withdrawn_candidates.iter() {
|
|
|
|
state.candidates.get_mut(&election.candidates[*withdrawn_idx]).unwrap().state = CandidateState::Withdrawn;
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:46:29 +10:00
|
|
|
// Init constraints
|
2021-06-27 21:57:24 +10:00
|
|
|
if let Some(constraints) = &election.constraints {
|
2021-10-29 23:07:38 +11:00
|
|
|
// Init constraint matrix
|
2021-06-27 21:57:24 +10:00
|
|
|
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.
|
2021-10-27 19:52:51 +11:00
|
|
|
cm.update_from_state(state.election, &state.candidates);
|
2021-06-27 21:57:24 +10:00
|
|
|
cm.init();
|
|
|
|
//println!("{}", cm);
|
|
|
|
|
2021-06-27 22:36:50 +10:00
|
|
|
// 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;
|
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
state.constraint_matrix = Some(cm);
|
|
|
|
}
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// [Step](CountCard::step) every [CountCard] to prepare for the next stage
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn step_all(&mut self) {
|
|
|
|
for (_, count_card) in self.candidates.iter_mut() {
|
|
|
|
count_card.step();
|
|
|
|
}
|
|
|
|
self.exhausted.step();
|
2021-05-29 00:43:58 +10:00
|
|
|
self.loss_fraction.step();
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
2021-07-31 17:41:28 +10:00
|
|
|
|
|
|
|
/// 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
|
2022-08-21 05:24:54 +10:00
|
|
|
candidates = self.candidates.iter().collect();
|
2021-07-31 17:41:28 +10:00
|
|
|
// 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 {
|
2022-04-16 02:27:59 +10:00
|
|
|
if candidate.is_dummy {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-07-31 17:41:28 +10:00
|
|
|
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));
|
|
|
|
|
2022-08-20 22:33:48 +10:00
|
|
|
let mut total_vote = self.candidates.iter().filter_map(|(c, cc)| if c.is_dummy { None } else { Some(cc) }).fold(N::new(), |mut acc, cc| { acc += &cc.votes; acc });
|
2021-07-31 17:41:28 +10:00
|
|
|
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));
|
|
|
|
|
2023-02-01 18:06:22 +11:00
|
|
|
if self.election.seats == 1 {
|
|
|
|
result.push_str(&format!("Majority: {:.dps$}\n", self.quota.as_ref().unwrap(), dps=opts.pp_decimals));
|
|
|
|
} else {
|
|
|
|
result.push_str(&format!("Quota: {:.dps$}\n", self.quota.as_ref().unwrap(), dps=opts.pp_decimals));
|
|
|
|
}
|
2021-09-10 02:41:40 +10:00
|
|
|
if stv::should_show_vre(opts) {
|
2021-09-11 01:19:38 +10:00
|
|
|
if let Some(vre) = &self.vote_required_election {
|
|
|
|
result.push_str(&format!("Vote required for election: {:.dps$}\n", vre, dps=opts.pp_decimals));
|
|
|
|
}
|
2021-07-31 17:41:28 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
2022-08-21 07:20:21 +10:00
|
|
|
|
|
|
|
// -------------------
|
|
|
|
// HELPER CALCULATIONS
|
|
|
|
|
|
|
|
/// Get the total vote, viz. the sum votes of all candidates
|
|
|
|
pub fn total_vote(&self) -> N {
|
|
|
|
return self.total_votes_of(self.candidates.values());
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the total votes of the given candidates
|
|
|
|
pub fn total_votes_of<I: Iterator<Item=&'a CountCard<'a, N>>>(&self, count_cards: I) -> N {
|
|
|
|
return count_cards.fold(N::new(), |mut acc, cc| { acc += &cc.votes; acc });
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the total active vote, viz. the votes of all continuing candidates plus all votes awaiting transfer
|
|
|
|
pub fn active_vote(&self) -> N {
|
|
|
|
return self.candidates.values().fold(N::new(), |mut acc, cc| {
|
|
|
|
match cc.state {
|
|
|
|
CandidateState::Elected => {
|
|
|
|
if !cc.finalised && &cc.votes > self.quota.as_ref().unwrap() {
|
|
|
|
acc += &cc.votes;
|
|
|
|
acc -= self.quota.as_ref().unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
acc += &cc.votes;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
acc
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the total surplus
|
|
|
|
///
|
|
|
|
/// A candidate has a surplus if the candidate's votes are not finalised, and the votes exceed the quota.
|
|
|
|
/// The votes of all other candidates are ignored.
|
|
|
|
pub fn total_surplus(&self) -> N {
|
|
|
|
return self.total_surplus_of(self.candidates.values());
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the total surpluses of the given candidates
|
|
|
|
///
|
2022-08-22 11:35:20 +10:00
|
|
|
/// See [CountState::total_surplus].
|
2022-08-21 07:20:21 +10:00
|
|
|
pub fn total_surplus_of<I: Iterator<Item=&'a CountCard<'a, N>>>(&self, count_cards: I) -> N {
|
|
|
|
return count_cards.fold(N::new(), |mut acc, cc| {
|
|
|
|
if !cc.finalised && &cc.votes > self.quota.as_ref().unwrap() {
|
|
|
|
acc += &cc.votes;
|
|
|
|
acc -= self.quota.as_ref().unwrap();
|
|
|
|
}
|
|
|
|
acc
|
|
|
|
});
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-09-06 02:43:33 +10:00
|
|
|
/// The kind, title, etc. of the stage being counted
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub enum StageKind<'a> {
|
|
|
|
/// First preferences
|
|
|
|
FirstPreferences,
|
|
|
|
/// Surplus of ...
|
|
|
|
SurplusOf(&'a Candidate),
|
|
|
|
/// Exclusion of ...
|
|
|
|
ExclusionOf(Vec<&'a Candidate>),
|
2022-04-16 02:27:59 +10:00
|
|
|
/// Rolled back (--constraint-mode repeat_count)
|
|
|
|
Rollback,
|
|
|
|
/// Exhausted ballots (--constraint-mode repeat_count)
|
|
|
|
RollbackExhausted,
|
|
|
|
/// Ballots of ... (--constraint-mode repeat_count)
|
|
|
|
BallotsOf(&'a Candidate),
|
2021-09-06 02:43:33 +10:00
|
|
|
/// Surpluses distributed (Meek)
|
|
|
|
SurplusesDistributed,
|
|
|
|
/// Bulk election
|
|
|
|
BulkElection,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> StageKind<'a> {
|
|
|
|
/// Return the "kind" portion of the title
|
|
|
|
pub fn kind_as_string(&self) -> &'static str {
|
|
|
|
return match self {
|
|
|
|
StageKind::FirstPreferences => "",
|
|
|
|
StageKind::SurplusOf(_) => "Surplus of",
|
|
|
|
StageKind::ExclusionOf(_) => "Exclusion of",
|
2022-04-16 02:27:59 +10:00
|
|
|
StageKind::Rollback => "",
|
|
|
|
StageKind::RollbackExhausted => "",
|
|
|
|
StageKind::BallotsOf(_) => "Ballots of",
|
2021-09-06 02:43:33 +10:00
|
|
|
StageKind::SurplusesDistributed => "",
|
|
|
|
StageKind::BulkElection => "",
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> fmt::Display for StageKind<'a> {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
StageKind::FirstPreferences => {
|
|
|
|
return f.write_str("First preferences");
|
|
|
|
}
|
|
|
|
StageKind::SurplusOf(candidate) => {
|
|
|
|
return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidate.name));
|
|
|
|
}
|
|
|
|
StageKind::ExclusionOf(candidates) => {
|
|
|
|
return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidates.iter().map(|c| &c.name).sorted().join(", ")));
|
|
|
|
}
|
2022-04-16 02:27:59 +10:00
|
|
|
StageKind::Rollback => {
|
|
|
|
return f.write_str("Constraints applied");
|
|
|
|
}
|
|
|
|
StageKind::RollbackExhausted => {
|
|
|
|
return f.write_str("Exhausted ballots");
|
|
|
|
}
|
|
|
|
StageKind::BallotsOf(candidate) => {
|
|
|
|
return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidate.name));
|
|
|
|
}
|
2021-09-06 02:43:33 +10:00
|
|
|
StageKind::SurplusesDistributed => {
|
|
|
|
return f.write_str("Surpluses distributed");
|
|
|
|
}
|
|
|
|
StageKind::BulkElection => {
|
|
|
|
return f.write_str("Bulk election");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Current state of a [Candidate] during an election count
|
2021-05-28 19:58:40 +10:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct CountCard<'a, N> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// State of the candidate
|
2021-05-28 19:58:40 +10:00
|
|
|
pub state: CandidateState,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Order of election or exclusion
|
|
|
|
///
|
2021-08-08 19:11:15 +10:00
|
|
|
/// Positive integers represent order of election; negative integers represent order of exclusion.
|
2021-05-28 19:58:40 +10:00
|
|
|
pub order_elected: isize,
|
2021-08-08 19:11:15 +10:00
|
|
|
/// Whether distribution of this candidate's surpluses/transfer of excluded candidate's votes is complete
|
|
|
|
pub finalised: bool,
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Net votes transferred to this candidate in this stage
|
2021-05-28 19:58:40 +10:00
|
|
|
pub transfers: N,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Votes of the candidate at the end of this stage
|
2021-05-28 19:58:40 +10:00
|
|
|
pub votes: N,
|
|
|
|
|
2021-08-16 18:48:49 +10:00
|
|
|
/// Net ballots transferred to this candidate in this stage
|
|
|
|
pub ballot_transfers: N,
|
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Parcels of ballots assigned to this candidate
|
2021-05-28 19:58:40 +10:00
|
|
|
pub parcels: Vec<Parcel<'a, N>>,
|
2021-06-16 13:00:54 +10:00
|
|
|
|
|
|
|
/// Candidate's keep value (Meek STV)
|
|
|
|
pub keep_value: Option<N>,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, N: Number> CountCard<'a, N> {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Returns a new blank [CountCard]
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn new() -> Self {
|
|
|
|
return CountCard {
|
2021-06-12 00:50:01 +10:00
|
|
|
state: CandidateState::Hopeful,
|
2021-05-28 19:58:40 +10:00
|
|
|
order_elected: 0,
|
2021-08-08 19:11:15 +10:00
|
|
|
finalised: false,
|
2021-05-28 19:58:40 +10:00
|
|
|
transfers: N::new(),
|
|
|
|
votes: N::new(),
|
2021-08-16 18:48:49 +10:00
|
|
|
ballot_transfers: N::new(),
|
2021-05-28 19:58:40 +10:00
|
|
|
parcels: Vec::new(),
|
2021-06-16 13:00:54 +10:00
|
|
|
keep_value: None,
|
2021-05-28 19:58:40 +10:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Transfer the given number of votes to this [CountCard], incrementing [transfers](CountCard::transfers) and [votes](CountCard::votes)
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn transfer(&mut self, transfer: &'_ N) {
|
|
|
|
self.transfers += transfer;
|
|
|
|
self.votes += transfer;
|
|
|
|
}
|
|
|
|
|
2021-06-16 13:00:54 +10:00
|
|
|
/// Set [transfers](CountCard::transfers) to 0
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn step(&mut self) {
|
|
|
|
self.transfers = N::new();
|
2021-08-16 18:48:49 +10:00
|
|
|
self.ballot_transfers = N::new();
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
2021-07-19 23:15:17 +10:00
|
|
|
|
|
|
|
/// 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;
|
|
|
|
}
|
2021-08-16 18:48:49 +10:00
|
|
|
|
|
|
|
/// Return the number of ballots across all parcels
|
|
|
|
pub fn num_ballots(&self) -> N {
|
2022-08-18 23:55:39 +10:00
|
|
|
return self.parcels.iter().fold(N::new(), |mut acc, p| { acc += p.num_ballots(); acc });
|
2021-08-16 18:48:49 +10:00
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Parcel of [Vote]s during a count
|
2021-07-19 23:15:17 +10:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Parcel<'a, N> {
|
|
|
|
/// [Vote]s in this parcel
|
|
|
|
pub votes: Vec<Vote<'a, N>>,
|
2021-08-16 00:46:05 +10:00
|
|
|
/// Accumulated relative value of each [Vote] in this parcel
|
|
|
|
pub value_fraction: N,
|
2021-07-23 16:45:54 +10:00
|
|
|
/// Order for sorting with [crate::stv::ExclusionMethod::BySource]
|
2021-07-19 23:15:17 +10:00
|
|
|
pub source_order: usize,
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-08-16 00:46:05 +10:00
|
|
|
impl<'a, N: Number> Parcel<'a, N> {
|
|
|
|
/// Return the number of ballots in this parcel
|
|
|
|
pub fn num_ballots(&self) -> N {
|
2022-08-18 23:55:39 +10:00
|
|
|
return self.votes.iter().fold(N::new(), |mut acc, v| { acc += &v.ballot.orig_value; acc });
|
2021-08-16 00:46:05 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the value of the votes in this parcel
|
|
|
|
pub fn num_votes(&self) -> N {
|
|
|
|
return self.num_ballots() * &self.value_fraction;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Represents a [Ballot] with an associated value
|
2021-05-28 19:58:40 +10:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Vote<'a, N> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Ballot from which the vote is derived
|
2021-05-28 19:58:40 +10:00
|
|
|
pub ballot: &'a Ballot<N>,
|
2021-06-16 13:00:54 +10:00
|
|
|
/// Index of the next preference to examine
|
2021-05-28 19:58:40 +10:00
|
|
|
pub up_to_pref: usize,
|
|
|
|
}
|
|
|
|
|
2021-09-03 23:53:15 +10:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// A record of a voter's preferences
|
2022-04-16 02:27:59 +10:00
|
|
|
#[derive(Clone)]
|
2021-09-02 17:17:45 +10:00
|
|
|
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
|
2021-05-28 19:58:40 +10:00
|
|
|
pub struct Ballot<N> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Original value/weight of the ballot
|
2021-09-05 00:04:09 +10:00
|
|
|
#[cfg_attr(not(target_arch = "wasm32"), with(SerializedNumber))]
|
2021-05-28 19:58:40 +10:00
|
|
|
pub orig_value: N,
|
2021-09-03 23:53:15 +10:00
|
|
|
/// Indexes of candidates preferenced at each level on the ballot
|
|
|
|
pub preferences: Vec<Vec<usize>>,
|
2022-08-25 21:49:50 +10:00
|
|
|
/// Whether multiple candidates are preferenced at the same level
|
|
|
|
pub has_equal_rankings: bool,
|
2021-09-03 23:53:15 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<N: Number> Ballot<N> {
|
|
|
|
/// Convert ballot with equal rankings to strict-preference "minivoters"
|
2022-08-18 00:15:44 +10:00
|
|
|
pub fn realise_equal_rankings_into(self, dest: &mut Vec<Ballot<N>>) {
|
2022-08-25 21:49:50 +10:00
|
|
|
if !self.has_equal_rankings {
|
2022-08-18 00:15:44 +10:00
|
|
|
dest.push(self);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-09-03 23:53:15 +10:00
|
|
|
// Preferences for each minivoter
|
|
|
|
let mut minivoters = vec![Vec::new()];
|
|
|
|
|
|
|
|
for preference in self.preferences.iter() {
|
|
|
|
if preference.len() == 1 {
|
2021-09-04 02:26:30 +10:00
|
|
|
// Single preference so just add to the end of existing preferences
|
2021-09-03 23:53:15 +10:00
|
|
|
for minivoter in minivoters.iter_mut() {
|
|
|
|
minivoter.push(preference.clone());
|
|
|
|
}
|
|
|
|
} else {
|
2021-09-04 02:26:30 +10:00
|
|
|
// Equal ranking
|
|
|
|
// Get all possible permutations
|
|
|
|
let permutations: Vec<Vec<usize>> = preference.iter().copied().permutations(preference.len()).collect();
|
|
|
|
|
|
|
|
// Split into new "minivoters" for each possible permutation
|
|
|
|
let mut new_minivoters = Vec::with_capacity(minivoters.len() * permutations.len());
|
|
|
|
for permutation in permutations {
|
|
|
|
for minivoter in minivoters.iter() {
|
|
|
|
let mut new_minivoter = minivoter.clone();
|
|
|
|
for p in permutation.iter() {
|
|
|
|
new_minivoter.push(vec![*p]);
|
|
|
|
}
|
|
|
|
new_minivoters.push(new_minivoter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
minivoters = new_minivoters;
|
2021-09-03 23:53:15 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let weight_each = self.orig_value.clone() / N::from(minivoters.len());
|
2022-08-18 00:15:44 +10:00
|
|
|
|
|
|
|
for minivoter in minivoters {
|
|
|
|
dest.push(Ballot {
|
|
|
|
orig_value: weight_each.clone(),
|
2022-08-25 21:49:50 +10:00
|
|
|
preferences: minivoter,
|
|
|
|
has_equal_rankings: false,
|
2022-08-18 00:15:44 +10:00
|
|
|
});
|
|
|
|
}
|
2021-09-03 23:53:15 +10:00
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// State of a [Candidate] during a count
|
2021-09-09 13:46:10 +10:00
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
2021-05-28 19:58:40 +10:00
|
|
|
pub enum CandidateState {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Hopeful (continuing candidate)
|
2021-06-12 00:50:01 +10:00
|
|
|
Hopeful,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Required by constraints to be guarded from exclusion
|
2021-06-12 00:50:01 +10:00
|
|
|
Guarded,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Declared elected
|
2021-06-12 00:50:01 +10:00
|
|
|
Elected,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Required by constraints to be doomed to be excluded
|
2021-06-12 00:50:01 +10:00
|
|
|
Doomed,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Withdrawn candidate
|
2021-06-12 00:50:01 +10:00
|
|
|
Withdrawn,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Declared excluded
|
2021-06-12 00:50:01 +10:00
|
|
|
Excluded,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
2022-04-16 02:27:59 +10:00
|
|
|
|
|
|
|
/// If --constraint-mode repeat_count and redistribution is required, tracks the ballot papers being redistributed
|
|
|
|
#[allow(missing_docs)]
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub enum RollbackState<'a, N> {
|
|
|
|
/// Not rolling back
|
|
|
|
Normal,
|
|
|
|
/// Start rolling back next stage
|
2022-08-20 23:20:42 +10:00
|
|
|
NeedsRollback {
|
2022-08-21 03:37:12 +10:00
|
|
|
candidates: Option<CandidateMap<'a, CountCard<'a, N>>>,
|
2022-08-20 23:20:42 +10:00
|
|
|
exhausted: Option<CountCard<'a, N>>,
|
|
|
|
constraint: &'a Constraint,
|
|
|
|
group: &'a ConstrainedGroup
|
|
|
|
},
|
2022-04-16 02:27:59 +10:00
|
|
|
/// Rolling back
|
2022-08-20 23:20:42 +10:00
|
|
|
RollingBack {
|
2022-08-21 03:37:12 +10:00
|
|
|
candidates: Option<CandidateMap<'a, CountCard<'a, N>>>,
|
2022-08-20 23:20:42 +10:00
|
|
|
exhausted: Option<CountCard<'a, N>>,
|
|
|
|
candidate_distributing: Option<&'a Candidate>,
|
|
|
|
constraint: Option<&'a Constraint>,
|
|
|
|
group: Option<&'a ConstrainedGroup>
|
|
|
|
},
|
2022-04-16 02:27:59 +10:00
|
|
|
}
|