Implement --defer-surpluses
This commit is contained in:
parent
08cb03d85a
commit
79f0f55942
@ -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,
|
||||
);
|
||||
|
||||
|
128
src/stv/mod.rs
128
src/stv/mod.rs
@ -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];
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,7 @@ fn aec_tas19_rational() {
|
||||
transferable_only: false,
|
||||
exclusion: stv::ExclusionMethod::ByValue,
|
||||
bulk_exclude: true,
|
||||
defer_surpluses: false,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user