From ea8c4527370632341a188c9025b5db93bbbad034 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 1 Aug 2021 23:50:15 +1000 Subject: [PATCH] Prevent bulk election and bulk exclusion violating constraints --- docs/options.md | 2 - src/constraints.rs | 34 ++++++++++++++--- src/election.rs | 1 + src/stv/mod.rs | 30 +++++++++------ tests/constraints.rs | 55 +++++++++++++++++++++++++++ tests/data/ers97_cantbulkexclude.con | 2 + tests/data/ers97_cantbulkexclude.csv | 15 ++++++++ tests/data/ers97_cantbulkexclude.ods | Bin 0 -> 14610 bytes tests/utils/mod.rs | 13 ++++--- 9 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 tests/data/ers97_cantbulkexclude.con create mode 100644 tests/data/ers97_cantbulkexclude.csv create mode 100644 tests/data/ers97_cantbulkexclude.ods diff --git a/docs/options.md b/docs/options.md index d3ec4d9..9cb83ee 100644 --- a/docs/options.md +++ b/docs/options.md @@ -178,8 +178,6 @@ In either case, candidates are declared elected in descending order of votes. Th Note that the OpenTally rules for early bulk election are aggressive, and many STV rules do not implement all 3 (if any at all). It is not possible at this time to selectively apply only some of the rules. In order to reproduce the result of a count performed by others, where not all rules were implemented, consider disabling early bulk election and comparing the results at the time a bulk election would have been made. -Note also that early bulk election can conflict with constraints. If an election is to be run with constraints, it is recommend that early bulk election be disabled. - ### Bulk exclusion (--bulk-exclude) When bulk exclusion is disabled (default), only one candidate is ever excluded per stage. diff --git a/src/constraints.rs b/src/constraints.rs index f691b04..c06e252 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -161,7 +161,8 @@ pub struct ConstraintMatrixCell { pub cands: usize, } -/// Hypercube/tensor of [ConstraintMatrixCell]s representing the conformant combinations of elected candidates +/// N-dimensional cube of [ConstraintMatrixCell]s representing the conformant combinations of elected candidates +#[derive(Clone)] pub struct ConstraintMatrix(pub Array); impl ConstraintMatrix { @@ -491,6 +492,28 @@ fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election, candi return result; } +/// Clone and update the constraints matrix, with the state of the given candidates set to candidate_state +pub fn try_constraints(state: &CountState, candidates: &Vec<&Candidate>, candidate_state: CandidateState) -> Result<(), ConstraintError> { + if state.constraint_matrix.is_none() { + return Ok(()); + } + let mut cm = state.constraint_matrix.as_ref().unwrap().clone(); + + let mut trial_candidates = state.candidates.clone(); // TODO: Can probably be optimised by not cloning CountCard::parcels + for candidate in candidates { + trial_candidates.get_mut(candidate).unwrap().state = candidate_state.clone(); + } + + // Update cands/elected + cm.update_from_state(&state.election, &trial_candidates); + cm.recount_cands(); + + // Iterate for stable state + while !cm.step()? {} + + return Ok(()); +} + /// Update the constraints matrix, and perform the necessary actions given by [STVOptions::constraint_mode] pub fn update_constraints(state: &mut CountState, opts: &STVOptions) -> bool { if state.constraint_matrix.is_none() { @@ -503,11 +526,12 @@ pub fn update_constraints(state: &mut CountState, opts: &STVOption cm.recount_cands(); // Iterate for stable state - //println!("{}", cm); - while !cm.step().expect("No conformant result is possible") { - //println!("{}", cm); + while !cm.step().expect("No conformant result is possible") {} + + if state.num_elected == state.election.seats { + // Election is complete, so skip guarding/dooming candidates + return false; } - //println!("{}", cm); match opts.constraint_mode { ConstraintMode::GuardDoom => { diff --git a/src/election.rs b/src/election.rs index 4f0795d..1d384cf 100644 --- a/src/election.rs +++ b/src/election.rs @@ -378,6 +378,7 @@ pub struct Ballot { #[allow(dead_code)] #[derive(PartialEq)] #[derive(Clone)] +#[derive(Debug)] pub enum CandidateState { /// Hopeful (continuing candidate) Hopeful, diff --git a/src/stv/mod.rs b/src/stv/mod.rs index d070d1a..056fe46 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -829,11 +829,16 @@ fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOp return Ok(false); } - let mut hopefuls: Vec<&Candidate> = hopefuls.iter().map(|(c, _)| *c).collect(); + let mut leading_hopefuls: Vec<&Candidate> = hopefuls.iter().take(num_vacancies).map(|(c, _)| *c).collect(); - // Bulk elect all remaining candidates + match constraints::try_constraints(state, &leading_hopefuls, CandidateState::Elected) { + Ok(_) => {} + Err(_) => { return Ok(false); } // Bulk election conflicts with constraints + } + + // Bulk elect all leading candidates while state.num_elected < state.election.seats { - let max_cands = ties::multiple_max_by(&hopefuls, |c| &state.candidates[c].votes); + let max_cands = ties::multiple_max_by(&leading_hopefuls, |c| &state.candidates[c].votes); let candidate = if max_cands.len() > 1 { choose_highest(state, opts, max_cands, "Which candidate to elect?")? } else { @@ -851,14 +856,11 @@ fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOp vec![&candidate.name] ); - if constraints::update_constraints(state, opts) { - // FIXME: Work out interaction between early bulk election and constraints - panic!("Attempted early bulk election resulted in changes to constraint matrix"); - } else { - hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap()); - } + leading_hopefuls.remove(leading_hopefuls.iter().position(|c| *c == candidate).unwrap()); } + constraints::update_constraints(state, opts); + return Ok(true); } @@ -1148,9 +1150,15 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST continue; } - for (c, _) in try_exclude.into_iter() { - excluded_candidates.push(**c); + let try_exclude = try_exclude.into_iter().map(|(c, _)| **c).collect(); + + // Do not exclude if this violates constraints + match constraints::try_constraints(state, &try_exclude, CandidateState::Excluded) { + Ok(_) => {} + Err(_) => { break; } // Bulk exclusion conflicts with constraints } + + excluded_candidates.extend(try_exclude); break; } diff --git a/tests/constraints.rs b/tests/constraints.rs index bdb3f38..4acf010 100644 --- a/tests/constraints.rs +++ b/tests/constraints.rs @@ -213,3 +213,58 @@ fn prsa1_constr3_rational() { assert_eq!(winners[2].0.name, "Thomson"); assert_eq!(winners[3].0.name, "Reid"); } + +/// Same election data as ers97_rational, but with a constraint that prevents the bulk exclusion of Glazier and Wright +#[test] +fn ers97_cantbulkexclude_rational() { + // Read CSV file + let reader = csv::ReaderBuilder::new() + .has_headers(false) + .from_path("tests/data/ers97_cantbulkexclude.csv") + .expect("IO Error"); + let records: Vec = reader.into_records().map(|r| r.expect("Syntax Error")).collect(); + + let mut candidates: Vec<&str> = records.iter().skip(2).map(|r| &r[0]).collect(); + // Remove exhausted/LBF rows + candidates.truncate(candidates.len() - 2); + + let stages: Vec = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect(); + + // Read BLT + let mut election: Election = Election::from_file("tests/data/ers97.blt").expect("Syntax Error"); + + // Read CON + let file = File::open("tests/data/ers97_cantbulkexclude.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(2), + round_weights: Some(2), + round_votes: Some(2), + round_quota: Some(2), + sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep, + meek_surplus_tolerance: String::new(), + normalise_ballots: false, + quota: stv::QuotaType::DroopExact, + quota_criterion: stv::QuotaCriterion::GreaterOrEqual, + quota_mode: stv::QuotaMode::ERS97, + ties: vec![], + surplus: stv::SurplusMethod::EG, + surplus_order: stv::SurplusOrder::BySize, + transferable_only: true, + exclusion: stv::ExclusionMethod::ByValue, + meek_nz_exclusion: false, + early_bulk_elect: false, + bulk_exclude: true, + defer_surpluses: true, + meek_immediate_elect: false, + constraints_path: Some("tests/data/ers97_cantbulkexclude".to_string()), + constraint_mode: stv::ConstraintMode::GuardDoom, + hide_excluded: false, + sort_votes: false, + pp_decimals: 2, + }; + utils::validate_election::(stages, records, election, stv_opts, None, &["nt", "vre"]); +} diff --git a/tests/data/ers97_cantbulkexclude.con b/tests/data/ers97_cantbulkexclude.con new file mode 100644 index 0000000..706a019 --- /dev/null +++ b/tests/data/ers97_cantbulkexclude.con @@ -0,0 +1,2 @@ +"Constraint" "Constrained" 1 99 3 4 +"Constraint" "Placeholder" 0 99 1 2 5 6 7 8 9 10 11 diff --git a/tests/data/ers97_cantbulkexclude.csv b/tests/data/ers97_cantbulkexclude.csv new file mode 100644 index 0000000..49b236c --- /dev/null +++ b/tests/data/ers97_cantbulkexclude.csv @@ -0,0 +1,15 @@ +Stage:,1,,2,,3,,4,,5, +Comment:,First preferences,,Surplus of Smith,,Exclusion of Monk,,Exclusion of Monk,,Exclusion of Glazier, +Smith,134,EL,107.58,EL,107.58,EL,107.58,EL,,EL +Carpenter,81,H,88.35,H,88.35,H,88.35,H,,H +Wright,27,H,32.25,H,32.25,H,32.25,H,,G +Glazier,24,H,30.51,H,30.51,H,30.51,H,,EX +Duke,105,H,106.68,H,108.68,EL,108.68,EL,,EL +Prince,91,H,91,H,91,H,91,H,,H +Baron,64,H,64,H,64,H,64.21,H,,H +Abbot,59,H,59.84,H,64.84,H,64.84,H,,H +Vicar,55,H,55,H,69,H,69.21,H,,H +Monk,23,H,23.42,H,0.42,EX,0,EX,,EX +Freeman,90,H,93.78,H,95.78,H,95.78,H,,H +Non-transferable,0,,0.59,,0.59,,0.59,,, +Votes required,,,,,,,,,, diff --git a/tests/data/ers97_cantbulkexclude.ods b/tests/data/ers97_cantbulkexclude.ods new file mode 100644 index 0000000000000000000000000000000000000000..55aa6a2f165309c5fc365d797193274444d9b040 GIT binary patch literal 14610 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