Implement --bulk-exclude

This commit is contained in:
RunasSudo 2021-06-08 22:22:43 +10:00
parent d50af1161e
commit 08cb03d85a
Signed by: RunasSudo
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")]
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,
);

View File

@ -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<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOption
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
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<N: Number>(state: &mut CountState<N>) -> bool {
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
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<N: Number>(state: &mut CountState<N>, 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<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
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 {

View File

@ -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,
};

View File

@ -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::<Rational>("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts);