-
Notifications
You must be signed in to change notification settings - Fork 30
Refactor and update Cumulative and NoOverlap constraints #694
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
base: master
Are you sure you want to change the base?
Changes from all commits
7b52275
901189f
2c7e8cd
0f0dc13
f37cbf2
8f190f5
71b4315
053baee
e46bbbc
bec36c0
34eb52e
8678655
812247b
19c8509
6339037
11ab221
cd3b04f
2d1b516
c09bbda
fd96cc2
d8d0891
05ccd20
4e0bf81
bae7173
63af96c
4bac809
e760171
8394413
b1e7984
ca82b5b
890c68e
74d70ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -672,30 +672,46 @@ def __repr__(self): | |
class Cumulative(GlobalConstraint): | ||
""" | ||
Global cumulative constraint. Used for resource aware scheduling. | ||
Ensures that the capacity of the resource is never exceeded. | ||
Ensures that the capacity of the resource is never exceeded and enforces: | ||
duration >= 0 | ||
demand >= 0 | ||
start + duration == end | ||
|
||
Equivalent to :class:`~cpmpy.expressions.globalconstraints.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): | ||
def __init__(self, start, duration, end=None, demand=None, capacity=None): | ||
""" | ||
Arguments of constructor: | ||
|
||
Arguments: | ||
`start`: List of Expression objects representing the start times of the tasks | ||
`duration`: List of Expression objects representing the durations of the tasks | ||
`end`: optional, list of Expression objects representing the end times of the tasks | ||
`demand`: List of Expression objects or single Expression to indicate constant demand for all tasks | ||
`capacity`: Expression object representing the capacity of the resource | ||
""" | ||
|
||
assert is_any_list(start), "start should be a list" | ||
assert is_any_list(duration), "duration should be a list" | ||
assert is_any_list(end), "end should be a list" | ||
if end is not None: | ||
assert is_any_list(end), "end should be a list if it is provided" | ||
|
||
assert demand is not None, "demand should be provided but was None" | ||
assert capacity is not None, "capacity should be provided but was None" | ||
|
||
start = flatlist(start) | ||
duration = flatlist(duration) | ||
end = flatlist(end) | ||
assert len(start) == len(duration) == len(end), "Start, duration and end should have equal length" | ||
n_jobs = len(start) | ||
|
||
for lb in get_bounds(duration)[0]: | ||
if lb < 0: | ||
raise TypeError("Durations should be non-negative") | ||
assert len(start) == len(duration), "Start and duration should have equal length" | ||
if end is not None: | ||
end = flatlist(end) | ||
assert len(start) == len(end), "Start and end should have equal length" | ||
|
||
if is_any_list(demand): | ||
demand = flatlist(demand) | ||
assert len(demand) == n_jobs, "Demand should be supplied for each task or be single constant" | ||
assert len(demand) == len(start), "Demand should be supplied for each task or be single constant" | ||
else: # constant demand | ||
demand = [demand] * n_jobs | ||
demand = [demand] * len(start) | ||
|
||
super(Cumulative, self).__init__("cumulative", [start, duration, end, demand, capacity]) | ||
|
||
|
@@ -706,49 +722,65 @@ def decompose(self): | |
International Conference on Principles and Practice of Constraint Programming. Springer, Berlin, Heidelberg, 2009. | ||
""" | ||
|
||
arr_args = (cpm_array(arg) if is_any_list(arg) else arg for arg in self.args) | ||
start, duration, end, demand, capacity = arr_args | ||
start, duration, end, demand, capacity = self.args | ||
|
||
cons = [] | ||
cons = [d >= 0 for d in duration] # enforce non-negative durations | ||
cons += [h >= 0 for h in demand] # enforce non-negative demand | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should maybe be in safening? It is defined as a restriction, not as an enforced constraint. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels more like safening code than constraining. It is a restriction of the cumulative constraint (https://sofdem.github.io/gccat/aux/pdf/cumulative.pdf) and not something that is enforced. However, after in person discussion I agree that it will create more problems if we do it in safening instead. Note to revisit this if we encounter more such cases. |
||
|
||
# set duration of tasks | ||
for t in range(len(start)): | ||
cons += [start[t] + duration[t] == end[t]] | ||
# set duration of tasks, only if end is user-provided | ||
if end is None: | ||
end = [start[i] + duration[i] for i in range(len(start))] | ||
else: | ||
cons += [start[i] + duration[i] == end[i] for i in range(len(start))] | ||
|
||
# demand doesn't exceed capacity | ||
lb, ub = min(get_bounds(start)[0]), max(get_bounds(end)[1]) | ||
lbs, ubs = get_bounds(start) | ||
lb, ub = min(lbs), max(ubs) | ||
for t in range(lb,ub+1): | ||
demand_at_t = 0 | ||
for job in range(len(start)): | ||
if is_num(demand): | ||
demand_at_t += demand * ((start[job] <= t) & (t < end[job])) | ||
else: | ||
demand_at_t += demand[job] * ((start[job] <= t) & (t < end[job])) | ||
demand_at_t += demand[job] * ((start[job] <= t) & (end[job] > t)) | ||
|
||
cons += [demand_at_t <= capacity] | ||
|
||
return cons, [] | ||
|
||
def value(self): | ||
arg_vals = [np.array(argvals(arg)) if is_any_list(arg) | ||
else argval(arg) for arg in self.args] | ||
|
||
if any(a is None for a in arg_vals): | ||
start, dur, end, demand, capacity = self.args | ||
|
||
start, dur, demand, capacity = argvals([start, dur, demand, capacity]) | ||
if end is None: | ||
end = [s + d for s,d in zip(start, dur)] | ||
else: | ||
end = argvals(end) | ||
|
||
if any(a is None for a in flatlist([start, dur, end, demand, capacity])): | ||
return None | ||
|
||
if any(d < 0 for d in dur): | ||
return False | ||
if any(s + d != e for s,d,e in zip(start, dur, end)): | ||
return False | ||
|
||
# start, dur, end are np arrays | ||
start, dur, end, demand, capacity = arg_vals | ||
# start and end seperated by duration | ||
if not (start + dur == end).all(): | ||
if any(d < 0 for d in demand): | ||
return False | ||
|
||
# demand doesn't exceed capacity | ||
# ensure demand doesn't exceed capacity | ||
lb, ub = min(start), max(end) | ||
start, end = np.array(start), np.array(end) # eases check below | ||
for t in range(lb, ub+1): | ||
if capacity < sum(demand * ((start <= t) & (t < end))): | ||
return False | ||
|
||
return True | ||
|
||
def __repr__(self): | ||
start, dur, end, demand, capacity = self.args | ||
if end is None: | ||
return f"Cumulative({start}, {dur}, {demand}, {capacity})" | ||
else: | ||
return f"Cumulative({start}, {dur}, {end}, {demand}, {capacity})" | ||
|
||
|
||
class Precedence(GlobalConstraint): | ||
|
@@ -795,37 +827,68 @@ def value(self): | |
|
||
class NoOverlap(GlobalConstraint): | ||
""" | ||
NoOverlap constraint, enforcing that the intervals defined by start, duration and end do not overlap. | ||
Global no-overlap constraint. Used for scheduling problems | ||
Ensures no tasks overlap and enforces: | ||
duration >= 0 | ||
demand >= 0 | ||
start + duration == end | ||
|
||
Equivalent to :class:`~cpmpy.expressions.globalconstraints.Cumulative` with demand and capacity 1 | ||
""" | ||
|
||
def __init__(self, start, dur, end): | ||
def __init__(self, start, dur, end=None): | ||
""" | ||
Arguments: | ||
`start`: List of Expression objects representing the start times of the tasks | ||
`duration`: List of Expression objects representing the durations of the tasks | ||
`end`: optional, list of Expression objects representing the end times of the tasks | ||
""" | ||
|
||
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" | ||
if end is not None: | ||
assert is_any_list(end), "end should be a list if it is provided" | ||
|
||
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" | ||
|
||
assert len(start) == len(dur), "start and duration should have equal length" | ||
if end is not None: | ||
end = flatlist(end) | ||
assert len(start) == len(end), "start and end should have equal length" | ||
|
||
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)] | ||
cons = [d >= 0 for d in dur] | ||
|
||
if end is None: | ||
end = [s+d for s,d in zip(start, dur)] | ||
else: # can use the expression directly below | ||
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)): | ||
if any(d < 0 for d in dur): | ||
return False | ||
if end is not None and 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: | ||
for (s1,d1), (s2,d2) in all_pairs(zip(start,dur)): | ||
if s1 + d1 > s2 and s2 + d2 > s1: | ||
return False | ||
return True | ||
|
||
def __repr__(self): | ||
start, dur, end = self.args | ||
if end is None: | ||
return f"NoOverlap({start}, {dur})" | ||
else: | ||
return f"NoOverlap({start}, {dur}, {end})" | ||
|
||
|
||
|
||
class GlobalCardinalityCount(GlobalConstraint): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what should be done here, but I am not really a fan of giving them a default value (making them optional) and then not allowing the optional value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I also agree it is quite annoying... For now this is the best I could come up with, as you cannot make an argument in the middle optional...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still troubled by this. So, someone either has to actually use the end var after all, or has to use kwargs to define demand and capacity, which are optional but not that optional after all.
Both minizinc (https://docs.minizinc.dev/en/stable/predicates.html) and essence (https://arxiv.org/pdf/2201.03472) actually do not accept end variables at all, and create them for solvers that need them. Should we stick to having them after all?
The question is what if the user needs the end variables for something else I guess; In this case, if the users define themselves the end = start + duration constraints, it is ok. Common subexpression elimination should capture it during the decomposition if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah, but removing it all together will break every CPMpy model currently out there that uses a Cumulative constraint...
I don't really have too much of an opinion on whether we want to keep the end variables or not, but if we decide to remove them, we should wait a couple of releases and raise a deprecation warning here