Show loss by fraction, and implement --sort-votes and --hide-excluded

This commit is contained in:
RunasSudo 2021-05-29 00:43:58 +10:00
parent e6d57685cb
commit df69ef456f
No known key found for this signature in database
GPG Key ID: 7234E476BF21C61A
4 changed files with 100 additions and 35 deletions

View File

@ -91,6 +91,7 @@ pub struct CountState<'a, N> {
pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>, pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>,
pub exhausted: CountCard<'a, N>, pub exhausted: CountCard<'a, N>,
pub loss_fraction: CountCard<'a, N>,
pub quota: N, pub quota: N,
@ -104,6 +105,7 @@ impl<'a, N: Number> CountState<'a, N> {
election: &election, election: &election,
candidates: HashMap::new(), candidates: HashMap::new(),
exhausted: CountCard::new(), exhausted: CountCard::new(),
loss_fraction: CountCard::new(),
quota: N::new(), quota: N::new(),
num_elected: 0, num_elected: 0,
num_excluded: 0, num_excluded: 0,
@ -121,6 +123,7 @@ impl<'a, N: Number> CountState<'a, N> {
count_card.step(); count_card.step();
} }
self.exhausted.step(); self.exhausted.step();
self.loss_fraction.step();
} }
} }

View File

@ -19,7 +19,7 @@ mod election;
mod numbers; mod numbers;
mod stv; mod stv;
use crate::election::{CandidateState, CountState, CountStateOrRef, Election, StageResult}; use crate::election::{Candidate, CandidateState, CountCard, CountState, CountStateOrRef, Election, StageResult};
use crate::numbers::{Number, NumType}; use crate::numbers::{Number, NumType};
use clap::Clap; use clap::Clap;
@ -48,33 +48,60 @@ enum Command {
struct STV { struct STV {
/// Path to the BLT file to be counted /// Path to the BLT file to be counted
filename: String, filename: String,
/// Hide excluded candidates from results report
#[clap(long)]
hide_excluded: bool,
/// Sort candidates by votes in results report
#[clap(long)]
sort_votes: bool,
/// Print votes to specified decimal places in results report /// Print votes to specified decimal places in results report
#[clap(long, default_value="2")] #[clap(long, default_value="2")]
pp_decimals: usize, pp_decimals: usize,
} }
fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>, cmd_opts: &STV) { fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(candidates: I, cmd_opts: &STV) {
println!("{}. {}", stage_num, result.title);
println!("{}", result.logs.join(" "));
// Sort candidates
//let mut candidates: Vec<(&&Candidate, &CountCard<N>)> = result.state.candidates.iter().collect();
//candidates.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
let candidates = result.state.as_ref().election.candidates.iter().map(|c| (c, result.state.as_ref().candidates.get(c).unwrap()));
// Print candidates
//for (candidate, count_card) in candidates.into_iter().rev() {
for (candidate, count_card) in candidates { for (candidate, count_card) in candidates {
if count_card.state == CandidateState::ELECTED { if count_card.state == CandidateState::ELECTED {
println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals); 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 { } else if count_card.state == CandidateState::EXCLUDED {
println!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=cmd_opts.pp_decimals); // 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() {
println!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=cmd_opts.pp_decimals);
}
} else { } else {
println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals); println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
} }
} }
}
fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>, cmd_opts: &STV) {
// Print stage details
println!("{}. {}", stage_num, result.title);
println!("{}", result.logs.join(" "));
println!("Quota: {:.dps$}", result.state.as_ref().quota, dps=cmd_opts.pp_decimals); let state = result.state.as_ref();
// Print candidates
if cmd_opts.sort_votes {
// Sort by votes if requested
let mut candidates: Vec<(&Candidate, &CountCard<N>)> = state.candidates.iter()
.map(|(c, cc)| (*c, cc)).collect();
// First sort by order of election (as a tie-breaker, if votes are equal)
candidates.sort_unstable_by(|a, b| b.1.order_elected.partial_cmp(&a.1.order_elected).unwrap());
// Then sort by votes
candidates.sort_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
print_candidates(candidates.into_iter().rev(), cmd_opts);
} else {
let candidates = state.election.candidates.iter()
.map(|c| (c, state.candidates.get(c).unwrap()));
print_candidates(candidates, cmd_opts);
}
// Print summary rows
println!("Exhausted: {:.dps$} ({:.dps$})", state.exhausted.votes, state.exhausted.transfers, dps=cmd_opts.pp_decimals);
println!("Loss by fraction: {:.dps$} ({:.dps$})", state.loss_fraction.votes, state.loss_fraction.transfers, dps=cmd_opts.pp_decimals);
println!("Quota: {:.dps$}", state.quota, dps=cmd_opts.pp_decimals);
println!(""); println!("");
} }

View File

@ -190,9 +190,7 @@ impl ops::AddAssign for Rational {
} }
impl ops::SubAssign for Rational { impl ops::SubAssign for Rational {
fn sub_assign(&mut self, _rhs: Self) { fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0 }
todo!()
}
} }
impl ops::MulAssign for Rational { impl ops::MulAssign for Rational {
@ -214,15 +212,11 @@ impl ops::RemAssign for Rational {
} }
impl ops::AddAssign<&Rational> for Rational { impl ops::AddAssign<&Rational> for Rational {
fn add_assign(&mut self, rhs: &Rational) { fn add_assign(&mut self, rhs: &Rational) { self.0 += &rhs.0 }
self.0 += &rhs.0;
}
} }
impl ops::SubAssign<&Rational> for Rational { impl ops::SubAssign<&Rational> for Rational {
fn sub_assign(&mut self, _rhs: &Rational) { fn sub_assign(&mut self, rhs: &Rational) { self.0 -= &rhs.0 }
todo!()
}
} }
impl ops::MulAssign<&Rational> for Rational { impl ops::MulAssign<&Rational> for Rational {
@ -243,6 +237,11 @@ impl ops::RemAssign<&Rational> for Rational {
} }
} }
impl ops::Neg for &Rational {
type Output = Rational;
fn neg(self) -> Self::Output { Rational(rug::Rational::from(-&self.0)) }
}
impl ops::Add<&Rational> for &Rational { impl ops::Add<&Rational> for &Rational {
type Output = Rational; type Output = Rational;
fn add(self, _rhs: &Rational) -> Self::Output { fn add(self, _rhs: &Rational) -> Self::Output {

View File

@ -21,7 +21,7 @@ use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Sub; use std::ops::{Neg, Sub};
struct NextPreferencesResult<'a, N> { struct NextPreferencesResult<'a, N> {
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
@ -148,7 +148,11 @@ pub fn elect_meeting_quota<N: Number>(state: &mut CountState<N>) {
} }
} }
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>) -> bool where for<'r> &'r N: Sub<&'r N, Output=N> { pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>) -> bool
where
for<'r> &'r N: Sub<&'r N, Output=N>,
for<'r> &'r N: Neg<Output=N>
{
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter() let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.votes > state.quota) .filter(|(_, cc)| cc.votes > state.quota)
.collect(); .collect();
@ -167,7 +171,11 @@ pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>) -> bool where
return false; return false;
} }
fn distribute_surplus<N: Number>(state: &mut CountState<N>, elected_candidate: &Candidate) where for<'r> &'r N: Sub<&'r N, Output=N> { fn distribute_surplus<N: Number>(state: &mut CountState<N>, elected_candidate: &Candidate)
where
for<'r> &'r N: Sub<&'r N, Output=N>,
for<'r> &'r N: Neg<Output=N>
{
let count_card = state.candidates.get(elected_candidate).unwrap(); let count_card = state.candidates.get(elected_candidate).unwrap();
let surplus = &count_card.votes - &state.quota; let surplus = &count_card.votes - &state.quota;
@ -181,35 +189,47 @@ fn distribute_surplus<N: Number>(state: &mut CountState<N>, elected_candidate: &
// Transfer candidate votes // Transfer candidate votes
// Unweighted inclusive Gregory // Unweighted inclusive Gregory
// TODO: Other methods // TODO: Other methods
let transfer_value = surplus.clone() / &result.total_ballots; //let transfer_value = surplus.clone() / &result.total_ballots;
let mut checksum = N::new();
for (candidate, entry) in result.candidates.into_iter() { for (candidate, entry) in result.candidates.into_iter() {
let mut parcel = entry.votes as Parcel<N>; let mut parcel = entry.votes as Parcel<N>;
// Reweight votes // Reweight votes
for vote in parcel.iter_mut() { for vote in parcel.iter_mut() {
vote.value = vote.ballot.orig_value.clone() * &transfer_value; //vote.value = vote.ballot.orig_value.clone() * &transfer_value;
vote.value = vote.ballot.orig_value.clone() * &surplus / &result.total_ballots;
} }
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);
let mut total_transferred = entry.num_ballots * &surplus / &result.total_ballots; let mut candidate_transfers = entry.num_ballots * &surplus / &result.total_ballots;
// Round transfers // Round transfers
// TODO: Make configurable // TODO: Make configurable
total_transferred.floor_mut(); candidate_transfers.floor_mut();
count_card.transfer(&total_transferred); count_card.transfer(&candidate_transfers);
checksum += candidate_transfers;
} }
// Transfer exhausted votes // Transfer exhausted votes
let parcel = result.exhausted.votes as Parcel<N>; let parcel = result.exhausted.votes as Parcel<N>;
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
state.exhausted.transfer(&result.exhausted.num_votes);
let mut exhausted_transfers = result.exhausted.num_ballots * &surplus / &result.total_ballots;
// TODO: Make configurable
exhausted_transfers.floor_mut();
state.exhausted.transfer(&exhausted_transfers);
checksum += exhausted_transfers;
// Finalise candidate votes // Finalise candidate votes
let count_card = state.candidates.get_mut(elected_candidate).unwrap(); let count_card = state.candidates.get_mut(elected_candidate).unwrap();
count_card.transfers = -surplus; count_card.transfers = -&surplus;
count_card.votes.assign(&state.quota); count_card.votes.assign(&state.quota);
checksum -= surplus;
// Update loss by fraction
state.loss_fraction.transfer(&-checksum);
} }
pub fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool { pub fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
@ -278,23 +298,39 @@ fn exclude_candidate<N: Number>(state: &mut CountState<N>, excluded_candidate: &
let result = next_preferences(state, votes); let result = next_preferences(state, votes);
// Transfer candidate votes // Transfer candidate votes
let mut checksum = N::new();
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 = entry.votes as Parcel<N>;
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); // Round transfers
// TODO: Make configurable
let mut candidate_transfers = entry.num_votes;
candidate_transfers.floor_mut();
count_card.transfer(&candidate_transfers);
checksum += candidate_transfers;
} }
// Transfer exhausted votes // Transfer exhausted votes
let parcel = result.exhausted.votes as Parcel<N>; let parcel = result.exhausted.votes as Parcel<N>;
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
state.exhausted.transfer(&result.exhausted.num_votes);
let mut exhausted_transfers = result.exhausted.num_votes;
// TODO: Make configurable
exhausted_transfers.floor_mut();
state.exhausted.transfer(&exhausted_transfers);
checksum += exhausted_transfers;
// Finalise candidate votes // Finalise candidate votes
let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
checksum -= &count_card.votes;
count_card.transfers = -count_card.votes.clone(); count_card.transfers = -count_card.votes.clone();
count_card.votes = N::new(); count_card.votes = N::new();
// Update loss by fraction
state.loss_fraction.transfer(&-checksum);
} }
pub fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool { pub fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {