From 6b5dbb93f616e123c68098c1d574d24c182908bb Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 18:06:06 +0200 Subject: [PATCH 01/19] add precedence global constraint --- cpmpy/expressions/globalconstraints.py | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index d193567e9..91f3d46ad 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -503,6 +503,46 @@ def value(self): return True +class Precedence(GlobalConstraint): + """ + Constraint enforcing some values have precedence over others. + Given an array of variables X and and a list of precedences P: + Then in order to satisfy the constraint, if X[i] = P[j+1], then there exists a X[i'] = P[j] with i' < i + """ + def __init__(self, vars, precedence): + if not is_any_list(vars): + raise TypeError("Precedence expects a list of variables, but got", vars) + if not is_any_list(precedence) or any(isinstance(x, Expression) for x in precedence): + raise TypeError("Precedence expects a list of values as precedence, but got", precedence) + super().__init__("precedence", [vars, precedence]) + + def decompose(self): + """ + Decomposition based on: + Law, Yat Chiu, and Jimmy HM Lee. "Global constraints for integer and set value precedence." + Principles and Practice of Constraint Programming–CP 2004: 10th International Conference, CP 2004 + """ + from .python_builtins import any as cpm_any + + args, precedence = self.args + constraints = [] + for s,t in zip(precedence[:-1], precedence[1:]): + for j in range(len(args)): + constraints += [(args[j] == t).implies(cpm_any(args[:j] == s))] + return constraints, [] + + def value(self): + + args, precedence = self.args + vals = np.array(argvals(args)) + for s,t in zip(precedence[:-1], precedence[1:]): + if vals[0] == t: return False + for j in range(len(args)): + if vals[0] == t and sum(args[:j] == s) == 0: + return False + return True + + class GlobalCardinalityCount(GlobalConstraint): """ GlobalCardinalityCount(vars,vals,occ): The number of occurrences of each value vals[i] in the list of variables vars From 26845051ab059c136a4ea2142457306851bbaa84 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 18:06:13 +0200 Subject: [PATCH 02/19] add tests --- tests/test_constraints.py | 17 +++++++++++++++-- tests/test_globalconstraints.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index b777ce4e7..baff35b13 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -12,12 +12,13 @@ # also add exclusions to the 3 EXCLUDE_* below as needed SOLVERNAMES = [name for name, solver in SolverLookup.base_solvers() if solver.supported()] ALL_SOLS = False # test wheter all solutions returned by the solver satisfy the constraint - +SOLVERNAMES = ["ortools"] # Exclude some global constraints for solvers NUM_GLOBAL = { - "AllEqual", "AllDifferent", "AllDifferentExcept0", "Cumulative", "GlobalCardinalityCount", "InDomain", "Inverse", "Table", "Circuit", + "AllEqual", "AllDifferent", "AllDifferentExcept0" , "GlobalCardinalityCount", "InDomain", "Inverse", "Table", "Circuit", "Increasing", "IncreasingStrict", "Decreasing", "DecreasingStrict", + "Precedence", "Cumulative", # also global functions "Abs", "Element", "Minimum", "Maximum", "Count", "NValue", "NValueExcept" } @@ -194,6 +195,9 @@ def global_constraints(solver): demand = [4, 5, 7] cap = 10 expr = Cumulative(s, dur, e, demand, cap) + elif name == "Precedence": + x = intvar(0,5, shape=3, name="x") + expr = cls(x, [3,1,0]) elif name == "GlobalCardinalityCount": vals = [1, 2, 3] cnts = intvar(0,10,shape=3) @@ -238,7 +242,16 @@ def test_bool_constaints(solver, constraint): n_sols = SolverLookup.get(solver, Model(constraint)).solveAll(display=lambda: verify(constraint)) assert n_sols >= 1 else: + print(constraint) + for c in constraint.decompose()[0]: + print("-", c) + assert SolverLookup.get(solver, Model(constraint)).solve() + + from cpmpy.transformations.get_variables import get_variables + for v in get_variables(constraint): + print(v, v.value()) + assert argval(constraint) assert constraint.value() diff --git a/tests/test_globalconstraints.py b/tests/test_globalconstraints.py index 311828afe..f6aa7b426 100644 --- a/tests/test_globalconstraints.py +++ b/tests/test_globalconstraints.py @@ -708,6 +708,17 @@ def check_true(): cp.Model(cons).solveAll(solver='minizinc') + + def test_precedence(self): + iv = cp.intvar(0,5, shape=6, name="x") + + cons = cp.Precedence(iv, [0,2,1]) + for c in cons.decompose()[0]: + print(c) + + self.assertTrue(cp.Model([cons, iv == [0,2,2,1,0,1]]).solve()) + self.assertTrue(cp.Model([cons, iv == [0,0,0,0,0,0]]).solve()) + class TestBounds(unittest.TestCase): def test_bounds_minimum(self): x = cp.intvar(-8, 8) From 8abcb8bab79dd74806810b60ee81f9c7da48cfc7 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 18:14:50 +0200 Subject: [PATCH 03/19] add to solvers --- cpmpy/expressions/__init__.py | 4 +++- cpmpy/solvers/choco.py | 3 +++ cpmpy/solvers/minizinc.py | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cpmpy/expressions/__init__.py b/cpmpy/expressions/__init__.py index 11a295527..ebe229b2c 100644 --- a/cpmpy/expressions/__init__.py +++ b/cpmpy/expressions/__init__.py @@ -22,7 +22,9 @@ 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, Precedence + from .globalconstraints import alldifferent, allequal, circuit # Old, to be deprecated from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue, NValueExcept from .core import BoolVal diff --git a/cpmpy/solvers/choco.py b/cpmpy/solvers/choco.py index 3869df8be..d1076ddd1 100644 --- a/cpmpy/solvers/choco.py +++ b/cpmpy/solvers/choco.py @@ -314,6 +314,7 @@ 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"} # choco supports reification of any constraint, but has a bug in increasing and decreasing @@ -536,6 +537,8 @@ def _get_constraint(self, cpm_expr): # Create task variables. Choco can create them only one by one tasks = [self.chc_model.task(s, d, e) for s, d, e in zip(start, dur, end)] return self.chc_model.cumulative(tasks, demand, cap) + elif cpm_expr.name == "precedence": + return self.chc_model.int_value_precede_chain(self._to_vars(cpm_expr.args[0]), cpm_expr.args[1]) elif cpm_expr.name == "gcc": vars, vals, occ = cpm_expr.args return self.chc_model.global_cardinality(*self.solver_vars([vars, vals]), self._to_vars(occ)) diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index d7335cf7c..037957a72 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -419,6 +419,7 @@ 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", + "precedence", "decreasing","strictly_increasing","strictly_decreasing"} return decompose_in_tree(cpm_cons, supported, supported_reified=supported - {"circuit"}) @@ -584,6 +585,12 @@ def zero_based(array): return format_str.format(args_str[0], args_str[1], args_str[3], args_str[4]) + elif expr.name == "precedence": + args, precedence = expr.args + format_str = "value_precede_chain({},{})".format(self._convert_expression(precedence), + self._convert_expression(args)) + return format_str + elif expr.name == 'ite': cond, tr, fal = expr.args return "if {} then {} else {} endif".format(self._convert_expression(cond), self._convert_expression(tr), From 0d6af5a833bf46ab9ea70bb55e03e4efe8c52256 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 18:14:59 +0200 Subject: [PATCH 04/19] update tests --- tests/test_globalconstraints.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_globalconstraints.py b/tests/test_globalconstraints.py index f6aa7b426..090a1ca6f 100644 --- a/tests/test_globalconstraints.py +++ b/tests/test_globalconstraints.py @@ -713,9 +713,6 @@ def test_precedence(self): iv = cp.intvar(0,5, shape=6, name="x") cons = cp.Precedence(iv, [0,2,1]) - for c in cons.decompose()[0]: - print(c) - self.assertTrue(cp.Model([cons, iv == [0,2,2,1,0,1]]).solve()) self.assertTrue(cp.Model([cons, iv == [0,0,0,0,0,0]]).solve()) From 69d95c7d9b1d1c32cf1cbf58bba2f99aa977dbad Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 18:15:28 +0200 Subject: [PATCH 05/19] run for all solvers --- tests/test_constraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index baff35b13..a8b5ced08 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -12,7 +12,7 @@ # also add exclusions to the 3 EXCLUDE_* below as needed SOLVERNAMES = [name for name, solver in SolverLookup.base_solvers() if solver.supported()] ALL_SOLS = False # test wheter all solutions returned by the solver satisfy the constraint -SOLVERNAMES = ["ortools"] + # Exclude some global constraints for solvers NUM_GLOBAL = { From 5157a43659e8de362baf699824a1f32ff1897d1d Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 20:11:09 +0200 Subject: [PATCH 06/19] remove debugging code --- tests/test_constraints.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index a8b5ced08..e4dae9416 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -242,16 +242,7 @@ def test_bool_constaints(solver, constraint): n_sols = SolverLookup.get(solver, Model(constraint)).solveAll(display=lambda: verify(constraint)) assert n_sols >= 1 else: - print(constraint) - for c in constraint.decompose()[0]: - print("-", c) - assert SolverLookup.get(solver, Model(constraint)).solve() - - from cpmpy.transformations.get_variables import get_variables - for v in get_variables(constraint): - print(v, v.value()) - assert argval(constraint) assert constraint.value() From 172274d1d1ddb8fe70532eaac75e9557f7c9cd96 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 20:21:55 +0200 Subject: [PATCH 07/19] add test for false --- tests/test_globalconstraints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_globalconstraints.py b/tests/test_globalconstraints.py index 090a1ca6f..1a97b6d33 100644 --- a/tests/test_globalconstraints.py +++ b/tests/test_globalconstraints.py @@ -715,6 +715,7 @@ def test_precedence(self): cons = cp.Precedence(iv, [0,2,1]) self.assertTrue(cp.Model([cons, iv == [0,2,2,1,0,1]]).solve()) self.assertTrue(cp.Model([cons, iv == [0,0,0,0,0,0]]).solve()) + self.assertFalse(cp.Model([cons, iv == [0,1,2,0,0,0]]).solve()) class TestBounds(unittest.TestCase): def test_bounds_minimum(self): From cc5190909f0df94d1b53d9dad502deb13f941964 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 20:22:12 +0200 Subject: [PATCH 08/19] minizinc does not support precedence reified --- cpmpy/solvers/minizinc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index 037957a72..fab787b27 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -421,7 +421,7 @@ def transform(self, cpm_expr): "inverse", "ite" "xor", "table", "cumulative", "circuit", "gcc", "increasing", "precedence", "decreasing","strictly_increasing","strictly_decreasing"} - return decompose_in_tree(cpm_cons, supported, supported_reified=supported - {"circuit"}) + return decompose_in_tree(cpm_cons, supported, supported_reified=supported - {"circuit", "precedence"}) def __add__(self, cpm_expr): From 406b4b377baaeff8ced09c57b19215699acb9eaf Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 20:22:23 +0200 Subject: [PATCH 09/19] fix value function --- cpmpy/expressions/globalconstraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 91f3d46ad..4b546b647 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -538,7 +538,7 @@ def value(self): for s,t in zip(precedence[:-1], precedence[1:]): if vals[0] == t: return False for j in range(len(args)): - if vals[0] == t and sum(args[:j] == s) == 0: + if vals[j] == t and sum(vals[:j] == s) == 0: return False return True From 8c282813ccdb7de80be1a6f2bde8b00fb31c21a0 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 21:11:53 +0200 Subject: [PATCH 10/19] add no overlap constraint --- cpmpy/expressions/__init__.py | 2 +- cpmpy/expressions/globalconstraints.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/cpmpy/expressions/__init__.py b/cpmpy/expressions/__init__.py index ebe229b2c..be6fd4647 100644 --- a/cpmpy/expressions/__init__.py +++ b/cpmpy/expressions/__init__.py @@ -23,7 +23,7 @@ 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 + DecreasingStrict, Precedence, NoOverlap from .globalconstraints import alldifferent, allequal, circuit # Old, to be deprecated from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue, NValueExcept diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 4b546b647..3e0e1d9f6 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -543,6 +543,27 @@ def value(self): return True +class NoOverlap(GlobalConstraint): + + def __init__(self, start, dur, end): + super().__init__("no_overlap", [start, dur, end]) + + def decompose(self): + start, dur, end = self.args + cons = [s + d == e for s,d,e in zip(start, dur, end)] + for (s1, e1), (s2, e2) in all_pairs(zip(start, end)): + cons += [(e1 <= s2) | (e2 <= s1)] + return cons, [] + def value(self): + start, dur, end = argvals(self.args) + for (s1,d1, e1), (s2,d2, e2) in all_pairs(zip(start,dur, end)): + if s1 + d1 != e1: return False + if s2 + d2 != e2: return False + if e1 > s2 and e2 > s1: + return False + return True + + class GlobalCardinalityCount(GlobalConstraint): """ GlobalCardinalityCount(vars,vals,occ): The number of occurrences of each value vals[i] in the list of variables vars From 128da3d4adbda288fd111181e12f9edc49739567 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 21:12:11 +0200 Subject: [PATCH 11/19] add tests --- tests/test_constraints.py | 7 ++++++- tests/test_globalconstraints.py | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index e4dae9416..d34c276c0 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -18,7 +18,7 @@ NUM_GLOBAL = { "AllEqual", "AllDifferent", "AllDifferentExcept0" , "GlobalCardinalityCount", "InDomain", "Inverse", "Table", "Circuit", "Increasing", "IncreasingStrict", "Decreasing", "DecreasingStrict", - "Precedence", "Cumulative", + "Precedence", "Cumulative", "NoOverlap", # also global functions "Abs", "Element", "Minimum", "Maximum", "Count", "NValue", "NValueExcept" } @@ -198,6 +198,11 @@ def global_constraints(solver): elif name == "Precedence": x = intvar(0,5, shape=3, name="x") expr = cls(x, [3,1,0]) + elif name == "NoOverlap": + s = intvar(0, 10, shape=3, name="start") + e = intvar(0, 10, shape=3, name="end") + dur = [1,4,3] + expr = cls(s, dur, e) elif name == "GlobalCardinalityCount": vals = [1, 2, 3] cnts = intvar(0,10,shape=3) diff --git a/tests/test_globalconstraints.py b/tests/test_globalconstraints.py index 1a97b6d33..b86b47f86 100644 --- a/tests/test_globalconstraints.py +++ b/tests/test_globalconstraints.py @@ -713,10 +713,27 @@ def test_precedence(self): iv = cp.intvar(0,5, shape=6, name="x") cons = cp.Precedence(iv, [0,2,1]) - self.assertTrue(cp.Model([cons, iv == [0,2,2,1,0,1]]).solve()) + self.assertTrue(cp.Model([cons, iv == [5,0,2,0,0,1]]).solve()) + self.assertTrue(cons.value()) self.assertTrue(cp.Model([cons, iv == [0,0,0,0,0,0]]).solve()) + self.assertTrue(cons.value()) self.assertFalse(cp.Model([cons, iv == [0,1,2,0,0,0]]).solve()) + + def test_no_overlap(self): + start = cp.intvar(0,5, shape=3) + end = cp.intvar(0,5, shape=3) + cons = cp.NoOverlap(start, [2,1,1], end) + self.assertTrue(cp.Model(cons).solve()) + self.assertTrue(cons.value()) + self.assertTrue(cp.Model(cons.decompose()).solve()) + self.assertTrue(cons.value()) + + def check_val(): + assert cons.value() is False + + cp.Model(~cons).solveAll(display=check_val) + class TestBounds(unittest.TestCase): def test_bounds_minimum(self): x = cp.intvar(-8, 8) From 4c68e3230e5f0c0cee715d946adbd4c292af73e3 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 21:12:22 +0200 Subject: [PATCH 12/19] make argvals truely recursive --- cpmpy/expressions/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cpmpy/expressions/utils.py b/cpmpy/expressions/utils.py index e5a78ed95..06bf92fc3 100644 --- a/cpmpy/expressions/utils.py +++ b/cpmpy/expressions/utils.py @@ -133,7 +133,9 @@ def argval(a): def argvals(arr): - return [argval(a) for a in arr] + if is_any_list(arr): + return [argvals(arg) for arg in arr] + return argval(arr) def eval_comparison(str_op, lhs, rhs): From 36fb6e4c98d019fd7d1cd09ce0e7765b8ce4199b Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 21:12:32 +0200 Subject: [PATCH 13/19] add to solvers --- cpmpy/solvers/minizinc.py | 8 +++++++- cpmpy/solvers/ortools.py | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index fab787b27..c9004f74c 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -419,7 +419,7 @@ 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", - "precedence", + "precedence","no_overlap", "decreasing","strictly_increasing","strictly_decreasing"} return decompose_in_tree(cpm_cons, supported, supported_reified=supported - {"circuit", "precedence"}) @@ -591,6 +591,12 @@ def zero_based(array): self._convert_expression(args)) return format_str + elif expr.name == "no_overlap": + start, dur, end = expr.args + durstr = self._convert_expression(s + d == e for s,d,e in zip(start, dur, end)) + format_str = "forall(" + durstr + " ++ [disjunctive({},{})])" + return format_str.format(start, dur) + elif expr.name == 'ite': cond, tr, fal = expr.args return "if {} then {} else {} endif".format(self._convert_expression(cond), self._convert_expression(tr), diff --git a/cpmpy/solvers/ortools.py b/cpmpy/solvers/ortools.py index c70dcbe8f..8ee98269b 100644 --- a/cpmpy/solvers/ortools.py +++ b/cpmpy/solvers/ortools.py @@ -330,7 +330,7 @@ def transform(self, cpm_expr): :return: list of Expression """ cpm_cons = toplevel_list(cpm_expr) - supported = {"min", "max", "abs", "element", "alldifferent", "xor", "table", "cumulative", "circuit", "inverse"} + supported = {"min", "max", "abs", "element", "alldifferent", "xor", "table", "cumulative", "circuit", "inverse", "no_overlap"} cpm_cons = decompose_in_tree(cpm_cons, supported) cpm_cons = flatten_constraint(cpm_cons) # flat normal form cpm_cons = reify_rewrite(cpm_cons, supported=frozenset(['sum', 'wsum'])) # constraints that support reification @@ -470,6 +470,10 @@ def _post_constraint(self, cpm_expr, reifiable=False): start, dur, end, demand, cap = self.solver_vars(cpm_expr.args) intervals = [self.ort_model.NewIntervalVar(s,d,e,f"interval_{s}-{d}-{e}") for s,d,e in zip(start,dur,end)] return self.ort_model.AddCumulative(intervals, demand, cap) + elif cpm_expr.name == "no_overlap": + start, dur, end = self.solver_vars(cpm_expr.args) + intervals = [self.ort_model.NewIntervalVar(s,d,e, f"interval_{s}-{d}-{d}") for s,d,e in zip(start,dur,end)] + return self.ort_model.add_no_overlap(intervals) elif cpm_expr.name == "circuit": # ortools has a constraint over the arcs, so we need to create these # when using an objective over arcs, using these vars direclty is recommended From 6256d7ae6a34cdda878a9247c26d0d955cae64b3 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 21:41:21 +0200 Subject: [PATCH 14/19] fix post to minizinc --- cpmpy/solvers/minizinc.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index c9004f74c..d488c7826 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -586,16 +586,13 @@ def zero_based(array): return format_str.format(args_str[0], args_str[1], args_str[3], args_str[4]) elif expr.name == "precedence": - args, precedence = expr.args - format_str = "value_precede_chain({},{})".format(self._convert_expression(precedence), - self._convert_expression(args)) - return format_str + return "value_precede_chain({},{})".format(args_str[1], args_str[0]) elif expr.name == "no_overlap": start, dur, end = expr.args - durstr = self._convert_expression(s + d == e for s,d,e in zip(start, dur, end)) + durstr = self._convert_expression([s + d == e for s, d, e in zip(start, dur, end)]) format_str = "forall(" + durstr + " ++ [disjunctive({},{})])" - return format_str.format(start, dur) + return format_str.format(args_str[0], args_str[1]) elif expr.name == 'ite': cond, tr, fal = expr.args From ec9e8651aa761c4a5803125beac8954f1affb328 Mon Sep 17 00:00:00 2001 From: IgnaceBleukx Date: Tue, 21 May 2024 21:41:32 +0200 Subject: [PATCH 15/19] better value() --- cpmpy/expressions/globalconstraints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 3e0e1d9f6..a7f9bbabc 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -556,9 +556,9 @@ def decompose(self): return cons, [] def value(self): start, dur, end = argvals(self.args) + if any(s + d != e for s,d,e in zip(start, dur, end)): + return False for (s1,d1, e1), (s2,d2, e2) in all_pairs(zip(start,dur, end)): - if s1 + d1 != e1: return False - if s2 + d2 != e2: return False if e1 > s2 and e2 > s1: return False return True From caac7e623eb915042f39a1900a62f7ef7f341d59 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 27 May 2024 16:33:25 +0200 Subject: [PATCH 16/19] minor --- cpmpy/expressions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/expressions/__init__.py b/cpmpy/expressions/__init__.py index 6163eca2e..76e3b7546 100644 --- a/cpmpy/expressions/__init__.py +++ b/cpmpy/expressions/__init__.py @@ -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, AllDifferentLists, 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, Precedence, NoOverlap from .globalconstraints import alldifferent, allequal, circuit # Old, to be deprecated from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue, NValueExcept From 17685a7abb1197d6b6044f16322779ce001230a0 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 27 May 2024 17:03:22 +0200 Subject: [PATCH 17/19] cumulative docstring fix --- cpmpy/expressions/globalconstraints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 0e4f4e833..b48a1843a 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -455,7 +455,8 @@ def __repr__(self): class Cumulative(GlobalConstraint): """ Global cumulative constraint. Used for resource aware scheduling. - Ensures no overlap between tasks and never exceeding the capacity of the resource + Ensures that the capacity of the resource is never exceeded + Equivalent to noOverlap when demand and capacity are equal to 1 Supports both varying demand across tasks or equal demand for all jobs """ def __init__(self, start, duration, end, demand, capacity): From d8be7f47b4eee9c743fcc377c534a9d8e78cd6b0 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 27 May 2024 17:04:12 +0200 Subject: [PATCH 18/19] precedence docstring fix --- cpmpy/expressions/globalconstraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index b48a1843a..180abbfe1 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -538,7 +538,7 @@ def value(self): class Precedence(GlobalConstraint): """ Constraint enforcing some values have precedence over others. - Given an array of variables X and and a list of precedences P: + Given an array of variables X and a list of precedences P: Then in order to satisfy the constraint, if X[i] = P[j+1], then there exists a X[i'] = P[j] with i' < i """ def __init__(self, vars, precedence): From 270a42eebbb048416866c20eb6942565eca5134b Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 27 May 2024 17:05:52 +0200 Subject: [PATCH 19/19] asserts in nooverlap init --- cpmpy/expressions/globalconstraints.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 180abbfe1..7d229bc27 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -578,6 +578,15 @@ def value(self): class NoOverlap(GlobalConstraint): def __init__(self, start, dur, end): + assert is_any_list(start), "start should be a list" + assert is_any_list(dur), "duration should be a list" + assert is_any_list(end), "end should be a list" + + start = flatlist(start) + dur = flatlist(dur) + end = flatlist(end) + assert len(start) == len(dur) == len(end), "Start, duration and end should have equal length in NoOverlap constraint" + super().__init__("no_overlap", [start, dur, end]) def decompose(self):