/* OpenTally: Open-source election vote counting * Copyright © 2021–2022 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 . */ // -------------- // Child packages /// Transfer tables mod transfers; pub use transfers::{TransferTable, TransferTableCell, TransferTableColumn}; /// prettytable-compatible API for HTML table output in WebAssembly pub mod prettytable_html; // -------- // STV code use super::{ExclusionMethod, RoundSubtransfersMode, STVError, STVOptions, SurplusMethod, SurplusOrder}; use super::sample; use crate::constraints; use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote}; use crate::numbers::Number; use crate::ties; use std::cmp::max; use std::ops; /// Distribute first preference votes according to the Gregory method pub fn distribute_first_preferences(state: &mut CountState, opts: &STVOptions) where for<'r> &'r N: ops::Sub<&'r N, Output=N> { let votes = state.election.ballots.iter().map(|b| Vote { ballot: b, 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 = Parcel { votes: entry.votes, value_fraction: N::one(), source_order: 0, }; let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); let mut vote_transfers = entry.num_ballots.clone(); if let Some(dps) = opts.round_votes { vote_transfers.floor_mut(dps); } count_card.transfer(&vote_transfers); count_card.ballot_transfers += entry.num_ballots; } // Transfer exhausted votes let parcel = Parcel { votes: result.exhausted.votes, value_fraction: N::one(), source_order: 0, }; state.exhausted.parcels.push(parcel); state.exhausted.transfer(&result.exhausted.num_ballots); state.exhausted.ballot_transfers += result.exhausted.num_ballots; // Calculate loss by fraction - if minivoters used if let Some(orig_total) = &state.election.total_votes { let mut total_votes = state.total_vote(); total_votes += &state.exhausted.votes; let lbf = orig_total - &total_votes; state.loss_fraction.votes = lbf.clone(); state.loss_fraction.transfers = lbf; } state.title = StageKind::FirstPreferences; state.logger.log_literal("First preferences distributed.".to_string()); } /// Distribute the largest surplus according to the Gregory or random subset method, based on [STVOptions::surplus] /// /// Returns `true` if any surpluses were distributed. pub fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> Result where for<'r> &'r N: ops::Add<&'r N, Output=N>, 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 { let quota = state.quota.as_ref().unwrap(); let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie .filter(|c| { let cc = &state.candidates[c]; &cc.votes > quota && !cc.finalised }) .collect(); if !has_surplus.is_empty() { let total_surpluses = state.total_surplus(); // Determine if surplues can be deferred if opts.defer_surpluses { if super::can_defer_surpluses(state, opts, &total_surpluses) { state.logger.log_literal(format!("Distribution of surpluses totalling {:.dps$} votes will be deferred.", total_surpluses, dps=opts.pp_decimals)); return Ok(false); } } // Distribute top candidate's surplus let max_cands = match opts.surplus_order { SurplusOrder::BySize => { ties::multiple_max_by(&has_surplus, |c| &state.candidates[c].votes) } SurplusOrder::ByOrder => { ties::multiple_min_by(&has_surplus, |c| state.candidates[c].order_elected) } }; let elected_candidate = if max_cands.len() > 1 { super::choose_highest(state, opts, &max_cands, "Which candidate's surplus to distribute?")? } else { max_cands[0] }; // If --no-immediate-elect, declare elected the candidate with the highest surplus if !opts.immediate_elect { let count_card = state.candidates.get_mut(elected_candidate).unwrap(); count_card.state = CandidateState::Elected; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; state.logger.log_smart( "{} meets the quota and is elected.", "{} meet the quota and are elected.", vec![elected_candidate.name.as_str()] ); constraints::update_constraints(state, opts); } match opts.surplus { SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { distribute_surplus(state, opts, elected_candidate); } SurplusMethod::IHare | SurplusMethod::Hare => { sample::distribute_surplus(state, opts, elected_candidate)?; } _ => unreachable!() } return Ok(true); } // If --no-immediate-elect, check for candidates with exactly a quota to elect // However, if --defer-surpluses, zero surplus is necessarily deferred so skip if !opts.immediate_elect && !opts.defer_surpluses { if super::elect_hopefuls(state, opts, false)? { return Ok(true); } } return Ok(false); } /// Return the denominator of the surplus fraction /// /// Returns `None` if the value of transferable votes <= surplus (i.e. all transferable votes are transferred at values received). fn calculate_surplus_denom<'n, N: Number>(surplus: &N, transferable_ballots: &'n N, transferable_votes: &'n N, total_ballots: &'n N, total_votes: &'n N, opts: &STVOptions) -> Option where for<'r> &'r N: ops::Sub<&'r N, Output=N> { if opts.transferable_only { let transferable_units = if opts.surplus.is_weighted() { transferable_votes } else { transferable_ballots }; if transferable_votes > surplus { return Some(transferable_units.clone()); } else { return None; } } else { if opts.surplus.is_weighted() { return Some(total_votes.clone()); } else { return Some(total_ballots.clone()); } } } /// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus] pub fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate) where for<'r> &'r N: ops::Add<&'r N, Output=N>, 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 { state.title = StageKind::SurplusOf(elected_candidate); state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name)); let count_card = &state.candidates[elected_candidate]; let surplus = &count_card.votes - state.quota.as_ref().unwrap(); // Determine which votes to examine let mut parcels; match opts.surplus { SurplusMethod::WIG | SurplusMethod::UIG => { // Inclusive Gregory parcels = Vec::new(); parcels.append(&mut state.candidates.get_mut(elected_candidate).unwrap().parcels); } SurplusMethod::EG => { // Exclusive Gregory // Should be safe to unwrap() - or else how did we get a quota! parcels = vec![state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap()]; } _ => unreachable!() } // Count votes let mut parcels_next_prefs= Vec::new(); let mut transferable_ballots = N::new(); let mut transferable_votes = N::new(); let mut exhausted_ballots = N::new(); let mut exhausted_votes = N::new(); for parcel in parcels { // Count next preferences let result = super::next_preferences(state, parcel.votes); for (_, entry) in result.candidates.iter() { transferable_ballots += &entry.num_ballots; transferable_votes += &entry.num_ballots * &parcel.value_fraction; } exhausted_ballots += &result.exhausted.num_ballots; exhausted_votes += &result.exhausted.num_ballots * &parcel.value_fraction; parcels_next_prefs.push((parcel.value_fraction, parcel.source_order, result)); } // Calculate and print surplus fraction let total_ballots = &transferable_ballots + &exhausted_ballots; let total_votes = &transferable_votes + &exhausted_votes; let count_card = state.candidates.get_mut(elected_candidate).unwrap(); count_card.ballot_transfers = -&total_ballots; if opts.transferable_only && opts.subtract_nontransferable { // Override transferable_votes transferable_votes = count_card.votes.clone() - exhausted_votes; } let mut surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, opts); let surplus_numer; let mut surplus_fraction; match &surplus_denom { Some(v) => { surplus_fraction = Some(surplus.clone() / v); // Round down if requested if let Some(dps) = opts.round_surplus_fractions { surplus_fraction.as_mut().unwrap().floor_mut(dps); surplus_numer = surplus_fraction.clone(); surplus_denom = None; } else { surplus_numer = Some(surplus.clone()); } if opts.transferable_only { if transferable_ballots == N::one() { state.logger.log_literal(format!("Transferring 1 transferable ballot, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } else { state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", transferable_ballots, transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } } else { if total_ballots == N::one() { state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } else { state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", total_ballots, total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } } } None => { surplus_fraction = None; surplus_numer = None; surplus_denom = None; // This can only happen if --transferable-only if transferable_ballots == N::one() { state.logger.log_literal(format!("Transferring 1 transferable ballot, totalling {:.dps$} transferable votes, at values received.", transferable_votes, dps=opts.pp_decimals)); } else { state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received.", transferable_ballots, transferable_votes, dps=opts.pp_decimals)); } } } // Reweight and transfer parcels let mut transfer_table = TransferTable::new_surplus( state.election.candidates.iter().filter(|c| state.candidates[c].state == CandidateState::Hopeful || state.candidates[c].state == CandidateState::Guarded).collect(), surplus.clone(), surplus_fraction.clone(), surplus_numer.clone(), surplus_denom.clone() ); for (value_fraction, source_order, result) in parcels_next_prefs { for (candidate, entry) in result.candidates.into_iter() { // Record transfers transfer_table.add_transfers( &value_fraction, match opts.round_subtransfers { RoundSubtransfersMode::ByValueAndSource => Some(source_order), RoundSubtransfersMode::ByParcel => None, // Force new column per parcel _ => Some(0) }, candidate, &entry.num_ballots ); let mut new_value_fraction; if opts.surplus.is_weighted() { new_value_fraction = value_fraction.clone(); new_value_fraction *= surplus_numer.as_ref().unwrap(); // Guaranteed to be Some in WIGM if let Some(n) = &surplus_denom { new_value_fraction /= n; } } else { if let Some(sf) = &surplus_fraction { new_value_fraction = sf.clone(); } else { new_value_fraction = value_fraction.clone(); } } if let Some(dps) = opts.round_values { new_value_fraction.floor_mut(dps); } // Transfer candidate votes let parcel = Parcel { votes: entry.votes, value_fraction: new_value_fraction, source_order: state.num_elected + state.num_excluded, }; let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); } // Record exhausted votes transfer_table.add_exhausted( &value_fraction, match opts.round_subtransfers { RoundSubtransfersMode::ByValueAndSource => Some(source_order), RoundSubtransfersMode::ByParcel => None, // Force new column per parcel _ => Some(0) }, &result.exhausted.num_ballots ); // Transfer exhausted votes let parcel = Parcel { votes: result.exhausted.votes, value_fraction, // TODO: Reweight exhausted votes source_order: state.num_elected + state.num_excluded, }; state.exhausted.parcels.push(parcel); } let mut checksum = N::new(); // Credit transferred votes transfer_table.calculate(opts); checksum += transfer_table.apply_to(state, opts); state.transfer_table = Some(transfer_table); // Finalise candidate votes let count_card = state.candidates.get_mut(elected_candidate).unwrap(); count_card.transfers = -&surplus; count_card.votes.assign(state.quota.as_ref().unwrap()); checksum -= surplus; count_card.finalised = true; // Mark surpluses as done // 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] #[allow(clippy::branches_sharing_code)] pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>, complete_type: &'static str) where 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); constraints::update_constraints(state, opts); } } // Determine votes to transfer in this stage let mut parcels = 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(); count_card.ballot_transfers = -count_card.num_ballots(); count_card.finalised = true; parcels.append(&mut count_card.parcels); // Update votes checksum -= &count_card.votes; count_card.transfers = -count_card.votes.clone(); count_card.votes = N::new(); } votes_remain = false; } ExclusionMethod::ByValue => { // Exclude by value let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter() .filter(|c| { let cc = &state.candidates[*c]; !cc.finalised && !cc.parcels.is_empty() }) .collect(); if excluded_with_votes.is_empty() { votes_remain = false; } else { // If candidates to exclude still having votes, select only those with the greatest value let max_value = excluded_with_votes.iter() .map(|c| state.candidates[*c].parcels.iter() .map(|p| &p.value_fraction) .max().unwrap()) .max().unwrap() .clone(); votes_remain = false; let mut votes = Vec::new(); for excluded_candidate in excluded_with_votes.iter() { let count_card = state.candidates.get_mut(*excluded_candidate).unwrap(); let mut cc_parcels = Vec::new(); cc_parcels.append(&mut count_card.parcels); // Filter out just those votes with max_value let mut remaining_parcels = Vec::new(); for mut parcel in cc_parcels { if parcel.value_fraction == max_value { count_card.ballot_transfers -= parcel.num_ballots(); let votes_transferred = parcel.num_votes(); votes.append(&mut parcel.votes); // Update votes checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } else { remaining_parcels.push(parcel); } } if !remaining_parcels.is_empty() { votes_remain = true; } // Leave remaining votes with candidate count_card.parcels = remaining_parcels; } // Group all votes of one value in single parcel parcels.push(Parcel { votes, value_fraction: max_value, source_order: 0, // source_order is unused in this mode }); } } ExclusionMethod::BySource => { // Exclude by source candidate let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter() .filter(|c| { let cc = &state.candidates[*c]; !cc.finalised && !cc.parcels.is_empty() }) .collect(); if excluded_with_votes.is_empty() { votes_remain = false; } else { // If candidates to exclude still having votes, select only those from the earliest elected/excluded source candidate let min_order = excluded_with_votes.iter() .map(|c| state.candidates[*c].parcels.iter() .map(|p| p.source_order) .min().unwrap()) .min().unwrap(); votes_remain = false; for excluded_candidate in excluded_with_votes.iter() { let count_card = state.candidates.get_mut(*excluded_candidate).unwrap(); let mut cc_parcels = Vec::new(); cc_parcels.append(&mut count_card.parcels); // Filter out just those votes with min_order let mut remaining_parcels = Vec::new(); for parcel in cc_parcels { if parcel.source_order == min_order { count_card.ballot_transfers -= parcel.num_ballots(); let votes_transferred = parcel.num_votes(); parcels.push(parcel); // Update votes checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } else { remaining_parcels.push(parcel); } } if !remaining_parcels.is_empty() { votes_remain = true; } // Leave remaining votes with candidate count_card.parcels = remaining_parcels; } } } ExclusionMethod::ParcelsByOrder => { // Exclude by parcel by order if excluded_candidates.len() > 1 && excluded_candidates.iter().any(|c| !state.candidates[c].parcels.is_empty()) { // TODO: We can probably support this actually panic!("--exclusion parcels_by_order is incompatible with multiple exclusions"); } let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap(); if count_card.parcels.is_empty() { votes_remain = false; } else { parcels.push(count_card.parcels.remove(0)); votes_remain = !count_card.parcels.is_empty(); count_card.ballot_transfers -= parcels.first().unwrap().num_ballots(); // Update votes let votes_transferred = parcels.first().unwrap().num_votes(); checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } } _ => panic!() } let mut total_ballots = N::new(); let mut total_votes = N::new(); let value = parcels.first().map(|p| p.value_fraction.clone()); let mut transfer_table = TransferTable::new_exclusion( state.election.candidates.iter().filter(|c| state.candidates[c].state == CandidateState::Hopeful || state.candidates[c].state == CandidateState::Guarded).collect(), ); for src_parcel in parcels { // Count next preferences let result = super::next_preferences(state, src_parcel.votes); total_ballots += &result.total_ballots; total_votes += &result.total_ballots * &src_parcel.value_fraction; // Transfer candidate votes for (candidate, entry) in result.candidates.into_iter() { let parcel = Parcel { votes: entry.votes, value_fraction: src_parcel.value_fraction.clone(), source_order: state.num_elected + state.num_excluded, }; // Record transfers transfer_table.add_transfers( &parcel.value_fraction, match opts.round_subtransfers { RoundSubtransfersMode::ByValueAndSource => Some(src_parcel.source_order), RoundSubtransfersMode::ByParcel => None, // Force new column per parcel _ => Some(0) }, candidate, &entry.num_ballots ); let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); } // Transfer exhausted votes let parcel = Parcel { votes: result.exhausted.votes, value_fraction: src_parcel.value_fraction, source_order: state.num_elected + state.num_excluded, }; // Record transfers transfer_table.add_exhausted( &parcel.value_fraction, match opts.round_subtransfers { RoundSubtransfersMode::ByValueAndSource => Some(src_parcel.source_order), RoundSubtransfersMode::ByParcel => None, // Force new column per parcel _ => Some(0) }, &result.exhausted.num_ballots ); state.exhausted.parcels.push(parcel); } if let ExclusionMethod::SingleStage = opts.exclusion { if total_ballots == N::one() { state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes.", total_votes, dps=opts.pp_decimals)); } else { state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes.", total_ballots, total_votes, dps=opts.pp_decimals)); } } else { if total_ballots.is_zero() { state.logger.log_literal(format!("Transferring 0 ballots, totalling {:.dps$} votes.", 0, dps=opts.pp_decimals)); } else if total_ballots == N::one() { state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, received at value {:.dps2$}.", total_votes, value.unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } else { state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, received at value {:.dps2$}.", total_ballots, total_votes, value.unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } } // Credit transferred votes transfer_table.calculate(opts); checksum += transfer_table.apply_to(state, opts); state.transfer_table = Some(transfer_table); 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(); count_card.finalised = true; } if opts.exclusion != ExclusionMethod::SingleStage { state.logger.log_literal(format!("{} complete.", complete_type)); } } // Update loss by fraction state.loss_fraction.transfer(&-checksum); } /// Exclude a candidate and reset the count from first preferences pub fn exclude_candidates_and_reset<'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); } constraints::update_constraints(state, opts); } // Reset count for (_, count_card) in state.candidates.iter_mut() { if count_card.order_elected > 0 { count_card.order_elected = 0; } count_card.parcels.clear(); count_card.votes = N::new(); count_card.transfers = N::new(); count_card.state = match count_card.state { CandidateState::Withdrawn => CandidateState::Withdrawn, CandidateState::Excluded => CandidateState::Excluded, _ => CandidateState::Hopeful, }; if count_card.state == CandidateState::Excluded { count_card.finalised = true; } else { count_card.finalised = false; } } state.exhausted.votes = N::new(); state.exhausted.transfers = N::new(); state.loss_fraction.votes = N::new(); state.loss_fraction.transfers = N::new(); state.num_elected = 0; let orig_title = state.title.clone(); // Redistribute first preferences super::distribute_first_preferences(state, opts); state.title = orig_title; // Trigger recalculation of quota within stv::count_one_stage state.quota = None; state.vote_required_election = None; }