Basic Otten constraints table iteration
This commit is contained in:
parent
7e8af03933
commit
64f698e182
144
pyRCV2/constraints.py
Normal file
144
pyRCV2/constraints.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
from pyRCV2.method.base_stv import STVException
|
||||||
|
|
||||||
|
class ConstraintMatrix:
|
||||||
|
def __init__(self, dimensions):
|
||||||
|
self.dimensions = dimensions
|
||||||
|
|
||||||
|
num_cells = 1
|
||||||
|
for d in dimensions:
|
||||||
|
num_cells *= (d + 1)
|
||||||
|
|
||||||
|
# FIXME: This is not the most efficient packing, as we require margins only along one dimension at a time
|
||||||
|
self.matrix = [ConstraintMatrixCell() for _ in range(num_cells)]
|
||||||
|
|
||||||
|
# Pass -1 to get margins
|
||||||
|
def index_of(self, address):
|
||||||
|
index = 0
|
||||||
|
for i, d in enumerate(self.dimensions):
|
||||||
|
index *= (d + 1)
|
||||||
|
index += (address[i] + 1)
|
||||||
|
return index
|
||||||
|
|
||||||
|
def get(self, address):
|
||||||
|
return self.matrix[self.index_of(address)]
|
||||||
|
|
||||||
|
class ConstraintMatrixCell:
|
||||||
|
def __init__(self):
|
||||||
|
self.elected = 0
|
||||||
|
self.min = 0
|
||||||
|
self.max = 0
|
||||||
|
self.cands = 0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<ConstraintMatrixCell: E={}, Max={}, Min={}, C={}>'.format(self.elected, self.min, self.max, self.cands)
|
||||||
|
|
||||||
|
def ndrange(dimensions):
|
||||||
|
# n-dimensional range function
|
||||||
|
if len(dimensions) == 0:
|
||||||
|
yield []
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(dimensions) == 1:
|
||||||
|
yield from ([n] for n in range(dimensions[0]))
|
||||||
|
return
|
||||||
|
|
||||||
|
yield from ([n] + j for n in range(dimensions[0]) for j in ndrange(dimensions[1:]))
|
||||||
|
|
||||||
|
def init_matrix(counter):
|
||||||
|
counter._constraint_matrix = ConstraintMatrix([len(category) for category_name, category in counter.election.constraints.items()])
|
||||||
|
|
||||||
|
def update_matrix(counter):
|
||||||
|
# Update matrix with elected numbers
|
||||||
|
...
|
||||||
|
|
||||||
|
def step_matrix(counter):
|
||||||
|
cm = counter._constraint_matrix
|
||||||
|
|
||||||
|
# Rule 1
|
||||||
|
for cell in cm.matrix:
|
||||||
|
if cell.elected > cell.min:
|
||||||
|
cell.min = cell.elected
|
||||||
|
return False
|
||||||
|
if cell.cands < cell.max:
|
||||||
|
cell.max = cell.cands
|
||||||
|
return False
|
||||||
|
if cell.min > cell.max:
|
||||||
|
raise STVException('No result conformant with the constraints is possible')
|
||||||
|
|
||||||
|
# Rule 2/3
|
||||||
|
for address in ndrange(cm.dimensions):
|
||||||
|
cell = cm.get(address)
|
||||||
|
|
||||||
|
for dimension in range(len(cm.dimensions)):
|
||||||
|
tot_min_others = 0
|
||||||
|
tot_max_others = 0
|
||||||
|
|
||||||
|
addr2 = [x for x in address]
|
||||||
|
for i in range(cm.dimensions[dimension]):
|
||||||
|
if i == address[dimension]:
|
||||||
|
continue
|
||||||
|
addr2[dimension] = i
|
||||||
|
|
||||||
|
cell2 = cm.get(addr2)
|
||||||
|
tot_min_others += cell2.min
|
||||||
|
tot_max_others += cell2.max
|
||||||
|
|
||||||
|
addr2[dimension] = -1
|
||||||
|
cell_dimension = cm.get(addr2)
|
||||||
|
min_dimension = cell_dimension.min
|
||||||
|
max_dimension = cell_dimension.max
|
||||||
|
|
||||||
|
# This many must be elected from this cell at least
|
||||||
|
this_cell_min = min_dimension - tot_max_others
|
||||||
|
this_cell_max = max_dimension - tot_min_others
|
||||||
|
|
||||||
|
# Rule 2
|
||||||
|
if this_cell_min > cell.min:
|
||||||
|
cell.min = this_cell_min
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Rule 3
|
||||||
|
if this_cell_max < cell.max:
|
||||||
|
cell.max = this_cell_max
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Rule 4/5
|
||||||
|
for dimension in range(len(cm.dimensions)):
|
||||||
|
for addr1 in ndrange(cm.dimensions[:dimension]):
|
||||||
|
for addr2 in ndrange(cm.dimensions[dimension+1:]):
|
||||||
|
tot_min = 0
|
||||||
|
tot_max = 0
|
||||||
|
|
||||||
|
for addr_d in range(cm.dimensions[dimension]):
|
||||||
|
address = addr1 + [addr_d] + addr2
|
||||||
|
tot_min += cm.get(address).min
|
||||||
|
tot_max += cm.get(address).max
|
||||||
|
|
||||||
|
address = addr1 + [-1] + addr2
|
||||||
|
cell = cm.get(address)
|
||||||
|
|
||||||
|
# Rule 4
|
||||||
|
if cell.min < tot_min:
|
||||||
|
cell.min = tot_min
|
||||||
|
|
||||||
|
# Rule 5
|
||||||
|
if cell.max > tot_max:
|
||||||
|
cell.max = tot_max
|
||||||
|
|
||||||
|
return True
|
132
tests/test_constraints_otten.py
Normal file
132
tests/test_constraints_otten.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
from pyRCV2 import constraints
|
||||||
|
|
||||||
|
class CounterStub:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_constraints_otten():
|
||||||
|
counter = CounterStub()
|
||||||
|
counter._constraint_matrix = constraints.ConstraintMatrix([2, 3])
|
||||||
|
cm = counter._constraint_matrix
|
||||||
|
|
||||||
|
# Fill in details
|
||||||
|
c = cm.get([0, 0]); c.max = 7; c.cands = 4
|
||||||
|
c = cm.get([0, 1]); c.max = 6; c.cands = 11
|
||||||
|
c = cm.get([0, 2]); c.max = 1; c.cands = 2
|
||||||
|
c = cm.get([0, -1]); c.min = 7; c.max = 7; c.cands = 17
|
||||||
|
|
||||||
|
c = cm.get([1, 0]); c.max = 7; c.cands = 7
|
||||||
|
c = cm.get([1, 1]); c.max = 6; c.cands = 3
|
||||||
|
c = cm.get([1, 2]); c.max = 1; c.cands = 1
|
||||||
|
c = cm.get([1, -1]); c.min = 7; c.max = 7; c.cands = 11
|
||||||
|
|
||||||
|
c = cm.get([-1, 0]); c.min = 7; c.max = 7; c.cands = 11
|
||||||
|
c = cm.get([-1, 1]); c.min = 6; c.max = 6; c.cands = 14
|
||||||
|
c = cm.get([-1, 2]); c.min = 1; c.max = 1; c.cands = 3
|
||||||
|
c = cm.get([-1, -1]); c.min = 14; c.max = 14; c.cands = 28
|
||||||
|
|
||||||
|
assert cm.get([0, 0]).max != 4
|
||||||
|
assert cm.get([1, 1]).max != 3
|
||||||
|
assert cm.get([0, 1]).min != 3
|
||||||
|
assert cm.get([1, 0]).min != 3
|
||||||
|
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == True
|
||||||
|
|
||||||
|
assert cm.get([0, 0]).max == 4
|
||||||
|
assert cm.get([1, 1]).max == 3
|
||||||
|
assert cm.get([0, 1]).min == 3
|
||||||
|
assert cm.get([1, 0]).min == 3
|
||||||
|
|
||||||
|
# Election of Welsh Man
|
||||||
|
cm.get([0, 2]).elected += 1
|
||||||
|
cm.get([-1, 2]).elected += 1
|
||||||
|
cm.get([0, -1]).elected += 1
|
||||||
|
|
||||||
|
assert cm.get([0, 2]).min != 1
|
||||||
|
assert cm.get([0, 0]).max != 3
|
||||||
|
assert cm.get([1, 0]).min != 4
|
||||||
|
assert cm.get([1, 2]).max != 0
|
||||||
|
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == True
|
||||||
|
|
||||||
|
assert cm.get([0, 2]).min == 1
|
||||||
|
assert cm.get([0, 0]).max == 3
|
||||||
|
assert cm.get([1, 0]).min == 4
|
||||||
|
assert cm.get([1, 2]).max == 0
|
||||||
|
|
||||||
|
# Welsh Man and Welsh Woman are doomed
|
||||||
|
assert cm.get([0, 2]).elected == cm.get([0, 2]).max
|
||||||
|
assert cm.get([1, 2]).elected == cm.get([1, 2]).max
|
||||||
|
cm.get([0, 2]).cands = 1
|
||||||
|
cm.get([1, 2]).cands = 0
|
||||||
|
|
||||||
|
# Election of 2 English Men
|
||||||
|
cm.get([0, 0]).elected += 2
|
||||||
|
cm.get([-1, 0]).elected += 2
|
||||||
|
cm.get([0, -1]).elected += 2
|
||||||
|
|
||||||
|
# Election of 2 English Women
|
||||||
|
cm.get([1, 0]).elected += 2
|
||||||
|
cm.get([-1, 0]).elected += 2
|
||||||
|
cm.get([1, -1]).elected += 2
|
||||||
|
|
||||||
|
# Exclusion of Scottish Woman
|
||||||
|
cm.get([1, 1]).cands -= 1
|
||||||
|
cm.get([-1, 1]).cands -= 1
|
||||||
|
cm.get([1, -1]).cands -= 1
|
||||||
|
|
||||||
|
assert cm.get([0, 0]).min != 2
|
||||||
|
assert cm.get([1, 1]).max != 2
|
||||||
|
assert cm.get([0, 1]).min != 4
|
||||||
|
assert cm.get([1, 0]).min != 5
|
||||||
|
assert cm.get([0, 0]).max != 2
|
||||||
|
assert cm.get([0, 1]).max != 4
|
||||||
|
assert cm.get([1, 1]).min != 2
|
||||||
|
assert cm.get([1, 0]).max != 5
|
||||||
|
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == False
|
||||||
|
assert constraints.step_matrix(counter) == True
|
||||||
|
|
||||||
|
assert cm.get([0, 0]).min == 2
|
||||||
|
assert cm.get([1, 1]).max == 2
|
||||||
|
assert cm.get([0, 1]).min == 4
|
||||||
|
assert cm.get([1, 0]).min == 5
|
||||||
|
assert cm.get([0, 0]).max == 2
|
||||||
|
assert cm.get([0, 1]).max == 4
|
||||||
|
assert cm.get([1, 1]).min == 2
|
||||||
|
assert cm.get([1, 0]).max == 5
|
||||||
|
|
||||||
|
# English Men doomed
|
||||||
|
assert cm.get([0, 0]).elected == cm.get([0, 0]).max
|
||||||
|
# Scottish Women guarded
|
||||||
|
assert cm.get([1, 1]).cands == cm.get([1, 1]).min
|
Reference in New Issue
Block a user