Store vote values at the parcel level rather than the vote level

~50% increase in performance
This commit is contained in:
RunasSudo 2021-08-16 00:46:05 +10:00
parent 7341522ba8
commit 94787e7677
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 293 additions and 225 deletions

View File

@ -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: 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.
* *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.
* *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. * *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. 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.

View File

@ -282,8 +282,8 @@
<span class="pill-grey" title="This option has effect only if “Method” is a Gregory method">Gregory</span> <span class="pill-grey" title="This option has effect only if “Method” is a Gregory method">Gregory</span>
Sum surplus transfers: Sum surplus transfers:
<select id="selSumTransfers"> <select id="selSumTransfers">
<option value="single_step" selected>Single step</option> <!--<option value="single_step" selected>Single step</option>-->
<option value="by_value">By value</option> <option value="by_value" selected>By value</option>
<option value="per_ballot">Per ballot</option> <option value="per_ballot">Per ballot</option>
</select> </select>
</label> </label>

View File

@ -379,7 +379,7 @@ function changePreset() {
document.getElementById('chkRoundVotes').checked = false; document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundSFs').checked = false; document.getElementById('chkRoundSFs').checked = false;
document.getElementById('chkRoundValues').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('selSurplus').value = 'by_size';
document.getElementById('selMethod').value = 'wig'; document.getElementById('selMethod').value = 'wig';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -511,7 +511,7 @@ function changePreset() {
document.getElementById('txtRoundVotes').value = '0'; document.getElementById('txtRoundVotes').value = '0';
document.getElementById('chkRoundSFs').checked = false; document.getElementById('chkRoundSFs').checked = false;
document.getElementById('chkRoundValues').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('selSurplus').value = 'by_order';
document.getElementById('selMethod').value = 'uig'; document.getElementById('selMethod').value = 'uig';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -559,7 +559,7 @@ function changePreset() {
document.getElementById('txtRoundVotes').value = '6'; document.getElementById('txtRoundVotes').value = '6';
document.getElementById('chkRoundSFs').checked = false; document.getElementById('chkRoundSFs').checked = false;
document.getElementById('chkRoundValues').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('selSurplus').value = 'by_order';
document.getElementById('selMethod').value = 'eg'; document.getElementById('selMethod').value = 'eg';
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'transferable';
@ -584,7 +584,7 @@ function changePreset() {
document.getElementById('chkRoundSFs').checked = true; document.getElementById('chkRoundSFs').checked = true;
document.getElementById('txtRoundSFs').value = '4'; document.getElementById('txtRoundSFs').value = '4';
document.getElementById('chkRoundValues').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('selSurplus').value = 'by_size';
document.getElementById('selMethod').value = 'wig'; document.getElementById('selMethod').value = 'wig';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -606,7 +606,7 @@ function changePreset() {
document.getElementById('chkNormaliseBallots').checked = true; document.getElementById('chkNormaliseBallots').checked = true;
document.getElementById('chkRoundQuota').checked = true; document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0'; document.getElementById('txtRoundQuota').value = '0';
document.getElementById('selSumTransfers').value = 'single_step'; document.getElementById('selSumTransfers').value = 'by_value';
document.getElementById('selMethod').value = 'cincinnati'; document.getElementById('selMethod').value = 'cincinnati';
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'transferable';
document.getElementById('selExclusion').value = 'single_stage'; document.getElementById('selExclusion').value = 'single_stage';
@ -629,7 +629,7 @@ function changePreset() {
document.getElementById('chkRoundVotes').checked = false; document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundSFs').checked = false; document.getElementById('chkRoundSFs').checked = false;
document.getElementById('chkRoundValues').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('selSurplus').value = 'by_size';
document.getElementById('selMethod').value = 'wig'; document.getElementById('selMethod').value = 'wig';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -656,7 +656,7 @@ function changePreset() {
document.getElementById('txtRoundSFs').value = '3'; document.getElementById('txtRoundSFs').value = '3';
document.getElementById('chkRoundValues').checked = true; document.getElementById('chkRoundValues').checked = true;
document.getElementById('txtRoundValues').value = '3'; 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('selSurplus').value = 'by_order';
document.getElementById('selMethod').value = 'eg'; document.getElementById('selMethod').value = 'eg';
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'transferable';
@ -683,7 +683,7 @@ function changePreset() {
document.getElementById('txtRoundSFs').value = '2'; document.getElementById('txtRoundSFs').value = '2';
document.getElementById('chkRoundValues').checked = true; document.getElementById('chkRoundValues').checked = true;
document.getElementById('txtRoundValues').value = '2'; 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('selSurplus').value = 'by_size';
document.getElementById('selMethod').value = 'eg'; document.getElementById('selMethod').value = 'eg';
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'transferable';
@ -710,7 +710,7 @@ function changePreset() {
document.getElementById('txtRoundSFs').value = '2'; document.getElementById('txtRoundSFs').value = '2';
document.getElementById('chkRoundValues').checked = true; document.getElementById('chkRoundValues').checked = true;
document.getElementById('txtRoundValues').value = '2'; 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('selSurplus').value = 'by_size';
document.getElementById('selMethod').value = 'eg'; document.getElementById('selMethod').value = 'eg';
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'transferable';
@ -737,7 +737,7 @@ function changePreset() {
document.getElementById('txtRoundSFs').value = '2'; document.getElementById('txtRoundSFs').value = '2';
document.getElementById('chkRoundValues').checked = true; document.getElementById('chkRoundValues').checked = true;
document.getElementById('txtRoundValues').value = '2'; 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('selSurplus').value = 'by_size';
document.getElementById('selMethod').value = 'eg'; document.getElementById('selMethod').value = 'eg';
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'transferable';

View File

@ -352,17 +352,29 @@ impl<'a, N: Number> CountCard<'a, N> {
pub struct Parcel<'a, N> { pub struct Parcel<'a, N> {
/// [Vote]s in this parcel /// [Vote]s in this parcel
pub votes: Vec<Vote<'a, N>>, pub votes: Vec<Vote<'a, N>>,
/// Accumulated relative value of each [Vote] in this parcel
pub value_fraction: N,
/// Order for sorting with [crate::stv::ExclusionMethod::BySource] /// Order for sorting with [crate::stv::ExclusionMethod::BySource]
pub source_order: usize, 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 /// Represents a [Ballot] with an associated value
#[derive(Clone)] #[derive(Clone)]
pub struct Vote<'a, N> { pub struct Vote<'a, N> {
/// Ballot from which the vote is derived /// Ballot from which the vote is derived
pub ballot: &'a Ballot<N>, pub ballot: &'a Ballot<N>,
/// Current value of the ballot
pub value: N,
/// Index of the next preference to examine /// Index of the next preference to examine
pub up_to_pref: usize, pub up_to_pref: usize,
} }

View File

@ -86,7 +86,7 @@ struct STV {
round_quota: Option<usize>, round_quota: Option<usize>,
/// (Gregory STV) How to calculate votes to credit to candidates in surplus transfers /// (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, sum_surplus_transfers: String,
/// (Meek STV) Limit for stopping iteration of surplus distribution /// (Meek STV) Limit for stopping iteration of surplus distribution
@ -259,6 +259,7 @@ fn maybe_load_constraints<N: Number>(election: &mut Election<N>, constraints: &O
fn count_election<N: Number>(mut election: Election<N>, cmd_opts: STV) -> Result<(), i32> fn count_election<N: Number>(mut election: Election<N>, cmd_opts: STV) -> Result<(), i32>
where 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::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'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::Div<&'r N, Output=N>,

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use super::{ExclusionMethod, NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder}; use super::{ExclusionMethod, NextPreferencesEntry, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
use super::sample; use super::sample;
use crate::constraints; use crate::constraints;
@ -23,16 +23,14 @@ use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote};
use crate::numbers::Number; use crate::numbers::Number;
use crate::ties; use crate::ties;
use itertools::Itertools;
use std::cmp::max; use std::cmp::max;
use std::collections::HashMap;
use std::ops; use std::ops;
/// Distribute first preference votes according to the Gregory method /// Distribute first preference votes according to the Gregory method
pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) { pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
let votes = state.election.ballots.iter().map(|b| Vote { let votes = state.election.ballots.iter().map(|b| Vote {
ballot: b, ballot: b,
value: b.orig_value.clone(),
up_to_pref: 0, up_to_pref: 0,
}).collect(); }).collect();
@ -42,20 +40,22 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
for (candidate, entry) in result.candidates.into_iter() { for (candidate, entry) in result.candidates.into_iter() {
let parcel = Parcel { let parcel = Parcel {
votes: entry.votes, votes: entry.votes,
value_fraction: N::one(),
source_order: 0, source_order: 0,
}; };
let count_card = state.candidates.get_mut(candidate).unwrap(); let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.push(parcel); count_card.parcels.push(parcel);
count_card.transfer(&entry.num_votes); count_card.transfer(&entry.num_ballots);
} }
// Transfer exhausted votes // Transfer exhausted votes
let parcel = Parcel { let parcel = Parcel {
votes: result.exhausted.votes, votes: result.exhausted.votes,
value_fraction: N::one(),
source_order: 0, source_order: 0,
}; };
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
state.exhausted.transfer(&result.exhausted.num_votes); state.exhausted.transfer(&result.exhausted.num_ballots);
state.kind = None; state.kind = None;
state.title = "First preferences".to_string(); state.title = "First preferences".to_string();
@ -67,7 +67,9 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
/// Returns `true` if any surpluses were distributed. /// Returns `true` if any surpluses were distributed.
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
where 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::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::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N> for<'r> &'r N: ops::Neg<Output=N>
{ {
@ -145,14 +147,12 @@ where
/// Return the denominator of the surplus fraction /// 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). /// Returns `None` if the value of transferable votes <= surplus (i.e. all transferable votes are transferred at values received).
fn calculate_surplus_denom<N: Number>(surplus: &N, result: &NextPreferencesResult<N>, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option<N> 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 where
for<'r> &'r N: ops::Sub<&'r N, Output=N> for<'r> &'r N: ops::Sub<&'r N, Output=N>
{ {
if transferable_only { if transferable_only {
let total_units = if weighted { &result.total_votes } else { &result.total_ballots }; let transferable_units = if weighted { transferable_votes } else { transferable_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 { if transferable_votes > surplus {
return Some(transferable_units); return Some(transferable_units);
@ -161,86 +161,64 @@ where
} }
} else { } else {
if weighted { if weighted {
return Some(result.total_votes.clone()); return Some(total_votes);
} else { } else {
return Some(result.total_ballots.clone()); return Some(total_ballots);
} }
} }
} }
/// Return the reweighted value of the vote after being transferred /// Return the reweighted value fraction of a parcel/vote after being transferred
fn reweight_vote<N: Number>( fn reweight_value_fraction<N: Number>(
num_votes: &N, value_fraction: &N,
num_ballots: &N,
surplus: &N, surplus: &N,
weighted: bool, weighted: bool,
surplus_fraction: &Option<N>, surplus_fraction: &Option<N>,
surplus_denom: &Option<N>, surplus_denom: &Option<&N>,
round_tvs: Option<usize>, round_tvs: Option<usize>) -> N
rounding: Option<usize>) -> N
{ {
let mut result; let result;
match surplus_denom { match surplus_denom {
Some(v) => { Some(v) => {
if let Some(_) = round_tvs { if let Some(_) = round_tvs {
// Rounding requested: use the rounded transfer value // Rounding requested: use the rounded transfer value
if weighted { if weighted {
result = num_votes.clone() * surplus_fraction.as_ref().unwrap(); result = value_fraction.clone() * surplus_fraction.as_ref().unwrap();
} else { } else {
result = num_ballots.clone() * surplus_fraction.as_ref().unwrap(); result = surplus_fraction.as_ref().unwrap().clone();
} }
} else { } else {
// Avoid unnecessary rounding error by first multiplying by the surplus // Avoid unnecessary rounding error by first multiplying by the surplus
if weighted { if weighted {
result = num_votes.clone() * surplus / v; result = value_fraction.clone() * surplus / *v;
} else { } else {
result = num_ballots.clone() * surplus / v; result = surplus.clone() / *v;
} }
} }
} }
None => { None => {
result = num_votes.clone(); result = value_fraction.clone();
} }
} }
// Round down if requested
if let Some(dps) = rounding {
result.floor_mut(dps);
}
return result; return result;
} }
/// Compute the number of votes to credit to a continuing candidate during a surplus transfer, based on [STVOptions::sum_surplus_transfers] /// 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<N: Number>(entry: &NextPreferencesEntry<N>, surplus: &N, is_weighted: bool, surplus_fraction: &Option<N>, surplus_denom: &Option<N>, _state: &mut CountState<N>, opts: &STVOptions) -> N fn sum_surplus_transfers<N: Number>(entry: &NextPreferencesEntry<N>, orig_value_fraction: &N, surplus: &N, is_weighted: bool, surplus_fraction: &Option<N>, surplus_denom: &Option<&N>, _state: &mut CountState<N>, opts: &STVOptions) -> N
where where
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::Div<&'r N, Output=N>,
{ {
match opts.sum_surplus_transfers { 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 => { SumSurplusTransfersMode::ByValue => {
// Sum transfers by value // Calculate transfer across all votes in this parcel
let mut result = N::new(); let mut result = N::new();
for vote in entry.votes.iter() {
// Sort into parcels by value result += &vote.ballot.orig_value;
let mut votes: Vec<&Vote<N>> = 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);
} }
result *= reweight_value_fraction(orig_value_fraction, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions);
return result; return result;
} }
SumSurplusTransfersMode::PerBallot => { 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 // TODO: This could be moved to distribute_surplus to avoid looping over the votes and calculating transfer values twice
let mut result = N::new(); let mut result = N::new();
for vote in entry.votes.iter() { 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)); //state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
return result; return result;
@ -259,7 +241,9 @@ where
/// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus] /// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus]
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate) fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
where 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::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::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N> for<'r> &'r N: ops::Neg<Output=N>
{ {
@ -270,36 +254,63 @@ where
let count_card = &state.candidates[elected_candidate]; let count_card = &state.candidates[elected_candidate];
let surplus = &count_card.votes - state.quota.as_ref().unwrap(); let surplus = &count_card.votes - state.quota.as_ref().unwrap();
let votes; // Determine which votes to examine
let mut parcels;
match opts.surplus { match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG => { SurplusMethod::WIG | SurplusMethod::UIG => {
// Inclusive Gregory // 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 => { SurplusMethod::EG => {
// Exclusive Gregory // Exclusive Gregory
// Should be safe to unwrap() - or else how did we get a quota! // 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!() _ => unreachable!()
} }
// Count next preferences // Count votes
let result = super::next_preferences(state, 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 { let is_weighted = match opts.surplus {
SurplusMethod::WIG => { true } SurplusMethod::WIG => { true }
SurplusMethod::UIG | SurplusMethod::EG => { false } SurplusMethod::UIG | SurplusMethod::EG => { false }
_ => unreachable!() _ => unreachable!()
}; };
let transferable_votes = &result.total_votes - &result.exhausted.num_votes; let total_ballots = &transferable_ballots + &exhausted_ballots;
let surplus_denom = calculate_surplus_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only); 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; let mut surplus_fraction;
match surplus_denom { match surplus_denom {
Some(ref v) => { Some(v) => {
surplus_fraction = Some(surplus.clone() / v); surplus_fraction = Some(surplus.clone() / v);
// Round down if requested // Round down if requested
@ -308,16 +319,16 @@ where
} }
if opts.transferable_only { 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))); 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 { } 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 { } else {
if result.total_ballots == N::one() { if 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))); 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 { } 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; surplus_fraction = None;
// This can only happen if --transferable-only // 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)); state.logger.log_literal(format!("Transferring 1 transferable ballot, totalling {:.dps$} transferable votes, at values received.", transferable_votes, dps=opts.pp_decimals));
} else { } 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(); let mut checksum = N::new();
for (candidate, entry) in result.candidates.into_iter() { // Credit transferred votes
// Credit transferred votes for (candidate, mut votes) in candidate_transfers {
let candidate_transfers = sum_surplus_transfers(&entry, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts); if let Some(dps) = opts.round_votes {
let count_card = state.candidates.get_mut(candidate).unwrap(); votes.floor_mut(dps);
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);
} }
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.push(parcel); count_card.transfer(&votes);
checksum += votes;
} }
// Credit exhausted votes // Credit exhausted votes
let mut exhausted_transfers; if let Some(dps) = opts.round_votes {
if opts.transferable_only { exhausted_transfers.floor_mut(dps);
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); state.exhausted.transfer(&exhausted_transfers);
checksum += 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 // Finalise candidate votes
let count_card = state.candidates.get_mut(elected_candidate).unwrap(); let count_card = state.candidates.get_mut(elected_candidate).unwrap();
count_card.transfers = -&surplus; 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] /// 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>) pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
where where
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::Div<&'r N, Output=N>,
{ {
// Used to give bulk excluded candidate the same order_elected // Used to give bulk excluded candidate the same order_elected
@ -416,7 +444,7 @@ where
} }
// Determine votes to transfer in this stage // Determine votes to transfer in this stage
let mut votes = Vec::new(); let mut parcels = Vec::new();
let mut votes_remain; let mut votes_remain;
let mut checksum = N::new(); let mut checksum = N::new();
@ -425,13 +453,14 @@ where
// Exclude in one round // Exclude in one round
for excluded_candidate in excluded_candidates.iter() { for excluded_candidate in excluded_candidates.iter() {
let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
votes.append(&mut count_card.concat_parcels());
count_card.finalised = true; count_card.finalised = true;
parcels.append(&mut count_card.parcels);
// Update votes // Update votes
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value); checksum -= &count_card.votes;
checksum -= &votes_transferred; count_card.transfers = -count_card.votes.clone();
count_card.transfer(&-votes_transferred); count_card.votes = N::new();
} }
votes_remain = false; votes_remain = false;
} }
@ -445,44 +474,50 @@ where
// If candidates to exclude still having votes, select only those with the greatest value // If candidates to exclude still having votes, select only those with the greatest value
let max_value = excluded_with_votes.iter() let max_value = excluded_with_votes.iter()
.map(|c| state.candidates[*c].parcels.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(); .max().unwrap()
.clone();
votes_remain = false; votes_remain = false;
let mut votes = Vec::new();
for excluded_candidate in excluded_with_votes.iter() { for excluded_candidate in excluded_with_votes.iter() {
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap(); 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 // 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(); for mut parcel in cc_parcels {
if parcel.value_fraction == max_value {
let votes_transferred = parcel.num_votes();
votes.append(&mut parcel.votes);
let mut votes_transferred = N::new(); // Update votes
for vote in cand_votes.into_iter() { checksum -= &votes_transferred;
if &vote.value / &vote.ballot.orig_value == max_value { count_card.transfer(&-votes_transferred);
votes_transferred += &vote.value;
votes.push(vote);
} else { } else {
remaining_votes.push(vote); remaining_parcels.push(parcel);
} }
} }
if !remaining_votes.is_empty() { if !remaining_parcels.is_empty() {
votes_remain = true; votes_remain = true;
} }
// Leave remaining votes with candidate (as one parcel) // Leave remaining votes with candidate
count_card.parcels = vec![Parcel { count_card.parcels = remaining_parcels;
votes: remaining_votes,
source_order: 0, // Unused in this mode
}];
// Update votes
checksum -= &votes_transferred;
count_card.transfer(&-votes_transferred);
} }
// 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 => { ExclusionMethod::BySource => {
@ -503,18 +538,20 @@ where
for excluded_candidate in excluded_with_votes.iter() { for excluded_candidate in excluded_with_votes.iter() {
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap(); 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 // Filter out just those votes with min_order
let mut remaining_parcels = Vec::new(); let mut remaining_parcels = Vec::new();
let mut votes_transferred = N::new(); for parcel in cc_parcels {
while !count_card.parcels.is_empty() {
let parcel = count_card.parcels.pop().unwrap();
if parcel.source_order == min_order { if parcel.source_order == min_order {
for vote in parcel.votes { let votes_transferred = parcel.num_votes();
votes_transferred += &vote.value; parcels.push(parcel);
votes.push(vote);
} // Update votes
checksum -= &votes_transferred;
count_card.transfer(&-votes_transferred);
} else { } else {
remaining_parcels.push(parcel); remaining_parcels.push(parcel);
} }
@ -524,12 +561,8 @@ where
votes_remain = true; votes_remain = true;
} }
// Leave remaining parcels with candidate // Leave remaining votes with candidate
count_card.parcels = remaining_parcels; 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() { if count_card.parcels.is_empty() {
votes_remain = false; votes_remain = false;
} else { } else {
votes = count_card.parcels.remove(0).votes; parcels.push(count_card.parcels.remove(0));
votes_remain = !count_card.parcels.is_empty(); votes_remain = !count_card.parcels.is_empty();
// Update votes // 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; checksum -= &votes_transferred;
count_card.transfer(&-votes_transferred); count_card.transfer(&-votes_transferred);
} }
@ -557,59 +590,88 @@ where
_ => panic!() _ => 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 // 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); total_ballots += &result.total_ballots;
total_votes += &result.total_ballots * &parcel.value_fraction;
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 // Transfer candidate votes
for (candidate, entry) in result.candidates.into_iter() { for (candidate, entry) in result.candidates.into_iter() {
let parcel = Parcel { let parcel = Parcel {
votes: entry.votes, votes: entry.votes,
value_fraction: parcel.value_fraction.clone(),
source_order: state.num_elected + state.num_excluded, 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(); let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.push(parcel); 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 // Transfer exhausted votes
let parcel = Parcel { let parcel = Parcel {
votes: result.exhausted.votes, votes: result.exhausted.votes,
value_fraction: parcel.value_fraction,
source_order: state.num_elected + state.num_excluded, source_order: state.num_elected + state.num_excluded,
}; };
// Record transfers
exhausted_transfers += &result.exhausted.num_ballots * &parcel.value_fraction;
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
let mut exhausted_transfers = result.exhausted.num_votes; // TODO: Detailed transfers logs
if let Some(dps) = opts.round_votes {
exhausted_transfers.floor_mut(dps);
}
state.exhausted.transfer(&exhausted_transfers);
checksum += exhausted_transfers;
} }
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 { if !votes_remain {
// Finalise candidate votes // Finalise candidate votes
for excluded_candidate in excluded_candidates.into_iter() { for excluded_candidate in excluded_candidates.into_iter() {
@ -620,8 +682,7 @@ where
count_card.finalised = true; count_card.finalised = true;
} }
if let ExclusionMethod::SingleStage = opts.exclusion { if opts.exclusion != ExclusionMethod::SingleStage {
} else {
state.logger.log_literal("Exclusion complete.".to_string()); state.logger.log_literal("Exclusion complete.".to_string());
} }
} }

View File

@ -63,7 +63,7 @@ pub struct STVOptions {
pub round_quota: Option<usize>, pub round_quota: Option<usize>,
/// How to calculate votes to credit to candidates in surplus transfers /// How to calculate votes to credit to candidates in surplus transfers
#[builder(default="SumSurplusTransfersMode::SingleStep")] #[builder(default="SumSurplusTransfersMode::ByValue")]
pub sum_surplus_transfers: SumSurplusTransfersMode, pub sum_surplus_transfers: SumSurplusTransfersMode,
/// (Meek STV) Limit for stopping iteration of surplus distribution /// (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_votes { flags.push(format!("--round-votes {}", dps)); }
} }
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", 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.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.normalise_ballots { flags.push("--normalise-ballots".to_string()); }
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); } if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
@ -226,8 +226,6 @@ impl STVOptions {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum SumSurplusTransfersMode { 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 /// Sum and round a candidate's surplus transfers separately for ballot papers received at each particular value
ByValue, ByValue,
/// Sum and round a candidate's surplus transfers individually for each ballot paper /// Sum and round a candidate's surplus transfers individually for each ballot paper
@ -238,7 +236,6 @@ impl SumSurplusTransfersMode {
/// Convert to CLI argument representation /// Convert to CLI argument representation
fn describe(self) -> String { fn describe(self) -> String {
match self { match self {
SumSurplusTransfersMode::SingleStep => "--sum-surplus-transfers single_step",
SumSurplusTransfersMode::ByValue => "--sum-surplus-transfers by_value", SumSurplusTransfersMode::ByValue => "--sum-surplus-transfers by_value",
SumSurplusTransfersMode::PerBallot => "--sum-surplus-transfers per_ballot", SumSurplusTransfersMode::PerBallot => "--sum-surplus-transfers per_ballot",
}.to_string() }.to_string()
@ -248,7 +245,6 @@ impl SumSurplusTransfersMode {
impl<S: AsRef<str>> From<S> for SumSurplusTransfersMode { impl<S: AsRef<str>> From<S> for SumSurplusTransfersMode {
fn from(s: S) -> Self { fn from(s: S) -> Self {
match s.as_ref() { match s.as_ref() {
"single_step" => SumSurplusTransfersMode::SingleStep,
"by_value" => SumSurplusTransfersMode::ByValue, "by_value" => SumSurplusTransfersMode::ByValue,
"per_ballot" => SumSurplusTransfersMode::PerBallot, "per_ballot" => SumSurplusTransfersMode::PerBallot,
_ => panic!("Invalid --sum-transfers"), _ => panic!("Invalid --sum-transfers"),
@ -609,6 +605,7 @@ where
/// Returns `true` if the count is complete, otherwise `false`. /// 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<bool, STVError> pub fn count_one_stage<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where 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::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'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::Div<&'r N, Output=N>,
@ -671,15 +668,12 @@ struct NextPreferencesResult<'a, N> {
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
exhausted: NextPreferencesEntry<'a, N>, exhausted: NextPreferencesEntry<'a, N>,
total_ballots: N, total_ballots: N,
total_votes: N,
} }
/// See [next_preferences] /// See [next_preferences]
struct NextPreferencesEntry<'a, N> { struct NextPreferencesEntry<'a, N> {
//count_card: Option<&'a CountCard<'a, N>>,
votes: Vec<Vote<'a, N>>, votes: Vec<Vote<'a, N>>,
num_ballots: N, num_ballots: N,
num_votes: N,
} }
/// Count the given votes, grouping according to next available preference /// 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<Vote<'a
exhausted: NextPreferencesEntry { exhausted: NextPreferencesEntry {
votes: Vec::new(), votes: Vec::new(),
num_ballots: N::new(), num_ballots: N::new(),
num_votes: N::new(),
}, },
total_ballots: N::new(), total_ballots: N::new(),
total_votes: N::new(),
}; };
for mut vote in votes.into_iter() { for mut vote in votes.into_iter() {
result.total_ballots += &vote.ballot.orig_value; result.total_ballots += &vote.ballot.orig_value;
result.total_votes += &vote.value;
let mut next_candidate = None; let mut next_candidate = None;
@ -717,19 +708,16 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a
if result.candidates.contains_key(candidate) { if result.candidates.contains_key(candidate) {
let entry = result.candidates.get_mut(candidate).unwrap(); let entry = result.candidates.get_mut(candidate).unwrap();
entry.num_ballots += &vote.ballot.orig_value; entry.num_ballots += &vote.ballot.orig_value;
entry.num_votes += &vote.value;
entry.votes.push(vote); entry.votes.push(vote);
} else { } else {
let entry = NextPreferencesEntry { let entry = NextPreferencesEntry {
num_ballots: vote.ballot.orig_value.clone(), num_ballots: vote.ballot.orig_value.clone(),
num_votes: vote.value.clone(),
votes: vec![vote], votes: vec![vote],
}; };
result.candidates.insert(candidate, entry); result.candidates.insert(candidate, entry);
} }
} else { } else {
result.exhausted.num_ballots += &vote.ballot.orig_value; result.exhausted.num_ballots += &vote.ballot.orig_value;
result.exhausted.num_votes += &vote.value;
result.exhausted.votes.push(vote); result.exhausted.votes.push(vote);
} }
} }
@ -1121,6 +1109,7 @@ where
/// Returns `true` if any surpluses were distributed. /// Returns `true` if any surpluses were distributed.
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
where 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::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'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::Div<&'r N, Output=N>,

View File

@ -140,6 +140,7 @@ where
let parcel = Parcel { let parcel = Parcel {
votes: entry.votes.into_iter().rev().take(candidate_transfers_usize).rev().collect(), votes: entry.votes.into_iter().rev().take(candidate_transfers_usize).rev().collect(),
value_fraction: N::one(),
source_order: state.num_elected + state.num_excluded, source_order: state.num_elected + state.num_excluded,
}; };
@ -171,6 +172,7 @@ where
// Transfer exhausted votes // Transfer exhausted votes
let parcel = Parcel { let parcel = Parcel {
votes: result.exhausted.votes.into_iter().rev().take(exhausted_transfers_usize).rev().collect(), 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, source_order: state.num_elected + state.num_excluded,
}; };
state.exhausted.parcels.push(parcel); 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 // Have to structure like this to satisfy Rust's borrow checker
if let Some(candidate) = next_candidate { if let Some(candidate) = next_candidate {
// Available preference // 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(); 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() { match count_card.parcels.last_mut() {
Some(parcel) => { Some(parcel) => {
@ -299,6 +301,7 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio
} else { } else {
let parcel = Parcel { let parcel = Parcel {
votes: vec![vote], votes: vec![vote],
value_fraction: N::one(),
source_order: state.num_elected + state.num_excluded, source_order: state.num_elected + state.num_excluded,
}; };
count_card.parcels.push(parcel); count_card.parcels.push(parcel);
@ -307,6 +310,7 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio
None => { None => {
let parcel = Parcel { let parcel = Parcel {
votes: vec![vote], votes: vec![vote],
value_fraction: N::one(),
source_order: state.num_elected + state.num_excluded, source_order: state.num_elected + state.num_excluded,
}; };
count_card.parcels.push(parcel); 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 { if opts.transferable_only && ignore_nontransferable {
// Another ballot paper required // Another ballot paper required
} else { } else {
state.candidates.get_mut(source_candidate).unwrap().transfer(&-vote.value.clone()); state.candidates.get_mut(source_candidate).unwrap().transfer(&-vote.ballot.orig_value.clone());
state.exhausted.transfer(&vote.value); state.exhausted.transfer(&vote.ballot.orig_value);
match state.exhausted.parcels.last_mut() { match state.exhausted.parcels.last_mut() {
Some(parcel) => { Some(parcel) => {
@ -331,6 +335,7 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio
} else { } else {
let parcel = Parcel { let parcel = Parcel {
votes: vec![vote], votes: vec![vote],
value_fraction: N::one(),
source_order: state.num_elected + state.num_excluded, source_order: state.num_elected + state.num_excluded,
}; };
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
@ -339,6 +344,7 @@ fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptio
None => { None => {
let parcel = Parcel { let parcel = Parcel {
votes: vec![vote], votes: vec![vote],
value_fraction: N::one(),
source_order: state.num_elected + state.num_excluded, source_order: state.num_elected + state.num_excluded,
}; };
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);

View File

@ -28,8 +28,8 @@ use std::ops;
fn scotland_linn07_fixed5() { fn scotland_linn07_fixed5() {
let stv_opts = stv::STVOptionsBuilder::default() let stv_opts = stv::STVOptionsBuilder::default()
.round_surplus_fractions(Some(5)) .round_surplus_fractions(Some(5))
.round_values(Some(5)) //.round_values(Some(5))
.round_votes(Some(5)) //.round_votes(Some(5))
.round_quota(Some(0)) .round_quota(Some(0))
.sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot) .sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot)
.normalise_ballots(true) .normalise_ballots(true)
@ -46,7 +46,7 @@ fn scotland_linn07_fixed5() {
fn scotland_linn07_gfixed5() { fn scotland_linn07_gfixed5() {
let stv_opts = stv::STVOptionsBuilder::default() let stv_opts = stv::STVOptionsBuilder::default()
.round_surplus_fractions(Some(5)) .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_votes(Some(5))
.round_quota(Some(0)) .round_quota(Some(0))
.sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot) .sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot)
@ -106,7 +106,7 @@ where
.get_text().unwrap() .get_text().unwrap()
.to_string(); .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()) { for (candidate, cand_xml) in state.election.candidates.iter().zip(candidates.iter()) {
let count_card = state.candidates.get(candidate).unwrap(); let count_card = state.candidates.get(candidate).unwrap();
@ -116,8 +116,8 @@ where
.get_child("value").unwrap() .get_child("value").unwrap()
.get_text().unwrap() .get_text().unwrap()
.to_string(); .to_string();
let cand_votes = parse_str(cand_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); assert!(count_card.votes == cand_votes, "Failed to validate votes for candidate {}. Expected {}, got {}", candidate.name, cand_votes, count_card.votes);
// Validate candidate states // Validate candidate states
let cand_state = get_cand_stage(cand_xml, i) 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(); .nth(idx).unwrap();
} }
fn parse_str<N: Number>(s: String) -> N { fn parse_str<N: Number>(s: &str) -> N {
if s == "-" { return N::zero(); } if s == "-" { return N::zero(); }
return N::parse(&s); return N::parse(s);
} }