Skip to content

Commit

Permalink
lexless and lexlesseq
Browse files Browse the repository at this point in the history
  • Loading branch information
Dimosts committed May 21, 2024
1 parent 2ed49d0 commit 023ea98
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 7 deletions.
2 changes: 1 addition & 1 deletion cpmpy/expressions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .variables import boolvar, intvar, cpm_array
from .variables import BoolVar, IntVar, cparray # Old, to be deprecated
from .globalconstraints import AllDifferent, AllDifferentExcept0, AllEqual, Circuit, Inverse, Table, Xor, Cumulative, \
IfThenElse, GlobalCardinalityCount, DirectConstraint, InDomain, Increasing, Decreasing, IncreasingStrict, DecreasingStrict
IfThenElse, GlobalCardinalityCount, DirectConstraint, InDomain, Increasing, Decreasing, IncreasingStrict, DecreasingStrict, LexLess, LexLessEq
from .globalconstraints import alldifferent, allequal, circuit # Old, to be deprecated
from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue
from .core import BoolVal
Expand Down
35 changes: 33 additions & 2 deletions cpmpy/expressions/globalconstraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,14 +615,14 @@ def value(self):


class LexLessEq(GlobalConstraint):
""" Given lists X,Y, enforcing that X is lexicographically less than Y.
""" Given lists X,Y, enforcing that X is lexicographically less than Y (or equal).
Implementation inspired by Hakan Kjellerstrand (http://hakank.org/cpmpy/cpmpy_hakank.py)
"""
def __init__(self, list1, list2):
X = flatlist(list1)
Y = flatlist(list2)
if len(X) != len(Y):
raise CPMpyException(f"The 2 lists given in LexLess must have the same size: X length is {len(X)} and Y length is {len(Y)}")
raise CPMpyException(f"The 2 lists given in LexLessEq must have the same size: X length is {len(X)} and Y length is {len(Y)}")
super().__init__("lex_lesseq", [X, Y])

def decompose(self):
Expand All @@ -645,6 +645,37 @@ def value(self):
return argval(any((X[i] < Y[i]) & all(X[j] <= Y[j] for j in range(i-1)) for i in range(len(X))) | all(X[i] == Y[i] for i in range(len(X))))


class LexLess(GlobalConstraint):
""" Given lists X,Y, enforcing that X is lexicographically less than Y.
Implementation inspired by Hakan Kjellerstrand (http://hakank.org/cpmpy/cpmpy_hakank.py)
"""
def __init__(self, list1, list2):
X = flatlist(list1)
Y = flatlist(list2)
if len(X) != len(Y):
raise CPMpyException(f"The 2 lists given in LexLess must have the same size: X length is {len(X)} and Y length is {len(Y)}")
super().__init__("lex_less", [X, Y])

def decompose(self):
X, Y = cpm_array(self.args[0]), cpm_array(self.args[1])
length = len(X)

constraining = []
bvar = boolvar(shape=(length + 1))
from cpmpy.transformations.normalize import toplevel_list
defining = toplevel_list(bvar == ((X <= Y) &
((X < Y) | bvar[1:])))
defining.append(bvar[-1] == (X[-1] < Y[-1])) #for strict case use <
constraining.append(bvar[0])

return constraining, defining

def value(self):
from .python_builtins import any, all
X, Y = self.args
return argval(any((X[i] < Y[i]) & all(X[j] <= Y[j] for j in range(i-1)) for i in range(len(X))))


class DirectConstraint(Expression):
"""
A DirectConstraint will directly call a function of the underlying solver when added to a CPMpy solver
Expand Down
1 change: 1 addition & 0 deletions cpmpy/solvers/minizinc.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class CPM_minizinc(SolverInterface):
required_version = (2, 8, 2)
@staticmethod
def supported():
return False
return CPM_minizinc.installed() and not CPM_minizinc.outdated()

@staticmethod
Expand Down
14 changes: 10 additions & 4 deletions tests/test_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
EXCLUDE_GLOBAL = {"ortools": {},
"gurobi": {},
"minizinc": {"circuit"},
"pysat": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative","increasing","decreasing","increasing_strict","decreasing_strict","lex_less"},
"pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative","xor","increasing","decreasing","increasing_strict","decreasing_strict","lex_less"},
"pysat": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative","increasing","decreasing","increasing_strict","decreasing_strict","lex_less","lex_lesseq"},
"pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative","xor","increasing","decreasing","increasing_strict","decreasing_strict","lex_less","lex_lesseq"},
"exact": {},
"choco": {}
}
Expand Down Expand Up @@ -150,7 +150,7 @@ def global_constraints(solver):
"""
Generate all global constraints
- AllDifferent, AllEqual, Circuit, Minimum, Maximum, Element,
Xor, Cumulative, NValue, Count, Increasing, Decreasing, IncreasingStrict, DecreasingStrict, LexLess
Xor, Cumulative, NValue, Count, Increasing, Decreasing, IncreasingStrict, DecreasingStrict, LexLessEq, LexLess
"""
global_cons = [AllDifferent, AllEqual, Minimum, Maximum, NValue, Increasing, Decreasing, IncreasingStrict, DecreasingStrict]
for global_type in global_cons:
Expand All @@ -176,6 +176,11 @@ def global_constraints(solver):
cap = 10
yield Cumulative(s, dur, e, demand, cap)

if solver not in EXCLUDE_GLOBAL or "lex_lesseq" not in EXCLUDE_GLOBAL[solver]:
X = intvar(0, 10, shape=10)
Y = intvar(0, 10, shape=10)
yield LexLessEq(X, Y)

if solver not in EXCLUDE_GLOBAL or "lex_less" not in EXCLUDE_GLOBAL[solver]:
X = intvar(0, 10, shape=10)
Y = intvar(0, 10, shape=10)
Expand Down Expand Up @@ -228,4 +233,5 @@ def test_reify_imply_constraints(solver, constraint):
Tests boolean expression by posting it to solver and checking the value after solve.
"""
assert SolverLookup.get(solver, Model(constraint)).solve()
assert constraint.value()
assert constraint.value(), f"Constraint {constraint} is not satisfied ({constraint.value()}), arguments: {[c.value() for c in constraint.args[0].args[0].args[0]]}, {[c.value() for c in constraint.args[0].args[0].args[1]]} "
; V
22 changes: 22 additions & 0 deletions tests/test_globalconstraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,28 @@ def test_InDomain(self):
self.assertTrue(model.solve())
self.assertIn(bv.value(), vals)


def test_lex_lesseq(self):
from cpmpy import BoolVal
X = cp.intvar(0, 3, shape=10)
c1 = X[:-1] == 1
Y = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
c = cp.LexLessEq(X, Y)
c2 = c != (BoolVal(True))
m = cp.Model([c1, c2])
self.assertTrue(m.solve())
self.assertTrue(c2.value())
self.assertFalse(c.value())

Y = cp.intvar(0, 0, shape=10)
c = cp.LexLessEq(X, Y)
m = cp.Model(c)
self.assertTrue(m.solve("choco"))
self.assertTrue(m.solve("minizinc"))
self.assertTrue(m.solve("ortools"))
from cpmpy.expressions.utils import argval
self.assertTrue(sum(argval(X)) == 0)

def test_indomain_onearg(self):

iv = cp.intvar(0, 10)
Expand Down

0 comments on commit 023ea98

Please sign in to comment.