Implement --exclusion first_prefs_then_by_value

This commit is contained in:
RunasSudo 2023-02-07 21:12:26 +11:00
parent 0ee5fd3285
commit eaf864062d
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 95 additions and 47 deletions

View File

@ -120,6 +120,7 @@ When *Surplus method* is set to a Gregory method, this option controls how candi
* *Single stage* (default): When excluding candidate(s), transfer all their ballots in one stage.
* *By value*: When excluding candidate(s), transfer their ballots 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, no further ballots are transferred to that candidate.
* *FPV then by value*: When excluding candidate(s), transfer their first preference ballot papers in the first stage, then transfer ballot papers received on transfers as in *By value*.
* *By source*: When excluding candidate(s), transfer their ballots according to the candidate from which those ballots were received, 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 ballots 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.
* *Reset and re-iterate*: When excluding candidate(s), reset the count from the distribution of first preferences, disregarding the excluded candidates.

View File

@ -1,6 +1,6 @@
<!--
* OpenTally: Open-source election vote counting
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
* Copyright © 2021–2023 Lee Yingtong Li (RunasSudo)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -134,6 +134,7 @@
<select id="selExclusion">
<option value="single_stage" selected>Single stage</option>
<option value="by_value">By value</option>
<option value="first_prefs_then_by_value">FPV then by value</option>
<option value="by_source">By source</option>
<option value="parcels_by_order">By parcel (by order)</option>
<option value="reset_and_reiterate">Reset and re-iterate</option>

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting
* Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* Copyright © 20212023 Lee Yingtong Li (RunasSudo)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -126,7 +126,7 @@ pub struct SubcmdOptions {
subtract_nontransferable: bool,
/// (Gregory STV) Method of exclusions [default: single_stage] [possible values: single_stage, by_value, by_source, parcels_by_order, reset_and_reiterate]
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "by_source", "parcels_by_order", "wright", "reset_and_reiterate"], default_value="single_stage", value_name="method", hide_possible_values=true, hide_default_value=true)]
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "first_prefs_then_by_value", "by_source", "parcels_by_order", "wright", "reset_and_reiterate"], default_value="single_stage", value_name="method", hide_possible_values=true, hide_default_value=true)]
exclusion: String,
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion

View File

@ -447,7 +447,7 @@ where
}
votes_remain = false;
}
ExclusionMethod::ByValue => {
ExclusionMethod::ByValue | ExclusionMethod::FirstPreferencesThenByValue => {
// Exclude by value
let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter()
.filter(|c| { let cc = &state.candidates[*c]; !cc.finalised && !cc.parcels.is_empty() })
@ -456,55 +456,97 @@ where
if excluded_with_votes.is_empty() {
votes_remain = false;
} else {
// If candidates to exclude still having votes, select only those with the greatest value
let max_value = excluded_with_votes.iter()
.map(|c| state.candidates[*c].parcels.iter()
.map(|p| &p.value_fraction)
.max().unwrap())
.max().unwrap()
.clone();
votes_remain = false;
let mut votes = Vec::new();
for excluded_candidate in excluded_with_votes.iter() {
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap();
let mut cc_parcels = Vec::new();
cc_parcels.append(&mut count_card.parcels);
// Filter out just those votes with max_value
let mut remaining_parcels = Vec::new();
for mut parcel in cc_parcels {
if parcel.value_fraction == max_value {
count_card.ballot_transfers -= parcel.num_ballots();
let votes_transferred = parcel.num_votes();
votes.append(&mut parcel.votes);
// Update votes
checksum -= &votes_transferred;
count_card.transfer(&-votes_transferred);
} else {
remaining_parcels.push(parcel);
if opts.exclusion == ExclusionMethod::FirstPreferencesThenByValue
&& excluded_with_votes.iter().any(|c| state.candidates[*c].parcels.iter().any(|p| p.source_order == 0))
{
// If candidates to exclude still having votes, select only those with first preferences
for excluded_candidate in excluded_with_votes.iter() {
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap();
let mut cc_parcels = Vec::new();
cc_parcels.append(&mut count_card.parcels);
// Filter out just those first preferences
let mut remaining_parcels = Vec::new();
for mut parcel in cc_parcels {
if parcel.source_order == 0 {
count_card.ballot_transfers -= parcel.num_ballots();
let votes_transferred = parcel.num_votes();
votes.append(&mut parcel.votes);
// Update votes
checksum -= &votes_transferred;
count_card.transfer(&-votes_transferred);
} else {
remaining_parcels.push(parcel);
}
}
if !remaining_parcels.is_empty() {
votes_remain = true;
}
// Leave remaining votes with candidate
count_card.parcels = remaining_parcels;
}
if !remaining_parcels.is_empty() {
votes_remain = true;
// Group all votes of one value in single parcel
parcels.push(Parcel {
votes,
value_fraction: N::one(), // By definition, first preferences have value of 1
source_order: 0, // Set this later
});
} else {
// If candidates to exclude still having votes, select only those with the greatest value
let max_value = excluded_with_votes.iter()
.map(|c| state.candidates[*c].parcels.iter()
.map(|p| &p.value_fraction)
.max().unwrap())
.max().unwrap()
.clone();
for excluded_candidate in excluded_with_votes.iter() {
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap();
let mut cc_parcels = Vec::new();
cc_parcels.append(&mut count_card.parcels);
// Filter out just those votes with max_value
let mut remaining_parcels = Vec::new();
for mut parcel in cc_parcels {
if parcel.value_fraction == max_value {
count_card.ballot_transfers -= parcel.num_ballots();
let votes_transferred = parcel.num_votes();
votes.append(&mut parcel.votes);
// Update votes
checksum -= &votes_transferred;
count_card.transfer(&-votes_transferred);
} else {
remaining_parcels.push(parcel);
}
}
if !remaining_parcels.is_empty() {
votes_remain = true;
}
// Leave remaining votes with candidate
count_card.parcels = remaining_parcels;
}
// Leave remaining votes with candidate
count_card.parcels = remaining_parcels;
// Group all votes of one value in single parcel
parcels.push(Parcel {
votes,
value_fraction: max_value,
source_order: 0, // Set this later
});
}
// Group all votes of one value in single parcel
parcels.push(Parcel {
votes,
value_fraction: max_value,
source_order: 0, // source_order is unused in this mode
});
}
}
ExclusionMethod::BySource => {

View File

@ -1081,8 +1081,8 @@ where
}
}
}
ExclusionMethod::ByValue | ExclusionMethod::BySource | ExclusionMethod::ParcelsByOrder => {
// Exclusion in parts compatible only with Gregory method
ExclusionMethod::ByValue | ExclusionMethod::FirstPreferencesThenByValue | ExclusionMethod::BySource | ExclusionMethod::ParcelsByOrder => {
// Segmented exclusion compatible only with Gregory method
gregory::exclude_candidates(state, opts, excluded_candidates, complete_type);
}
ExclusionMethod::ResetAndReiterate => {

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting
* Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* Copyright © 20212023 Lee Yingtong Li (RunasSudo)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -526,6 +526,8 @@ pub enum ExclusionMethod {
SingleStage,
/// Transfer the ballot papers of an excluded candidate in descending order of accumulated transfer value
ByValue,
/// Transfer the first preferences for an excluded candidate, then ballot papers received on transfers in descending order of accumulated transfer value
FirstPreferencesThenByValue,
/// 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
@ -540,6 +542,7 @@ impl ExclusionMethod {
match self {
ExclusionMethod::SingleStage => "--exclusion single_stage",
ExclusionMethod::ByValue => "--exclusion by_value",
ExclusionMethod::FirstPreferencesThenByValue => "--exclusion first_prefs_then_by_value",
ExclusionMethod::BySource => "--exclusion by_source",
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
ExclusionMethod::ResetAndReiterate => "--exclusion reset_and_reiterate",
@ -552,6 +555,7 @@ impl<S: AsRef<str>> From<S> for ExclusionMethod {
match s.as_ref() {
"single_stage" => ExclusionMethod::SingleStage,
"by_value" => ExclusionMethod::ByValue,
"first_prefs_then_by_value" => ExclusionMethod::FirstPreferencesThenByValue,
"by_source" => ExclusionMethod::BySource,
"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
"reset_and_reiterate" => ExclusionMethod::ResetAndReiterate,