Implement --exclusion by_value

This commit is contained in:
RunasSudo 2021-05-30 02:28:52 +10:00
parent f4fdf64072
commit 4f661722e4
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
5 changed files with 127 additions and 43 deletions

View File

@ -57,7 +57,7 @@ struct STV {
// -- Numbers settings --
/// 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,
// -- Rounding settings --
@ -66,6 +66,12 @@ struct STV {
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
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 --
/// Hide excluded candidates from results report
@ -101,11 +107,13 @@ fn main() {
fn count_election<N: Number>(election: Election<N>, cmd_opts: STV)
where
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>
{
// Copy applicable options
let stv_opts = stv::STVOptions {
round_votes: cmd_opts.round_votes,
exclusion: &cmd_opts.exclusion,
};
// Initialise count state

View File

@ -21,12 +21,12 @@ mod rational;
use num_traits::{NumAssignRef, NumRef};
use rug::{self, Assign};
use std::cmp::{PartialOrd};
use std::cmp::Ord;
use std::fmt;
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 + 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 from(n: usize) -> Self;

View File

@ -20,7 +20,7 @@ use super::Number;
use num_traits::{Num, One, Zero};
use rug::Assign;
use std::cmp::{Ordering, PartialEq, PartialOrd};
use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
use std::num::ParseIntError;
use std::fmt;
use std::ops;
@ -76,10 +76,13 @@ impl Zero for NativeFloat64 {
fn is_zero(&self) -> bool { self.0.is_zero() }
}
impl Eq for NativeFloat64 {}
impl PartialEq for NativeFloat64 {
fn eq(&self, _other: &Self) -> bool {
todo!()
}
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
}
impl Ord for NativeFloat64 {
fn cmp(&self, other: &Self) -> Ordering { self.0.partial_cmp(&other.0).unwrap() }
}
impl PartialOrd for NativeFloat64 {
@ -234,9 +237,7 @@ impl ops::Mul<&NativeFloat64> for &NativeFloat64 {
impl ops::Div<&NativeFloat64> for &NativeFloat64 {
type Output = NativeFloat64;
fn div(self, _rhs: &NativeFloat64) -> Self::Output {
todo!()
}
fn div(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 / &rhs.0) }
}
impl ops::Rem<&NativeFloat64> for &NativeFloat64 {

View File

@ -20,7 +20,7 @@ use super::Number;
use num_traits::{Num, One, Zero};
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::ops;
@ -106,10 +106,13 @@ impl Zero for Rational {
fn is_zero(&self) -> bool { self.0 == rug::Rational::new() }
}
impl Eq for Rational {}
impl PartialEq for Rational {
fn eq(&self, _other: &Self) -> bool {
todo!()
}
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
}
impl Ord for Rational {
fn cmp(&self, other: &Self) -> Ordering { self.0.cmp(&other.0) }
}
impl PartialOrd for Rational {
@ -264,9 +267,7 @@ impl ops::Mul<&Rational> for &Rational {
impl ops::Div<&Rational> for &Rational {
type Output = Rational;
fn div(self, _rhs: &Rational) -> Self::Output {
todo!()
}
fn div(self, rhs: &Rational) -> Self::Output { Rational(rug::Rational::from(&self.0 / &rhs.0)) }
}
impl ops::Rem<&Rational> for &Rational {

View File

@ -21,10 +21,11 @@ use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
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 exclusion: &'a str,
}
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
where
for<'r> &'r N: Sub<&'r N, Output=N>,
for<'r> &'r N: Neg<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>,
{
state.logger.entries.clear();
state.step_all();
@ -77,6 +79,7 @@ struct NextPreferencesResult<'a, N> {
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
exhausted: NextPreferencesEntry<'a, N>,
total_ballots: N,
total_votes: 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(),
},
total_ballots: N::new(),
total_votes: N::new(),
};
for mut vote in votes.into_iter() {
result.total_ballots += &vote.ballot.orig_value;
result.total_votes += &vote.value;
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
where
for<'r> &'r N: Sub<&'r N, Output=N>,
for<'r> &'r N: Neg<Output=N>
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>
{
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.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)
where
for<'r> &'r N: Sub<&'r N, Output=N>,
for<'r> &'r N: Neg<Output=N>
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>
{
let count_card = state.candidates.get(elected_candidate).unwrap();
let surplus = &count_card.votes - &state.quota;
@ -333,7 +338,10 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
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()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
.collect();
@ -358,9 +366,14 @@ fn exclude_hopefuls<N: Number>(state: &mut CountState<N>, opts: &STVOptions) ->
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()
.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();
if excluded_with_votes.len() > 0 {
@ -382,19 +395,67 @@ fn continue_exclusion<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -
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();
// 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 = -(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
let votes = state.candidates.get(excluded_candidate).unwrap().parcels.concat();
panic!("Invalid --exclusion");
}
let value = &votes[0].value / &votes[0].ballot.orig_value;
// Count next preferences
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
let mut checksum = N::new();
@ -423,6 +484,14 @@ fn exclude_candidate<N: Number>(state: &mut CountState<N>, opts: &STVOptions, ex
state.exhausted.transfer(&exhausted_transfers);
checksum += exhausted_transfers;
if votes_remaining > 0 {
// Subtract from candidate tally
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
checksum -= &result.total_votes;
count_card.transfer(&-result.total_votes);
// By definition, there is no loss by fraction
} else {
// Finalise candidate votes
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
checksum -= &count_card.votes;
@ -431,6 +500,11 @@ fn exclude_candidate<N: Number>(state: &mut CountState<N>, opts: &STVOptions, ex
// 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 {