Implement --surplus and --exclusion

This commit is contained in:
RunasSudo 2021-05-31 22:25:53 +10:00
parent 2428fcb4ed
commit c114d3a4ee
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 130 additions and 60 deletions

View File

@ -64,8 +64,12 @@ struct STV {
// -- STV variants -- // -- STV variants --
/// Method of surplus transfers
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
surplus: String,
/// Method of exclusions /// Method of exclusions
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["one_round", "by_value"], default_value="one_round", value_name="mode")] #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "parcels_by_order"], default_value="single_stage", value_name="method")]
exclusion: String, exclusion: String,
// -- Display settings -- // -- Display settings --
@ -109,7 +113,20 @@ where
// Copy applicable options // Copy applicable options
let stv_opts = stv::STVOptions { let stv_opts = stv::STVOptions {
round_votes: cmd_opts.round_votes, round_votes: cmd_opts.round_votes,
exclusion: &cmd_opts.exclusion, surplus: match cmd_opts.surplus.as_str() {
"wig" => stv::SurplusMethod::WIG,
"uig" => stv::SurplusMethod::UIG,
"eg" => stv::SurplusMethod::EG,
"meek" => stv::SurplusMethod::Meek,
_ => panic!("Invalid --surplus"),
},
exclusion: match cmd_opts.exclusion.as_str() {
"single_stage" => stv::ExclusionMethod::SingleStage,
"by_value" => stv::ExclusionMethod::ByValue,
"parcels_by_order" => stv::ExclusionMethod::ParcelsByOrder,
_ => panic!("Invalid --exclusion"),
},
pp_decimals: cmd_opts.pp_decimals,
}; };
// Initialise count state // Initialise count state

View File

@ -23,17 +23,39 @@ pub mod wasm;
use crate::numbers::Number; use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
use wasm_bindgen::prelude::wasm_bindgen;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops; use std::ops;
pub struct STVOptions<'a> { #[wasm_bindgen]
pub struct STVOptions {
pub round_votes: Option<usize>, pub round_votes: Option<usize>,
pub exclusion: &'a str, pub surplus: SurplusMethod,
pub exclusion: ExclusionMethod,
pub pp_decimals: usize,
} }
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, _opts: &STVOptions) { #[wasm_bindgen]
#[derive(Clone, Copy)]
pub enum SurplusMethod {
WIG,
UIG,
EG,
Meek,
}
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub enum ExclusionMethod {
SingleStage,
ByValue,
ParcelsByOrder,
}
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) {
distribute_first_preferences(&mut state); distribute_first_preferences(&mut state);
calculate_quota(&mut state); calculate_quota(&mut state, opts);
elect_meeting_quota(&mut state); elect_meeting_quota(&mut state);
} }
@ -173,12 +195,12 @@ fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
state.logger.log_literal("First preferences distributed.".to_string()); state.logger.log_literal("First preferences distributed.".to_string());
} }
fn calculate_quota<N: Number>(state: &mut CountState<N>) { fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
let mut log = String::new(); let mut log = String::new();
// Calculate the total vote // Calculate the total vote
state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
log.push_str(format!("{:.2} usable votes, so the quota is ", state.quota).as_str()); log.push_str(format!("{:.dps$} usable votes, so the quota is ", state.quota, dps=opts.pp_decimals).as_str());
// TODO: Different quotas // TODO: Different quotas
state.quota /= N::from(state.election.seats + 1); state.quota /= N::from(state.election.seats + 1);
@ -186,7 +208,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>) {
// TODO: Different rounding rules // TODO: Different rounding rules
state.quota += N::one(); state.quota += N::one();
state.quota.floor_mut(0); state.quota.floor_mut(0);
log.push_str(format!("{:.2}.", state.quota).as_str()); log.push_str(format!("{:.dps$}.", state.quota, dps=opts.pp_decimals).as_str());
state.logger.log_literal(log); state.logger.log_literal(log);
} }
@ -251,21 +273,42 @@ where
let count_card = state.candidates.get(elected_candidate).unwrap(); let count_card = state.candidates.get(elected_candidate).unwrap();
let surplus = &count_card.votes - &state.quota; let surplus = &count_card.votes - &state.quota;
let votes;
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG => {
// Inclusive Gregory // Inclusive Gregory
// TODO: Other methods votes = state.candidates.get(elected_candidate).unwrap().parcels.concat();
let votes = state.candidates.get(elected_candidate).unwrap().parcels.concat(); }
SurplusMethod::EG => {
// Exclusive Gregory
// Should be safe to unwrap() - or else how did we get a quota!
votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap();
}
SurplusMethod::Meek => {
todo!();
}
}
// Count next preferences // Count next preferences
let result = next_preferences(state, votes); let result = next_preferences(state, votes);
// Transfer candidate votes // Transfer candidate votes
let transfer_value;
match opts.surplus {
SurplusMethod::WIG => {
// Weighted inclusive Gregory
transfer_value = surplus.clone() / &result.total_votes;
}
SurplusMethod::UIG | SurplusMethod::EG => {
// Unweighted inclusive Gregory // Unweighted inclusive Gregory
// TODO: Other methods transfer_value = surplus.clone() / &result.total_ballots;
let transfer_value = surplus.clone() / &result.total_ballots; }
SurplusMethod::Meek => { todo!(); }
}
state.kind = Some("Surplus of"); state.kind = Some("Surplus of");
state.title = String::from(&elected_candidate.name); state.title = String::from(&elected_candidate.name);
state.logger.log_literal(format!("Surplus of {} distributed at value {:.2}.", elected_candidate.name, transfer_value)); state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value, dps=opts.pp_decimals));
let mut checksum = N::new(); let mut checksum = N::new();
@ -413,14 +456,15 @@ where
// Determine votes to transfer in this stage // Determine votes to transfer in this stage
let mut votes; let mut votes;
let votes_remaining; let votes_remain;
if opts.exclusion == "one_round" { match opts.exclusion {
ExclusionMethod::SingleStage => {
// Exclude in one round // Exclude in one round
votes = count_card.parcels.concat(); votes = count_card.parcels.concat();
votes_remaining = 0; votes_remain = false;
}
} else if opts.exclusion == "by_value" { ExclusionMethod::ByValue => {
// Exclude by value // Exclude by value
let all_votes = count_card.parcels.concat(); let all_votes = count_card.parcels.concat();
@ -439,13 +483,15 @@ where
} }
} }
votes_remaining = remaining_votes.len(); votes_remain = remaining_votes.len() > 0;
// Leave remaining votes with candidate (as one parcel) // Leave remaining votes with candidate (as one parcel)
count_card.parcels = vec![remaining_votes]; count_card.parcels = vec![remaining_votes];
}
} else { ExclusionMethod::ParcelsByOrder => {
// TODO: Exclude by parcel // Exclude by parcel by order
panic!("Invalid --exclusion"); votes = count_card.parcels.remove(0);
votes_remain = count_card.parcels.len() > 0;
}
} }
let mut checksum = N::new(); let mut checksum = N::new();
@ -456,10 +502,10 @@ where
// Count next preferences // Count next preferences
let result = next_preferences(state, votes); let result = next_preferences(state, votes);
if opts.exclusion == "one_round" { if let ExclusionMethod::SingleStage = opts.exclusion {
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.2} votes.", result.total_ballots, result.total_votes)); state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals));
} else if opts.exclusion == "by_value" { } else {
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.2} votes, received at value {:.2}.", result.total_ballots, result.total_votes, value)); state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals));
} }
// Transfer candidate votes // Transfer candidate votes
@ -488,7 +534,7 @@ where
state.exhausted.transfer(&exhausted_transfers); state.exhausted.transfer(&exhausted_transfers);
checksum += exhausted_transfers; checksum += exhausted_transfers;
if votes_remaining > 0 { if votes_remain {
// Subtract from candidate tally // Subtract from candidate tally
let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
checksum -= &result.total_votes; checksum -= &result.total_votes;
@ -498,7 +544,7 @@ where
} }
} }
if votes_remaining == 0 { if !votes_remain {
// Finalise candidate votes // Finalise candidate votes
let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
checksum -= &count_card.votes; checksum -= &count_card.votes;
@ -508,7 +554,8 @@ where
// Update loss by fraction // Update loss by fraction
state.loss_fraction.transfer(&-checksum); state.loss_fraction.transfer(&-checksum);
if opts.exclusion != "one_round" { if let ExclusionMethod::SingleStage = opts.exclusion {
} else {
state.logger.log_literal("Exclusion complete.".to_string()); state.logger.log_literal("Exclusion complete.".to_string());
} }
} }

View File

@ -30,6 +30,7 @@ extern "C" {
#[wasm_bindgen(js_namespace = console)] #[wasm_bindgen(js_namespace = console)]
fn log(s: &str); fn log(s: &str);
} }
/// println! to console
macro_rules! cprintln { macro_rules! cprintln {
($($t:tt)*) => ( ($($t:tt)*) => (
#[allow(unused_unsafe)] #[allow(unused_unsafe)]
@ -55,14 +56,14 @@ macro_rules! impl_type {
#[wasm_bindgen] #[wasm_bindgen]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) { pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) {
stv::count_init(&mut state.0, &opts.0); stv::count_init(&mut state.0, &opts);
} }
#[wasm_bindgen] #[wasm_bindgen]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &STVOptions) -> bool { pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) -> bool {
return stv::count_one_stage(&mut state.0, &opts.0); return stv::count_one_stage(&mut state.0, &opts);
} }
// Reporting // Reporting
@ -154,8 +155,9 @@ fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>) {
cprintln!(""); cprintln!("");
} }
/*
#[wasm_bindgen] #[wasm_bindgen]
pub struct STVOptions(stv::STVOptions<'static>); pub struct STVOptions(stv::STVOptions);
#[wasm_bindgen] #[wasm_bindgen]
impl STVOptions { impl STVOptions {
pub fn new(round_votes: Option<usize>, exclusion: String) -> Self { pub fn new(round_votes: Option<usize>, exclusion: String) -> Self {
@ -163,9 +165,11 @@ impl STVOptions {
return STVOptions(stv::STVOptions { return STVOptions(stv::STVOptions {
round_votes: round_votes, round_votes: round_votes,
exclusion: &"one_round", exclusion: &"one_round",
pp_decimals: 2,
}); });
} else { } else {
panic!("Unknown --exclusion"); panic!("Unknown --exclusion");
} }
} }
} }
*/

View File

@ -55,7 +55,9 @@ fn aec_tas19_rational() {
// Initialise options // Initialise options
let stv_opts = stv::STVOptions { let stv_opts = stv::STVOptions {
round_votes: Some(0), round_votes: Some(0),
exclusion: "by_value", surplus: stv::SurplusMethod::UIG,
exclusion: stv::ExclusionMethod::ByValue,
pp_decimals: 2,
}; };
// Initialise count state // Initialise count state