Add documentation on constraints

This commit is contained in:
RunasSudo 2021-06-27 23:13:39 +10:00
parent 31eda1fcec
commit c12743285a
No known key found for this signature in database
GPG Key ID: 7234E476BF21C61A
5 changed files with 104 additions and 1 deletions

View File

@ -23,6 +23,7 @@ OpenTally is highly customisable, including options for:
* different quotas and quota rules (e.g. exact Droop, Hare) * 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 * 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 * different tie breaking rules (backwards, random, manual) with auditable deterministic random number generation
* multiple constraints (e.g. affirmative action rules)
## Online usage ## Online usage

24
docs/con.md Normal file
View File

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

View File

@ -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). 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 GreyFitzgerald 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):24](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;13(3):47](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)).
## Numeric representation ## Numeric representation
### Numbers (-n/--numbers), Decimal places (--decimals) ### 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 ## 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. 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.

View File

@ -152,3 +152,67 @@ fn prsa1_constr2_rational() {
assert_eq!(winners[2].0.name, "White"); assert_eq!(winners[2].0.name, "White");
assert_eq!(winners[3].0.name, "Evans"); 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<Rational> = 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::<Rational>(&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");
}

View File

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