diff --git a/pyRCV2/constraints.py b/pyRCV2/constraints.py
new file mode 100644
index 0000000..3c58823
--- /dev/null
+++ b/pyRCV2/constraints.py
@@ -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 .
+
+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 ''.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
diff --git a/tests/test_constraints_otten.py b/tests/test_constraints_otten.py
new file mode 100644
index 0000000..cf65ec7
--- /dev/null
+++ b/tests/test_constraints_otten.py
@@ -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 .
+
+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