Prevent bulk election and bulk exclusion violating constraints
This commit is contained in:
parent
116ff39fa5
commit
ea8c452737
|
@ -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 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)
|
### Bulk exclusion (--bulk-exclude)
|
||||||
|
|
||||||
When bulk exclusion is disabled (default), only one candidate is ever excluded per stage.
|
When bulk exclusion is disabled (default), only one candidate is ever excluded per stage.
|
||||||
|
|
|
@ -161,7 +161,8 @@ pub struct ConstraintMatrixCell {
|
||||||
pub cands: usize,
|
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<ConstraintMatrixCell, IxDyn>);
|
pub struct ConstraintMatrix(pub Array<ConstraintMatrixCell, IxDyn>);
|
||||||
|
|
||||||
impl ConstraintMatrix {
|
impl ConstraintMatrix {
|
||||||
|
@ -491,6 +492,28 @@ fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election<N>, candi
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clone and update the constraints matrix, with the state of the given candidates set to candidate_state
|
||||||
|
pub fn try_constraints<N: Number>(state: &CountState<N>, 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]
|
/// Update the constraints matrix, and perform the necessary actions given by [STVOptions::constraint_mode]
|
||||||
pub fn update_constraints<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
pub fn update_constraints<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
||||||
if state.constraint_matrix.is_none() {
|
if state.constraint_matrix.is_none() {
|
||||||
|
@ -503,11 +526,12 @@ pub fn update_constraints<N: Number>(state: &mut CountState<N>, opts: &STVOption
|
||||||
cm.recount_cands();
|
cm.recount_cands();
|
||||||
|
|
||||||
// Iterate for stable state
|
// Iterate for stable state
|
||||||
//println!("{}", cm);
|
while !cm.step().expect("No conformant result is possible") {}
|
||||||
while !cm.step().expect("No conformant result is possible") {
|
|
||||||
//println!("{}", cm);
|
if state.num_elected == state.election.seats {
|
||||||
|
// Election is complete, so skip guarding/dooming candidates
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
//println!("{}", cm);
|
|
||||||
|
|
||||||
match opts.constraint_mode {
|
match opts.constraint_mode {
|
||||||
ConstraintMode::GuardDoom => {
|
ConstraintMode::GuardDoom => {
|
||||||
|
|
|
@ -378,6 +378,7 @@ pub struct Ballot<N> {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum CandidateState {
|
pub enum CandidateState {
|
||||||
/// Hopeful (continuing candidate)
|
/// Hopeful (continuing candidate)
|
||||||
Hopeful,
|
Hopeful,
|
||||||
|
|
|
@ -829,11 +829,16 @@ fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOp
|
||||||
return Ok(false);
|
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 {
|
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 {
|
let candidate = if max_cands.len() > 1 {
|
||||||
choose_highest(state, opts, max_cands, "Which candidate to elect?")?
|
choose_highest(state, opts, max_cands, "Which candidate to elect?")?
|
||||||
} else {
|
} else {
|
||||||
|
@ -851,14 +856,11 @@ fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOp
|
||||||
vec![&candidate.name]
|
vec![&candidate.name]
|
||||||
);
|
);
|
||||||
|
|
||||||
if constraints::update_constraints(state, opts) {
|
leading_hopefuls.remove(leading_hopefuls.iter().position(|c| *c == candidate).unwrap());
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constraints::update_constraints(state, opts);
|
||||||
|
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1148,9 +1150,15 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (c, _) in try_exclude.into_iter() {
|
let try_exclude = try_exclude.into_iter().map(|(c, _)| **c).collect();
|
||||||
excluded_candidates.push(**c);
|
|
||||||
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -213,3 +213,58 @@ fn prsa1_constr3_rational() {
|
||||||
assert_eq!(winners[2].0.name, "Thomson");
|
assert_eq!(winners[2].0.name, "Thomson");
|
||||||
assert_eq!(winners[3].0.name, "Reid");
|
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<csv::StringRecord> = 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<usize> = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect();
|
||||||
|
|
||||||
|
// Read BLT
|
||||||
|
let mut election: Election<Rational> = 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::<Rational>(stages, records, election, stv_opts, None, &["nt", "vre"]);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
"Constraint" "Constrained" 1 99 3 4
|
||||||
|
"Constraint" "Placeholder" 0 99 1 2 5 6 7 8 9 10 11
|
|
@ -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,,,,,,,,,,
|
|
Binary file not shown.
|
@ -134,13 +134,16 @@ where
|
||||||
for (candidate, candidate_state) in state.election.candidates.iter().zip(candidate_states) {
|
for (candidate, candidate_state) in state.election.candidates.iter().zip(candidate_states) {
|
||||||
let count_card = state.candidates.get(candidate).unwrap();
|
let count_card = state.candidates.get(candidate).unwrap();
|
||||||
if candidate_state == "" {
|
if candidate_state == "" {
|
||||||
}
|
} else if candidate_state == "H" {
|
||||||
else if candidate_state == "H" {
|
assert!(count_card.state == CandidateState::Hopeful, "Unexpected state for \"{}\" at stage {}, expected \"Hopeful\", got \"{:?}\"", candidate.name, stage_num, count_card.state);
|
||||||
assert!(count_card.state == CandidateState::Hopeful, "Unexpected state for \"{}\" at stage {}", candidate.name, stage_num);
|
} else if candidate_state == "G" {
|
||||||
|
assert!(count_card.state == CandidateState::Guarded, "Unexpected state for \"{}\" at stage {}, expected \"Guarded\", got \"{:?}\"", candidate.name, stage_num, count_card.state);
|
||||||
} else if candidate_state == "EL" || candidate_state == "PEL" {
|
} else if candidate_state == "EL" || candidate_state == "PEL" {
|
||||||
assert!(count_card.state == CandidateState::Elected, "Unexpected state for \"{}\" at stage {}", candidate.name, stage_num);
|
assert!(count_card.state == CandidateState::Elected, "Unexpected state for \"{}\" at stage {}, expected \"Elected\", got \"{:?}\"", candidate.name, stage_num, count_card.state);
|
||||||
|
} else if candidate_state == "D" {
|
||||||
|
assert!(count_card.state == CandidateState::Doomed, "Unexpected state for \"{}\" at stage {}, expected \"Doomed\", got \"{:?}\"", candidate.name, stage_num, count_card.state);
|
||||||
} else if candidate_state == "EX" || candidate_state == "EXCLUDING" {
|
} else if candidate_state == "EX" || candidate_state == "EXCLUDING" {
|
||||||
assert!(count_card.state == CandidateState::Excluded, "Unexpected state for \"{}\" at stage {}", candidate.name, stage_num);
|
assert!(count_card.state == CandidateState::Excluded, "Unexpected state for \"{}\" at stage {}, expected \"Excluded\", got \"{:?}\"", candidate.name, stage_num, count_card.state);
|
||||||
} else {
|
} else {
|
||||||
panic!("Unknown state descriptor {}", candidate_state);
|
panic!("Unknown state descriptor {}", candidate_state);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue