Store vote values at the parcel level rather than the vote level
~50% increase in performance
This commit is contained in:
parent
7341522ba8
commit
94787e7677
@ -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.
|
||||||
|
@ -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>
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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>,
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>,
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user