Implement Meek STV

This commit is contained in:
RunasSudo 2021-06-16 13:00:54 +10:00
parent f395e6f064
commit 4ebb6474fd
No known key found for this signature in database
GPG Key ID: 7234E476BF21C61A
15 changed files with 604 additions and 245 deletions

View File

@ -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>

View File

@ -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';

View File

@ -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;

View File

@ -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,
}

View File

@ -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 {
println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals);
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() {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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();
let total_surpluses = has_surplus.iter()
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
if !has_surplus.is_empty() {
let total_surpluses = has_surplus.iter()
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
// 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);
}

301
src/stv/meek.rs Normal file
View File

@ -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;
}

View File

@ -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

View File

@ -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,19 +338,23 @@ 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() {
result.push_str(&format!("<li>{}</li>", winner.name));
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>");

View File

@ -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>,
{