From 59539d807a3898a738b93c46df224fbc645d30c6 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 11 Jun 2021 21:23:08 +1000 Subject: [PATCH] Implement --normalise-ballots This, with --sum-surplus-transfers, allows us to fully replicate the Scottish STV result --- html/index.html | 16 +++++++++------- html/index.js | 6 ++++++ html/worker.js | 4 ++++ src/election.rs | 17 +++++++++++++++++ src/main.rs | 11 ++++++++++- src/stv/wasm.rs | 6 ++++++ tests/scotland.rs | 23 ++++++----------------- 7 files changed, 58 insertions(+), 25 deletions(-) diff --git a/html/index.html b/html/index.html index f01d77c..590c187 100644 --- a/html/index.html +++ b/html/index.html @@ -158,13 +158,15 @@ -
- -
+ +
Count optimisations:
diff --git a/html/index.js b/html/index.js index 3aabe75..87f14c6 100644 --- a/html/index.js +++ b/html/index.js @@ -116,6 +116,7 @@ async function clickCount() { 'filePath': filePath, 'numbers': document.getElementById('selNumbers').value, 'decimals': document.getElementById('txtDP').value, + 'normaliseBallots': document.getElementById('chkNormaliseBallots').checked, }); } @@ -301,6 +302,7 @@ function changePreset() { document.getElementById('chkDeferSurpluses').checked = false; document.getElementById('selNumbers').value = 'rational'; document.getElementById('txtPPDP').value = '2'; + document.getElementById('chkNormaliseBallots').checked = false; document.getElementById('chkRoundQuota').checked = false; document.getElementById('chkRoundVotes').checked = false; document.getElementById('chkRoundTVs').checked = false; @@ -321,6 +323,7 @@ function changePreset() { document.getElementById('selNumbers').value = 'fixed'; document.getElementById('txtDP').value = '5'; document.getElementById('txtPPDP').value = '5'; + document.getElementById('chkNormaliseBallots').checked = true; document.getElementById('chkRoundQuota').checked = true; document.getElementById('txtRoundQuota').value = '0'; document.getElementById('chkRoundVotes').checked = false; @@ -343,6 +346,7 @@ function changePreset() { document.getElementById('selNumbers').value = 'fixed'; document.getElementById('txtDP').value = '5'; document.getElementById('txtPPDP').value = '0'; + document.getElementById('chkNormaliseBallots').checked = false; document.getElementById('chkRoundQuota').checked = true; document.getElementById('txtRoundQuota').value = '0'; document.getElementById('chkRoundVotes').checked = true; @@ -365,6 +369,7 @@ function changePreset() { document.getElementById('selNumbers').value = 'fixed'; document.getElementById('txtDP').value = '5'; document.getElementById('txtPPDP').value = '3'; + document.getElementById('chkNormaliseBallots').checked = false; document.getElementById('chkRoundQuota').checked = true; document.getElementById('txtRoundQuota').value = '3'; document.getElementById('chkRoundVotes').checked = true; @@ -389,6 +394,7 @@ function changePreset() { document.getElementById('selNumbers').value = 'fixed'; document.getElementById('txtDP').value = '5'; document.getElementById('txtPPDP').value = '2'; + document.getElementById('chkNormaliseBallots').checked = false; document.getElementById('chkRoundQuota').checked = true; document.getElementById('txtRoundQuota').value = '2'; document.getElementById('chkRoundVotes').checked = true; diff --git a/html/worker.js b/html/worker.js index d25b23d..f766283 100644 --- a/html/worker.js +++ b/html/worker.js @@ -25,6 +25,10 @@ onmessage = function(evt) { // Init election let election = wasm['election_from_blt_' + numbers](evt.data.electionData); + if (evt.data.normaliseBallots) { + wasm['election_normalise_ballots_' + numbers](election); + } + // Init STV options let opts = wasm.STVOptions.new.apply(null, evt.data.optsStr); diff --git a/src/election.rs b/src/election.rs index 4a06dd9..a238ca3 100644 --- a/src/election.rs +++ b/src/election.rs @@ -87,6 +87,23 @@ impl Election { return election; } + + pub fn normalise_ballots(&mut self) { + let mut normalised_ballots = Vec::new(); + for ballot in self.ballots.iter() { + let mut n = N::new(); + let one = N::one(); + while n < ballot.orig_value { + let new_ballot = Ballot { + orig_value: N::one(), + preferences: ballot.preferences.clone(), + }; + normalised_ballots.push(new_ballot); + n += &one; + } + } + self.ballots = normalised_ballots; + } } #[derive(PartialEq, Eq, Hash)] diff --git a/src/main.rs b/src/main.rs index 24b4ebb..b9df557 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,10 @@ struct STV { #[clap(help_heading=Some("NUMBERS"), long, default_value="5", value_name="dps")] decimals: usize, + /// Convert ballots with value >1 to multiple ballots of value 1 + #[clap(help_heading=Some("NUMBERS"), long)] + normalise_ballots: bool, + // ----------------------- // -- Rounding settings -- @@ -164,7 +168,7 @@ fn main() { } } -fn count_election(election: Election, cmd_opts: STV) +fn count_election(mut election: Election, cmd_opts: STV) where for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, @@ -189,6 +193,11 @@ where cmd_opts.pp_decimals, ); + // Normalise ballots if requested + if cmd_opts.normalise_ballots { + election.normalise_ballots(); + } + // Initialise count state let mut state = CountState::new(&election); diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 475187f..2002e12 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -47,6 +47,12 @@ macro_rules! impl_type { return [](election); } + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn [](election: &mut []) { + election.0.normalise_ballots(); + } + #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &mut [], opts: &stv::STVOptions) { diff --git a/tests/scotland.rs b/tests/scotland.rs index 7af4aa9..0ebec2a 100644 --- a/tests/scotland.rs +++ b/tests/scotland.rs @@ -69,7 +69,10 @@ fn scotland_linn07_fixed5() { let file_reader = io::BufReader::new(file); let lines = file_reader.lines(); - let election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); + let mut election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); + + // !!! FOR SCOTTISH STV !!! + election.normalise_ballots(); // Initialise count state let mut state = CountState::new(&election); @@ -87,7 +90,7 @@ fn scotland_linn07_fixed5() { .get_text().unwrap() .to_string(); - assert!(approx_eq(&(&state.exhausted.votes + &state.loss_fraction.votes), &parse_str(nt_votes))); + assert!((&state.exhausted.votes + &state.loss_fraction.votes) == parse_str(nt_votes)); for (candidate, cand_xml) in state.election.candidates.iter().zip(candidates.iter()) { let count_card = state.candidates.get(candidate).unwrap(); @@ -98,7 +101,7 @@ fn scotland_linn07_fixed5() { .get_text().unwrap() .to_string(); let cand_votes = parse_str(cand_votes); - assert!(approx_eq(&count_card.votes, &cand_votes), "Failed to validate votes for candidate {}. Expected {:}, got {:}", candidate.name, cand_votes, count_card.votes); + assert!(count_card.votes == cand_votes, "Failed to validate votes for candidate {}. Expected {:}, got {:}", candidate.name, cand_votes, count_card.votes); // Validate candidate states let cand_state = get_cand_stage(cand_xml, i) @@ -137,17 +140,3 @@ fn parse_str(s: String) -> Fixed { let f: f64 = s.parse().expect("Syntax Error"); return opentally::numbers::From::from(f); } - -fn approx_eq(a: &Fixed, b: &Fixed) -> bool { - // Some tolerance required in equality comparisons, as eSTV incorrectly computes transfers - // by value, instead of all at once, resulting in increased rounding error - let eq_tol: Fixed = opentally::numbers::From::from(0.0001); - - if a - b > eq_tol { - return false; - } - if b - a > eq_tol { - return false; - } - return true; -}