diff --git a/cpmpy/expressions/__init__.py b/cpmpy/expressions/__init__.py index 43e448126..db4b42975 100644 --- a/cpmpy/expressions/__init__.py +++ b/cpmpy/expressions/__init__.py @@ -21,7 +21,7 @@ # 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, AllDifferentExceptN, AllEqualExceptN, AllEqual, Circuit, Inverse, Table, Xor, Cumulative, IfThenElse, GlobalCardinalityCount, DirectConstraint, InDomain +from .globalconstraints import AllDifferent, AllDifferentExcept0, AllDifferentExceptN, AllEqualExceptN, AllEqual, Circuit, Inverse, Table, Xor, Cumulative, IfThenElse, GlobalCardinalityCount, DirectConstraint, InDomain, Increasing, Decreasing, IncreasingStrict, DecreasingStrict from .globalconstraints import alldifferent, allequal, circuit # Old, to be deprecated from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue from .core import BoolVal diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index c7f8ece89..b2fa35e2b 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -104,7 +104,14 @@ def my_circuit_decomp(self): Table Xor Cumulative + IfThenElse GlobalCardinalityCount + DirectConstraint + InDomain + Increasing + Decreasing + IncreasingStrict + DecreasingStrict """ import copy @@ -380,8 +387,6 @@ class InDomain(GlobalConstraint): """ def __init__(self, expr, arr): - assert not (is_boolexpr(expr) or any(is_boolexpr(a) for a in arr)), \ - "The expressions in the InDomain constraint should not be boolean" super().__init__("InDomain", [expr, arr]) def decompose(self): @@ -553,6 +558,98 @@ def value(self): return all(decomposed).value() +class Increasing(GlobalConstraint): + """ + The "Increasing" constraint, the expressions will have increasing (not strictly) values + """ + + def __init__(self, *args): + super().__init__("increasing", flatlist(args)) + + def decompose(self): + """ + Returns two lists of constraints: + 1) the decomposition of the Increasing constraint + 2) empty list of defining constraints + """ + args = self.args + 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)) + + +class Decreasing(GlobalConstraint): + """ + The "Decreasing" constraint, the expressions will have decreasing (not strictly) values + """ + + def __init__(self, *args): + super().__init__("decreasing", flatlist(args)) + + def decompose(self): + """ + Returns two lists of constraints: + 1) the decomposition of the Decreasing constraint + 2) empty list of defining constraints + """ + args = self.args + 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)) + + +class IncreasingStrict(GlobalConstraint): + """ + The "IncreasingStrict" constraint, the expressions will have increasing (strictly) values + """ + + def __init__(self, *args): + super().__init__("strictly_increasing", flatlist(args)) + + def decompose(self): + """ + Returns two lists of constraints: + 1) the decomposition of the IncreasingStrict constraint + 2) empty list of defining constraints + """ + args = self.args + 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)) + + +class DecreasingStrict(GlobalConstraint): + """ + The "DecreasingStrict" constraint, the expressions will have decreasing (strictly) values + """ + + def __init__(self, *args): + super().__init__("strictly_decreasing", flatlist(args)) + + def decompose(self): + """ + Returns two lists of constraints: + 1) the decomposition of the DecreasingStrict constraint + 2) empty list of defining constraints + """ + args = self.args + 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)) + + class DirectConstraint(Expression): """ A DirectConstraint will directly call a function of the underlying solver when added to a CPMpy solver diff --git a/cpmpy/solvers/choco.py b/cpmpy/solvers/choco.py index 5bfc09084..d2d502794 100644 --- a/cpmpy/solvers/choco.py +++ b/cpmpy/solvers/choco.py @@ -313,8 +313,14 @@ 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"} - supported_reified = supported # choco supports reification of any constraint + "table", "InDomain", "cumulative", "circuit", "gcc", "inverse", "nvalue", "increasing", + "decreasing","strictly_increasing","strictly_decreasing"} + + # 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"} + # 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) cpm_cons = flatten_constraint(cpm_cons) # flat normal form cpm_cons = canonical_comparison(cpm_cons) @@ -490,7 +496,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"}: + if cpm_expr.name in {"alldifferent", "alldifferent_except0", "allequal", "circuit", "inverse","increasing","decreasing","strictly_increasing","strictly_decreasing"}: chc_args = self._to_vars(cpm_expr.args) if cpm_expr.name == 'alldifferent': return self.chc_model.all_different(chc_args) @@ -502,6 +508,14 @@ def _get_constraint(self, cpm_expr): return self.chc_model.circuit(chc_args) elif cpm_expr.name == "inverse": return self.chc_model.inverse_channeling(*chc_args) + elif cpm_expr.name == "increasing": + return self.chc_model.increasing(chc_args,0) + elif cpm_expr.name == "decreasing": + return self.chc_model.decreasing(chc_args,0) + elif cpm_expr.name == "strictly_increasing": + return self.chc_model.increasing(chc_args,1) + elif cpm_expr.name == "strictly_decreasing": + return self.chc_model.decreasing(chc_args,1) # but not all elif cpm_expr.name == 'table': diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index 55c2d59f2..0bb6bd9fd 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -418,7 +418,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"} + "inverse", "ite" "xor", "table", "cumulative", "circuit", "gcc", "increasing", + "decreasing","strictly_increasing","strictly_decreasing"} return decompose_in_tree(cpm_cons, supported, supported_reified=supported - {"circuit"}) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 40b7e1590..062d727ca 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -14,8 +14,8 @@ EXCLUDE_GLOBAL = {"ortools": {}, "gurobi": {}, "minizinc": {"circuit"}, - "pysat": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative"}, - "pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative",'xor'}, + "pysat": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative","increasing","decreasing","strictly_increasing","strictly_decreasing"}, + "pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative","xor","increasing","decreasing","strictly_increasing","strictly_decreasing"}, "exact": {}, "choco": {} } @@ -152,7 +152,7 @@ def global_constraints(solver): - AllDifferent, AllEqual, Circuit, Minimum, Maximum, Element, Xor, Cumulative, NValue, Count """ - global_cons = [AllDifferent, AllEqual, Minimum, Maximum, NValue] + global_cons = [AllDifferent, AllEqual, Minimum, Maximum, NValue, Increasing, Decreasing, IncreasingStrict, DecreasingStrict] for global_type in global_cons: cons = global_type(NUM_ARGS) if solver not in EXCLUDE_GLOBAL or cons.name not in EXCLUDE_GLOBAL[solver]: diff --git a/tests/test_globalconstraints.py b/tests/test_globalconstraints.py index 10f4237c8..c5fb40efb 100644 --- a/tests/test_globalconstraints.py +++ b/tests/test_globalconstraints.py @@ -265,6 +265,24 @@ def test_InDomain(self): model = cp.Model(cons) self.assertTrue(model.solve()) self.assertIn(iv.value(), vals) + vals = [1, 5, 8, -4] + bv = cp.boolvar() + cons = [cp.InDomain(bv, vals)] + model = cp.Model(cons) + self.assertTrue(model.solve()) + self.assertIn(bv.value(), vals) + vals = [iv2, 5, 8, -4] + bv = cp.boolvar() + cons = [cp.InDomain(bv, vals)] + model = cp.Model(cons) + self.assertTrue(model.solve()) + self.assertIn(bv.value(), vals) + vals = [bv & bv, 5, 8, -4] + bv = cp.boolvar() + cons = [cp.InDomain(bv, vals)] + model = cp.Model(cons) + self.assertTrue(model.solve()) + self.assertIn(bv.value(), vals) def test_indomain_onearg(self): @@ -830,6 +848,50 @@ def test_not_allEqualExceptn(self): self.assertEqual(total, len(circuit_sols) + len(not_circuit_sols)) + def test_increasing(self): + x = cp.intvar(-8, 8) + y = cp.intvar(-7, -1) + b = cp.boolvar() + a = cp.boolvar() + self.assertTrue(cp.Model([cp.Increasing(x,y)]).solve()) + self.assertTrue(cp.Model([cp.Increasing(a,b)]).solve()) + self.assertTrue(cp.Model([cp.Increasing(x,y,b)]).solve()) + z = cp.intvar(2,5) + self.assertFalse(cp.Model([cp.Increasing(z,b)]).solve()) + + def test_decreasing(self): + x = cp.intvar(-8, 8) + y = cp.intvar(-7, -1) + b = cp.boolvar() + a = cp.boolvar() + self.assertTrue(cp.Model([cp.Decreasing(x,y)]).solve()) + self.assertTrue(cp.Model([cp.Decreasing(a,b)]).solve()) + self.assertFalse(cp.Model([cp.Decreasing(x,y,b)]).solve()) + z = cp.intvar(2,5) + self.assertTrue(cp.Model([cp.Decreasing(z,b)]).solve()) + + def test_increasing_strict(self): + x = cp.intvar(-8, 8) + y = cp.intvar(-7, -1) + b = cp.boolvar() + a = cp.boolvar() + self.assertTrue(cp.Model([cp.IncreasingStrict(x,y)]).solve()) + self.assertTrue(cp.Model([cp.IncreasingStrict(a,b)]).solve()) + self.assertTrue(cp.Model([cp.IncreasingStrict(x,y,b)]).solve()) + z = cp.intvar(1,5) + self.assertFalse(cp.Model([cp.IncreasingStrict(z,b)]).solve()) + + def test_decreasing_strict(self): + x = cp.intvar(-8, 8) + y = cp.intvar(-7, 0) + b = cp.boolvar() + a = cp.boolvar() + self.assertTrue(cp.Model([cp.DecreasingStrict(x,y)]).solve()) + self.assertTrue(cp.Model([cp.DecreasingStrict(a,b)]).solve()) + self.assertFalse(cp.Model([cp.DecreasingStrict(x,y,b)]).solve()) + z = cp.intvar(1,5) + self.assertTrue(cp.Model([cp.DecreasingStrict(z,b)]).solve()) + def test_circuit(self): x = cp.intvar(-8, 8) y = cp.intvar(-7, -1)