From 1b39b8b138ad8b9fea7da14d2292491d8c9bf966 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 20 Jun 2021 01:28:54 +1000 Subject: [PATCH] Implement --meek-nz-exclusion for NZ Meek STV --- docs/options.md | 15 +++++++++++---- html/index.html | 5 +++++ html/index.js | 31 +++++++++++++++++++++++++++++++ src/main.rs | 5 +++++ src/stv/meek.rs | 14 +++++++++++++- src/stv/mod.rs | 19 ++++++++++++------- src/stv/wasm.rs | 2 ++ 7 files changed, 79 insertions(+), 12 deletions(-) diff --git a/docs/options.md b/docs/options.md index 073fccd..4f1b184 100644 --- a/docs/options.md +++ b/docs/options.md @@ -7,7 +7,7 @@ The preset dropdown allows you to choose from a hardcoded list of preloaded STV * *Recommended WIGM*: A recommended set of simple STV rules designed for computer counting, using the weighted inclusive Gregory method and rational arithmetic. * *Scottish STV*: Rules from the [*Scottish Local Government Elections Order 2011*](https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made), using the weighted inclusive Gregory method. Validated against the [2007 Scottish local government election result for Linn ward](https://web.archive.org/web/20121004213938/http://www.glasgow.gov.uk/en/YourCouncil/Elections_Voting/Election_Results/ElectionScotland2007/LGWardResults.htm?ward=1&wardname=1%20-%20Linn). * [*Meek STV*](http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf): Advanced STV rules designed for computer counting, recognised by the Proportional Representation Society of Australia (Victoria–Tasmania) as the superior STV system. - * *Meek STV (1986)* operates according to the original [Hill–Wichmann–Woodall specification](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) of Meek STV, with the modifications, relevant only in exceptional cases, that (a) fixed-point arithmetic with 5 decimal places is used, and (b) candidates are elected on strictly exceeding the quota. Validated against the Hill–Wichmann–Woodall implementation for the [ERS97 model election](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/#sub-section-24). + * *Meek STV (1987)* operates according to the original [Hill–Wichmann–Woodall specification](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) of Meek STV, with the modifications, relevant only in exceptional cases, that (a) fixed-point arithmetic with 5 decimal places is used, and (b) candidates are elected on strictly exceeding the quota. Validated against the Hill–Wichmann–Woodall implementation for the [ERS97 model election](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/#sub-section-24). * *Meek STV (2006)* operates according to [Hill's 2006 revisions](http://www.votingmatters.org.uk/ISSUE22/I22P2.pdf). This is the algorithm referred to in OpenSTV/OpaVote as ‘Meek STV’, and forms the basis of New Zealand's Meek STV rules. Validated against OpenSTV for the ERS97 model election. * *Australian Senate STV*: Rules from the [*Commonwealth Electoral Act 1918*](https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700), using the unweighted inclusive Gregory method. Validated against the [2019 Australian Senate election result for Tasmania](https://results.aec.gov.au/24310/Website/SenateDownloadsMenu-24310-Csv.htm). * [*PRSA 1977*](https://www.prsa.org.au/rule1977.htm): Simple rules designed for hand counting, using the exclusive Gregory method, with counting automatically performed in thousandths of a vote. Validated against [example 1](https://www.prsa.org.au/example1.pdf) of the PRSA's [*Proportional Representation Manual*](https://www.prsa.org.au/publicat.htm#p2). @@ -83,6 +83,13 @@ Other surplus transfer methods, such as non-fractional transfers (e.g. random sa When *Surplus method* is set to *Meek method*, this setting is ignored, and the Meek method is instead applied. +### (Meek) NZ-style exclusion (--meek-nz-exclusion) + +When *Surplus method* is set to *Meek method*, this option controls how candidate keep values are updated when candidates are excluded: + +* When NZ-style exclusion is disabled (default), the excluded candidate's keep value is immediately reduced to 0. This is the method specified in the 1987 and 2006 Meek rules. +* When NZ-style exclusion is enabled, all elected candidates' keep values are first updated by one further iteration; only then is the excluded candidate's keep value reduced to 0. This is the method specified in the New Zealand *Local Electoral Regulations 2001*. + ### Ties (-t/--ties) This dropdown allows you to select how ties (in surplus transfer or exclusion) are broken. The options are: @@ -141,9 +148,9 @@ When deferred surpluses is enabled, the transfer of all surpluses is deferred if ### (Meek) Immediate election (--meek-immediate-elect) -This option controls when candidates are elected when *Surplus method* is set to *Meek method*: +When *Surplus method* is set to *Meek method*, this option controls when candidates are elected: -* When immediate election is disabled (default), all current surpluses are distributed and keep values finalised, before any candidates exceeding the quota are then declared elected. This is the method specified in the 1986 Meek rules. +* When immediate election is disabled (default), all current surpluses are distributed and keep values finalised, before any candidates exceeding the quota are then declared elected. This is the method specified in the 1987 Meek rules. * When immediate election is enabled, a candidate meeting the quota interrupts a surplus distribution. The candidate is immediately declared elected, before the distribution of all surpluses of all now-elected candidates continues. This is the method specified in the 2006 Meek rules. ## Rounding @@ -181,5 +188,5 @@ This option affects the result only insofar as rounding (due to use of fixed-pre When *Surplus method* is set to *Meek method*, this option allows you to specify when the distribution of surpluses will be considered complete. The tolerance may be specified either as a percentage (ends with a `%`) or absolute number of votes (no `%`): -* Percentage: Surplus distributions will be considered complete when every elected candidate's surplus exceeds the quota by no more than the specified percentage. This is the method specified in the 1986 Meek rules. +* Percentage: Surplus distributions will be considered complete when every elected candidate's surplus exceeds the quota by no more than the specified percentage. This is the method specified in the 1987 Meek rules. * Absolute number of votes: Surplus distributions will be considered complete when the total surpluses of all elected candidates is no greater than the specified number of votes. This is the simpler method specified in the 2006 Meek rules. diff --git a/html/index.html b/html/index.html index 9777f8c..cca8429 100644 --- a/html/index.html +++ b/html/index.html @@ -39,6 +39,7 @@ + @@ -114,6 +115,10 @@ +
Tie-breaking: diff --git a/html/index.js b/html/index.js index 0a80c7a..edb8bd1 100644 --- a/html/index.js +++ b/html/index.js @@ -114,6 +114,7 @@ async function clickCount() { document.getElementById('selSurplus').value, document.getElementById('selPapers').value == 'transferable', document.getElementById('selExclusion').value, + document.getElementById('chkMeekNZExclusion').checked, document.getElementById('chkBulkExclusion').checked, document.getElementById('chkDeferSurpluses').checked, document.getElementById('chkMeekImmediateElect').checked, @@ -364,6 +365,7 @@ function changePreset() { document.getElementById('chkBulkExclusion').checked = false; document.getElementById('chkDeferSurpluses').checked = false; document.getElementById('chkMeekImmediateElect').checked = false; + document.getElementById('chkMeekNZExclusion').checked = false; document.getElementById('selNumbers').value = 'fixed'; document.getElementById('txtDP').value = '5'; document.getElementById('txtPPDP').value = '2'; @@ -387,6 +389,35 @@ function changePreset() { document.getElementById('chkBulkExclusion').checked = false; document.getElementById('chkDeferSurpluses').checked = true; document.getElementById('chkMeekImmediateElect').checked = true; + document.getElementById('chkMeekNZExclusion').checked = false; + document.getElementById('selNumbers').value = 'fixed'; + document.getElementById('txtDP').value = '12'; + document.getElementById('txtPPDP').value = '2'; + document.getElementById('chkNormaliseBallots').checked = false; + document.getElementById('chkRoundQuota').checked = true; + document.getElementById('txtRoundQuota').value = '9'; + document.getElementById('chkRoundVotes').checked = true; + document.getElementById('txtRoundVotes').value = '9'; + document.getElementById('chkRoundTVs').checked = true; + document.getElementById('txtRoundTVs').value = '9'; + document.getElementById('chkRoundWeights').checked = true; + document.getElementById('txtRoundWeights').value = '9'; + //document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('txtMeekSurplusTolerance').value = '0.0001'; + //document.getElementById('selSurplus').value = 'by_size'; + document.getElementById('selTransfers').value = 'meek'; + //document.getElementById('selPapers').value = 'both'; + //document.getElementById('selExclusion').value = 'single_stage'; + document.getElementById('selTies').value = 'backwards,random'; + } else if (document.getElementById('selPreset').value === 'meeknz') { + 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; + document.getElementById('chkDeferSurpluses').checked = true; + document.getElementById('chkMeekImmediateElect').checked = true; + document.getElementById('chkMeekNZExclusion').checked = true; document.getElementById('selNumbers').value = 'fixed'; document.getElementById('txtDP').value = '12'; document.getElementById('txtPPDP').value = '2'; diff --git a/src/main.rs b/src/main.rs index 5e312cd..4defe22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -133,6 +133,10 @@ struct STV { #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "parcels_by_order"], default_value="single_stage", value_name="method")] exclusion: String, + /// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion + #[clap(help_heading=Some("STV VARIANTS"), long)] + meek_nz_exclusion: bool, + // ------------------------- // -- Count optimisations -- @@ -219,6 +223,7 @@ where cmd_opts.bulk_exclude, cmd_opts.defer_surpluses, cmd_opts.meek_immediate_elect, + cmd_opts.meek_nz_exclusion, cmd_opts.pp_decimals, ); diff --git a/src/stv/meek.rs b/src/stv/meek.rs index 5a36b4b..5453d43 100644 --- a/src/stv/meek.rs +++ b/src/stv/meek.rs @@ -240,7 +240,7 @@ where for<'r> &'r N: ops::Div<&'r N, Output=N>, { let quota = state.quota.as_ref().unwrap(); - let mut has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie + let mut has_surplus: Vec<&Candidate> = state.election.candidates.iter() .filter(|c| { let count_card = state.candidates.get(c).unwrap(); return count_card.state == CandidateState::Elected && &count_card.votes > quota; @@ -358,6 +358,18 @@ where for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Mul<&'r N, Output=N>, { + // NZ Meek STV: Iterate keep values one round before exclusion + if opts.meek_nz_exclusion { + let quota = state.quota.as_ref().unwrap(); + let has_surplus: Vec<&Candidate> = state.election.candidates.iter() + .filter(|c| { + let count_card = state.candidates.get(c).unwrap(); + return count_card.state == CandidateState::Elected && &count_card.votes > quota; + }) + .collect(); + recompute_keep_values(state, opts, &has_surplus); + } + // Used to give bulk excluded candidate the same order_elected let order_excluded = state.num_excluded + 1; diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 6092213..a686d41 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -69,6 +69,8 @@ pub struct STVOptions { pub transferable_only: bool, /// Method of exclusions pub exclusion: ExclusionMethod, + /// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion + pub meek_nz_exclusion: bool, /// Use bulk exclusion pub bulk_exclude: bool, /// Defer surplus distributions if possible @@ -98,6 +100,7 @@ impl STVOptions { surplus_order: &str, transferable_only: bool, exclusion: &str, + meek_nz_exclusion: bool, bulk_exclude: bool, defer_surpluses: bool, meek_immediate_elect: bool, @@ -159,6 +162,7 @@ impl STVOptions { "parcels_by_order" => ExclusionMethod::ParcelsByOrder, _ => panic!("Invalid --exclusion"), }, + meek_nz_exclusion, bulk_exclude, defer_surpluses, meek_immediate_elect, @@ -174,19 +178,20 @@ impl STVOptions { if let Some(dps) = self.round_weights { 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.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.describe()); } + 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)); } if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); } + if self.quota != QuotaType::DroopExact { flags.push(self.quota.describe()); } + if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); } + if self.surplus != SurplusMethod::Meek && self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); } let ties_str = self.ties.iter().map(|t| t.describe()).join(" "); if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); } for t in self.ties.iter() { if let TieStrategy::Random(seed) = t { flags.push(format!("--random-seed {}", seed)); } } - if self.quota != QuotaType::DroopExact { flags.push(self.quota.describe()); } - if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); } - if self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); } if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); } - if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); } - if self.transferable_only { flags.push("--transferable-only".to_string()); } - if self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); } + if self.surplus != SurplusMethod::Meek && self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); } + if self.surplus != SurplusMethod::Meek && self.transferable_only { flags.push("--transferable-only".to_string()); } + if self.surplus != SurplusMethod::Meek && 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()); } if self.bulk_exclude { flags.push("--bulk-exclude".to_string()); } if self.defer_surpluses { flags.push("--defer-surpluses".to_string()); } if self.surplus == SurplusMethod::Meek && self.meek_immediate_elect { flags.push("--meek-immediate-elect".to_string()); } diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index e5e3955..ec7964d 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -199,6 +199,7 @@ impl STVOptions { surplus_order: &str, transferable_only: bool, exclusion: &str, + meek_nz_exclusion: bool, bulk_exclude: bool, defer_surpluses: bool, meek_immediate_elect: bool, @@ -221,6 +222,7 @@ impl STVOptions { surplus_order, transferable_only, exclusion, + meek_nz_exclusion, bulk_exclude, defer_surpluses, meek_immediate_elect,