Validate constraints

This commit is contained in:
RunasSudo 2021-05-22 01:38:36 +10:00
parent 15c6f3debe
commit 9f198c5e0f
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
10 changed files with 84 additions and 8 deletions

View File

@ -133,7 +133,7 @@ function handleException(ex) {
if (py.isinstance(ex, py.pyRCV2.ties.RequireInput)) { if (py.isinstance(ex, py.pyRCV2.ties.RequireInput)) {
// Signals we require input to break a tie // Signals we require input to break a tie
postMessage({'type': 'require_input', 'message': ex.message}); postMessage({'type': 'require_input', 'message': ex.message});
} else if (py.isinstance(ex, py.pyRCV2.method.base_stv.STVException)) { } else if (py.isinstance(ex, py.pyRCV2.exceptions.BaseRCVException)) {
console.error(ex); console.error(ex);
postMessage({'type': 'stv_exception', 'message': ex.message}); postMessage({'type': 'stv_exception', 'message': ex.message});
} else { } else {

View File

@ -16,10 +16,11 @@
__pragma__ = lambda x: None __pragma__ = lambda x: None
from pyRCV2.exceptions import BaseRCVException
from pyRCV2.model import CandidateState from pyRCV2.model import CandidateState
from pyRCV2.safedict import SafeDict from pyRCV2.safedict import SafeDict
class ConstraintException(Exception): class ConstraintException(BaseRCVException):
pass pass
class ConstraintMatrix: class ConstraintMatrix:

20
pyRCV2/exceptions.py Normal file
View File

@ -0,0 +1,20 @@
# pyRCV2: Preferential vote counting
# Copyright © 2020–2021 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/>.
class BaseRCVException(Exception):
def __init__(self, message):
Exception.__init__(self, message)
self.message = message

View File

@ -17,16 +17,15 @@
__pragma__ = lambda x: None __pragma__ = lambda x: None
from pyRCV2 import constraints from pyRCV2 import constraints
from pyRCV2.exceptions import BaseRCVException
from pyRCV2.model import BallotInCount, CandidateState, CountCard, CountCompleted, CountStepResult from pyRCV2.model import BallotInCount, CandidateState, CountCard, CountCompleted, CountStepResult
import pyRCV2.numbers import pyRCV2.numbers
from pyRCV2.numbers import Num from pyRCV2.numbers import Num
from pyRCV2.safedict import SafeDict from pyRCV2.safedict import SafeDict
import pyRCV2.ties import pyRCV2.ties
class STVException(Exception): class STVException(BaseRCVException):
def __init__(self, message): pass
Exception.__init__(self)
self.message = message
class BaseSTVCounter: class BaseSTVCounter:
""" """
@ -87,6 +86,7 @@ class BaseSTVCounter:
self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots
if self.election.constraints: if self.election.constraints:
self.election.validate_constraints()
constraints.init_matrix(self) constraints.init_matrix(self)
constraints.stabilise_matrix(self) constraints.stabilise_matrix(self)
self.logs.extend(constraints.guard_or_doom(self)) self.logs.extend(constraints.guard_or_doom(self))

View File

@ -127,6 +127,7 @@ class MeekSTVCounter(BaseSTVCounter):
self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots self._exclusion = None # Optimisation to avoid re-collating/re-sorting ballots
if self.election.constraints: if self.election.constraints:
self.election.validate_constraints()
constraints.init_matrix(self) constraints.init_matrix(self)
constraints.stabilise_matrix(self) constraints.stabilise_matrix(self)
self.logs.extend(constraints.guard_or_doom(self)) self.logs.extend(constraints.guard_or_doom(self))

View File

@ -96,8 +96,18 @@ class Election:
def validate_constraints(self): def validate_constraints(self):
"""Confirm that each constraint features each candidate once and only once""" """Confirm that each constraint features each candidate once and only once"""
# TODO from pyRCV2.constraints import ConstraintException
return True
for category_name, category in self.constraints.items():
candidates = [x for x in self.candidates]
for group_name, group in category.items():
for candidate in group.candidates:
if candidate in candidates:
candidates.remove(candidate)
else:
raise ConstraintException('Candidate "' + candidate.name + '" duplicated in category "' + category_name + '"')
if len(candidates) > 0:
raise ConstraintException('Candidate "' + candidates[0].name + '" not assigned to a group in category "' + category_name + '"')
class CountCard: class CountCard:
""" """

View File

@ -17,6 +17,7 @@
import pyRCV2.blt import pyRCV2.blt
import pyRCV2.con import pyRCV2.con
import pyRCV2.constraints import pyRCV2.constraints
import pyRCV2.exceptions
import pyRCV2.model import pyRCV2.model
import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.gregory, pyRCV2.method.meek import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.gregory, pyRCV2.method.meek
import pyRCV2.numbers import pyRCV2.numbers

View File

@ -0,0 +1 @@
"Gender" "Men" 0 2 2 3 4 6

View File

@ -0,0 +1,2 @@
"Gender" "Men" 0 2 2 3 4 2 6
"Gender" "Women" 2 99 1 5 7

View File

@ -0,0 +1,40 @@
# pyRCV2: Preferential vote counting
# Copyright © 2020–2021 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/>.
import pytest
import pyRCV2.blt, pyRCV2.con
from pyRCV2.constraints import ConstraintException
def test_constraints_validation_invalid1():
with open('tests/data/prsa1.blt', 'r') as f:
election = pyRCV2.blt.readBLT(f.read())
with open('tests/data/prsa1_invalid1.con', 'r') as f:
election.constraints = pyRCV2.con.readCON(f.read(), election)
with pytest.raises(ConstraintException, match='Candidate "Evans" not assigned to a group in category "Gender"'):
election.validate_constraints()
def test_constraints_validation_invalid2():
with open('tests/data/prsa1.blt', 'r') as f:
election = pyRCV2.blt.readBLT(f.read())
with open('tests/data/prsa1_invalid2.con', 'r') as f:
election.constraints = pyRCV2.con.readCON(f.read(), election)
with pytest.raises(ConstraintException, match='Candidate "Grey" duplicated in category "Gender"'):
election.validate_constraints()