Implement Cambridge STV - Cincinnati/Hare methods of surpluses
This commit is contained in:
parent
6da51837a5
commit
f182ca02bd
@ -79,15 +79,22 @@ Some STV counting rules provide, for example, that ‘no surplus shall be transf
|
||||
|
||||
This dropdown allows you to select how ballots are transferred during surplus transfers. The recommended methods are:
|
||||
|
||||
* *Weighted inclusive Gregory* (default): During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are weighted according to the weights of the ballot papers.
|
||||
* *Weighted inclusive Gregory* (default): During surplus transfers, all applicable ballot papers of the elected candidate are examined. Transfers are weighted according to the values of the ballot papers.
|
||||
* *Meek method*: Transfers are computed as described at <http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf>.
|
||||
|
||||
Other methods are supported, but not recommended:
|
||||
Other Gregory methods are supported, but not recommended:
|
||||
|
||||
* *Unweighted inclusive Gregory*: During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are not weighted, and each ballot paper has equal value in the calculation.
|
||||
* *Exclusive Gregory (last bundle)*: During surplus transfers, only the ballot papers received in the last transfer are examined. Transfers are not weighted.
|
||||
* *Unweighted inclusive Gregory*: During surplus transfers, all applicable ballot papers of the elected candidate are examined. Transfers are not weighted, and each ballot paper has equal value in the calculation.
|
||||
* *Exclusive Gregory (last bundle)*: During surplus transfers, only the ballot papers received in the last transfer (all of one value) are examined.
|
||||
|
||||
Other surplus transfer methods, such as non-fractional transfers (e.g. random sample) are not supported at this time.
|
||||
Random subset methods are also supported, but also not recommended:
|
||||
|
||||
* *Cincinnati (inclusive subset)*: During surplus transfers, a subset of the elected candidate's ballot papers, equal in size to the surplus, is examined.
|
||||
* *Hare (exclusive subset)*: During surplus transfers, a subset of the ballot papers received in the last transfer, equal in size to the surplus, is examined.
|
||||
|
||||
The use of a random subset method requires *Normalise ballots* to be enabled, and requires the *Quota criterion* to be set to *>=*. When a random subset method is used, surplus transfers and exclusions are performed one ballot paper at a time, and candidates are declared elected immediately on reaching the quota. Consequential surpluses therefore do not arise, and surpluses only occur during the count of first preferences.
|
||||
|
||||
In both random subset methods, the subset is selected using the deterministic method used in [Cambridge, Massachusetts](https://web.archive.org/web/20081118104049/http://www.fairvote.org/media/1993countmanual.pdf) (derived from Article IX of the former 1938 Cincinnati *Code of Ordinances*). This depends on the order of ballot papers in the BLT file, and is independent of the *Random seed* option.
|
||||
|
||||
### Papers to examine in surplus transfer (--transferable-only)
|
||||
|
||||
@ -223,13 +230,13 @@ When *Surplus method* is set to *Meek method*:
|
||||
|
||||
### (Gregory) Sum surplus transfers (--sum-surplus-transfers)
|
||||
|
||||
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. 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.
|
||||
|
||||
This option affects the result only insofar as rounding (due to use of fixed-precision arithmetic, or due to 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.
|
||||
|
||||
### (Meek) Surplus tolerance (--meek-surplus-tolerance)
|
||||
|
||||
|
@ -45,6 +45,7 @@
|
||||
<option value="act">Australian Capital Territory STV</option>
|
||||
<option value="wa">Western Australia STV</option>
|
||||
<option value="meeknz">Meek STV (New Zealand)</option>
|
||||
<option value="cambridge">Cambridge STV</option>
|
||||
</optgroup>
|
||||
<optgroup label="Hand-count">
|
||||
<option value="prsa77">PRSA 1977</option>
|
||||
@ -110,6 +111,8 @@
|
||||
<option value="uig">Unweighted inclusive Gregory</option>
|
||||
<option value="eg">Exclusive Gregory (last bundle)</option>
|
||||
<option value="meek">Meek method</option>
|
||||
<option value="cincinnati">Cincinnati (inclusive subset)</option>
|
||||
<option value="hare">Hare (exclusive subset)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
|
@ -547,6 +547,23 @@ function changePreset() {
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'cambridge') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false; // TODO: Cambridge-style bulk exclusion
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '0';
|
||||
document.getElementById('chkNormaliseBallots').checked = true;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selTransfers').value = 'cincinnati';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'wright') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
|
@ -119,7 +119,7 @@ struct STV {
|
||||
random_seed: Option<String>,
|
||||
|
||||
/// Method of surplus distributions
|
||||
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
|
||||
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek", "cincinnati", "hare"], default_value="wig", value_name="method")]
|
||||
surplus: String,
|
||||
|
||||
/// (Gregory STV) Order to distribute surpluses
|
||||
|
@ -80,6 +80,19 @@ impl Number for Fixed {
|
||||
}
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
// Only do something if truncating
|
||||
if dps < get_dps() {
|
||||
// TODO: Streamline
|
||||
let mut factor = Self::from(10);
|
||||
factor.pow_assign(-(dps as i32));
|
||||
factor /= Self::from(2);
|
||||
|
||||
*self = self.clone() - factor;
|
||||
self.ceil_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Self {
|
||||
// Parse decimal
|
||||
if s.contains('.') {
|
||||
|
@ -87,6 +87,19 @@ impl Number for GuardedFixed {
|
||||
}
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
// Only do something if truncating
|
||||
if dps < get_dps() * 2 {
|
||||
// TODO: Streamline
|
||||
let mut factor = Self::from(10);
|
||||
factor.pow_assign(-(dps as i32));
|
||||
factor /= Self::from(2);
|
||||
|
||||
*self = self.clone() - factor;
|
||||
self.ceil_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Self {
|
||||
// Parse decimal
|
||||
if s.contains('.') {
|
||||
|
@ -47,7 +47,7 @@ pub trait Assign<Src=Self> {
|
||||
pub trait Number:
|
||||
NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign + From<usize> + Clone + fmt::Debug + fmt::Display
|
||||
where
|
||||
for<'a> Self: Assign<&'a Self>
|
||||
for<'a> Self: Assign<&'a Self>,
|
||||
{
|
||||
/// Return a new [Number]
|
||||
fn new() -> Self;
|
||||
@ -63,6 +63,8 @@ where
|
||||
fn floor_mut(&mut self, dps: usize);
|
||||
/// Round `self` up if necessary to `dps` decimal places
|
||||
fn ceil_mut(&mut self, dps: usize);
|
||||
/// Round `self` half-up to the nearest `dps` decimal places
|
||||
fn round_mut(&mut self, dps: usize);
|
||||
|
||||
/// Parse the given string into a [Number]
|
||||
fn parse(s: &str) -> Self {
|
||||
|
@ -47,6 +47,11 @@ impl Number for NativeFloat64 {
|
||||
let factor = 10.0_f64.powi(dps as i32);
|
||||
self.0 = (self.0 * factor).ceil() / factor;
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
let factor = 10.0_f64.powi(dps as i32);
|
||||
self.0 = (self.0 * factor).round() / factor;
|
||||
}
|
||||
}
|
||||
|
||||
impl Num for NativeFloat64 {
|
||||
|
@ -62,6 +62,20 @@ impl Number for Rational {
|
||||
}
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
if dps == 0 {
|
||||
self.0 = self.0.round();
|
||||
} else {
|
||||
// TODO: Streamline
|
||||
let mut factor = Self::from(10);
|
||||
factor.pow_assign(-(dps as i32));
|
||||
factor /= Self::from(2);
|
||||
|
||||
*self = self.clone() - factor;
|
||||
self.ceil_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Self {
|
||||
// Parse decimal
|
||||
if s.contains('.') {
|
||||
|
@ -61,6 +61,20 @@ impl Number for Rational {
|
||||
}
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
if dps == 0 {
|
||||
self.0.round_mut();
|
||||
} else {
|
||||
// TODO: Streamline
|
||||
let mut factor = Self::from(10);
|
||||
factor.pow_assign(-(dps as i32));
|
||||
factor /= Self::from(2);
|
||||
|
||||
*self = self.clone() - factor;
|
||||
self.ceil_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Self {
|
||||
// Parse decimal
|
||||
if s.contains('.') {
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
use super::{ExclusionMethod, NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
|
||||
use super::subset;
|
||||
|
||||
use crate::constraints;
|
||||
use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote};
|
||||
@ -61,7 +62,7 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
||||
state.logger.log_literal("First preferences distributed.".to_string());
|
||||
}
|
||||
|
||||
/// Distribute the largest surplus according to the Gregory method, based on [STVOptions::surplus]
|
||||
/// Distribute the largest surplus according to the Gregory or random subset method, based on [STVOptions::surplus]
|
||||
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
@ -103,7 +104,11 @@ where
|
||||
max_cands[0]
|
||||
};
|
||||
|
||||
distribute_surplus(state, &opts, elected_candidate);
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { distribute_surplus(state, &opts, elected_candidate); }
|
||||
SurplusMethod::Cincinnati | SurplusMethod::Hare => { subset::distribute_surplus(state, &opts, elected_candidate)?; }
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
@ -229,6 +234,8 @@ where
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
state.kind = Some("Surplus of");
|
||||
state.title = String::from(&elected_candidate.name);
|
||||
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
||||
|
||||
let count_card = &state.candidates[elected_candidate];
|
||||
@ -245,21 +252,18 @@ where
|
||||
// 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;
|
||||
}
|
||||
_ => { panic!("Invalid --surplus for Gregory method"); }
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
// Count next preferences
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
state.kind = Some("Surplus of");
|
||||
state.title = String::from(&elected_candidate.name);
|
||||
|
||||
// Transfer candidate votes
|
||||
// TODO: Refactor??
|
||||
let is_weighted = match opts.surplus {
|
||||
SurplusMethod::WIG => { true }
|
||||
SurplusMethod::UIG | SurplusMethod::EG => { false }
|
||||
SurplusMethod::Meek => { todo!() }
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
let transferable_votes = &result.total_votes - &result.exhausted.num_votes;
|
||||
@ -524,9 +528,9 @@ where
|
||||
}
|
||||
|
||||
if !votes.is_empty() {
|
||||
// Count next preferences
|
||||
let value = &votes[0].value / &votes[0].ballot.orig_value;
|
||||
|
||||
// Count next preferences
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||
|
@ -17,10 +17,12 @@
|
||||
|
||||
#![allow(mutable_borrow_reservation_conflict)]
|
||||
|
||||
/// Gregory method of surplus distributions
|
||||
/// Gregory methods of surplus distributions
|
||||
pub mod gregory;
|
||||
/// Meek method of surplus distributions, etc.
|
||||
pub mod meek;
|
||||
/// Random subset methods of surplus distributions
|
||||
pub mod subset;
|
||||
|
||||
/// WebAssembly wrappers
|
||||
//#[cfg(target_arch = "wasm32")]
|
||||
@ -166,6 +168,8 @@ impl STVOptions {
|
||||
"uig" => SurplusMethod::UIG,
|
||||
"eg" => SurplusMethod::EG,
|
||||
"meek" => SurplusMethod::Meek,
|
||||
"cincinnati" => SurplusMethod::Cincinnati,
|
||||
"hare" => SurplusMethod::Hare,
|
||||
_ => panic!("Invalid --surplus"),
|
||||
},
|
||||
surplus_order: match surplus_order {
|
||||
@ -206,9 +210,11 @@ impl STVOptions {
|
||||
pub fn describe<N: Number>(&self) -> String {
|
||||
let mut flags = Vec::new();
|
||||
let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) };
|
||||
if self.surplus != SurplusMethod::Cincinnati && self.surplus != SurplusMethod::Hare {
|
||||
if let Some(dps) = self.round_surplus_fractions { flags.push(format!("--round-tvs {}", dps)); }
|
||||
if let Some(dps) = self.round_values { flags.push(format!("--round-weights {}", 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 self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { 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)); }
|
||||
@ -244,6 +250,11 @@ impl STVOptions {
|
||||
if self.transferable_only { return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only")); }
|
||||
if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage")); }
|
||||
}
|
||||
if self.surplus == SurplusMethod::Cincinnati || self.surplus == SurplusMethod::Hare {
|
||||
if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus cincinnati and --surplus hare require --exclusion single_stage")); }
|
||||
if self.quota_criterion != QuotaCriterion::GreaterOrEqual { return Err(STVError::InvalidOptions("--surplus cincinnati and --surplus hare require --quota-criterion geq")); }
|
||||
if !self.normalise_ballots { return Err(STVError::InvalidOptions("--surplus cincinnati and --surplus hare require --normalise-ballots")); }
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@ -357,6 +368,10 @@ pub enum SurplusMethod {
|
||||
EG,
|
||||
/// Meek method
|
||||
Meek,
|
||||
/// Cincinnati method (inclusive random subset)
|
||||
Cincinnati,
|
||||
/// Hare method (exclusive random subset)
|
||||
Hare,
|
||||
}
|
||||
|
||||
impl SurplusMethod {
|
||||
@ -367,6 +382,8 @@ impl SurplusMethod {
|
||||
SurplusMethod::UIG => "--surplus uig",
|
||||
SurplusMethod::EG => "--surplus eg",
|
||||
SurplusMethod::Meek => "--surplus meek",
|
||||
SurplusMethod::Cincinnati => "--surplus cincinnati",
|
||||
SurplusMethod::Hare => "--surplus hare",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
@ -544,14 +561,11 @@ where
|
||||
}
|
||||
|
||||
// Exclude lowest hopeful
|
||||
if exclude_hopefuls(state, &opts)? {
|
||||
exclude_hopefuls(state, &opts)?; // Cannot fail
|
||||
calculate_quota(state, opts);
|
||||
elect_hopefuls(state, opts)?;
|
||||
update_tiebreaks(state, opts);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
panic!("Count incomplete but unable to proceed");
|
||||
}
|
||||
|
||||
/// See [next_preferences]
|
||||
@ -632,7 +646,7 @@ where
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
{
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG | SurplusMethod::Cincinnati | SurplusMethod::Hare => {
|
||||
gregory::distribute_first_preferences(state);
|
||||
}
|
||||
SurplusMethod::Meek => {
|
||||
@ -981,7 +995,7 @@ where
|
||||
for<'r> &'r N: ops::Neg<Output=N>,
|
||||
{
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG | SurplusMethod::Cincinnati | SurplusMethod::Hare => {
|
||||
return gregory::distribute_surpluses(state, opts);
|
||||
}
|
||||
SurplusMethod::Meek => {
|
||||
@ -1007,7 +1021,7 @@ fn can_bulk_elect<N: Number>(state: &CountState<N>, num_to_exclude: usize) -> bo
|
||||
}
|
||||
|
||||
/// Declare all continuing candidates to be elected
|
||||
fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, template1: &'static str, template2: &'static str) -> Result<bool, STVError> {
|
||||
fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, template1: &'static str, template2: &'static str) -> Result<(), STVError> {
|
||||
let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter()
|
||||
.filter(|c| {
|
||||
let cc = &state.candidates[c];
|
||||
@ -1048,7 +1062,7 @@ fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, templa
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies
|
||||
@ -1057,7 +1071,8 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result
|
||||
state.kind = None;
|
||||
state.title = "Bulk election".to_string();
|
||||
|
||||
return do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.");
|
||||
do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.")?;
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
@ -1108,11 +1123,13 @@ where
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
}
|
||||
|
||||
return do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.");
|
||||
do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.")?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
return exclude_candidates(state, opts, excluded_candidates);
|
||||
exclude_candidates(state, opts, excluded_candidates)?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
@ -1167,7 +1184,7 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST
|
||||
}
|
||||
|
||||
/// Exclude the lowest-ranked hopeful candidate(s)
|
||||
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<(), STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
@ -1216,11 +1233,13 @@ where
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
}
|
||||
|
||||
return do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.");
|
||||
do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
return exclude_candidates(state, opts, excluded_candidates);
|
||||
exclude_candidates(state, opts, excluded_candidates)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Continue the exclusion of a candidate who is being excluded
|
||||
@ -1253,14 +1272,15 @@ where
|
||||
names
|
||||
);
|
||||
|
||||
return exclude_candidates(state, opts, excluded_candidates);
|
||||
exclude_candidates(state, opts, excluded_candidates)?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
|
||||
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<bool, STVError>
|
||||
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<(), STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
@ -1275,6 +1295,9 @@ where
|
||||
SurplusMethod::Meek => {
|
||||
meek::exclude_candidates(state, opts, excluded_candidates);
|
||||
}
|
||||
SurplusMethod::Cincinnati | SurplusMethod::Hare => {
|
||||
subset::exclude_candidates(state, opts, excluded_candidates)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
ExclusionMethod::ByValue | ExclusionMethod::BySource | ExclusionMethod::ParcelsByOrder => {
|
||||
@ -1286,7 +1309,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
|
||||
|
286
src/stv/subset.rs
Normal file
286
src/stv/subset.rs
Normal file
@ -0,0 +1,286 @@
|
||||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::constraints;
|
||||
use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote};
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::{STVOptions, SurplusMethod};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ops;
|
||||
|
||||
use super::STVError;
|
||||
|
||||
/// Distribute the surplus of a given candidate according to the random subset method, based on [STVOptions::surplus]
|
||||
pub fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate) -> Result<(), STVError>
|
||||
where
|
||||
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>
|
||||
{
|
||||
state.kind = Some("Surplus of");
|
||||
state.title = String::from(&elected_candidate.name);
|
||||
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
||||
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
||||
|
||||
let votes;
|
||||
match opts.surplus {
|
||||
SurplusMethod::Cincinnati => {
|
||||
// Inclusive
|
||||
votes = count_card.concat_parcels();
|
||||
}
|
||||
SurplusMethod::Hare => {
|
||||
// Exclusive
|
||||
// Should be safe to unwrap() - or else how did we get a quota!
|
||||
votes = count_card.parcels.pop().unwrap().votes;
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
// Calculate skip value
|
||||
let total_ballots = votes.len();
|
||||
let mut skip_fraction = N::from(total_ballots) / &surplus;
|
||||
skip_fraction.round_mut(0);
|
||||
|
||||
state.logger.log_literal(format!("Examining {:.0} ballots, with skip value {:.0}.", total_ballots, skip_fraction));
|
||||
|
||||
// Number the votes
|
||||
let mut numbered_votes: HashMap<usize, Vote<N>> = HashMap::new();
|
||||
for (i, vote) in votes.into_iter().enumerate() {
|
||||
numbered_votes.insert(i, vote);
|
||||
}
|
||||
|
||||
// Transfer candidate votes
|
||||
let skip_value: usize = format!("{:.0}", skip_fraction).parse().expect("Skip value overflows usize");
|
||||
let mut iteration = 0;
|
||||
let mut index = skip_value - 1; // Subtract 1 as votes are 0-indexed
|
||||
|
||||
while &state.candidates[elected_candidate].votes > state.quota.as_ref().unwrap() {
|
||||
let mut vote = numbered_votes.remove(&index).unwrap();
|
||||
|
||||
// Transfer to next preference
|
||||
let mut next_candidate = None;
|
||||
for (i, preference) in vote.ballot.preferences.iter().enumerate().skip(vote.up_to_pref) {
|
||||
let candidate = &state.election.candidates[*preference];
|
||||
let count_card = &state.candidates[candidate];
|
||||
|
||||
if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
|
||||
next_candidate = Some(candidate);
|
||||
vote.up_to_pref = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Have to structure like this to satisfy Rust's borrow checker
|
||||
if let Some(candidate) = next_candidate {
|
||||
// Available preference
|
||||
state.candidates.get_mut(elected_candidate).unwrap().transfer(&-vote.value.clone());
|
||||
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.transfer(&vote.value);
|
||||
|
||||
match count_card.parcels.last_mut() {
|
||||
Some(parcel) => {
|
||||
if parcel.source_order == state.num_elected + state.num_excluded {
|
||||
parcel.votes.push(vote);
|
||||
} else {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
|
||||
super::elect_hopefuls(state, opts)?;
|
||||
} else {
|
||||
// Exhausted
|
||||
if opts.transferable_only {
|
||||
// Another ballot paper required
|
||||
} else {
|
||||
state.candidates.get_mut(elected_candidate).unwrap().transfer(&-vote.value.clone());
|
||||
state.exhausted.transfer(&vote.value);
|
||||
|
||||
match state.exhausted.parcels.last_mut() {
|
||||
Some(parcel) => {
|
||||
if parcel.source_order == state.num_elected + state.num_excluded {
|
||||
parcel.votes.push(vote);
|
||||
} else {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index += skip_value;
|
||||
if index >= total_ballots {
|
||||
iteration += 1;
|
||||
index = iteration + skip_value - 1;
|
||||
|
||||
if iteration >= skip_value {
|
||||
// We have run out of ballot papers
|
||||
// Remaining ballot papers exhaust
|
||||
|
||||
let surplus = &state.candidates[elected_candidate].votes - state.quota.as_ref().unwrap();
|
||||
state.exhausted.transfer(&surplus);
|
||||
state.candidates.get_mut(elected_candidate).unwrap().transfer(&-surplus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Perform one stage of a candidate exclusion according to the random subset method
|
||||
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<(), STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
// Used to give bulk excluded candidate the same order_elected
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
|
||||
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
|
||||
if count_card.state != CandidateState::Excluded {
|
||||
count_card.state = CandidateState::Excluded;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
|
||||
constraints::update_constraints(state, opts);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine votes to transfer
|
||||
let mut votes = Vec::new();
|
||||
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
votes.append(&mut count_card.concat_parcels());
|
||||
count_card.parcels.clear();
|
||||
}
|
||||
|
||||
let total_ballots = votes.len();
|
||||
|
||||
if !votes.is_empty() {
|
||||
if total_ballots == 1 {
|
||||
state.logger.log_literal("Transferring 1 ballot.".to_string());
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots.", total_ballots));
|
||||
}
|
||||
|
||||
// Transfer vote by vote
|
||||
for mut vote in votes {
|
||||
// Subtract votes from excluded candidate
|
||||
let count_card = state.candidates.get_mut(&state.election.candidates[vote.ballot.preferences[vote.up_to_pref - 1]]).unwrap();
|
||||
count_card.transfer(&-vote.value.clone());
|
||||
|
||||
// Transfer to next preference
|
||||
let mut next_candidate = None;
|
||||
for (i, preference) in vote.ballot.preferences.iter().enumerate().skip(vote.up_to_pref) {
|
||||
let candidate = &state.election.candidates[*preference];
|
||||
let count_card = &state.candidates[candidate];
|
||||
|
||||
if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
|
||||
next_candidate = Some(candidate);
|
||||
vote.up_to_pref = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Have to structure like this to satisfy Rust's borrow checker
|
||||
if let Some(candidate) = next_candidate {
|
||||
// Available preference
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.transfer(&vote.value);
|
||||
|
||||
match count_card.parcels.last_mut() {
|
||||
Some(parcel) => {
|
||||
if parcel.source_order == state.num_elected + state.num_excluded {
|
||||
parcel.votes.push(vote);
|
||||
} else {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
|
||||
super::elect_hopefuls(state, opts)?;
|
||||
} else {
|
||||
// Exhausted
|
||||
state.exhausted.transfer(&vote.value);
|
||||
|
||||
match state.exhausted.parcels.last_mut() {
|
||||
Some(parcel) => {
|
||||
if parcel.source_order == state.num_elected + state.num_excluded {
|
||||
parcel.votes.push(vote);
|
||||
} else {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
Loading…
Reference in New Issue
Block a user