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

View File

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

View File

@ -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';

View File

@ -352,17 +352,29 @@ impl<'a, N: Number> CountCard<'a, N> {
pub struct Parcel<'a, N> {
/// [Vote]s in this parcel
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]
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<N>,
/// Current value of the ballot
pub value: N,
/// Index of the next preference to examine
pub up_to_pref: usize,
}

View File

@ -86,7 +86,7 @@ struct STV {
round_quota: Option<usize>,
/// (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<N: Number>(election: &mut Election<N>, constraints: &O
fn count_election<N: Number>(mut election: Election<N>, 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>,

View File

@ -15,7 +15,7 @@
* 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 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<N: Number>(state: &mut CountState<N>) {
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<N: Number>(state: &mut CountState<N>) {
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<N: Number>(state: &mut CountState<N>) {
/// Returns `true` if any surpluses were distributed.
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
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<Output=N>
{
@ -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<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
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<N: Number>(
num_votes: &N,
num_ballots: &N,
/// Return the reweighted value fraction of a parcel/vote after being transferred
fn reweight_value_fraction<N: Number>(
value_fraction: &N,
surplus: &N,
weighted: bool,
surplus_fraction: &Option<N>,
surplus_denom: &Option<N>,
round_tvs: Option<usize>,
rounding: Option<usize>) -> N
surplus_denom: &Option<&N>,
round_tvs: Option<usize>) -> 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<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
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<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);
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<N: Number>(state: &mut CountState<N>, 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<Output=N>
{
@ -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();
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();
for vote in cand_votes.into_iter() {
if &vote.value / &vote.ballot.orig_value == max_value {
votes_transferred += &vote.value;
votes.push(vote);
// 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());
}
}

View File

@ -63,7 +63,7 @@ pub struct STVOptions {
pub round_quota: Option<usize>,
/// 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<S: AsRef<str>> From<S> 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<bool, STVError>
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<Vote<'a, N>>,
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<Vote<'a
exhausted: NextPreferencesEntry {
votes: Vec::new(),
num_ballots: N::new(),
num_votes: N::new(),
},
total_ballots: N::new(),
total_votes: N::new(),
};
for mut vote in votes.into_iter() {
result.total_ballots += &vote.ballot.orig_value;
result.total_votes += &vote.value;
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) {
let entry = result.candidates.get_mut(candidate).unwrap();
entry.num_ballots += &vote.ballot.orig_value;
entry.num_votes += &vote.value;
entry.votes.push(vote);
} else {
let entry = NextPreferencesEntry {
num_ballots: vote.ballot.orig_value.clone(),
num_votes: vote.value.clone(),
votes: vec![vote],
};
result.candidates.insert(candidate, entry);
}
} else {
result.exhausted.num_ballots += &vote.ballot.orig_value;
result.exhausted.num_votes += &vote.value;
result.exhausted.votes.push(vote);
}
}
@ -1121,6 +1109,7 @@ where
/// Returns `true` if any surpluses were distributed.
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
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>,

View File

@ -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);

View File

@ -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<N: Number>(s: String) -> N {
fn parse_str<N: Number>(s: &str) -> N {
if s == "-" { return N::zero(); }
return N::parse(&s);
return N::parse(s);
}