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 --
|
// -- 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
|
||||||
|
|
151
src/stv/mod.rs
151
src/stv/mod.rs
|
@ -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;
|
||||||
|
|
||||||
// Inclusive Gregory
|
let votes;
|
||||||
// TODO: Other methods
|
match opts.surplus {
|
||||||
let votes = state.candidates.get(elected_candidate).unwrap().parcels.concat();
|
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
|
// Count next preferences
|
||||||
let result = next_preferences(state, votes);
|
let result = next_preferences(state, votes);
|
||||||
|
|
||||||
// Transfer candidate votes
|
// Transfer candidate votes
|
||||||
// Unweighted inclusive Gregory
|
let transfer_value;
|
||||||
// TODO: Other methods
|
match opts.surplus {
|
||||||
let transfer_value = surplus.clone() / &result.total_ballots;
|
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.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,39 +456,42 @@ 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 {
|
||||||
// Exclude in one round
|
ExclusionMethod::SingleStage => {
|
||||||
votes = count_card.parcels.concat();
|
// Exclude in one round
|
||||||
votes_remaining = 0;
|
votes = count_card.parcels.concat();
|
||||||
|
votes_remain = false;
|
||||||
} else if opts.exclusion == "by_value" {
|
}
|
||||||
// Exclude by value
|
ExclusionMethod::ByValue => {
|
||||||
let all_votes = count_card.parcels.concat();
|
// 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();
|
// 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();
|
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() {
|
// This could be implemented using Vec.drain_filter, but that is experimental currently
|
||||||
if &vote.value / &vote.ballot.orig_value == min_value {
|
for vote in all_votes.into_iter() {
|
||||||
votes.push(vote);
|
if &vote.value / &vote.ballot.orig_value == min_value {
|
||||||
} else {
|
votes.push(vote);
|
||||||
remaining_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
votes_remaining = remaining_votes.len();
|
|
||||||
// Leave remaining votes with candidate (as one parcel)
|
|
||||||
count_card.parcels = vec![remaining_votes];
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// TODO: Exclude by parcel
|
|
||||||
panic!("Invalid --exclusion");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue