/* 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::{ExclusionMethod, NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder}; use super::subset; use crate::constraints; use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote}; use crate::numbers::Number; use crate::ties; 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 = Parcel { votes: entry.votes, source_order: 0, }; 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 = Parcel { votes: result.exhausted.votes, source_order: 0, }; 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 or random subset method, based on [STVOptions::surplus] 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::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.parcels.iter().any(|p| !p.votes.is_empty()) }) .collect(); if !has_surplus.is_empty() { let total_surpluses = has_surplus.iter() .fold(N::new(), |acc, c| acc + &state.candidates[c].votes - quota); // 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] }; match opts.surplus { SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { distribute_surplus(state, &opts, elected_candidate); } SurplusMethod::Cincinnati | SurplusMethod::Hare => { subset::distribute_surplus(state, &opts, elected_candidate)?; } _ => unreachable!() } return Ok(true); } return Ok(false); } /// Return the denominator of the transfer value fn calculate_surplus_denom(surplus: &N, result: &NextPreferencesResult, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option where for<'r> &'r N: ops::Sub<&'r N, Output=N> { if transferable_only { let total_units = if weighted { &result.total_votes } else { &result.total_ballots }; let exhausted_units = if weighted { &result.exhausted.num_votes } else { &result.exhausted.num_ballots }; let transferable_units = total_units - exhausted_units; if transferable_votes > surplus { return Some(transferable_units); } else { return None; } } else { if weighted { return Some(result.total_votes.clone()); } else { return Some(result.total_ballots.clone()); } } } /// Return the reweighted value of the vote after being transferred fn reweight_vote( num_votes: &N, num_ballots: &N, surplus: &N, weighted: bool, surplus_fraction: &Option, surplus_denom: &Option, round_tvs: Option, rounding: Option) -> N { let mut result; match surplus_denom { Some(v) => { if let Some(_) = round_tvs { // Rounding requested: use the rounded transfer value if weighted { result = num_votes.clone() * surplus_fraction.as_ref().unwrap(); } else { result = num_ballots.clone() * surplus_fraction.as_ref().unwrap(); } } else { // Avoid unnecessary rounding error by first multiplying by the surplus if weighted { result = num_votes.clone() * surplus / v; } else { result = num_ballots.clone() * surplus / v; } } } None => { result = num_votes.clone(); } } // Round down if requested if let Some(dps) = rounding { result.floor_mut(dps); } return result; } /// Compute the number of votes to credit to a continuing candidate during a surplus transfer, based on [STVOptions::sum_surplus_transfers] fn sum_surplus_transfers(entry: &NextPreferencesEntry, surplus: &N, is_weighted: bool, surplus_fraction: &Option, surplus_denom: &Option, _state: &mut CountState, opts: &STVOptions) -> N where for<'r> &'r N: ops::Div<&'r N, Output=N>, { match opts.sum_surplus_transfers { SumSurplusTransfersMode::SingleStep => { // Calculate transfer across all votes //state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals)); return reweight_vote(&entry.num_votes, &entry.num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions, opts.round_votes); } SumSurplusTransfersMode::ByValue => { // Sum transfers by value let mut result = N::new(); // Sort into parcels by value let mut votes: Vec<&Vote> = entry.votes.iter().collect(); votes.sort_unstable_by(|a, b| (&a.value / &a.ballot.orig_value).cmp(&(&b.value / &b.ballot.orig_value))); for (_value, parcel) in &votes.into_iter().group_by(|v| &v.value / &v.ballot.orig_value) { let mut num_votes = N::new(); let mut num_ballots = N::new(); for vote in parcel { num_votes += &vote.value; num_ballots += &vote.ballot.orig_value; } //state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", num_ballots, num_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); result += reweight_vote(&num_votes, &num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions, opts.round_votes); } return result; } SumSurplusTransfersMode::PerBallot => { // Sum transfer per each individual ballot // TODO: This could be moved to distribute_surplus to avoid looping over the votes and calculating transfer values twice let mut result = N::new(); for vote in entry.votes.iter() { result += reweight_vote(&vote.value, &vote.ballot.orig_value, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions, opts.round_votes); } //state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals)); return result; } } } /// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus] fn distribute_surplus(state: &mut CountState, opts: &STVOptions, elected_candidate: &Candidate) where for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg { state.kind = Some("Surplus of"); state.title = String::from(&elected_candidate.name); 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(); let votes; match opts.surplus { SurplusMethod::WIG | SurplusMethod::UIG => { // Inclusive Gregory votes = state.candidates.get_mut(elected_candidate).unwrap().concat_parcels(); } SurplusMethod::EG => { // Exclusive Gregory // Should be safe to unwrap() - or else how did we get a quota! votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap().votes; } _ => unreachable!() } // Count next preferences let result = super::next_preferences(state, votes); // Transfer candidate votes // TODO: Refactor?? let is_weighted = match opts.surplus { SurplusMethod::WIG => { true } SurplusMethod::UIG | SurplusMethod::EG => { false } _ => unreachable!() }; let transferable_votes = &result.total_votes - &result.exhausted.num_votes; let surplus_denom = calculate_surplus_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only); let mut surplus_fraction; match surplus_denom { Some(ref 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); } if opts.transferable_only { if &result.total_ballots - &result.exhausted.num_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$}.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } } else { if result.total_ballots == N::one() { state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", result.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$}.", result.total_ballots, result.total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } } } None => { surplus_fraction = None; if opts.transferable_only { state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, dps=opts.pp_decimals)); } else { state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, at values received.", result.total_ballots, result.total_votes, dps=opts.pp_decimals)); } } } let mut checksum = N::new(); for (candidate, entry) in result.candidates.into_iter() { // Credit transferred votes let candidate_transfers = sum_surplus_transfers(&entry, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts); let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.transfer(&candidate_transfers); checksum += candidate_transfers; let mut parcel = Parcel { votes: entry.votes, source_order: state.num_elected + state.num_excluded, }; // Reweight votes for vote in parcel.votes.iter_mut() { vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_surplus_fractions, opts.round_values); } count_card.parcels.push(parcel); } // Credit exhausted votes let mut exhausted_transfers; if opts.transferable_only { if transferable_votes > surplus { // No ballots exhaust exhausted_transfers = N::new(); } else { exhausted_transfers = &surplus - &transferable_votes; if let Some(dps) = opts.round_votes { exhausted_transfers.floor_mut(dps); } } } else { exhausted_transfers = sum_surplus_transfers(&result.exhausted, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts); } state.exhausted.transfer(&exhausted_transfers); checksum += exhausted_transfers; // Transfer exhausted votes let parcel = Parcel { votes: result.exhausted.votes, source_order: state.num_elected + state.num_excluded, }; state.exhausted.parcels.push(parcel); // 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.parcels.clear(); // 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] 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); constraints::update_constraints(state, opts); } } // 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.concat_parcels()); 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 excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter().filter(|c| !state.candidates[*c].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.votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap()) .max().unwrap()) .max().unwrap(); votes_remain = false; for excluded_candidate in excluded_with_votes.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.concat_parcels(); 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![Parcel { votes: remaining_votes, source_order: 0, // Unused in this mode }]; // Update votes checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } } } ExclusionMethod::BySource => { // Exclude by source candidate let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter().filter(|c| !state.candidates[*c].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(); // Filter out just those votes with min_order let mut remaining_parcels = Vec::new(); let mut votes_transferred = N::new(); while !count_card.parcels.is_empty() { let parcel = count_card.parcels.pop().unwrap(); if parcel.source_order == min_order { for vote in parcel.votes { votes_transferred += &vote.value; votes.push(vote); } } else { remaining_parcels.push(parcel); } } if !remaining_parcels.is_empty() { votes_remain = true; } // Leave remaining parcels with candidate count_card.parcels = remaining_parcels; // Update votes checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } } } ExclusionMethod::ParcelsByOrder => { // Exclude by parcel by order if excluded_candidates.len() > 1 { // TODO: We can probably support this actually panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude"); } let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap(); if count_card.parcels.is_empty() { votes_remain = false; } else { votes = count_card.parcels.remove(0).votes; 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); } } _ => panic!() } if !votes.is_empty() { // Count next preferences let value = &votes[0].value / &votes[0].ballot.orig_value; let result = super::next_preferences(state, votes); if let ExclusionMethod::SingleStage = opts.exclusion { if result.total_ballots == N::one() { state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes.", result.total_votes, dps=opts.pp_decimals)); } else { state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals)); } } else { if result.total_ballots == N::one() { state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, received at value {:.dps2$}.", result.total_votes, value, 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$}.", 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 = Parcel { votes: entry.votes, source_order: state.num_elected + state.num_excluded, }; 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 = Parcel { votes: result.exhausted.votes, source_order: state.num_elected + state.num_excluded, }; 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); } /// Perform one stage of a candidate exclusion according to the Wright method pub fn wright_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); } 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, }; } 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.kind = Some("Exclusion of"); state.title = orig_title; // Trigger recalculation of quota within stv::count_one_stage state.quota = None; state.vote_required_election = None; }