Implement --exclusion by_source
This commit is contained in:
parent
7f16090395
commit
f80875b583
|
@ -81,9 +81,10 @@ Other surplus transfer methods, such as non-fractional transfers (e.g. random sa
|
||||||
|
|
||||||
### Exclusion method (--exclusion)
|
### Exclusion method (--exclusion)
|
||||||
|
|
||||||
* *Exclude in one round* (default): When excluding candidate(s), transfer all their ballot papers in one stage.
|
* *Single stage* (default): When excluding candidate(s), transfer all their ballot papers in one stage.
|
||||||
* *Exclude by parcel (by order)*: When excluding a candidate, transfer their ballot papers one parcel at a time, in the order each was received. Each parcel forms a separate stage, i.e. if a transfer allows another candidate to meet the quota criterion, no further papers are transferred to that candidate. This option cannot be combined with bulk exclusion.
|
* *By value*: When excluding candidate(s), transfer their ballot papers in descending order of accumulated transfer value. Each transfer of all ballots of a certain transfer value forms a separate stage, i.e. if a transfer allows another candidate to meet the quota criterion, no further papers are transferred to that candidate.
|
||||||
* *Exclude by value*: When excluding candidate(s), transfer their ballot papers in descending order of accumulated transfer value. Each transfer of all ballots of a certain transfer value forms a separate stage.
|
* *By source*: When excluding candidate(s), transfer their ballot papers according to the candidate from which those papers were received, in the order received, i.e. in the order the transferring candidates were elected or excluded. Each transfer of all ballots received from a certain candidate forms a separate stage.
|
||||||
|
* *By parcel (by order)*: When excluding a candidate, transfer their ballot papers one parcel at a time, in the order each was received. Each parcel forms a separate stage. This option cannot be combined with bulk exclusion.
|
||||||
* *Wright method (re-iterate)*: When excluding candidate(s), reset the count from the distribution of first preferences, disregarding the excluded candidates.
|
* *Wright method (re-iterate)*: When excluding candidate(s), reset the count from the distribution of first preferences, disregarding the excluded candidates.
|
||||||
|
|
||||||
When *Surplus method* is set to *Meek method*, this setting is ignored, and the Meek method is instead applied.
|
When *Surplus method* is set to *Meek method*, this setting is ignored, and the Meek method is instead applied.
|
||||||
|
|
|
@ -114,6 +114,7 @@
|
||||||
<select id="selExclusion">
|
<select id="selExclusion">
|
||||||
<option value="single_stage" selected>Single stage</option>
|
<option value="single_stage" selected>Single stage</option>
|
||||||
<option value="by_value">By value</option>
|
<option value="by_value">By value</option>
|
||||||
|
<option value="by_source">By source</option>
|
||||||
<option value="parcels_by_order">By parcel (by order)</option>
|
<option value="parcels_by_order">By parcel (by order)</option>
|
||||||
<option value="wright">Wright method (re-iterate)</option>
|
<option value="wright">Wright method (re-iterate)</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -305,10 +305,25 @@ impl<'a, N: Number> CountCard<'a, N> {
|
||||||
//self.orig_votes = self.votes.clone();
|
//self.orig_votes = self.votes.clone();
|
||||||
self.transfers = N::new();
|
self.transfers = N::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty
|
||||||
|
pub fn concat_parcels(&mut self) -> Vec<Vote<'a, N>> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for parcel in self.parcels.iter_mut() {
|
||||||
|
result.append(&mut parcel.votes);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parcel of [Vote]s during a count
|
/// Parcel of [Vote]s during a count
|
||||||
pub type Parcel<'a, N> = Vec<Vote<'a, N>>;
|
#[derive(Clone)]
|
||||||
|
pub struct Parcel<'a, N> {
|
||||||
|
/// [Vote]s in this parcel
|
||||||
|
pub votes: Vec<Vote<'a, N>>,
|
||||||
|
/// Order for sorting with [opentally::stv::ExclusionMethod::BySource]
|
||||||
|
pub source_order: usize,
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents a [Ballot] with an associated value
|
/// Represents a [Ballot] with an associated value
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
|
@ -131,7 +131,7 @@ struct STV {
|
||||||
transferable_only: bool,
|
transferable_only: bool,
|
||||||
|
|
||||||
/// Method of exclusions
|
/// Method of exclusions
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")]
|
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "by_source", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")]
|
||||||
exclusion: String,
|
exclusion: String,
|
||||||
|
|
||||||
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
|
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
|
||||||
|
|
|
@ -39,14 +39,20 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
||||||
|
|
||||||
// Transfer candidate votes
|
// Transfer candidate votes
|
||||||
for (candidate, entry) in result.candidates.into_iter() {
|
for (candidate, entry) in result.candidates.into_iter() {
|
||||||
let parcel = entry.votes as Parcel<N>;
|
let parcel = Parcel {
|
||||||
|
votes: entry.votes,
|
||||||
|
source_order: 0,
|
||||||
|
};
|
||||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||||
count_card.parcels.push(parcel);
|
count_card.parcels.push(parcel);
|
||||||
count_card.transfer(&entry.num_votes);
|
count_card.transfer(&entry.num_votes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer exhausted votes
|
// Transfer exhausted votes
|
||||||
let parcel = result.exhausted.votes as Parcel<N>;
|
let parcel = Parcel {
|
||||||
|
votes: result.exhausted.votes,
|
||||||
|
source_order: 0,
|
||||||
|
};
|
||||||
state.exhausted.parcels.push(parcel);
|
state.exhausted.parcels.push(parcel);
|
||||||
state.exhausted.transfer(&result.exhausted.num_votes);
|
state.exhausted.transfer(&result.exhausted.num_votes);
|
||||||
|
|
||||||
|
@ -66,7 +72,7 @@ where
|
||||||
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
||||||
.filter(|c| {
|
.filter(|c| {
|
||||||
let cc = &state.candidates[c];
|
let cc = &state.candidates[c];
|
||||||
&cc.votes > quota && cc.parcels.iter().any(|p| !p.is_empty())
|
&cc.votes > quota && cc.parcels.iter().any(|p| !p.votes.is_empty())
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -232,12 +238,12 @@ where
|
||||||
match opts.surplus {
|
match opts.surplus {
|
||||||
SurplusMethod::WIG | SurplusMethod::UIG => {
|
SurplusMethod::WIG | SurplusMethod::UIG => {
|
||||||
// Inclusive Gregory
|
// Inclusive Gregory
|
||||||
votes = count_card.parcels.concat();
|
votes = state.candidates.get_mut(elected_candidate).unwrap().concat_parcels();
|
||||||
}
|
}
|
||||||
SurplusMethod::EG => {
|
SurplusMethod::EG => {
|
||||||
// Exclusive Gregory
|
// Exclusive Gregory
|
||||||
// Should be safe to unwrap() - or else how did we get a quota!
|
// Should be safe to unwrap() - or else how did we get a quota!
|
||||||
votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap();
|
votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap().votes;
|
||||||
}
|
}
|
||||||
_ => { panic!("Invalid --surplus for Gregory method"); }
|
_ => { panic!("Invalid --surplus for Gregory method"); }
|
||||||
}
|
}
|
||||||
|
@ -302,10 +308,13 @@ where
|
||||||
count_card.transfer(&candidate_transfers);
|
count_card.transfer(&candidate_transfers);
|
||||||
checksum += candidate_transfers;
|
checksum += candidate_transfers;
|
||||||
|
|
||||||
let mut parcel = entry.votes as Parcel<N>;
|
let mut parcel = Parcel {
|
||||||
|
votes: entry.votes,
|
||||||
|
source_order: state.num_elected + state.num_excluded,
|
||||||
|
};
|
||||||
|
|
||||||
// Reweight votes
|
// Reweight votes
|
||||||
for vote in parcel.iter_mut() {
|
for vote in parcel.votes.iter_mut() {
|
||||||
vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_tvs, opts.round_weights);
|
vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_tvs, opts.round_weights);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,7 +342,10 @@ where
|
||||||
checksum += exhausted_transfers;
|
checksum += exhausted_transfers;
|
||||||
|
|
||||||
// Transfer exhausted votes
|
// Transfer exhausted votes
|
||||||
let parcel = result.exhausted.votes as Parcel<N>;
|
let parcel = Parcel {
|
||||||
|
votes: result.exhausted.votes,
|
||||||
|
source_order: state.num_elected + state.num_excluded,
|
||||||
|
};
|
||||||
state.exhausted.parcels.push(parcel);
|
state.exhausted.parcels.push(parcel);
|
||||||
|
|
||||||
// Finalise candidate votes
|
// Finalise candidate votes
|
||||||
|
@ -379,7 +391,7 @@ where
|
||||||
// Exclude in one round
|
// Exclude in one round
|
||||||
for excluded_candidate in excluded_candidates.iter() {
|
for excluded_candidate in excluded_candidates.iter() {
|
||||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||||
votes.append(&mut count_card.parcels.concat());
|
votes.append(&mut count_card.concat_parcels());
|
||||||
count_card.parcels.clear();
|
count_card.parcels.clear();
|
||||||
|
|
||||||
// Update votes
|
// Update votes
|
||||||
|
@ -399,7 +411,7 @@ where
|
||||||
// If candidates to exclude still having votes, select only those with the greatest value
|
// If candidates to exclude still having votes, select only those with the greatest value
|
||||||
let max_value = excluded_with_votes.iter()
|
let max_value = excluded_with_votes.iter()
|
||||||
.map(|c| state.candidates[*c].parcels.iter()
|
.map(|c| state.candidates[*c].parcels.iter()
|
||||||
.map(|p| p.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap())
|
.map(|p| p.votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap())
|
||||||
.max().unwrap())
|
.max().unwrap())
|
||||||
.max().unwrap();
|
.max().unwrap();
|
||||||
|
|
||||||
|
@ -411,7 +423,7 @@ where
|
||||||
// Filter out just those votes with max_value
|
// Filter out just those votes with max_value
|
||||||
let mut remaining_votes = Vec::new();
|
let mut remaining_votes = Vec::new();
|
||||||
|
|
||||||
let cand_votes = count_card.parcels.concat();
|
let cand_votes = count_card.concat_parcels();
|
||||||
|
|
||||||
let mut votes_transferred = N::new();
|
let mut votes_transferred = N::new();
|
||||||
for vote in cand_votes.into_iter() {
|
for vote in cand_votes.into_iter() {
|
||||||
|
@ -428,7 +440,58 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leave remaining votes with candidate (as one parcel)
|
// Leave remaining votes with candidate (as one parcel)
|
||||||
count_card.parcels = vec![remaining_votes];
|
count_card.parcels = vec![Parcel {
|
||||||
|
votes: remaining_votes,
|
||||||
|
source_order: 0, // Unused in this mode
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Update votes
|
||||||
|
checksum -= &votes_transferred;
|
||||||
|
count_card.transfer(&-votes_transferred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExclusionMethod::BySource => {
|
||||||
|
// Exclude by source candidate
|
||||||
|
let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter().filter(|c| !state.candidates[*c].parcels.is_empty()).collect();
|
||||||
|
|
||||||
|
if excluded_with_votes.is_empty() {
|
||||||
|
votes_remain = false;
|
||||||
|
} else {
|
||||||
|
// If candidates to exclude still having votes, select only those from the earliest elected/excluded source candidate
|
||||||
|
let min_order = excluded_with_votes.iter()
|
||||||
|
.map(|c| state.candidates[*c].parcels.iter()
|
||||||
|
.map(|p| p.source_order)
|
||||||
|
.min().unwrap())
|
||||||
|
.min().unwrap();
|
||||||
|
|
||||||
|
votes_remain = false;
|
||||||
|
|
||||||
|
for excluded_candidate in excluded_with_votes.iter() {
|
||||||
|
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap();
|
||||||
|
|
||||||
|
// Filter out just those votes with min_order
|
||||||
|
let mut remaining_parcels = Vec::new();
|
||||||
|
|
||||||
|
let mut votes_transferred = N::new();
|
||||||
|
while !count_card.parcels.is_empty() {
|
||||||
|
let parcel = count_card.parcels.pop().unwrap();
|
||||||
|
if parcel.source_order == min_order {
|
||||||
|
for vote in parcel.votes {
|
||||||
|
votes_transferred += &vote.value;
|
||||||
|
votes.push(vote);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remaining_parcels.push(parcel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !remaining_parcels.is_empty() {
|
||||||
|
votes_remain = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave remaining parcels with candidate
|
||||||
|
count_card.parcels = remaining_parcels;
|
||||||
|
|
||||||
// Update votes
|
// Update votes
|
||||||
checksum -= &votes_transferred;
|
checksum -= &votes_transferred;
|
||||||
|
@ -439,6 +502,7 @@ where
|
||||||
ExclusionMethod::ParcelsByOrder => {
|
ExclusionMethod::ParcelsByOrder => {
|
||||||
// Exclude by parcel by order
|
// Exclude by parcel by order
|
||||||
if excluded_candidates.len() > 1 {
|
if excluded_candidates.len() > 1 {
|
||||||
|
// TODO: We can probably support this actually
|
||||||
panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude");
|
panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,7 +511,7 @@ where
|
||||||
if count_card.parcels.is_empty() {
|
if count_card.parcels.is_empty() {
|
||||||
votes_remain = false;
|
votes_remain = false;
|
||||||
} else {
|
} else {
|
||||||
votes = count_card.parcels.remove(0);
|
votes = count_card.parcels.remove(0).votes;
|
||||||
votes_remain = !count_card.parcels.is_empty();
|
votes_remain = !count_card.parcels.is_empty();
|
||||||
|
|
||||||
// Update votes
|
// Update votes
|
||||||
|
@ -481,7 +545,10 @@ where
|
||||||
|
|
||||||
// Transfer candidate votes
|
// Transfer candidate votes
|
||||||
for (candidate, entry) in result.candidates.into_iter() {
|
for (candidate, entry) in result.candidates.into_iter() {
|
||||||
let parcel = entry.votes as Parcel<N>;
|
let parcel = Parcel {
|
||||||
|
votes: entry.votes,
|
||||||
|
source_order: state.num_elected + state.num_excluded,
|
||||||
|
};
|
||||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||||
count_card.parcels.push(parcel);
|
count_card.parcels.push(parcel);
|
||||||
|
|
||||||
|
@ -495,7 +562,10 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer exhausted votes
|
// Transfer exhausted votes
|
||||||
let parcel = result.exhausted.votes as Parcel<N>;
|
let parcel = Parcel {
|
||||||
|
votes: result.exhausted.votes,
|
||||||
|
source_order: state.num_elected + state.num_excluded,
|
||||||
|
};
|
||||||
state.exhausted.parcels.push(parcel);
|
state.exhausted.parcels.push(parcel);
|
||||||
|
|
||||||
let mut exhausted_transfers = result.exhausted.num_votes;
|
let mut exhausted_transfers = result.exhausted.num_votes;
|
||||||
|
|
|
@ -170,6 +170,7 @@ impl STVOptions {
|
||||||
exclusion: match exclusion {
|
exclusion: match exclusion {
|
||||||
"single_stage" => ExclusionMethod::SingleStage,
|
"single_stage" => ExclusionMethod::SingleStage,
|
||||||
"by_value" => ExclusionMethod::ByValue,
|
"by_value" => ExclusionMethod::ByValue,
|
||||||
|
"by_source" => ExclusionMethod::BySource,
|
||||||
"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
|
"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
|
||||||
"wright" => ExclusionMethod::Wright,
|
"wright" => ExclusionMethod::Wright,
|
||||||
_ => panic!("Invalid --exclusion"),
|
_ => panic!("Invalid --exclusion"),
|
||||||
|
@ -388,6 +389,8 @@ pub enum ExclusionMethod {
|
||||||
SingleStage,
|
SingleStage,
|
||||||
/// Transfer the ballot papers of an excluded candidate in descending order of accumulated transfer value
|
/// Transfer the ballot papers of an excluded candidate in descending order of accumulated transfer value
|
||||||
ByValue,
|
ByValue,
|
||||||
|
/// Transfer the ballot papers of an excluded candidate according to the candidate who transferred the papers to the excluded candidate, in the order the transferring candidates were elected or excluded
|
||||||
|
BySource,
|
||||||
/// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received
|
/// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received
|
||||||
ParcelsByOrder,
|
ParcelsByOrder,
|
||||||
/// Wright method (re-iterate)
|
/// Wright method (re-iterate)
|
||||||
|
@ -400,6 +403,7 @@ impl ExclusionMethod {
|
||||||
match self {
|
match self {
|
||||||
ExclusionMethod::SingleStage => "--exclusion single_stage",
|
ExclusionMethod::SingleStage => "--exclusion single_stage",
|
||||||
ExclusionMethod::ByValue => "--exclusion by_value",
|
ExclusionMethod::ByValue => "--exclusion by_value",
|
||||||
|
ExclusionMethod::BySource => "--exclusion by_source",
|
||||||
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
|
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
|
||||||
ExclusionMethod::Wright => "--exclusion wright",
|
ExclusionMethod::Wright => "--exclusion wright",
|
||||||
}.to_string()
|
}.to_string()
|
||||||
|
@ -1040,7 +1044,7 @@ where
|
||||||
{
|
{
|
||||||
// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
|
// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
|
||||||
let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||||||
.filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| !p.is_empty()))
|
.filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| !p.votes.is_empty()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !excluded_with_votes.is_empty() {
|
if !excluded_with_votes.is_empty() {
|
||||||
|
@ -1086,7 +1090,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ExclusionMethod::ByValue | ExclusionMethod::ParcelsByOrder => {
|
ExclusionMethod::ByValue | ExclusionMethod::BySource | ExclusionMethod::ParcelsByOrder => {
|
||||||
// Exclusion in parts compatible only with Gregory method
|
// Exclusion in parts compatible only with Gregory method
|
||||||
gregory::exclude_candidates(state, opts, excluded_candidates);
|
gregory::exclude_candidates(state, opts, excluded_candidates);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue