Implement --normalise-ballots

This, with --sum-surplus-transfers, allows us to fully replicate the Scottish STV result
This commit is contained in:
RunasSudo 2021-06-11 21:23:08 +10:00
parent 96a3eaec84
commit 59539d807a
No known key found for this signature in database
GPG Key ID: 7234E476BF21C61A
7 changed files with 58 additions and 25 deletions

View File

@ -158,13 +158,15 @@
<input type="number" id="txtDP" value="5" min="0" style="width: 3em;"> <input type="number" id="txtDP" value="5" min="0" style="width: 3em;">
</label> </label>
</div> </div>
<div class="col-12"> <label class="col-12">
<label>
Display up to Display up to
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;"> <input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
d.p. d.p.
</label> </label>
</div> <label class="col-12">
<input type="checkbox" id="chkNormaliseBallots">
Normalise ballots
</label>
<div class="col-12 subheading"> <div class="col-12 subheading">
Count optimisations: Count optimisations:
</div> </div>

View File

@ -116,6 +116,7 @@ async function clickCount() {
'filePath': filePath, 'filePath': filePath,
'numbers': document.getElementById('selNumbers').value, 'numbers': document.getElementById('selNumbers').value,
'decimals': document.getElementById('txtDP').value, 'decimals': document.getElementById('txtDP').value,
'normaliseBallots': document.getElementById('chkNormaliseBallots').checked,
}); });
} }
@ -301,6 +302,7 @@ function changePreset() {
document.getElementById('chkDeferSurpluses').checked = false; document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'rational'; document.getElementById('selNumbers').value = 'rational';
document.getElementById('txtPPDP').value = '2'; document.getElementById('txtPPDP').value = '2';
document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = false; document.getElementById('chkRoundQuota').checked = false;
document.getElementById('chkRoundVotes').checked = false; document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundTVs').checked = false; document.getElementById('chkRoundTVs').checked = false;
@ -321,6 +323,7 @@ function changePreset() {
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5'; document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '5'; document.getElementById('txtPPDP').value = '5';
document.getElementById('chkNormaliseBallots').checked = true;
document.getElementById('chkRoundQuota').checked = true; document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0'; document.getElementById('txtRoundQuota').value = '0';
document.getElementById('chkRoundVotes').checked = false; document.getElementById('chkRoundVotes').checked = false;
@ -343,6 +346,7 @@ function changePreset() {
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5'; document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '0'; document.getElementById('txtPPDP').value = '0';
document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = true; document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0'; document.getElementById('txtRoundQuota').value = '0';
document.getElementById('chkRoundVotes').checked = true; document.getElementById('chkRoundVotes').checked = true;
@ -365,6 +369,7 @@ function changePreset() {
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5'; document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '3'; document.getElementById('txtPPDP').value = '3';
document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = true; document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '3'; document.getElementById('txtRoundQuota').value = '3';
document.getElementById('chkRoundVotes').checked = true; document.getElementById('chkRoundVotes').checked = true;
@ -389,6 +394,7 @@ function changePreset() {
document.getElementById('selNumbers').value = 'fixed'; document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5'; document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '2'; document.getElementById('txtPPDP').value = '2';
document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = true; document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '2'; document.getElementById('txtRoundQuota').value = '2';
document.getElementById('chkRoundVotes').checked = true; document.getElementById('chkRoundVotes').checked = true;

View File

@ -25,6 +25,10 @@ onmessage = function(evt) {
// Init election // Init election
let election = wasm['election_from_blt_' + numbers](evt.data.electionData); let election = wasm['election_from_blt_' + numbers](evt.data.electionData);
if (evt.data.normaliseBallots) {
wasm['election_normalise_ballots_' + numbers](election);
}
// Init STV options // Init STV options
let opts = wasm.STVOptions.new.apply(null, evt.data.optsStr); let opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);

View File

@ -87,6 +87,23 @@ impl<N: Number> Election<N> {
return 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)] #[derive(PartialEq, Eq, Hash)]

View File

@ -59,6 +59,10 @@ struct STV {
#[clap(help_heading=Some("NUMBERS"), long, default_value="5", value_name="dps")] #[clap(help_heading=Some("NUMBERS"), long, default_value="5", value_name="dps")]
decimals: usize, decimals: usize,
/// Convert ballots with value >1 to multiple ballots of value 1
#[clap(help_heading=Some("NUMBERS"), long)]
normalise_ballots: bool,
// ----------------------- // -----------------------
// -- Rounding settings -- // -- Rounding settings --
@ -164,7 +168,7 @@ fn main() {
} }
} }
fn count_election<N: Number>(election: Election<N>, cmd_opts: STV) fn count_election<N: Number>(mut election: Election<N>, cmd_opts: STV)
where where
for<'r> &'r N: ops::Sub<&'r N, Output=N>, 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::Div<&'r N, Output=N>,
@ -189,6 +193,11 @@ where
cmd_opts.pp_decimals, cmd_opts.pp_decimals,
); );
// Normalise ballots if requested
if cmd_opts.normalise_ballots {
election.normalise_ballots();
}
// Initialise count state // Initialise count state
let mut state = CountState::new(&election); let mut state = CountState::new(&election);

View File

@ -47,6 +47,12 @@ macro_rules! impl_type {
return [<Election$type>](election); return [<Election$type>](election);
} }
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn [<election_normalise_ballots_$type>](election: &mut [<Election$type>]) {
election.0.normalise_ballots();
}
#[wasm_bindgen] #[wasm_bindgen]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) { pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) {

View File

@ -69,7 +69,10 @@ fn scotland_linn07_fixed5() {
let file_reader = io::BufReader::new(file); let file_reader = io::BufReader::new(file);
let lines = file_reader.lines(); let lines = file_reader.lines();
let election: Election<Fixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); let mut election: Election<Fixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
// !!! FOR SCOTTISH STV !!!
election.normalise_ballots();
// Initialise count state // Initialise count state
let mut state = CountState::new(&election); let mut state = CountState::new(&election);
@ -87,7 +90,7 @@ fn scotland_linn07_fixed5() {
.get_text().unwrap() .get_text().unwrap()
.to_string(); .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()) { for (candidate, cand_xml) in state.election.candidates.iter().zip(candidates.iter()) {
let count_card = state.candidates.get(candidate).unwrap(); let count_card = state.candidates.get(candidate).unwrap();
@ -98,7 +101,7 @@ fn scotland_linn07_fixed5() {
.get_text().unwrap() .get_text().unwrap()
.to_string(); .to_string();
let cand_votes = parse_str(cand_votes); 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 // Validate candidate states
let cand_state = get_cand_stage(cand_xml, i) 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"); let f: f64 = s.parse().expect("Syntax Error");
return opentally::numbers::From::from(f); 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;
}