Implement Meek STV
This commit is contained in:
parent
f395e6f064
commit
4ebb6474fd
|
@ -37,9 +37,9 @@
|
|||
<select id="selPreset" onchange="changePreset()">
|
||||
<option value="wigm" selected>Recommended WIGM</option>
|
||||
<option value="scottish">Scottish STV</option>
|
||||
<option value="meek">Meek STV</option>
|
||||
<option value="senate">Australian Senate STV</option>
|
||||
<!--<option value="meek">Meek STV</option>
|
||||
<option value="wright">Wright STV</option>-->
|
||||
<!--<option value="wright">Wright STV</option>-->
|
||||
<option value="prsa77">PRSA 1977</option>
|
||||
<option value="ers97">ERS97</option>
|
||||
</select>
|
||||
|
|
|
@ -353,6 +353,27 @@ function changePreset() {
|
|||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'meek') {
|
||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkNormaliseBallots').checked = false;
|
||||
document.getElementById('chkRoundQuota').checked = false;
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundTVs').checked = false;
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selTransfers').value = 'meek';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'senate') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
|
|
|
@ -79,7 +79,7 @@ function resume_count() {
|
|||
}
|
||||
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)});
|
||||
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state)});
|
||||
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)});
|
||||
}
|
||||
|
||||
var user_input_buffer = null;
|
||||
|
|
|
@ -129,14 +129,16 @@ pub struct Candidate {
|
|||
}
|
||||
|
||||
/// The current state of counting an [Election]
|
||||
#[derive(Clone)]
|
||||
pub struct CountState<'a, N> {
|
||||
//#[derive(Clone)]
|
||||
pub struct CountState<'a, N: Number> {
|
||||
pub election: &'a Election<N>,
|
||||
|
||||
pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>,
|
||||
pub exhausted: CountCard<'a, N>,
|
||||
pub loss_fraction: CountCard<'a, N>,
|
||||
|
||||
pub ballot_tree: Option<crate::stv::meek::BallotTree<'a, N>>,
|
||||
|
||||
pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
||||
pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
||||
pub random: Option<SHARandom<'a>>,
|
||||
|
@ -160,6 +162,7 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
candidates: HashMap::new(),
|
||||
exhausted: CountCard::new(),
|
||||
loss_fraction: CountCard::new(),
|
||||
ballot_tree: None,
|
||||
forwards_tiebreak: None,
|
||||
backwards_tiebreak: None,
|
||||
random: None,
|
||||
|
@ -195,12 +198,12 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
|
||||
/// Represents either a reference to a [CountState] or a clone
|
||||
#[allow(dead_code)]
|
||||
pub enum CountStateOrRef<'a, N> {
|
||||
pub enum CountStateOrRef<'a, N: Number> {
|
||||
State(CountState<'a, N>), // NYI: May be used e.g. for tie-breaking or rollback-based constraints
|
||||
Ref(&'a CountState<'a, N>),
|
||||
}
|
||||
|
||||
impl<'a, N> CountStateOrRef<'a, N> {
|
||||
impl<'a, N: Number> CountStateOrRef<'a, N> {
|
||||
/// Construct a [CountStateOrRef] as a reference to a [CountState]
|
||||
pub fn from(state: &'a CountState<N>) -> Self {
|
||||
return Self::Ref(state);
|
||||
|
@ -216,7 +219,7 @@ impl<'a, N> CountStateOrRef<'a, N> {
|
|||
}
|
||||
|
||||
/// Result of a stage of counting
|
||||
pub struct StageResult<'a, N> {
|
||||
pub struct StageResult<'a, N: Number> {
|
||||
pub kind: Option<&'a str>,
|
||||
pub title: &'a String,
|
||||
pub logs: Vec<String>,
|
||||
|
@ -229,11 +232,14 @@ pub struct CountCard<'a, N> {
|
|||
pub state: CandidateState,
|
||||
pub order_elected: isize,
|
||||
|
||||
pub orig_votes: N,
|
||||
//pub orig_votes: N,
|
||||
pub transfers: N,
|
||||
pub votes: N,
|
||||
|
||||
pub parcels: Vec<Parcel<'a, N>>,
|
||||
|
||||
/// Candidate's keep value (Meek STV)
|
||||
pub keep_value: Option<N>,
|
||||
}
|
||||
|
||||
impl<'a, N: Number> CountCard<'a, N> {
|
||||
|
@ -242,10 +248,11 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||
return CountCard {
|
||||
state: CandidateState::Hopeful,
|
||||
order_elected: 0,
|
||||
orig_votes: N::new(),
|
||||
//orig_votes: N::new(),
|
||||
transfers: N::new(),
|
||||
votes: N::new(),
|
||||
parcels: Vec::new(),
|
||||
keep_value: None,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -255,9 +262,9 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||
self.votes += transfer;
|
||||
}
|
||||
|
||||
/// Set [orig_votes](CountCard::orig_votes) to [votes](CountCard::votes), and set [transfers](CountCard::transfers) to 0
|
||||
/// Set [transfers](CountCard::transfers) to 0
|
||||
pub fn step(&mut self) {
|
||||
self.orig_votes = self.votes.clone();
|
||||
//self.orig_votes = self.votes.clone();
|
||||
self.transfers = N::new();
|
||||
}
|
||||
}
|
||||
|
@ -270,6 +277,7 @@ pub type Parcel<'a, N> = Vec<Vote<'a, N>>;
|
|||
pub struct Vote<'a, N> {
|
||||
pub ballot: &'a Ballot<N>,
|
||||
pub value: N,
|
||||
/// Index of the next preference to examine
|
||||
pub up_to_pref: usize,
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ use opentally::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
|||
|
||||
use clap::{AppSettings, Clap};
|
||||
|
||||
use std::cmp::max;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead};
|
||||
use std::ops;
|
||||
|
@ -185,6 +186,7 @@ fn main() {
|
|||
fn count_election<N: Number>(mut election: Election<N>, cmd_opts: STV)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
|
@ -261,7 +263,11 @@ where
|
|||
fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(candidates: I, cmd_opts: &STV) {
|
||||
for (candidate, count_card) in candidates {
|
||||
if count_card.state == CandidateState::Elected {
|
||||
if let Some(kv) = &count_card.keep_value {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {} (kv = {:.dps2$})", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, kv, dps=cmd_opts.pp_decimals, dps2=max(cmd_opts.pp_decimals, 2));
|
||||
} else {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
} else if count_card.state == CandidateState::Excluded {
|
||||
// If --hide-excluded, hide unless nonzero votes or nonzero transfers
|
||||
if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
|
||||
|
|
|
@ -153,9 +153,7 @@ impl ops::Neg for Fixed {
|
|||
|
||||
impl ops::Add for Fixed {
|
||||
type Output = Self;
|
||||
fn add(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Sub for Fixed {
|
||||
|
@ -174,9 +172,7 @@ impl ops::Mul for Fixed {
|
|||
|
||||
impl ops::Div for Fixed {
|
||||
type Output = Self;
|
||||
fn div(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn div(self, rhs: Self) -> Self::Output { Self(self.0 * get_factor() / rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Rem for Fixed {
|
||||
|
@ -285,9 +281,7 @@ impl ops::Sub<Self> for &Fixed {
|
|||
|
||||
impl ops::Mul<Self> for &Fixed {
|
||||
type Output = Fixed;
|
||||
fn mul(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn mul(self, rhs: Self) -> Self::Output { Fixed(&self.0 * &rhs.0 / get_factor()) }
|
||||
}
|
||||
|
||||
impl ops::Div<Self> for &Fixed {
|
||||
|
|
|
@ -186,9 +186,7 @@ impl ops::Neg for GuardedFixed {
|
|||
|
||||
impl ops::Add for GuardedFixed {
|
||||
type Output = Self;
|
||||
fn add(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Sub for GuardedFixed {
|
||||
|
@ -207,9 +205,7 @@ impl ops::Mul for GuardedFixed {
|
|||
|
||||
impl ops::Div for GuardedFixed {
|
||||
type Output = Self;
|
||||
fn div(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn div(self, rhs: Self) -> Self::Output { Self(self.0 * get_factor() / rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Rem for GuardedFixed {
|
||||
|
@ -318,9 +314,7 @@ impl ops::Sub<Self> for &GuardedFixed {
|
|||
|
||||
impl ops::Mul<Self> for &GuardedFixed {
|
||||
type Output = GuardedFixed;
|
||||
fn mul(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn mul(self, rhs: Self) -> Self::Output { GuardedFixed(&self.0 * &rhs.0 / get_factor()) }
|
||||
}
|
||||
|
||||
impl ops::Div<Self> for &GuardedFixed {
|
||||
|
|
|
@ -97,9 +97,7 @@ impl ops::Neg for NativeFloat64 {
|
|||
|
||||
impl ops::Add for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn add(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Sub for NativeFloat64 {
|
||||
|
@ -118,9 +116,7 @@ impl ops::Mul for NativeFloat64 {
|
|||
|
||||
impl ops::Div for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn div(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Rem for NativeFloat64 {
|
||||
|
@ -220,9 +216,7 @@ impl ops::Sub<Self> for &NativeFloat64 {
|
|||
|
||||
impl ops::Mul<Self> for &NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn mul(self, _rhs: &NativeFloat64) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn mul(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 * &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Div<Self> for &NativeFloat64 {
|
||||
|
|
|
@ -144,9 +144,7 @@ impl ops::Neg for Rational {
|
|||
|
||||
impl ops::Add for Rational {
|
||||
type Output = Rational;
|
||||
fn add(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Sub for Rational {
|
||||
|
@ -165,9 +163,7 @@ impl ops::Mul for Rational {
|
|||
|
||||
impl ops::Div for Rational {
|
||||
type Output = Rational;
|
||||
fn div(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Rem for Rational {
|
||||
|
@ -267,9 +263,7 @@ impl ops::Sub<Self> for &Rational {
|
|||
|
||||
impl ops::Mul<Self> for &Rational {
|
||||
type Output = Rational;
|
||||
fn mul(self, _rhs: &Rational) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn mul(self, rhs: &Rational) -> Self::Output { Rational(&self.0 * &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Div<Self> for &Rational {
|
||||
|
|
|
@ -143,9 +143,7 @@ impl ops::Neg for Rational {
|
|||
|
||||
impl ops::Add for Rational {
|
||||
type Output = Self;
|
||||
fn add(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Sub for Rational {
|
||||
|
@ -164,9 +162,7 @@ impl ops::Mul for Rational {
|
|||
|
||||
impl ops::Div for Rational {
|
||||
type Output = Self;
|
||||
fn div(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Rem for Rational {
|
||||
|
@ -266,9 +262,7 @@ impl ops::Sub<Self> for &Rational {
|
|||
|
||||
impl ops::Mul<Self> for &Rational {
|
||||
type Output = Rational;
|
||||
fn mul(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn mul(self, rhs: Self) -> Self::Output { Rational(rug::Rational::from(&self.0 * &rhs.0)) }
|
||||
}
|
||||
|
||||
impl ops::Div<Self> for &Rational {
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::{NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
|
||||
use super::{ExclusionMethod, NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
|
||||
|
||||
use crate::election::{Candidate, CountCard, CountState, Parcel, Vote};
|
||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
|
||||
use crate::numbers::Number;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
@ -25,6 +25,34 @@ use itertools::Itertools;
|
|||
use std::cmp::max;
|
||||
use std::ops;
|
||||
|
||||
/// Distribute first preference votes according to the Gregory method
|
||||
pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
||||
let votes = state.election.ballots.iter().map(|b| Vote {
|
||||
ballot: b,
|
||||
value: b.orig_value.clone(),
|
||||
up_to_pref: 0,
|
||||
}).collect();
|
||||
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
// Transfer candidate votes
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
let parcel = entry.votes as Parcel<N>;
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
count_card.transfer(&entry.num_votes);
|
||||
}
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = result.exhausted.votes as Parcel<N>;
|
||||
state.exhausted.parcels.push(parcel);
|
||||
state.exhausted.transfer(&result.exhausted.num_votes);
|
||||
|
||||
state.kind = None;
|
||||
state.title = "First preferences".to_string();
|
||||
state.logger.log_literal("First preferences distributed.".to_string());
|
||||
}
|
||||
|
||||
/// Distribute the largest surplus according to the Gregory method, based on [STVOptions::surplus]
|
||||
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
|
@ -37,10 +65,11 @@ where
|
|||
.map(|c| (c, state.candidates.get(c).unwrap()))
|
||||
.filter(|(_, cc)| &cc.votes > quota)
|
||||
.collect();
|
||||
|
||||
if !has_surplus.is_empty() {
|
||||
let total_surpluses = has_surplus.iter()
|
||||
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
|
||||
|
||||
if !has_surplus.is_empty() {
|
||||
// Determine if surplues can be deferred
|
||||
if opts.defer_surpluses {
|
||||
if super::can_defer_surpluses(state, opts, &total_surpluses) {
|
||||
|
@ -311,3 +340,157 @@ where
|
|||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
|
||||
/// Perform one stage of a candidate exclusion according to the Gregory method, based on [STVOptions::exclusion]
|
||||
pub 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>,
|
||||
{
|
||||
// Used to give bulk excluded candidate the same order_elected
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
|
||||
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 = Vec::new();
|
||||
let mut votes_remain;
|
||||
let mut checksum = N::new();
|
||||
|
||||
match opts.exclusion {
|
||||
ExclusionMethod::SingleStage => {
|
||||
// Exclude in one round
|
||||
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();
|
||||
|
||||
// Update votes
|
||||
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
votes_remain = false;
|
||||
}
|
||||
ExclusionMethod::ByValue => {
|
||||
// Exclude by value
|
||||
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();
|
||||
|
||||
votes_remain = false;
|
||||
|
||||
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();
|
||||
|
||||
let mut votes_transferred = N::new();
|
||||
for vote in cand_votes.into_iter() {
|
||||
if &vote.value / &vote.ballot.orig_value == max_value {
|
||||
votes_transferred += &vote.value;
|
||||
votes.push(vote);
|
||||
} else {
|
||||
remaining_votes.push(vote);
|
||||
}
|
||||
}
|
||||
|
||||
if !remaining_votes.is_empty() {
|
||||
votes_remain = true;
|
||||
}
|
||||
|
||||
// Leave remaining votes with candidate (as one parcel)
|
||||
count_card.parcels = vec![remaining_votes];
|
||||
|
||||
// Update votes
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
}
|
||||
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[0]).unwrap();
|
||||
votes = count_card.parcels.remove(0);
|
||||
votes_remain = !count_card.parcels.is_empty();
|
||||
|
||||
// Update votes
|
||||
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
}
|
||||
|
||||
if !votes.is_empty() {
|
||||
let value = &votes[0].value / &votes[0].ballot.orig_value;
|
||||
|
||||
// Count next preferences
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
|
||||
// Transfer candidate votes
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
let parcel = entry.votes as Parcel<N>;
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
|
||||
// Round transfers
|
||||
let mut candidate_transfers = entry.num_votes;
|
||||
if let Some(dps) = opts.round_votes {
|
||||
candidate_transfers.floor_mut(dps);
|
||||
}
|
||||
count_card.transfer(&candidate_transfers);
|
||||
checksum += candidate_transfers;
|
||||
}
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = result.exhausted.votes as Parcel<N>;
|
||||
state.exhausted.parcels.push(parcel);
|
||||
|
||||
let mut exhausted_transfers = result.exhausted.num_votes;
|
||||
if let Some(dps) = opts.round_votes {
|
||||
exhausted_transfers.floor_mut(dps);
|
||||
}
|
||||
state.exhausted.transfer(&exhausted_transfers);
|
||||
checksum += exhausted_transfers;
|
||||
}
|
||||
|
||||
if !votes_remain {
|
||||
// Finalise candidate votes
|
||||
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;
|
||||
count_card.votes = N::new();
|
||||
}
|
||||
|
||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||
} else {
|
||||
state.logger.log_literal("Exclusion complete.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::{STVError, STVOptions};
|
||||
|
||||
use crate::election::{Ballot, Candidate, CandidateState, CountCard, CountState, Election};
|
||||
use crate::numbers::Number;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::HashMap;
|
||||
use std::ops;
|
||||
|
||||
/// Ballot in a [BallotTree]
|
||||
#[derive(Clone)]
|
||||
struct BallotInTree<'b, N: Number> {
|
||||
ballot: &'b Ballot<N>,
|
||||
/// Index of the next preference to examine
|
||||
up_to_pref: usize,
|
||||
}
|
||||
|
||||
/// Tree-packed ballot representation
|
||||
pub struct BallotTree<'t, N: Number> {
|
||||
num_ballots: N,
|
||||
ballots: Vec<BallotInTree<'t, N>>,
|
||||
next_preferences: Option<Box<HashMap<&'t Candidate, BallotTree<'t, N>>>>,
|
||||
next_exhausted: Option<Box<BallotTree<'t, N>>>,
|
||||
}
|
||||
|
||||
impl<'t, N: Number> BallotTree<'t, N> {
|
||||
/// Construct a new empty [BallotTree]
|
||||
fn new() -> Self {
|
||||
BallotTree {
|
||||
num_ballots: N::new(),
|
||||
ballots: Vec::new(),
|
||||
next_preferences: None,
|
||||
next_exhausted: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Descend one level of the [BallotTree]
|
||||
fn descend_tree(&mut self, candidates: &'t Vec<Candidate>) {
|
||||
let mut next_preferences: HashMap<&Candidate, BallotTree<N>> = HashMap::new();
|
||||
let mut next_exhausted = BallotTree::new();
|
||||
|
||||
for bit in self.ballots.iter() {
|
||||
if bit.up_to_pref < bit.ballot.preferences.len() {
|
||||
let candidate = &candidates[bit.ballot.preferences[bit.up_to_pref]];
|
||||
|
||||
if next_preferences.contains_key(candidate) {
|
||||
let np_bt = next_preferences.get_mut(candidate).unwrap();
|
||||
np_bt.num_ballots += &bit.ballot.orig_value;
|
||||
np_bt.ballots.push(BallotInTree {
|
||||
ballot: bit.ballot,
|
||||
up_to_pref: bit.up_to_pref + 1,
|
||||
});
|
||||
} else {
|
||||
let mut np_bt = BallotTree::new();
|
||||
np_bt.num_ballots += &bit.ballot.orig_value;
|
||||
np_bt.ballots.push(BallotInTree {
|
||||
ballot: bit.ballot,
|
||||
up_to_pref: bit.up_to_pref + 1,
|
||||
});
|
||||
next_preferences.insert(candidate, np_bt);
|
||||
}
|
||||
} else {
|
||||
// Exhausted
|
||||
next_exhausted.num_ballots += &bit.ballot.orig_value;
|
||||
next_exhausted.ballots.push(bit.clone());
|
||||
}
|
||||
}
|
||||
|
||||
self.next_preferences = Some(Box::new(next_preferences));
|
||||
self.next_exhausted = Some(Box::new(next_exhausted));
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialise keep values, ballot tree and distribute preferences
|
||||
pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
{
|
||||
// Initialise keep values
|
||||
for (_, count_card) in state.candidates.iter_mut() {
|
||||
count_card.keep_value = Some(N::one());
|
||||
}
|
||||
|
||||
// Initialise ballot tree
|
||||
let mut ballot_tree = BallotTree::new();
|
||||
for ballot in state.election.ballots.iter() {
|
||||
ballot_tree.ballots.push(BallotInTree {
|
||||
ballot: ballot,
|
||||
up_to_pref: 0,
|
||||
});
|
||||
ballot_tree.num_ballots += &ballot.orig_value;
|
||||
}
|
||||
state.ballot_tree = Some(ballot_tree);
|
||||
|
||||
// Distribute preferences
|
||||
distribute_preferences(state);
|
||||
|
||||
// Recalculate transfers
|
||||
for (_, count_card) in state.candidates.iter_mut() {
|
||||
count_card.transfers.assign(&count_card.votes);
|
||||
}
|
||||
state.exhausted.transfers.assign(&state.exhausted.votes);
|
||||
|
||||
state.kind = None;
|
||||
state.title = "First preferences".to_string();
|
||||
state.logger.log_literal("First preferences distributed.".to_string());
|
||||
}
|
||||
|
||||
/// (Re)distribute preferences according to candidate keep values
|
||||
pub fn distribute_preferences<N: Number>(state: &mut CountState<N>)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
{
|
||||
// Reset votes
|
||||
for (_, count_card) in state.candidates.iter_mut() {
|
||||
//count_card.orig_votes = N::new();
|
||||
//count_card.transfers = N::new();
|
||||
count_card.votes = N::new();
|
||||
}
|
||||
state.exhausted.votes = N::new();
|
||||
|
||||
distribute_recursively(&mut state.candidates, &mut state.exhausted, state.ballot_tree.as_mut().unwrap(), N::one(), &state.election);
|
||||
}
|
||||
|
||||
/// Distribute preferences recursively
|
||||
///
|
||||
/// Called by [distribute_preferences]
|
||||
fn distribute_recursively<'t, N: Number>(candidates: &mut HashMap<&'t Candidate, CountCard<N>>, exhausted: &mut CountCard<N>, tree: &mut BallotTree<'t, N>, remaining_multiplier: N, election: &'t Election<N>)
|
||||
where
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
{
|
||||
// Descend tree if required
|
||||
if let None = tree.next_exhausted {
|
||||
tree.descend_tree(&election.candidates);
|
||||
}
|
||||
|
||||
// FIXME: Possibility of infinite loop if malformed inputs?
|
||||
|
||||
// TODO: Round transfers?
|
||||
|
||||
// Credit votes at this level
|
||||
for (candidate, cand_tree) in tree.next_preferences.as_mut().unwrap().as_mut().iter_mut() {
|
||||
let count_card = candidates.get_mut(candidate).unwrap();
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful | CandidateState::Guarded | CandidateState::Doomed => {
|
||||
// Hopeful candidate has keep value 1, so transfer entire remaining value
|
||||
count_card.votes += &remaining_multiplier * &cand_tree.num_ballots;
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
// Transfer according to elected candidate's keep value
|
||||
count_card.votes += &remaining_multiplier * &cand_tree.num_ballots * count_card.keep_value.as_ref().unwrap();
|
||||
let new_remaining_multiplier = &remaining_multiplier * &(N::one() - count_card.keep_value.as_ref().unwrap());
|
||||
// Recurse
|
||||
distribute_recursively(candidates, exhausted, cand_tree, new_remaining_multiplier, election);
|
||||
}
|
||||
CandidateState::Excluded | CandidateState::Withdrawn => {
|
||||
// Excluded candidate has keep value 0, so skip over this candidate
|
||||
// Recurse
|
||||
distribute_recursively(candidates, exhausted, cand_tree, remaining_multiplier.clone(), election);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Credit exhausted votes at this level
|
||||
exhausted.votes += &remaining_multiplier * &tree.next_exhausted.as_ref().unwrap().as_ref().num_ballots;
|
||||
}
|
||||
|
||||
/// Recalculate all candidate keep factors to distribute all surpluses according to the Meek method
|
||||
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
// TODO: Make configurable
|
||||
let quota_tolerance = N::one() / N::from(100000) + N::one();
|
||||
|
||||
let quota = state.quota.as_ref().unwrap();
|
||||
let mut has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
||||
.filter(|c| {
|
||||
let count_card = state.candidates.get(c).unwrap();
|
||||
return count_card.state == CandidateState::Elected && (&count_card.votes / quota > quota_tolerance);
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !has_surplus.is_empty() {
|
||||
// TODO: Defer surpluses?
|
||||
|
||||
let orig_candidates = state.candidates.clone();
|
||||
let orig_exhausted = state.exhausted.clone();
|
||||
|
||||
let mut num_iterations: u32 = 0;
|
||||
|
||||
while !has_surplus.is_empty() {
|
||||
num_iterations += 1;
|
||||
|
||||
// Recompute keep values
|
||||
for candidate in has_surplus.into_iter() {
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.keep_value = Some(count_card.keep_value.take().unwrap() * state.quota.as_ref().unwrap() / &count_card.votes);
|
||||
}
|
||||
|
||||
// Redistribute votes
|
||||
distribute_preferences(state);
|
||||
|
||||
// Recompute quota if more ballots have become exhausted
|
||||
super::calculate_quota(state, opts);
|
||||
|
||||
//println!("Debug {}", num_iterations);
|
||||
|
||||
let quota = state.quota.as_ref().unwrap();
|
||||
has_surplus = state.election.candidates.iter()
|
||||
.filter(|c| {
|
||||
let count_card = state.candidates.get(c).unwrap();
|
||||
return count_card.state == CandidateState::Elected && (&count_card.votes / quota > quota_tolerance);
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
// Recalculate transfers
|
||||
for (candidate, count_card) in state.candidates.iter_mut() {
|
||||
count_card.transfers = &count_card.votes - &orig_candidates.get(candidate).unwrap().votes;
|
||||
}
|
||||
state.exhausted.transfers = &state.exhausted.votes - &orig_exhausted.votes;
|
||||
|
||||
// Remove intermediate logs on quota calculation
|
||||
state.logger.entries.clear();
|
||||
|
||||
state.kind = None;
|
||||
state.title = "Surpluses distributed".to_string();
|
||||
if num_iterations == 1 {
|
||||
state.logger.log_literal("Surpluses distributed, requiring 1 iteration.".to_string());
|
||||
} else {
|
||||
state.logger.log_literal(format!("Surpluses distributed, requiring {} iterations.", num_iterations));
|
||||
}
|
||||
|
||||
let kv_str = state.election.candidates.iter()
|
||||
.map(|c| (c, state.candidates.get(c).unwrap()))
|
||||
.filter(|(_, cc)| cc.state == CandidateState::Elected)
|
||||
.sorted_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected))
|
||||
.map(|(c, cc)| format!("{} ({:.dps2$})", c.name, cc.keep_value.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2)))
|
||||
.join(", ");
|
||||
|
||||
state.logger.log_literal(format!("Keep values of elected candidates are: {}.", kv_str));
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
/// Exclude the given candidates according to the Meek method
|
||||
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, _opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
{
|
||||
// Used to give bulk excluded candidate the same order_elected
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
|
||||
for excluded_candidate in excluded_candidates.into_iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
|
||||
if count_card.state != CandidateState::Excluded {
|
||||
count_card.state = CandidateState::Excluded;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
}
|
||||
}
|
||||
|
||||
let orig_candidates = state.candidates.clone();
|
||||
let orig_exhausted = state.exhausted.clone();
|
||||
|
||||
distribute_preferences(state);
|
||||
|
||||
// Recalculate transfers
|
||||
for (candidate, count_card) in state.candidates.iter_mut() {
|
||||
count_card.transfers = &count_card.votes - &orig_candidates.get(candidate).unwrap().votes;
|
||||
}
|
||||
state.exhausted.transfers = &state.exhausted.votes - &orig_exhausted.votes;
|
||||
}
|
217
src/stv/mod.rs
217
src/stv/mod.rs
|
@ -17,20 +17,22 @@
|
|||
|
||||
#![allow(mutable_borrow_reservation_conflict)]
|
||||
|
||||
/// Gregory method of surplus distributions
|
||||
pub mod gregory;
|
||||
/// Meek method of surplus distributions, etc.
|
||||
pub mod meek;
|
||||
|
||||
//#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm;
|
||||
|
||||
use crate::numbers::Number;
|
||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
|
||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Vote};
|
||||
use crate::sharandom::SHARandom;
|
||||
use crate::ties::TieStrategy;
|
||||
|
||||
use itertools::Itertools;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::HashMap;
|
||||
use std::ops;
|
||||
|
||||
|
@ -319,7 +321,11 @@ pub enum STVError {
|
|||
}
|
||||
|
||||
/// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules
|
||||
pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions) {
|
||||
pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
{
|
||||
// Initialise RNG
|
||||
for t in opts.ties.iter() {
|
||||
if let TieStrategy::Random(seed) = t {
|
||||
|
@ -327,7 +333,7 @@ pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a ST
|
|||
}
|
||||
}
|
||||
|
||||
distribute_first_preferences(&mut state);
|
||||
distribute_first_preferences(&mut state, opts);
|
||||
calculate_quota(&mut state, opts);
|
||||
elect_meeting_quota(&mut state, opts);
|
||||
init_tiebreaks(&mut state, opts);
|
||||
|
@ -337,6 +343,7 @@ pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a ST
|
|||
pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>,
|
||||
{
|
||||
|
@ -452,31 +459,19 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a
|
|||
}
|
||||
|
||||
/// Distribute first preference votes
|
||||
fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
||||
let votes = state.election.ballots.iter().map(|b| Vote {
|
||||
ballot: b,
|
||||
value: b.orig_value.clone(),
|
||||
up_to_pref: 0,
|
||||
}).collect();
|
||||
|
||||
let result = next_preferences(state, votes);
|
||||
|
||||
// Transfer candidate votes
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
let parcel = entry.votes as Parcel<N>;
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
count_card.transfer(&entry.num_votes);
|
||||
fn distribute_first_preferences<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
{
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||||
gregory::distribute_first_preferences(state);
|
||||
}
|
||||
SurplusMethod::Meek => {
|
||||
meek::distribute_first_preferences(state);
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = result.exhausted.votes as Parcel<N>;
|
||||
state.exhausted.parcels.push(parcel);
|
||||
state.exhausted.transfer(&result.exhausted.num_votes);
|
||||
|
||||
state.kind = None;
|
||||
state.title = "First preferences".to_string();
|
||||
state.logger.log_literal("First preferences distributed.".to_string());
|
||||
}
|
||||
|
||||
/// Calculate the quota, given the total vote, according to [STVOptions::quota]
|
||||
|
@ -514,7 +509,7 @@ fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N
|
|||
/// Calculate the quota according to [STVOptions::quota]
|
||||
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||
// Calculate quota
|
||||
if let None = state.quota {
|
||||
if state.quota.is_none() || opts.surplus == SurplusMethod::Meek {
|
||||
let mut log = String::new();
|
||||
|
||||
// Calculate the total vote
|
||||
|
@ -587,7 +582,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
|||
}
|
||||
} else {
|
||||
// No ERS97 rules
|
||||
if let None = state.vote_required_election {
|
||||
if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek {
|
||||
state.vote_required_election = state.quota.clone();
|
||||
}
|
||||
}
|
||||
|
@ -678,15 +673,16 @@ where
|
|||
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
for<'r> &'r N: ops::Neg<Output=N>,
|
||||
{
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||||
return gregory::distribute_surpluses(state, opts);
|
||||
}
|
||||
SurplusMethod::Meek => {
|
||||
todo!();
|
||||
return meek::distribute_surpluses(state, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -784,6 +780,8 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST
|
|||
/// Exclude the lowest-ranked hopeful candidate(s)
|
||||
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
let mut excluded_candidates: Vec<&Candidate> = Vec::new();
|
||||
|
@ -831,6 +829,8 @@ where
|
|||
/// Continue the exclusion of a candidate who is being excluded
|
||||
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
|
||||
|
@ -867,155 +867,18 @@ where
|
|||
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
|
||||
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
// Used to give bulk excluded candidate the same order_elected
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
|
||||
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);
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||||
gregory::exclude_candidates(state, opts, excluded_candidates);
|
||||
}
|
||||
SurplusMethod::Meek => {
|
||||
meek::exclude_candidates(state, opts, excluded_candidates);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine votes to transfer in this stage
|
||||
let mut votes = Vec::new();
|
||||
let mut votes_remain;
|
||||
let mut checksum = N::new();
|
||||
|
||||
match opts.exclusion {
|
||||
ExclusionMethod::SingleStage => {
|
||||
// Exclude in one round
|
||||
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();
|
||||
|
||||
// Update votes
|
||||
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
votes_remain = false;
|
||||
}
|
||||
ExclusionMethod::ByValue => {
|
||||
// Exclude by value
|
||||
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();
|
||||
|
||||
votes_remain = false;
|
||||
|
||||
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();
|
||||
|
||||
let mut votes_transferred = N::new();
|
||||
for vote in cand_votes.into_iter() {
|
||||
if &vote.value / &vote.ballot.orig_value == max_value {
|
||||
votes_transferred += &vote.value;
|
||||
votes.push(vote);
|
||||
} else {
|
||||
remaining_votes.push(vote);
|
||||
}
|
||||
}
|
||||
|
||||
if !remaining_votes.is_empty() {
|
||||
votes_remain = true;
|
||||
}
|
||||
|
||||
// Leave remaining votes with candidate (as one parcel)
|
||||
count_card.parcels = vec![remaining_votes];
|
||||
|
||||
// Update votes
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
}
|
||||
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[0]).unwrap();
|
||||
votes = count_card.parcels.remove(0);
|
||||
votes_remain = !count_card.parcels.is_empty();
|
||||
|
||||
// Update votes
|
||||
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
}
|
||||
|
||||
if !votes.is_empty() {
|
||||
let value = &votes[0].value / &votes[0].ballot.orig_value;
|
||||
|
||||
// Count next preferences
|
||||
let result = next_preferences(state, votes);
|
||||
|
||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
|
||||
// Transfer candidate votes
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
let parcel = entry.votes as Parcel<N>;
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
|
||||
// Round transfers
|
||||
let mut candidate_transfers = entry.num_votes;
|
||||
if let Some(dps) = opts.round_votes {
|
||||
candidate_transfers.floor_mut(dps);
|
||||
}
|
||||
count_card.transfer(&candidate_transfers);
|
||||
checksum += candidate_transfers;
|
||||
}
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = result.exhausted.votes as Parcel<N>;
|
||||
state.exhausted.parcels.push(parcel);
|
||||
|
||||
let mut exhausted_transfers = result.exhausted.num_votes;
|
||||
if let Some(dps) = opts.round_votes {
|
||||
exhausted_transfers.floor_mut(dps);
|
||||
}
|
||||
state.exhausted.transfer(&exhausted_transfers);
|
||||
checksum += exhausted_transfers;
|
||||
}
|
||||
|
||||
if !votes_remain {
|
||||
// Finalise candidate votes
|
||||
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;
|
||||
count_card.votes = N::new();
|
||||
}
|
||||
|
||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||
} else {
|
||||
state.logger.log_literal("Exclusion complete.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
|
||||
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
|
||||
|
|
|
@ -26,6 +26,8 @@ extern crate console_error_panic_hook;
|
|||
use js_sys::Array;
|
||||
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
|
||||
|
||||
use std::cmp::max;
|
||||
|
||||
// Init
|
||||
|
||||
/// Wrapper for [Fixed::set_dps]
|
||||
|
@ -122,8 +124,8 @@ macro_rules! impl_type {
|
|||
/// Wrapper for [final_result_summary]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<final_result_summary_$type>](state: &[<CountState$type>]) -> String {
|
||||
return final_result_summary(&state.0);
|
||||
pub fn [<final_result_summary_$type>](state: &[<CountState$type>], opts: &STVOptions) -> String {
|
||||
return final_result_summary(&state.0, &opts.0);
|
||||
}
|
||||
|
||||
// Wrapper structs
|
||||
|
@ -336,20 +338,24 @@ fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array {
|
|||
}
|
||||
|
||||
/// Generate the final lead-out text summarising the result of the election
|
||||
fn final_result_summary<N: Number>(state: &CountState<N>) -> String {
|
||||
fn final_result_summary<N: Number>(state: &CountState<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
|
||||
|
||||
let mut winners = Vec::new();
|
||||
for (candidate, count_card) in state.candidates.iter() {
|
||||
if count_card.state == CandidateState::Elected {
|
||||
winners.push((candidate, count_card.order_elected));
|
||||
winners.push((candidate, count_card.order_elected, &count_card.keep_value));
|
||||
}
|
||||
}
|
||||
winners.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
|
||||
for (winner, _) in winners.into_iter() {
|
||||
for (winner, _, kv_opt) in winners.into_iter() {
|
||||
if let Some(kv) = kv_opt {
|
||||
result.push_str(&format!("<li>{} (<i>kv</i> = {:.dps2$})</li>", winner.name, kv, dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
result.push_str(&format!("<li>{}</li>", winner.name));
|
||||
}
|
||||
}
|
||||
|
||||
result.push_str("</ol>");
|
||||
return result;
|
||||
|
|
|
@ -55,6 +55,7 @@ pub fn read_validate_election<N: Number>(csv_file: &str, blt_file: &str, stv_opt
|
|||
pub fn validate_election<N: Number>(stages: Vec<usize>, records: Vec<StringRecord>, election: Election<N>, stv_opts: stv::STVOptions)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>,
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue