OpenTally/src/constraints.rs

1218 lines
41 KiB
Rust

/* OpenTally: Open-source election vote counting
* Copyright © 2021–2022 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 crate::candmap::CandidateMap;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, StageKind, RollbackState};
use crate::numbers::Number;
use crate::stv::{self, gregory, sample, ConstraintMode, STVError, STVOptions, SurplusMethod, SurplusOrder};
use crate::ties::{self, TieStrategy};
use itertools::Itertools;
use ndarray::{Array, Dimension, IxDyn};
#[cfg(not(target_arch = "wasm32"))]
use rkyv::{Archive, Deserialize, Serialize};
use std::fmt;
use std::num::ParseIntError;
use std::ops;
/// Constraints for an [crate::election::Election]
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
pub struct Constraints(pub Vec<Constraint>);
impl Constraints {
/// Parse the given CON file and return a [Constraints]
pub fn from_con<S: AsRef<str>, I: Iterator<Item=S>>(lines: I) -> Result<Self, ParseError> {
let mut constraints = Constraints(Vec::new());
for (line_no, line) in lines.enumerate() {
let mut bits = line.as_ref().split(' ').peekable();
// Read constraint category and group
let constraint_name = read_quoted_string(line_no, &mut bits)?;
let group_name = read_quoted_string(line_no, &mut bits)?;
// Read min, max
let min: usize = bits
.next().ok_or(ParseError::UnexpectedEOL(line_no, "minimum number"))?
.parse().map_err(|e| ParseError::InvalidNumber(line_no, e))?;
let max: usize = bits
.next().ok_or(ParseError::UnexpectedEOL(line_no, "maximum number"))?
.parse().map_err(|e| ParseError::InvalidNumber(line_no, e))?;
// Read candidates
let mut candidates: Vec<usize> = Vec::new();
for x in bits {
candidates.push(x.parse::<usize>().map_err(|e| ParseError::InvalidNumber(line_no, e))? - 1);
}
// Insert constraint/group
let constraint = match constraints.0.iter_mut().find(|c| c.name == constraint_name) {
Some(c) => { c }
None => {
let c = Constraint {
name: constraint_name,
groups: Vec::new(),
};
constraints.0.push(c);
constraints.0.last_mut().unwrap()
}
};
if constraint.groups.iter().any(|g| g.name == group_name) {
return Err(ParseError::DuplicateGroup(line_no, group_name, constraint.name.clone()));
}
constraint.groups.push(ConstrainedGroup {
name: group_name,
candidates,
min,
max,
});
}
return Ok(constraints);
}
/// Validate that each candidate is specified exactly once in each constraint, and (if applicable) limitations of the constraint mode are applied
pub fn validate_constraints(&self, num_candidates: usize, constraint_mode: ConstraintMode) -> Result<(), ValidationError> {
for constraint in &self.0 {
let mut remaining_candidates: Vec<usize> = (0..num_candidates).collect();
for group in &constraint.groups {
for candidate in &group.candidates {
match remaining_candidates.iter().position(|c| c == candidate) {
Some(idx) => {
remaining_candidates.remove(idx);
}
None => {
return Err(ValidationError::DuplicateCandidate(*candidate, constraint.name.clone()));
}
}
}
if constraint_mode == ConstraintMode::RepeatCount {
// Each group must be either a maximum constraint, or the remaining group
if group.min == 0 {
// Maximum constraint: OK
} else if group.max >= group.candidates.len() {
// Remaining group: OK
} else {
return Err(ValidationError::InvalidTwoStage(constraint.name.clone(), group.name.clone()));
}
// FIXME: Is other validation required?
}
}
if !remaining_candidates.is_empty() {
return Err(ValidationError::UnassignedCandidate(*remaining_candidates.first().unwrap(), constraint.name.clone()));
}
}
Ok(())
}
/// Check if any elected candidates exceed constrained maximums
pub fn exceeds_maximum<'a, N: Number>(&self, election: &Election<N>, candidates: CandidateMap<CountCard<'a, N>>) -> Option<(&Constraint, &ConstrainedGroup)> {
for constraint in &self.0 {
for group in &constraint.groups {
let mut num_elected = 0;
for candidate in &group.candidates {
if candidates[&election.candidates[*candidate]].state == CandidateState::Elected {
num_elected += 1;
}
}
if num_elected > group.max {
return Some((&constraint, &group));
}
}
}
return None;
}
}
/// Error parsing constraints
pub enum ParseError {
/// Duplicate group in a constraint
DuplicateGroup(usize, String, String),
/// Unexpected EOL, expected ...
UnexpectedEOL(usize, &'static str),
/// Invalid number
InvalidNumber(usize, ParseIntError),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::DuplicateGroup(line_no, group_name, constraint_name) => {
f.write_fmt(format_args!(r#"Line {}, duplicate group "{}" in constraint "{}""#, line_no, group_name, constraint_name))
}
ParseError::UnexpectedEOL(line_no, expected) => {
f.write_fmt(format_args!(r#"Line {}, unexpected end-of-line, expected {}"#, line_no, expected))
}
ParseError::InvalidNumber(line_no, err) => {
f.write_fmt(format_args!(r#"Line {}, invalid number: {}"#, line_no, err))
}
}
}
}
impl fmt::Debug for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
return fmt::Display::fmt(self, f);
}
}
/// Error validating constraints
pub enum ValidationError {
/// Duplicate candidate in a constraint
DuplicateCandidate(usize, String),
/// Unassigned candidate in a constraint
UnassignedCandidate(usize, String),
/// Constraint is incompatible with ConstraintMode::TwoStage
InvalidTwoStage(String, String),
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::DuplicateCandidate(candidate, constraint_name) => {
f.write_fmt(format_args!(r#"Duplicate candidate {} in constraint "{}""#, candidate + 1, constraint_name))
}
ValidationError::UnassignedCandidate(candidate, constraint_name) => {
f.write_fmt(format_args!(r#"Unassigned candidate {} in constraint "{}""#, candidate + 1, constraint_name))
}
ValidationError::InvalidTwoStage(constraint_name, group_name) => {
f.write_fmt(format_args!(r#"Constraint "{}" group "{}" is incompatible with --constraint-mode repeat_count"#, constraint_name, group_name))
}
}
}
}
impl fmt::Debug for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
return fmt::Display::fmt(self, f);
}
}
#[test]
fn duplicate_contraint_group() {
let input = r#""Constraint 1" "Group 1" 0 3 1 2 3
"Constraint 1" "Group 1" 0 3 4 5 6"#;
Constraints::from_con(input.lines()).unwrap_err();
}
#[test]
fn duplicate_candidate() {
let input = r#""Constraint 1" "Group 1" 0 3 1 2 3 4
"Constraint 1" "Group 2" 0 3 4 5 6"#;
let constraints = Constraints::from_con(input.lines()).unwrap();
constraints.validate_constraints(6, ConstraintMode::GuardDoom).unwrap_err();
}
#[test]
fn unassigned_candidate() {
let input = r#""Constraint 1" "Group 1" 0 3 1 2 3
"Constraint 1" "Group 2" 0 3 4 5 6"#;
let constraints = Constraints::from_con(input.lines()).unwrap();
constraints.validate_constraints(7, ConstraintMode::GuardDoom).unwrap_err();
}
/// Read an optionally quoted string, returning the string without quotes
fn read_quoted_string<'a, I: Iterator<Item=&'a str>>(line_no: usize, bits: &mut I) -> Result<String, ParseError> {
let x = bits.next().ok_or(ParseError::UnexpectedEOL(line_no, "string continuation"))?;
if let Some(x1) = x.strip_prefix('"') {
if let Some(x2) = x.strip_suffix('"') {
// Complete string
return Ok(String::from(x2));
} else {
// Incomplete string
let mut result = String::from(x1);
// Read until matching "
loop {
let x = bits.next().ok_or(ParseError::UnexpectedEOL(line_no, "string continuation"))?;
result.push(' ');
if let Some(x1) = x.strip_suffix('"') {
// End of string
result.push_str(x1);
break;
} else {
// Middle of string
result.push_str(x);
}
}
return Ok(result);
}
} else {
// Unquoted string
return Ok(String::from(x));
}
}
/// A single dimension of constraint
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
pub struct Constraint {
/// Name of this constraint
pub name: String,
/// Groups of candidates within this constraint
pub groups: Vec<ConstrainedGroup>,
}
/// A group of candidates, of which a certain minimum and maximum must be elected
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
pub struct ConstrainedGroup {
/// Name of this group
pub name: String,
/// Indexes of [crate::election::Candidate]s to constrain
pub candidates: Vec<usize>,
/// Minimum number to elect
pub min: usize,
/// Maximum number to elect
pub max: usize,
}
/// Error reaching a stable state when processing constraints
#[derive(Debug)]
pub enum ConstraintError {
/// No conformant result is possible
NoConformantResult,
}
// ----------------------
// GUARD/DOOM CONSTRAINTS
// ----------------------
/// Cell in a [ConstraintMatrix]
#[derive(Clone)]
pub struct ConstraintMatrixCell {
/// Number of elected candidates in this cell
pub elected: usize,
/// Minimum number of candidates which must be elected from this cell for a conformant result
pub min: usize,
/// Maximum number of candidates which may be elected from this cell for a conformant result
pub max: usize,
/// Total number of elected or hopeful candidates in this cell
pub cands: usize,
}
/// N-dimensional cube of [ConstraintMatrixCell]s representing the conformant combinations of elected candidates
#[derive(Clone)]
pub struct ConstraintMatrix(pub Array<ConstraintMatrixCell, IxDyn>);
impl ConstraintMatrix {
/// Return a new [ConstraintMatrix], with the specified number of groups for each constraint dimension
pub fn new(constraints: &mut [usize]) -> Self {
// Add 1 to dimensions for totals cells
for c in constraints.iter_mut() {
*c += 1;
}
return Self(Array::from_elem(
IxDyn(constraints),
ConstraintMatrixCell {
elected: 0,
min: 0,
max: 0,
cands: 0,
}
));
}
/// Initialise the matrix once the number of candidates in each innermost cell, and min/max for each constraint group, have been specified
pub fn init(&mut self) {
// Compute candidate totals
self.recount_cands();
// Initialise max for grand total cell
let idx = IxDyn(&vec![0; self.0.ndim()][..]);
self.0[&idx].max = self.0[&idx].cands;
// Initialise max for inner cells (>=2 zeroes)
for idx in ndarray::indices(self.0.shape()) {
if (0..idx.ndim()).fold(0, |acc, d| if idx[d] != 0 { acc + 1 } else { acc }) < 2 {
continue;
}
self.0[&idx].max = self.0[&idx].cands;
}
// NB: Bounds on min, max, etc. will be further refined in initial step() calls
}
/// Update cands/elected in innermost cells based on the provided [CountState::candidates](crate::election::CountState::candidates)
pub fn update_from_state<N: Number>(&mut self, election: &Election<N>, candidates: &CandidateMap<CountCard<N>>) {
let constraints = election.constraints.as_ref().unwrap();
// Reset innermost cells
for idx in ndarray::indices(self.0.shape()) {
if (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc }) != 0 {
continue;
}
self.0[&idx].cands = 0;
self.0[&idx].elected = 0;
}
for (i, candidate) in election.candidates.iter().enumerate() {
if candidate.is_dummy {
continue;
}
let idx: Vec<usize> = constraints.0.iter().map(|c| {
for (j, group) in c.groups.iter().enumerate() {
if group.candidates.contains(&i) {
return j + 1;
}
}
// Should be caught by validate_constraints
unreachable!("Candidate \"{}\" not represented in constraint \"{}\"", candidate.name, c.name);
}).collect();
let cell = &mut self[&idx[..]];
let count_card = &candidates[candidate];
match count_card.state {
CandidateState::Hopeful | CandidateState::Guarded => {
cell.cands += 1;
}
CandidateState::Elected => {
cell.cands += 1;
cell.elected += 1;
}
CandidateState::Withdrawn | CandidateState::Doomed | CandidateState::Excluded => {}
}
}
}
/// Recompute [cands](ConstraintMatrixCell::cands) and [elected](ConstraintMatrixCell::elected) for totals cells based on the innermost cells
pub fn recount_cands(&mut self) {
let shape = Vec::from(self.0.shape());
// Compute cands/elected totals
for nzeroes in 1..self.0.ndim()+1 {
for idx in ndarray::indices(self.0.shape()) {
// First compute totals cells with 1 zero, then 2 zeroes, ... then grand total (all zeroes)
if (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc }) != nzeroes {
continue;
}
self.0[&idx].cands = 0;
self.0[&idx].elected = 0;
// The axis along which to sum - if multiple, just pick the first, as these should agree
let zero_axis = (0..idx.ndim()).find(|d| idx[*d] == 0).unwrap();
// Traverse along the axis and sum the candidates
let mut idx2 = idx.clone();
for coord in 1..shape[zero_axis] {
idx2[zero_axis] = coord;
self.0[&idx].cands += self.0[&idx2].cands;
self.0[&idx].elected += self.0[&idx2].elected;
}
}
}
}
/// Attempt to advance the matrix one step towards a stable state
///
/// Returns `Ok(true)` if in a stable state, `Ok(false)` if not, and `ConstraintError` on an error.
///
pub fn step(&mut self) -> Result<bool, ConstraintError> {
let shape = Vec::from(self.0.shape());
for idx in ndarray::indices(self.0.shape()) {
let cell = &mut self.0[&idx];
// Rule 1: Ensure elected < min < max < cands
if cell.min < cell.elected {
cell.min = cell.elected;
return Ok(false);
}
if cell.max > cell.cands {
cell.max = cell.cands;
return Ok(false);
}
if cell.min > cell.max {
return Err(ConstraintError::NoConformantResult);
}
let nzeroes = (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc });
// Rule 2/3: Ensure min/max is possible in innermost cells
if nzeroes == 0 {
for axis in 0..self.0.ndim() {
let mut idx2 = idx.clone();
// What is the min/max number of candidates that can be elected from other cells in this axis?
let (other_max, other_min) = (1..shape[axis]).fold((0, 0), |(acc_max, acc_min), coord| {
if coord == idx[axis] {
return (acc_max, acc_min);
}
idx2[axis] = coord;
return (acc_max + self.0[&idx2].max, acc_min + self.0[&idx2].min);
});
// What is the min/max number of candidates that can be elected along this axis?
idx2[axis] = 0;
let axis_max = self.0[&idx2].max;
let axis_min = self.0[&idx2].min;
// How many must be elected from this cell?
let this_max = (axis_max as i32) - (other_min as i32);
let this_min = (axis_min as i32) - (other_max as i32);
if this_max < (self.0[&idx].max as i32) {
self.0[&idx].max = this_max as usize;
return Ok(false);
}
if this_min > (self.0[&idx].min as i32) {
self.0[&idx].min = this_min as usize;
return Ok(false);
}
}
}
// Rule 4/5: Ensure min/max is possible in totals cells
if nzeroes > 0 {
for axis in 0..self.0.ndim() {
if idx[axis] != 0 {
continue;
}
// What is the total min/max along this axis?
let mut idx2 = idx.clone();
let (axis_max, axis_min) = (1..shape[axis]).fold((0, 0), |(acc_max, acc_min), coord| {
idx2[axis] = coord;
return (acc_max + self.0[&idx2].max, acc_min + self.0[&idx2].min);
});
if axis_max < self.0[&idx].max {
self.0[&idx].max = axis_max;
return Ok(false);
}
if axis_min > self.0[&idx].min {
self.0[&idx].min = axis_min;
return Ok(false);
}
}
}
}
return Ok(true);
}
}
impl fmt::Display for ConstraintMatrix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
let shape = self.0.shape();
let mut result = String::new();
// TODO: >2 dimensions
if shape.len() == 1 {
result.push('+');
for _ in 0..shape[0] {
result.push_str("-------------+");
}
result.push('\n');
result.push('|');
for x in 0..shape[0] {
result.push_str(&format!(" Elected: {:2}", self[&[x]].elected));
result.push_str(if x == 0 { "" } else { " |" });
}
result.push('\n');
result.push('|');
for x in 0..shape[0] {
result.push_str(&format!(" Min: {:2}", self[&[x]].min));
result.push_str(if x == 0 { "" } else { " |" });
}
result.push('\n');
result.push('|');
for x in 0..shape[0] {
result.push_str(&format!(" Max: {:2}", self[&[x]].max));
result.push_str(if x == 0 { "" } else { " |" });
}
result.push('\n');
result.push('|');
for x in 0..shape[0] {
result.push_str(&format!(" Cands: {:2}", self[&[x]].cands));
result.push_str(if x == 0 { "" } else { " |" });
}
result.push('\n');
result.push('+');
for _ in 0..shape[0] {
result.push_str("-------------+");
}
result.push('\n');
} else if shape.len() == 2 {
for y in 0..shape[1] {
result.push('+');
for _ in 0..shape[0] {
result.push_str(if y == 1 { "=============+" } else { "-------------+" });
}
result.push('\n');
result.push('|');
for x in 0..shape[0] {
result.push_str(&format!(" Elected: {:2}", self[&[x, y]].elected));
result.push_str(if x == 0 { "" } else { " |" });
}
result.push('\n');
result.push('|');
for x in 0..shape[0] {
result.push_str(&format!(" Min: {:2}", self[&[x, y]].min));
result.push_str(if x == 0 { "" } else { " |" });
}
result.push('\n');
result.push('|');
for x in 0..shape[0] {
result.push_str(&format!(" Max: {:2}", self[&[x, y]].max));
result.push_str(if x == 0 { "" } else { " |" });
}
result.push('\n');
result.push('|');
for x in 0..shape[0] {
result.push_str(&format!(" Cands: {:2}", self[&[x, y]].cands));
result.push_str(if x == 0 { "" } else { " |" });
}
result.push('\n');
}
result.push('+');
for _ in 0..shape[0] {
result.push_str("-------------+");
}
result.push('\n');
} else {
todo!();
}
return f.write_str(&result);
}
}
impl ops::Index<&[usize]> for ConstraintMatrix {
type Output = ConstraintMatrixCell;
fn index(&self, index: &[usize]) -> &Self::Output { &self.0[index] }
}
impl ops::IndexMut<&[usize]> for ConstraintMatrix {
fn index_mut(&mut self, index: &[usize]) -> &mut Self::Output { &mut self.0[index] }
}
/// Return the [Candidate]s referred to in the given [ConstraintMatrixCell] at location `idx`
fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election<N>, candidates: &CandidateMap<CountCard<N>>, idx: &[usize]) -> Vec<&'a Candidate> {
let mut result: Vec<&Candidate> = Vec::new();
for (i, candidate) in election.candidates.iter().enumerate() {
let cc = &candidates[candidate];
if cc.state != CandidateState::Hopeful {
continue;
}
// Is this candidate within this constraint cell?
let mut matches = true;
for (coord, constraint) in idx.iter().zip(election.constraints.as_ref().unwrap().0.iter()) {
let group = &constraint.groups[coord - 1]; // The group referred to by this constraint cell
if !group.candidates.contains(&i) {
matches = false;
break;
}
}
if matches {
result.push(candidate);
}
}
return result;
}
/// Clone and update the constraints matrix, with the state of the given candidates set to candidate_state – check if a conformant result is possible
pub fn test_constraints_any_time<N: Number>(state: &CountState<N>, candidates: &[&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;
}
// 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<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
if state.constraint_matrix.is_none() {
return false;
}
let cm = state.constraint_matrix.as_mut().unwrap();
// Update cands/elected
cm.update_from_state(state.election, &state.candidates);
cm.recount_cands();
// Iterate for stable state
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;
}
match opts.constraint_mode {
ConstraintMode::GuardDoom => {
// Check for guarded or doomed candidates
let mut guarded_or_doomed = false;
for idx in ndarray::indices(cm.0.shape()) {
if (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc }) != 0 {
continue;
}
let cell = &cm.0[&idx];
if cell.elected == cell.max {
// Doom remaining candidates in this cell
let doomed = candidates_in_constraint_cell(state.election, &state.candidates, idx.slice());
if !doomed.is_empty() {
for candidate in doomed.iter() {
state.candidates.get_mut(candidate).unwrap().state = CandidateState::Doomed;
}
state.logger.log_smart(
"{} must be doomed to comply with constraints.",
"{} must be doomed to comply with constraints.",
doomed.iter().map(|c| c.name.as_str()).sorted().collect()
);
guarded_or_doomed = true;
}
}
if cell.cands == cell.min {
// Guard remaining candidates in this cell
let guarded = candidates_in_constraint_cell(state.election, &state.candidates, idx.slice());
if !guarded.is_empty() {
for candidate in guarded.iter() {
state.candidates.get_mut(candidate).unwrap().state = CandidateState::Guarded;
}
state.logger.log_smart(
"{} must be guarded to comply with constraints.",
"{} must be guarded to comply with constraints.",
guarded.iter().map(|c| c.name.as_str()).sorted().collect()
);
guarded_or_doomed = true;
}
}
}
return guarded_or_doomed;
}
ConstraintMode::RepeatCount => { return false; } // No action needed here: elect_hopefuls checks test_constraints_immediate
}
}
// ----------------------------------
// FOR --constraint-mode repeat_count
// ----------------------------------
/// Check constraints, with the state of the given candidates set to candidate_state – check if this immediately violates constraints
pub fn test_constraints_immediate<'a, N: Number>(state: &CountState<'a, N>, candidates: &[&Candidate], candidate_state: CandidateState) -> Result<(), (&'a Constraint, &'a ConstrainedGroup)> {
if state.election.constraints.is_none() {
return Ok(());
}
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;
}
if let Some((a, b)) = state.election.constraints.as_ref().unwrap().exceeds_maximum(state.election, trial_candidates) {
return Err((a, b));
}
return Ok(());
}
/// Initialise the [Election] as required for --constraint-mode repeat_count
pub fn init_repeat_count<N: Number>(election: &mut Election<N>) {
// Add dummy candidates
let mut new_candidates = Vec::new();
for candidate in &election.candidates {
let mut new_candidate = candidate.clone();
new_candidate.index += election.candidates.len(); // Ensure unique index
new_candidate.is_dummy = true;
new_candidates.push(new_candidate);
}
election.candidates.append(&mut new_candidates);
}
/// Initialise the rollback for [ConstraintMode::RepeatCount]
pub fn init_repeat_count_rollback<'a, N: Number>(state: &mut CountState<'a, N>, constraint: &'a Constraint, group: &'a ConstrainedGroup) {
let mut rollback_candidates = CandidateMap::with_capacity(state.candidates.len());
let rollback_exhausted = state.exhausted.clone();
// Copy ballot papers to rollback state
for (candidate, count_card) in state.candidates.iter_mut() {
rollback_candidates.insert(candidate, count_card.clone());
}
state.rollback_state = RollbackState::NeedsRollback { candidates: Some(rollback_candidates), exhausted: Some(rollback_exhausted), constraint, group };
}
/// Process one stage of rollback for [ConstraintMode::RepeatCount]
pub fn rollback_one_stage<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<(), STVError>
where
for<'r> &'r N: ops::Add<&'r N, Output=N>,
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>
{
if let RollbackState::NeedsRollback { candidates, exhausted, constraint, group } = &mut state.rollback_state {
let mut candidates = candidates.take().unwrap();
// Exclude candidates who cannot be elected due to constraint violations
let order_excluded = state.num_excluded + 1;
let mut excluded_candidates = Vec::new();
for candidate_idx in &group.candidates {
let count_card = state.candidates.get_mut(&state.election.candidates[*candidate_idx]).unwrap();
if count_card.state == CandidateState::Hopeful {
count_card.state = CandidateState::Excluded;
count_card.finalised = true;
state.num_excluded += 1;
count_card.order_elected = -(order_excluded as isize);
excluded_candidates.push(state.election.candidates[*candidate_idx].name.as_str());
}
}
// Prepare dummy candidates, etc.
for candidate in &state.election.candidates {
if candidate.is_dummy {
continue;
}
// Move ballot papers to dummy candidate
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == candidate.name && c.is_dummy).unwrap();
let dummy_count_card = state.candidates.get_mut(dummy_candidate).unwrap();
dummy_count_card.parcels.append(&mut candidates.get_mut(candidate).unwrap().parcels);
dummy_count_card.votes = candidates[candidate].votes.clone();
// Reset count
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.clear();
count_card.votes = N::new();
count_card.transfers = N::new();
if candidates[candidate].state == CandidateState::Elected {
if &candidates[candidate].votes > state.quota.as_ref().unwrap() {
count_card.votes = state.quota.as_ref().unwrap().clone();
} else {
count_card.votes = candidates[candidate].votes.clone();
}
}
}
state.title = StageKind::Rollback;
state.logger.log_smart(
"Rolled back to apply constraints. {} is excluded.",
"Rolled back to apply constraints. {} are excluded.",
excluded_candidates.into_iter().sorted().collect()
);
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: exhausted.take(), candidate_distributing: None, constraint: Some(constraint), group: Some(group) };
return Ok(());
}
if let RollbackState::RollingBack { candidates, exhausted, candidate_distributing, constraint, group } = &mut state.rollback_state {
let candidates = candidates.take().unwrap();
let mut exhausted = exhausted.take().unwrap();
let mut candidate_distributing = candidate_distributing.take();
let constraint = constraint.take().unwrap();
let group = group.take().unwrap();
// --------------------
// Distribute surpluses
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
.filter(|c| {
let cc = &candidates[c];
if !c.is_dummy && cc.state == CandidateState::Elected && !cc.finalised {
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
!state.candidates[dummy_candidate].finalised
} else {
false
}
})
.collect();
if !has_surplus.is_empty() {
// Distribute top candidate's surplus
let max_cands = match opts.surplus_order {
SurplusOrder::BySize => {
ties::multiple_max_by(&has_surplus, |c| &candidates[c].votes)
}
SurplusOrder::ByOrder => {
ties::multiple_min_by(&has_surplus, |c| candidates[c].order_elected)
}
};
let elected_candidate = if max_cands.len() > 1 {
stv::choose_highest(state, opts, &max_cands, "Which candidate's surplus to distribute?")?
} else {
max_cands[0]
};
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == elected_candidate.name && c.is_dummy).unwrap();
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { gregory::distribute_surplus(state, opts, dummy_candidate); }
SurplusMethod::IHare | SurplusMethod::Hare => { sample::distribute_surplus(state, opts, dummy_candidate)?; }
_ => unreachable!()
}
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing, constraint: Some(constraint), group: Some(group) };
rollback_check_complete(state);
return Ok(());
}
// ----------------------------------
// Distribute exhausted ballot papers
// FIXME: Untested!
if exhausted.parcels.iter().any(|p| !p.votes.is_empty()) {
// Use arbitrary dummy candidate
let dummy_candidate = state.election.candidates.iter().find(|c| c.is_dummy).unwrap();
let dummy_count_card = state.candidates.get_mut(dummy_candidate).unwrap();
dummy_count_card.parcels.append(&mut exhausted.parcels);
// Nasty hack to check if continuing distribution!!
if let StageKind::RollbackExhausted = state.title {
// Continuing
state.logger.log_literal(String::from("Continuing distribution of exhausted ballots."));
} else {
state.title = StageKind::RollbackExhausted;
state.logger.log_literal(String::from("Distributing exhausted ballots."));
}
stv::exclude_candidates(state, opts, vec![dummy_candidate], "Distribution")?;
let dummy_count_card = state.candidates.get_mut(dummy_candidate).unwrap();
exhausted.parcels.append(&mut dummy_count_card.parcels);
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing: None, constraint: Some(constraint), group: Some(group) };
rollback_check_complete(state);
return Ok(());
}
// ------------------------------------------
// Distribute ballots of electable candidates
let electable_candidates_old: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of multiple
.filter(|c| {
let cc = &candidates[c];
let cand_idx = state.election.candidates.iter().position(|x| x == *c).unwrap();
if !c.is_dummy && !group.candidates.contains(&cand_idx) && !cc.finalised {
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
!state.candidates[dummy_candidate].finalised
} else {
false
}
})
.collect();
if !electable_candidates_old.is_empty() {
if candidate_distributing.is_none() || !electable_candidates_old.contains(candidate_distributing.as_ref().unwrap()) {
if electable_candidates_old.len() > 1 {
// Determine or prompt for which candidate to distribute
for strategy in opts.ties.iter() {
match strategy {
TieStrategy::Random(_) | TieStrategy::Prompt => {
candidate_distributing = Some(strategy.choose_lowest(state, opts, &electable_candidates_old, "Which candidate's ballots to distribute?").unwrap());
break;
}
TieStrategy::Forwards | TieStrategy::Backwards => {}
}
}
} else {
candidate_distributing = Some(electable_candidates_old[0]);
}
state.logger.log_smart(
"Distributing ballots of {}.",
"Distributing ballots of {}.",
vec![candidate_distributing.unwrap().name.as_str()]
);
} else {
state.logger.log_smart(
"Continuing distribution of ballots of {}.",
"Continuing distribution of ballots of {}.",
vec![candidate_distributing.unwrap().name.as_str()]
);
}
let candidate_distributing = candidate_distributing.unwrap();
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == candidate_distributing.name && c.is_dummy).unwrap();
state.title = StageKind::BallotsOf(candidate_distributing);
stv::exclude_candidates(state, opts, vec![dummy_candidate], "Distribution")?;
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing: Some(candidate_distributing), constraint: Some(constraint), group: Some(group) };
rollback_check_complete(state);
return Ok(());
}
// --------------------------------------------
// Distribute ballots of unelectable candidates
let unelectable_candidates_old: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of multiple
.filter(|c| {
let cc = &candidates[c];
let cand_idx = state.election.candidates.iter().position(|x| x == *c).unwrap();
if !c.is_dummy && group.candidates.contains(&cand_idx) && !cc.finalised {
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
!state.candidates[dummy_candidate].finalised
} else {
false
}
})
.collect();
if !unelectable_candidates_old.is_empty() {
if candidate_distributing.is_none() || !unelectable_candidates_old.contains(candidate_distributing.as_ref().unwrap()) {
if unelectable_candidates_old.len() > 1 {
// Determine or prompt for which candidate to distribute
for strategy in opts.ties.iter() {
match strategy {
TieStrategy::Random(_) | TieStrategy::Prompt => {
candidate_distributing = Some(strategy.choose_lowest(state, opts, &unelectable_candidates_old, "Which candidate's ballots to distribute?").unwrap());
break;
}
TieStrategy::Forwards | TieStrategy::Backwards => {}
}
}
} else {
candidate_distributing = Some(unelectable_candidates_old[0]);
}
state.logger.log_smart(
"Distributing ballots of {}.",
"Distributing ballots of {}.",
vec![candidate_distributing.unwrap().name.as_str()]
);
} else {
state.logger.log_smart(
"Continuing distribution of ballots of {}.",
"Continuing distribution of ballots of {}.",
vec![candidate_distributing.unwrap().name.as_str()]
);
}
let candidate_distributing = candidate_distributing.unwrap();
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == candidate_distributing.name && c.is_dummy).unwrap();
state.title = StageKind::BallotsOf(candidate_distributing);
stv::exclude_candidates(state, opts, vec![dummy_candidate], "Distribution")?;
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing: Some(candidate_distributing), constraint: Some(constraint), group: Some(group) };
rollback_check_complete(state);
return Ok(());
}
}
unreachable!();
}
fn rollback_check_complete<N: Number>(state: &mut CountState<N>) {
if let RollbackState::RollingBack { candidates, exhausted, candidate_distributing: _, constraint: _, group: _ } = &state.rollback_state {
let candidates = candidates.as_ref().unwrap();
let exhausted = exhausted.as_ref().unwrap();
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
.filter(|c| {
let cc = &candidates[c];
if !c.is_dummy && cc.state == CandidateState::Elected && !cc.finalised {
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
!state.candidates[dummy_candidate].finalised
} else {
false
}
})
.collect();
if !has_surplus.is_empty() {
return;
}
if exhausted.parcels.iter().any(|p| !p.votes.is_empty()) {
return;
}
let continuing_candidates: Vec<&Candidate> = state.election.candidates.iter()
.filter(|c| {
let cc = &candidates[c];
if !c.is_dummy && !cc.finalised {
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
!state.candidates[dummy_candidate].finalised
} else {
false
}
})
.collect();
if !continuing_candidates.is_empty() {
return;
}
// ---------------------------
// Rollback complete: finalise
// Delete dummy candidates
for (candidate, count_card) in state.candidates.iter_mut() {
if candidate.is_dummy {
count_card.state = CandidateState::Withdrawn;
count_card.parcels.clear();
count_card.votes = N::new();
count_card.transfers = N::new();
}
}
state.logger.log_literal(String::from("Rollback complete."));
state.rollback_state = RollbackState::Normal;
state.num_excluded = state.candidates.values().filter(|cc| cc.state == CandidateState::Excluded).count();
return;
}
unreachable!();
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_cell(cm: &ConstraintMatrix, idx: &[usize], elected: usize, min: usize, max: usize, cands: usize) {
assert_eq!(cm[idx].elected, elected, "Failed to validate elected at {:?}", idx);
assert_eq!(cm[idx].min, min, "Failed to validate min at {:?}", idx);
assert_eq!(cm[idx].max, max, "Failed to validate min at {:?}", idx);
assert_eq!(cm[idx].cands, cands, "Failed to validate cands at {:?}", idx);
}
#[test]
fn cm_otten() {
let mut cm = ConstraintMatrix::new(&mut [3, 2]);
// Totals
let c = &mut cm[&[0, 1]]; c.min = 7; c.max = 7;
let c = &mut cm[&[0, 2]]; c.min = 7; c.max = 7;
let c = &mut cm[&[1, 0]]; c.min = 7; c.max = 7;
let c = &mut cm[&[2, 0]]; c.min = 6; c.max = 6;
let c = &mut cm[&[3, 0]]; c.min = 1; c.max = 1;
// Candidates
let c = &mut cm[&[1, 1]]; c.cands = 4;
let c = &mut cm[&[2, 1]]; c.cands = 11;
let c = &mut cm[&[3, 1]]; c.cands = 2;
let c = &mut cm[&[1, 2]]; c.cands = 7;
let c = &mut cm[&[2, 2]]; c.cands = 3;
let c = &mut cm[&[3, 2]]; c.cands = 1;
// Init
cm.init();
while !cm.step().expect("No conformant result") {}
println!("{}", cm);
assert_cell(&cm, &[1, 1], 0, 0, 4, 4);
assert_cell(&cm, &[2, 1], 0, 3, 6, 11);
assert_cell(&cm, &[3, 1], 0, 0, 1, 2);
assert_cell(&cm, &[0, 1], 0, 7, 7, 17);
assert_cell(&cm, &[1, 2], 0, 3, 7, 7);
assert_cell(&cm, &[2, 2], 0, 0, 3, 3);
assert_cell(&cm, &[3, 2], 0, 0, 1, 1);
assert_cell(&cm, &[0, 2], 0, 7, 7, 11);
assert_cell(&cm, &[1, 0], 0, 7, 7, 11);
assert_cell(&cm, &[2, 0], 0, 6, 6, 14);
assert_cell(&cm, &[3, 0], 0, 1, 1, 3);
assert_cell(&cm, &[0, 0], 0, 14, 14, 28);
// Election of Welsh man
cm[&[3, 1]].elected += 1;
cm.recount_cands();
while !cm.step().expect("No conformant result") {}
println!("{}", cm);
assert_cell(&cm, &[1, 1], 0, 0, 3, 4);
assert_cell(&cm, &[2, 1], 0, 3, 6, 11);
assert_cell(&cm, &[3, 1], 1, 1, 1, 2);
assert_cell(&cm, &[0, 1], 1, 7, 7, 17); // Error in Otten paper
assert_cell(&cm, &[1, 2], 0, 4, 7, 7);
assert_cell(&cm, &[2, 2], 0, 0, 3, 3);
assert_cell(&cm, &[3, 2], 0, 0, 0, 1);
assert_cell(&cm, &[0, 2], 0, 7, 7, 11);
assert_cell(&cm, &[1, 0], 0, 7, 7, 11);
assert_cell(&cm, &[2, 0], 0, 6, 6, 14);
assert_cell(&cm, &[3, 0], 1, 1, 1, 3);
assert_cell(&cm, &[0, 0], 1, 14, 14, 28);
// Remaining Welsh man, Welsh woman doomed
cm[&[3, 1]].cands -= 1;
cm[&[3, 2]].cands -= 1;
// Election of 2 English men, 2 English women
// Exclusion of 1 Scottish woman
cm[&[1, 1]].elected += 2;
cm[&[1, 2]].elected += 2;
cm[&[2, 2]].cands -= 1;
cm.recount_cands();
while !cm.step().expect("No conformant result") {}
println!("{}", cm);
assert_cell(&cm, &[1, 1], 2, 2, 2, 4);
assert_cell(&cm, &[2, 1], 0, 4, 4, 11);
assert_cell(&cm, &[3, 1], 1, 1, 1, 1);
assert_cell(&cm, &[0, 1], 3, 7, 7, 16); // Error in Otten paper
assert_cell(&cm, &[1, 2], 2, 5, 5, 7);
assert_cell(&cm, &[2, 2], 0, 2, 2, 2);
assert_cell(&cm, &[3, 2], 0, 0, 0, 0);
assert_cell(&cm, &[0, 2], 2, 7, 7, 9);
assert_cell(&cm, &[1, 0], 4, 7, 7, 11);
assert_cell(&cm, &[2, 0], 0, 6, 6, 13);
assert_cell(&cm, &[3, 0], 1, 1, 1, 1);
assert_cell(&cm, &[0, 0], 5, 14, 14, 25);
}
}