diff --git a/docs/options.md b/docs/options.md index 771c215..c54dc42 100644 --- a/docs/options.md +++ b/docs/options.md @@ -261,8 +261,7 @@ When *Surplus method* is set to *Meek method*: When *Surplus method* is set to a Gregory method, this option allows you to specify how the numbers of votes credited to candidates in a surplus transfer is calculated. In each case, votes are grouped according to the next available preference for a continuing candidate. Subsequently: -* *Single step*: The total value of all votes expressing a next available preference for that candidate is multiplied by the surplus fraction. The product is credited to that candidate. -* *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product is credited to that candidate. This is distinct to *Single step* only for weighted inclusive Gregory. +* *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product is credited to that candidate. * *Per ballot*: For each individual vote expressing a next available preference for that candidate, the value of the vote is multiplied by the surplus fraction. The product is credited to that candidate. This option affects the result only as far as rounding (due to use of fixed-precision/floating-point arithmetic, or an explicit rounding option) is concerned. diff --git a/html/index.html b/html/index.html index 47fdd58..454bdf4 100644 --- a/html/index.html +++ b/html/index.html @@ -282,8 +282,8 @@ Gregory Sum surplus transfers: diff --git a/html/index.js b/html/index.js index fd277b8..42e3d4f 100644 --- a/html/index.js +++ b/html/index.js @@ -379,7 +379,7 @@ function changePreset() { document.getElementById('chkRoundVotes').checked = false; document.getElementById('chkRoundSFs').checked = false; document.getElementById('chkRoundValues').checked = false; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selSurplus').value = 'by_size'; document.getElementById('selMethod').value = 'wig'; document.getElementById('selPapers').value = 'both'; @@ -511,7 +511,7 @@ function changePreset() { document.getElementById('txtRoundVotes').value = '0'; document.getElementById('chkRoundSFs').checked = false; document.getElementById('chkRoundValues').checked = false; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selSurplus').value = 'by_order'; document.getElementById('selMethod').value = 'uig'; document.getElementById('selPapers').value = 'both'; @@ -559,7 +559,7 @@ function changePreset() { document.getElementById('txtRoundVotes').value = '6'; document.getElementById('chkRoundSFs').checked = false; document.getElementById('chkRoundValues').checked = false; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selSurplus').value = 'by_order'; document.getElementById('selMethod').value = 'eg'; document.getElementById('selPapers').value = 'transferable'; @@ -584,7 +584,7 @@ function changePreset() { document.getElementById('chkRoundSFs').checked = true; document.getElementById('txtRoundSFs').value = '4'; document.getElementById('chkRoundValues').checked = false; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selSurplus').value = 'by_size'; document.getElementById('selMethod').value = 'wig'; document.getElementById('selPapers').value = 'both'; @@ -606,7 +606,7 @@ function changePreset() { document.getElementById('chkNormaliseBallots').checked = true; document.getElementById('chkRoundQuota').checked = true; document.getElementById('txtRoundQuota').value = '0'; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selMethod').value = 'cincinnati'; document.getElementById('selPapers').value = 'transferable'; document.getElementById('selExclusion').value = 'single_stage'; @@ -629,7 +629,7 @@ function changePreset() { document.getElementById('chkRoundVotes').checked = false; document.getElementById('chkRoundSFs').checked = false; document.getElementById('chkRoundValues').checked = false; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selSurplus').value = 'by_size'; document.getElementById('selMethod').value = 'wig'; document.getElementById('selPapers').value = 'both'; @@ -656,7 +656,7 @@ function changePreset() { document.getElementById('txtRoundSFs').value = '3'; document.getElementById('chkRoundValues').checked = true; document.getElementById('txtRoundValues').value = '3'; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selSurplus').value = 'by_order'; document.getElementById('selMethod').value = 'eg'; document.getElementById('selPapers').value = 'transferable'; @@ -683,7 +683,7 @@ function changePreset() { document.getElementById('txtRoundSFs').value = '2'; document.getElementById('chkRoundValues').checked = true; document.getElementById('txtRoundValues').value = '2'; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selSurplus').value = 'by_size'; document.getElementById('selMethod').value = 'eg'; document.getElementById('selPapers').value = 'transferable'; @@ -710,7 +710,7 @@ function changePreset() { document.getElementById('txtRoundSFs').value = '2'; document.getElementById('chkRoundValues').checked = true; document.getElementById('txtRoundValues').value = '2'; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selSurplus').value = 'by_size'; document.getElementById('selMethod').value = 'eg'; document.getElementById('selPapers').value = 'transferable'; @@ -737,7 +737,7 @@ function changePreset() { document.getElementById('txtRoundSFs').value = '2'; document.getElementById('chkRoundValues').checked = true; document.getElementById('txtRoundValues').value = '2'; - document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSumTransfers').value = 'by_value'; document.getElementById('selSurplus').value = 'by_size'; document.getElementById('selMethod').value = 'eg'; document.getElementById('selPapers').value = 'transferable'; diff --git a/src/election.rs b/src/election.rs index 830835d..2e25ca4 100644 --- a/src/election.rs +++ b/src/election.rs @@ -352,17 +352,29 @@ impl<'a, N: Number> CountCard<'a, N> { pub struct Parcel<'a, N> { /// [Vote]s in this parcel pub votes: Vec>, + /// Accumulated relative value of each [Vote] in this parcel + pub value_fraction: N, /// Order for sorting with [crate::stv::ExclusionMethod::BySource] pub source_order: usize, } +impl<'a, N: Number> Parcel<'a, N> { + /// Return the number of ballots in this parcel + pub fn num_ballots(&self) -> N { + return self.votes.iter().fold(N::new(), |acc, v| acc + &v.ballot.orig_value); + } + + /// Return the value of the votes in this parcel + pub fn num_votes(&self) -> N { + return self.num_ballots() * &self.value_fraction; + } +} + /// Represents a [Ballot] with an associated value #[derive(Clone)] pub struct Vote<'a, N> { /// Ballot from which the vote is derived pub ballot: &'a Ballot, - /// Current value of the 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 0eac96d..5ae4e56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,7 +86,7 @@ struct STV { round_quota: Option, /// (Gregory STV) How to calculate votes to credit to candidates in surplus transfers - #[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "by_value", "per_ballot"], default_value="single_step", value_name="mode")] + #[clap(help_heading=Some("ROUNDING"), long, possible_values=&["by_value", "per_ballot"], default_value="by_value", value_name="mode")] sum_surplus_transfers: String, /// (Meek STV) Limit for stopping iteration of surplus distribution @@ -259,6 +259,7 @@ fn maybe_load_constraints(election: &mut Election, constraints: &O fn count_election(mut election: Election, cmd_opts: STV) -> Result<(), i32> 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>, diff --git a/src/stv/gregory.rs b/src/stv/gregory.rs index 7f664d2..4b54a21 100644 --- a/src/stv/gregory.rs +++ b/src/stv/gregory.rs @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -use super::{ExclusionMethod, NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder}; +use super::{ExclusionMethod, NextPreferencesEntry, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder}; use super::sample; use crate::constraints; @@ -23,16 +23,14 @@ use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote}; use crate::numbers::Number; use crate::ties; -use itertools::Itertools; - use std::cmp::max; +use std::collections::HashMap; 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(); @@ -42,20 +40,22 @@ pub fn distribute_first_preferences(state: &mut CountState) { 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); - count_card.transfer(&entry.num_votes); + count_card.transfer(&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_votes); + state.exhausted.transfer(&result.exhausted.num_ballots); state.kind = None; state.title = "First preferences".to_string(); @@ -67,7 +67,9 @@ pub fn distribute_first_preferences(state: &mut CountState) { /// 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 { @@ -145,14 +147,12 @@ where /// 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(surplus: &N, result: &NextPreferencesResult, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option +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, weighted: bool, transferable_only: bool) -> Option<&'n N> 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; + let transferable_units = if weighted { transferable_votes } else { transferable_ballots }; if transferable_votes > surplus { return Some(transferable_units); @@ -161,86 +161,64 @@ where } } else { if weighted { - return Some(result.total_votes.clone()); + return Some(total_votes); } else { - return Some(result.total_ballots.clone()); + return Some(total_ballots); } } } -/// Return the reweighted value of the vote after being transferred -fn reweight_vote( - num_votes: &N, - num_ballots: &N, +/// Return the reweighted value fraction of a parcel/vote after being transferred +fn reweight_value_fraction( + value_fraction: &N, surplus: &N, weighted: bool, surplus_fraction: &Option, - surplus_denom: &Option, - round_tvs: Option, - rounding: Option) -> N + surplus_denom: &Option<&N>, + round_tvs: Option) -> N { - let mut result; + let 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(); + result = value_fraction.clone() * surplus_fraction.as_ref().unwrap(); } else { - result = num_ballots.clone() * surplus_fraction.as_ref().unwrap(); + result = surplus_fraction.as_ref().unwrap().clone(); } } else { // Avoid unnecessary rounding error by first multiplying by the surplus if weighted { - result = num_votes.clone() * surplus / v; + result = value_fraction.clone() * surplus / *v; } else { - result = num_ballots.clone() * surplus / v; + result = surplus.clone() / *v; } } } None => { - result = num_votes.clone(); + result = value_fraction.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 +fn sum_surplus_transfers(entry: &NextPreferencesEntry, orig_value_fraction: &N, surplus: &N, is_weighted: bool, surplus_fraction: &Option, surplus_denom: &Option<&N>, _state: &mut CountState, opts: &STVOptions) -> N where + for<'r> &'r N: ops::Mul<&'r N, Output=N>, 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 + // Calculate transfer across all votes in this parcel 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); + for vote in entry.votes.iter() { + result += &vote.ballot.orig_value; } - + result *= reweight_value_fraction(orig_value_fraction, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions); return result; } SumSurplusTransfersMode::PerBallot => { @@ -248,7 +226,11 @@ where // 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); + let mut vote_value = &vote.ballot.orig_value * &reweight_value_fraction(orig_value_fraction, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions); + if let Some(dps) = opts.round_votes { + vote_value.floor_mut(dps); + } + result += vote_value; } //state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals)); return result; @@ -259,7 +241,9 @@ where /// 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::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 { @@ -270,36 +254,63 @@ where let count_card = &state.candidates[elected_candidate]; let surplus = &count_card.votes - state.quota.as_ref().unwrap(); - let votes; + // Determine which votes to examine + + let mut parcels; match opts.surplus { SurplusMethod::WIG | SurplusMethod::UIG => { // Inclusive Gregory - votes = state.candidates.get_mut(elected_candidate).unwrap().concat_parcels(); + 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! - votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap().votes; + parcels = vec![state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap()]; } _ => unreachable!() } - // Count next preferences - let result = super::next_preferences(state, votes); + // 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, result)); + } + + // Calculate surplus fraction - // 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 total_ballots = &transferable_ballots + &exhausted_ballots; + let total_votes = &transferable_votes + &exhausted_votes; + + let surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, is_weighted, opts.transferable_only); let mut surplus_fraction; match surplus_denom { - Some(ref v) => { + Some(v) => { surplus_fraction = Some(surplus.clone() / v); // Round down if requested @@ -308,16 +319,16 @@ where } if opts.transferable_only { - if &result.total_ballots - &result.exhausted.num_ballots == N::one() { + 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$}.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); + 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 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))); + 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$}.", result.total_ballots, result.total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); + 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))); } } } @@ -325,63 +336,79 @@ where surplus_fraction = None; // This can only happen if --transferable-only - if &result.total_ballots - &result.exhausted.num_ballots == N::one() { + 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.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, dps=opts.pp_decimals)); + 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 candidate_transfers: HashMap<&Candidate, N> = HashMap::new(); + for candidate in state.election.candidates.iter() { + candidate_transfers.insert(candidate, N::new()); + } + let mut exhausted_transfers = N::new(); + + for (value_fraction, result) in parcels_next_prefs { + for (candidate, entry) in result.candidates.into_iter() { + // Record transfers + // TODO: Is there a better way of writing this? + let transfers_orig = candidate_transfers.remove(candidate).unwrap(); + let transfers_add = sum_surplus_transfers(&entry, &value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts); + candidate_transfers.insert(candidate, transfers_orig + transfers_add); + + // Transfer candidate votes + let new_parcel = Parcel { + votes: entry.votes, + value_fraction: reweight_value_fraction(&value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_surplus_fractions), + source_order: state.num_elected + state.num_excluded, + }; + let count_card = state.candidates.get_mut(candidate).unwrap(); + count_card.parcels.push(new_parcel); + } + + // Record exhausted votes + if opts.transferable_only { + if transferable_votes > surplus { + // No ballots exhaust + } else { + exhausted_transfers += &surplus - &transferable_votes; + } + } else { + exhausted_transfers += sum_surplus_transfers(&result.exhausted, &value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts); + } + + // Transfer exhausted votes + let parcel = Parcel { + votes: result.exhausted.votes, + value_fraction: value_fraction, // TODO: Reweight exhausted votes + source_order: state.num_elected + state.num_excluded, + }; + state.exhausted.parcels.push(parcel); + } + 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); + // Credit transferred votes + for (candidate, mut votes) in candidate_transfers { + if let Some(dps) = opts.round_votes { + votes.floor_mut(dps); } - - count_card.parcels.push(parcel); + let count_card = state.candidates.get_mut(candidate).unwrap(); + count_card.transfer(&votes); + checksum += votes; } // 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); + if let Some(dps) = opts.round_votes { + exhausted_transfers.floor_mut(dps); } - 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; @@ -397,6 +424,7 @@ where /// 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::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 @@ -416,7 +444,7 @@ where } // Determine votes to transfer in this stage - let mut votes = Vec::new(); + let mut parcels = Vec::new(); let mut votes_remain; let mut checksum = N::new(); @@ -425,13 +453,14 @@ where // 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.finalised = true; + parcels.append(&mut count_card.parcels); + // Update votes - let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value); - checksum -= &votes_transferred; - count_card.transfer(&-votes_transferred); + checksum -= &count_card.votes; + count_card.transfers = -count_card.votes.clone(); + count_card.votes = N::new(); } votes_remain = false; } @@ -445,44 +474,50 @@ where // 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()) + .map(|p| &p.value_fraction) .max().unwrap()) - .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_votes = Vec::new(); + let mut remaining_parcels = 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); + for mut parcel in cc_parcels { + if parcel.value_fraction == max_value { + let votes_transferred = parcel.num_votes(); + votes.append(&mut parcel.votes); + + // Update votes + checksum -= &votes_transferred; + count_card.transfer(&-votes_transferred); } else { - remaining_votes.push(vote); + remaining_parcels.push(parcel); } } - if !remaining_votes.is_empty() { + if !remaining_parcels.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); + // Leave remaining votes with candidate + count_card.parcels = remaining_parcels; } + + // Group all votes of one value in single parcel + parcels.push(Parcel { + votes: votes, + value_fraction: max_value, + source_order: 0, // source_order is unused in this mode + }); } } ExclusionMethod::BySource => { @@ -503,18 +538,20 @@ where 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(); - let mut votes_transferred = N::new(); - while !count_card.parcels.is_empty() { - let parcel = count_card.parcels.pop().unwrap(); + for parcel in cc_parcels { if parcel.source_order == min_order { - for vote in parcel.votes { - votes_transferred += &vote.value; - votes.push(vote); - } + let votes_transferred = parcel.num_votes(); + parcels.push(parcel); + + // Update votes + checksum -= &votes_transferred; + count_card.transfer(&-votes_transferred); } else { remaining_parcels.push(parcel); } @@ -524,12 +561,8 @@ where votes_remain = true; } - // Leave remaining parcels with candidate + // Leave remaining votes with candidate count_card.parcels = remaining_parcels; - - // Update votes - checksum -= &votes_transferred; - count_card.transfer(&-votes_transferred); } } } @@ -545,11 +578,11 @@ where if count_card.parcels.is_empty() { votes_remain = false; } else { - votes = count_card.parcels.remove(0).votes; + parcels.push(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); + let votes_transferred = parcels.first().unwrap().num_votes(); checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } @@ -557,59 +590,88 @@ where _ => panic!() } - if !votes.is_empty() { + let mut total_ballots = N::new(); + let mut total_votes = N::new(); + + let value = match parcels.first() { Some(p) => Some(p.value_fraction.clone()), _ => None }; + + let mut candidate_transfers: HashMap<&Candidate, N> = HashMap::new(); + for candidate in state.election.candidates.iter() { + candidate_transfers.insert(candidate, N::new()); + } + let mut exhausted_transfers = N::new(); + + for parcel in parcels { // Count next preferences - let value = &votes[0].value / &votes[0].ballot.orig_value; + let result = super::next_preferences(state, parcel.votes); - 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))); - } - } + total_ballots += &result.total_ballots; + total_votes += &result.total_ballots * &parcel.value_fraction; // Transfer candidate votes for (candidate, entry) in result.candidates.into_iter() { let parcel = Parcel { votes: entry.votes, + value_fraction: parcel.value_fraction.clone(), source_order: state.num_elected + state.num_excluded, }; + + // Record transfers + let transfers_orig = candidate_transfers.remove(candidate).unwrap(); + candidate_transfers.insert(candidate, transfers_orig + &entry.num_ballots * &parcel.value_fraction); + 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, + value_fraction: parcel.value_fraction, source_order: state.num_elected + state.num_excluded, }; + + // Record transfers + exhausted_transfers += &result.exhausted.num_ballots * &parcel.value_fraction; + 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; + // TODO: Detailed transfers logs } + 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 + for (candidate, mut votes) in candidate_transfers { + if let Some(dps) = opts.round_votes { + votes.floor_mut(dps); + } + let count_card = state.candidates.get_mut(candidate).unwrap(); + count_card.transfer(&votes); + checksum += votes; + } + + // Credit exhausted votes + if let Some(dps) = opts.round_votes { + exhausted_transfers.floor_mut(dps); + } + state.exhausted.transfer(&exhausted_transfers); + checksum += exhausted_transfers; + if !votes_remain { // Finalise candidate votes for excluded_candidate in excluded_candidates.into_iter() { @@ -620,8 +682,7 @@ where count_card.finalised = true; } - if let ExclusionMethod::SingleStage = opts.exclusion { - } else { + if opts.exclusion != ExclusionMethod::SingleStage { state.logger.log_literal("Exclusion complete.".to_string()); } } diff --git a/src/stv/mod.rs b/src/stv/mod.rs index b04a1e0..fb575ee 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -63,7 +63,7 @@ pub struct STVOptions { pub round_quota: Option, /// How to calculate votes to credit to candidates in surplus transfers - #[builder(default="SumSurplusTransfersMode::SingleStep")] + #[builder(default="SumSurplusTransfersMode::ByValue")] pub sum_surplus_transfers: SumSurplusTransfersMode, /// (Meek STV) Limit for stopping iteration of surplus distribution @@ -170,7 +170,7 @@ impl STVOptions { if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); } } if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); } - if self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.describe()); } + if self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::ByValue { flags.push(self.sum_surplus_transfers.describe()); } if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); } if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); } if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); } @@ -226,8 +226,6 @@ impl STVOptions { #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SumSurplusTransfersMode { - /// Sum and round all surplus transfers for a candidate in a single step - SingleStep, /// Sum and round a candidate's surplus transfers separately for ballot papers received at each particular value ByValue, /// Sum and round a candidate's surplus transfers individually for each ballot paper @@ -238,7 +236,6 @@ impl SumSurplusTransfersMode { /// Convert to CLI argument representation fn describe(self) -> String { match self { - SumSurplusTransfersMode::SingleStep => "--sum-surplus-transfers single_step", SumSurplusTransfersMode::ByValue => "--sum-surplus-transfers by_value", SumSurplusTransfersMode::PerBallot => "--sum-surplus-transfers per_ballot", }.to_string() @@ -248,7 +245,6 @@ impl SumSurplusTransfersMode { impl> From for SumSurplusTransfersMode { fn from(s: S) -> Self { match s.as_ref() { - "single_step" => SumSurplusTransfersMode::SingleStep, "by_value" => SumSurplusTransfersMode::ByValue, "per_ballot" => SumSurplusTransfersMode::PerBallot, _ => panic!("Invalid --sum-transfers"), @@ -609,6 +605,7 @@ where /// Returns `true` if the count is complete, otherwise `false`. pub fn count_one_stage<'a, N: Number>(state: &mut CountState<'a, N>, 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>, @@ -671,15 +668,12 @@ struct NextPreferencesResult<'a, N> { candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, exhausted: NextPreferencesEntry<'a, N>, total_ballots: N, - total_votes: N, } /// See [next_preferences] struct NextPreferencesEntry<'a, N> { - //count_card: Option<&'a CountCard<'a, N>>, votes: Vec>, num_ballots: N, - num_votes: N, } /// Count the given votes, grouping according to next available preference @@ -689,15 +683,12 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec(state: &CountState<'a, N>, votes: Vec(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>, diff --git a/src/stv/sample.rs b/src/stv/sample.rs index 56217cf..cb9a88f 100644 --- a/src/stv/sample.rs +++ b/src/stv/sample.rs @@ -140,6 +140,7 @@ where let parcel = Parcel { votes: entry.votes.into_iter().rev().take(candidate_transfers_usize).rev().collect(), + value_fraction: N::one(), source_order: state.num_elected + state.num_excluded, }; @@ -171,6 +172,7 @@ where // Transfer exhausted votes let parcel = Parcel { votes: result.exhausted.votes.into_iter().rev().take(exhausted_transfers_usize).rev().collect(), + value_fraction: N::one(), source_order: state.num_elected + state.num_excluded, }; state.exhausted.parcels.push(parcel); @@ -287,10 +289,10 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio // Have to structure like this to satisfy Rust's borrow checker if let Some(candidate) = next_candidate { // Available preference - state.candidates.get_mut(source_candidate).unwrap().transfer(&-vote.value.clone()); + state.candidates.get_mut(source_candidate).unwrap().transfer(&-vote.ballot.orig_value.clone()); let count_card = state.candidates.get_mut(candidate).unwrap(); - count_card.transfer(&vote.value); + count_card.transfer(&vote.ballot.orig_value); match count_card.parcels.last_mut() { Some(parcel) => { @@ -299,6 +301,7 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio } else { let parcel = Parcel { votes: vec![vote], + value_fraction: N::one(), source_order: state.num_elected + state.num_excluded, }; count_card.parcels.push(parcel); @@ -307,6 +310,7 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio None => { let parcel = Parcel { votes: vec![vote], + value_fraction: N::one(), source_order: state.num_elected + state.num_excluded, }; count_card.parcels.push(parcel); @@ -321,8 +325,8 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio if opts.transferable_only && ignore_nontransferable { // Another ballot paper required } else { - state.candidates.get_mut(source_candidate).unwrap().transfer(&-vote.value.clone()); - state.exhausted.transfer(&vote.value); + state.candidates.get_mut(source_candidate).unwrap().transfer(&-vote.ballot.orig_value.clone()); + state.exhausted.transfer(&vote.ballot.orig_value); match state.exhausted.parcels.last_mut() { Some(parcel) => { @@ -331,6 +335,7 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio } else { let parcel = Parcel { votes: vec![vote], + value_fraction: N::one(), source_order: state.num_elected + state.num_excluded, }; state.exhausted.parcels.push(parcel); @@ -339,6 +344,7 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio None => { let parcel = Parcel { votes: vec![vote], + value_fraction: N::one(), source_order: state.num_elected + state.num_excluded, }; state.exhausted.parcels.push(parcel); diff --git a/tests/scotland.rs b/tests/scotland.rs index 862b3b5..9ce0541 100644 --- a/tests/scotland.rs +++ b/tests/scotland.rs @@ -28,8 +28,8 @@ use std::ops; fn scotland_linn07_fixed5() { let stv_opts = stv::STVOptionsBuilder::default() .round_surplus_fractions(Some(5)) - .round_values(Some(5)) - .round_votes(Some(5)) + //.round_values(Some(5)) + //.round_votes(Some(5)) .round_quota(Some(0)) .sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot) .normalise_ballots(true) @@ -46,7 +46,7 @@ fn scotland_linn07_fixed5() { fn scotland_linn07_gfixed5() { let stv_opts = stv::STVOptionsBuilder::default() .round_surplus_fractions(Some(5)) - .round_values(Some(5)) + .round_values(Some(5)) // Must specify rounding as guarded decimals represented to 10 dps internally .round_votes(Some(5)) .round_quota(Some(0)) .sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot) @@ -106,7 +106,7 @@ where .get_text().unwrap() .to_string(); - assert!((&state.exhausted.votes + &state.loss_fraction.votes) == parse_str(nt_votes)); + assert!((&state.exhausted.votes + &state.loss_fraction.votes) == parse_str(&nt_votes), "Failed to validate NTs. Expected {}, got {}", nt_votes, &state.exhausted.votes + &state.loss_fraction.votes); for (candidate, cand_xml) in state.election.candidates.iter().zip(candidates.iter()) { let count_card = state.candidates.get(candidate).unwrap(); @@ -116,8 +116,8 @@ where .get_child("value").unwrap() .get_text().unwrap() .to_string(); - let cand_votes = parse_str(cand_votes); - assert!(count_card.votes == cand_votes, "Failed to validate votes for candidate {}. Expected {:}, got {:}", candidate.name, cand_votes, count_card.votes); + let cand_votes = parse_str(&cand_votes); + assert!(count_card.votes == cand_votes, "Failed to validate votes for candidate {}. Expected {}, got {}", candidate.name, cand_votes, count_card.votes); // Validate candidate states let cand_state = get_cand_stage(cand_xml, i) @@ -151,7 +151,7 @@ fn get_cand_stage(candidate: &Element, idx: usize) -> &Element { .nth(idx).unwrap(); } -fn parse_str(s: String) -> N { +fn parse_str(s: &str) -> N { if s == "-" { return N::zero(); } - return N::parse(&s); + return N::parse(s); }