Implement --defer-surpluses
This commit is contained in:
parent
08cb03d85a
commit
79f0f55942
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
126
src/stv/mod.rs
126
src/stv/mod.rs
@ -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,43 +835,60 @@ 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 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();
|
let mut excluded_candidates: Vec<&Candidate> = Vec::new();
|
||||||
|
|
||||||
// Attempt a bulk exclusion
|
// Attempt a bulk exclusion
|
||||||
if opts.bulk_exclude {
|
if opts.bulk_exclude {
|
||||||
let total_surpluses = state.candidates.iter()
|
excluded_candidates = hopefuls_to_bulk_exclude(state, opts);
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user