Implement --bulk-exclude
This commit is contained in:
parent
d50af1161e
commit
08cb03d85a
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
170
src/stv/mod.rs
170
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<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();
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
votes_remain = remaining_votes.len() > 0;
|
||||
// Leave remaining votes with candidate (as one parcel)
|
||||
count_card.parcels = vec![remaining_votes];
|
||||
if remaining_votes.len() > 0 {
|
||||
votes_remain = true;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue