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;">
|
<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>
|
<label class="col-12">
|
||||||
</div>
|
<input type="checkbox" id="chkNormaliseBallots">
|
||||||
|
Normalise ballots
|
||||||
|
</label>
|
||||||
<div class="col-12 subheading">
|
<div class="col-12 subheading">
|
||||||
Count optimisations:
|
Count optimisations:
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
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")]
|
#[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);
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue