Implement --normalise-ballots
This, with --sum-surplus-transfers, allows us to fully replicate the Scottish STV result
This commit is contained in:
parent
96a3eaec84
commit
59539d807a
@ -158,13 +158,15 @@
|
||||
<input type="number" id="txtDP" value="5" min="0" style="width: 3em;">
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label>
|
||||
Display up to
|
||||
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
</div>
|
||||
<label class="col-12">
|
||||
Display up to
|
||||
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
<label class="col-12">
|
||||
<input type="checkbox" id="chkNormaliseBallots">
|
||||
Normalise ballots
|
||||
</label>
|
||||
<div class="col-12 subheading">
|
||||
Count optimisations:
|
||||
</div>
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -87,6 +87,23 @@ impl<N: Number> Election<N> {
|
||||
|
||||
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)]
|
||||
|
11
src/main.rs
11
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<N: Number>(election: Election<N>, cmd_opts: STV)
|
||||
fn count_election<N: Number>(mut election: Election<N>, 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);
|
||||
|
||||
|
@ -47,6 +47,12 @@ macro_rules! impl_type {
|
||||
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]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) {
|
||||
|
@ -69,7 +69,10 @@ fn scotland_linn07_fixed5() {
|
||||
let file_reader = io::BufReader::new(file);
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user