Implement --defer-surpluses

This commit is contained in:
RunasSudo 2021-06-09 12:16:25 +10:00
parent 08cb03d85a
commit 79f0f55942
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 102 additions and 36 deletions

View File

@ -96,7 +96,7 @@ struct STV {
// ------------------
// -- 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")]
surplus: String,
@ -114,9 +114,14 @@ struct STV {
// -------------------------
// -- Count optimisations --
/// Use bulk exclusion
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
bulk_exclude: bool,
/// Defer surplus distributions if possible
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
defer_surpluses: bool,
// ----------------------
// -- Display settings --
@ -176,6 +181,7 @@ where
cmd_opts.transferable_only,
&cmd_opts.exclusion,
cmd_opts.bulk_exclude,
cmd_opts.defer_surpluses,
cmd_opts.pp_decimals,
);

View File

@ -43,6 +43,7 @@ pub struct STVOptions {
pub transferable_only: bool,
pub exclusion: ExclusionMethod,
pub bulk_exclude: bool,
pub defer_surpluses: bool,
pub pp_decimals: usize,
}
@ -61,6 +62,7 @@ impl STVOptions {
transferable_only: bool,
exclusion: &str,
bulk_exclude: bool,
defer_surpluses: bool,
pp_decimals: usize,
) -> Self {
return STVOptions {
@ -105,6 +107,7 @@ impl STVOptions {
_ => panic!("Invalid --exclusion"),
},
bulk_exclude,
defer_surpluses,
pp_decimals,
};
}
@ -465,7 +468,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
// Calculate total active vote
let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
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 }
}
});
@ -541,11 +544,40 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
if opts.quota_mode == QuotaMode::ERS97 {
// Repeat in case vote required for election has changed
//calculate_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
where
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()
.filter(|(_, cc)| &cc.votes > quota)
.collect();
let total_surpluses = has_surplus.iter()
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
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 {
SurplusOrder::BySize => {
// Compare b with a to sort high-low
@ -782,10 +824,9 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
return false;
}
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>,
{
fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> {
let mut excluded_candidates = Vec::new();
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
.collect();
@ -794,43 +835,60 @@ where
// TODO: Handle ties
hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
let total_surpluses = state.candidates.iter()
.filter(|(_, cc)| &cc.votes > state.quota.as_ref().unwrap())
.fold(N::new(), |agg, (_, cc)| agg + &cc.votes - state.quota.as_ref().unwrap());
// Attempt to exclude as many candidates as possible
for i in 0..hopefuls.len() {
let try_exclude = &hopefuls[0..hopefuls.len()-i];
// Do not exclude if this splits tied candidates
if i != 0 && try_exclude.last().unwrap().1.votes == hopefuls[hopefuls.len()-i].1.votes {
continue;
}
// Do not exclude if this leaves insufficient candidates
if state.num_elected + hopefuls.len() - try_exclude.len() < state.election.seats {
continue;
}
// Do not exclude if this could change the order of exclusion
let total_votes = try_exclude.into_iter().fold(N::new(), |agg, (_, cc)| agg + &cc.votes);
if i != 0 && total_votes + &total_surpluses > hopefuls[hopefuls.len()-i].1.votes {
continue;
}
for (c, _) in try_exclude.into_iter() {
excluded_candidates.push(**c);
}
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 {
let total_surpluses = state.candidates.iter()
.filter(|(_, cc)| &cc.votes > state.quota.as_ref().unwrap())
.fold(N::new(), |agg, (_, cc)| agg + &cc.votes - state.quota.as_ref().unwrap());
// Attempt to exclude as many candidates as possible
for i in 0..hopefuls.len() {
let try_exclude = &hopefuls[0..hopefuls.len()-i];
// Do not exclude if this splits tied candidates
if i != 0 && try_exclude.last().unwrap().1.votes == hopefuls[hopefuls.len()-i].1.votes {
continue;
}
// Do not exclude if this leaves insufficient candidates
if state.num_elected + hopefuls.len() - try_exclude.len() < state.election.seats {
continue;
}
// Do not exclude if this could change the order of exclusion
let total_votes = try_exclude.into_iter().fold(N::new(), |agg, (_, cc)| agg + &cc.votes);
if i != 0 && total_votes + &total_surpluses > hopefuls[hopefuls.len()-i].1.votes {
continue;
}
for (c, _) in try_exclude.into_iter() {
excluded_candidates.push(c);
}
break;
}
excluded_candidates = hopefuls_to_bulk_exclude(state, opts);
}
// Exclude lowest ranked candidate
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];
}

View File

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

View File

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