From 8829fa5a7be38584e030b964ed171ea64597de34 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 16 Jun 2021 17:20:29 +1000 Subject: [PATCH] Update documentation --- README.md | 1 + docs/options.md | 7 +++++- src/election.rs | 52 ++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 4 ++++ src/logger.rs | 3 +++ src/numbers/fixed.rs | 2 ++ src/numbers/gfixed.rs | 1 + src/stv/mod.rs | 40 +++++++++++++++++++++++++++++++++ src/stv/wasm.rs | 17 +++++++++++--- src/ties.rs | 4 ++++ 10 files changed, 126 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ebcd5b7..58a28f7 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ OpenTally accepts data in the [BLT file format](https://yingtongli.me/git/OpenTa * weighted inclusive Gregory STV (e.g. [Scottish STV](https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made)) * unweighted inclusive Gregory STV (e.g. [Australian Senate STV](https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700)) * exclusive Gregory STV (e.g. [PRSA 1977](https://www.prsa.org.au/rule1977.htm) and [ERS97](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/)) +* [Meek STV](http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) – with [tree-packed ballots](http://www.votingmatters.org.uk/ISSUE21/I21P1.pdf) for efficient computation OpenTally is highly customisable, including options for: diff --git a/docs/options.md b/docs/options.md index 3bf048e..f44595c 100644 --- a/docs/options.md +++ b/docs/options.md @@ -6,6 +6,7 @@ The preset dropdown allows you to choose from a hardcoded list of preloaded STV * *Recommended WIGM*: A recommended set of simple STV rules designed for computer counting, using the weighted inclusive Gregory method and rational arithmetic. * *Scottish STV*: Rules from the [*Scottish Local Government Elections Order 2011*](https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made), using the weighted inclusive Gregory method. Validated against the [2007 Scottish local government election result for Linn ward](https://web.archive.org/web/20121004213938/http://www.glasgow.gov.uk/en/YourCouncil/Elections_Voting/Election_Results/ElectionScotland2007/LGWardResults.htm?ward=1&wardname=1%20-%20Linn). +* [*Meek STV*](http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf): Advanced STV rules designed for computer counting, recognised by the Proportional Representation Society of Australia (Victoria–Tasmania) as the superior STV system. Validated against the [Hill–Wichmann–Woodall implementation](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) for the ERS97 model election (see below). * *Australian Senate STV*: Rules from the [*Commonwealth Electoral Act 1918*](https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700), using the unweighted inclusive Gregory method. Validated against the [2019 Australian Senate election result for Tasmania](https://results.aec.gov.au/24310/Website/SenateDownloadsMenu-24310-Csv.htm). * [*PRSA 1977*](https://www.prsa.org.au/rule1977.htm): Simple rules designed for hand counting, using the exclusive Gregory method, with counting automatically performed in thousandths of a vote. Validated against [example 1](https://www.prsa.org.au/example1.pdf) of the PRSA's [*Proportional Representation Manual*](https://www.prsa.org.au/publicat.htm#p2). * [*ERS97*](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/): More complex rules designed for hand counting, using the exclusive Gregory method. Validated against the ERS97 [model election](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/#sub-section-24). @@ -40,6 +41,8 @@ This option allows you to specify whether the votes required for election can ch * *Static quota*: The quota is calculated once after all first-preference votes are allocated, and remains constant throughout the count. * *Static with ERS97 rules*: The quota is static, but candidates may be elected if their vote exceeds (or equals, according to the *Quota criterion*) the total active vote, divided by (*S* + 1) (or *S*, according to the *Quota* option). +When *Surplus method* is set to *Meek method*, this setting is ignored, and the progressively reducing quota of the Meek method is instead applied. + ## STV variants ### Surplus order (--surplus-order) @@ -56,7 +59,7 @@ Some STV counting rules provide, for example, that ‘no surplus shall be transf This dropdown allows you to select how ballots are transferred during surplus transfers. The recommended methods are: * *Weighted inclusive Gregory* (default): During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are weighted according to the weights of the ballot papers. -* *Meek STV*: Transfers are computed as described at . +* *Meek method*: Transfers are computed as described at . Other methods are supported, but not recommended: @@ -76,6 +79,8 @@ Other surplus transfer methods, such as non-fractional transfers (e.g. random sa * *Exclude by parcel (by order)*: When excluding a candidate, transfer their ballot papers one parcel at a time, in the order each was received. Each parcel forms a separate stage, i.e. if a transfer allows another candidate to meet the quota criterion, no further papers are transferred to that candidate. This option cannot be combined with bulk exclusion. * *Exclude by value*: When excluding candidate(s), transfer their ballot papers in descending order of accumulated transfer value. Each transfer of all ballots of a certain transfer value forms a separate stage. +When *Surplus method* is set to *Meek method*, this setting is ignored, and the Meek method is instead applied. + ### Ties (-t/--ties) This dropdown allows you to select how ties (in surplus transfer or exclusion) are broken. The options are: diff --git a/src/election.rs b/src/election.rs index a8ddb60..e92eb00 100644 --- a/src/election.rs +++ b/src/election.rs @@ -23,10 +23,15 @@ use std::collections::HashMap; /// An election to be counted pub struct Election { + /// Name of the election pub name: String, + /// Number of candidates to be elected pub seats: usize, + /// [Vec] of [Candidate]s in the election pub candidates: Vec, + /// Indexes of withdrawn candidates pub withdrawn_candidates: Vec, + /// [Vec] of [Ballot]s cast in the election pub ballots: Vec>, } @@ -125,32 +130,52 @@ impl Election { /// A candidate in an [Election] #[derive(PartialEq, Eq, Hash)] pub struct Candidate { + /// Name of the candidate pub name: String, } /// The current state of counting an [Election] //#[derive(Clone)] pub struct CountState<'a, N: Number> { + /// Pointer to the [Election] being counted pub election: &'a Election, + /// [HashMap] of [CountCard]s for each [Candidate] in the election pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>, + /// [CountCard] representing the exhausted pile pub exhausted: CountCard<'a, N>, + /// [CountCard] representing loss by fraction pub loss_fraction: CountCard<'a, N>, + /// [crate::stv::meek::BallotTree] for Meek STV pub ballot_tree: Option>, + /// Values used to break ties, based on forwards tie-breaking pub forwards_tiebreak: Option>, + /// Values used to break ties, based on backwards tie-breaking pub backwards_tiebreak: Option>, + /// [SHARandom] for random tie-breaking pub random: Option>, + /// Quota for election pub quota: Option, + /// Vote required for election + /// + /// With a static quota, this is equal to the quota. With ERS97 rules, this may vary from the quota. pub vote_required_election: Option, + /// Number of candidates who have been declared elected pub num_elected: usize, + /// Number of candidates who have been declared excluded pub num_excluded: usize, + /// The type of stage being counted + /// + /// For example, "Surplus of", "Exclusion of" pub kind: Option<&'a str>, + /// The description of the stage being counted, excluding [CountState::kind] pub title: String, + /// [Logger] for this stage of the count pub logger: Logger<'a>, } @@ -199,7 +224,11 @@ 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: Number> { - State(CountState<'a, N>), // NYI: May be used e.g. for tie-breaking or rollback-based constraints + /// Cloned [CountState] + /// + /// Currently unused/unimplemented, but may be used in future for rollback-based constraints + State(CountState<'a, N>), + /// Reference to a [CountState] Ref(&'a CountState<'a, N>), } @@ -220,22 +249,33 @@ impl<'a, N: Number> CountStateOrRef<'a, N> { /// Result of a stage of counting pub struct StageResult<'a, N: Number> { + /// See [CountState::kind] pub kind: Option<&'a str>, + /// See [CountState::title] pub title: &'a String, + /// Detailed logs of this stage, rendered from [CountState::logger] pub logs: Vec, + /// Reference to the [CountState] or cloned [CountState] of this stage pub state: CountStateOrRef<'a, N>, } /// Current state of a [Candidate] during an election count #[derive(Clone)] pub struct CountCard<'a, N> { + /// State of the candidate pub state: CandidateState, + /// Order of election or exclusion + /// + /// Positive integers represent order of election; negative integers represent order of exclusion pub order_elected: isize, //pub orig_votes: N, + /// Net votes transferred to this candidate in this stage pub transfers: N, + /// Votes of the candidate at the end of this stage pub votes: N, + /// Parcels of ballots assigned to this candidate pub parcels: Vec>, /// Candidate's keep value (Meek STV) @@ -275,7 +315,9 @@ pub type Parcel<'a, N> = Vec>; /// Represents a [Ballot] with an associated value #[derive(Clone)] pub struct Vote<'a, N> { + /// Ballot from which the vote is derived pub ballot: &'a Ballot, + /// Current value of the ballot pub value: N, /// Index of the next preference to examine pub up_to_pref: usize, @@ -283,7 +325,9 @@ pub struct Vote<'a, N> { /// A record of a voter's preferences pub struct Ballot { + /// Original value/weight of the ballot pub orig_value: N, + /// Indexes of candidates preferenced on the ballot pub preferences: Vec, } @@ -292,10 +336,16 @@ pub struct Ballot { #[derive(PartialEq)] #[derive(Clone)] pub enum CandidateState { + /// Hopeful (continuing candidate) Hopeful, + /// Required by constraints to be guarded from exclusion Guarded, + /// Declared elected Elected, + /// Required by constraints to be doomed to be excluded Doomed, + /// Withdrawn candidate Withdrawn, + /// Declared excluded Excluded, } diff --git a/src/lib.rs b/src/lib.rs index 3b08338..02888ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,10 @@ * along with this program. If not, see . */ +#![warn(missing_docs)] + +//! Open source counting software for various preferential voting election systems + /// Data types for representing abstract elections pub mod election; /// Smart logging framework diff --git a/src/logger.rs b/src/logger.rs index 7dbd85e..33b5426 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -18,6 +18,7 @@ /// Smart logger used in election counts #[derive(Clone)] pub struct Logger<'a> { + /// [Vec] of log entries for the current stage pub entries: Vec>, } @@ -73,7 +74,9 @@ impl<'a> Logger<'a> { /// Represents either a literal or smart log entry #[derive(Clone)] pub enum LogEntry<'a> { + /// Smart log entry - see [SmartLogEntry] Smart(SmartLogEntry<'a>), + /// Literal log entry Literal(String) } diff --git a/src/numbers/fixed.rs b/src/numbers/fixed.rs index 5b5470e..8f5b6e2 100644 --- a/src/numbers/fixed.rs +++ b/src/numbers/fixed.rs @@ -42,6 +42,7 @@ fn get_factor() -> &'static IBig { pub struct Fixed(IBig); impl Fixed { + /// Set the number of decimal places to compute results to pub fn set_dps(dps: usize) { unsafe { DPS = Some(dps); @@ -108,6 +109,7 @@ impl From for Fixed { } } +// TODO: Fix rounding impl fmt::Display for Fixed { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let dps = match f.precision() { diff --git a/src/numbers/gfixed.rs b/src/numbers/gfixed.rs index ec10a9e..ed72448 100644 --- a/src/numbers/gfixed.rs +++ b/src/numbers/gfixed.rs @@ -48,6 +48,7 @@ fn get_factor_cmp() -> &'static IBig { pub struct GuardedFixed(IBig); impl GuardedFixed { + /// Set the number of decimal places to compute results to pub fn set_dps(dps: usize) { unsafe { DPS = Some(dps); diff --git a/src/stv/mod.rs b/src/stv/mod.rs index f77c9bf..88ec274 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -22,6 +22,7 @@ pub mod gregory; /// Meek method of surplus distributions, etc. pub mod meek; +/// WebAssembly wrappers //#[cfg(target_arch = "wasm32")] pub mod wasm; @@ -38,22 +39,39 @@ use std::ops; /// Options for conducting an STV count pub struct STVOptions { + /// Round transfer values to specified decimal places pub round_tvs: Option, + /// Round ballot weights to specified decimal places pub round_weights: Option, + /// Round votes to specified decimal places pub round_votes: Option, + /// Round quota to specified decimal places pub round_quota: Option, + /// How to calculate votes to credit to candidates in surplus transfers pub sum_surplus_transfers: SumSurplusTransfersMode, + /// Convert ballots with value >1 to multiple ballots of value 1 pub normalise_ballots: bool, + /// Quota type pub quota: QuotaType, + /// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota pub quota_criterion: QuotaCriterion, + /// Whether to apply a form of progressive quota pub quota_mode: QuotaMode, + /// Tie-breaking method pub ties: Vec, + /// Method of surplus distributions pub surplus: SurplusMethod, + /// Order to distribute surpluses pub surplus_order: SurplusOrder, + /// Examine only transferable papers during surplus distributions pub transferable_only: bool, + /// Method of exclusions pub exclusion: ExclusionMethod, + /// Use bulk exclusion pub bulk_exclude: bool, + /// Defer surplus distributions if possible pub defer_surpluses: bool, + /// Print votes to specified decimal places in results report pub pp_decimals: usize, } @@ -172,8 +190,11 @@ impl STVOptions { #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SumSurplusTransfersMode { + /// Sum and round all surplus transfers for a candidate in a single step SingleStep, + /// Sum and round a candidate's surplus transfers separately for ballot papers received at each particular value ByValue, + /// Sum and round a candidate's surplus transfers individually for each ballot paper PerBallot, } @@ -193,9 +214,13 @@ impl SumSurplusTransfersMode { #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum QuotaType { + /// Droop quota Droop, + /// Hare quota Hare, + /// Exact Droop quota (Newland–Britton/Hagenbach-Bischoff quota) DroopExact, + /// Exact Hare quota HareExact, } @@ -216,7 +241,9 @@ impl QuotaType { #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum QuotaCriterion { + /// Elect candidates on equalling or exceeding the quota GreaterOrEqual, + /// Elect candidates on strictly exceeding the quota Greater, } @@ -235,7 +262,9 @@ impl QuotaCriterion { #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum QuotaMode { + /// Static quota Static, + /// Static quota with ERS97 rules ERS97, } @@ -254,9 +283,13 @@ impl QuotaMode { #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SurplusMethod { + /// Weighted inclusive Gregory method WIG, + /// Unweighted inclusive Gregory method UIG, + /// Exclusive Gregory method (last bundle) EG, + /// Meek method Meek, } @@ -277,7 +310,9 @@ impl SurplusMethod { #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SurplusOrder { + /// Transfer the largest surplus first, even if it arose at a later stage of the count BySize, + /// Transfer the surplus of the candidate elected first, even if it is smaller than another ByOrder, } @@ -296,8 +331,11 @@ impl SurplusOrder { #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum ExclusionMethod { + /// Transfer all ballot papers of an excluded candidate in one stage SingleStage, + /// Transfer the ballot papers of an excluded candidate in descending order of accumulated transfer value ByValue, + /// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received ParcelsByOrder, } @@ -316,7 +354,9 @@ impl ExclusionMethod { #[wasm_bindgen] #[derive(Debug)] pub enum STVError { + /// User input is required RequireInput, + /// Tie could not be resolved UnresolvedTie, } diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 34f978a..0101c3b 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -129,27 +129,38 @@ macro_rules! impl_type { } // Wrapper structs - // Required as we cannot specify &'static in wasm-bindgen: issue #1187 /// Wrapper for [CountState] + /// + /// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187). + /// #[wasm_bindgen] pub struct [](CountState<'static, $type>); #[wasm_bindgen] impl [] { + /// Create a new [CountState] wrapper pub fn new(election: &[]) -> Self { return [](CountState::new(election.as_static())); } } /// Wrapper for [Election] + /// + /// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187). + /// #[wasm_bindgen] pub struct [](Election<$type>); #[wasm_bindgen] impl [] { + /// Return [Election::seats] pub fn seats(&self) -> usize { self.0.seats } + /// Return the underlying [Election] as a `&'static Election` + /// + /// # Safety + /// This assumes that the underlying [Election] is valid for the `'static` lifetime, as it would be if the [Election] were created from Javascript. + /// fn as_static(&self) -> &'static Election<$type> { - // Need to have this as we cannot specify &'static in wasm-bindgen: issue #1187 unsafe { let ptr = &self.0 as *const Election<$type>; &*ptr @@ -218,7 +229,7 @@ impl STVOptions { /// Return the underlying [stv::STVOptions] as a `&'static stv::STVOptions` /// /// # Safety - /// Assumes that the underlying [stv::STVOptions] is valid for the `'static` lifetime, as it would be if the [stv::STVOptions] were created from Javascript + /// This assumes that the underlying [stv::STVOptions] is valid for the `'static` lifetime, as it would be if the [stv::STVOptions] were created from Javascript. /// fn as_static(&self) -> &'static stv::STVOptions { unsafe { diff --git a/src/ties.rs b/src/ties.rs index 4d09d83..809d24d 100644 --- a/src/ties.rs +++ b/src/ties.rs @@ -29,9 +29,13 @@ use std::io::{stdin, stdout, Write}; /// Strategy for breaking ties #[derive(PartialEq)] pub enum TieStrategy { + /// Break ties according to the candidate who first had more/fewer votes Forwards, + /// Break ties according to the candidate who most recently had more/fewer votes Backwards, + /// Break ties randomly (see [crate::sharandom]) Random(String), + /// Prompt the user to break ties Prompt, }