From 36191761ecd288abfcc56d6aeafe6a77c352b226 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Fri, 10 May 2024 13:45:35 +0200 Subject: [PATCH 1/3] Increasing and decreasing global constraints (#471) --- cpmpy/expressions/__init__.py | 5 +- cpmpy/expressions/globalconstraints.py | 99 ++++++++++++++++++++++++++ cpmpy/solvers/choco.py | 19 ++++- tests/test_constraints.py | 6 +- 4 files changed, 121 insertions(+), 8 deletions(-) diff --git a/cpmpy/expressions/__init__.py b/cpmpy/expressions/__init__.py index e6859d70a..bd71085cf 100644 --- a/cpmpy/expressions/__init__.py +++ b/cpmpy/expressions/__init__.py @@ -5,13 +5,13 @@ List of submodules ================== .. autosummary:: + python_builtins :nosignatures: variables core globalconstraints globalfunctions - python_builtins utils @@ -21,7 +21,8 @@ # 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 +from .globalconstraints import AllDifferent, AllDifferentExcept0, 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 cb792bd89..61e7965a6 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 @@ -517,6 +524,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__("increasing_strict", 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__("decreasing_strict", 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..7976cfe66 100644 --- a/cpmpy/solvers/choco.py +++ b/cpmpy/solvers/choco.py @@ -313,8 +313,13 @@ 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","increasing_strict","decreasing_strict"} + # 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 +495,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","increasing_strict","decreasing_strict"}: chc_args = self._to_vars(cpm_expr.args) if cpm_expr.name == 'alldifferent': return self.chc_model.all_different(chc_args) @@ -502,6 +507,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 == "increasing_strict": + return self.chc_model.increasing(chc_args,1) + elif cpm_expr.name == "decreasing_strict": + return self.chc_model.decreasing(chc_args,1) # but not all elif cpm_expr.name == 'table': diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 40b7e1590..53a7a2669 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","increasing_strict","decreasing_strict"}, + "pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative","xor","increasing","decreasing","increasing_strict","decreasing_strict"}, "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]: From dfa81ff529edcddaded9546037bfaca4f727dbfb Mon Sep 17 00:00:00 2001 From: Wout Date: Fri, 10 May 2024 13:46:02 +0200 Subject: [PATCH 2/3] remove type restriction for InDomain (#470) --- cpmpy/expressions/globalconstraints.py | 2 -- tests/test_globalconstraints.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 61e7965a6..341fcd3f8 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -351,8 +351,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): diff --git a/tests/test_globalconstraints.py b/tests/test_globalconstraints.py index c5665d252..e9ae2df66 100644 --- a/tests/test_globalconstraints.py +++ b/tests/test_globalconstraints.py @@ -234,6 +234,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): From 3a9469135a66c184a6762f2e25dfbf4709ece185 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Fri, 10 May 2024 14:57:45 +0200 Subject: [PATCH 3/3] Increasing decreasing, adding tests + minizinc (#472) * increasing and decreasing in minizinc --- cpmpy/expressions/globalconstraints.py | 4 +-- cpmpy/solvers/choco.py | 9 +++--- cpmpy/solvers/minizinc.py | 3 +- tests/test_constraints.py | 4 +-- tests/test_globalconstraints.py | 44 ++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 9 deletions(-) diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 341fcd3f8..9c6fe3933 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -574,7 +574,7 @@ class IncreasingStrict(GlobalConstraint): """ def __init__(self, *args): - super().__init__("increasing_strict", flatlist(args)) + super().__init__("strictly_increasing", flatlist(args)) def decompose(self): """ @@ -597,7 +597,7 @@ class DecreasingStrict(GlobalConstraint): """ def __init__(self, *args): - super().__init__("decreasing_strict", flatlist(args)) + super().__init__("strictly_decreasing", flatlist(args)) def decompose(self): """ diff --git a/cpmpy/solvers/choco.py b/cpmpy/solvers/choco.py index 7976cfe66..d2d502794 100644 --- a/cpmpy/solvers/choco.py +++ b/cpmpy/solvers/choco.py @@ -314,7 +314,8 @@ 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","increasing_strict","decreasing_strict"} + "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"} @@ -495,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","increasing","decreasing","increasing_strict","decreasing_strict"}: + 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) @@ -511,9 +512,9 @@ def _get_constraint(self, cpm_expr): 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 == "increasing_strict": + elif cpm_expr.name == "strictly_increasing": return self.chc_model.increasing(chc_args,1) - elif cpm_expr.name == "decreasing_strict": + elif cpm_expr.name == "strictly_decreasing": return self.chc_model.decreasing(chc_args,1) # but not all 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 53a7a2669..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","increasing","decreasing","increasing_strict","decreasing_strict"}, - "pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative","xor","increasing","decreasing","increasing_strict","decreasing_strict"}, + "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": {} } diff --git a/tests/test_globalconstraints.py b/tests/test_globalconstraints.py index e9ae2df66..1f74c80c1 100644 --- a/tests/test_globalconstraints.py +++ b/tests/test_globalconstraints.py @@ -770,6 +770,50 @@ def test_allEqual(self): self.assertTrue(cp.Model([cp.AllEqual(a,b,False, a | b)]).solve()) #self.assertTrue(cp.Model([cp.AllEqual(x,y,b)]).solve()) + 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)