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

Scheduling constraints #486

Merged
merged 21 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
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
4 changes: 3 additions & 1 deletion cpmpy/expressions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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
61 changes: 61 additions & 0 deletions cpmpy/expressions/globalconstraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,67 @@ 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[j] == t and sum(vals[:j] == s) == 0:
return False
return True


class NoOverlap(GlobalConstraint):
Dimosts marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, start, dur, end):
super().__init__("no_overlap", [start, dur, end])
Dimosts marked this conversation as resolved.
Show resolved Hide resolved

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)
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 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
Expand Down
4 changes: 3 additions & 1 deletion cpmpy/expressions/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions cpmpy/solvers/choco.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
12 changes: 11 additions & 1 deletion cpmpy/solvers/minizinc.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,9 @@ 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","no_overlap",
"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):
Expand Down Expand Up @@ -584,6 +585,15 @@ def zero_based(array):

return format_str.format(args_str[0], args_str[1], args_str[3], args_str[4])

elif expr.name == "precedence":
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)])
format_str = "forall(" + durstr + " ++ [disjunctive({},{})])"
Dimosts marked this conversation as resolved.
Show resolved Hide resolved
return format_str.format(args_str[0], args_str[1])

elif expr.name == 'ite':
cond, tr, fal = expr.args
return "if {} then {} else {} endif".format(self._convert_expression(cond), self._convert_expression(tr),
Expand Down
6 changes: 5 additions & 1 deletion cpmpy/solvers/ortools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion tests/test_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
# 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", "NoOverlap",
# also global functions
"Abs", "Element", "Minimum", "Maximum", "Count", "NValue", "NValueExcept"
}
Expand Down Expand Up @@ -194,6 +195,14 @@ 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 == "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)
Expand Down
26 changes: 26 additions & 0 deletions tests/test_globalconstraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,32 @@ 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])
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)
Expand Down
Loading