From 26d45cac50a50605a4f5e23a1968b06a82c48fe8 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 25 Mar 2022 02:46:30 +1100 Subject: [PATCH] Implement --subtract-nontransferable for NSW Local Government rules --- docs/options.md | 5 +++-- html/index.html | 1 + html/index.js | 2 +- html/presets.js | 2 +- src/cli/stv.rs | 5 +++++ src/stv/gregory/mod.rs | 5 +++++ src/stv/mod.rs | 6 ++++++ src/stv/wasm.rs | 5 +++-- tests/tests_impl/nswlg.rs | 3 ++- 9 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/options.md b/docs/options.md index 8ea4b98..8a6398f 100644 --- a/docs/options.md +++ b/docs/options.md @@ -104,10 +104,11 @@ Random sample methods are also supported, but also not recommended: The use of a random sample method requires *Normalise ballots* to be enabled, and will usually be used with a *Quota criterion* set to *>=*. -### Papers to examine in surplus transfer (--transferable-only) +### Papers to examine in surplus transfer (--transferable-only/--subtract-nontransferable) * *Include non-transferable papers* (default): When this option is selected, all ballot papers of the transferring candidate are examined. Non-transferable papers are always exhausted at the relevant surplus fractions. This is the method typically used with the weighted inclusive Gregory or Meek methods. -* *Use transferable papers only*: When this option is selected, only transferable papers of the transferring candidate are examined. Non-transferable papers are exhausted only if the value of the transferable papers is less than the surplus. This is the method typically used with other surplus distribution methods. +* *Use transferable papers only* (--transferable-only): When this option is selected, only transferable papers of the transferring candidate are examined. Non-transferable papers are exhausted only if the value of the transferable papers is less than the surplus. This is the method typically used with other surplus distribution methods. +* *Subtract non-transferables* (--transferable-only --subtract-nontransferable): Same as *Use transferable papers only*, but the value of the transferable papers is calculated by subtracting the value of non-transferable papers from the progress total. This has effect only as far as concerns rounding. ### (Gregory) Exclusion method (--exclusion) diff --git a/html/index.html b/html/index.html index 74cc4ef..149a195 100644 --- a/html/index.html +++ b/html/index.html @@ -123,6 +123,7 @@ diff --git a/html/index.js b/html/index.js index ad49e62..2c3c7ec 100644 --- a/html/index.js +++ b/html/index.js @@ -154,7 +154,7 @@ async function clickCount() { document.getElementById('txtSeed').value, document.getElementById('selMethod').value, document.getElementById('selSurplus').value, - document.getElementById('selPapers').value == 'transferable', + document.getElementById('selPapers').value, document.getElementById('selExclusion').value, document.getElementById('chkMeekNZExclusion').checked, document.getElementById('selSample').value, diff --git a/html/presets.js b/html/presets.js index 9e5c505..a8416ad 100644 --- a/html/presets.js +++ b/html/presets.js @@ -239,7 +239,7 @@ function changePreset() { document.getElementById('selSumTransfers').value = 'by_value_and_source'; document.getElementById('selSurplus').value = 'by_order'; document.getElementById('selMethod').value = 'wig'; - document.getElementById('selPapers').value = 'transferable'; + document.getElementById('selPapers').value = 'subtract_nontransferable'; document.getElementById('selExclusion').value = 'single_stage'; document.getElementById('selTies').value = 'backwards,random'; } else if (document.getElementById('selPreset').value === 'minneapolis') { diff --git a/src/cli/stv.rs b/src/cli/stv.rs index 788497e..5f7c5b1 100644 --- a/src/cli/stv.rs +++ b/src/cli/stv.rs @@ -125,6 +125,10 @@ pub struct SubcmdOptions { #[clap(help_heading=Some("STV VARIANTS"), long)] transferable_only: bool, + /// (Gregory STV) If --transferable-only, calculate value of transferable papers by subtracting value of non-transferable papers + #[clap(help_heading=Some("STV VARIANTS"), long)] + subtract_nontransferable: bool, + /// (Gregory STV) Method of exclusions #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "by_source", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")] exclusion: String, @@ -294,6 +298,7 @@ where cmd_opts.surplus.into(), cmd_opts.surplus_order.into(), cmd_opts.transferable_only, + cmd_opts.subtract_nontransferable, cmd_opts.exclusion.into(), cmd_opts.meek_nz_exclusion, cmd_opts.sample.into(), diff --git a/src/stv/gregory/mod.rs b/src/stv/gregory/mod.rs index b626f44..22a5ad1 100644 --- a/src/stv/gregory/mod.rs +++ b/src/stv/gregory/mod.rs @@ -265,6 +265,11 @@ where let count_card = state.candidates.get_mut(elected_candidate).unwrap(); count_card.ballot_transfers = -&total_ballots; + if opts.transferable_only && opts.subtract_nontransferable { + // Override transferable_votes + transferable_votes = count_card.votes.clone() - exhausted_votes; + } + let mut surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, opts); let surplus_numer; let mut surplus_fraction; diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 1bb912d..31f65e4 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -104,6 +104,10 @@ pub struct STVOptions { #[builder(default="false")] pub transferable_only: bool, + /// (Gregory STV) If --transferable-only, calculate value of transferable papers by subtracting value of non-transferable papers + #[builder(default="false")] + pub subtract_nontransferable: bool, + /// (Gregory STV) Method of exclusions #[builder(default="ExclusionMethod::SingleStage")] pub exclusion: ExclusionMethod, @@ -189,6 +193,7 @@ impl STVOptions { if self.surplus != SurplusMethod::Meek { if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); } if self.transferable_only { flags.push("--transferable-only".to_string()); } + if self.subtract_nontransferable { flags.push("--subtract-nontransferable".to_string()); } if self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); } } if self.surplus == SurplusMethod::Meek && self.meek_nz_exclusion { flags.push("--meek-nz-exclusion".to_string()); } @@ -224,6 +229,7 @@ impl STVOptions { //if self.sample == SampleMethod::StratifyFloor && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify_floor is incompatible with --sample-per-ballot")); } if self.sample_per_ballot && !self.immediate_elect { return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect")); } } + if self.subtract_nontransferable && !self.transferable_only { return Err(STVError::InvalidOptions("--subtract-nontransferable requires --transferable-only")) } if self.min_threshold != "0" && self.defer_surpluses { return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)")); } // TODO: NYI if self.round_subtransfers == RoundSubtransfersMode::ByValueAndSource && self.bulk_exclude { return Err(STVError::InvalidOptions("--round-subtransfers by_value_and_source is incompatible with --bulk-exclude (not yet implemented)")); } // TODO: NYI return Ok(()); diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 7ece0cc..d470389 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -249,7 +249,7 @@ impl STVOptions { random_seed: String, surplus: &str, surplus_order: &str, - transferable_only: bool, + papers: &str, exclusion: &str, meek_nz_exclusion: bool, sample: &str, @@ -277,7 +277,8 @@ impl STVOptions { ties::from_strs(ties.iter().map(|v| v.as_string().unwrap()).collect(), Some(random_seed)), surplus.into(), surplus_order.into(), - transferable_only, + if papers == "transferable" || papers == "subtract_nontransferable" { true } else { false }, + if papers == "subtract_nontransferable" { true } else { false }, exclusion.into(), meek_nz_exclusion, sample.into(), diff --git a/tests/tests_impl/nswlg.rs b/tests/tests_impl/nswlg.rs index 3b1ca6e..8d8abed 100644 --- a/tests/tests_impl/nswlg.rs +++ b/tests/tests_impl/nswlg.rs @@ -33,9 +33,10 @@ fn nswlg_albury21_rational() { .ties(vec![TieStrategy::Backwards, TieStrategy::Random(String::from("20220322"))]) .surplus_order(stv::SurplusOrder::ByOrder) .transferable_only(true) + .subtract_nontransferable(true) .build().unwrap(); - assert_eq!(stv_opts.describe::(), "--round-votes 0 --round-quota 0 --round-subtransfers by_value_and_source --quota-criterion geq --ties backwards random --random-seed 20220322 --surplus-order by_order --transferable-only"); + assert_eq!(stv_opts.describe::(), "--round-votes 0 --round-quota 0 --round-subtransfers by_value_and_source --quota-criterion geq --ties backwards random --random-seed 20220322 --surplus-order by_order --transferable-only --subtract-nontransferable"); utils::read_validate_election::("tests/data/City_of_Albury-finalpreferencedatafile.csv", "tests/data/City_of_Albury-finalpreferencedatafile.blt", stv_opts, None, &[]); }