Refactor implementation of --sum-surplus-transfers -> --round-subtransfers in preparation for NSW Local Gov't STV
This commit is contained in:
parent
d94549dc42
commit
c5d6b8d460
@ -272,12 +272,13 @@ When *Surplus method* is set to *Meek method*:
|
||||
* --round-votes controls the rounding of the final number of votes credited to each candidate
|
||||
* Keep values, intermediate products and candidate votes are rounded *up*
|
||||
|
||||
### (Gregory) Sum surplus transfers (--sum-surplus-transfers)
|
||||
### (Gregory) Round subtransfers (--round-subtransfers)
|
||||
|
||||
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/exclusion is calculated. In each case, votes are grouped according to the next available preference for a continuing candidate. Subsequently:
|
||||
|
||||
* *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product is credited to that candidate.
|
||||
* *Per ballot*: For each individual vote expressing a next available preference for that candidate, the value of the vote is multiplied by the surplus fraction. The product is credited to that candidate.
|
||||
* *Single step* (default): The total value of all votes expressing a next available preference for that candidate is multiplied by the surplus fraction. The product (rounded if requested) 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 (rounded if requested) 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 (rounded if requested) 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.
|
||||
|
||||
|
@ -295,10 +295,10 @@
|
||||
</div>
|
||||
<label class="col-12">
|
||||
<span class="pill-grey" title="This option has effect only if “Method” is a Gregory method">Gregory</span>
|
||||
Sum surplus transfers:
|
||||
Round subtransfers:
|
||||
<select id="selSumTransfers">
|
||||
<!--<option value="single_step" selected>Single step</option>-->
|
||||
<option value="by_value" selected>By value</option>
|
||||
<option value="single_step" selected>Single step</option>
|
||||
<option value="by_value">By value</option>
|
||||
<option value="per_ballot">Per ballot</option>
|
||||
</select>
|
||||
</label>
|
||||
|
@ -32,7 +32,7 @@ function changePreset() {
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
@ -164,11 +164,11 @@ function changePreset() {
|
||||
document.getElementById('txtRoundVotes').value = '0';
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'uig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selExclusion').value = 'single_step';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'wa') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
@ -212,11 +212,11 @@ function changePreset() {
|
||||
document.getElementById('txtRoundVotes').value = '6';
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selExclusion').value = 'single_step';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'nswlg') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
@ -261,7 +261,7 @@ function changePreset() {
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '4';
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
@ -283,7 +283,7 @@ function changePreset() {
|
||||
document.getElementById('chkNormaliseBallots').checked = true;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selMethod').value = 'hare';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
@ -304,7 +304,7 @@ function changePreset() {
|
||||
document.getElementById('chkNormaliseBallots').checked = true;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'hare';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
@ -328,7 +328,7 @@ function changePreset() {
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
@ -355,7 +355,7 @@ function changePreset() {
|
||||
document.getElementById('txtRoundSFs').value = '3';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '3';
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
@ -382,11 +382,11 @@ function changePreset() {
|
||||
document.getElementById('txtRoundSFs').value = '2';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '2';
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selExclusion').value = 'single_step';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'ers76') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
@ -409,11 +409,11 @@ function changePreset() {
|
||||
document.getElementById('txtRoundSFs').value = '2';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '2';
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selExclusion').value = 'single_step';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'ers73') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
@ -436,11 +436,11 @@ function changePreset() {
|
||||
document.getElementById('txtRoundSFs').value = '2';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '2';
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selExclusion').value = 'single_step';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'cofe') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
@ -467,7 +467,7 @@ function changePreset() {
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selExclusion').value = 'single_step';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@ -79,9 +79,9 @@ pub struct SubcmdOptions {
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_quota: Option<usize>,
|
||||
|
||||
/// (Gregory STV) How to calculate votes to credit to candidates in surplus transfers
|
||||
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["by_value", "per_ballot"], default_value="by_value", value_name="mode")]
|
||||
sum_surplus_transfers: String,
|
||||
/// (Gregory STV) How to round subtransfers during surpluses/exclusions
|
||||
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "per_ballot"], default_value="single_step", value_name="mode")]
|
||||
round_subtransfers: String,
|
||||
|
||||
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
||||
#[clap(help_heading=Some("ROUNDING"), long, default_value="0.001%", value_name="tolerance")]
|
||||
@ -284,7 +284,7 @@ where
|
||||
cmd_opts.round_values,
|
||||
cmd_opts.round_votes,
|
||||
cmd_opts.round_quota,
|
||||
cmd_opts.sum_surplus_transfers.into(),
|
||||
cmd_opts.round_subtransfers.into(),
|
||||
cmd_opts.meek_surplus_tolerance,
|
||||
cmd_opts.normalise_ballots,
|
||||
cmd_opts.quota.into(),
|
||||
|
@ -22,7 +22,7 @@ use super::prettytable_html::{Cell, Row, Table};
|
||||
|
||||
use crate::election::{Candidate, CountState};
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::{STVOptions, SumSurplusTransfersMode};
|
||||
use crate::stv::{STVOptions, RoundSubtransfersMode};
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::HashMap;
|
||||
@ -128,36 +128,8 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
let is_weighted = self.surplus.is_none() || opts.surplus.is_weighted();
|
||||
|
||||
// Iterate through columns
|
||||
// Sum votes_in, etc.
|
||||
for column in self.columns.iter_mut() {
|
||||
let mut new_value_fraction;
|
||||
if self.surplus.is_some() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
||||
if is_weighted {
|
||||
new_value_fraction = column.value_fraction.clone();
|
||||
// If surplus, multiply by surplus fraction
|
||||
if let Some(n) = &self.surpfrac_numer {
|
||||
new_value_fraction *= n;
|
||||
}
|
||||
} else {
|
||||
if let Some(n) = &self.surpfrac_numer {
|
||||
new_value_fraction = n.clone();
|
||||
} else {
|
||||
// Transferred at original value
|
||||
new_value_fraction = column.value_fraction.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(n) = &self.surpfrac_denom {
|
||||
new_value_fraction /= n;
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
} else {
|
||||
new_value_fraction = column.value_fraction.clone();
|
||||
}
|
||||
|
||||
// Candidate votes
|
||||
for (candidate, cell) in column.cells.iter_mut() {
|
||||
column.total.ballots += &cell.ballots;
|
||||
@ -169,19 +141,6 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
column.total.votes_in += &votes_in;
|
||||
self.total.cells.get_mut(*candidate).unwrap().votes_in += &votes_in;
|
||||
self.total.total.votes_in += votes_in;
|
||||
|
||||
if self.surplus.is_some() || opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
||||
let mut votes_out = cell.ballots.clone() * &new_value_fraction;
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
votes_out.floor_mut(dps);
|
||||
}
|
||||
|
||||
cell.votes_out += &votes_out;
|
||||
column.total.votes_out += &votes_out;
|
||||
self.total.cells.get_mut(*candidate).unwrap().votes_out += &votes_out;
|
||||
self.total.total.votes_out += votes_out;
|
||||
}
|
||||
}
|
||||
|
||||
// Exhausted votes
|
||||
@ -194,73 +153,167 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
column.total.votes_in += &votes_in;
|
||||
self.total.exhausted.votes_in += &votes_in;
|
||||
self.total.total.votes_in += votes_in;
|
||||
|
||||
if self.surplus.is_some() || opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
||||
if !opts.transferable_only {
|
||||
let mut votes_out = column.exhausted.ballots.clone() * &new_value_fraction;
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
votes_out.floor_mut(dps);
|
||||
}
|
||||
|
||||
match opts.round_subtransfers {
|
||||
RoundSubtransfersMode::SingleStep => {
|
||||
// No need to calculate votes_out for each column
|
||||
|
||||
// Calculate total votes_out per candidate
|
||||
for (_candidate, cell) in self.total.cells.iter_mut() {
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply votes in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
cell.votes_out = cell.votes_in.clone();
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
column.exhausted.votes_out += &votes_out;
|
||||
column.total.votes_out += &votes_out;
|
||||
self.total.exhausted.votes_out += &votes_out;
|
||||
self.total.total.votes_out += votes_out;
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
cell.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need to calculate total candidate votes_out?
|
||||
if opts.sum_surplus_transfers == SumSurplusTransfersMode::ByValue {
|
||||
for (_candidate, cell) in self.total.cells.iter_mut() {
|
||||
let mut votes_out;
|
||||
|
||||
if is_weighted || self.surpfrac.is_none() {
|
||||
// NB: If surplus.is_none, then votes transferred at values received
|
||||
votes_out = cell.votes_in.clone();
|
||||
// Calculate total exhausted votes
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply votes in by surplus fraction
|
||||
self.total.exhausted.votes_out = multiply_surpfrac(self.total.exhausted.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
// This can only happen with --transferable-only, so this will be calculated in apply_to
|
||||
} else {
|
||||
votes_out = cell.ballots.clone();
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
self.total.exhausted.votes_out = multiply_surpfrac(self.total.exhausted.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// If surplus, multiply by surplus fraction
|
||||
if let Some(n) = &self.surpfrac_numer {
|
||||
votes_out *= n;
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
self.total.exhausted.votes_out.floor_mut(dps);
|
||||
}
|
||||
if let Some(n) = &self.surpfrac_denom {
|
||||
votes_out /= n;
|
||||
}
|
||||
|
||||
cell.votes_out = votes_out; // Rounded later
|
||||
}
|
||||
|
||||
if self.surplus.is_none() || !opts.transferable_only {
|
||||
let mut votes_out;
|
||||
if is_weighted || self.surpfrac.is_none() {
|
||||
votes_out = self.total.exhausted.votes_in.clone();
|
||||
} else {
|
||||
votes_out = self.total.exhausted.ballots.clone();
|
||||
RoundSubtransfersMode::ByValue => {
|
||||
// Calculate votes_out for each column
|
||||
for column in self.columns.iter_mut() {
|
||||
// Calculate votes_out per candidate in the column
|
||||
for (_candidate, cell) in column.cells.iter_mut() {
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply votes in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
cell.votes_out = cell.votes_in.clone();
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
cell.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate exhausted votes in the column
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply votes in by surplus fraction
|
||||
column.exhausted.votes_out = multiply_surpfrac(column.exhausted.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
// This can only happen with --transferable-only, so this will be calculated in apply_to
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
column.exhausted.votes_out = multiply_surpfrac(column.exhausted.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
column.exhausted.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
// If surplus, multiply by surplus fraction
|
||||
if let Some(n) = &self.surpfrac_numer {
|
||||
votes_out *= n;
|
||||
}
|
||||
if let Some(n) = &self.surpfrac_denom {
|
||||
votes_out /= n;
|
||||
// Sum total votes_out per candidate
|
||||
for (candidate, cell) in self.total.cells.iter_mut() {
|
||||
cell.votes_out = self.columns.iter().fold(N::new(), |acc, col| acc + &col.cells[candidate].votes_out);
|
||||
}
|
||||
|
||||
self.total.exhausted.votes_out = votes_out; // Rounded later
|
||||
// Sum total exhausted votes
|
||||
self.total.exhausted.votes_out = self.columns.iter().fold(N::new(), |acc, col| acc + &col.exhausted.votes_out);
|
||||
}
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
for (_candidate, cell) in self.total.cells.iter_mut() {
|
||||
cell.votes_out.floor_mut(dps);
|
||||
RoundSubtransfersMode::PerBallot => {
|
||||
// Calculate votes_out for each column
|
||||
for column in self.columns.iter_mut() {
|
||||
// Calculate votes_out per candidate in the column
|
||||
for (_candidate, cell) in column.cells.iter_mut() {
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply ballots in by new value fraction
|
||||
let mut new_value_fraction = multiply_surpfrac(column.value_fraction.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
|
||||
cell.votes_out = cell.ballots.clone() * new_value_fraction;
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
cell.votes_out = cell.votes_in.clone();
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
cell.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate exhausted votes in the column
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply ballots in by new value fraction
|
||||
let mut new_value_fraction = multiply_surpfrac(column.value_fraction.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
|
||||
column.exhausted.votes_out = column.exhausted.ballots.clone() * new_value_fraction;
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
// This can only happen with --transferable-only, so this will be calculated in apply_to
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
column.exhausted.votes_out = multiply_surpfrac(column.exhausted.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
column.exhausted.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
// Sum total votes_out per candidate
|
||||
for (candidate, cell) in self.total.cells.iter_mut() {
|
||||
cell.votes_out = self.columns.iter().fold(N::new(), |acc, col| acc + &col.cells[candidate].votes_out);
|
||||
}
|
||||
|
||||
// Sum total exhausted votes
|
||||
self.total.exhausted.votes_out = self.columns.iter().fold(N::new(), |acc, col| acc + &col.exhausted.votes_out);
|
||||
}
|
||||
|
||||
self.total.exhausted.votes_out.floor_mut(dps);
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,10 +363,10 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
let mut table = Table::new();
|
||||
set_table_format(&mut table);
|
||||
|
||||
let show_transfers_per_ballot = self.surpfrac.is_some() || opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot;
|
||||
let show_transfers_per_column = opts.round_subtransfers != RoundSubtransfersMode::SingleStep;
|
||||
|
||||
let num_cols;
|
||||
if show_transfers_per_ballot {
|
||||
if show_transfers_per_column {
|
||||
num_cols = self.columns.len() * 3 + 4;
|
||||
} else {
|
||||
if self.surpfrac.is_none() {
|
||||
@ -331,7 +384,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
for column in self.columns.iter() {
|
||||
row.push(Cell::new(&format!("Ballots @ {:.dps2$}", column.value_fraction, dps2=max(opts.pp_decimals, 2))).style_spec("cH2"));
|
||||
|
||||
if show_transfers_per_ballot {
|
||||
if show_transfers_per_column {
|
||||
if self.surplus.is_some() {
|
||||
row.push(Cell::new(&format!("× {:.dps2$}", self.surpfrac.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))).style_spec("r"));
|
||||
} else {
|
||||
@ -342,7 +395,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
row.push(Cell::new("Total").style_spec("cH2"));
|
||||
if self.surpfrac.is_some() {
|
||||
row.push(Cell::new(&format!("× {:.dps2$}", self.surpfrac.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))).style_spec("r"));
|
||||
} else if show_transfers_per_ballot {
|
||||
} else if show_transfers_per_column {
|
||||
row.push(Cell::new("=").style_spec("c"));
|
||||
}
|
||||
table.set_titles(Row::new(row));
|
||||
@ -357,13 +410,13 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
if let Some(cell) = column.cells.get(candidate) {
|
||||
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if show_transfers_per_ballot {
|
||||
if show_transfers_per_column {
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if show_transfers_per_ballot {
|
||||
if show_transfers_per_column {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
@ -373,13 +426,13 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
if let Some(cell) = self.total.cells.get(candidate) {
|
||||
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
@ -396,7 +449,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
if !column.exhausted.ballots.is_zero() {
|
||||
row.push(Cell::new(&format!("{:.0}", column.exhausted.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if show_transfers_per_ballot {
|
||||
if show_transfers_per_column {
|
||||
if column.exhausted.votes_out.is_zero() {
|
||||
row.push(Cell::new("-").style_spec("c"));
|
||||
} else {
|
||||
@ -406,7 +459,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if show_transfers_per_ballot {
|
||||
if show_transfers_per_column {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
@ -416,7 +469,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
if !self.total.exhausted.ballots.is_zero() {
|
||||
row.push(Cell::new(&format!("{:.0}", self.total.exhausted.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", self.total.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
if self.total.exhausted.votes_out.is_zero() {
|
||||
row.push(Cell::new("-").style_spec("c"));
|
||||
} else {
|
||||
@ -426,7 +479,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
@ -442,7 +495,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
for column in self.columns.iter() {
|
||||
row.push(Cell::new(&format!("{:.0}", column.total.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if show_transfers_per_ballot {
|
||||
if show_transfers_per_column {
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
}
|
||||
@ -466,7 +519,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
|
||||
row.push(Cell::new(&format!("{:.0}", gt_ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", gt_votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
row.push(Cell::new(&format!("{:.dps$}", gt_votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
|
||||
@ -493,6 +546,17 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
||||
//}
|
||||
}
|
||||
|
||||
/// Multiply the specified number by the surplus fraction (if applicable)
|
||||
fn multiply_surpfrac<N: Number>(mut number: N, surpfrac_numer: &Option<N>, surpfrac_denom: &Option<N>) -> N {
|
||||
if let Some(n) = surpfrac_numer {
|
||||
number *= n;
|
||||
}
|
||||
if let Some(n) = surpfrac_denom {
|
||||
number /= n;
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
/// Column in a [TransferTable]
|
||||
pub struct TransferTableColumn<'e, N: Number> {
|
||||
/// Value fraction of ballots counted in this column
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@ -64,9 +64,9 @@ pub struct STVOptions {
|
||||
#[builder(default="None")]
|
||||
pub round_quota: Option<usize>,
|
||||
|
||||
/// How to calculate votes to credit to candidates in surplus transfers
|
||||
#[builder(default="SumSurplusTransfersMode::ByValue")]
|
||||
pub sum_surplus_transfers: SumSurplusTransfersMode,
|
||||
/// How to round votes in transfer table
|
||||
#[builder(default="RoundSubtransfersMode::SingleStep")]
|
||||
pub round_subtransfers: RoundSubtransfersMode,
|
||||
|
||||
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
||||
#[builder(default=r#"String::from("0.001%")"#)]
|
||||
@ -176,7 +176,7 @@ impl STVOptions {
|
||||
if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); }
|
||||
}
|
||||
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); }
|
||||
if self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::ByValue { flags.push(self.sum_surplus_transfers.describe()); }
|
||||
if self.surplus != SurplusMethod::Meek && self.round_subtransfers != RoundSubtransfersMode::SingleStep { flags.push(self.round_subtransfers.describe()); }
|
||||
if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); }
|
||||
if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); }
|
||||
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
|
||||
@ -229,32 +229,40 @@ impl STVOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::sum_surplus_transfers]
|
||||
/// Enum of options for [STVOptions::round_subtransfers]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum SumSurplusTransfersMode {
|
||||
/// Sum and round a candidate's surplus transfers separately for ballot papers received at each particular value
|
||||
pub enum RoundSubtransfersMode {
|
||||
/// Do not round subtransfers (only round final number of votes credited)
|
||||
SingleStep,
|
||||
/// Round in subtransfers according to the value when received
|
||||
ByValue,
|
||||
/// Sum and round a candidate's surplus transfers individually for each ballot paper
|
||||
/// Round in subtransfers according to the candidate from who each vote was received, and the value when received
|
||||
ByValueAndSource,
|
||||
/// Sum and round transfers individually for each ballot paper
|
||||
PerBallot,
|
||||
}
|
||||
|
||||
impl SumSurplusTransfersMode {
|
||||
impl RoundSubtransfersMode {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
SumSurplusTransfersMode::ByValue => "--sum-surplus-transfers by_value",
|
||||
SumSurplusTransfersMode::PerBallot => "--sum-surplus-transfers per_ballot",
|
||||
RoundSubtransfersMode::SingleStep => "--sum-surplus-transfers single_step",
|
||||
RoundSubtransfersMode::ByValue => "--sum-surplus-transfers by_value",
|
||||
RoundSubtransfersMode::ByValueAndSource => "--sum-surplus-transfers by_value_and_source",
|
||||
RoundSubtransfersMode::PerBallot => "--sum-surplus-transfers per_ballot",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for SumSurplusTransfersMode {
|
||||
impl<S: AsRef<str>> From<S> for RoundSubtransfersMode {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"by_value" => SumSurplusTransfersMode::ByValue,
|
||||
"per_ballot" => SumSurplusTransfersMode::PerBallot,
|
||||
"single_step" => RoundSubtransfersMode::SingleStep,
|
||||
"by_value" => RoundSubtransfersMode::ByValue,
|
||||
"by_value_and_source" => RoundSubtransfersMode::ByValueAndSource,
|
||||
"per_ballot" => RoundSubtransfersMode::PerBallot,
|
||||
_ => panic!("Invalid --sum-transfers"),
|
||||
}
|
||||
}
|
||||
|
@ -239,7 +239,7 @@ impl STVOptions {
|
||||
round_values: Option<usize>,
|
||||
round_votes: Option<usize>,
|
||||
round_quota: Option<usize>,
|
||||
sum_surplus_transfers: &str,
|
||||
round_subtransfers: &str,
|
||||
meek_surplus_tolerance: String,
|
||||
normalise_ballots: bool,
|
||||
quota: &str,
|
||||
@ -268,7 +268,7 @@ impl STVOptions {
|
||||
round_values,
|
||||
round_votes,
|
||||
round_quota,
|
||||
sum_surplus_transfers.into(),
|
||||
round_subtransfers.into(),
|
||||
meek_surplus_tolerance,
|
||||
normalise_ballots,
|
||||
quota.into(),
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@ -27,7 +27,7 @@ fn ers97_coe_rational() {
|
||||
.round_values(Some(2))
|
||||
.round_votes(Some(2))
|
||||
.round_quota(Some(2))
|
||||
.sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot)
|
||||
.round_subtransfers(stv::RoundSubtransfersMode::PerBallot)
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.surplus(stv::SurplusMethod::EG)
|
||||
.transferable_only(true)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@ -32,7 +32,7 @@ fn scotland_linn07_fixed5() {
|
||||
//.round_values(Some(5))
|
||||
//.round_votes(Some(5))
|
||||
.round_quota(Some(0))
|
||||
.sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot)
|
||||
.round_subtransfers(stv::RoundSubtransfersMode::PerBallot)
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.early_bulk_elect(false)
|
||||
.pp_decimals(5)
|
||||
@ -52,7 +52,7 @@ fn scotland_linn07_gfixed5() {
|
||||
.round_values(Some(5)) // Must specify rounding as guarded decimals represented to 10 dps internally
|
||||
.round_votes(Some(5))
|
||||
.round_quota(Some(0))
|
||||
.sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot)
|
||||
.round_subtransfers(stv::RoundSubtransfersMode::PerBallot)
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.early_bulk_elect(false)
|
||||
.pp_decimals(5)
|
||||
|
Loading…
Reference in New Issue
Block a user