Implement --ties random

This commit is contained in:
RunasSudo 2021-06-13 03:15:15 +10:00
parent 266d8e2495
commit 4845ebe52f
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
12 changed files with 192 additions and 13 deletions

63
Cargo.lock generated
View File

@ -38,6 +38,15 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "0.2.16" version = "0.2.16"
@ -122,6 +131,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cpufeatures"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.2.1" version = "1.2.1"
@ -171,6 +189,15 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "doc-comment" name = "doc-comment"
version = "0.3.3" version = "0.3.3"
@ -195,6 +222,16 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "generic-array"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "git-version" name = "git-version"
version = "0.3.4" version = "0.3.4"
@ -368,6 +405,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "opentally" name = "opentally"
version = "0.1.0" version = "0.1.0"
@ -387,6 +430,7 @@ dependencies = [
"num-traits", "num-traits",
"paste", "paste",
"rug", "rug",
"sha2",
"wasm-bindgen", "wasm-bindgen",
"xmltree", "xmltree",
] ]
@ -524,6 +568,19 @@ version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
[[package]]
name = "sha2"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
dependencies = [
"block-buffer",
"cfg-if 1.0.0",
"cpufeatures",
"digest",
"opaque-debug",
]
[[package]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@ -553,6 +610,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
[[package]]
name = "typenum"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.7.1" version = "1.7.1"

View File

@ -13,6 +13,7 @@ git-version = "0.3.4"
ibig = "0.3.2" ibig = "0.3.2"
itertools = "0.10.1" itertools = "0.10.1"
num-traits = "0.2" num-traits = "0.2"
sha2 = "0.9.5"
wasm-bindgen = "0.2.74" wasm-bindgen = "0.2.74"
# Only for WebAssembly - include here for syntax highlighting # Only for WebAssembly - include here for syntax highlighting

11
docs/rng.md Normal file
View File

@ -0,0 +1,11 @@
# Deterministic random number generator specification
The deterministic random number generator used in OpenTally is based on an algorithm by [Ronald L Rivest](https://people.csail.mit.edu/rivest/sampler.py).
The algorithm takes a *seed* value, which is an arbitrary character string. The algorithm has, in its internal state, a *counter*, whose value is initially 0.
In order to generate a value between 0 (inclusive) and *n* (exclusive), to the state is appended a comma (",") followed by the value of the counter, and a SHA-256 hash *H* is computed of the resulting string encoded using UTF-8. The hash *H* is represented as an unsigned hexadecimal integer, *k*. The counter is incremented by 1.
In order to avoid modulo bias, if *k* ≥ ⌊*M*/*n*⌋ × *n* (where *M* = 2^256), *k* is discarded and the algorithm is repeated.
Otherwise, the result is *k* modulo *n*.

View File

@ -127,10 +127,10 @@
<option value="prompt">Prompt</option> <option value="prompt">Prompt</option>
</select> </select>
</label> </label>
<!--<label> <label>
Random seed: Random seed:
<input type="text" id="txtSeed" value=""> <input type="text" id="txtSeed" value="">
</label>--> </label>
</div> </div>
<!--<div class="subheading"> <!--<div class="subheading">
Constraints: Constraints:

View File

@ -108,6 +108,7 @@ async function clickCount() {
document.getElementById('selQuotaCriterion').value, document.getElementById('selQuotaCriterion').value,
document.getElementById('selQuotaMode').value, document.getElementById('selQuotaMode').value,
document.getElementById('selTies').value.split(','), document.getElementById('selTies').value.split(','),
document.getElementById('txtSeed').value,
document.getElementById('selTransfers').value, document.getElementById('selTransfers').value,
document.getElementById('selSurplus').value, document.getElementById('selSurplus').value,
document.getElementById('selPapers').value == 'transferable', document.getElementById('selPapers').value == 'transferable',
@ -129,6 +130,13 @@ async function clickCount() {
}); });
} }
// Provide a default seed
if (document.getElementById('txtSeed').value === '') {
function pad(x) { if (x < 10) { return '0' + x; } return '' + x; }
let d = new Date();
document.getElementById('txtSeed').value = d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate());
}
// Print logic // Print logic
async function printResult() { async function printResult() {

View File

@ -17,6 +17,7 @@
use crate::logger::Logger; use crate::logger::Logger;
use crate::numbers::Number; use crate::numbers::Number;
use crate::sharandom::SHARandom;
use std::collections::HashMap; use std::collections::HashMap;
@ -133,6 +134,7 @@ pub struct CountState<'a, N> {
pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>, pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>, pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
pub random: Option<SHARandom<'a>>,
pub quota: Option<N>, pub quota: Option<N>,
pub vote_required_election: Option<N>, pub vote_required_election: Option<N>,
@ -154,6 +156,7 @@ impl<'a, N: Number> CountState<'a, N> {
loss_fraction: CountCard::new(), loss_fraction: CountCard::new(),
forwards_tiebreak: None, forwards_tiebreak: None,
backwards_tiebreak: None, backwards_tiebreak: None,
random: None,
quota: None, quota: None,
vote_required_election: None, vote_required_election: None,
num_elected: 0, num_elected: 0,

View File

@ -18,6 +18,7 @@
pub mod election; pub mod election;
pub mod logger; pub mod logger;
pub mod numbers; pub mod numbers;
pub mod sharandom;
pub mod stv; pub mod stv;
pub mod ties; pub mod ties;

View File

@ -108,6 +108,10 @@ struct STV {
#[clap(help_heading=Some("STV VARIANTS"), short='t', long, possible_values=&["forwards", "backwards", "random", "prompt"], default_value="prompt", value_name="methods")] #[clap(help_heading=Some("STV VARIANTS"), short='t', long, possible_values=&["forwards", "backwards", "random", "prompt"], default_value="prompt", value_name="methods")]
ties: Vec<String>, ties: Vec<String>,
/// Random seed to use with --ties random
#[clap(help_heading=Some("STV VARIANTS"), long, value_name="seed")]
random_seed: Option<String>,
/// Method of surplus distributions /// Method of surplus distributions
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")] #[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
surplus: String, surplus: String,
@ -192,6 +196,7 @@ where
&cmd_opts.quota_criterion, &cmd_opts.quota_criterion,
&cmd_opts.quota_mode, &cmd_opts.quota_mode,
&cmd_opts.ties, &cmd_opts.ties,
&cmd_opts.random_seed,
&cmd_opts.surplus, &cmd_opts.surplus,
&cmd_opts.surplus_order, &cmd_opts.surplus_order,
cmd_opts.transferable_only, cmd_opts.transferable_only,

63
src/sharandom.rs Normal file
View File

@ -0,0 +1,63 @@
/* 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 ibig::UBig;
use sha2::{Digest, Sha256};
#[derive(Clone)]
pub struct SHARandom<'r> {
seed: &'r str,
counter: usize,
}
impl<'r> SHARandom<'r> {
pub fn new(seed: &'r str) -> Self {
Self {
seed: seed,
counter: 0,
}
}
pub fn next(&mut self, max: usize) -> usize {
let mut hasher = Sha256::new();
hasher.update(format!("{},{}", self.seed, self.counter).as_bytes());
self.counter += 1;
let hash = hasher.finalize();
let hash = UBig::from_be_bytes(&hash);
if hash >= UBig::from(2_u32).pow(256) / UBig::from(max) * UBig::from(max) {
return self.next(max);
}
return (hash % UBig::from(max)).to_string().parse().unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sharandom1() {
let mut random = SHARandom::new(&"foobar");
assert_eq!(random.next(10), 0);
assert_eq!(random.next(42), 30);
assert_eq!(random.next(64), 13);
}
}

View File

@ -22,6 +22,7 @@ pub mod wasm;
use crate::numbers::Number; use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
use crate::sharandom::SHARandom;
use crate::ties::TieStrategy; use crate::ties::TieStrategy;
use itertools::Itertools; use itertools::Itertools;
@ -31,7 +32,7 @@ use std::cmp::max;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops; use std::ops;
pub struct STVOptions<'o> { pub struct STVOptions {
pub round_tvs: Option<usize>, pub round_tvs: Option<usize>,
pub round_weights: Option<usize>, pub round_weights: Option<usize>,
pub round_votes: Option<usize>, pub round_votes: Option<usize>,
@ -41,7 +42,7 @@ pub struct STVOptions<'o> {
pub quota: QuotaType, pub quota: QuotaType,
pub quota_criterion: QuotaCriterion, pub quota_criterion: QuotaCriterion,
pub quota_mode: QuotaMode, pub quota_mode: QuotaMode,
pub ties: Vec<TieStrategy<'o>>, pub ties: Vec<TieStrategy>,
pub surplus: SurplusMethod, pub surplus: SurplusMethod,
pub surplus_order: SurplusOrder, pub surplus_order: SurplusOrder,
pub transferable_only: bool, pub transferable_only: bool,
@ -51,7 +52,7 @@ pub struct STVOptions<'o> {
pub pp_decimals: usize, pub pp_decimals: usize,
} }
impl<'o> STVOptions<'o> { impl STVOptions {
pub fn new( pub fn new(
round_tvs: Option<usize>, round_tvs: Option<usize>,
round_weights: Option<usize>, round_weights: Option<usize>,
@ -63,6 +64,7 @@ impl<'o> STVOptions<'o> {
quota_criterion: &str, quota_criterion: &str,
quota_mode: &str, quota_mode: &str,
ties: &Vec<String>, ties: &Vec<String>,
random_seed: &Option<String>,
surplus: &str, surplus: &str,
surplus_order: &str, surplus_order: &str,
transferable_only: bool, transferable_only: bool,
@ -103,7 +105,7 @@ impl<'o> STVOptions<'o> {
ties: ties.into_iter().map(|t| match t.as_str() { ties: ties.into_iter().map(|t| match t.as_str() {
"forwards" => TieStrategy::Forwards, "forwards" => TieStrategy::Forwards,
"backwards" => TieStrategy::Backwards, "backwards" => TieStrategy::Backwards,
"random" => TieStrategy::Random(&"TODO"), "random" => TieStrategy::Random(random_seed.as_ref().expect("Must provide a --random-seed if using --ties random").clone()),
"prompt" => TieStrategy::Prompt, "prompt" => TieStrategy::Prompt,
_ => panic!("Invalid --ties"), _ => panic!("Invalid --ties"),
}).collect(), }).collect(),
@ -143,6 +145,7 @@ impl<'o> STVOptions<'o> {
if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); } if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); }
let ties_str = self.ties.iter().map(|t| t.describe()).join(" "); let ties_str = self.ties.iter().map(|t| t.describe()).join(" ");
if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); } if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); }
for t in self.ties.iter() { if let TieStrategy::Random(seed) = t { flags.push(format!("--random-seed {}", seed)); } }
if self.quota != QuotaType::DroopExact { flags.push(self.quota.describe()); } if self.quota != QuotaType::DroopExact { flags.push(self.quota.describe()); }
if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); } if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); }
if self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); } if self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); }
@ -295,7 +298,14 @@ pub enum STVError {
UnresolvedTie, UnresolvedTie,
} }
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) { pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions) {
// Initialise RNG
for t in opts.ties.iter() {
if let TieStrategy::Random(seed) = t {
state.random = Some(SHARandom::new(seed));
}
}
distribute_first_preferences(&mut state); distribute_first_preferences(&mut state);
calculate_quota(&mut state, opts); calculate_quota(&mut state, opts);
elect_meeting_quota(&mut state, opts); elect_meeting_quota(&mut state, opts);

View File

@ -56,7 +56,7 @@ macro_rules! impl_type {
#[wasm_bindgen] #[wasm_bindgen]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) { pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) {
stv::count_init(&mut state.0, &opts.0); stv::count_init(&mut state.0, opts.as_static());
} }
#[wasm_bindgen] #[wasm_bindgen]
@ -141,7 +141,7 @@ impl_type!(NativeFloat64);
impl_type!(Rational); impl_type!(Rational);
#[wasm_bindgen] #[wasm_bindgen]
pub struct STVOptions(stv::STVOptions<'static>); pub struct STVOptions(stv::STVOptions);
#[wasm_bindgen] #[wasm_bindgen]
impl STVOptions { impl STVOptions {
@ -156,6 +156,7 @@ impl STVOptions {
quota_criterion: &str, quota_criterion: &str,
quota_mode: &str, quota_mode: &str,
ties: Array, ties: Array,
random_seed: String,
surplus: &str, surplus: &str,
surplus_order: &str, surplus_order: &str,
transferable_only: bool, transferable_only: bool,
@ -175,6 +176,7 @@ impl STVOptions {
quota_criterion, quota_criterion,
quota_mode, quota_mode,
&ties.iter().map(|v| v.as_string().unwrap()).collect(), &ties.iter().map(|v| v.as_string().unwrap()).collect(),
&Some(random_seed),
surplus, surplus,
surplus_order, surplus_order,
transferable_only, transferable_only,
@ -186,6 +188,15 @@ impl STVOptions {
} }
} }
impl STVOptions {
fn as_static(&self) -> &'static stv::STVOptions {
unsafe {
let ptr = &self.0 as *const stv::STVOptions;
&*ptr
}
}
}
// Reporting // Reporting
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String { fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {

View File

@ -27,14 +27,14 @@ use wasm_bindgen::prelude::wasm_bindgen;
use std::io::{stdin, stdout, Write}; use std::io::{stdin, stdout, Write};
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum TieStrategy<'s> { pub enum TieStrategy {
Forwards, Forwards,
Backwards, Backwards,
Random(&'s str), Random(String),
Prompt, Prompt,
} }
impl<'s> TieStrategy<'s> { impl TieStrategy {
pub fn describe(&self) -> String { pub fn describe(&self) -> String {
match self { match self {
Self::Forwards => "forwards", Self::Forwards => "forwards",
@ -73,7 +73,10 @@ impl<'s> TieStrategy<'s> {
return Ok(candidates[0]); return Ok(candidates[0]);
} }
} }
Self::Random(_seed) => { todo!() } Self::Random(_) => {
state.logger.log_literal(format!("Tie between {} broken at random.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
return Ok(candidates[state.random.as_mut().unwrap().next(candidates.len())]);
}
Self::Prompt => { Self::Prompt => {
match prompt(candidates) { match prompt(candidates) {
Ok(c) => { Ok(c) => {