Implement --defer-surpluses

This commit is contained in:
RunasSudo 2021-06-09 12:16:25 +10:00
parent 08cb03d85a
commit 79f0f55942
No known key found for this signature in database
GPG Key ID: 7234E476BF21C61A
4 changed files with 102 additions and 36 deletions

View File

@ -96,7 +96,7 @@ struct STV {
// ------------------ // ------------------
// -- STV variants -- // -- STV variants --
/// Method of surplus transfers /// Method of surplus distributions
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")] #[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
surplus: String, surplus: String,
@ -114,9 +114,14 @@ struct STV {
// ------------------------- // -------------------------
// -- Count optimisations -- // -- Count optimisations --
/// Use bulk exclusion
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)] #[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
bulk_exclude: bool, bulk_exclude: bool,
/// Defer surplus distributions if possible
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
defer_surpluses: bool,
// ---------------------- // ----------------------
// -- Display settings -- // -- Display settings --
@ -176,6 +181,7 @@ where
cmd_opts.transferable_only, cmd_opts.transferable_only,
&cmd_opts.exclusion, &cmd_opts.exclusion,
cmd_opts.bulk_exclude, cmd_opts.bulk_exclude,
cmd_opts.defer_surpluses,
cmd_opts.pp_decimals, cmd_opts.pp_decimals,
); );

View File

@ -43,6 +43,7 @@ pub struct STVOptions {
pub transferable_only: bool, pub transferable_only: bool,
pub exclusion: ExclusionMethod, pub exclusion: ExclusionMethod,
pub bulk_exclude: bool, pub bulk_exclude: bool,
pub defer_surpluses: bool,
pub pp_decimals: usize, pub pp_decimals: usize,
} }
@ -61,6 +62,7 @@ impl STVOptions {
transferable_only: bool, transferable_only: bool,
exclusion: &str, exclusion: &str,
bulk_exclude: bool, bulk_exclude: bool,
defer_surpluses: bool,
pp_decimals: usize, pp_decimals: usize,
) -> Self { ) -> Self {
return STVOptions { return STVOptions {
@ -105,6 +107,7 @@ impl STVOptions {
_ => panic!("Invalid --exclusion"), _ => panic!("Invalid --exclusion"),
}, },
bulk_exclude, bulk_exclude,
defer_surpluses,
pp_decimals, pp_decimals,
}; };
} }
@ -465,7 +468,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
// Calculate total active vote // Calculate total active vote
let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| { let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
match cc.state { match cc.state {
CandidateState::ELECTED => { acc + &cc.votes - state.quota.as_ref().unwrap() } CandidateState::ELECTED => { if &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
_ => { acc + &cc.votes } _ => { acc + &cc.votes }
} }
}); });
@ -541,11 +544,40 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
if opts.quota_mode == QuotaMode::ERS97 { if opts.quota_mode == QuotaMode::ERS97 {
// Repeat in case vote required for election has changed // Repeat in case vote required for election has changed
//calculate_quota(state, opts);
elect_meeting_quota(state, opts); elect_meeting_quota(state, opts);
} }
} }
} }
fn can_defer_surpluses<N: Number>(state: &CountState<N>, opts: &STVOptions, has_surplus: &Vec<(&&Candidate, &CountCard<N>)>, total_surpluses: &N) -> bool
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>
{
// Do not defer if this could change the last 2 candidates
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL || cc.state == CandidateState::GUARDED)
.collect();
hopefuls.sort_unstable_by(|(_, cc1), (_, cc2)| cc1.votes.cmp(&cc2.votes));
if total_surpluses > &(&hopefuls[1].1.votes - &hopefuls[0].1.votes) {
return false;
}
// Do not defer if this could affect a bulk exclusion
if opts.bulk_exclude {
let to_exclude = hopefuls_to_bulk_exclude(state, opts);
let num_to_exclude = to_exclude.len();
if num_to_exclude > 0 {
let total_excluded = to_exclude.into_iter()
.fold(N::new(), |acc, c| acc + &state.candidates.get(c).unwrap().votes);
if total_surpluses > &(&hopefuls[num_to_exclude + 1].1.votes - &total_excluded) {
return false;
}
}
}
return true;
}
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool
where where
for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Sub<&'r N, Output=N>,
@ -555,8 +587,18 @@ where
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter() let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| &cc.votes > quota) .filter(|(_, cc)| &cc.votes > quota)
.collect(); .collect();
let total_surpluses = has_surplus.iter()
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
if has_surplus.len() > 0 { if has_surplus.len() > 0 {
// Determine if surplues can be deferred
if opts.defer_surpluses {
if can_defer_surpluses(state, opts, &has_surplus, &total_surpluses) {
state.logger.log_literal(format!("Distribution of surpluses totalling {:.2} votes will be deferred.", total_surpluses));
return false;
}
}
match opts.surplus_order { match opts.surplus_order {
SurplusOrder::BySize => { SurplusOrder::BySize => {
// Compare b with a to sort high-low // Compare b with a to sort high-low
@ -782,10 +824,9 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
return false; return false;
} }
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> {
where let mut excluded_candidates = Vec::new();
for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter() let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
.collect(); .collect();
@ -794,10 +835,6 @@ where
// TODO: Handle ties // TODO: Handle ties
hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
let mut excluded_candidates: Vec<&Candidate> = Vec::new();
// Attempt a bulk exclusion
if opts.bulk_exclude {
let total_surpluses = state.candidates.iter() let total_surpluses = state.candidates.iter()
.filter(|(_, cc)| &cc.votes > state.quota.as_ref().unwrap()) .filter(|(_, cc)| &cc.votes > state.quota.as_ref().unwrap())
.fold(N::new(), |agg, (_, cc)| agg + &cc.votes - state.quota.as_ref().unwrap()); .fold(N::new(), |agg, (_, cc)| agg + &cc.votes - state.quota.as_ref().unwrap());
@ -823,14 +860,35 @@ where
} }
for (c, _) in try_exclude.into_iter() { for (c, _) in try_exclude.into_iter() {
excluded_candidates.push(c); excluded_candidates.push(**c);
} }
break; break;
} }
return excluded_candidates;
}
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
where
for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
let mut excluded_candidates: Vec<&Candidate> = Vec::new();
// Attempt a bulk exclusion
if opts.bulk_exclude {
excluded_candidates = hopefuls_to_bulk_exclude(state, opts);
} }
// Exclude lowest ranked candidate // Exclude lowest ranked candidate
if excluded_candidates.len() == 0 { if excluded_candidates.len() == 0 {
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
.collect();
// Sort by votes
// TODO: Handle ties
hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
excluded_candidates = vec![&hopefuls.first().unwrap().0]; excluded_candidates = vec![&hopefuls.first().unwrap().0];
} }

View File

@ -67,6 +67,7 @@ fn aec_tas19_rational() {
transferable_only: false, transferable_only: false,
exclusion: stv::ExclusionMethod::ByValue, exclusion: stv::ExclusionMethod::ByValue,
bulk_exclude: true, bulk_exclude: true,
defer_surpluses: false,
pp_decimals: 2, pp_decimals: 2,
}; };

View File

@ -35,6 +35,7 @@ fn prsa1_rational() {
transferable_only: true, transferable_only: true,
exclusion: stv::ExclusionMethod::ParcelsByOrder, exclusion: stv::ExclusionMethod::ParcelsByOrder,
bulk_exclude: false, bulk_exclude: false,
defer_surpluses: false,
pp_decimals: 2, pp_decimals: 2,
}; };
utils::read_validate_election::<Rational>("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts); utils::read_validate_election::<Rational>("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts);