Implement --sum-surplus-transfers

This commit is contained in:
RunasSudo 2021-06-11 21:22:28 +10:00
parent 9d4cac2e89
commit 96a3eaec84
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
10 changed files with 176 additions and 40 deletions

16
Cargo.lock generated
View File

@ -151,6 +151,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.20" version = "1.0.20"
@ -233,6 +239,15 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "itertools"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.7" version = "0.4.7"
@ -338,6 +353,7 @@ dependencies = [
"flate2", "flate2",
"git-version", "git-version",
"ibig", "ibig",
"itertools",
"js-sys", "js-sys",
"num-bigint", "num-bigint",
"num-rational", "num-rational",

View File

@ -11,6 +11,7 @@ crate-type = ["lib", "cdylib"]
derive_more = "0.99.14" derive_more = "0.99.14"
git-version = "0.3.4" git-version = "0.3.4"
ibig = "0.3.2" ibig = "0.3.2"
itertools = "0.10.1"
num-traits = "0.2" num-traits = "0.2"
wasm-bindgen = "0.2.74" wasm-bindgen = "0.2.74"

View File

@ -35,7 +35,8 @@
<label> <label>
Preset: Preset:
<select id="selPreset" onchange="changePreset()"> <select id="selPreset" onchange="changePreset()">
<option value="scottish" selected>Scottish STV</option> <option value="wigm" selected>Recommended WIGM</option>
<option value="scottish">Scottish STV</option>
<option value="senate">Australian Senate STV</option> <option value="senate">Australian Senate STV</option>
<!--<option value="meek">Meek STV</option> <!--<option value="meek">Meek STV</option>
<option value="wright">Wright STV</option>--> <option value="wright">Wright STV</option>-->
@ -58,14 +59,14 @@
<label> <label>
Quota: Quota:
<select id="selQuotaCriterion"> <select id="selQuotaCriterion">
<option value="geq" selected>&gt;=</option> <option value="geq">&geq;</option>
<option value="gt">&gt;</option> <option value="gt" selected>&gt;</option>
</select> </select>
</label> </label>
<label> <label>
<select id="selQuota"> <select id="selQuota">
<option value="droop" selected>Droop</option> <option value="droop">Droop</option>
<option value="droop_exact">Droop (exact)</option> <option value="droop_exact" selected>Droop (exact)</option>
<option value="hare">Hare</option> <option value="hare">Hare</option>
<option value="hare_exact">Hare (exact)</option> <option value="hare_exact">Hare (exact)</option>
</select> </select>
@ -146,8 +147,8 @@
<label> <label>
Numbers: Numbers:
<select id="selNumbers"> <select id="selNumbers">
<option value="rational">Rational</option> <option value="rational" selected>Rational</option>
<option value="fixed" selected>Fixed</option> <option value="fixed">Fixed</option>
<!--<option value="gfixed">Fixed (guarded)</option>--> <!--<option value="gfixed">Fixed (guarded)</option>-->
<option value="float64">Float (64-bit)</option> <option value="float64">Float (64-bit)</option>
</select> </select>
@ -184,7 +185,7 @@
</div> </div>
<div class="col-6"> <div class="col-6">
<label> <label>
<input type="checkbox" id="chkRoundQuota" checked> <input type="checkbox" id="chkRoundQuota">
Quota: Quota:
</label> </label>
<label> <label>
@ -205,7 +206,7 @@
<div class="col-6"> <div class="col-6">
<label> <label>
<input type="checkbox" id="chkRoundTVs"> <input type="checkbox" id="chkRoundTVs">
Transfer values: Surplus fractions:
</label> </label>
<label> <label>
<input type="number" id="txtRoundTVs" value="0" min="0" style="width: 3em;"> <input type="number" id="txtRoundTVs" value="0" min="0" style="width: 3em;">
@ -222,6 +223,14 @@
d.p. d.p.
</label> </label>
</div> </div>
<label class="col-12">
Sum surplus transfers:
<select id="selSumTransfers">
<option value="single_step" selected>Single step</option>
<option value="by_value">By value</option>
<option value="per_ballot">Per ballot</option>
</select>
</label>
</div> </div>
</div> </div>

View File

@ -95,6 +95,7 @@ async function clickCount() {
document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null, document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
document.getElementById('chkRoundVotes').checked ? parseInt(document.getElementById('txtRoundVotes').value) : null, document.getElementById('chkRoundVotes').checked ? parseInt(document.getElementById('txtRoundVotes').value) : null,
document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null, document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
document.getElementById('selSumTransfers').value,
document.getElementById('selQuota').value, document.getElementById('selQuota').value,
document.getElementById('selQuotaCriterion').value, document.getElementById('selQuotaCriterion').value,
document.getElementById('selQuotaMode').value, document.getElementById('selQuotaMode').value,
@ -291,7 +292,26 @@ async function printResult() {
// Presets // Presets
function changePreset() { function changePreset() {
if (document.getElementById('selPreset').value === 'scottish') { if (document.getElementById('selPreset').value === 'wigm') {
document.getElementById('selQuotaCriterion').value = 'gt';
document.getElementById('selQuota').value = 'droop_exact';
document.getElementById('selQuotaMode').value = 'static';
document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'rational';
document.getElementById('txtPPDP').value = '2';
document.getElementById('chkRoundQuota').checked = false;
document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundTVs').checked = false;
document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSumTransfers').value = 'single_step';
document.getElementById('selSurplus').value = 'by_size';
document.getElementById('selTransfers').value = 'wig';
document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'single_stage';
//document.getElementById('selTies').value = 'backwards_random';
} else if (document.getElementById('selPreset').value === 'scottish') {
document.getElementById('selQuotaCriterion').value = 'geq'; document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop'; document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static'; document.getElementById('selQuotaMode').value = 'static';
@ -300,12 +320,14 @@ function changePreset() {
document.getElementById('chkDeferSurpluses').checked = false; document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5'; document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '2'; document.getElementById('txtPPDP').value = '5';
document.getElementById('chkRoundQuota').checked = true; document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0'; document.getElementById('txtRoundQuota').value = '0';
document.getElementById('chkRoundVotes').checked = false; document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundTVs').checked = false; document.getElementById('chkRoundTVs').checked = true;
document.getElementById('txtRoundTVs').value = '5';
document.getElementById('chkRoundWeights').checked = false; document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSumTransfers').value = 'per_ballot';
document.getElementById('selSurplus').value = 'by_size'; document.getElementById('selSurplus').value = 'by_size';
document.getElementById('selTransfers').value = 'wig'; document.getElementById('selTransfers').value = 'wig';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -327,6 +349,7 @@ function changePreset() {
document.getElementById('txtRoundVotes').value = '0'; document.getElementById('txtRoundVotes').value = '0';
document.getElementById('chkRoundTVs').checked = false; document.getElementById('chkRoundTVs').checked = false;
document.getElementById('chkRoundWeights').checked = false; document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSumTransfers').value = 'single_step';
document.getElementById('selSurplus').value = 'by_order'; document.getElementById('selSurplus').value = 'by_order';
document.getElementById('selTransfers').value = 'uig'; document.getElementById('selTransfers').value = 'uig';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
@ -350,6 +373,7 @@ function changePreset() {
document.getElementById('txtRoundTVs').value = '3'; document.getElementById('txtRoundTVs').value = '3';
document.getElementById('chkRoundWeights').checked = true; document.getElementById('chkRoundWeights').checked = true;
document.getElementById('txtRoundWeights').value = '3'; document.getElementById('txtRoundWeights').value = '3';
document.getElementById('selSumTransfers').value = 'single_step';
document.getElementById('selSurplus').value = 'by_order'; document.getElementById('selSurplus').value = 'by_order';
document.getElementById('selTransfers').value = 'eg'; document.getElementById('selTransfers').value = 'eg';
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'transferable';
@ -373,6 +397,7 @@ function changePreset() {
document.getElementById('txtRoundTVs').value = '2'; document.getElementById('txtRoundTVs').value = '2';
document.getElementById('chkRoundWeights').checked = true; document.getElementById('chkRoundWeights').checked = true;
document.getElementById('txtRoundWeights').value = '2'; document.getElementById('txtRoundWeights').value = '2';
document.getElementById('selSumTransfers').value = 'single_step';
document.getElementById('selSurplus').value = 'by_size'; document.getElementById('selSurplus').value = 'by_size';
document.getElementById('selTransfers').value = 'eg'; document.getElementById('selTransfers').value = 'eg';
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'transferable';

View File

@ -78,6 +78,9 @@ struct STV {
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
round_quota: Option<usize>, round_quota: Option<usize>,
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "by_value", "per_ballot"], default_value="single_step", value_name="mode")]
sum_surplus_transfers: String,
// ----------- // -----------
// -- Quota -- // -- Quota --
@ -173,6 +176,7 @@ where
cmd_opts.round_weights, cmd_opts.round_weights,
cmd_opts.round_votes, cmd_opts.round_votes,
cmd_opts.round_quota, cmd_opts.round_quota,
&cmd_opts.sum_surplus_transfers,
&cmd_opts.quota, &cmd_opts.quota,
&cmd_opts.quota_criterion, &cmd_opts.quota_criterion,
&cmd_opts.quota_mode, &cmd_opts.quota_mode,

View File

@ -23,6 +23,7 @@ pub mod wasm;
use crate::numbers::Number; use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
use itertools::Itertools;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use std::cmp::max; use std::cmp::max;
@ -35,6 +36,7 @@ pub struct STVOptions {
pub round_weights: Option<usize>, pub round_weights: Option<usize>,
pub round_votes: Option<usize>, pub round_votes: Option<usize>,
pub round_quota: Option<usize>, pub round_quota: Option<usize>,
pub sum_surplus_transfers: SumSurplusTransfersMode,
pub quota: QuotaType, pub quota: QuotaType,
pub quota_criterion: QuotaCriterion, pub quota_criterion: QuotaCriterion,
pub quota_mode: QuotaMode, pub quota_mode: QuotaMode,
@ -54,6 +56,7 @@ impl STVOptions {
round_weights: Option<usize>, round_weights: Option<usize>,
round_votes: Option<usize>, round_votes: Option<usize>,
round_quota: Option<usize>, round_quota: Option<usize>,
sum_transfers: &str,
quota: &str, quota: &str,
quota_criterion: &str, quota_criterion: &str,
quota_mode: &str, quota_mode: &str,
@ -70,6 +73,12 @@ impl STVOptions {
round_weights, round_weights,
round_votes, round_votes,
round_quota, round_quota,
sum_surplus_transfers: match sum_transfers {
"single_step" => SumSurplusTransfersMode::SingleStep,
"by_value" => SumSurplusTransfersMode::ByValue,
"per_ballot" => SumSurplusTransfersMode::PerBallot,
_ => panic!("Invalid --sum-transfers"),
},
quota: match quota { quota: match quota {
"droop" => QuotaType::Droop, "droop" => QuotaType::Droop,
"hare" => QuotaType::Hare, "hare" => QuotaType::Hare,
@ -134,6 +143,15 @@ impl STVOptions {
} }
} }
#[wasm_bindgen]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum SumSurplusTransfersMode {
SingleStep,
ByValue,
PerBallot,
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[derive(PartialEq)] #[derive(PartialEq)]
@ -581,6 +599,7 @@ where
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool
where where
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::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N> for<'r> &'r N: ops::Neg<Output=N>
{ {
let quota = state.quota.as_ref().unwrap(); let quota = state.quota.as_ref().unwrap();
@ -620,7 +639,7 @@ where
} }
/// Return the denominator of the transfer value /// Return the denominator of the transfer value
fn calculate_transfer_denom<N: Number>(surplus: &N, result: &NextPreferencesResult<N>, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option<N> fn calculate_surplus_denom<N: Number>(surplus: &N, result: &NextPreferencesResult<N>, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option<N>
where where
for<'r> &'r N: ops::Sub<&'r N, Output=N> for<'r> &'r N: ops::Sub<&'r N, Output=N>
{ {
@ -648,21 +667,21 @@ fn reweight_vote<N: Number>(
num_ballots: &N, num_ballots: &N,
surplus: &N, surplus: &N,
weighted: bool, weighted: bool,
transfer_value: &Option<N>, surplus_fraction: &Option<N>,
transfer_denom: &Option<N>, surplus_denom: &Option<N>,
round_tvs: Option<usize>, round_tvs: Option<usize>,
rounding: Option<usize>) -> N rounding: Option<usize>) -> N
{ {
let mut result; let mut result;
match transfer_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() * transfer_value.as_ref().unwrap(); result = num_votes.clone() * surplus_fraction.as_ref().unwrap();
} else { } else {
result = num_ballots.clone() * transfer_value.as_ref().unwrap(); result = num_ballots.clone() * surplus_fraction.as_ref().unwrap();
} }
} else { } else {
// Avoid unnecessary rounding error by first multiplying by the surplus // Avoid unnecessary rounding error by first multiplying by the surplus
@ -686,11 +705,57 @@ fn reweight_vote<N: Number>(
return result; return result;
} }
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
where
for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
match opts.sum_surplus_transfers {
SumSurplusTransfersMode::SingleStep => {
// Calculate transfer across all votes
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
return reweight_vote(&entry.num_votes, &entry.num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
}
SumSurplusTransfersMode::ByValue => {
// Sum transfers by value
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_tvs, opts.round_votes);
}
return result;
}
SumSurplusTransfersMode::PerBallot => {
// Sum transfer per each individual ballot
// TODO: This could be moved to distribute_surplus to avoid looping over the votes and calculating transfer values twice
let mut result = N::new();
for vote in entry.votes.iter() {
result += reweight_vote(&vote.value, &vote.ballot.orig_value, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
}
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
return result;
}
}
}
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::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N> for<'r> &'r N: ops::Neg<Output=N>
{ {
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
let count_card = state.candidates.get(elected_candidate).unwrap(); let count_card = state.candidates.get(elected_candidate).unwrap();
let surplus = &count_card.votes - state.quota.as_ref().unwrap(); let surplus = &count_card.votes - state.quota.as_ref().unwrap();
@ -725,47 +790,54 @@ where
}; };
let transferable_votes = &result.total_votes - &result.exhausted.num_votes; let transferable_votes = &result.total_votes - &result.exhausted.num_votes;
let transfer_denom = calculate_transfer_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only); let surplus_denom = calculate_surplus_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only);
let mut transfer_value; let mut surplus_fraction;
match transfer_denom { match surplus_denom {
Some(ref v) => { Some(ref v) => {
transfer_value = Some(surplus.clone() / v); surplus_fraction = Some(surplus.clone() / v);
// Round down if requested // Round down if requested
if let Some(dps) = opts.round_tvs { if let Some(dps) = opts.round_tvs {
transfer_value.as_mut().unwrap().floor_mut(dps); surplus_fraction.as_mut().unwrap().floor_mut(dps);
} }
state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps2$}.", elected_candidate.name, transfer_value.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))); if opts.transferable_only {
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
} else {
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", result.total_ballots, result.total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
}
} }
None => { None => {
transfer_value = None; surplus_fraction = None;
state.logger.log_literal(format!("Surplus of {} distributed at values received.", elected_candidate.name));
if opts.transferable_only {
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, dps=opts.pp_decimals));
} else {
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, at values received.", result.total_ballots, result.total_votes, dps=opts.pp_decimals));
}
} }
} }
let mut checksum = N::new(); let mut checksum = N::new();
for (candidate, entry) in result.candidates.into_iter() { 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 = entry.votes as Parcel<N>; let mut parcel = entry.votes as Parcel<N>;
// Reweight votes // Reweight votes
for vote in parcel.iter_mut() { for vote in parcel.iter_mut() {
vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &transfer_value, &transfer_denom, opts.round_tvs, opts.round_weights); vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_tvs, opts.round_weights);
} }
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.push(parcel); count_card.parcels.push(parcel);
let candidate_transfers = reweight_vote(&entry.num_votes, &entry.num_ballots, &surplus, is_weighted, &transfer_value, &transfer_denom, opts.round_tvs, opts.round_votes);
count_card.transfer(&candidate_transfers);
checksum += candidate_transfers;
} }
// Transfer exhausted votes // Credit exhausted votes
let parcel = result.exhausted.votes as Parcel<N>;
state.exhausted.parcels.push(parcel);
let mut exhausted_transfers; let mut exhausted_transfers;
if opts.transferable_only { if opts.transferable_only {
if transferable_votes > surplus { if transferable_votes > surplus {
@ -773,17 +845,22 @@ where
exhausted_transfers = N::new(); exhausted_transfers = N::new();
} else { } else {
exhausted_transfers = &surplus - &transferable_votes; exhausted_transfers = &surplus - &transferable_votes;
if let Some(dps) = opts.round_votes {
exhausted_transfers.floor_mut(dps);
}
} }
} else { } else {
exhausted_transfers = reweight_vote(&result.exhausted.num_votes, &result.exhausted.num_ballots, &surplus, is_weighted, &transfer_value, &transfer_denom, opts.round_tvs, opts.round_votes); 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); state.exhausted.transfer(&exhausted_transfers);
checksum += exhausted_transfers; checksum += exhausted_transfers;
// Transfer exhausted votes
let parcel = result.exhausted.votes as Parcel<N>;
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;

View File

@ -59,6 +59,7 @@ fn aec_tas19_rational() {
round_weights: None, round_weights: None,
round_votes: Some(0), round_votes: Some(0),
round_quota: Some(0), round_quota: Some(0),
sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep,
quota: stv::QuotaType::Droop, quota: stv::QuotaType::Droop,
quota_criterion: stv::QuotaCriterion::GreaterOrEqual, quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
quota_mode: stv::QuotaMode::Static, quota_mode: stv::QuotaMode::Static,

View File

@ -33,6 +33,7 @@ fn ers97_rational() {
round_weights: Some(2), round_weights: Some(2),
round_votes: Some(2), round_votes: Some(2),
round_quota: Some(2), round_quota: Some(2),
sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep,
quota: stv::QuotaType::DroopExact, quota: stv::QuotaType::DroopExact,
quota_criterion: stv::QuotaCriterion::GreaterOrEqual, quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
quota_mode: stv::QuotaMode::ERS97, quota_mode: stv::QuotaMode::ERS97,

View File

@ -27,6 +27,7 @@ fn prsa1_rational() {
round_weights: Some(3), round_weights: Some(3),
round_votes: Some(3), round_votes: Some(3),
round_quota: Some(3), round_quota: Some(3),
sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep,
quota: stv::QuotaType::Droop, quota: stv::QuotaType::Droop,
quota_criterion: stv::QuotaCriterion::GreaterOrEqual, quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
quota_mode: stv::QuotaMode::Static, quota_mode: stv::QuotaMode::Static,

View File

@ -34,6 +34,7 @@ fn scotland_linn07_fixed5() {
round_weights: None, round_weights: None,
round_votes: None, round_votes: None,
round_quota: Some(0), round_quota: Some(0),
sum_surplus_transfers: stv::SumSurplusTransfersMode::PerBallot,
quota: stv::QuotaType::Droop, quota: stv::QuotaType::Droop,
quota_criterion: stv::QuotaCriterion::GreaterOrEqual, quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
quota_mode: stv::QuotaMode::Static, quota_mode: stv::QuotaMode::Static,