From 4ebb6474fdcfa2aeb733e5454f24ef2060f97442 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 16 Jun 2021 13:00:54 +1000 Subject: [PATCH] Implement Meek STV --- html/index.html | 4 +- html/index.js | 21 +++ html/worker.js | 2 +- src/election.rs | 26 ++-- src/main.rs | 8 +- src/numbers/fixed.rs | 12 +- src/numbers/gfixed.rs | 12 +- src/numbers/native.rs | 12 +- src/numbers/rational_num.rs | 12 +- src/numbers/rational_rug.rs | 12 +- src/stv/gregory.rs | 191 ++++++++++++++++++++++- src/stv/meek.rs | 301 ++++++++++++++++++++++++++++++++++++ src/stv/mod.rs | 217 +++++--------------------- src/stv/wasm.rs | 18 ++- tests/utils/mod.rs | 1 + 15 files changed, 604 insertions(+), 245 deletions(-) create mode 100644 src/stv/meek.rs diff --git a/html/index.html b/html/index.html index d014ead..e6e8afc 100644 --- a/html/index.html +++ b/html/index.html @@ -37,9 +37,9 @@ diff --git a/html/index.js b/html/index.js index aea0df2..c090828 100644 --- a/html/index.js +++ b/html/index.js @@ -353,6 +353,27 @@ function changePreset() { document.getElementById('selPapers').value = 'both'; document.getElementById('selExclusion').value = 'single_stage'; document.getElementById('selTies').value = 'backwards,random'; + } else if (document.getElementById('selPreset').value === 'meek') { + document.getElementById('selQuotaCriterion').value = 'gt'; + document.getElementById('selQuota').value = 'droop_exact'; + document.getElementById('selQuotaMode').value = 'static'; + document.getElementById('chkBulkElection').checked = true; + document.getElementById('chkBulkExclusion').checked = false; + document.getElementById('chkDeferSurpluses').checked = false; + document.getElementById('selNumbers').value = 'fixed'; + document.getElementById('txtDP').value = '5'; + document.getElementById('txtPPDP').value = '2'; + document.getElementById('chkNormaliseBallots').checked = false; + document.getElementById('chkRoundQuota').checked = false; + document.getElementById('chkRoundVotes').checked = false; + document.getElementById('chkRoundTVs').checked = false; + document.getElementById('chkRoundWeights').checked = false; + document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSurplus').value = 'by_size'; + document.getElementById('selTransfers').value = 'meek'; + document.getElementById('selPapers').value = 'both'; + document.getElementById('selExclusion').value = 'single_stage'; + document.getElementById('selTies').value = 'backwards,random'; } else if (document.getElementById('selPreset').value === 'senate') { document.getElementById('selQuotaCriterion').value = 'geq'; document.getElementById('selQuota').value = 'droop'; diff --git a/html/worker.js b/html/worker.js index 10ea1a1..7d5ef5f 100644 --- a/html/worker.js +++ b/html/worker.js @@ -79,7 +79,7 @@ function resume_count() { } postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)}); - postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state)}); + postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)}); } var user_input_buffer = null; diff --git a/src/election.rs b/src/election.rs index 08b8541..a8ddb60 100644 --- a/src/election.rs +++ b/src/election.rs @@ -129,14 +129,16 @@ pub struct Candidate { } /// The current state of counting an [Election] -#[derive(Clone)] -pub struct CountState<'a, N> { +//#[derive(Clone)] +pub struct CountState<'a, N: Number> { pub election: &'a Election, pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>, pub exhausted: CountCard<'a, N>, pub loss_fraction: CountCard<'a, N>, + pub ballot_tree: Option>, + pub forwards_tiebreak: Option>, pub backwards_tiebreak: Option>, pub random: Option>, @@ -160,6 +162,7 @@ impl<'a, N: Number> CountState<'a, N> { candidates: HashMap::new(), exhausted: CountCard::new(), loss_fraction: CountCard::new(), + ballot_tree: None, forwards_tiebreak: None, backwards_tiebreak: None, random: None, @@ -195,12 +198,12 @@ impl<'a, N: Number> CountState<'a, N> { /// Represents either a reference to a [CountState] or a clone #[allow(dead_code)] -pub enum CountStateOrRef<'a, N> { +pub enum CountStateOrRef<'a, N: Number> { State(CountState<'a, N>), // NYI: May be used e.g. for tie-breaking or rollback-based constraints Ref(&'a CountState<'a, N>), } -impl<'a, N> CountStateOrRef<'a, N> { +impl<'a, N: Number> CountStateOrRef<'a, N> { /// Construct a [CountStateOrRef] as a reference to a [CountState] pub fn from(state: &'a CountState) -> Self { return Self::Ref(state); @@ -216,7 +219,7 @@ impl<'a, N> CountStateOrRef<'a, N> { } /// Result of a stage of counting -pub struct StageResult<'a, N> { +pub struct StageResult<'a, N: Number> { pub kind: Option<&'a str>, pub title: &'a String, pub logs: Vec, @@ -229,11 +232,14 @@ pub struct CountCard<'a, N> { pub state: CandidateState, pub order_elected: isize, - pub orig_votes: N, + //pub orig_votes: N, pub transfers: N, pub votes: N, pub parcels: Vec>, + + /// Candidate's keep value (Meek STV) + pub keep_value: Option, } impl<'a, N: Number> CountCard<'a, N> { @@ -242,10 +248,11 @@ impl<'a, N: Number> CountCard<'a, N> { return CountCard { state: CandidateState::Hopeful, order_elected: 0, - orig_votes: N::new(), + //orig_votes: N::new(), transfers: N::new(), votes: N::new(), parcels: Vec::new(), + keep_value: None, }; } @@ -255,9 +262,9 @@ impl<'a, N: Number> CountCard<'a, N> { self.votes += transfer; } - /// Set [orig_votes](CountCard::orig_votes) to [votes](CountCard::votes), and set [transfers](CountCard::transfers) to 0 + /// Set [transfers](CountCard::transfers) to 0 pub fn step(&mut self) { - self.orig_votes = self.votes.clone(); + //self.orig_votes = self.votes.clone(); self.transfers = N::new(); } } @@ -270,6 +277,7 @@ pub type Parcel<'a, N> = Vec>; pub struct Vote<'a, N> { pub ballot: &'a Ballot, pub value: N, + /// Index of the next preference to examine pub up_to_pref: usize, } diff --git a/src/main.rs b/src/main.rs index 693b6f3..3e02513 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ use opentally::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational}; use clap::{AppSettings, Clap}; +use std::cmp::max; use std::fs::File; use std::io::{self, BufRead}; use std::ops; @@ -185,6 +186,7 @@ fn main() { fn count_election(mut election: Election, cmd_opts: STV) where for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg { @@ -261,7 +263,11 @@ where fn print_candidates<'a, N: 'a + Number, I: Iterator)>>(candidates: I, cmd_opts: &STV) { for (candidate, count_card) in candidates { if count_card.state == CandidateState::Elected { - println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals); + if let Some(kv) = &count_card.keep_value { + println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {} (kv = {:.dps2$})", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, kv, dps=cmd_opts.pp_decimals, dps2=max(cmd_opts.pp_decimals, 2)); + } else { + println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals); + } } else if count_card.state == CandidateState::Excluded { // If --hide-excluded, hide unless nonzero votes or nonzero transfers if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() { diff --git a/src/numbers/fixed.rs b/src/numbers/fixed.rs index af951e9..5b5470e 100644 --- a/src/numbers/fixed.rs +++ b/src/numbers/fixed.rs @@ -153,9 +153,7 @@ impl ops::Neg for Fixed { impl ops::Add for Fixed { type Output = Self; - fn add(self, _rhs: Self) -> Self::Output { - todo!() - } + fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) } } impl ops::Sub for Fixed { @@ -174,9 +172,7 @@ impl ops::Mul for Fixed { impl ops::Div for Fixed { type Output = Self; - fn div(self, _rhs: Self) -> Self::Output { - todo!() - } + fn div(self, rhs: Self) -> Self::Output { Self(self.0 * get_factor() / rhs.0) } } impl ops::Rem for Fixed { @@ -285,9 +281,7 @@ impl ops::Sub for &Fixed { impl ops::Mul for &Fixed { type Output = Fixed; - fn mul(self, _rhs: Self) -> Self::Output { - todo!() - } + fn mul(self, rhs: Self) -> Self::Output { Fixed(&self.0 * &rhs.0 / get_factor()) } } impl ops::Div for &Fixed { diff --git a/src/numbers/gfixed.rs b/src/numbers/gfixed.rs index 5c09a53..ec10a9e 100644 --- a/src/numbers/gfixed.rs +++ b/src/numbers/gfixed.rs @@ -186,9 +186,7 @@ impl ops::Neg for GuardedFixed { impl ops::Add for GuardedFixed { type Output = Self; - fn add(self, _rhs: Self) -> Self::Output { - todo!() - } + fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) } } impl ops::Sub for GuardedFixed { @@ -207,9 +205,7 @@ impl ops::Mul for GuardedFixed { impl ops::Div for GuardedFixed { type Output = Self; - fn div(self, _rhs: Self) -> Self::Output { - todo!() - } + fn div(self, rhs: Self) -> Self::Output { Self(self.0 * get_factor() / rhs.0) } } impl ops::Rem for GuardedFixed { @@ -318,9 +314,7 @@ impl ops::Sub for &GuardedFixed { impl ops::Mul for &GuardedFixed { type Output = GuardedFixed; - fn mul(self, _rhs: Self) -> Self::Output { - todo!() - } + fn mul(self, rhs: Self) -> Self::Output { GuardedFixed(&self.0 * &rhs.0 / get_factor()) } } impl ops::Div for &GuardedFixed { diff --git a/src/numbers/native.rs b/src/numbers/native.rs index 5e5fba4..e036db5 100644 --- a/src/numbers/native.rs +++ b/src/numbers/native.rs @@ -97,9 +97,7 @@ impl ops::Neg for NativeFloat64 { impl ops::Add for NativeFloat64 { type Output = NativeFloat64; - fn add(self, _rhs: Self) -> Self::Output { - todo!() - } + fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) } } impl ops::Sub for NativeFloat64 { @@ -118,9 +116,7 @@ impl ops::Mul for NativeFloat64 { impl ops::Div for NativeFloat64 { type Output = NativeFloat64; - fn div(self, _rhs: Self) -> Self::Output { - todo!() - } + fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) } } impl ops::Rem for NativeFloat64 { @@ -220,9 +216,7 @@ impl ops::Sub for &NativeFloat64 { impl ops::Mul for &NativeFloat64 { type Output = NativeFloat64; - fn mul(self, _rhs: &NativeFloat64) -> Self::Output { - todo!() - } + fn mul(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 * &rhs.0) } } impl ops::Div for &NativeFloat64 { diff --git a/src/numbers/rational_num.rs b/src/numbers/rational_num.rs index dd58f03..23e6c8d 100644 --- a/src/numbers/rational_num.rs +++ b/src/numbers/rational_num.rs @@ -144,9 +144,7 @@ impl ops::Neg for Rational { impl ops::Add for Rational { type Output = Rational; - fn add(self, _rhs: Self) -> Self::Output { - todo!() - } + fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) } } impl ops::Sub for Rational { @@ -165,9 +163,7 @@ impl ops::Mul for Rational { impl ops::Div for Rational { type Output = Rational; - fn div(self, _rhs: Self) -> Self::Output { - todo!() - } + fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) } } impl ops::Rem for Rational { @@ -267,9 +263,7 @@ impl ops::Sub for &Rational { impl ops::Mul for &Rational { type Output = Rational; - fn mul(self, _rhs: &Rational) -> Self::Output { - todo!() - } + fn mul(self, rhs: &Rational) -> Self::Output { Rational(&self.0 * &rhs.0) } } impl ops::Div for &Rational { diff --git a/src/numbers/rational_rug.rs b/src/numbers/rational_rug.rs index 9c3e018..9252137 100644 --- a/src/numbers/rational_rug.rs +++ b/src/numbers/rational_rug.rs @@ -143,9 +143,7 @@ impl ops::Neg for Rational { impl ops::Add for Rational { type Output = Self; - fn add(self, _rhs: Self) -> Self::Output { - todo!() - } + fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) } } impl ops::Sub for Rational { @@ -164,9 +162,7 @@ impl ops::Mul for Rational { impl ops::Div for Rational { type Output = Self; - fn div(self, _rhs: Self) -> Self::Output { - todo!() - } + fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) } } impl ops::Rem for Rational { @@ -266,9 +262,7 @@ impl ops::Sub for &Rational { impl ops::Mul for &Rational { type Output = Rational; - fn mul(self, _rhs: Self) -> Self::Output { - todo!() - } + fn mul(self, rhs: Self) -> Self::Output { Rational(rug::Rational::from(&self.0 * &rhs.0)) } } impl ops::Div for &Rational { diff --git a/src/stv/gregory.rs b/src/stv/gregory.rs index 6678d76..31b7ac1 100644 --- a/src/stv/gregory.rs +++ b/src/stv/gregory.rs @@ -15,9 +15,9 @@ * along with this program. If not, see . */ -use super::{NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder}; +use super::{ExclusionMethod, NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder}; -use crate::election::{Candidate, CountCard, CountState, Parcel, Vote}; +use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use crate::numbers::Number; use itertools::Itertools; @@ -25,6 +25,34 @@ use itertools::Itertools; use std::cmp::max; use std::ops; +/// Distribute first preference votes according to the Gregory method +pub fn distribute_first_preferences(state: &mut CountState) { + let votes = state.election.ballots.iter().map(|b| Vote { + ballot: b, + value: b.orig_value.clone(), + up_to_pref: 0, + }).collect(); + + let result = super::next_preferences(state, votes); + + // Transfer candidate votes + for (candidate, entry) in result.candidates.into_iter() { + let parcel = entry.votes as Parcel; + let count_card = state.candidates.get_mut(candidate).unwrap(); + count_card.parcels.push(parcel); + count_card.transfer(&entry.num_votes); + } + + // Transfer exhausted votes + let parcel = result.exhausted.votes as Parcel; + state.exhausted.parcels.push(parcel); + state.exhausted.transfer(&result.exhausted.num_votes); + + state.kind = None; + state.title = "First preferences".to_string(); + state.logger.log_literal("First preferences distributed.".to_string()); +} + /// Distribute the largest surplus according to the Gregory method, based on [STVOptions::surplus] pub fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> Result where @@ -37,10 +65,11 @@ where .map(|c| (c, state.candidates.get(c).unwrap())) .filter(|(_, cc)| &cc.votes > quota) .collect(); - let total_surpluses = has_surplus.iter() - .fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota); if !has_surplus.is_empty() { + let total_surpluses = has_surplus.iter() + .fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota); + // Determine if surplues can be deferred if opts.defer_surpluses { if super::can_defer_surpluses(state, opts, &total_surpluses) { @@ -311,3 +340,157 @@ where // Update loss by fraction state.loss_fraction.transfer(&-checksum); } + +/// Perform one stage of a candidate exclusion according to the Gregory method, based on [STVOptions::exclusion] +pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) +where + for<'r> &'r N: ops::Div<&'r N, Output=N>, +{ + // Used to give bulk excluded candidate the same order_elected + let order_excluded = state.num_excluded + 1; + + for excluded_candidate in excluded_candidates.iter() { + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + + // Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??! + if count_card.state != CandidateState::Excluded { + count_card.state = CandidateState::Excluded; + state.num_excluded += 1; + count_card.order_elected = -(order_excluded as isize); + } + } + + // Determine votes to transfer in this stage + let mut votes = Vec::new(); + let mut votes_remain; + let mut checksum = N::new(); + + match opts.exclusion { + ExclusionMethod::SingleStage => { + // Exclude in one round + for excluded_candidate in excluded_candidates.iter() { + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + votes.append(&mut count_card.parcels.concat()); + count_card.parcels.clear(); + + // Update votes + let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value); + checksum -= &votes_transferred; + count_card.transfer(&-votes_transferred); + } + votes_remain = false; + } + ExclusionMethod::ByValue => { + // Exclude by value + let max_value = excluded_candidates.iter() + .map(|c| state.candidates.get(c).unwrap().parcels.iter() + .map(|p| p.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap()) + .max().unwrap()) + .max().unwrap(); + + votes_remain = false; + + for excluded_candidate in excluded_candidates.iter() { + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + + // Filter out just those votes with max_value + let mut remaining_votes = Vec::new(); + + let cand_votes = count_card.parcels.concat(); + + let mut votes_transferred = N::new(); + for vote in cand_votes.into_iter() { + if &vote.value / &vote.ballot.orig_value == max_value { + votes_transferred += &vote.value; + votes.push(vote); + } else { + remaining_votes.push(vote); + } + } + + if !remaining_votes.is_empty() { + votes_remain = true; + } + + // Leave remaining votes with candidate (as one parcel) + count_card.parcels = vec![remaining_votes]; + + // Update votes + checksum -= &votes_transferred; + count_card.transfer(&-votes_transferred); + } + } + ExclusionMethod::ParcelsByOrder => { + // Exclude by parcel by order + if excluded_candidates.len() > 1 { + panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude"); + } + + let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap(); + votes = count_card.parcels.remove(0); + votes_remain = !count_card.parcels.is_empty(); + + // Update votes + let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value); + checksum -= &votes_transferred; + count_card.transfer(&-votes_transferred); + } + } + + if !votes.is_empty() { + let value = &votes[0].value / &votes[0].ballot.orig_value; + + // Count next preferences + let result = super::next_preferences(state, votes); + + if let ExclusionMethod::SingleStage = opts.exclusion { + state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals)); + } else { + state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); + } + + // Transfer candidate votes + for (candidate, entry) in result.candidates.into_iter() { + let parcel = entry.votes as Parcel; + let count_card = state.candidates.get_mut(candidate).unwrap(); + count_card.parcels.push(parcel); + + // Round transfers + let mut candidate_transfers = entry.num_votes; + if let Some(dps) = opts.round_votes { + candidate_transfers.floor_mut(dps); + } + count_card.transfer(&candidate_transfers); + checksum += candidate_transfers; + } + + // Transfer exhausted votes + let parcel = result.exhausted.votes as Parcel; + state.exhausted.parcels.push(parcel); + + let mut exhausted_transfers = result.exhausted.num_votes; + if let Some(dps) = opts.round_votes { + exhausted_transfers.floor_mut(dps); + } + state.exhausted.transfer(&exhausted_transfers); + checksum += exhausted_transfers; + } + + if !votes_remain { + // Finalise candidate votes + for excluded_candidate in excluded_candidates.into_iter() { + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + checksum -= &count_card.votes; + count_card.transfers -= &count_card.votes; + count_card.votes = N::new(); + } + + if let ExclusionMethod::SingleStage = opts.exclusion { + } else { + state.logger.log_literal("Exclusion complete.".to_string()); + } + } + + // Update loss by fraction + state.loss_fraction.transfer(&-checksum); +} diff --git a/src/stv/meek.rs b/src/stv/meek.rs new file mode 100644 index 0000000..50ad88d --- /dev/null +++ b/src/stv/meek.rs @@ -0,0 +1,301 @@ +/* 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::{STVError, STVOptions}; + +use crate::election::{Ballot, Candidate, CandidateState, CountCard, CountState, Election}; +use crate::numbers::Number; + +use itertools::Itertools; + +use std::cmp::max; +use std::collections::HashMap; +use std::ops; + +/// Ballot in a [BallotTree] +#[derive(Clone)] +struct BallotInTree<'b, N: Number> { + ballot: &'b Ballot, + /// Index of the next preference to examine + up_to_pref: usize, +} + +/// Tree-packed ballot representation +pub struct BallotTree<'t, N: Number> { + num_ballots: N, + ballots: Vec>, + next_preferences: Option>>>, + next_exhausted: Option>>, +} + +impl<'t, N: Number> BallotTree<'t, N> { + /// Construct a new empty [BallotTree] + fn new() -> Self { + BallotTree { + num_ballots: N::new(), + ballots: Vec::new(), + next_preferences: None, + next_exhausted: None, + } + } + + /// Descend one level of the [BallotTree] + fn descend_tree(&mut self, candidates: &'t Vec) { + let mut next_preferences: HashMap<&Candidate, BallotTree> = HashMap::new(); + let mut next_exhausted = BallotTree::new(); + + for bit in self.ballots.iter() { + if bit.up_to_pref < bit.ballot.preferences.len() { + let candidate = &candidates[bit.ballot.preferences[bit.up_to_pref]]; + + if next_preferences.contains_key(candidate) { + let np_bt = next_preferences.get_mut(candidate).unwrap(); + np_bt.num_ballots += &bit.ballot.orig_value; + np_bt.ballots.push(BallotInTree { + ballot: bit.ballot, + up_to_pref: bit.up_to_pref + 1, + }); + } else { + let mut np_bt = BallotTree::new(); + np_bt.num_ballots += &bit.ballot.orig_value; + np_bt.ballots.push(BallotInTree { + ballot: bit.ballot, + up_to_pref: bit.up_to_pref + 1, + }); + next_preferences.insert(candidate, np_bt); + } + } else { + // Exhausted + next_exhausted.num_ballots += &bit.ballot.orig_value; + next_exhausted.ballots.push(bit.clone()); + } + } + + self.next_preferences = Some(Box::new(next_preferences)); + self.next_exhausted = Some(Box::new(next_exhausted)); + } +} + +/// Initialise keep values, ballot tree and distribute preferences +pub fn distribute_first_preferences(state: &mut CountState) +where + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, +{ + // Initialise keep values + for (_, count_card) in state.candidates.iter_mut() { + count_card.keep_value = Some(N::one()); + } + + // Initialise ballot tree + let mut ballot_tree = BallotTree::new(); + for ballot in state.election.ballots.iter() { + ballot_tree.ballots.push(BallotInTree { + ballot: ballot, + up_to_pref: 0, + }); + ballot_tree.num_ballots += &ballot.orig_value; + } + state.ballot_tree = Some(ballot_tree); + + // Distribute preferences + distribute_preferences(state); + + // Recalculate transfers + for (_, count_card) in state.candidates.iter_mut() { + count_card.transfers.assign(&count_card.votes); + } + state.exhausted.transfers.assign(&state.exhausted.votes); + + state.kind = None; + state.title = "First preferences".to_string(); + state.logger.log_literal("First preferences distributed.".to_string()); +} + +/// (Re)distribute preferences according to candidate keep values +pub fn distribute_preferences(state: &mut CountState) +where + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, +{ + // Reset votes + for (_, count_card) in state.candidates.iter_mut() { + //count_card.orig_votes = N::new(); + //count_card.transfers = N::new(); + count_card.votes = N::new(); + } + state.exhausted.votes = N::new(); + + distribute_recursively(&mut state.candidates, &mut state.exhausted, state.ballot_tree.as_mut().unwrap(), N::one(), &state.election); +} + +/// Distribute preferences recursively +/// +/// Called by [distribute_preferences] +fn distribute_recursively<'t, N: Number>(candidates: &mut HashMap<&'t Candidate, CountCard>, exhausted: &mut CountCard, tree: &mut BallotTree<'t, N>, remaining_multiplier: N, election: &'t Election) +where + for<'r> &'r N: ops::Mul<&'r N, Output=N>, +{ + // Descend tree if required + if let None = tree.next_exhausted { + tree.descend_tree(&election.candidates); + } + + // FIXME: Possibility of infinite loop if malformed inputs? + + // TODO: Round transfers? + + // Credit votes at this level + for (candidate, cand_tree) in tree.next_preferences.as_mut().unwrap().as_mut().iter_mut() { + let count_card = candidates.get_mut(candidate).unwrap(); + match count_card.state { + CandidateState::Hopeful | CandidateState::Guarded | CandidateState::Doomed => { + // Hopeful candidate has keep value 1, so transfer entire remaining value + count_card.votes += &remaining_multiplier * &cand_tree.num_ballots; + } + CandidateState::Elected => { + // Transfer according to elected candidate's keep value + count_card.votes += &remaining_multiplier * &cand_tree.num_ballots * count_card.keep_value.as_ref().unwrap(); + let new_remaining_multiplier = &remaining_multiplier * &(N::one() - count_card.keep_value.as_ref().unwrap()); + // Recurse + distribute_recursively(candidates, exhausted, cand_tree, new_remaining_multiplier, election); + } + CandidateState::Excluded | CandidateState::Withdrawn => { + // Excluded candidate has keep value 0, so skip over this candidate + // Recurse + distribute_recursively(candidates, exhausted, cand_tree, remaining_multiplier.clone(), election); + } + } + } + + // Credit exhausted votes at this level + exhausted.votes += &remaining_multiplier * &tree.next_exhausted.as_ref().unwrap().as_ref().num_ballots; +} + +/// Recalculate all candidate keep factors to distribute all surpluses according to the Meek method +pub fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> Result +where + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, + for<'r> &'r N: ops::Div<&'r N, Output=N>, +{ + // TODO: Make configurable + let quota_tolerance = N::one() / N::from(100000) + N::one(); + + let quota = state.quota.as_ref().unwrap(); + let mut has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie + .filter(|c| { + let count_card = state.candidates.get(c).unwrap(); + return count_card.state == CandidateState::Elected && (&count_card.votes / quota > quota_tolerance); + }) + .collect(); + + if !has_surplus.is_empty() { + // TODO: Defer surpluses? + + let orig_candidates = state.candidates.clone(); + let orig_exhausted = state.exhausted.clone(); + + let mut num_iterations: u32 = 0; + + while !has_surplus.is_empty() { + num_iterations += 1; + + // Recompute keep values + for candidate in has_surplus.into_iter() { + let count_card = state.candidates.get_mut(candidate).unwrap(); + count_card.keep_value = Some(count_card.keep_value.take().unwrap() * state.quota.as_ref().unwrap() / &count_card.votes); + } + + // Redistribute votes + distribute_preferences(state); + + // Recompute quota if more ballots have become exhausted + super::calculate_quota(state, opts); + + //println!("Debug {}", num_iterations); + + let quota = state.quota.as_ref().unwrap(); + has_surplus = state.election.candidates.iter() + .filter(|c| { + let count_card = state.candidates.get(c).unwrap(); + return count_card.state == CandidateState::Elected && (&count_card.votes / quota > quota_tolerance); + }) + .collect(); + } + + // Recalculate transfers + for (candidate, count_card) in state.candidates.iter_mut() { + count_card.transfers = &count_card.votes - &orig_candidates.get(candidate).unwrap().votes; + } + state.exhausted.transfers = &state.exhausted.votes - &orig_exhausted.votes; + + // Remove intermediate logs on quota calculation + state.logger.entries.clear(); + + state.kind = None; + state.title = "Surpluses distributed".to_string(); + if num_iterations == 1 { + state.logger.log_literal("Surpluses distributed, requiring 1 iteration.".to_string()); + } else { + state.logger.log_literal(format!("Surpluses distributed, requiring {} iterations.", num_iterations)); + } + + let kv_str = state.election.candidates.iter() + .map(|c| (c, state.candidates.get(c).unwrap())) + .filter(|(_, cc)| cc.state == CandidateState::Elected) + .sorted_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected)) + .map(|(c, cc)| format!("{} ({:.dps2$})", c.name, cc.keep_value.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))) + .join(", "); + + state.logger.log_literal(format!("Keep values of elected candidates are: {}.", kv_str)); + + return Ok(true); + } + return Ok(false); +} + +/// Exclude the given candidates according to the Meek method +pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, _opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) +where + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, +{ + // Used to give bulk excluded candidate the same order_elected + let order_excluded = state.num_excluded + 1; + + for excluded_candidate in excluded_candidates.into_iter() { + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + + if count_card.state != CandidateState::Excluded { + count_card.state = CandidateState::Excluded; + state.num_excluded += 1; + count_card.order_elected = -(order_excluded as isize); + } + } + + let orig_candidates = state.candidates.clone(); + let orig_exhausted = state.exhausted.clone(); + + distribute_preferences(state); + + // Recalculate transfers + for (candidate, count_card) in state.candidates.iter_mut() { + count_card.transfers = &count_card.votes - &orig_candidates.get(candidate).unwrap().votes; + } + state.exhausted.transfers = &state.exhausted.votes - &orig_exhausted.votes; +} diff --git a/src/stv/mod.rs b/src/stv/mod.rs index a0ca667..f77c9bf 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -17,20 +17,22 @@ #![allow(mutable_borrow_reservation_conflict)] +/// Gregory method of surplus distributions pub mod gregory; +/// Meek method of surplus distributions, etc. +pub mod meek; //#[cfg(target_arch = "wasm32")] pub mod wasm; use crate::numbers::Number; -use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; +use crate::election::{Candidate, CandidateState, CountCard, CountState, Vote}; use crate::sharandom::SHARandom; use crate::ties::TieStrategy; use itertools::Itertools; use wasm_bindgen::prelude::wasm_bindgen; -use std::cmp::max; use std::collections::HashMap; use std::ops; @@ -319,7 +321,11 @@ pub enum STVError { } /// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules -pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions) { +pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions) +where + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, +{ // Initialise RNG for t in opts.ties.iter() { if let TieStrategy::Random(seed) = t { @@ -327,7 +333,7 @@ pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a ST } } - distribute_first_preferences(&mut state); + distribute_first_preferences(&mut state, opts); calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); init_tiebreaks(&mut state, opts); @@ -337,6 +343,7 @@ pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a ST pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result where for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg, { @@ -452,31 +459,19 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec(state: &mut CountState) { - let votes = state.election.ballots.iter().map(|b| Vote { - ballot: b, - value: b.orig_value.clone(), - up_to_pref: 0, - }).collect(); - - let result = next_preferences(state, votes); - - // Transfer candidate votes - for (candidate, entry) in result.candidates.into_iter() { - let parcel = entry.votes as Parcel; - let count_card = state.candidates.get_mut(candidate).unwrap(); - count_card.parcels.push(parcel); - count_card.transfer(&entry.num_votes); +fn distribute_first_preferences(state: &mut CountState, opts: &STVOptions) +where + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, +{ + match opts.surplus { + SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { + gregory::distribute_first_preferences(state); + } + SurplusMethod::Meek => { + meek::distribute_first_preferences(state); + } } - - // Transfer exhausted votes - let parcel = result.exhausted.votes as Parcel; - state.exhausted.parcels.push(parcel); - state.exhausted.transfer(&result.exhausted.num_votes); - - state.kind = None; - state.title = "First preferences".to_string(); - state.logger.log_literal("First preferences distributed.".to_string()); } /// Calculate the quota, given the total vote, according to [STVOptions::quota] @@ -514,7 +509,7 @@ fn total_to_quota(mut total: N, seats: usize, opts: &STVOptions) -> N /// Calculate the quota according to [STVOptions::quota] fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // Calculate quota - if let None = state.quota { + if state.quota.is_none() || opts.surplus == SurplusMethod::Meek { let mut log = String::new(); // Calculate the total vote @@ -587,7 +582,7 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { } } else { // No ERS97 rules - if let None = state.vote_required_election { + if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek { state.vote_required_election = state.quota.clone(); } } @@ -678,15 +673,16 @@ where fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> Result where for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, - for<'r> &'r N: ops::Neg + for<'r> &'r N: ops::Neg, { match opts.surplus { SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { return gregory::distribute_surpluses(state, opts); } SurplusMethod::Meek => { - todo!(); + return meek::distribute_surpluses(state, opts); } } } @@ -784,6 +780,8 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST /// Exclude the lowest-ranked hopeful candidate(s) fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result where + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, { let mut excluded_candidates: Vec<&Candidate> = Vec::new(); @@ -831,6 +829,8 @@ where /// Continue the exclusion of a candidate who is being excluded fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool where + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, { // Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers @@ -867,155 +867,18 @@ where /// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion] fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) where + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, { - // Used to give bulk excluded candidate the same order_elected - let order_excluded = state.num_excluded + 1; - - for excluded_candidate in excluded_candidates.iter() { - let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); - - // Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??! - if count_card.state != CandidateState::Excluded { - count_card.state = CandidateState::Excluded; - state.num_excluded += 1; - count_card.order_elected = -(order_excluded as isize); + match opts.surplus { + SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { + gregory::exclude_candidates(state, opts, excluded_candidates); + } + SurplusMethod::Meek => { + meek::exclude_candidates(state, opts, excluded_candidates); } } - - // Determine votes to transfer in this stage - let mut votes = Vec::new(); - let mut votes_remain; - let mut checksum = N::new(); - - match opts.exclusion { - ExclusionMethod::SingleStage => { - // Exclude in one round - for excluded_candidate in excluded_candidates.iter() { - let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); - votes.append(&mut count_card.parcels.concat()); - count_card.parcels.clear(); - - // Update votes - let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value); - checksum -= &votes_transferred; - count_card.transfer(&-votes_transferred); - } - votes_remain = false; - } - ExclusionMethod::ByValue => { - // Exclude by value - let max_value = excluded_candidates.iter() - .map(|c| state.candidates.get(c).unwrap().parcels.iter() - .map(|p| p.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap()) - .max().unwrap()) - .max().unwrap(); - - votes_remain = false; - - for excluded_candidate in excluded_candidates.iter() { - let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); - - // Filter out just those votes with max_value - let mut remaining_votes = Vec::new(); - - let cand_votes = count_card.parcels.concat(); - - let mut votes_transferred = N::new(); - for vote in cand_votes.into_iter() { - if &vote.value / &vote.ballot.orig_value == max_value { - votes_transferred += &vote.value; - votes.push(vote); - } else { - remaining_votes.push(vote); - } - } - - if !remaining_votes.is_empty() { - votes_remain = true; - } - - // Leave remaining votes with candidate (as one parcel) - count_card.parcels = vec![remaining_votes]; - - // Update votes - checksum -= &votes_transferred; - count_card.transfer(&-votes_transferred); - } - } - ExclusionMethod::ParcelsByOrder => { - // Exclude by parcel by order - if excluded_candidates.len() > 1 { - panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude"); - } - - let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap(); - votes = count_card.parcels.remove(0); - votes_remain = !count_card.parcels.is_empty(); - - // Update votes - let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value); - checksum -= &votes_transferred; - count_card.transfer(&-votes_transferred); - } - } - - if !votes.is_empty() { - let value = &votes[0].value / &votes[0].ballot.orig_value; - - // Count next preferences - let result = next_preferences(state, votes); - - if let ExclusionMethod::SingleStage = opts.exclusion { - state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals)); - } else { - state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); - } - - // Transfer candidate votes - for (candidate, entry) in result.candidates.into_iter() { - let parcel = entry.votes as Parcel; - let count_card = state.candidates.get_mut(candidate).unwrap(); - count_card.parcels.push(parcel); - - // Round transfers - let mut candidate_transfers = entry.num_votes; - if let Some(dps) = opts.round_votes { - candidate_transfers.floor_mut(dps); - } - count_card.transfer(&candidate_transfers); - checksum += candidate_transfers; - } - - // Transfer exhausted votes - let parcel = result.exhausted.votes as Parcel; - state.exhausted.parcels.push(parcel); - - let mut exhausted_transfers = result.exhausted.num_votes; - if let Some(dps) = opts.round_votes { - exhausted_transfers.floor_mut(dps); - } - state.exhausted.transfer(&exhausted_transfers); - checksum += exhausted_transfers; - } - - if !votes_remain { - // Finalise candidate votes - for excluded_candidate in excluded_candidates.into_iter() { - let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); - checksum -= &count_card.votes; - count_card.transfers -= &count_card.votes; - count_card.votes = N::new(); - } - - if let ExclusionMethod::SingleStage = opts.exclusion { - } else { - state.logger.log_literal("Exclusion complete.".to_string()); - } - } - - // Update loss by fraction - state.loss_fraction.transfer(&-checksum); } /// Determine if the count is complete because the number of elected candidates equals the number of vacancies diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 8639ec1..34f978a 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -26,6 +26,8 @@ extern crate console_error_panic_hook; use js_sys::Array; use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; +use std::cmp::max; + // Init /// Wrapper for [Fixed::set_dps] @@ -122,8 +124,8 @@ macro_rules! impl_type { /// Wrapper for [final_result_summary] #[wasm_bindgen] #[allow(non_snake_case)] - pub fn [](state: &[]) -> String { - return final_result_summary(&state.0); + pub fn [](state: &[], opts: &STVOptions) -> String { + return final_result_summary(&state.0, &opts.0); } // Wrapper structs @@ -336,19 +338,23 @@ fn finalise_results_table(state: &CountState) -> Array { } /// Generate the final lead-out text summarising the result of the election -fn final_result_summary(state: &CountState) -> String { +fn final_result_summary(state: &CountState, opts: &stv::STVOptions) -> String { let mut result = String::from("

Count complete. The winning candidates are, in order of election:

    "); let mut winners = Vec::new(); for (candidate, count_card) in state.candidates.iter() { if count_card.state == CandidateState::Elected { - winners.push((candidate, count_card.order_elected)); + winners.push((candidate, count_card.order_elected, &count_card.keep_value)); } } winners.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); - for (winner, _) in winners.into_iter() { - result.push_str(&format!("
  1. {}
  2. ", winner.name)); + for (winner, _, kv_opt) in winners.into_iter() { + if let Some(kv) = kv_opt { + result.push_str(&format!("
  3. {} (kv = {:.dps2$})
  4. ", winner.name, kv, dps2=max(opts.pp_decimals, 2))); + } else { + result.push_str(&format!("
  5. {}
  6. ", winner.name)); + } } result.push_str("
"); diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index df76276..5f86777 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -55,6 +55,7 @@ pub fn read_validate_election(csv_file: &str, blt_file: &str, stv_opt pub fn validate_election(stages: Vec, records: Vec, election: Election, stv_opts: stv::STVOptions) where for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg, {