diff --git a/README.md b/README.md index 32747e5..1048edf 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ OpenTally is highly customisable, including options for: * different quotas and quota rules (e.g. exact Droop, Hare) * calculations using fixed-point arithmetic, guarded fixed-point ([quasi-exact](http://www.votingmatters.org.uk/ISSUE24/I24P2.pdf)) or exact rational numbers * different tie breaking rules (backwards, random, manual) with auditable deterministic random number generation +* multiple constraints (e.g. affirmative action rules) ## Online usage diff --git a/docs/con.md b/docs/con.md new file mode 100644 index 0000000..ce485e8 --- /dev/null +++ b/docs/con.md @@ -0,0 +1,24 @@ +# CON file format + +OpenTally accepts the specification of constraints in a nonstandard file format, referred to as a CON file. The CON format is inspired by the standard [BLT file format](blt.md) used for ballot data. + +Suppose there are 7 candidates in the election. An example CON file is as follows: + +``` +"Gender" "Men" 0 99 2 3 4 6 +"Gender" "Women" 2 99 1 5 7 +"District" "District 1" 2 2 1 2 3 +"District" "District 2" 2 2 4 5 6 7 +``` + +For the purpose of constraints, one or more *categories* are established (in the example, ‘Gender’ and ‘District’). Each category contains one or more *groups*. Within each category, every candidate must be assigned to exactly one group. The constraints are placed on groups, such that a certain minimum number, and a certain maximum number, must be elected from each group. + +If there is no minimum for a particular group, specify `0`. If there is no maximum for a particular group, specify any number greater than or equal to the number of candidates in the group (in the above example, `99`). + +If there are candidates who do not fit into any group within a particular category, assign those candidates to a placeholder group with a minimum of 0 and a maximum greater than or equal to the number of candidates in the group. + +For example, the line `"Gender" "Men" 0 99 2 3 4 6` means that within the ‘Gender’ category ‘Men’ group, a minimum of 0 candidates and a maximum of 99 candidates can be elected (i.e. there are no constraints). The candidates in the ‘Men’ group are candidates 2, 3, 4 and 6 (in the order listed in the BLT file). + +The remaining lines indicate that a minimum of 2 women must be elected (with a maximum of 99, i.e. no maximum), and exactly 2 candidates must be elected from each of districts 1 and 2. + +Every line describes one group within a category, and every group must be described on its own line. diff --git a/docs/options.md b/docs/options.md index 3f46ce9..529dcdf 100644 --- a/docs/options.md +++ b/docs/options.md @@ -112,6 +112,14 @@ The default value is the current date, formatted YYYYMMDD. The algorithm used by the random number generator is specified at [rng.md](rng.md). +## Constraints (--constraints) + +This file selector allows you to load a [CON file](con.md) specifying constraints on the election. For example, if a certain minimum or maximum number of candidates can be elected from a particular category. + +OpenTally applies constraints using the Grey–Fitzgerald method. Whenever a candidate is declared elected or excluded, any candidate who must be elected to secure a conformant result is deemed *guarded*, and any candidate who must not be elected to secure a conformant result is deemed *doomed*. Any candidate who is doomed is excluded at the next opportunity. Any candidate who is guarded is prevented from being excluded. + +Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;9(1):2–4](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;13(3):4–7](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)). + ## Numeric representation ### Numbers (-n/--numbers), Decimal places (--decimals) @@ -137,7 +145,7 @@ When ballots are normalised, a set of preferences with weight *n* > 1 is instead ## Count optimisations -## Early bulk election (--no-early-bulk-elect) +### Early bulk election (--no-early-bulk-elect) When early bulk election is enabled (default), all remaining candidates are declared elected in a single stage as soon as the number of not-excluded candidates exactly equals the number of vacancies to fill. Further surplus distributions are not performed, and outstanding exclusions, if any, are not completed. This is typical of most STV rules. diff --git a/tests/constraints.rs b/tests/constraints.rs index 5babe95..1432400 100644 --- a/tests/constraints.rs +++ b/tests/constraints.rs @@ -152,3 +152,67 @@ fn prsa1_constr2_rational() { assert_eq!(winners[2].0.name, "White"); assert_eq!(winners[3].0.name, "Evans"); } + +#[test] +fn prsa1_constr3_rational() { + // FIXME: This is unvalidated! + + // Read BLT + let file = File::open("tests/data/prsa1.blt").expect("IO Error"); + let file_reader = io::BufReader::new(file); + let lines = file_reader.lines(); + let mut election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); + + // Read CON + let file = File::open("tests/data/prsa1_constr3.con").expect("IO Error"); + let file_reader = io::BufReader::new(file); + let lines = file_reader.lines(); + election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter())); + + let stv_opts = stv::STVOptions { + round_tvs: Some(3), + round_weights: Some(3), + round_votes: Some(3), + round_quota: Some(3), + sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep, + meek_surplus_tolerance: String::new(), + normalise_ballots: false, + quota: stv::QuotaType::Droop, + quota_criterion: stv::QuotaCriterion::GreaterOrEqual, + quota_mode: stv::QuotaMode::Static, + ties: vec![], + surplus: stv::SurplusMethod::EG, + surplus_order: stv::SurplusOrder::ByOrder, + transferable_only: true, + exclusion: stv::ExclusionMethod::ParcelsByOrder, + meek_nz_exclusion: false, + early_bulk_elect: false, + bulk_exclude: false, + defer_surpluses: false, + meek_immediate_elect: false, + constraints_path: Some("tests/data/prsa1_constr2.con".to_string()), + constraint_mode: stv::ConstraintMode::GuardDoom, + pp_decimals: 2, + }; + + // Initialise count state + let mut state = CountState::new(&election); + + // Count election + stv::count_init(&mut state, &stv_opts); + while stv::count_one_stage::(&mut state, &stv_opts).unwrap() == false {} + + // Validate winners + let mut winners = Vec::new(); + for (candidate, count_card) in state.candidates.iter() { + if count_card.state == CandidateState::Elected { + winners.push((candidate, count_card)); + } + } + winners.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); + + assert_eq!(winners[0].0.name, "Grey"); + assert_eq!(winners[1].0.name, "White"); + assert_eq!(winners[2].0.name, "Thomson"); + assert_eq!(winners[3].0.name, "Reid"); +} diff --git a/tests/data/prsa1_constr3.con b/tests/data/prsa1_constr3.con new file mode 100644 index 0000000..496bc06 --- /dev/null +++ b/tests/data/prsa1_constr3.con @@ -0,0 +1,6 @@ +"Gender" "Men" 0 99 2 3 4 6 +"Gender" "Women" 2 99 1 5 7 +"District" "District 1" 1 1 1 2 +"District" "District 2" 1 1 3 +"District" "District 3" 1 1 4 5 +"District" "District 4" 1 1 6 7