Skip to content

Commit

Permalink
Merge branch 'master' into scheduling_constriants
Browse files Browse the repository at this point in the history
  • Loading branch information
Dimosts committed May 27, 2024
2 parents ec9e865 + 37f0dbe commit 40ad2a9
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 31 deletions.
7 changes: 3 additions & 4 deletions cpmpy/expressions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
# others need to be imported by the developer explicitely
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, Precedence, NoOverlap

from .globalconstraints import AllDifferent, AllDifferentExcept0, AllDifferentLists, AllEqual, Circuit, Inverse, Table, Xor, Cumulative, \
IfThenElse, GlobalCardinalityCount, DirectConstraint, InDomain, Increasing, Decreasing, IncreasingStrict, DecreasingStrict,
LexLess, LexLessEq, LexChainLess, LexChainLessEq, Precedence, NoOverlap
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
174 changes: 162 additions & 12 deletions cpmpy/expressions/globalconstraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def my_circuit_decomp(self):
AllDifferent
AllDifferentExcept0
AllDifferentLists
AllEqual
Circuit
Inverse
Expand Down Expand Up @@ -196,6 +197,33 @@ def value(self):
vals = [argval(a) for a in self.args if argval(a) != 0]
return len(set(vals)) == len(vals)


class AllDifferentLists(GlobalConstraint):
"""
Ensures none of the lists given are exactly the same.
Called 'lex_alldifferent' in the global constraint catalog:
https://sofdem.github.io/gccat/gccat/Clex_alldifferent.html#uid24923
"""
def __init__(self, lists):
if any(not is_any_list(lst) for lst in lists):
raise TypeError(f"AllDifferentLists expects a list of lists, but got {lists}")
if any(len(lst) != len(lists[0]) for lst in lists):
raise ValueError("Lists should have equal length, but got these lengths:", list(map(len, lists)))
super().__init__("alldifferent_lists", [flatlist(lst) for lst in lists])

def decompose(self):
"""Returns the decomposition
"""
from .python_builtins import any as cpm_any
constraints = []
for lst1, lst2 in all_pairs(self.args):
constraints += [cpm_any(var1 != var2 for var1, var2 in zip(lst1, lst2))]
return constraints, []

def value(self):
lst_vals = [tuple(argvals(a)) for a in self.args]
return len(set(lst_vals)) == len(self.args)

def allequal(args):
warnings.warn("Deprecated, use AllEqual(v1,v2,...,vn) instead, will be removed in stable version", DeprecationWarning)
return AllEqual(*args) # unfold list as individual arguments
Expand Down Expand Up @@ -605,9 +633,8 @@ def decompose(self):
return [args[i] <= args[i+1] for i in range(len(args)-1)], []

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))
args = argvals(self.args)
return all(args[i] <= args[i+1] for i in range(len(args)-1))


class Decreasing(GlobalConstraint):
Expand All @@ -628,9 +655,8 @@ def decompose(self):
return [args[i] >= args[i+1] for i in range(len(args)-1)], []

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))
args = argvals(self.args)
return all(args[i] >= args[i+1] for i in range(len(args)-1))


class IncreasingStrict(GlobalConstraint):
Expand All @@ -651,9 +677,8 @@ def decompose(self):
return [args[i] < args[i+1] for i in range(len(args)-1)], []

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))
args = argvals(self.args)
return all(args[i] < args[i+1] for i in range(len(args)-1))


class DecreasingStrict(GlobalConstraint):
Expand All @@ -674,9 +699,134 @@ def decompose(self):
return [(args[i] > args[i+1]) for i in range(len(args)-1)], []

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))
args = argvals(self.args)
return 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.
"""
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):
"""
Implementation inspired by Hakan Kjellerstrand (http://hakank.org/cpmpy/cpmpy_hakank.py)
The decomposition creates auxiliary Boolean variables and constraints that
collectively ensure X is lexicographically less than Y
The auxiliary boolean vars are defined to represent if the given lists are lexicographically ordered
(less or equal) up to the given index.
Decomposition enforces through the constraining part that the first boolean variable needs to be true, and thus
through the defining part it is enforced that if it is not strictly lexicographically less in a given index,
then next index must be lexicographically less or equal. It needs to be strictly less in at least one index.
The use of auxiliary Boolean variables bvar ensures that the constraints propagate immediately,
maintaining arc-consistency. Each bvar[i] enforces the lexicographic ordering at each position, ensuring that
every value in the domain of X[i] can be extended to a consistent value in the domain of $Y_i$ for all
subsequent positions.
"""
X, Y = cpm_array(self.args)

bvar = boolvar(shape=(len(X) + 1))

# Constraint ensuring that each element in X is less than or equal to the corresponding element in Y,
# until a strict inequality is encountered.
defining = [bvar == ((X <= Y) & ((X < Y) | bvar[1:]))]
# enforce the last element to be true iff (X[-1] < Y[-1]), enforcing strict lexicographic order
defining.append(bvar[-1] == (X[-1] < Y[-1]))
constraining = [bvar[0]]

return constraining, defining

def value(self):
X, Y = argvals(self.args)
return 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).
"""
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):
"""
Implementation inspired by Hakan Kjellerstrand (http://hakank.org/cpmpy/cpmpy_hakank.py)
The decomposition creates auxiliary Boolean variables and constraints that
collectively ensure X is lexicographically less than Y
The auxiliary boolean vars are defined to represent if the given lists are lexicographically ordered
(less or equal) up to the given index.
Decomposition enforces through the constraining part that the first boolean variable needs to be true, and thus
through the defining part it is enforced that if it is not strictly lexicographically less in a given index,
then next index must be lexicographically less or equal.
The use of auxiliary Boolean variables bvar ensures that the constraints propagate immediately,
maintaining arc-consistency. Each bvar[i] enforces the lexicographic ordering at each position, ensuring that
every value in the domain of X[i] can be extended to a consistent value in the domain of $Y_i$ for all
subsequent positions.
"""
X, Y = cpm_array(self.args)

bvar = boolvar(shape=(len(X) + 1))
defining = [bvar == ((X <= Y) & ((X < Y) | bvar[1:]))]
defining.append(bvar[-1] == (X[-1] <= Y[-1]))
constraining = [bvar[0]]

return constraining, defining

def value(self):
X, Y = argvals(self.args)
return 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, LexChainLess 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"
super().__init__("lex_chain_less", X.tolist())

def decompose(self):
""" Decompose to a series of LexLess constraints between subsequent rows
"""
X = self.args
return [LexLess(prev_row, curr_row) for prev_row, curr_row in zip(X, X[1:])], []

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


class LexChainLessEq(GlobalConstraint):
""" Given a matrix X, LexChainLessEq 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"
super().__init__("lex_chain_lesseq", X.tolist())

def decompose(self):
""" Decompose to a series of LexLessEq constraints between subsequent rows
"""
X = self.args
return [LexLessEq(prev_row, curr_row) for prev_row, curr_row in zip(X, X[1:])], []

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


class DirectConstraint(Expression):
Expand Down
16 changes: 11 additions & 5 deletions cpmpy/solvers/choco.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +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",
"precedence",
"decreasing","strictly_increasing","strictly_decreasing"}

"precedence","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 @@ -498,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 @@ -518,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 (https://github.com/chocoteam/pychoco/issues/30)
# 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
21 changes: 18 additions & 3 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 @@ -418,9 +418,10 @@ 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",
"inverse", "ite" "xor", "table", "cumulative", "circuit", "gcc", "increasing","decreasing",
"precedence","no_overlap",
"decreasing","strictly_increasing","strictly_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", "precedence"})


Expand Down Expand Up @@ -504,6 +505,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
4 changes: 2 additions & 2 deletions 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)
newlist.append(all(decomposed))

else:
Expand Down Expand Up @@ -146,7 +146,7 @@ def decompose_in_tree(lst_of_expr, supported=set(), supported_reified=set(), _to
return toplevel_list(newlist)
else:
# we are toplevel and some new constraints are introduced, decompose new constraints!
return toplevel_list(newlist) + decompose_in_tree(_toplevel, supported, supported_reified, nested=False)
return toplevel_list(newlist) + decompose_in_tree(toplevel_list(_toplevel), supported, supported_reified, nested=False)


# DEPRECATED!
Expand Down
2 changes: 2 additions & 0 deletions cpmpy/transformations/normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ def simplify_boolean(lst_of_expr, num_context=False):
"""
if is_boolexpr(lhs) and is_num(rhs):
# direct simplification of boolean comparisons
if isinstance(rhs, BoolVal):
rhs = int(rhs.value()) # ensure proper comparisons below
if rhs < 0:
newlist.append(BoolVal(name in {"!=", ">", ">="})) # all other operators evaluate to False
elif rhs == 0:
Expand Down
Loading

0 comments on commit 40ad2a9

Please sign in to comment.