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 --
/// 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

View File

@ -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();
// 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);
}
}
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();
@ -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());
}
}

View File

@ -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");
}
}
}
*/

View File

@ -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