Implement --exclusion by_value
This commit is contained in:
parent
f4fdf64072
commit
4f661722e4
10
src/main.rs
10
src/main.rs
|
@ -57,7 +57,7 @@ struct STV {
|
||||||
// -- Numbers settings --
|
// -- Numbers settings --
|
||||||
|
|
||||||
/// Numbers mode
|
/// Numbers mode
|
||||||
#[clap(help_heading=Some("NUMBERS"), short, long, possible_values(&["rational", "float64"]), default_value="rational", value_name="mode")]
|
#[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "float64"], default_value="rational", value_name="mode")]
|
||||||
numbers: String,
|
numbers: String,
|
||||||
|
|
||||||
// -- Rounding settings --
|
// -- Rounding settings --
|
||||||
|
@ -66,6 +66,12 @@ struct STV {
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||||
round_votes: Option<usize>,
|
round_votes: Option<usize>,
|
||||||
|
|
||||||
|
// -- STV variants --
|
||||||
|
|
||||||
|
/// Method of exclusions
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["one_round", "by_value"], default_value="one_round", value_name="mode")]
|
||||||
|
exclusion: String,
|
||||||
|
|
||||||
// -- Display settings --
|
// -- Display settings --
|
||||||
|
|
||||||
/// Hide excluded candidates from results report
|
/// Hide excluded candidates from results report
|
||||||
|
@ -101,11 +107,13 @@ fn main() {
|
||||||
fn count_election<N: Number>(election: Election<N>, cmd_opts: STV)
|
fn count_election<N: Number>(election: Election<N>, cmd_opts: STV)
|
||||||
where
|
where
|
||||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Neg<Output=N>
|
for<'r> &'r N: ops::Neg<Output=N>
|
||||||
{
|
{
|
||||||
// Copy applicable options
|
// Copy applicable options
|
||||||
let stv_opts = stv::STVOptions {
|
let stv_opts = stv::STVOptions {
|
||||||
round_votes: cmd_opts.round_votes,
|
round_votes: cmd_opts.round_votes,
|
||||||
|
exclusion: &cmd_opts.exclusion,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialise count state
|
// Initialise count state
|
||||||
|
|
|
@ -21,12 +21,12 @@ mod rational;
|
||||||
use num_traits::{NumAssignRef, NumRef};
|
use num_traits::{NumAssignRef, NumRef};
|
||||||
use rug::{self, Assign};
|
use rug::{self, Assign};
|
||||||
|
|
||||||
use std::cmp::{PartialOrd};
|
use std::cmp::Ord;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
//pub trait Number: NumRef + NumAssignRef + PartialOrd + Assign + Clone + fmt::Display where for<'a> &'a Self: RefNum<&'a Self> {
|
//pub trait Number: NumRef + NumAssignRef + PartialOrd + Assign + Clone + fmt::Display where for<'a> &'a Self: RefNum<&'a Self> {
|
||||||
pub trait Number: NumRef + NumAssignRef + ops::Neg<Output=Self> + PartialOrd + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self>{
|
pub trait Number: NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self>{
|
||||||
fn new() -> Self;
|
fn new() -> Self;
|
||||||
fn from(n: usize) -> Self;
|
fn from(n: usize) -> Self;
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ use super::Number;
|
||||||
use num_traits::{Num, One, Zero};
|
use num_traits::{Num, One, Zero};
|
||||||
use rug::Assign;
|
use rug::Assign;
|
||||||
|
|
||||||
use std::cmp::{Ordering, PartialEq, PartialOrd};
|
use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
@ -76,10 +76,13 @@ impl Zero for NativeFloat64 {
|
||||||
fn is_zero(&self) -> bool { self.0.is_zero() }
|
fn is_zero(&self) -> bool { self.0.is_zero() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Eq for NativeFloat64 {}
|
||||||
impl PartialEq for NativeFloat64 {
|
impl PartialEq for NativeFloat64 {
|
||||||
fn eq(&self, _other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
|
||||||
todo!()
|
}
|
||||||
}
|
|
||||||
|
impl Ord for NativeFloat64 {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering { self.0.partial_cmp(&other.0).unwrap() }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for NativeFloat64 {
|
impl PartialOrd for NativeFloat64 {
|
||||||
|
@ -234,9 +237,7 @@ impl ops::Mul<&NativeFloat64> for &NativeFloat64 {
|
||||||
|
|
||||||
impl ops::Div<&NativeFloat64> for &NativeFloat64 {
|
impl ops::Div<&NativeFloat64> for &NativeFloat64 {
|
||||||
type Output = NativeFloat64;
|
type Output = NativeFloat64;
|
||||||
fn div(self, _rhs: &NativeFloat64) -> Self::Output {
|
fn div(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 / &rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Rem<&NativeFloat64> for &NativeFloat64 {
|
impl ops::Rem<&NativeFloat64> for &NativeFloat64 {
|
||||||
|
|
|
@ -20,7 +20,7 @@ use super::Number;
|
||||||
use num_traits::{Num, One, Zero};
|
use num_traits::{Num, One, Zero};
|
||||||
use rug::{self, Assign, ops::Pow, rational::ParseRationalError};
|
use rug::{self, Assign, ops::Pow, rational::ParseRationalError};
|
||||||
|
|
||||||
use std::cmp::{Ordering, PartialEq, PartialOrd};
|
use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
|
@ -106,10 +106,13 @@ impl Zero for Rational {
|
||||||
fn is_zero(&self) -> bool { self.0 == rug::Rational::new() }
|
fn is_zero(&self) -> bool { self.0 == rug::Rational::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Eq for Rational {}
|
||||||
impl PartialEq for Rational {
|
impl PartialEq for Rational {
|
||||||
fn eq(&self, _other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
|
||||||
todo!()
|
}
|
||||||
}
|
|
||||||
|
impl Ord for Rational {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering { self.0.cmp(&other.0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for Rational {
|
impl PartialOrd for Rational {
|
||||||
|
@ -264,9 +267,7 @@ impl ops::Mul<&Rational> for &Rational {
|
||||||
|
|
||||||
impl ops::Div<&Rational> for &Rational {
|
impl ops::Div<&Rational> for &Rational {
|
||||||
type Output = Rational;
|
type Output = Rational;
|
||||||
fn div(self, _rhs: &Rational) -> Self::Output {
|
fn div(self, rhs: &Rational) -> Self::Output { Rational(rug::Rational::from(&self.0 / &rhs.0)) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Rem<&Rational> for &Rational {
|
impl ops::Rem<&Rational> for &Rational {
|
||||||
|
|
124
src/stv/mod.rs
124
src/stv/mod.rs
|
@ -21,10 +21,11 @@ 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::{Neg, Sub};
|
use std::ops;
|
||||||
|
|
||||||
pub struct STVOptions {
|
pub struct STVOptions<'a> {
|
||||||
pub round_votes: Option<usize>,
|
pub round_votes: Option<usize>,
|
||||||
|
pub exclusion: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, _opts: &STVOptions) {
|
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, _opts: &STVOptions) {
|
||||||
|
@ -35,8 +36,9 @@ pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, _opts: &STVOptio
|
||||||
|
|
||||||
pub fn count_one_stage<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) -> bool
|
pub fn count_one_stage<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) -> bool
|
||||||
where
|
where
|
||||||
for<'r> &'r N: Sub<&'r N, Output=N>,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
for<'r> &'r N: Neg<Output=N>
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Neg<Output=N>,
|
||||||
{
|
{
|
||||||
state.logger.entries.clear();
|
state.logger.entries.clear();
|
||||||
state.step_all();
|
state.step_all();
|
||||||
|
@ -77,6 +79,7 @@ struct NextPreferencesResult<'a, N> {
|
||||||
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
|
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
|
||||||
exhausted: NextPreferencesEntry<'a, N>,
|
exhausted: NextPreferencesEntry<'a, N>,
|
||||||
total_ballots: N,
|
total_ballots: N,
|
||||||
|
total_votes: N,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NextPreferencesEntry<'a, N> {
|
struct NextPreferencesEntry<'a, N> {
|
||||||
|
@ -95,10 +98,12 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a
|
||||||
num_votes: N::new(),
|
num_votes: N::new(),
|
||||||
},
|
},
|
||||||
total_ballots: N::new(),
|
total_ballots: N::new(),
|
||||||
|
total_votes: N::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for mut vote in votes.into_iter() {
|
for mut vote in votes.into_iter() {
|
||||||
result.total_ballots += &vote.ballot.orig_value;
|
result.total_ballots += &vote.ballot.orig_value;
|
||||||
|
result.total_votes += &vote.value;
|
||||||
|
|
||||||
let mut next_candidate = None;
|
let mut next_candidate = None;
|
||||||
|
|
||||||
|
@ -214,8 +219,8 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>) {
|
||||||
|
|
||||||
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool
|
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool
|
||||||
where
|
where
|
||||||
for<'r> &'r N: Sub<&'r N, Output=N>,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
for<'r> &'r N: Neg<Output=N>
|
for<'r> &'r N: ops::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)
|
||||||
|
@ -237,8 +242,8 @@ where
|
||||||
|
|
||||||
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
|
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
|
||||||
where
|
where
|
||||||
for<'r> &'r N: Sub<&'r N, Output=N>,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
for<'r> &'r N: Neg<Output=N>
|
for<'r> &'r N: ops::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;
|
||||||
|
@ -333,7 +338,10 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exclude_hopefuls<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
fn exclude_hopefuls<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
|
{
|
||||||
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||||||
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
|
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -358,9 +366,14 @@ fn exclude_hopefuls<N: Number>(state: &mut CountState<N>, opts: &STVOptions) ->
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn continue_exclusion<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
fn continue_exclusion<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool
|
||||||
|
where
|
||||||
|
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
|
||||||
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.votes.is_zero())
|
//.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero())
|
||||||
|
.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && cc.parcels.iter().any(|p| p.len() > 0))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if excluded_with_votes.len() > 0 {
|
if excluded_with_votes.len() > 0 {
|
||||||
|
@ -382,19 +395,67 @@ fn continue_exclusion<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exclude_candidate<N: Number>(state: &mut CountState<N>, opts: &STVOptions, excluded_candidate: &Candidate) {
|
fn exclude_candidate<N: Number>(state: &mut CountState<N>, opts: &STVOptions, excluded_candidate: &Candidate)
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
|
{
|
||||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||||
count_card.state = CandidateState::EXCLUDED;
|
|
||||||
state.num_excluded += 1;
|
|
||||||
count_card.order_elected = -(state.num_excluded as isize);
|
|
||||||
|
|
||||||
// Exclude in one round
|
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
|
||||||
// TODO: Exclude by parcel
|
if count_card.state != CandidateState::EXCLUDED {
|
||||||
let votes = state.candidates.get(excluded_candidate).unwrap().parcels.concat();
|
count_card.state = CandidateState::EXCLUDED;
|
||||||
|
state.num_excluded += 1;
|
||||||
|
count_card.order_elected = -(state.num_excluded as isize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine votes to transfer in this stage
|
||||||
|
let mut votes;
|
||||||
|
let votes_remaining;
|
||||||
|
|
||||||
|
if opts.exclusion == "one_round" {
|
||||||
|
// Exclude in one round
|
||||||
|
votes = count_card.parcels.concat();
|
||||||
|
votes_remaining = 0;
|
||||||
|
|
||||||
|
} else if opts.exclusion == "by_value" {
|
||||||
|
// Exclude by value
|
||||||
|
let all_votes = count_card.parcels.concat();
|
||||||
|
|
||||||
|
// TODO: Write a multiple min/max function
|
||||||
|
let min_value = all_votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap();
|
||||||
|
|
||||||
|
votes = Vec::new();
|
||||||
|
let mut remaining_votes = Vec::new();
|
||||||
|
|
||||||
|
// This could be implemented using Vec.drain_filter, but that is experimental currently
|
||||||
|
for vote in all_votes.into_iter() {
|
||||||
|
if &vote.value / &vote.ballot.orig_value == min_value {
|
||||||
|
votes.push(vote);
|
||||||
|
} else {
|
||||||
|
remaining_votes.push(vote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
votes_remaining = remaining_votes.len();
|
||||||
|
// Leave remaining votes with candidate (as one parcel)
|
||||||
|
count_card.parcels = vec![remaining_votes];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// TODO: Exclude by parcel
|
||||||
|
panic!("Invalid --exclusion");
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = &votes[0].value / &votes[0].ballot.orig_value;
|
||||||
|
|
||||||
// Count next preferences
|
// Count next preferences
|
||||||
let result = next_preferences(state, votes);
|
let result = next_preferences(state, votes);
|
||||||
|
|
||||||
|
if opts.exclusion == "one_round" {
|
||||||
|
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.2} votes.", result.total_ballots, result.total_votes));
|
||||||
|
} else if opts.exclusion == "by_value" {
|
||||||
|
state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.2} votes, received at value {:.2}.", result.total_ballots, result.total_votes, value));
|
||||||
|
}
|
||||||
|
|
||||||
// Transfer candidate votes
|
// Transfer candidate votes
|
||||||
let mut checksum = N::new();
|
let mut checksum = N::new();
|
||||||
|
|
||||||
|
@ -423,14 +484,27 @@ fn exclude_candidate<N: Number>(state: &mut CountState<N>, opts: &STVOptions, ex
|
||||||
state.exhausted.transfer(&exhausted_transfers);
|
state.exhausted.transfer(&exhausted_transfers);
|
||||||
checksum += exhausted_transfers;
|
checksum += exhausted_transfers;
|
||||||
|
|
||||||
// Finalise candidate votes
|
if votes_remaining > 0 {
|
||||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
// Subtract from candidate tally
|
||||||
checksum -= &count_card.votes;
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||||
count_card.transfers = -count_card.votes.clone();
|
checksum -= &result.total_votes;
|
||||||
count_card.votes = N::new();
|
count_card.transfer(&-result.total_votes);
|
||||||
|
|
||||||
// Update loss by fraction
|
// By definition, there is no loss by fraction
|
||||||
state.loss_fraction.transfer(&-checksum);
|
} else {
|
||||||
|
// Finalise candidate votes
|
||||||
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||||
|
checksum -= &count_card.votes;
|
||||||
|
count_card.transfers = -count_card.votes.clone();
|
||||||
|
count_card.votes = N::new();
|
||||||
|
|
||||||
|
// Update loss by fraction
|
||||||
|
state.loss_fraction.transfer(&-checksum);
|
||||||
|
|
||||||
|
if opts.exclusion != "one_round" {
|
||||||
|
state.logger.log_literal("Exclusion complete.".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
|
fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
|
||||||
|
|
Loading…
Reference in New Issue