Implement --surplus and --exclusion
This commit is contained in:
parent
2428fcb4ed
commit
c114d3a4ee
21
src/main.rs
21
src/main.rs
|
@ -64,8 +64,12 @@ struct STV {
|
|||
|
||||
// -- 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
|
||||
#[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,
|
||||
|
||||
// -- Display settings --
|
||||
|
@ -109,7 +113,20 @@ where
|
|||
// Copy applicable options
|
||||
let stv_opts = stv::STVOptions {
|
||||
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
|
||||
|
|
147
src/stv/mod.rs
147
src/stv/mod.rs
|
@ -23,17 +23,39 @@ pub mod wasm;
|
|||
use crate::numbers::Number;
|
||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ops;
|
||||
|
||||
pub struct STVOptions<'a> {
|
||||
#[wasm_bindgen]
|
||||
pub struct STVOptions {
|
||||
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);
|
||||
calculate_quota(&mut state);
|
||||
calculate_quota(&mut state, opts);
|
||||
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());
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Calculate the total vote
|
||||
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
|
||||
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
|
||||
state.quota += N::one();
|
||||
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);
|
||||
}
|
||||
|
@ -251,21 +273,42 @@ where
|
|||
let count_card = state.candidates.get(elected_candidate).unwrap();
|
||||
let surplus = &count_card.votes - &state.quota;
|
||||
|
||||
// Inclusive Gregory
|
||||
// TODO: Other methods
|
||||
let votes = state.candidates.get(elected_candidate).unwrap().parcels.concat();
|
||||
let votes;
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG => {
|
||||
// Inclusive Gregory
|
||||
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
|
||||
let result = next_preferences(state, votes);
|
||||
|
||||
// Transfer candidate votes
|
||||
// Unweighted inclusive Gregory
|
||||
// TODO: Other methods
|
||||
let transfer_value = surplus.clone() / &result.total_ballots;
|
||||
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
|
||||
transfer_value = surplus.clone() / &result.total_ballots;
|
||||
}
|
||||
SurplusMethod::Meek => { todo!(); }
|
||||
}
|
||||
|
||||
state.kind = Some("Surplus of");
|
||||
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();
|
||||
|
||||
|
@ -413,39 +456,42 @@ where
|
|||
|
||||
// Determine votes to transfer in this stage
|
||||
let mut votes;
|
||||
let votes_remaining;
|
||||
let votes_remain;
|
||||
|
||||
if opts.exclusion == "one_round" {
|
||||
// Exclude in one round
|
||||
votes = count_card.parcels.concat();
|
||||
votes_remaining = 0;
|
||||
|
||||
} else if opts.exclusion == "by_value" {
|
||||
// Exclude by value
|
||||
let all_votes = count_card.parcels.concat();
|
||||
|
||||
// TODO: Write a multiple min/max function
|
||||
let min_value = all_votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap();
|
||||
|
||||
votes = Vec::new();
|
||||
let mut remaining_votes = Vec::new();
|
||||
|
||||
// This could be implemented using Vec.drain_filter, but that is experimental currently
|
||||
for vote in all_votes.into_iter() {
|
||||
if &vote.value / &vote.ballot.orig_value == min_value {
|
||||
votes.push(vote);
|
||||
} else {
|
||||
remaining_votes.push(vote);
|
||||
}
|
||||
match opts.exclusion {
|
||||
ExclusionMethod::SingleStage => {
|
||||
// Exclude in one round
|
||||
votes = count_card.parcels.concat();
|
||||
votes_remain = false;
|
||||
}
|
||||
ExclusionMethod::ByValue => {
|
||||
// Exclude by value
|
||||
let all_votes = count_card.parcels.concat();
|
||||
|
||||
votes_remaining = remaining_votes.len();
|
||||
// Leave remaining votes with candidate (as one parcel)
|
||||
count_card.parcels = vec![remaining_votes];
|
||||
// TODO: Write a multiple min/max function
|
||||
let min_value = all_votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap();
|
||||
|
||||
} else {
|
||||
// TODO: Exclude by parcel
|
||||
panic!("Invalid --exclusion");
|
||||
votes = Vec::new();
|
||||
let mut remaining_votes = Vec::new();
|
||||
|
||||
// This could be implemented using Vec.drain_filter, but that is experimental currently
|
||||
for vote in all_votes.into_iter() {
|
||||
if &vote.value / &vote.ballot.orig_value == min_value {
|
||||
votes.push(vote);
|
||||
} else {
|
||||
remaining_votes.push(vote);
|
||||
}
|
||||
}
|
||||
|
||||
votes_remain = remaining_votes.len() > 0;
|
||||
// Leave remaining votes with candidate (as one parcel)
|
||||
count_card.parcels = vec![remaining_votes];
|
||||
}
|
||||
ExclusionMethod::ParcelsByOrder => {
|
||||
// Exclude by parcel by order
|
||||
votes = count_card.parcels.remove(0);
|
||||
votes_remain = count_card.parcels.len() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
let mut checksum = N::new();
|
||||
|
@ -456,10 +502,10 @@ where
|
|||
// Count next preferences
|
||||
let result = next_preferences(state, votes);
|
||||
|
||||
if opts.exclusion == "one_round" {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.2} votes.", result.total_ballots, result.total_votes));
|
||||
} else if opts.exclusion == "by_value" {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.2} votes, received at value {:.2}.", result.total_ballots, result.total_votes, value));
|
||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals));
|
||||
} else {
|
||||
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
|
||||
|
@ -488,7 +534,7 @@ where
|
|||
state.exhausted.transfer(&exhausted_transfers);
|
||||
checksum += exhausted_transfers;
|
||||
|
||||
if votes_remaining > 0 {
|
||||
if votes_remain {
|
||||
// Subtract from candidate tally
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
checksum -= &result.total_votes;
|
||||
|
@ -498,7 +544,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
if votes_remaining == 0 {
|
||||
if !votes_remain {
|
||||
// Finalise candidate votes
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
checksum -= &count_card.votes;
|
||||
|
@ -508,7 +554,8 @@ where
|
|||
// Update loss by fraction
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ extern "C" {
|
|||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
/// println! to console
|
||||
macro_rules! cprintln {
|
||||
($($t:tt)*) => (
|
||||
#[allow(unused_unsafe)]
|
||||
|
@ -55,14 +56,14 @@ macro_rules! impl_type {
|
|||
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) {
|
||||
stv::count_init(&mut state.0, &opts.0);
|
||||
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) {
|
||||
stv::count_init(&mut state.0, &opts);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &STVOptions) -> bool {
|
||||
return stv::count_one_stage(&mut state.0, &opts.0);
|
||||
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) -> bool {
|
||||
return stv::count_one_stage(&mut state.0, &opts);
|
||||
}
|
||||
|
||||
// Reporting
|
||||
|
@ -154,8 +155,9 @@ fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>) {
|
|||
cprintln!("");
|
||||
}
|
||||
|
||||
/*
|
||||
#[wasm_bindgen]
|
||||
pub struct STVOptions(stv::STVOptions<'static>);
|
||||
pub struct STVOptions(stv::STVOptions);
|
||||
#[wasm_bindgen]
|
||||
impl STVOptions {
|
||||
pub fn new(round_votes: Option<usize>, exclusion: String) -> Self {
|
||||
|
@ -163,9 +165,11 @@ impl STVOptions {
|
|||
return STVOptions(stv::STVOptions {
|
||||
round_votes: round_votes,
|
||||
exclusion: &"one_round",
|
||||
pp_decimals: 2,
|
||||
});
|
||||
} else {
|
||||
panic!("Unknown --exclusion");
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -55,7 +55,9 @@ fn aec_tas19_rational() {
|
|||
// Initialise options
|
||||
let stv_opts = stv::STVOptions {
|
||||
round_votes: Some(0),
|
||||
exclusion: "by_value",
|
||||
surplus: stv::SurplusMethod::UIG,
|
||||
exclusion: stv::ExclusionMethod::ByValue,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
|
||||
// Initialise count state
|
||||
|
|
Loading…
Reference in New Issue