Implement --transfers-detail

This commit is contained in:
RunasSudo 2021-09-11 18:42:15 +10:00
parent 056242514d
commit 9817d6c199
No known key found for this signature in database
GPG Key ID: 7234E476BF21C61A
9 changed files with 682 additions and 266 deletions

158
Cargo.lock generated
View File

@ -14,7 +14,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.3",
"once_cell", "once_cell",
"version_check", "version_check",
] ]
@ -28,6 +28,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.5.2" version = "0.5.2"
@ -48,6 +54,17 @@ dependencies = [
"wait-timeout", "wait-timeout",
] ]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.0.1" version = "1.0.1"
@ -60,12 +77,29 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "822d7d63e0c0260a050f6b1f0d316f5c79b9eab830aca526ed904e1011bd64ca" checksum = "822d7d63e0c0260a050f6b1f0d316f5c79b9eab830aca526ed904e1011bd64ca"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "blake2b_simd"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
dependencies = [
"arrayref",
"arrayvec",
"constant_time_eq",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.9.0" version = "0.9.0"
@ -174,6 +208,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27d614f23f34f7b5165a77dc1591f497e2518f9cec4b4f4b92bfc4dc6cf7a190" checksum = "27d614f23f34f7b5165a77dc1591f497e2518f9cec4b4f4b92bfc4dc6cf7a190"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -198,6 +238,16 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
[[package]]
name = "crossbeam-utils"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [
"cfg-if 1.0.0",
"lazy_static",
]
[[package]] [[package]]
name = "csv" name = "csv"
version = "1.1.6" version = "1.1.6"
@ -314,6 +364,17 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "dirs"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]] [[package]]
name = "doc-comment" name = "doc-comment"
version = "0.3.3" version = "0.3.3"
@ -326,6 +387,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.20" version = "1.0.20"
@ -363,6 +430,17 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.3"
@ -371,7 +449,7 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"wasi", "wasi 0.10.2+wasi-snapshot-preview1",
] ]
[[package]] [[package]]
@ -430,6 +508,15 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "ibig" name = "ibig"
version = "0.3.2" version = "0.3.2"
@ -632,6 +719,7 @@ dependencies = [
"num-traits", "num-traits",
"paste", "paste",
"predicates", "predicates",
"prettytable-rs",
"rkyv", "rkyv",
"rug", "rug",
"sha2", "sha2",
@ -690,6 +778,20 @@ dependencies = [
"treeline", "treeline",
] ]
[[package]]
name = "prettytable-rs"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e"
dependencies = [
"atty",
"csv",
"encode_unicode",
"lazy_static",
"term",
"unicode-width",
]
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
version = "1.0.4" version = "1.0.4"
@ -779,6 +881,23 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_users"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
dependencies = [
"getrandom 0.1.16",
"redox_syscall",
"rust-argon2",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.4" version = "1.5.4"
@ -850,6 +969,18 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "rust-argon2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
dependencies = [
"base64",
"blake2b_simd",
"constant_time_eq",
"crossbeam-utils",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.3.3" version = "0.3.3"
@ -931,6 +1062,17 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "term"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
dependencies = [
"byteorder",
"dirs",
"winapi",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.13.4" version = "0.13.4"
@ -961,6 +1103,12 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"
@ -997,6 +1145,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.2+wasi-snapshot-preview1" version = "0.10.2+wasi-snapshot-preview1"

View File

@ -32,6 +32,7 @@ paste = "1.0.5"
assert_cmd = "1.0.5" assert_cmd = "1.0.5"
csv = "1.1.6" csv = "1.1.6"
flate2 = "1.0" flate2 = "1.0"
prettytable-rs = "0.8.0"
rkyv = "0.7.15" rkyv = "0.7.15"
utf8-chars = "1.0.2" utf8-chars = "1.0.2"
xmltree = "0.10.3" xmltree = "0.10.3"

View File

@ -391,13 +391,20 @@ fn print_stage<N: Number>(stage_num: u32, state: &CountState<N>, opts: &STVOptio
println!("{}. {}", stage_num, state.title); println!("{}. {}", stage_num, state.title);
println!("{}", state.logger.render().join(" ")); println!("{}", state.logger.render().join(" "));
if opts.transfers_detail {
if let Some(tt) = &state.transfer_table {
println!();
println!("{}", tt.render_text(state, opts));
}
}
// Print candidates // Print candidates
print!("{}", state.describe_candidates(opts)); print!("{}", state.describe_candidates(opts));
// Print summary rows // Print summary rows
print!("{}", state.describe_summary(opts)); print!("{}", state.describe_summary(opts));
println!(""); println!();
} }
// ---------------------------------- // ----------------------------------

View File

@ -20,8 +20,8 @@ use crate::logger::Logger;
use crate::numbers::Number; use crate::numbers::Number;
use crate::sharandom::SHARandom; use crate::sharandom::SHARandom;
use crate::stv::{self, STVOptions}; use crate::stv::{self, STVOptions};
use crate::stv::gregory::TransferTable;
use crate::stv::meek::BallotTree; use crate::stv::meek::BallotTree;
use crate::stv::transfers::TransferTable;
use itertools::Itertools; use itertools::Itertools;

View File

@ -15,13 +15,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
mod transfers;
pub use transfers::{TransferTable, TransferTableCell, TransferTableColumn};
use super::{ExclusionMethod, STVError, STVOptions, SurplusMethod, SurplusOrder}; use super::{ExclusionMethod, STVError, STVOptions, SurplusMethod, SurplusOrder};
use super::sample; use super::sample;
use crate::constraints; use crate::constraints;
use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote}; use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote};
use crate::numbers::Number; use crate::numbers::Number;
use crate::stv::transfers::TransferTable;
use crate::ties; use crate::ties;
use std::cmp::max; use std::cmp::max;
@ -299,7 +301,7 @@ where
// Reweight and transfer parcels // Reweight and transfer parcels
let mut transfer_table = TransferTable::new(); let mut transfer_table = TransferTable::new_surplus(surplus.clone(), surplus_fraction.clone(), surplus_numer.clone(), surplus_denom.clone());
for (value_fraction, result) in parcels_next_prefs { for (value_fraction, result) in parcels_next_prefs {
for (candidate, entry) in result.candidates.into_iter() { for (candidate, entry) in result.candidates.into_iter() {
@ -350,7 +352,9 @@ where
let mut checksum = N::new(); let mut checksum = N::new();
// Credit transferred votes // Credit transferred votes
checksum += transfer_table.apply_to(state, opts, Some(&surplus), &surplus_numer, &surplus_denom); transfer_table.calculate(opts);
checksum += transfer_table.apply_to(state, opts);
state.transfer_table = Some(transfer_table);
// Finalise candidate votes // Finalise candidate votes
let count_card = state.candidates.get_mut(elected_candidate).unwrap(); let count_card = state.candidates.get_mut(elected_candidate).unwrap();
@ -549,7 +553,7 @@ where
let value = match parcels.first() { Some(p) => Some(p.value_fraction.clone()), _ => None }; let value = match parcels.first() { Some(p) => Some(p.value_fraction.clone()), _ => None };
let mut transfer_table = TransferTable::new(); let mut transfer_table = TransferTable::new_exclusion();
for parcel in parcels { for parcel in parcels {
// Count next preferences // Count next preferences
@ -603,7 +607,9 @@ where
} }
// Credit transferred votes // Credit transferred votes
checksum += transfer_table.apply_to(state, opts, None, &None, &None); transfer_table.calculate(opts);
checksum += transfer_table.apply_to(state, opts);
state.transfer_table = Some(transfer_table);
if !votes_remain { if !votes_remain {
// Finalise candidate votes // Finalise candidate votes

View File

@ -0,0 +1,499 @@
/* 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 prettytable::{Cell, Row, Table};
use crate::election::{Candidate, CountState};
use crate::numbers::Number;
use crate::stv::{STVOptions, SumSurplusTransfersMode};
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<TransferTableColumn<'e, N>>,
/// Total column
pub total: TransferTableColumn<'e, N>,
/// Size of surplus, or `None` if an exclusion
pub surplus: Option<N>,
/// Surplus fraction, or `None` if votes not reweighted/an exclusion (for display/optimisation only)
pub surpfrac: Option<N>,
/// Numerator of surplus fraction, or `None` if votes not reweighted/an exclusion
pub surpfrac_numer: Option<N>,
/// Denominator of surplus fraction, or `None`
pub surpfrac_denom: Option<N>,
}
impl<'e, N: Number> TransferTable<'e, N> {
/// Return a new [TransferTable] for an exclusion
pub fn new_exclusion() -> Self {
TransferTable {
columns: Vec::new(),
total: TransferTableColumn {
value_fraction: N::new(),
cells: HashMap::new(),
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
},
surplus: None,
surpfrac: None,
surpfrac_numer: None,
surpfrac_denom: None,
}
}
/// Return a new [TransferTable] for a surplus distribution
pub fn new_surplus(surplus: N, surpfrac: Option<N>, surpfrac_numer: Option<N>, surpfrac_denom: Option<N>) -> Self {
TransferTable {
columns: Vec::new(),
total: TransferTableColumn {
value_fraction: N::new(),
cells: HashMap::new(),
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
},
surplus: Some(surplus),
surpfrac,
surpfrac_numer,
surpfrac_denom,
}
}
/// 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(), votes_in: N::new(), votes_out: N::new() },
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: 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(), votes_in: N::new(), votes_out: N::new() },
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
};
self.columns.push(col);
}
/// Calculate the votes to be transferred according to this table
pub fn calculate(&mut self, opts: &STVOptions) {
// Use weighted rules if exclusion or WIGM
let is_weighted = self.surplus.is_none() || opts.surplus.is_weighted();
// Iterate through columns
for column in self.columns.iter_mut() {
let mut new_value_fraction = N::new();
if self.surplus.is_some() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
if is_weighted {
new_value_fraction = column.value_fraction.clone();
// If surplus, multiply by surplus fraction
if let Some(n) = &self.surpfrac_numer {
new_value_fraction *= n;
}
if let Some(n) = &self.surpfrac_denom {
new_value_fraction /= n;
}
// Round if required
if let Some(dps) = opts.round_values {
new_value_fraction.floor_mut(dps);
}
} else {
if let Some(n) = &self.surpfrac_numer {
new_value_fraction = n.clone();
} else {
// Transferred at original value
new_value_fraction = column.value_fraction.clone();
}
if let Some(n) = &self.surpfrac_denom {
new_value_fraction /= n;
}
// Round if required
if let Some(dps) = opts.round_values {
new_value_fraction.floor_mut(dps);
}
}
}
// Candidate votes
for (candidate, cell) in column.cells.iter_mut() {
column.total.ballots += &cell.ballots;
self.total.add_transfers(*candidate, &cell.ballots);
self.total.total.ballots += &cell.ballots;
let votes_in = cell.ballots.clone() * &column.value_fraction;
cell.votes_in += &votes_in;
column.total.votes_in += &votes_in;
self.total.cells.get_mut(*candidate).unwrap().votes_in += &votes_in;
self.total.total.votes_in += votes_in;
if self.surplus.is_some() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
let votes_out = cell.ballots.clone() * &new_value_fraction;
cell.votes_out += &votes_out;
column.total.votes_out += &votes_out;
self.total.cells.get_mut(*candidate).unwrap().votes_out += &votes_out;
self.total.total.votes_out += votes_out;
}
}
// Exhausted votes
column.total.ballots += &column.exhausted.ballots;
self.total.exhausted.ballots += &column.exhausted.ballots;
self.total.total.ballots += &column.exhausted.ballots;
let votes_in = column.exhausted.ballots.clone() * &column.value_fraction;
column.exhausted.votes_in += &votes_in;
column.total.votes_in += &votes_in;
self.total.exhausted.votes_in += &votes_in;
self.total.total.votes_in += votes_in;
if self.surplus.is_some() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
if !opts.transferable_only {
let votes_out = column.exhausted.ballots.clone() * &new_value_fraction;
column.exhausted.votes_out += &votes_out;
column.total.votes_out += &votes_out;
self.total.exhausted.votes_out += &votes_out;
self.total.total.votes_out += votes_out;
}
}
}
// Need to calculate votes_out?
if self.surplus.is_none() || opts.sum_surplus_transfers == SumSurplusTransfersMode::ByValue {
for (_candidate, cell) in self.total.cells.iter_mut() {
let mut votes_out;
if is_weighted {
votes_out = cell.votes_in.clone();
} else {
votes_out = cell.ballots.clone();
}
// If surplus, multiply by surplus fraction
if let Some(n) = &self.surpfrac_numer {
votes_out *= n;
}
if let Some(n) = &self.surpfrac_denom {
votes_out /= n;
}
cell.votes_out = votes_out; // Rounded later
}
if self.surplus.is_none() || !opts.transferable_only {
let mut votes_out;
if is_weighted {
votes_out = self.total.exhausted.votes_in.clone();
} else {
votes_out = self.total.exhausted.ballots.clone();
}
// If surplus, multiply by surplus fraction
if let Some(n) = &self.surpfrac_numer {
votes_out *= n;
}
if let Some(n) = &self.surpfrac_denom {
votes_out /= n;
}
self.total.exhausted.votes_out = votes_out; // Rounded later
}
}
// Round if required
if let Some(dps) = opts.round_votes {
for (_candidate, cell) in self.total.cells.iter_mut() {
cell.votes_out.floor_mut(dps);
}
self.total.exhausted.votes_out.floor_mut(dps);
}
}
/// 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<N>, opts: &STVOptions) -> N {
let mut checksum = N::new();
// Credit transferred votes
for (candidate, count_card) in state.candidates.iter_mut() {
if let Some(cell) = self.total.cells.get(candidate) {
count_card.transfer(&cell.votes_out);
count_card.ballot_transfers += &cell.ballots;
checksum += &cell.votes_out;
}
}
// Credit exhausted votes
// If exclusion or not --transferable-only
if self.surplus.is_none() || !opts.transferable_only {
// Standard rules
state.exhausted.transfer(&self.total.exhausted.votes_out);
state.exhausted.ballot_transfers += &self.total.exhausted.ballots;
checksum += &self.total.exhausted.votes_out;
} else {
// Credit only nontransferable difference
if self.surpfrac_numer.is_none() {
// TODO: Is there a purer way of calculating this?
let difference = self.surplus.as_ref().unwrap().clone() - &checksum;
state.exhausted.transfer(&difference);
checksum += difference;
for column in self.columns.iter() {
state.exhausted.ballot_transfers += &column.exhausted.ballots;
}
} else {
// No ballots exhaust
}
}
return checksum;
}
/// Render table as plain text
pub fn render_text(&self, state: &CountState<N>, opts: &STVOptions) -> String {
let mut table = Table::new();
table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
let show_transfers_per_ballot = !self.surpfrac.is_none() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot;
let num_cols;
if show_transfers_per_ballot {
num_cols = state.candidates.len() * 3 + 4;
} else {
if self.surpfrac.is_none() {
num_cols = state.candidates.len() * 2 + 3;
} else {
num_cols = state.candidates.len() * 2 + 4;
}
}
// ----------
// Header row
let mut row = Vec::with_capacity(num_cols);
row.push(Cell::new("Preference"));
for column in self.columns.iter() {
row.push(Cell::new(&format!("Ballots @ {:.dps$}", column.value_fraction, dps=opts.pp_decimals)).style_spec("cH2"));
if show_transfers_per_ballot {
row.push(Cell::new(&format!("× {:.dps$}", self.surpfrac.as_ref().unwrap(), dps=opts.pp_decimals)).style_spec("r"));
}
}
row.push(Cell::new("Total").style_spec("cH2"));
if self.surpfrac.is_some() {
row.push(Cell::new(&format!("× {:.dps$}", self.surpfrac.as_ref().unwrap(), dps=opts.pp_decimals)).style_spec("r"));
}
table.set_titles(Row::new(row));
// --------------
// Candidate rows
for candidate in state.election.candidates.iter() {
let mut row = Vec::with_capacity(num_cols);
row.push(Cell::new(&candidate.name));
for column in self.columns.iter() {
if let Some(cell) = column.cells.get(candidate) {
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
if show_transfers_per_ballot {
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
}
} else {
row.push(Cell::new(""));
row.push(Cell::new(""));
if show_transfers_per_ballot {
row.push(Cell::new(""));
}
}
}
// Totals
if let Some(cell) = self.total.cells.get(candidate) {
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
if self.surpfrac.is_some() {
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
}
} else {
row.push(Cell::new(""));
row.push(Cell::new(""));
if self.surpfrac.is_some() {
row.push(Cell::new(""));
}
}
table.add_row(Row::new(row));
}
// -------------
// Exhausted row
let mut row = Vec::with_capacity(num_cols);
row.push(Cell::new("Exhausted"));
for column in self.columns.iter() {
if !column.exhausted.ballots.is_zero() {
row.push(Cell::new(&format!("{:.0}", column.exhausted.ballots)).style_spec("r"));
row.push(Cell::new(&format!("{:.dps$}", column.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
if show_transfers_per_ballot {
if column.exhausted.votes_out.is_zero() {
row.push(Cell::new("-").style_spec("c"));
} else {
row.push(Cell::new(&format!("{:.dps$}", column.exhausted.votes_out, dps=opts.pp_decimals)).style_spec("r"));
}
}
} else {
row.push(Cell::new(""));
row.push(Cell::new(""));
if show_transfers_per_ballot {
row.push(Cell::new(""));
}
}
}
// Totals
if !self.total.exhausted.ballots.is_zero() {
row.push(Cell::new(&format!("{:.0}", self.total.exhausted.ballots)).style_spec("r"));
row.push(Cell::new(&format!("{:.dps$}", self.total.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
if self.surpfrac.is_some() {
if self.total.exhausted.votes_out.is_zero() {
row.push(Cell::new("-").style_spec("c"));
} else {
row.push(Cell::new(&format!("{:.dps$}", self.total.exhausted.votes_out, dps=opts.pp_decimals)).style_spec("r"));
}
}
} else {
row.push(Cell::new(""));
row.push(Cell::new(""));
if self.surpfrac.is_some() {
row.push(Cell::new(""));
}
}
table.add_row(Row::new(row));
// ----------
// Totals row
let mut row = Vec::with_capacity(num_cols);
row.push(Cell::new("Total"));
for column in self.columns.iter() {
row.push(Cell::new(&format!("{:.0}", column.total.ballots)).style_spec("r"));
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_in, dps=opts.pp_decimals)).style_spec("r"));
if show_transfers_per_ballot {
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_out, dps=opts.pp_decimals)).style_spec("r"));
}
}
// Grand total cell
let mut gt_ballots = N::new();
let mut gt_votes_in = N::new();
let mut gt_votes_out = N::new();
for candidate in state.election.candidates.iter() {
if let Some(cell) = self.total.cells.get(candidate) {
gt_ballots += &cell.ballots;
gt_votes_in += &cell.votes_in;
gt_votes_out += &cell.votes_out;
}
}
gt_ballots += &self.total.exhausted.ballots;
gt_votes_in += &self.total.exhausted.votes_in;
gt_votes_out += &self.total.exhausted.votes_out;
row.push(Cell::new(&format!("{:.0}", gt_ballots)).style_spec("r"));
row.push(Cell::new(&format!("{:.dps$}", gt_votes_in, dps=opts.pp_decimals)).style_spec("r"));
if self.surpfrac.is_some() {
row.push(Cell::new(&format!("{:.dps$}", gt_votes_out, dps=opts.pp_decimals)).style_spec("r"));
}
table.add_row(Row::new(row));
return table.to_string();
}
/// Render table as HTML
pub fn render_html(&self) -> String {
todo!();
}
}
/// 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<N>>,
/// Exhausted cell
pub exhausted: TransferTableCell<N>,
/// Totals cell
pub total: TransferTableCell<N>,
}
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(),
votes_in: N::new(),
votes_out: N::new(),
};
self.cells.insert(candidate, cell);
}
}
}
/// Cell in a [TransferTable], representing transfers to one candidate at a particular value
pub struct TransferTableCell<N: Number> {
/// Ballots expressing a next preference for the continuing candidate
pub ballots: N,
/// Value of votes when received by the transferring candidate
pub votes_in: N,
/// Votes transferred to the continuing candidate
pub votes_out: N,
}

View File

@ -23,8 +23,6 @@ pub mod gregory;
pub mod meek; pub mod meek;
/// Random sample methods of surplus distributions /// Random sample methods of surplus distributions
pub mod sample; pub mod sample;
/// Transfer tables
pub mod transfers;
/// WebAssembly wrappers /// WebAssembly wrappers
//#[cfg(target_arch = "wasm32")] //#[cfg(target_arch = "wasm32")]
@ -149,15 +147,15 @@ pub struct STVOptions {
#[builder(default="ConstraintMode::GuardDoom")] #[builder(default="ConstraintMode::GuardDoom")]
pub constraint_mode: ConstraintMode, pub constraint_mode: ConstraintMode,
/// Hide excluded candidates from results report /// (CLI) Hide excluded candidates from results report
#[builder(default="false")] #[builder(default="false")]
pub hide_excluded: bool, pub hide_excluded: bool,
/// Sort candidates by votes in results report /// (CLI) Sort candidates by votes in results report
#[builder(default="false")] #[builder(default="false")]
pub sort_votes: bool, pub sort_votes: bool,
/// Show details of transfers to candidates during surplus distributions/candidate exclusions /// (CLI) Show details of transfers to candidates during surplus distributions/candidate exclusions
#[builder(default="false")] #[builder(default="false")]
pub transfers_detail: bool, pub transfers_detail: bool,
@ -206,6 +204,7 @@ impl STVOptions {
} }
if self.hide_excluded { flags.push(format!("--hide-excluded")); } if self.hide_excluded { flags.push(format!("--hide-excluded")); }
if self.sort_votes { flags.push(format!("--sort-votes")); } if self.sort_votes { flags.push(format!("--sort-votes")); }
if self.transfers_detail { flags.push(format!("--transfers-detail")); }
if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); } if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); }
return flags.join(" "); return flags.join(" ");
} }
@ -641,6 +640,7 @@ where
for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>, for<'r> &'r N: ops::Neg<Output=N>,
{ {
state.transfer_table = None;
state.logger.entries.clear(); state.logger.entries.clear();
state.step_all(); state.step_all();

View File

@ -1,250 +0,0 @@
/* 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 super::{STVOptions, SumSurplusTransfersMode};
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<TransferTableColumn<'e, N>>,
}
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<N>, opts: &STVOptions, surplus: Option<&N>, surplus_numer: &Option<N>, surplus_denom: &Option<N>) -> N {
// Use weighted rules if exclusion or WIGM
let is_weighted = surplus.is_none() || opts.surplus.is_weighted();
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();
// If exclusion, or surplus at present value, or SumSurplusTransfersMode::ByValue
if surplus_numer.is_none() || opts.sum_surplus_transfers == SumSurplusTransfersMode::ByValue {
// Calculate transfer across all votes in this parcel
for column in self.columns.iter() {
if let Some(cell) = column.cells.get(*candidate) {
if is_weighted {
votes_transferred += cell.ballots.clone() * &column.value_fraction;
}
ballots_transferred += &cell.ballots;
}
}
if !is_weighted {
votes_transferred = ballots_transferred.clone();
}
// If surplus, multiply by surplus fraction
if let Some(n) = &surplus_numer {
votes_transferred *= n;
}
if let Some(n) = &surplus_denom {
votes_transferred /= n;
}
} else if opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
// Sum transfer per each individual ballot
for column in self.columns.iter() {
if let Some(cell) = column.cells.get(*candidate) {
ballots_transferred += &cell.ballots;
let mut new_value_fraction;
if is_weighted {
new_value_fraction = column.value_fraction.clone();
// If surplus, multiply by surplus fraction
if let Some(n) = &surplus_numer {
new_value_fraction *= n;
}
if let Some(n) = &surplus_denom {
new_value_fraction /= n;
}
// Round if required
if let Some(dps) = opts.round_values {
new_value_fraction.floor_mut(dps);
}
} else {
if let Some(n) = &surplus_numer {
new_value_fraction = n.clone();
} else {
// Transferred at original value
new_value_fraction = column.value_fraction.clone();
}
if let Some(n) = &surplus_denom {
new_value_fraction /= n;
}
// Round if required
if let Some(dps) = opts.round_values {
new_value_fraction.floor_mut(dps);
}
}
votes_transferred += cell.ballots.clone() * new_value_fraction;
}
}
} else {
unreachable!();
}
// Round if required
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
// If exclusion or not --transferable-only
if surplus.is_none() || !opts.transferable_only {
// Standard rules
let mut votes_transferred = N::new();
let mut ballots_transferred = N::new();
for column in self.columns.iter() {
if is_weighted {
votes_transferred += column.exhausted.ballots.clone() * &column.value_fraction;
}
ballots_transferred += &column.exhausted.ballots;
}
if !is_weighted {
votes_transferred = ballots_transferred.clone();
}
// If surplus, multiply by surplus fraction
if let Some(n) = &surplus_numer {
votes_transferred *= n;
}
if let Some(n) = &surplus_denom {
votes_transferred /= n;
}
// Round if required
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;
} else {
// Credit only nontransferable difference
if surplus_numer.is_none() {
// TODO: Is there a purer way of calculating this?
let difference = surplus.unwrap().clone() - &checksum;
state.exhausted.transfer(&difference);
checksum += difference;
for column in self.columns.iter() {
state.exhausted.ballot_transfers += &column.exhausted.ballots;
}
} else {
// No ballots exhaust
}
}
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<N>>,
/// Exhausted cell
pub exhausted: TransferTableCell<N>,
}
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<N: Number> {
/// Ballots transferred to this candidate
pub ballots: N,
}

View File

@ -240,7 +240,6 @@ impl STVOptions {
min_threshold: String, min_threshold: String,
constraints_path: Option<String>, constraints_path: Option<String>,
constraint_mode: &str, constraint_mode: &str,
transfers_detail: bool,
pp_decimals: usize, pp_decimals: usize,
) -> Self { ) -> Self {
Self(stv::STVOptions::new( Self(stv::STVOptions::new(
@ -271,7 +270,7 @@ impl STVOptions {
constraint_mode.into(), constraint_mode.into(),
false, false,
false, false,
transfers_detail, false,
pp_decimals, pp_decimals,
)) ))
} }