From 056242514dd478745df7a6d1be6661e8780fc424 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 11 Sep 2021 02:43:11 +1000 Subject: [PATCH] Implement TransferTable for surpluses (WIP) --- src/numbers/fixed.rs | 9 +- src/numbers/gfixed.rs | 9 +- src/numbers/native.rs | 8 +- src/numbers/rational_num.rs | 8 +- src/numbers/rational_rug.rs | 8 +- src/stv/gregory.rs | 173 +++++++++--------------------------- src/stv/mod.rs | 9 ++ src/stv/transfers.rs | 138 +++++++++++++++++++++++----- 8 files changed, 182 insertions(+), 180 deletions(-) diff --git a/src/numbers/fixed.rs b/src/numbers/fixed.rs index 53e1f3f..d315177 100644 --- a/src/numbers/fixed.rs +++ b/src/numbers/fixed.rs @@ -206,9 +206,7 @@ impl ops::Sub for Fixed { impl ops::Mul for Fixed { type Output = Self; - fn mul(self, _rhs: Self) -> Self::Output { - todo!() - } + fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0 / get_factor()) } } impl ops::Div for Fixed { @@ -294,8 +292,9 @@ impl ops::MulAssign<&Self> for Fixed { } impl ops::DivAssign<&Self> for Fixed { - fn div_assign(&mut self, _rhs: &Self) { - todo!() + fn div_assign(&mut self, rhs: &Self) { + self.0 *= get_factor(); + self.0 /= &rhs.0; } } diff --git a/src/numbers/gfixed.rs b/src/numbers/gfixed.rs index 9b816ec..a47d9ea 100644 --- a/src/numbers/gfixed.rs +++ b/src/numbers/gfixed.rs @@ -238,9 +238,7 @@ impl ops::Sub for GuardedFixed { impl ops::Mul for GuardedFixed { type Output = Self; - fn mul(self, _rhs: Self) -> Self::Output { - todo!() - } + fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0 / get_factor())} } impl ops::Div for GuardedFixed { @@ -326,8 +324,9 @@ impl ops::MulAssign<&Self> for GuardedFixed { } impl ops::DivAssign<&Self> for GuardedFixed { - fn div_assign(&mut self, _rhs: &Self) { - todo!() + fn div_assign(&mut self, rhs: &Self) { + self.0 *= get_factor(); + self.0 /= &rhs.0; } } diff --git a/src/numbers/native.rs b/src/numbers/native.rs index 5ded8aa..f33c764 100644 --- a/src/numbers/native.rs +++ b/src/numbers/native.rs @@ -109,9 +109,7 @@ impl ops::Sub for NativeFloat64 { impl ops::Mul for NativeFloat64 { type Output = NativeFloat64; - fn mul(self, _rhs: Self) -> Self::Output { - todo!() - } + fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0) } } impl ops::Div for NativeFloat64 { @@ -188,9 +186,7 @@ impl ops::MulAssign<&NativeFloat64> for NativeFloat64 { } impl ops::DivAssign<&NativeFloat64> for NativeFloat64 { - fn div_assign(&mut self, _rhs: &NativeFloat64) { - todo!() - } + fn div_assign(&mut self, rhs: &NativeFloat64) { self.0 /= &rhs.0; } } impl ops::RemAssign<&NativeFloat64> for NativeFloat64 { diff --git a/src/numbers/rational_num.rs b/src/numbers/rational_num.rs index 8c8bf4e..859b6b5 100644 --- a/src/numbers/rational_num.rs +++ b/src/numbers/rational_num.rs @@ -186,9 +186,7 @@ impl ops::Sub for Rational { impl ops::Mul for Rational { type Output = Rational; - fn mul(self, _rhs: Self) -> Self::Output { - todo!() - } + fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0) } } impl ops::Div for Rational { @@ -265,9 +263,7 @@ impl ops::MulAssign<&Rational> for Rational { } impl ops::DivAssign<&Rational> for Rational { - fn div_assign(&mut self, _rhs: &Rational) { - todo!() - } + fn div_assign(&mut self, rhs: &Rational) { self.0 /= &rhs.0 } } impl ops::RemAssign<&Rational> for Rational { diff --git a/src/numbers/rational_rug.rs b/src/numbers/rational_rug.rs index 3974d96..bbbfd07 100644 --- a/src/numbers/rational_rug.rs +++ b/src/numbers/rational_rug.rs @@ -185,9 +185,7 @@ impl ops::Sub for Rational { impl ops::Mul for Rational { type Output = Self; - fn mul(self, _rhs: Self) -> Self::Output { - todo!() - } + fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0) } } impl ops::Div for Rational { @@ -264,9 +262,7 @@ impl ops::MulAssign<&Self> for Rational { } impl ops::DivAssign<&Self> for Rational { - fn div_assign(&mut self, _rhs: &Self) { - todo!() - } + fn div_assign(&mut self, rhs: &Self) { self.0 /= &rhs.0 } } impl ops::RemAssign<&Self> for Rational { diff --git a/src/stv/gregory.rs b/src/stv/gregory.rs index 1233e43..ade495f 100644 --- a/src/stv/gregory.rs +++ b/src/stv/gregory.rs @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -use super::{ExclusionMethod, NextPreferencesEntry, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder}; +use super::{ExclusionMethod, STVError, STVOptions, SurplusMethod, SurplusOrder}; use super::sample; use crate::constraints; @@ -25,7 +25,6 @@ use crate::stv::transfers::TransferTable; use crate::ties; use std::cmp::max; -use std::collections::HashMap; use std::ops; /// Distribute first preference votes according to the Gregory method @@ -168,97 +167,23 @@ where /// Return the denominator of the surplus fraction /// /// Returns `None` if the value of transferable votes <= surplus (i.e. all transferable votes are transferred at values received). -fn calculate_surplus_denom<'n, N: Number>(surplus: &N, transferable_ballots: &'n N, transferable_votes: &'n N, total_ballots: &'n N, total_votes: &'n N, weighted: bool, transferable_only: bool) -> Option<&'n N> +fn calculate_surplus_denom<'n, N: Number>(surplus: &N, transferable_ballots: &'n N, transferable_votes: &'n N, total_ballots: &'n N, total_votes: &'n N, opts: &STVOptions) -> Option where for<'r> &'r N: ops::Sub<&'r N, Output=N> { - if transferable_only { - let transferable_units = if weighted { transferable_votes } else { transferable_ballots }; + if opts.transferable_only { + let transferable_units = if opts.surplus.is_weighted() { transferable_votes } else { transferable_ballots }; if transferable_votes > surplus { - return Some(transferable_units); + return Some(transferable_units.clone()); } else { return None; } } else { - if weighted { - return Some(total_votes); + if opts.surplus.is_weighted() { + return Some(total_votes.clone()); } else { - return Some(total_ballots); - } - } -} - -/// Return the reweighted value fraction of a parcel/vote after being transferred -fn reweight_value_fraction( - value_fraction: &N, - surplus: &N, - weighted: bool, - surplus_fraction: &Option, - surplus_denom: &Option<&N>, - round_tvs: Option) -> N -{ - let result; - - match surplus_denom { - Some(v) => { - if let Some(_) = round_tvs { - // Rounding requested: use the rounded transfer value - if weighted { - result = value_fraction.clone() * surplus_fraction.as_ref().unwrap(); - } else { - result = surplus_fraction.as_ref().unwrap().clone(); - } - } else { - // Avoid unnecessary rounding error by first multiplying by the surplus - if weighted { - result = value_fraction.clone() * surplus / *v; - } else { - result = surplus.clone() / *v; - } - } - } - None => { - result = value_fraction.clone(); - } - } - - return result; -} - -/// Compute the number of votes to credit to a continuing candidate during a surplus transfer, based on [STVOptions::sum_surplus_transfers] -fn sum_surplus_transfers(entry: &NextPreferencesEntry, orig_value_fraction: &N, surplus: &N, is_weighted: bool, surplus_fraction: &Option, surplus_denom: &Option<&N>, _state: &mut CountState, opts: &STVOptions) -> N -where - for<'r> &'r N: ops::Mul<&'r N, Output=N>, - for<'r> &'r N: ops::Div<&'r N, Output=N>, -{ - match opts.sum_surplus_transfers { - SumSurplusTransfersMode::ByValue => { - // Calculate transfer across all votes in this parcel - let mut result = N::new(); - for vote in entry.votes.iter() { - result += &vote.ballot.orig_value; - } - result *= reweight_value_fraction(orig_value_fraction, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions); - return result; - } - SumSurplusTransfersMode::PerBallot => { - // Sum transfer per each individual ballot - // TODO: This could be moved to distribute_surplus to avoid looping over the votes and calculating transfer values twice - let mut new_value_fraction = reweight_value_fraction(orig_value_fraction, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions); - if let Some(dps) = opts.round_votes { - new_value_fraction.floor_mut(dps); - } - - let mut result = N::new(); - for vote in entry.votes.iter() { - let mut vote_value = &new_value_fraction * &vote.ballot.orig_value; - if let Some(dps) = opts.round_votes { - vote_value.floor_mut(dps); - } - result += vote_value; - } - return result; + return Some(total_ballots.clone()); } } } @@ -320,13 +245,7 @@ where parcels_next_prefs.push((parcel.value_fraction, result)); } - // Calculate surplus fraction - - let is_weighted = match opts.surplus { - SurplusMethod::WIG => { true } - SurplusMethod::UIG | SurplusMethod::EG => { false } - _ => unreachable!() - }; + // Calculate and print surplus fraction let total_ballots = &transferable_ballots + &exhausted_ballots; let total_votes = &transferable_votes + &exhausted_votes; @@ -334,15 +253,20 @@ where let count_card = state.candidates.get_mut(elected_candidate).unwrap(); count_card.ballot_transfers = -&total_ballots; - let surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, is_weighted, opts.transferable_only); + let mut surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, opts); + let surplus_numer; let mut surplus_fraction; - match surplus_denom { + match &surplus_denom { Some(v) => { surplus_fraction = Some(surplus.clone() / v); // Round down if requested if let Some(dps) = opts.round_surplus_fractions { surplus_fraction.as_mut().unwrap().floor_mut(dps); + surplus_numer = surplus_fraction.clone(); + surplus_denom = None; + } else { + surplus_numer = Some(surplus.clone()); } if opts.transferable_only { @@ -361,6 +285,8 @@ where } None => { surplus_fraction = None; + surplus_numer = None; + surplus_denom = None; // This can only happen if --transferable-only if transferable_ballots == N::one() { @@ -373,41 +299,44 @@ where // Reweight and transfer parcels - let mut candidate_transfers: HashMap<&Candidate, N> = HashMap::new(); - for candidate in state.election.candidates.iter() { - candidate_transfers.insert(candidate, N::new()); - } - let mut exhausted_transfers = N::new(); + let mut transfer_table = TransferTable::new(); for (value_fraction, result) in parcels_next_prefs { for (candidate, entry) in result.candidates.into_iter() { // Record transfers - // TODO: Is there a better way of writing this? - let transfers_orig = candidate_transfers.remove(candidate).unwrap(); - let transfers_add = sum_surplus_transfers(&entry, &value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts); - candidate_transfers.insert(candidate, transfers_orig + transfers_add); + transfer_table.add_transfers(&value_fraction, candidate, &entry.num_ballots); + + let mut new_value_fraction; + if opts.surplus.is_weighted() { + new_value_fraction = value_fraction.clone(); + new_value_fraction *= surplus_numer.as_ref().unwrap(); // Guaranteed to be Some in WIGM + if let Some(n) = &surplus_denom { + new_value_fraction /= n; + } + } else { + if let Some(sf) = &surplus_fraction { + new_value_fraction = sf.clone(); + } else { + new_value_fraction = value_fraction.clone(); + } + } + + if let Some(dps) = opts.round_values { + new_value_fraction.floor_mut(dps); + } // Transfer candidate votes let parcel = Parcel { votes: entry.votes, - value_fraction: reweight_value_fraction(&value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_surplus_fractions), + value_fraction: new_value_fraction, source_order: state.num_elected + state.num_excluded, }; let count_card = state.candidates.get_mut(candidate).unwrap(); - count_card.ballot_transfers += parcel.num_ballots(); count_card.parcels.push(parcel); } // Record exhausted votes - if opts.transferable_only { - if transferable_votes > surplus { - // No ballots exhaust - } else { - exhausted_transfers += &surplus - &transferable_votes; - } - } else { - exhausted_transfers += sum_surplus_transfers(&result.exhausted, &value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts); - } + transfer_table.add_exhausted(&value_fraction, &result.exhausted.num_ballots); // Transfer exhausted votes let parcel = Parcel { @@ -415,29 +344,13 @@ where value_fraction: value_fraction, // TODO: Reweight exhausted votes source_order: state.num_elected + state.num_excluded, }; - state.exhausted.ballot_transfers += parcel.num_ballots(); state.exhausted.parcels.push(parcel); } let mut checksum = N::new(); // Credit transferred votes - // ballot_transfers updated above - for (candidate, mut votes) in candidate_transfers { - if let Some(dps) = opts.round_votes { - votes.floor_mut(dps); - } - let count_card = state.candidates.get_mut(candidate).unwrap(); - count_card.transfer(&votes); - checksum += votes; - } - - // Credit exhausted votes - if let Some(dps) = opts.round_votes { - exhausted_transfers.floor_mut(dps); - } - state.exhausted.transfer(&exhausted_transfers); - checksum += exhausted_transfers; + checksum += transfer_table.apply_to(state, opts, Some(&surplus), &surplus_numer, &surplus_denom); // Finalise candidate votes let count_card = state.candidates.get_mut(elected_candidate).unwrap(); @@ -690,7 +603,7 @@ where } // Credit transferred votes - checksum += transfer_table.apply_to(state, opts); + checksum += transfer_table.apply_to(state, opts, None, &None, &None); if !votes_remain { // Finalise candidate votes diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 5e99aa1..bd075a2 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -403,6 +403,15 @@ impl SurplusMethod { SurplusMethod::Hare => "--surplus hare", }.to_string() } + + /// Returns `true` if this is a weighted method + pub fn is_weighted(&self) -> bool { + return match self { + SurplusMethod::WIG => { true } + SurplusMethod::UIG | SurplusMethod::EG => { false } + _ => unreachable!() + }; + } } impl> From for SurplusMethod { diff --git a/src/stv/transfers.rs b/src/stv/transfers.rs index ad3b1ec..cfe8ba5 100644 --- a/src/stv/transfers.rs +++ b/src/stv/transfers.rs @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -use super::STVOptions; +use super::{STVOptions, SumSurplusTransfersMode}; use crate::election::{Candidate, CountState}; use crate::numbers::Number; @@ -74,8 +74,9 @@ impl<'e, N: Number> TransferTable<'e, N> { /// Apply the transfers described in the table to the count sheet /// /// Credit continuing candidates and exhausted pile with the appropriate number of ballot papers and votes. - pub fn apply_to(&self, state: &mut CountState, opts: &STVOptions) -> N { - // TODO: SumSurplusTransfers + pub fn apply_to(&self, state: &mut CountState, opts: &STVOptions, surplus: Option<&N>, surplus_numer: &Option, surplus_denom: &Option) -> N { + // Use weighted rules if exclusion or WIGM + let is_weighted = surplus.is_none() || opts.surplus.is_weighted(); let mut checksum = N::new(); @@ -84,13 +85,73 @@ impl<'e, N: Number> TransferTable<'e, N> { let mut votes_transferred = N::new(); let mut ballots_transferred = N::new(); - for column in self.columns.iter() { - if let Some(cell) = column.cells.get(*candidate) { - votes_transferred += cell.ballots.clone() * &column.value_fraction; - ballots_transferred += &cell.ballots; + // If exclusion, or surplus at present value, or SumSurplusTransfersMode::ByValue + if surplus_numer.is_none() || opts.sum_surplus_transfers == SumSurplusTransfersMode::ByValue { + // Calculate transfer across all votes in this parcel + for column in self.columns.iter() { + if let Some(cell) = column.cells.get(*candidate) { + if is_weighted { + votes_transferred += cell.ballots.clone() * &column.value_fraction; + } + ballots_transferred += &cell.ballots; + } } + + if !is_weighted { + votes_transferred = ballots_transferred.clone(); + } + + // If surplus, multiply by surplus fraction + if let Some(n) = &surplus_numer { + votes_transferred *= n; + } + if let Some(n) = &surplus_denom { + votes_transferred /= n; + } + } else if opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot { + // Sum transfer per each individual ballot + for column in self.columns.iter() { + if let Some(cell) = column.cells.get(*candidate) { + ballots_transferred += &cell.ballots; + + let mut new_value_fraction; + if is_weighted { + new_value_fraction = column.value_fraction.clone(); + // If surplus, multiply by surplus fraction + if let Some(n) = &surplus_numer { + new_value_fraction *= n; + } + if let Some(n) = &surplus_denom { + new_value_fraction /= n; + } + // Round if required + if let Some(dps) = opts.round_values { + new_value_fraction.floor_mut(dps); + } + } else { + if let Some(n) = &surplus_numer { + new_value_fraction = n.clone(); + } else { + // Transferred at original value + new_value_fraction = column.value_fraction.clone(); + } + if let Some(n) = &surplus_denom { + new_value_fraction /= n; + } + // Round if required + if let Some(dps) = opts.round_values { + new_value_fraction.floor_mut(dps); + } + } + + votes_transferred += cell.ballots.clone() * new_value_fraction; + } + } + } else { + unreachable!(); } + // Round if required if let Some(dps) = opts.round_votes { votes_transferred.floor_mut(dps); } @@ -102,23 +163,56 @@ impl<'e, N: Number> TransferTable<'e, N> { } // Credit exhausted votes - let mut votes_transferred = N::new(); - let mut ballots_transferred = N::new(); - - for column in self.columns.iter() { - votes_transferred += column.exhausted.ballots.clone() * &column.value_fraction; - ballots_transferred += &column.exhausted.ballots; + // If exclusion or not --transferable-only + if surplus.is_none() || !opts.transferable_only { + // Standard rules + let mut votes_transferred = N::new(); + let mut ballots_transferred = N::new(); + + for column in self.columns.iter() { + if is_weighted { + votes_transferred += column.exhausted.ballots.clone() * &column.value_fraction; + } + ballots_transferred += &column.exhausted.ballots; + } + + if !is_weighted { + votes_transferred = ballots_transferred.clone(); + } + + // If surplus, multiply by surplus fraction + if let Some(n) = &surplus_numer { + votes_transferred *= n; + } + if let Some(n) = &surplus_denom { + votes_transferred /= n; + } + + // Round if required + if let Some(dps) = opts.round_votes { + votes_transferred.floor_mut(dps); + } + + state.exhausted.transfer(&votes_transferred); + state.exhausted.ballot_transfers += ballots_transferred; + + checksum += votes_transferred; + } else { + // Credit only nontransferable difference + if surplus_numer.is_none() { + // TODO: Is there a purer way of calculating this? + let difference = surplus.unwrap().clone() - &checksum; + state.exhausted.transfer(&difference); + checksum += difference; + + for column in self.columns.iter() { + state.exhausted.ballot_transfers += &column.exhausted.ballots; + } + } else { + // No ballots exhaust + } } - if let Some(dps) = opts.round_votes { - votes_transferred.floor_mut(dps); - } - - state.exhausted.transfer(&votes_transferred); - state.exhausted.ballot_transfers += ballots_transferred; - - checksum += votes_transferred; - return checksum; } }