Implement --transferable-only

This commit is contained in:
RunasSudo 2021-05-31 23:17:21 +10:00
parent c114d3a4ee
commit 76d69913c7
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 88 additions and 19 deletions

View File

@ -45,41 +45,52 @@ enum Command {
#[derive(Clap)] #[derive(Clap)]
#[clap(setting=AppSettings::DeriveDisplayOrder)] #[clap(setting=AppSettings::DeriveDisplayOrder)]
struct STV { struct STV {
// ----------------
// -- File input -- // -- File input --
/// Path to the BLT file to be counted /// Path to the BLT file to be counted
filename: String, filename: String,
// ----------------------
// -- Numbers settings -- // -- Numbers settings --
/// Numbers mode /// Numbers mode
#[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "float64"], default_value="rational", value_name="mode")] #[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "float64"], default_value="rational", value_name="mode")]
numbers: String, numbers: String,
// -----------------------
// -- Rounding settings -- // -- Rounding settings --
/// Round votes to specified decimal places /// Round votes to specified decimal places
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
round_votes: Option<usize>, round_votes: Option<usize>,
// ------------------
// -- STV variants -- // -- STV variants --
/// Method of surplus transfers /// Method of surplus transfers
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")] #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
surplus: String, surplus: String,
/// Examine only transferable papers during surplus distributions
#[clap(help_heading=Some("STV VARIANTS"), long)]
transferable_only: bool,
/// Method of exclusions /// Method of exclusions
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "parcels_by_order"], default_value="single_stage", value_name="method")] #[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 --
/// Hide excluded candidates from results report /// Hide excluded candidates from results report
#[clap(help_heading=Some("DISPLAY"), long)] #[clap(help_heading=Some("DISPLAY"), long)]
hide_excluded: bool, hide_excluded: bool,
/// Sort candidates by votes in results report /// Sort candidates by votes in results report
#[clap(help_heading=Some("DISPLAY"), long)] #[clap(help_heading=Some("DISPLAY"), long)]
sort_votes: bool, sort_votes: bool,
/// Print votes to specified decimal places in results report /// Print votes to specified decimal places in results report
#[clap(help_heading=Some("DISPLAY"), long, default_value="2", value_name="dps")] #[clap(help_heading=Some("DISPLAY"), long, default_value="2", value_name="dps")]
pp_decimals: usize, pp_decimals: usize,
@ -120,6 +131,7 @@ where
"meek" => stv::SurplusMethod::Meek, "meek" => stv::SurplusMethod::Meek,
_ => panic!("Invalid --surplus"), _ => panic!("Invalid --surplus"),
}, },
transferable_only: cmd_opts.transferable_only,
exclusion: match cmd_opts.exclusion.as_str() { exclusion: match cmd_opts.exclusion.as_str() {
"single_stage" => stv::ExclusionMethod::SingleStage, "single_stage" => stv::ExclusionMethod::SingleStage,
"by_value" => stv::ExclusionMethod::ByValue, "by_value" => stv::ExclusionMethod::ByValue,

View File

@ -32,6 +32,7 @@ use std::ops;
pub struct STVOptions { pub struct STVOptions {
pub round_votes: Option<usize>, pub round_votes: Option<usize>,
pub surplus: SurplusMethod, pub surplus: SurplusMethod,
pub transferable_only: bool,
pub exclusion: ExclusionMethod, pub exclusion: ExclusionMethod,
pub pp_decimals: usize, pub pp_decimals: usize,
} }
@ -265,6 +266,45 @@ where
return false; return false;
} }
/// Return the denominator of the transfer value
fn calculate_transfer_denom<N: Number>(surplus: &N, result: &NextPreferencesResult<N>, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option<N>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>
{
if transferable_only {
let total_units = if weighted { &result.total_votes } else { &result.total_ballots };
let exhausted_units = if weighted { &result.exhausted.num_votes } else { &result.exhausted.num_ballots };
let transferable_units = total_units - exhausted_units;
if transferable_votes > surplus {
return Some(transferable_units);
} else {
return None;
}
} else {
if weighted {
return Some(result.total_votes.clone());
} else {
return Some(result.total_ballots.clone());
}
}
}
fn reweight_vote<N: Number>(num_votes: &N, num_ballots: &N, surplus: &N, weighted: bool, transfer_denom: &Option<N>) -> N {
match transfer_denom {
Some(v) => {
if weighted {
return num_votes.clone() * surplus / v;
} else {
return num_ballots.clone() * surplus / v;
}
}
None => {
return num_votes.clone();
}
}
}
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate) fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
where where
for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Sub<&'r N, Output=N>,
@ -292,23 +332,29 @@ where
// Count next preferences // Count next preferences
let result = next_preferences(state, votes); let result = next_preferences(state, 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
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);
// Transfer candidate votes
// TODO: Refactor??
let is_weighted = match opts.surplus {
SurplusMethod::WIG => { true }
SurplusMethod::UIG | SurplusMethod::EG => { false }
SurplusMethod::Meek => { todo!() }
};
let transferable_votes = &result.total_votes - &result.exhausted.num_votes;
let transfer_denom = calculate_transfer_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only);
let transfer_value;
match transfer_denom {
Some(ref v) => {
transfer_value = surplus.clone() / v;
state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value, dps=opts.pp_decimals)); state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value, dps=opts.pp_decimals));
}
None => {
state.logger.log_literal(format!("Surplus of {} distributed at values received.", elected_candidate.name));
}
}
let mut checksum = N::new(); let mut checksum = N::new();
@ -317,14 +363,13 @@ where
// Reweight votes // Reweight votes
for vote in parcel.iter_mut() { for vote in parcel.iter_mut() {
//vote.value = vote.ballot.orig_value.clone() * &transfer_value; vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &transfer_denom);
vote.value = vote.ballot.orig_value.clone() * &surplus / &result.total_ballots;
} }
let count_card = state.candidates.get_mut(candidate).unwrap(); let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.push(parcel); count_card.parcels.push(parcel);
let mut candidate_transfers = entry.num_ballots * &surplus / &result.total_ballots; let mut candidate_transfers = reweight_vote(&entry.num_votes, &entry.num_ballots, &surplus, is_weighted, &transfer_denom);
// Round transfers // Round transfers
if let Some(dps) = opts.round_votes { if let Some(dps) = opts.round_votes {
candidate_transfers.floor_mut(dps); candidate_transfers.floor_mut(dps);
@ -337,7 +382,18 @@ where
let parcel = result.exhausted.votes as Parcel<N>; let parcel = result.exhausted.votes as Parcel<N>;
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
let mut exhausted_transfers = result.exhausted.num_ballots * &surplus / &result.total_ballots; let mut exhausted_transfers;
if opts.transferable_only {
if transferable_votes > surplus {
// No ballots exhaust
exhausted_transfers = N::new();
} else {
exhausted_transfers = &surplus - &transferable_votes;
}
} else {
exhausted_transfers = reweight_vote(&result.exhausted.num_votes, &result.exhausted.num_ballots, &surplus, is_weighted, &transfer_denom);
}
if let Some(dps) = opts.round_votes { if let Some(dps) = opts.round_votes {
exhausted_transfers.floor_mut(dps); exhausted_transfers.floor_mut(dps);
} }

View File

@ -56,6 +56,7 @@ fn aec_tas19_rational() {
let stv_opts = stv::STVOptions { let stv_opts = stv::STVOptions {
round_votes: Some(0), round_votes: Some(0),
surplus: stv::SurplusMethod::UIG, surplus: stv::SurplusMethod::UIG,
transferable_only: false,
exclusion: stv::ExclusionMethod::ByValue, exclusion: stv::ExclusionMethod::ByValue,
pp_decimals: 2, pp_decimals: 2,
}; };