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")]
|
#[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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
172
src/stv/mod.rs
172
src/stv/mod.rs
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 => {
|
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 {
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue