From 08cb03d85a61487680aa60ac212683a30121aa92 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 8 Jun 2021 22:22:43 +1000 Subject: [PATCH] Implement --bulk-exclude --- src/main.rs | 7 ++ src/stv/mod.rs | 172 +++++++++++++++++++++++++++++++++++-------------- tests/aec.rs | 1 + tests/prsa.rs | 1 + 4 files changed, 134 insertions(+), 47 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2447bf5..f63da0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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")] exclusion: String, + // ------------------------- + // -- Count optimisations -- + + #[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)] + bulk_exclude: bool, + // ---------------------- // -- Display settings -- @@ -169,6 +175,7 @@ where &cmd_opts.surplus_order, cmd_opts.transferable_only, &cmd_opts.exclusion, + cmd_opts.bulk_exclude, cmd_opts.pp_decimals, ); diff --git a/src/stv/mod.rs b/src/stv/mod.rs index ed5a5ce..b9b4888 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -42,6 +42,7 @@ pub struct STVOptions { pub surplus_order: SurplusOrder, pub transferable_only: bool, pub exclusion: ExclusionMethod, + pub bulk_exclude: bool, pub pp_decimals: usize, } @@ -59,6 +60,7 @@ impl STVOptions { surplus_order: &str, transferable_only: bool, exclusion: &str, + bulk_exclude: bool, pp_decimals: usize, ) -> Self { return STVOptions { @@ -102,6 +104,7 @@ impl STVOptions { "parcels_by_order" => ExclusionMethod::ParcelsByOrder, _ => panic!("Invalid --exclusion"), }, + bulk_exclude, pp_decimals, }; } @@ -246,7 +249,7 @@ pub fn count_init(mut state: &mut CountState<'_, N>, opts: &STVOption elect_meeting_quota(&mut state, opts); } -pub fn count_one_stage(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 for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, @@ -779,7 +782,7 @@ fn bulk_elect(state: &mut CountState) -> bool { return false; } -fn exclude_hopefuls(state: &mut CountState, opts: &STVOptions) -> bool +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>, { @@ -791,23 +794,62 @@ where // TODO: Handle ties hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); - // Exclude lowest ranked candidate - let excluded_candidate = hopefuls.first().unwrap().0; + 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; + } + } + + // 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.title = String::from(&excluded_candidate.name); + state.title = names.join(", "); state.logger.log_smart( "No surpluses to distribute, so {} is 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; } -fn continue_exclusion(state: &mut CountState, opts: &STVOptions) -> bool +fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool where for<'r> &'r N: ops::Div<&'r N, Output=N>, { @@ -819,72 +861,103 @@ where 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()); - 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.title = String::from(&excluded_candidate.name); + state.title = names.join(", "); state.logger.log_smart( "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 false; } -fn exclude_candidate(state: &mut CountState, opts: &STVOptions, excluded_candidate: &Candidate) +fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) where 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 ??! - if count_card.state != CandidateState::EXCLUDED { - count_card.state = CandidateState::EXCLUDED; - state.num_excluded += 1; - count_card.order_elected = -(state.num_excluded as isize); + for excluded_candidate in excluded_candidates.iter() { + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + + // Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??! + 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 - let mut votes; - let votes_remain; + let mut votes = Vec::new(); + let mut votes_remain; match opts.exclusion { ExclusionMethod::SingleStage => { // Exclude in one round - votes = count_card.parcels.concat(); - count_card.parcels.clear(); + for excluded_candidate in excluded_candidates.iter() { + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + votes.append(&mut count_card.parcels.concat()); + //count_card.parcels.clear(); + } votes_remain = false; } ExclusionMethod::ByValue => { // 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 - let min_value = all_votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap(); + votes_remain = false; - votes = Vec::new(); - let mut remaining_votes = Vec::new(); - - // This could be implemented using Vec.drain_filter, but that is experimental currently - for vote in all_votes.into_iter() { - if &vote.value / &vote.ballot.orig_value == min_value { - votes.push(vote); - } else { - remaining_votes.push(vote); + for excluded_candidate in excluded_candidates.iter() { + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + + // Filter out just those votes with max_value + let mut remaining_votes = Vec::new(); + + let cand_votes = count_card.parcels.concat(); + + for vote in cand_votes.into_iter() { + if &vote.value / &vote.ballot.orig_value == max_value { + votes.push(vote); + } else { + remaining_votes.push(vote); + } } + + if remaining_votes.len() > 0 { + votes_remain = true; + } + + // Leave remaining votes with candidate (as one parcel) + count_card.parcels = vec![remaining_votes]; } - - votes_remain = remaining_votes.len() > 0; - // Leave remaining votes with candidate (as one parcel) - count_card.parcels = vec![remaining_votes]; } ExclusionMethod::ParcelsByOrder => { // 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_remain = count_card.parcels.len() > 0; } @@ -931,19 +1004,24 @@ where checksum += exhausted_transfers; if votes_remain { - // Subtract from candidate tally - let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); - checksum -= &result.total_votes; - count_card.transfer(&-result.total_votes); + if excluded_candidates.len() == 1 { + // TODO: Handle >1 excluded candidate + // Subtract from candidate tally + 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 { // Finalise candidate votes - let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); - checksum -= &count_card.votes; - count_card.transfers = -count_card.votes.clone(); - count_card.votes = N::new(); + for excluded_candidate in excluded_candidates.into_iter() { + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + checksum -= &count_card.votes; + count_card.transfers = -count_card.votes.clone(); + count_card.votes = N::new(); + } if let ExclusionMethod::SingleStage = opts.exclusion { } else { diff --git a/tests/aec.rs b/tests/aec.rs index 3226669..fa7d94b 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -66,6 +66,7 @@ fn aec_tas19_rational() { surplus_order: stv::SurplusOrder::ByOrder, transferable_only: false, exclusion: stv::ExclusionMethod::ByValue, + bulk_exclude: true, pp_decimals: 2, }; diff --git a/tests/prsa.rs b/tests/prsa.rs index 63594e5..9b2dddb 100644 --- a/tests/prsa.rs +++ b/tests/prsa.rs @@ -34,6 +34,7 @@ fn prsa1_rational() { surplus_order: stv::SurplusOrder::ByOrder, transferable_only: true, exclusion: stv::ExclusionMethod::ParcelsByOrder, + bulk_exclude: false, pp_decimals: 2, }; utils::read_validate_election::("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts);