From b0f869bf02f4bc14ef71da55480a0a97b14aedd2 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 3 Sep 2021 23:53:15 +1000 Subject: [PATCH] Initial framework for equal rankings --- coverage.sh | 1 + html/worker.js | 4 ++++ src/cli/stv.rs | 3 +++ src/election.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++--- src/parser/blt.rs | 2 +- src/parser/csp.rs | 2 +- src/stv/meek.rs | 8 ++++++- src/stv/mod.rs | 20 ++++++++++------- src/stv/sample.rs | 21 ++++++++++------- src/stv/wasm.rs | 7 ++++++ src/writer/blt.rs | 2 +- src/writer/csp.rs | 4 +++- 12 files changed, 107 insertions(+), 24 deletions(-) diff --git a/coverage.sh b/coverage.sh index d0dba13..4412e87 100755 --- a/coverage.sh +++ b/coverage.sh @@ -14,6 +14,7 @@ llvm-profdata merge -sparse target/coverage/prof/*.profraw -o target/coverage/op eval llvm-cov show target/coverage/debug/opentally -instr-profile=target/coverage/opentally.profdata -Xdemangler="$HOME/.cargo/bin/rustfilt" \ $(for file in $(cargo test --no-run --message-format=json 2>/dev/null | jq -r "select(.profile.test == true) | .filenames[]"); do echo -n --object '"'$file'" '; done) \ -ignore-filename-regex="$HOME/." \ + -ignore-filename-regex=rustc \ -ignore-filename-regex=numbers/rational_num.rs \ -ignore-filename-regex=stv/wasm.rs \ -ignore-filename-regex=tests \ diff --git a/html/worker.js b/html/worker.js index 1f0a208..3234e3c 100644 --- a/html/worker.js +++ b/html/worker.js @@ -44,10 +44,14 @@ onmessage = function(evt) { // Init election election = wasm['election_from_blt_' + numbers](evt.data.bltData); + // Normalise ballots if requested if (evt.data.normaliseBallots) { wasm['election_normalise_ballots_' + numbers](election); } + // Process equal rankings + wasm['election_realise_equal_rankings_' + numbers](election); + // Init constraints if applicable if (evt.data.conData) { wasm['election_load_constraints_' + numbers](election, evt.data.conData); diff --git a/src/cli/stv.rs b/src/cli/stv.rs index b9cb08b..dd0261b 100644 --- a/src/cli/stv.rs +++ b/src/cli/stv.rs @@ -309,6 +309,9 @@ where election.normalise_ballots(); } + // Process equal rankings + election.realise_equal_rankings(); + // Initialise count state let mut state = CountState::new(&election); diff --git a/src/election.rs b/src/election.rs index 3e1a895..fd67ab2 100644 --- a/src/election.rs +++ b/src/election.rs @@ -47,6 +47,8 @@ pub struct Election { impl Election { /// Convert ballots with weight >1 to multiple ballots of weight 1 + /// + /// Assumes ballots have integer weight. pub fn normalise_ballots(&mut self) { let mut normalised_ballots = Vec::new(); for ballot in self.ballots.iter() { @@ -63,6 +65,16 @@ impl Election { } self.ballots = normalised_ballots; } + + /// Convert ballots with equal rankings to strict-preference "minivoters" + pub fn realise_equal_rankings(&mut self) { + let mut realised_ballots = Vec::new(); + for ballot in self.ballots.iter() { + let mut b = ballot.realise_equal_rankings(); + realised_ballots.append(&mut b); + } + self.ballots = realised_ballots; + } } /// A candidate in an [Election] @@ -74,7 +86,6 @@ pub struct Candidate { } /// The current state of counting an [Election] -//#[derive(Clone)] pub struct CountState<'a, N: Number> { /// Pointer to the [Election] being counted pub election: &'a Election, @@ -368,6 +379,22 @@ pub struct Vote<'a, N> { pub up_to_pref: usize, } +impl<'a, N> Vote<'a, N> { + /// Get the next preference and increment `up_to_pref` + /// + /// Assumes that each preference level contains only one preference. + pub fn next_preference(&mut self) -> Option { + if self.up_to_pref >= self.ballot.preferences.len() { + return None; + } + + let preference = &self.ballot.preferences[self.up_to_pref]; + self.up_to_pref += 1; + + return Some(*preference.first().unwrap()); + } +} + /// A record of a voter's preferences #[derive(Clone)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] @@ -375,8 +402,32 @@ pub struct Ballot { /// Original value/weight of the ballot #[cfg_attr(not(target_arch = "wasm32"), with(SerializedNum))] pub orig_value: N, - /// Indexes of candidates preferenced on the ballot - pub preferences: Vec, + /// Indexes of candidates preferenced at each level on the ballot + pub preferences: Vec>, +} + +impl Ballot { + /// Convert ballot with equal rankings to strict-preference "minivoters" + pub fn realise_equal_rankings(&self) -> Vec> { + // Preferences for each minivoter + let mut minivoters = vec![Vec::new()]; + + for preference in self.preferences.iter() { + if preference.len() == 1 { + for minivoter in minivoters.iter_mut() { + minivoter.push(preference.clone()); + } + } else { + todo!(); + } + } + + let weight_each = self.orig_value.clone() / N::from(minivoters.len()); + let ballots = minivoters.into_iter() + .map(|p| Ballot { orig_value: weight_each.clone(), preferences: p }) + .collect(); + return ballots; + } } /// rkyv-serialized representation of [Number] diff --git a/src/parser/blt.rs b/src/parser/blt.rs index 694e1d9..505aafd 100644 --- a/src/parser/blt.rs +++ b/src/parser/blt.rs @@ -151,7 +151,7 @@ impl> BLTParser { self.accept(); break; } else { - preferences.push(self.usize()? - 1); + preferences.push(vec![self.usize()? - 1]); self.delimiter_not_nl(); } } diff --git a/src/parser/csp.rs b/src/parser/csp.rs index f72e997..30d233a 100644 --- a/src/parser/csp.rs +++ b/src/parser/csp.rs @@ -87,7 +87,7 @@ pub fn parse_reader(reader: R) -> Election { ballots.push(Ballot { orig_value: value, - preferences: preferences.into_iter().map(|(_, i)| *i).collect(), + preferences: preferences.into_iter().map(|(_, i)| vec![*i]).collect(), }); } diff --git a/src/stv/meek.rs b/src/stv/meek.rs index 1391ae3..fa5fa41 100644 --- a/src/stv/meek.rs +++ b/src/stv/meek.rs @@ -60,7 +60,13 @@ impl<'t, N: Number> BallotTree<'t, N> { for bit in self.ballots.iter() { if bit.up_to_pref < bit.ballot.preferences.len() { - let candidate = &candidates[bit.ballot.preferences[bit.up_to_pref]]; + let preference = &bit.ballot.preferences[bit.up_to_pref]; + + if preference.len() != 1 { + todo!(); + } + + let candidate = &candidates[*preference.first().unwrap()]; if next_preferences.contains_key(candidate) { let np_bt = next_preferences.get_mut(candidate).unwrap(); diff --git a/src/stv/mod.rs b/src/stv/mod.rs index fb575ee..5652216 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -692,14 +692,18 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec { + 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); + break; + } + } + None => { break; } } } diff --git a/src/stv/sample.rs b/src/stv/sample.rs index cb9a88f..6b71145 100644 --- a/src/stv/sample.rs +++ b/src/stv/sample.rs @@ -275,14 +275,19 @@ where fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, source_candidate: &Candidate, mut vote: Vote<'a, N>, ignore_nontransferable: bool) -> Result<(), STVError> { // Get 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; + + loop { + match vote.next_preference() { + Some(preference) => { + 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); + break; + } + } + None => { break; } } } diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 11dd6d0..8afc2e7 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -87,6 +87,13 @@ macro_rules! impl_type { election.0.normalise_ballots(); } + /// Wrapper for [Election::realise_equal_rankings] + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn [](election: &mut []) { + election.0.realise_equal_rankings(); + } + /// Call [Constraints::from_con] and set [Election::constraints] #[wasm_bindgen] #[allow(non_snake_case)] diff --git a/src/writer/blt.rs b/src/writer/blt.rs index c8e042e..a41370a 100644 --- a/src/writer/blt.rs +++ b/src/writer/blt.rs @@ -40,7 +40,7 @@ pub fn write(election: Election, output: W) { output.write_fmt(format_args!("{}", ballot.orig_value)).expect("IO Error"); for preference in ballot.preferences { - output.write_fmt(format_args!(" {}", preference + 1)).expect("IO Error"); + output.write_fmt(format_args!(" {}", preference.into_iter().map(|p| p + 1).join("="))).expect("IO Error"); } output.write(b" 0\n").expect("IO Error"); diff --git a/src/writer/csp.rs b/src/writer/csp.rs index d6f208f..ad5998f 100644 --- a/src/writer/csp.rs +++ b/src/writer/csp.rs @@ -40,7 +40,9 @@ pub fn write(election: Election, output: W) { // Code preferences to rankings let mut rankings = vec![0_usize; election.candidates.len()]; for (i, preference) in ballot.preferences.into_iter().enumerate() { - rankings[preference] = i + 1; + for p in preference { + rankings[p] = i + 1; + } } // Write rankings