Basic Otten constraints table iteration

This commit is contained in:
RunasSudo 2021-05-17 01:30:31 +10:00
parent 7e8af03933
commit 64f698e182
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
2 changed files with 276 additions and 0 deletions

144
pyRCV2/constraints.py Normal file
View 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

View 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