Implement --bulk-exclude

This commit is contained in:
RunasSudo 2021-06-08 22:22:43 +10:00
parent d50af1161e
commit 08cb03d85a
No known key found for this signature in database
GPG Key ID: 7234E476BF21C61A
4 changed files with 134 additions and 47 deletions

View File

@ -111,6 +111,12 @@ struct STV {
#[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,
// -------------------------
// -- Count optimisations --
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
bulk_exclude: bool,
// ---------------------- // ----------------------
// -- Display settings -- // -- Display settings --
@ -169,6 +175,7 @@ where
&cmd_opts.surplus_order, &cmd_opts.surplus_order,
cmd_opts.transferable_only, cmd_opts.transferable_only,
&cmd_opts.exclusion, &cmd_opts.exclusion,
cmd_opts.bulk_exclude,
cmd_opts.pp_decimals, cmd_opts.pp_decimals,
); );

View File

@ -42,6 +42,7 @@ pub struct STVOptions {
pub surplus_order: SurplusOrder, pub surplus_order: SurplusOrder,
pub transferable_only: bool, pub transferable_only: bool,
pub exclusion: ExclusionMethod, pub exclusion: ExclusionMethod,
pub bulk_exclude: bool,
pub pp_decimals: usize, pub pp_decimals: usize,
} }
@ -59,6 +60,7 @@ impl STVOptions {
surplus_order: &str, surplus_order: &str,
transferable_only: bool, transferable_only: bool,
exclusion: &str, exclusion: &str,
bulk_exclude: bool,
pp_decimals: usize, pp_decimals: usize,
) -> Self { ) -> Self {
return STVOptions { return STVOptions {
@ -102,6 +104,7 @@ impl STVOptions {
"parcels_by_order" => ExclusionMethod::ParcelsByOrder, "parcels_by_order" => ExclusionMethod::ParcelsByOrder,
_ => panic!("Invalid --exclusion"), _ => panic!("Invalid --exclusion"),
}, },
bulk_exclude,
pp_decimals, pp_decimals,
}; };
} }
@ -246,7 +249,7 @@ pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOption
elect_meeting_quota(&mut state, opts); elect_meeting_quota(&mut state, opts);
} }
pub fn count_one_stage<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) -> bool pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, 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>,
for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>,
@ -779,7 +782,7 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
return false; return false;
} }
fn exclude_hopefuls<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
where where
for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>,
{ {
@ -791,23 +794,62 @@ 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());
// Exclude lowest ranked candidate let mut excluded_candidates: Vec<&Candidate> = Vec::new();
let excluded_candidate = hopefuls.first().unwrap().0;
// 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;
}
}
// Exclude lowest ranked candidate
if excluded_candidates.len() == 0 {
excluded_candidates = vec![&hopefuls.first().unwrap().0];
}
let mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
names.sort();
state.kind = Some("Exclusion of"); state.kind = Some("Exclusion of");
state.title = String::from(&excluded_candidate.name); state.title = names.join(", ");
state.logger.log_smart( state.logger.log_smart(
"No surpluses to distribute, so {} is excluded.", "No surpluses to distribute, so {} is excluded.",
"No surpluses to distribute, so {} are excluded.", "No surpluses to distribute, so {} are excluded.",
vec![&excluded_candidate.name] names
); );
exclude_candidate(state, opts, excluded_candidate); exclude_candidates(state, opts, excluded_candidates);
return true; return true;
} }
fn continue_exclusion<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
where where
for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>,
{ {
@ -819,72 +861,103 @@ where
if excluded_with_votes.len() > 0 { if excluded_with_votes.len() > 0 {
excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap());
let excluded_candidate = excluded_with_votes.first().unwrap().0;
let order_excluded = excluded_with_votes.first().unwrap().1.order_elected;
let excluded_candidates: Vec<&Candidate> = excluded_with_votes.into_iter()
.filter(|(_, cc)| cc.order_elected == order_excluded)
.map(|(c, _)| *c)
.collect();
let mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
names.sort();
state.kind = Some("Exclusion of"); state.kind = Some("Exclusion of");
state.title = String::from(&excluded_candidate.name); state.title = names.join(", ");
state.logger.log_smart( state.logger.log_smart(
"Continuing exclusion of {}.", "Continuing exclusion of {}.",
"Continuing exclusion of {}.", "Continuing exclusion of {}.",
vec![&excluded_candidate.name] names
); );
exclude_candidate(state, opts, excluded_candidate); exclude_candidates(state, opts, excluded_candidates);
return true; return true;
} }
return false; return false;
} }
fn exclude_candidate<N: Number>(state: &mut CountState<N>, opts: &STVOptions, excluded_candidate: &Candidate) fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
where where
for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>,
{ {
let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); // Used to give bulk excluded candidate the same order_elected
let order_excluded = state.num_excluded + 1;
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??! for excluded_candidate in excluded_candidates.iter() {
if count_card.state != CandidateState::EXCLUDED { let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
count_card.state = CandidateState::EXCLUDED;
state.num_excluded += 1; // Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
count_card.order_elected = -(state.num_excluded as isize); if count_card.state != CandidateState::EXCLUDED {
count_card.state = CandidateState::EXCLUDED;
state.num_excluded += 1;
count_card.order_elected = -(order_excluded as isize);
}
} }
// Determine votes to transfer in this stage // Determine votes to transfer in this stage
let mut votes; let mut votes = Vec::new();
let votes_remain; let mut votes_remain;
match opts.exclusion { match opts.exclusion {
ExclusionMethod::SingleStage => { ExclusionMethod::SingleStage => {
// Exclude in one round // Exclude in one round
votes = count_card.parcels.concat(); for excluded_candidate in excluded_candidates.iter() {
count_card.parcels.clear(); let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
votes.append(&mut count_card.parcels.concat());
//count_card.parcels.clear();
}
votes_remain = false; votes_remain = false;
} }
ExclusionMethod::ByValue => { ExclusionMethod::ByValue => {
// Exclude by value // Exclude by value
let all_votes = count_card.parcels.concat(); let max_value = excluded_candidates.iter()
.map(|c| state.candidates.get(c).unwrap().parcels.iter()
.map(|p| p.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap())
.max().unwrap())
.max().unwrap();
// TODO: Write a multiple min/max function votes_remain = false;
let min_value = all_votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap();
votes = Vec::new(); for excluded_candidate in excluded_candidates.iter() {
let mut remaining_votes = Vec::new(); let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
// This could be implemented using Vec.drain_filter, but that is experimental currently // Filter out just those votes with max_value
for vote in all_votes.into_iter() { let mut remaining_votes = Vec::new();
if &vote.value / &vote.ballot.orig_value == min_value {
votes.push(vote); let cand_votes = count_card.parcels.concat();
} else {
remaining_votes.push(vote); for vote in cand_votes.into_iter() {
if &vote.value / &vote.ballot.orig_value == max_value {
votes.push(vote);
} else {
remaining_votes.push(vote);
}
} }
}
votes_remain = remaining_votes.len() > 0; if remaining_votes.len() > 0 {
// Leave remaining votes with candidate (as one parcel) votes_remain = true;
count_card.parcels = vec![remaining_votes]; }
// Leave remaining votes with candidate (as one parcel)
count_card.parcels = vec![remaining_votes];
}
} }
ExclusionMethod::ParcelsByOrder => { ExclusionMethod::ParcelsByOrder => {
// Exclude by parcel by order // Exclude by parcel by order
if excluded_candidates.len() > 1 {
panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude");
}
let count_card = state.candidates.get_mut(excluded_candidates.first().unwrap()).unwrap();
votes = count_card.parcels.remove(0); votes = count_card.parcels.remove(0);
votes_remain = count_card.parcels.len() > 0; votes_remain = count_card.parcels.len() > 0;
} }
@ -931,19 +1004,24 @@ where
checksum += exhausted_transfers; checksum += exhausted_transfers;
if votes_remain { if votes_remain {
// Subtract from candidate tally if excluded_candidates.len() == 1 {
let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); // TODO: Handle >1 excluded candidate
checksum -= &result.total_votes; // Subtract from candidate tally
count_card.transfer(&-result.total_votes); let count_card = state.candidates.get_mut(excluded_candidates.first().unwrap()).unwrap();
checksum -= &result.total_votes;
count_card.transfer(&-result.total_votes);
}
} }
} }
if !votes_remain { if !votes_remain {
// Finalise candidate votes // Finalise candidate votes
let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); for excluded_candidate in excluded_candidates.into_iter() {
checksum -= &count_card.votes; let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
count_card.transfers = -count_card.votes.clone(); checksum -= &count_card.votes;
count_card.votes = N::new(); count_card.transfers = -count_card.votes.clone();
count_card.votes = N::new();
}
if let ExclusionMethod::SingleStage = opts.exclusion { if let ExclusionMethod::SingleStage = opts.exclusion {
} else { } else {

View File

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

View File

@ -34,6 +34,7 @@ fn prsa1_rational() {
surplus_order: stv::SurplusOrder::ByOrder, surplus_order: stv::SurplusOrder::ByOrder,
transferable_only: true, transferable_only: true,
exclusion: stv::ExclusionMethod::ParcelsByOrder, exclusion: stv::ExclusionMethod::ParcelsByOrder,
bulk_exclude: 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);