Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lex list global constraints #477

Merged
merged 30 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bf47912
lex less + fix in .value() of increasing and decreasing
Dimosts May 10, 2024
0054eee
add tests
Dimosts May 10, 2024
a6f0e7e
correct value() function
Dimosts May 10, 2024
59e9fec
Update globalconstraints.py
Dimosts May 10, 2024
e3528b1
correct defining and constraining
Dimosts May 15, 2024
a96d266
correct .value
Dimosts May 15, 2024
2ed49d0
change to lex_lesseq and add to choco and minizinc
Dimosts May 15, 2024
023ea98
lexless and lexlesseq
Dimosts May 21, 2024
16ec8ea
woops
Dimosts May 21, 2024
9c1412a
woops2
Dimosts May 21, 2024
d78ca4a
fixed issue on .value()
Dimosts May 21, 2024
f79ad43
add in minizinc and choco
Dimosts May 21, 2024
fb4c06b
fix decompose globals
Dimosts May 22, 2024
15f2ccb
tests
Dimosts May 22, 2024
17d5a1b
LexChain correct init
Dimosts May 22, 2024
293964c
LexChainLessEq
Dimosts May 22, 2024
5a98d7f
globals to solvers
Dimosts May 22, 2024
28309ac
minor
Dimosts May 22, 2024
c4f95e5
Merge branch 'master' into lex-list-globals
Dimosts May 22, 2024
b69d72d
fix in tests
Dimosts May 22, 2024
cbb23bf
tests for chain
Dimosts May 22, 2024
37dac0f
recursive argvals
Dimosts May 24, 2024
4218554
smaller tests
Dimosts May 24, 2024
8a44cc9
fix decompose_in_tree
Dimosts May 24, 2024
99480fd
argvals to args in value functions
Dimosts May 24, 2024
981ef5e
some decomposition improvements
Dimosts May 24, 2024
3aa091a
documentation in docstring
Dimosts May 24, 2024
5c4230d
call super init
Dimosts May 24, 2024
975e32c
pychoco issue link
Dimosts May 24, 2024
a5399a2
Merge branch 'master' into lex-list-globals
Dimosts May 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, LexChainLess, LexChainLessEq
from .globalconstraints import alldifferent, allequal, circuit # Old, to be deprecated
from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue, NValueExcept
from .core import BoolVal
Expand Down
110 changes: 106 additions & 4 deletions cpmpy/expressions/globalconstraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ def decompose(self):
def value(self):
from .python_builtins import all
args = self.args
return all(args[i].value() <= args[i+1].value() for i in range(len(args)-1))
return argval(all(args[i] <= args[i+1] for i in range(len(args)-1)))
Dimosts marked this conversation as resolved.
Show resolved Hide resolved


class Decreasing(GlobalConstraint):
Expand All @@ -569,7 +569,7 @@ def decompose(self):
def value(self):
from .python_builtins import all
args = self.args
return all(args[i].value() >= args[i+1].value() for i in range(len(args)-1))
return argval(all(args[i] >= args[i+1] for i in range(len(args)-1)))


class IncreasingStrict(GlobalConstraint):
Expand All @@ -592,7 +592,7 @@ def decompose(self):
def value(self):
from .python_builtins import all
args = self.args
return all((args[i].value() < args[i+1].value()) for i in range(len(args)-1))
return argval(all(args[i] < args[i+1] for i in range(len(args)-1)))


class DecreasingStrict(GlobalConstraint):
Expand All @@ -615,7 +615,109 @@ def decompose(self):
def value(self):
from .python_builtins import all
args = self.args
return all((args[i].value() > args[i+1].value()) for i in range(len(args)-1))
return argval(all(args[i] > args[i+1] for i in range(len(args)-1)))


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):
Dimosts marked this conversation as resolved.
Show resolved Hide resolved
X, Y = cpm_array(self.args[0]), cpm_array(self.args[1])
Dimosts marked this conversation as resolved.
Show resolved Hide resolved
length = len(X)
Dimosts marked this conversation as resolved.
Show resolved Hide resolved

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

return constraining, defining
Dimosts marked this conversation as resolved.
Show resolved Hide resolved

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)) for i in range(len(X))))


class LexLessEq(GlobalConstraint):
""" 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 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):
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 <
Dimosts marked this conversation as resolved.
Show resolved Hide resolved
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)) for i in range(len(X))) | all(X[i] == Y[i] for i in range(len(X))))


class LexChainLess(GlobalConstraint):
""" Given a matrix X,, enforces that all rows are lexicographically ordered.
"""
def __init__(self, X):
# Ensure the numpy array is 2D
X = cpm_array(X)
assert X.ndim == 2, "Input must be a 2D array or a list of lists"
self.name = "lex_chain_less"
self.args = X

def decompose(self):
X = self.args
return [LexLess(prev_row, curr_row) for prev_row, curr_row in zip(X, X[1:])], []

def value(self):
X = self.args
from .python_builtins import all
return argval(all(LexLess(prev_row, curr_row) for prev_row, curr_row in zip(X, X[1:])))


class LexChainLessEq(GlobalConstraint):
""" Given a matrix X,, enforces that all rows are lexicographically ordered.
"""
def __init__(self, X):
# Ensure the numpy array is 2D
X = cpm_array(X)
assert X.ndim == 2, "Input must be a 2D array or a list of lists"
self.name = "lex_chain_lesseq"
self.args = X

def decompose(self):
X = self.args
return [LexLessEq(prev_row, curr_row) for prev_row, curr_row in zip(X, X[1:])], []

def value(self):
X = self.args
from .python_builtins import all
return argval(all(LexLessEq(prev_row, curr_row) for prev_row, curr_row in zip(X, X[1:])))


class DirectConstraint(Expression):
Expand Down
15 changes: 11 additions & 4 deletions cpmpy/solvers/choco.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,11 @@ def transform(self, cpm_expr):
cpm_cons = toplevel_list(cpm_expr)
supported = {"min", "max", "abs", "count", "element", "alldifferent", "alldifferent_except0", "allequal",
"table", "InDomain", "cumulative", "circuit", "gcc", "inverse", "nvalue", "increasing",
"decreasing","strictly_increasing","strictly_decreasing"}

"decreasing","strictly_increasing","strictly_decreasing","lex_lesseq", "lex_less"}
# choco supports reification of any constraint, but has a bug in increasing and decreasing
supported_reified = {"min", "max", "abs", "count", "element", "alldifferent", "alldifferent_except0",
"allequal", "table", "InDomain", "cumulative", "circuit", "gcc", "inverse", "nvalue"}
"allequal", "table", "InDomain", "cumulative", "circuit", "gcc", "inverse", "nvalue","lex_lesseq", "lex_less"}
# for when choco new release comes, fixing the bug on increasing and decreasing
#supported_reified = supported
cpm_cons = decompose_in_tree(cpm_cons, supported, supported_reified)
Expand Down Expand Up @@ -497,7 +497,7 @@ def _get_constraint(self, cpm_expr):
elif isinstance(cpm_expr, GlobalConstraint):

# many globals require all variables as arguments
if cpm_expr.name in {"alldifferent", "alldifferent_except0", "allequal", "circuit", "inverse","increasing","decreasing","strictly_increasing","strictly_decreasing"}:
if cpm_expr.name in {"alldifferent", "alldifferent_except0", "allequal", "circuit", "inverse","increasing","decreasing","strictly_increasing","strictly_decreasing","lex_lesseq","lex_less"}:
chc_args = self._to_vars(cpm_expr.args)
if cpm_expr.name == 'alldifferent':
return self.chc_model.all_different(chc_args)
Expand All @@ -517,6 +517,13 @@ def _get_constraint(self, cpm_expr):
return self.chc_model.increasing(chc_args,1)
elif cpm_expr.name == "strictly_decreasing":
return self.chc_model.decreasing(chc_args,1)
elif cpm_expr.name in ["lex_lesseq", "lex_less"]:
if cpm_expr.name == "lex_lesseq":
return self.chc_model.lex_less_eq(*chc_args)
return self.chc_model.lex_less(*chc_args)
# Ready for when it is fixed in pychoco
Dimosts marked this conversation as resolved.
Show resolved Hide resolved
# elif cpm_expr.name == "lex_chain_less":
# return self.chc_model.lex_chain_less(chc_args)

# but not all
elif cpm_expr.name == 'table':
Expand Down
19 changes: 17 additions & 2 deletions cpmpy/solvers/minizinc.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from .solver_interface import SolverInterface, SolverStatus, ExitStatus
from ..exceptions import MinizincNameException, MinizincBoundsException
from ..expressions.core import Expression, Comparison, Operator, BoolVal
from ..expressions.variables import _NumVarImpl, _IntVarImpl, _BoolVarImpl, NegBoolView, intvar
from ..expressions.variables import _NumVarImpl, _IntVarImpl, _BoolVarImpl, NegBoolView, intvar, cpm_array
from ..expressions.globalconstraints import DirectConstraint
from ..expressions.utils import is_num, is_any_list, eval_comparison, argvals, argval
from ..transformations.decompose_global import decompose_in_tree
Expand Down Expand Up @@ -419,7 +419,8 @@ def transform(self, cpm_expr):
cpm_cons = toplevel_list(cpm_expr)
supported = {"min", "max", "abs", "element", "count", "nvalue", "alldifferent", "alldifferent_except0", "allequal",
"inverse", "ite" "xor", "table", "cumulative", "circuit", "gcc", "increasing",
"decreasing","strictly_increasing","strictly_decreasing"}
"decreasing", "strictly_increasing", "strictly_decreasing", "lex_lesseq", "lex_less", "lex_chain_less",
"lex_chain_lesseq"}
return decompose_in_tree(cpm_cons, supported, supported_reified=supported - {"circuit"})


Expand Down Expand Up @@ -503,6 +504,20 @@ def zero_based(array):
args_str = [self._convert_expression(e) for e in expr.args]
return "alldifferent_except_0([{}])".format(",".join(args_str))

if expr.name in ["lex_lesseq", "lex_less"]:
X = [self._convert_expression(e) for e in expr.args[0]]
Y = [self._convert_expression(e) for e in expr.args[1]]
return f"{expr.name}({{}}, {{}})".format(X, Y)


if expr.name in ["lex_chain_less", "lex_chain_lesseq"]:
X = cpm_array([[self._convert_expression(e) for e in row] for row in expr.args])
str_X = "[|\n" # opening
for row in X.T: # Minizinc enforces lexicographic order on columns
str_X += ",".join(map(str, row)) + " |" # rows
str_X += "\n|]" # closing
return f"{expr.name}({{}})".format(str_X)

args_str = [self._convert_expression(e) for e in expr.args]
# standard expressions: comparison, operator, element
if isinstance(expr, Comparison):
Expand Down
2 changes: 1 addition & 1 deletion cpmpy/transformations/decompose_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def decompose_in_tree(lst_of_expr, supported=set(), supported_reified=set(), _to

_toplevel.extend(define) # definitions should be added toplevel
# the `decomposed` expression might contain other global constraints, check it
decomposed = decompose_in_tree(decomposed, supported, supported_reified, [], nested=nested)
decomposed = decompose_in_tree(decomposed, supported, supported_reified, _toplevel, nested=nested)
Dimosts marked this conversation as resolved.
Show resolved Hide resolved
newlist.append(all(decomposed))

else:
Expand Down
25 changes: 21 additions & 4 deletions tests/test_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@
ALL_SOLS = False # test wheter all solutions returned by the solver satisfy the constraint

# Exclude some global constraints for solvers

NUM_GLOBAL = {
"AllEqual", "AllDifferent", "AllDifferentExcept0", "Cumulative", "GlobalCardinalityCount", "InDomain", "Inverse", "Table", "Circuit",
"Increasing", "IncreasingStrict", "Decreasing", "DecreasingStrict",
"Increasing", "IncreasingStrict", "Decreasing", "DecreasingStrict", "LexLess", "LexLessEq", "LexChainLess", "LexChainLessEq",
# also global functions
"Abs", "Element", "Minimum", "Maximum", "Count", "NValue", "NValueExcept"
}
Expand Down Expand Up @@ -169,7 +168,7 @@ def global_constraints(solver):
"""
Generate all global constraints
- AllDifferent, AllEqual, Circuit, Minimum, Maximum, Element,
Xor, Cumulative, NValue, Count
Xor, Cumulative, NValue, Count, Increasing, Decreasing, IncreasingStrict, DecreasingStrict, LexLessEq, LexLess
"""
classes = inspect.getmembers(cpmpy.expressions.globalconstraints, inspect.isclass)
classes = [(name, cls) for name, cls in classes if issubclass(cls, GlobalConstraint) and name != "GlobalConstraint"]
Expand Down Expand Up @@ -198,14 +197,32 @@ def global_constraints(solver):
vals = [1, 2, 3]
cnts = intvar(0,10,shape=3)
expr = cls(NUM_ARGS, vals, cnts)
elif name == "LexLessEq":
Dimosts marked this conversation as resolved.
Show resolved Hide resolved
X = intvar(0, 10, shape=10)
Y = intvar(0, 10, shape=10)
expr = LexLessEq(X, Y)

elif name == "LexLess":
X = intvar(0, 10, shape=10)
Y = intvar(0, 10, shape=10)
expr = LexLess(X, Y)

elif name == "LexChainLess":
X = intvar(0, 10, shape=(10,10))
expr = LexChainLess(X)

elif name == "LexChainLessEq":
X = intvar(0, 10, shape=(10,10))
expr = LexChainLess(X)
else: # default constructor, list of numvars
expr= cls(NUM_ARGS)
expr= cls(NUM_ARGS)

if solver in EXCLUDE_GLOBAL and name in EXCLUDE_GLOBAL[solver]:
continue
else:
yield expr


def reify_imply_exprs(solver):
"""
- Reification (double implication): Boolexpr == Var (CPMpy class 'Comparison')
Expand Down
72 changes: 72 additions & 0 deletions tests/test_globalconstraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,78 @@ def test_InDomain(self):
self.assertTrue(model.solve())
self.assertIn(bv.value(), vals)

def test_lex_lesseq(self):
Dimosts marked this conversation as resolved.
Show resolved Hide resolved
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("ortools"))
from cpmpy.expressions.utils import argval
self.assertTrue(sum(argval(X)) == 0)

def test_lex_less(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.LexLess(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.LexLess(X, Y)
m = cp.Model(c)
self.assertFalse(m.solve("ortools"))

Z = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
c = cp.LexLess(X, Z)
m = cp.Model(c)
self.assertTrue(m.solve("ortools"))
from cpmpy.expressions.utils import argval
self.assertTrue(sum(argval(X)) == 0)


def test_lex_chain(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.LexChainLess([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.LexChainLessEq([X, Y])
m = cp.Model(c)
self.assertTrue(m.solve("ortools"))
from cpmpy.expressions.utils import argval
self.assertTrue(sum(argval(X)) == 0)

Z = cp.intvar(0, 1, shape=(3,2))
c = cp.LexChainLess(Z)
m = cp.Model(c)
self.assertTrue(m.solve())
self.assertTrue(sum(argval(Z[0])) == 0)
self.assertTrue(sum(argval(Z[1])) == 1)
self.assertTrue(sum(argval(Z[2])) >= 1)


def test_indomain_onearg(self):

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