pycsp3-scheduling 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,376 @@
1
+ """
2
+ Precedence constraints for interval variables.
3
+
4
+ This module provides two families of precedence constraints:
5
+
6
+ 1. **Exact timing constraints** (equality):
7
+ - start_at_start(a, b, delay): start(b) == start(a) + delay
8
+ - start_at_end(a, b, delay): start(b) == end(a) + delay
9
+ - end_at_start(a, b, delay): end(a) == start(b) + delay
10
+ - end_at_end(a, b, delay): end(b) == end(a) + delay
11
+
12
+ 2. **Before constraints** (inequality):
13
+ - start_before_start(a, b, delay): start(b) >= start(a) + delay
14
+ - start_before_end(a, b, delay): end(b) >= start(a) + delay
15
+ - end_before_start(a, b, delay): start(b) >= end(a) + delay
16
+ - end_before_end(a, b, delay): end(b) >= end(a) + delay
17
+
18
+ All constraints return pycsp3 Node objects that can be used with satisfy().
19
+
20
+ Absent interval semantics:
21
+ - When both intervals are present: constraint is enforced
22
+ - When either interval is absent: constraint is trivially satisfied
23
+ (Note: optional intervals not yet implemented)
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from pycsp3_scheduling.constraints._pycsp3 import length_value, start_var
29
+ from pycsp3_scheduling.variables.interval import IntervalVar
30
+
31
+
32
+ def _validate_precedence_args(
33
+ a: IntervalVar, b: IntervalVar, delay: int, func_name: str
34
+ ) -> None:
35
+ """Validate arguments for precedence constraint functions."""
36
+ if not isinstance(a, IntervalVar) or not isinstance(b, IntervalVar):
37
+ raise TypeError(f"{func_name} expects IntervalVar inputs")
38
+ if not isinstance(delay, int):
39
+ raise TypeError("delay must be an int")
40
+
41
+
42
+ def _get_node_builders():
43
+ """Import and return pycsp3 Node building utilities."""
44
+ from pycsp3.classes.nodes import Node, TypeNode
45
+
46
+ return Node, TypeNode
47
+
48
+
49
+ # =============================================================================
50
+ # Exact Timing Constraints (Equality)
51
+ # =============================================================================
52
+
53
+
54
+ def start_at_start(a: IntervalVar, b: IntervalVar, delay: int = 0):
55
+ """
56
+ Enforce that interval b starts exactly when interval a starts (plus delay).
57
+
58
+ Semantics (when both present):
59
+ start(b) == start(a) + delay
60
+
61
+ Args:
62
+ a: First interval variable.
63
+ b: Second interval variable.
64
+ delay: Time delay between start of a and start of b. Default 0.
65
+
66
+ Returns:
67
+ A pycsp3 Node representing the constraint.
68
+
69
+ Raises:
70
+ TypeError: If inputs are not IntervalVar or delay is not int.
71
+
72
+ Example:
73
+ >>> task1 = IntervalVar(size=10, name="task1")
74
+ >>> task2 = IntervalVar(size=5, name="task2")
75
+ >>> satisfy(start_at_start(task1, task2)) # Start together
76
+ >>> satisfy(start_at_start(task1, task2, delay=5)) # task2 starts 5 after task1
77
+ """
78
+ _validate_precedence_args(a, b, delay, "start_at_start")
79
+ Node, TypeNode = _get_node_builders()
80
+
81
+ start_a = start_var(a)
82
+ start_b = start_var(b)
83
+
84
+ # start(b) == start(a) + delay
85
+ if delay:
86
+ rhs = Node.build(TypeNode.ADD, start_a, delay)
87
+ else:
88
+ rhs = start_a
89
+ return Node.build(TypeNode.EQ, start_b, rhs)
90
+
91
+
92
+ def start_at_end(a: IntervalVar, b: IntervalVar, delay: int = 0):
93
+ """
94
+ Enforce that interval b starts exactly when interval a ends (plus delay).
95
+
96
+ Semantics (when both present):
97
+ start(b) == end(a) + delay
98
+ start(b) == start(a) + length(a) + delay
99
+
100
+ Args:
101
+ a: First interval variable.
102
+ b: Second interval variable.
103
+ delay: Time delay between end of a and start of b. Default 0.
104
+
105
+ Returns:
106
+ A pycsp3 Node representing the constraint.
107
+
108
+ Raises:
109
+ TypeError: If inputs are not IntervalVar or delay is not int.
110
+
111
+ Example:
112
+ >>> task1 = IntervalVar(size=10, name="task1")
113
+ >>> task2 = IntervalVar(size=5, name="task2")
114
+ >>> satisfy(start_at_end(task1, task2)) # task2 starts exactly when task1 ends
115
+ """
116
+ _validate_precedence_args(a, b, delay, "start_at_end")
117
+ Node, TypeNode = _get_node_builders()
118
+
119
+ start_a = start_var(a)
120
+ start_b = start_var(b)
121
+ length_a = length_value(a)
122
+
123
+ # start(b) == start(a) + length(a) + delay
124
+ rhs = Node.build(TypeNode.ADD, start_a, length_a)
125
+ if delay:
126
+ rhs = Node.build(TypeNode.ADD, rhs, delay)
127
+ return Node.build(TypeNode.EQ, start_b, rhs)
128
+
129
+
130
+ def end_at_start(a: IntervalVar, b: IntervalVar, delay: int = 0):
131
+ """
132
+ Enforce that interval a ends exactly when interval b starts (plus delay).
133
+
134
+ Semantics (when both present):
135
+ end(a) == start(b) + delay
136
+ start(a) + length(a) == start(b) + delay
137
+
138
+ Note: This is equivalent to start_at_end(b, a, -delay) but expressed
139
+ from a's perspective.
140
+
141
+ Args:
142
+ a: First interval variable.
143
+ b: Second interval variable.
144
+ delay: Time delay between end of a and start of b. Default 0.
145
+
146
+ Returns:
147
+ A pycsp3 Node representing the constraint.
148
+
149
+ Raises:
150
+ TypeError: If inputs are not IntervalVar or delay is not int.
151
+
152
+ Example:
153
+ >>> task1 = IntervalVar(size=10, name="task1")
154
+ >>> task2 = IntervalVar(size=5, name="task2")
155
+ >>> satisfy(end_at_start(task1, task2)) # task1 ends exactly when task2 starts
156
+ """
157
+ _validate_precedence_args(a, b, delay, "end_at_start")
158
+ Node, TypeNode = _get_node_builders()
159
+
160
+ start_a = start_var(a)
161
+ start_b = start_var(b)
162
+ length_a = length_value(a)
163
+
164
+ # end(a) == start(b) + delay
165
+ # start(a) + length(a) == start(b) + delay
166
+ lhs = Node.build(TypeNode.ADD, start_a, length_a)
167
+ if delay:
168
+ rhs = Node.build(TypeNode.ADD, start_b, delay)
169
+ else:
170
+ rhs = start_b
171
+ return Node.build(TypeNode.EQ, lhs, rhs)
172
+
173
+
174
+ def end_at_end(a: IntervalVar, b: IntervalVar, delay: int = 0):
175
+ """
176
+ Enforce that interval b ends exactly when interval a ends (plus delay).
177
+
178
+ Semantics (when both present):
179
+ end(b) == end(a) + delay
180
+ start(b) + length(b) == start(a) + length(a) + delay
181
+
182
+ Args:
183
+ a: First interval variable.
184
+ b: Second interval variable.
185
+ delay: Time delay between end of a and end of b. Default 0.
186
+
187
+ Returns:
188
+ A pycsp3 Node representing the constraint.
189
+
190
+ Raises:
191
+ TypeError: If inputs are not IntervalVar or delay is not int.
192
+
193
+ Example:
194
+ >>> task1 = IntervalVar(size=10, name="task1")
195
+ >>> task2 = IntervalVar(size=5, name="task2")
196
+ >>> satisfy(end_at_end(task1, task2)) # Both end at the same time
197
+ """
198
+ _validate_precedence_args(a, b, delay, "end_at_end")
199
+ Node, TypeNode = _get_node_builders()
200
+
201
+ start_a = start_var(a)
202
+ start_b = start_var(b)
203
+ length_a = length_value(a)
204
+ length_b = length_value(b)
205
+
206
+ # end(b) == end(a) + delay
207
+ # start(b) + length(b) == start(a) + length(a) + delay
208
+ lhs = Node.build(TypeNode.ADD, start_b, length_b)
209
+ rhs = Node.build(TypeNode.ADD, start_a, length_a)
210
+ if delay:
211
+ rhs = Node.build(TypeNode.ADD, rhs, delay)
212
+ return Node.build(TypeNode.EQ, lhs, rhs)
213
+
214
+
215
+ # =============================================================================
216
+ # Before Constraints (Inequality)
217
+ # =============================================================================
218
+
219
+
220
+ def start_before_start(a: IntervalVar, b: IntervalVar, delay: int = 0):
221
+ """
222
+ Enforce that interval b cannot start before interval a starts (plus delay).
223
+
224
+ Semantics (when both present):
225
+ start(b) >= start(a) + delay
226
+
227
+ Args:
228
+ a: First interval variable.
229
+ b: Second interval variable.
230
+ delay: Minimum time between start of a and start of b. Default 0.
231
+
232
+ Returns:
233
+ A pycsp3 Node representing the constraint.
234
+
235
+ Raises:
236
+ TypeError: If inputs are not IntervalVar or delay is not int.
237
+
238
+ Example:
239
+ >>> task1 = IntervalVar(size=10, name="task1")
240
+ >>> task2 = IntervalVar(size=5, name="task2")
241
+ >>> satisfy(start_before_start(task1, task2)) # task2 starts after task1 starts
242
+ """
243
+ _validate_precedence_args(a, b, delay, "start_before_start")
244
+ Node, TypeNode = _get_node_builders()
245
+
246
+ start_a = start_var(a)
247
+ start_b = start_var(b)
248
+
249
+ # start(b) >= start(a) + delay => start(a) + delay <= start(b)
250
+ if delay:
251
+ lhs = Node.build(TypeNode.ADD, start_a, delay)
252
+ else:
253
+ lhs = start_a
254
+ return Node.build(TypeNode.LE, lhs, start_b)
255
+
256
+
257
+ def start_before_end(a: IntervalVar, b: IntervalVar, delay: int = 0):
258
+ """
259
+ Enforce that interval b cannot end before interval a starts (plus delay).
260
+
261
+ Semantics (when both present):
262
+ end(b) >= start(a) + delay
263
+
264
+ Args:
265
+ a: First interval variable.
266
+ b: Second interval variable.
267
+ delay: Minimum time between start of a and end of b. Default 0.
268
+
269
+ Returns:
270
+ A pycsp3 Node representing the constraint.
271
+
272
+ Raises:
273
+ TypeError: If inputs are not IntervalVar or delay is not int.
274
+
275
+ Example:
276
+ >>> task1 = IntervalVar(size=10, name="task1")
277
+ >>> task2 = IntervalVar(size=5, name="task2")
278
+ >>> satisfy(start_before_end(task1, task2)) # task2 ends after task1 starts
279
+ """
280
+ _validate_precedence_args(a, b, delay, "start_before_end")
281
+ Node, TypeNode = _get_node_builders()
282
+
283
+ start_a = start_var(a)
284
+ start_b = start_var(b)
285
+ length_b = length_value(b)
286
+
287
+ # end(b) >= start(a) + delay
288
+ # start(b) + length(b) >= start(a) + delay
289
+ # start(a) + delay <= start(b) + length(b)
290
+ if delay:
291
+ lhs = Node.build(TypeNode.ADD, start_a, delay)
292
+ else:
293
+ lhs = start_a
294
+ rhs = Node.build(TypeNode.ADD, start_b, length_b)
295
+ return Node.build(TypeNode.LE, lhs, rhs)
296
+
297
+
298
+ def end_before_start(a: IntervalVar, b: IntervalVar, delay: int = 0):
299
+ """
300
+ Enforce that interval a ends before interval b starts.
301
+
302
+ This is the classic precedence constraint used in job-shop scheduling.
303
+
304
+ Semantics (when both present):
305
+ start(b) >= end(a) + delay
306
+ start(b) >= start(a) + length(a) + delay
307
+
308
+ Args:
309
+ a: First interval variable (predecessor).
310
+ b: Second interval variable (successor).
311
+ delay: Minimum time between end of a and start of b. Default 0.
312
+
313
+ Returns:
314
+ A pycsp3 Node representing the constraint.
315
+
316
+ Raises:
317
+ TypeError: If inputs are not IntervalVar or delay is not int.
318
+
319
+ Example:
320
+ >>> task1 = IntervalVar(size=10, name="task1")
321
+ >>> task2 = IntervalVar(size=5, name="task2")
322
+ >>> satisfy(end_before_start(task1, task2)) # task2 starts after task1 ends
323
+ """
324
+ _validate_precedence_args(a, b, delay, "end_before_start")
325
+ Node, TypeNode = _get_node_builders()
326
+
327
+ start_a = start_var(a)
328
+ start_b = start_var(b)
329
+ length_a = length_value(a)
330
+
331
+ # start(b) >= start(a) + length(a) + delay
332
+ lhs = Node.build(TypeNode.ADD, start_a, length_a)
333
+ if delay:
334
+ lhs = Node.build(TypeNode.ADD, lhs, delay)
335
+ return Node.build(TypeNode.LE, lhs, start_b)
336
+
337
+
338
+ def end_before_end(a: IntervalVar, b: IntervalVar, delay: int = 0):
339
+ """
340
+ Enforce that interval b cannot end before interval a ends (plus delay).
341
+
342
+ Semantics (when both present):
343
+ end(b) >= end(a) + delay
344
+ start(b) + length(b) >= start(a) + length(a) + delay
345
+
346
+ Args:
347
+ a: First interval variable.
348
+ b: Second interval variable.
349
+ delay: Minimum time between end of a and end of b. Default 0.
350
+
351
+ Returns:
352
+ A pycsp3 Node representing the constraint.
353
+
354
+ Raises:
355
+ TypeError: If inputs are not IntervalVar or delay is not int.
356
+
357
+ Example:
358
+ >>> task1 = IntervalVar(size=10, name="task1")
359
+ >>> task2 = IntervalVar(size=5, name="task2")
360
+ >>> satisfy(end_before_end(task1, task2)) # task2 ends after task1 ends
361
+ """
362
+ _validate_precedence_args(a, b, delay, "end_before_end")
363
+ Node, TypeNode = _get_node_builders()
364
+
365
+ start_a = start_var(a)
366
+ start_b = start_var(b)
367
+ length_a = length_value(a)
368
+ length_b = length_value(b)
369
+
370
+ # end(b) >= end(a) + delay
371
+ # start(b) + length(b) >= start(a) + length(a) + delay
372
+ lhs = Node.build(TypeNode.ADD, start_a, length_a)
373
+ if delay:
374
+ lhs = Node.build(TypeNode.ADD, lhs, delay)
375
+ rhs = Node.build(TypeNode.ADD, start_b, length_b)
376
+ return Node.build(TypeNode.LE, lhs, rhs)