From fbdc32ba30c5a3ef00d8897d4928b2664ec2d152 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 11 Sep 2021 01:19:38 +1000 Subject: [PATCH] Implement TransferTable for exclusions (WIP) --- src/cli/stv.rs | 5 ++ src/election.rs | 14 +++- src/stv/gregory.rs | 34 ++-------- src/stv/mod.rs | 6 ++ src/stv/transfers.rs | 156 +++++++++++++++++++++++++++++++++++++++++++ src/stv/wasm.rs | 2 + 6 files changed, 186 insertions(+), 31 deletions(-) create mode 100644 src/stv/transfers.rs diff --git a/src/cli/stv.rs b/src/cli/stv.rs index 0235c8d..3bf98dd 100644 --- a/src/cli/stv.rs +++ b/src/cli/stv.rs @@ -189,6 +189,10 @@ pub struct SubcmdOptions { #[clap(help_heading=Some("OUTPUT"), long)] sort_votes: bool, + /// Show details of transfers to candidates during surplus distributions/candidate exclusions + #[clap(help_heading=Some("OUTPUT"), long)] + transfers_detail: bool, + /// Print votes to specified decimal places in results report #[clap(help_heading=Some("OUTPUT"), long, default_value="2", value_name="dps")] pp_decimals: usize, @@ -285,6 +289,7 @@ where cmd_opts.constraint_mode.into(), cmd_opts.hide_excluded, cmd_opts.sort_votes, + cmd_opts.transfers_detail, cmd_opts.pp_decimals, ); diff --git a/src/election.rs b/src/election.rs index 8599cc4..fb17228 100644 --- a/src/election.rs +++ b/src/election.rs @@ -20,6 +20,8 @@ use crate::logger::Logger; use crate::numbers::Number; use crate::sharandom::SHARandom; use crate::stv::{self, STVOptions}; +use crate::stv::meek::BallotTree; +use crate::stv::transfers::TransferTable; use itertools::Itertools; @@ -110,8 +112,8 @@ pub struct CountState<'a, N: Number> { /// [CountCard] representing loss by fraction pub loss_fraction: CountCard<'a, N>, - /// [crate::stv::meek::BallotTree] for Meek STV - pub ballot_tree: Option>, + /// [BallotTree] for Meek STV + pub ballot_tree: Option>, /// Values used to break ties, based on forwards tie-breaking pub forwards_tiebreak: Option>, @@ -135,6 +137,9 @@ pub struct CountState<'a, N: Number> { /// [ConstraintMatrix] for constrained elections pub constraint_matrix: Option, + /// Transfer table for this surplus/exclusion + pub transfer_table: Option>, + /// The type of stage being counted, etc. pub title: StageKind<'a>, /// [Logger] for this stage of the count @@ -158,6 +163,7 @@ impl<'a, N: Number> CountState<'a, N> { num_elected: 0, num_excluded: 0, constraint_matrix: None, + transfer_table: None, title: StageKind::FirstPreferences, logger: Logger { entries: Vec::new() }, }; @@ -282,7 +288,9 @@ impl<'a, N: Number> CountState<'a, N> { result.push_str(&format!("Quota: {:.dps$}\n", self.quota.as_ref().unwrap(), dps=opts.pp_decimals)); if stv::should_show_vre(opts) { - result.push_str(&format!("Vote required for election: {:.dps$}\n", self.vote_required_election.as_ref().unwrap(), dps=opts.pp_decimals)); + if let Some(vre) = &self.vote_required_election { + result.push_str(&format!("Vote required for election: {:.dps$}\n", vre, dps=opts.pp_decimals)); + } } return result; diff --git a/src/stv/gregory.rs b/src/stv/gregory.rs index c41c047..1233e43 100644 --- a/src/stv/gregory.rs +++ b/src/stv/gregory.rs @@ -21,6 +21,7 @@ use super::sample; use crate::constraints; use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote}; use crate::numbers::Number; +use crate::stv::transfers::TransferTable; use crate::ties; use std::cmp::max; @@ -635,11 +636,7 @@ where let value = match parcels.first() { Some(p) => Some(p.value_fraction.clone()), _ => None }; - let mut candidate_transfers: HashMap<&Candidate, N> = HashMap::new(); - for candidate in state.election.candidates.iter() { - candidate_transfers.insert(candidate, N::new()); - } - let mut exhausted_transfers = N::new(); + let mut transfer_table = TransferTable::new(); for parcel in parcels { // Count next preferences @@ -657,11 +654,9 @@ where }; // Record transfers - let transfers_orig = candidate_transfers.remove(candidate).unwrap(); - candidate_transfers.insert(candidate, transfers_orig + &entry.num_ballots * &parcel.value_fraction); + transfer_table.add_transfers(&parcel.value_fraction, candidate, &entry.num_ballots); let count_card = state.candidates.get_mut(candidate).unwrap(); - count_card.ballot_transfers += parcel.num_ballots(); count_card.parcels.push(parcel); } @@ -673,11 +668,9 @@ where }; // Record transfers - state.exhausted.ballot_transfers += parcel.num_ballots(); - exhausted_transfers += &result.exhausted.num_ballots * &parcel.value_fraction; - state.exhausted.parcels.push(parcel); + transfer_table.add_exhausted(&parcel.value_fraction, &result.exhausted.num_ballots); - // TODO: Detailed transfers logs + state.exhausted.parcels.push(parcel); } if let ExclusionMethod::SingleStage = opts.exclusion { @@ -697,22 +690,7 @@ where } // Credit transferred votes - // ballot_transfers updated above - for (candidate, mut votes) in candidate_transfers { - if let Some(dps) = opts.round_votes { - votes.floor_mut(dps); - } - let count_card = state.candidates.get_mut(candidate).unwrap(); - count_card.transfer(&votes); - checksum += votes; - } - - // Credit exhausted votes - if let Some(dps) = opts.round_votes { - exhausted_transfers.floor_mut(dps); - } - state.exhausted.transfer(&exhausted_transfers); - checksum += exhausted_transfers; + checksum += transfer_table.apply_to(state, opts); if !votes_remain { // Finalise candidate votes diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 1c893ab..5e99aa1 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -23,6 +23,8 @@ pub mod gregory; pub mod meek; /// Random sample methods of surplus distributions pub mod sample; +/// Transfer tables +pub mod transfers; /// WebAssembly wrappers //#[cfg(target_arch = "wasm32")] @@ -155,6 +157,10 @@ pub struct STVOptions { #[builder(default="false")] pub sort_votes: bool, + /// 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, diff --git a/src/stv/transfers.rs b/src/stv/transfers.rs new file mode 100644 index 0000000..ad3b1ec --- /dev/null +++ b/src/stv/transfers.rs @@ -0,0 +1,156 @@ +/* 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 super::STVOptions; + +use crate::election::{Candidate, CountState}; +use crate::numbers::Number; + +use std::collections::HashMap; + +/// Table describing vote transfers during a surplus distribution or exclusion +pub struct TransferTable<'e, N: Number> { + /// Columns in the table + pub columns: Vec>, +} + +impl<'e, N: Number> TransferTable<'e, N> { + /// Return a new [TransferTable] + pub fn new() -> Self { + TransferTable { + columns: Vec::new(), + } + } + + /// Record the specified transfer + pub fn add_transfers(&mut self, value_fraction: &N, candidate: &'e Candidate, ballots: &N) { + for col in self.columns.iter_mut() { + if &col.value_fraction == value_fraction { + col.add_transfers(candidate, ballots); + return; + } + } + + let mut col = TransferTableColumn { + value_fraction: value_fraction.clone(), + cells: HashMap::new(), + exhausted: TransferTableCell { ballots: N::new() }, + }; + col.add_transfers(candidate, ballots); + self.columns.push(col); + } + + /// Record the specified exhaustion + pub fn add_exhausted(&mut self, value_fraction: &N, ballots: &N) { + for col in self.columns.iter_mut() { + if &col.value_fraction == value_fraction { + col.exhausted.ballots += ballots; + return; + } + } + + let col = TransferTableColumn { + value_fraction: value_fraction.clone(), + cells: HashMap::new(), + exhausted: TransferTableCell { ballots: ballots.clone() }, + }; + self.columns.push(col); + } + + /// Apply the transfers described in the table to the count sheet + /// + /// Credit continuing candidates and exhausted pile with the appropriate number of ballot papers and votes. + pub fn apply_to(&self, state: &mut CountState, opts: &STVOptions) -> N { + // TODO: SumSurplusTransfers + + let mut checksum = N::new(); + + // Credit transferred votes + for (candidate, count_card) in state.candidates.iter_mut() { + let mut votes_transferred = N::new(); + let mut ballots_transferred = N::new(); + + for column in self.columns.iter() { + if let Some(cell) = column.cells.get(*candidate) { + votes_transferred += cell.ballots.clone() * &column.value_fraction; + ballots_transferred += &cell.ballots; + } + } + + if let Some(dps) = opts.round_votes { + votes_transferred.floor_mut(dps); + } + + count_card.transfer(&votes_transferred); + count_card.ballot_transfers += ballots_transferred; + + checksum += votes_transferred; + } + + // Credit exhausted votes + let mut votes_transferred = N::new(); + let mut ballots_transferred = N::new(); + + for column in self.columns.iter() { + votes_transferred += column.exhausted.ballots.clone() * &column.value_fraction; + ballots_transferred += &column.exhausted.ballots; + } + + if let Some(dps) = opts.round_votes { + votes_transferred.floor_mut(dps); + } + + state.exhausted.transfer(&votes_transferred); + state.exhausted.ballot_transfers += ballots_transferred; + + checksum += votes_transferred; + + return checksum; + } +} + +/// Column in a [TransferTable] +pub struct TransferTableColumn<'e, N: Number> { + /// Value fraction of ballots counted in this column + pub value_fraction: N, + + /// Cells in this column + pub cells: HashMap<&'e Candidate, TransferTableCell>, + + /// Exhausted cell + pub exhausted: TransferTableCell, +} + +impl<'e, N: Number> TransferTableColumn<'e, N> { + /// Record the specified transfer + pub fn add_transfers(&mut self, candidate: &'e Candidate, ballots: &N) { + if let Some(cell) = self.cells.get_mut(candidate) { + cell.ballots += ballots; + } else { + let cell = TransferTableCell { + ballots: ballots.clone(), + }; + self.cells.insert(candidate, cell); + } + } +} + +/// Cell in a [TransferTable], representing transfers to one candidate at a particular value +pub struct TransferTableCell { + /// Ballots transferred to this candidate + pub ballots: N, +} diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 5ed1933..36095f2 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -240,6 +240,7 @@ impl STVOptions { min_threshold: String, constraints_path: Option, constraint_mode: &str, + transfers_detail: bool, pp_decimals: usize, ) -> Self { Self(stv::STVOptions::new( @@ -270,6 +271,7 @@ impl STVOptions { constraint_mode.into(), false, false, + transfers_detail, pp_decimals, )) }