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.
- pycsp3_scheduling/__init__.py +220 -0
- pycsp3_scheduling/constraints/__init__.py +87 -0
- pycsp3_scheduling/constraints/_pycsp3.py +701 -0
- pycsp3_scheduling/constraints/cumulative.py +227 -0
- pycsp3_scheduling/constraints/grouping.py +382 -0
- pycsp3_scheduling/constraints/precedence.py +376 -0
- pycsp3_scheduling/constraints/sequence.py +814 -0
- pycsp3_scheduling/expressions/__init__.py +80 -0
- pycsp3_scheduling/expressions/element.py +313 -0
- pycsp3_scheduling/expressions/interval_expr.py +495 -0
- pycsp3_scheduling/expressions/sequence_expr.py +865 -0
- pycsp3_scheduling/functions/__init__.py +111 -0
- pycsp3_scheduling/functions/cumul_functions.py +891 -0
- pycsp3_scheduling/functions/state_functions.py +494 -0
- pycsp3_scheduling/interop.py +356 -0
- pycsp3_scheduling/output/__init__.py +13 -0
- pycsp3_scheduling/solvers/__init__.py +14 -0
- pycsp3_scheduling/solvers/adapters/__init__.py +7 -0
- pycsp3_scheduling/variables/__init__.py +45 -0
- pycsp3_scheduling/variables/interval.py +450 -0
- pycsp3_scheduling/variables/sequence.py +244 -0
- pycsp3_scheduling/visu.py +1315 -0
- pycsp3_scheduling-0.2.1.dist-info/METADATA +234 -0
- pycsp3_scheduling-0.2.1.dist-info/RECORD +26 -0
- pycsp3_scheduling-0.2.1.dist-info/WHEEL +4 -0
- pycsp3_scheduling-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sequence accessor expressions for scheduling models.
|
|
3
|
+
|
|
4
|
+
These functions return expressions that access properties of neighboring
|
|
5
|
+
intervals in a sequence relative to a given interval.
|
|
6
|
+
|
|
7
|
+
The key functions for building transition-based objectives are:
|
|
8
|
+
- next_arg(sequence, interval, last_value, absent_value)
|
|
9
|
+
- prev_arg(sequence, interval, first_value, absent_value)
|
|
10
|
+
|
|
11
|
+
These return pycsp3 variables that can be used to index into transition matrices.
|
|
12
|
+
Similar to pycsp3's maximum_arg/minimum_arg pattern.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from pycsp3_scheduling.expressions.interval_expr import ExprType, IntervalExpr
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
23
|
+
from pycsp3_scheduling.variables.sequence import SequenceVar
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Cache for next_arg/prev_arg variables to avoid duplication
|
|
27
|
+
_next_arg_vars: dict[tuple[int, int], Any] = {}
|
|
28
|
+
_prev_arg_vars: dict[tuple[int, int], Any] = {}
|
|
29
|
+
_sequence_position_vars: dict[int, list[Any]] = {}
|
|
30
|
+
_sequence_present_count_vars: dict[int, Any] = {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def clear_sequence_expr_cache() -> None:
|
|
34
|
+
"""Clear cached sequence expression variables."""
|
|
35
|
+
_next_arg_vars.clear()
|
|
36
|
+
_prev_arg_vars.clear()
|
|
37
|
+
_sequence_position_vars.clear()
|
|
38
|
+
_sequence_present_count_vars.clear()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _validate_sequence_and_interval(sequence, interval: IntervalVar) -> tuple[list, int]:
|
|
42
|
+
"""Validate inputs and return intervals list and index."""
|
|
43
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
44
|
+
from pycsp3_scheduling.variables.sequence import SequenceVar
|
|
45
|
+
|
|
46
|
+
if isinstance(sequence, SequenceVar):
|
|
47
|
+
intervals = sequence.intervals
|
|
48
|
+
elif isinstance(sequence, (list, tuple)):
|
|
49
|
+
intervals = list(sequence)
|
|
50
|
+
else:
|
|
51
|
+
raise TypeError(
|
|
52
|
+
f"sequence must be a SequenceVar or list, got {type(sequence).__name__}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if not isinstance(interval, IntervalVar):
|
|
56
|
+
raise TypeError(
|
|
57
|
+
f"interval must be an IntervalVar, got {type(interval).__name__}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
idx = intervals.index(interval)
|
|
62
|
+
except ValueError:
|
|
63
|
+
raise ValueError(f"interval '{interval.name}' is not in the sequence")
|
|
64
|
+
|
|
65
|
+
return intervals, idx
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _ensure_sequence_positions(sequence: SequenceVar) -> tuple[list[Any], Any]:
|
|
69
|
+
"""
|
|
70
|
+
Create (or reuse) position variables and ordering constraints for a sequence.
|
|
71
|
+
|
|
72
|
+
This function creates position variables that track the order of intervals
|
|
73
|
+
in a sequence. For optional intervals, position 0 indicates absence.
|
|
74
|
+
"""
|
|
75
|
+
if sequence._id in _sequence_position_vars:
|
|
76
|
+
return (
|
|
77
|
+
_sequence_position_vars[sequence._id],
|
|
78
|
+
_sequence_present_count_vars[sequence._id],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
from pycsp3 import AllDifferent, Var, satisfy
|
|
83
|
+
from pycsp3.classes.nodes import Node, TypeNode
|
|
84
|
+
except ImportError:
|
|
85
|
+
raise ImportError("pycsp3 is required for sequence position variables")
|
|
86
|
+
|
|
87
|
+
from pycsp3_scheduling.constraints._pycsp3 import (
|
|
88
|
+
start_var,
|
|
89
|
+
length_value,
|
|
90
|
+
presence_var,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
intervals = sequence.intervals
|
|
94
|
+
n = len(intervals)
|
|
95
|
+
|
|
96
|
+
if n == 0:
|
|
97
|
+
# Empty sequence - no constraints needed
|
|
98
|
+
count_var = Var(dom={0}, id=f"seqcount{sequence._id}")
|
|
99
|
+
satisfy(count_var == 0)
|
|
100
|
+
_sequence_position_vars[sequence._id] = []
|
|
101
|
+
_sequence_present_count_vars[sequence._id] = count_var
|
|
102
|
+
return [], count_var
|
|
103
|
+
|
|
104
|
+
positions: list[Any] = []
|
|
105
|
+
presences: list[Any] = []
|
|
106
|
+
has_optional = False
|
|
107
|
+
|
|
108
|
+
for interval in intervals:
|
|
109
|
+
pres = presence_var(interval) if interval.optional else 1
|
|
110
|
+
presences.append(pres)
|
|
111
|
+
|
|
112
|
+
if interval.optional:
|
|
113
|
+
has_optional = True
|
|
114
|
+
pos_dom = range(0, n + 1) # 0 = absent
|
|
115
|
+
else:
|
|
116
|
+
pos_dom = range(1, n + 1)
|
|
117
|
+
|
|
118
|
+
pos_var = Var(dom=pos_dom, id=f"seqpos{sequence._id}_{interval._id}")
|
|
119
|
+
positions.append(pos_var)
|
|
120
|
+
|
|
121
|
+
if interval.optional:
|
|
122
|
+
# Presence <-> position != 0 (bidirectional channeling)
|
|
123
|
+
# present=1 => pos != 0, and pos != 0 => present=1
|
|
124
|
+
satisfy(
|
|
125
|
+
Node.build(
|
|
126
|
+
TypeNode.OR,
|
|
127
|
+
Node.build(TypeNode.EQ, pres, 1),
|
|
128
|
+
Node.build(TypeNode.EQ, pos_var, 0),
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
satisfy(
|
|
132
|
+
Node.build(
|
|
133
|
+
TypeNode.OR,
|
|
134
|
+
Node.build(TypeNode.NE, pos_var, 0),
|
|
135
|
+
Node.build(TypeNode.EQ, pres, 0),
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Count of present intervals
|
|
140
|
+
count_var = Var(dom=range(0, n + 1), id=f"seqcount{sequence._id}")
|
|
141
|
+
if len(presences) == 1:
|
|
142
|
+
sum_presences = presences[0]
|
|
143
|
+
else:
|
|
144
|
+
sum_presences = Node.build(TypeNode.ADD, *presences)
|
|
145
|
+
satisfy(Node.build(TypeNode.EQ, count_var, sum_presences))
|
|
146
|
+
|
|
147
|
+
# Present intervals must occupy positions 1..count_var (no gaps)
|
|
148
|
+
for interval, pos_var, pres in zip(intervals, positions, presences):
|
|
149
|
+
if interval.optional:
|
|
150
|
+
satisfy(
|
|
151
|
+
Node.build(
|
|
152
|
+
TypeNode.OR,
|
|
153
|
+
Node.build(TypeNode.EQ, pres, 0),
|
|
154
|
+
Node.build(TypeNode.LE, pos_var, count_var),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
satisfy(Node.build(TypeNode.LE, pos_var, count_var))
|
|
159
|
+
|
|
160
|
+
# All-different positions for present intervals
|
|
161
|
+
# Use native AllDifferent constraint instead of O(n²) pairwise decomposition
|
|
162
|
+
# excepting=0 allows multiple intervals to have position 0 (absent)
|
|
163
|
+
if has_optional:
|
|
164
|
+
# With optional intervals, use AllDifferent with excepting=0
|
|
165
|
+
# This allows multiple absent intervals (position=0) while ensuring
|
|
166
|
+
# all present intervals have unique positions
|
|
167
|
+
satisfy(AllDifferent(positions, excepting=0))
|
|
168
|
+
else:
|
|
169
|
+
# All mandatory: simple AllDifferent
|
|
170
|
+
satisfy(AllDifferent(positions))
|
|
171
|
+
|
|
172
|
+
# Link temporal order to positions
|
|
173
|
+
# Pre-compute start and end expressions once
|
|
174
|
+
starts = [start_var(interval) for interval in intervals]
|
|
175
|
+
ends: list[Any] = []
|
|
176
|
+
for interval, start in zip(intervals, starts):
|
|
177
|
+
length = length_value(interval)
|
|
178
|
+
if isinstance(length, int):
|
|
179
|
+
end = Node.build(TypeNode.ADD, start, length) if length > 0 else start
|
|
180
|
+
else:
|
|
181
|
+
end = Node.build(TypeNode.ADD, start, length)
|
|
182
|
+
ends.append(end)
|
|
183
|
+
|
|
184
|
+
# Temporal ordering constraint: if i ends before j starts, then pos[i] < pos[j]
|
|
185
|
+
# Formulated as: (start[j] < end[i]) OR (pos[i] < pos[j]) OR absent conditions
|
|
186
|
+
for i in range(n):
|
|
187
|
+
for j in range(n):
|
|
188
|
+
if i == j:
|
|
189
|
+
continue
|
|
190
|
+
disjuncts = [
|
|
191
|
+
Node.build(TypeNode.LT, starts[j], ends[i]),
|
|
192
|
+
Node.build(TypeNode.LT, positions[i], positions[j]),
|
|
193
|
+
]
|
|
194
|
+
if intervals[i].optional:
|
|
195
|
+
disjuncts.insert(0, Node.build(TypeNode.EQ, presences[i], 0))
|
|
196
|
+
if intervals[j].optional:
|
|
197
|
+
disjuncts.insert(0, Node.build(TypeNode.EQ, presences[j], 0))
|
|
198
|
+
satisfy(Node.build(TypeNode.OR, *disjuncts))
|
|
199
|
+
|
|
200
|
+
_sequence_position_vars[sequence._id] = positions
|
|
201
|
+
_sequence_present_count_vars[sequence._id] = count_var
|
|
202
|
+
return positions, count_var
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# =============================================================================
|
|
206
|
+
# Next Interval Accessors
|
|
207
|
+
# =============================================================================
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def start_of_next(
|
|
211
|
+
sequence,
|
|
212
|
+
interval: IntervalVar,
|
|
213
|
+
last_value: int = 0,
|
|
214
|
+
absent_value: int = 0,
|
|
215
|
+
) -> IntervalExpr:
|
|
216
|
+
"""
|
|
217
|
+
Return the start time of the next interval in the sequence.
|
|
218
|
+
|
|
219
|
+
If the given interval is last in the sequence, returns last_value.
|
|
220
|
+
If the given interval is absent, returns absent_value.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
sequence: SequenceVar or list of IntervalVar.
|
|
224
|
+
interval: The reference interval.
|
|
225
|
+
last_value: Value when interval is last (default: 0).
|
|
226
|
+
absent_value: Value when interval is absent (default: 0).
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
An expression representing the start of the next interval.
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
>>> seq = SequenceVar(intervals=[t1, t2, t3], name="machine")
|
|
233
|
+
>>> expr = start_of_next(seq, t1) # Returns start of t2 (or next in order)
|
|
234
|
+
"""
|
|
235
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
236
|
+
return IntervalExpr(
|
|
237
|
+
expr_type=ExprType.START_OF, # Placeholder - actual logic in evaluation
|
|
238
|
+
interval=interval,
|
|
239
|
+
absent_value=absent_value,
|
|
240
|
+
value=last_value, # Store last_value for evaluation
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def end_of_next(
|
|
245
|
+
sequence,
|
|
246
|
+
interval: IntervalVar,
|
|
247
|
+
last_value: int = 0,
|
|
248
|
+
absent_value: int = 0,
|
|
249
|
+
) -> IntervalExpr:
|
|
250
|
+
"""
|
|
251
|
+
Return the end time of the next interval in the sequence.
|
|
252
|
+
|
|
253
|
+
If the given interval is last in the sequence, returns last_value.
|
|
254
|
+
If the given interval is absent, returns absent_value.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
sequence: SequenceVar or list of IntervalVar.
|
|
258
|
+
interval: The reference interval.
|
|
259
|
+
last_value: Value when interval is last (default: 0).
|
|
260
|
+
absent_value: Value when interval is absent (default: 0).
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
An expression representing the end of the next interval.
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
>>> seq = SequenceVar(intervals=[t1, t2, t3], name="machine")
|
|
267
|
+
>>> expr = end_of_next(seq, t1) # Returns end of next interval after t1
|
|
268
|
+
"""
|
|
269
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
270
|
+
return IntervalExpr(
|
|
271
|
+
expr_type=ExprType.END_OF,
|
|
272
|
+
interval=interval,
|
|
273
|
+
absent_value=absent_value,
|
|
274
|
+
value=last_value,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def size_of_next(
|
|
279
|
+
sequence,
|
|
280
|
+
interval: IntervalVar,
|
|
281
|
+
last_value: int = 0,
|
|
282
|
+
absent_value: int = 0,
|
|
283
|
+
) -> IntervalExpr:
|
|
284
|
+
"""
|
|
285
|
+
Return the size (duration) of the next interval in the sequence.
|
|
286
|
+
|
|
287
|
+
If the given interval is last in the sequence, returns last_value.
|
|
288
|
+
If the given interval is absent, returns absent_value.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
sequence: SequenceVar or list of IntervalVar.
|
|
292
|
+
interval: The reference interval.
|
|
293
|
+
last_value: Value when interval is last (default: 0).
|
|
294
|
+
absent_value: Value when interval is absent (default: 0).
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
An expression representing the size of the next interval.
|
|
298
|
+
"""
|
|
299
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
300
|
+
return IntervalExpr(
|
|
301
|
+
expr_type=ExprType.SIZE_OF,
|
|
302
|
+
interval=interval,
|
|
303
|
+
absent_value=absent_value,
|
|
304
|
+
value=last_value,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def length_of_next(
|
|
309
|
+
sequence,
|
|
310
|
+
interval: IntervalVar,
|
|
311
|
+
last_value: int = 0,
|
|
312
|
+
absent_value: int = 0,
|
|
313
|
+
) -> IntervalExpr:
|
|
314
|
+
"""
|
|
315
|
+
Return the length of the next interval in the sequence.
|
|
316
|
+
|
|
317
|
+
If the given interval is last in the sequence, returns last_value.
|
|
318
|
+
If the given interval is absent, returns absent_value.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
sequence: SequenceVar or list of IntervalVar.
|
|
322
|
+
interval: The reference interval.
|
|
323
|
+
last_value: Value when interval is last (default: 0).
|
|
324
|
+
absent_value: Value when interval is absent (default: 0).
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
An expression representing the length of the next interval.
|
|
328
|
+
"""
|
|
329
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
330
|
+
return IntervalExpr(
|
|
331
|
+
expr_type=ExprType.LENGTH_OF,
|
|
332
|
+
interval=interval,
|
|
333
|
+
absent_value=absent_value,
|
|
334
|
+
value=last_value,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def next_arg(
|
|
339
|
+
sequence,
|
|
340
|
+
interval: IntervalVar,
|
|
341
|
+
last_value: int = 0,
|
|
342
|
+
absent_value: int = 0,
|
|
343
|
+
) -> Any:
|
|
344
|
+
"""
|
|
345
|
+
Return a variable representing the ID of the next interval in the sequence.
|
|
346
|
+
|
|
347
|
+
Similar to pycsp3's maximum_arg pattern, this returns the argument (ID)
|
|
348
|
+
of the successor interval. Used for building transition-based objectives.
|
|
349
|
+
|
|
350
|
+
Requires a SequenceVar with types (IDs) defined.
|
|
351
|
+
|
|
352
|
+
Semantics:
|
|
353
|
+
- If the interval is present and not last: returns the ID of the next interval
|
|
354
|
+
- If the interval is present and last: returns last_value
|
|
355
|
+
- If the interval is absent: returns absent_value
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
sequence: SequenceVar with types (IDs) defined.
|
|
359
|
+
interval: The reference interval.
|
|
360
|
+
last_value: Value when interval is last (default: 0).
|
|
361
|
+
absent_value: Value when interval is absent (default: 0).
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
A pycsp3 variable representing the ID of the next interval.
|
|
365
|
+
This variable can be used to index into arrays/matrices.
|
|
366
|
+
|
|
367
|
+
Raises:
|
|
368
|
+
TypeError: If sequence is not a SequenceVar or has no types.
|
|
369
|
+
|
|
370
|
+
Example:
|
|
371
|
+
>>> seq = SequenceVar(intervals=[t1, t2, t3], types=[0, 1, 2], name="machine")
|
|
372
|
+
>>> next_id = next_arg(seq, t1, last_value=3, absent_value=4)
|
|
373
|
+
>>> # If t2 follows t1 in the schedule, next_id == 1
|
|
374
|
+
>>> # If t1 is last, next_id == 3
|
|
375
|
+
>>> # If t1 is absent, next_id == 4
|
|
376
|
+
>>>
|
|
377
|
+
>>> # Use with ElementMatrix for distance objective:
|
|
378
|
+
>>> M = ElementMatrix(travel_times, last_value=depot_return)
|
|
379
|
+
>>> cost = M[current_id, next_id]
|
|
380
|
+
"""
|
|
381
|
+
from pycsp3_scheduling.variables.sequence import SequenceVar
|
|
382
|
+
|
|
383
|
+
if not isinstance(sequence, SequenceVar):
|
|
384
|
+
raise TypeError("next_arg requires a SequenceVar")
|
|
385
|
+
if not sequence.has_types:
|
|
386
|
+
raise ValueError("next_arg requires sequence with types defined")
|
|
387
|
+
|
|
388
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
389
|
+
|
|
390
|
+
# Check cache
|
|
391
|
+
cache_key = (sequence._id, interval._id)
|
|
392
|
+
if cache_key in _next_arg_vars:
|
|
393
|
+
return _next_arg_vars[cache_key]
|
|
394
|
+
|
|
395
|
+
# Build the next_arg variable and constraints
|
|
396
|
+
var = _build_next_arg_var(sequence, interval, idx, last_value, absent_value)
|
|
397
|
+
_next_arg_vars[cache_key] = var
|
|
398
|
+
return var
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# Backward compatibility alias
|
|
402
|
+
type_of_next = next_arg
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _build_next_arg_var(
|
|
406
|
+
sequence: SequenceVar,
|
|
407
|
+
interval: IntervalVar,
|
|
408
|
+
idx: int,
|
|
409
|
+
last_value: int,
|
|
410
|
+
absent_value: int,
|
|
411
|
+
) -> Any:
|
|
412
|
+
"""
|
|
413
|
+
Build a pycsp3 variable for next_arg with appropriate constraints.
|
|
414
|
+
|
|
415
|
+
Successor-variable encoding using position variables:
|
|
416
|
+
- Each interval has a position (0 if absent, otherwise 1..m).
|
|
417
|
+
- The successor index is the interval at position +1, or a last/absent marker.
|
|
418
|
+
- Use an element constraint to map successor index to ID values.
|
|
419
|
+
"""
|
|
420
|
+
try:
|
|
421
|
+
from pycsp3 import Var, satisfy
|
|
422
|
+
from pycsp3.classes.nodes import Node, TypeNode
|
|
423
|
+
except ImportError:
|
|
424
|
+
raise ImportError("pycsp3 is required for next_arg")
|
|
425
|
+
|
|
426
|
+
from pycsp3_scheduling.constraints._pycsp3 import (
|
|
427
|
+
presence_var,
|
|
428
|
+
)
|
|
429
|
+
from pycsp3_scheduling.expressions.element import element
|
|
430
|
+
|
|
431
|
+
intervals = sequence.intervals
|
|
432
|
+
types = sequence.types
|
|
433
|
+
n = len(intervals)
|
|
434
|
+
|
|
435
|
+
# Build extended types array: [type_0, ..., type_{n-1}, last_value, absent_value]
|
|
436
|
+
last_idx = n
|
|
437
|
+
absent_idx = n + 1
|
|
438
|
+
types_extended = list(types) + [last_value, absent_value]
|
|
439
|
+
|
|
440
|
+
# Successor index variable (interval index, last, absent)
|
|
441
|
+
next_idx_domain = set(range(n)) - {idx}
|
|
442
|
+
next_idx_domain.add(last_idx)
|
|
443
|
+
if interval.optional:
|
|
444
|
+
next_idx_domain.add(absent_idx)
|
|
445
|
+
|
|
446
|
+
next_idx = Var(dom=next_idx_domain, id=f"succ{sequence._id}_{interval._id}")
|
|
447
|
+
|
|
448
|
+
# Result variable mapped from successor index
|
|
449
|
+
result_domain = set(types_extended[j] for j in next_idx_domain)
|
|
450
|
+
result_var = Var(dom=result_domain, id=f"tonext{sequence._id}_{interval._id}")
|
|
451
|
+
satisfy(result_var == element(types_extended, next_idx))
|
|
452
|
+
|
|
453
|
+
# Position-based successor channeling
|
|
454
|
+
positions, count_var = _ensure_sequence_positions(sequence)
|
|
455
|
+
pos_i = positions[idx]
|
|
456
|
+
pres_i = presence_var(interval) if interval.optional else 1
|
|
457
|
+
pos_i_plus_1 = Node.build(TypeNode.ADD, pos_i, 1)
|
|
458
|
+
|
|
459
|
+
if interval.optional:
|
|
460
|
+
# Absent <-> successor is absent marker
|
|
461
|
+
satisfy(
|
|
462
|
+
Node.build(
|
|
463
|
+
TypeNode.OR,
|
|
464
|
+
Node.build(TypeNode.EQ, pres_i, 1),
|
|
465
|
+
Node.build(TypeNode.EQ, next_idx, absent_idx),
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
satisfy(
|
|
469
|
+
Node.build(
|
|
470
|
+
TypeNode.OR,
|
|
471
|
+
Node.build(TypeNode.NE, next_idx, absent_idx),
|
|
472
|
+
Node.build(TypeNode.EQ, pres_i, 0),
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Last position <-> successor is last marker
|
|
477
|
+
if interval.optional:
|
|
478
|
+
satisfy(
|
|
479
|
+
Node.build(
|
|
480
|
+
TypeNode.OR,
|
|
481
|
+
Node.build(TypeNode.EQ, pres_i, 0),
|
|
482
|
+
Node.build(TypeNode.NE, pos_i, count_var),
|
|
483
|
+
Node.build(TypeNode.EQ, next_idx, last_idx),
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
satisfy(
|
|
487
|
+
Node.build(
|
|
488
|
+
TypeNode.OR,
|
|
489
|
+
Node.build(TypeNode.NE, next_idx, last_idx),
|
|
490
|
+
Node.build(TypeNode.EQ, pres_i, 1),
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
else:
|
|
494
|
+
satisfy(
|
|
495
|
+
Node.build(
|
|
496
|
+
TypeNode.OR,
|
|
497
|
+
Node.build(TypeNode.NE, pos_i, count_var),
|
|
498
|
+
Node.build(TypeNode.EQ, next_idx, last_idx),
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
satisfy(
|
|
503
|
+
Node.build(
|
|
504
|
+
TypeNode.OR,
|
|
505
|
+
Node.build(TypeNode.NE, next_idx, last_idx),
|
|
506
|
+
Node.build(TypeNode.EQ, pos_i, count_var),
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Successor mapping: pos_j == pos_i + 1 <-> next_idx = j
|
|
511
|
+
for j in range(n):
|
|
512
|
+
if j == idx:
|
|
513
|
+
continue
|
|
514
|
+
pos_j = positions[j]
|
|
515
|
+
|
|
516
|
+
# next_idx = j => pos_j = pos_i + 1
|
|
517
|
+
satisfy(
|
|
518
|
+
Node.build(
|
|
519
|
+
TypeNode.OR,
|
|
520
|
+
Node.build(TypeNode.NE, next_idx, j),
|
|
521
|
+
Node.build(TypeNode.EQ, pos_j, pos_i_plus_1),
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# If i is present and pos_j = pos_i + 1, then next_idx = j
|
|
526
|
+
if interval.optional:
|
|
527
|
+
satisfy(
|
|
528
|
+
Node.build(
|
|
529
|
+
TypeNode.OR,
|
|
530
|
+
Node.build(TypeNode.EQ, pres_i, 0),
|
|
531
|
+
Node.build(TypeNode.NE, pos_j, pos_i_plus_1),
|
|
532
|
+
Node.build(TypeNode.EQ, next_idx, j),
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
else:
|
|
536
|
+
satisfy(
|
|
537
|
+
Node.build(
|
|
538
|
+
TypeNode.OR,
|
|
539
|
+
Node.build(TypeNode.NE, pos_j, pos_i_plus_1),
|
|
540
|
+
Node.build(TypeNode.EQ, next_idx, j),
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
return result_var
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
# =============================================================================
|
|
548
|
+
# Previous Interval Accessors
|
|
549
|
+
# =============================================================================
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def start_of_prev(
|
|
553
|
+
sequence,
|
|
554
|
+
interval: IntervalVar,
|
|
555
|
+
first_value: int = 0,
|
|
556
|
+
absent_value: int = 0,
|
|
557
|
+
) -> IntervalExpr:
|
|
558
|
+
"""
|
|
559
|
+
Return the start time of the previous interval in the sequence.
|
|
560
|
+
|
|
561
|
+
If the given interval is first in the sequence, returns first_value.
|
|
562
|
+
If the given interval is absent, returns absent_value.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
sequence: SequenceVar or list of IntervalVar.
|
|
566
|
+
interval: The reference interval.
|
|
567
|
+
first_value: Value when interval is first (default: 0).
|
|
568
|
+
absent_value: Value when interval is absent (default: 0).
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
An expression representing the start of the previous interval.
|
|
572
|
+
|
|
573
|
+
Example:
|
|
574
|
+
>>> seq = SequenceVar(intervals=[t1, t2, t3], name="machine")
|
|
575
|
+
>>> expr = start_of_prev(seq, t2) # Returns start of t1 (or prev in order)
|
|
576
|
+
"""
|
|
577
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
578
|
+
return IntervalExpr(
|
|
579
|
+
expr_type=ExprType.START_OF,
|
|
580
|
+
interval=interval,
|
|
581
|
+
absent_value=absent_value,
|
|
582
|
+
value=first_value,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def end_of_prev(
|
|
587
|
+
sequence,
|
|
588
|
+
interval: IntervalVar,
|
|
589
|
+
first_value: int = 0,
|
|
590
|
+
absent_value: int = 0,
|
|
591
|
+
) -> IntervalExpr:
|
|
592
|
+
"""
|
|
593
|
+
Return the end time of the previous interval in the sequence.
|
|
594
|
+
|
|
595
|
+
If the given interval is first in the sequence, returns first_value.
|
|
596
|
+
If the given interval is absent, returns absent_value.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
sequence: SequenceVar or list of IntervalVar.
|
|
600
|
+
interval: The reference interval.
|
|
601
|
+
first_value: Value when interval is first (default: 0).
|
|
602
|
+
absent_value: Value when interval is absent (default: 0).
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
An expression representing the end of the previous interval.
|
|
606
|
+
"""
|
|
607
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
608
|
+
return IntervalExpr(
|
|
609
|
+
expr_type=ExprType.END_OF,
|
|
610
|
+
interval=interval,
|
|
611
|
+
absent_value=absent_value,
|
|
612
|
+
value=first_value,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def size_of_prev(
|
|
617
|
+
sequence,
|
|
618
|
+
interval: IntervalVar,
|
|
619
|
+
first_value: int = 0,
|
|
620
|
+
absent_value: int = 0,
|
|
621
|
+
) -> IntervalExpr:
|
|
622
|
+
"""
|
|
623
|
+
Return the size (duration) of the previous interval in the sequence.
|
|
624
|
+
|
|
625
|
+
If the given interval is first in the sequence, returns first_value.
|
|
626
|
+
If the given interval is absent, returns absent_value.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
sequence: SequenceVar or list of IntervalVar.
|
|
630
|
+
interval: The reference interval.
|
|
631
|
+
first_value: Value when interval is first (default: 0).
|
|
632
|
+
absent_value: Value when interval is absent (default: 0).
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
An expression representing the size of the previous interval.
|
|
636
|
+
"""
|
|
637
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
638
|
+
return IntervalExpr(
|
|
639
|
+
expr_type=ExprType.SIZE_OF,
|
|
640
|
+
interval=interval,
|
|
641
|
+
absent_value=absent_value,
|
|
642
|
+
value=first_value,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def length_of_prev(
|
|
647
|
+
sequence,
|
|
648
|
+
interval: IntervalVar,
|
|
649
|
+
first_value: int = 0,
|
|
650
|
+
absent_value: int = 0,
|
|
651
|
+
) -> IntervalExpr:
|
|
652
|
+
"""
|
|
653
|
+
Return the length of the previous interval in the sequence.
|
|
654
|
+
|
|
655
|
+
If the given interval is first in the sequence, returns first_value.
|
|
656
|
+
If the given interval is absent, returns absent_value.
|
|
657
|
+
|
|
658
|
+
Args:
|
|
659
|
+
sequence: SequenceVar or list of IntervalVar.
|
|
660
|
+
interval: The reference interval.
|
|
661
|
+
first_value: Value when interval is first (default: 0).
|
|
662
|
+
absent_value: Value when interval is absent (default: 0).
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
An expression representing the length of the previous interval.
|
|
666
|
+
"""
|
|
667
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
668
|
+
return IntervalExpr(
|
|
669
|
+
expr_type=ExprType.LENGTH_OF,
|
|
670
|
+
interval=interval,
|
|
671
|
+
absent_value=absent_value,
|
|
672
|
+
value=first_value,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def prev_arg(
|
|
677
|
+
sequence,
|
|
678
|
+
interval: IntervalVar,
|
|
679
|
+
first_value: int = 0,
|
|
680
|
+
absent_value: int = 0,
|
|
681
|
+
) -> Any:
|
|
682
|
+
"""
|
|
683
|
+
Return a variable representing the ID of the previous interval in the sequence.
|
|
684
|
+
|
|
685
|
+
Similar to pycsp3's maximum_arg pattern, this returns the argument (ID)
|
|
686
|
+
of the predecessor interval. Used for building transition-based objectives.
|
|
687
|
+
|
|
688
|
+
Requires a SequenceVar with types (IDs) defined.
|
|
689
|
+
|
|
690
|
+
Semantics:
|
|
691
|
+
- If the interval is present and not first: returns the ID of the previous interval
|
|
692
|
+
- If the interval is present and first: returns first_value
|
|
693
|
+
- If the interval is absent: returns absent_value
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
sequence: SequenceVar with types (IDs) defined.
|
|
697
|
+
interval: The reference interval.
|
|
698
|
+
first_value: Value when interval is first (default: 0).
|
|
699
|
+
absent_value: Value when interval is absent (default: 0).
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
A pycsp3 variable representing the ID of the previous interval.
|
|
703
|
+
|
|
704
|
+
Raises:
|
|
705
|
+
TypeError: If sequence is not a SequenceVar or has no types.
|
|
706
|
+
"""
|
|
707
|
+
from pycsp3_scheduling.variables.sequence import SequenceVar
|
|
708
|
+
|
|
709
|
+
if not isinstance(sequence, SequenceVar):
|
|
710
|
+
raise TypeError("prev_arg requires a SequenceVar")
|
|
711
|
+
if not sequence.has_types:
|
|
712
|
+
raise ValueError("prev_arg requires sequence with types defined")
|
|
713
|
+
|
|
714
|
+
intervals, idx = _validate_sequence_and_interval(sequence, interval)
|
|
715
|
+
|
|
716
|
+
# Check cache
|
|
717
|
+
cache_key = (sequence._id, interval._id)
|
|
718
|
+
if cache_key in _prev_arg_vars:
|
|
719
|
+
return _prev_arg_vars[cache_key]
|
|
720
|
+
|
|
721
|
+
# Build the prev_arg variable and constraints
|
|
722
|
+
var = _build_prev_arg_var(sequence, interval, idx, first_value, absent_value)
|
|
723
|
+
_prev_arg_vars[cache_key] = var
|
|
724
|
+
return var
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
# Backward compatibility alias
|
|
728
|
+
type_of_prev = prev_arg
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _build_prev_arg_var(
|
|
732
|
+
sequence: SequenceVar,
|
|
733
|
+
interval: IntervalVar,
|
|
734
|
+
idx: int,
|
|
735
|
+
first_value: int,
|
|
736
|
+
absent_value: int,
|
|
737
|
+
) -> Any:
|
|
738
|
+
"""
|
|
739
|
+
Build a pycsp3 variable for prev_arg with appropriate constraints.
|
|
740
|
+
"""
|
|
741
|
+
try:
|
|
742
|
+
from pycsp3 import Var, satisfy
|
|
743
|
+
from pycsp3.classes.nodes import Node, TypeNode
|
|
744
|
+
except ImportError:
|
|
745
|
+
raise ImportError("pycsp3 is required for prev_arg")
|
|
746
|
+
|
|
747
|
+
from pycsp3_scheduling.constraints._pycsp3 import (
|
|
748
|
+
presence_var,
|
|
749
|
+
)
|
|
750
|
+
from pycsp3_scheduling.expressions.element import element
|
|
751
|
+
|
|
752
|
+
intervals = sequence.intervals
|
|
753
|
+
types = sequence.types
|
|
754
|
+
n = len(intervals)
|
|
755
|
+
|
|
756
|
+
# Build extended types array: [type_0, ..., type_{n-1}, first_value, absent_value]
|
|
757
|
+
first_idx = n
|
|
758
|
+
absent_idx = n + 1
|
|
759
|
+
types_extended = list(types) + [first_value, absent_value]
|
|
760
|
+
|
|
761
|
+
# Predecessor index variable (interval index, first, absent)
|
|
762
|
+
prev_idx_domain = set(range(n)) - {idx}
|
|
763
|
+
prev_idx_domain.add(first_idx)
|
|
764
|
+
if interval.optional:
|
|
765
|
+
prev_idx_domain.add(absent_idx)
|
|
766
|
+
|
|
767
|
+
prev_idx = Var(dom=prev_idx_domain, id=f"pred{sequence._id}_{interval._id}")
|
|
768
|
+
|
|
769
|
+
# Result variable mapped from predecessor index
|
|
770
|
+
result_domain = set(types_extended[j] for j in prev_idx_domain)
|
|
771
|
+
result_var = Var(dom=result_domain, id=f"toprev{sequence._id}_{interval._id}")
|
|
772
|
+
satisfy(result_var == element(types_extended, prev_idx))
|
|
773
|
+
|
|
774
|
+
# Position-based predecessor channeling
|
|
775
|
+
positions, _count_var = _ensure_sequence_positions(sequence)
|
|
776
|
+
pos_i = positions[idx]
|
|
777
|
+
pres_i = presence_var(interval) if interval.optional else 1
|
|
778
|
+
pos_i_minus_1 = Node.build(TypeNode.ADD, pos_i, -1)
|
|
779
|
+
|
|
780
|
+
if interval.optional:
|
|
781
|
+
# Absent <-> predecessor is absent marker
|
|
782
|
+
satisfy(
|
|
783
|
+
Node.build(
|
|
784
|
+
TypeNode.OR,
|
|
785
|
+
Node.build(TypeNode.EQ, pres_i, 1),
|
|
786
|
+
Node.build(TypeNode.EQ, prev_idx, absent_idx),
|
|
787
|
+
)
|
|
788
|
+
)
|
|
789
|
+
satisfy(
|
|
790
|
+
Node.build(
|
|
791
|
+
TypeNode.OR,
|
|
792
|
+
Node.build(TypeNode.NE, prev_idx, absent_idx),
|
|
793
|
+
Node.build(TypeNode.EQ, pres_i, 0),
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
# First position <-> predecessor is first marker
|
|
798
|
+
if interval.optional:
|
|
799
|
+
satisfy(
|
|
800
|
+
Node.build(
|
|
801
|
+
TypeNode.OR,
|
|
802
|
+
Node.build(TypeNode.EQ, pres_i, 0),
|
|
803
|
+
Node.build(TypeNode.NE, pos_i, 1),
|
|
804
|
+
Node.build(TypeNode.EQ, prev_idx, first_idx),
|
|
805
|
+
)
|
|
806
|
+
)
|
|
807
|
+
satisfy(
|
|
808
|
+
Node.build(
|
|
809
|
+
TypeNode.OR,
|
|
810
|
+
Node.build(TypeNode.NE, prev_idx, first_idx),
|
|
811
|
+
Node.build(TypeNode.EQ, pres_i, 1),
|
|
812
|
+
)
|
|
813
|
+
)
|
|
814
|
+
else:
|
|
815
|
+
satisfy(
|
|
816
|
+
Node.build(
|
|
817
|
+
TypeNode.OR,
|
|
818
|
+
Node.build(TypeNode.NE, pos_i, 1),
|
|
819
|
+
Node.build(TypeNode.EQ, prev_idx, first_idx),
|
|
820
|
+
)
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
satisfy(
|
|
824
|
+
Node.build(
|
|
825
|
+
TypeNode.OR,
|
|
826
|
+
Node.build(TypeNode.NE, prev_idx, first_idx),
|
|
827
|
+
Node.build(TypeNode.EQ, pos_i, 1),
|
|
828
|
+
)
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
# Predecessor mapping: pos_j == pos_i - 1 <-> prev_idx = j
|
|
832
|
+
for j in range(n):
|
|
833
|
+
if j == idx:
|
|
834
|
+
continue
|
|
835
|
+
pos_j = positions[j]
|
|
836
|
+
|
|
837
|
+
# prev_idx = j => pos_j = pos_i - 1
|
|
838
|
+
satisfy(
|
|
839
|
+
Node.build(
|
|
840
|
+
TypeNode.OR,
|
|
841
|
+
Node.build(TypeNode.NE, prev_idx, j),
|
|
842
|
+
Node.build(TypeNode.EQ, pos_j, pos_i_minus_1),
|
|
843
|
+
)
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
# If i is present and pos_j = pos_i - 1, then prev_idx = j
|
|
847
|
+
if interval.optional:
|
|
848
|
+
satisfy(
|
|
849
|
+
Node.build(
|
|
850
|
+
TypeNode.OR,
|
|
851
|
+
Node.build(TypeNode.EQ, pres_i, 0),
|
|
852
|
+
Node.build(TypeNode.NE, pos_j, pos_i_minus_1),
|
|
853
|
+
Node.build(TypeNode.EQ, prev_idx, j),
|
|
854
|
+
)
|
|
855
|
+
)
|
|
856
|
+
else:
|
|
857
|
+
satisfy(
|
|
858
|
+
Node.build(
|
|
859
|
+
TypeNode.OR,
|
|
860
|
+
Node.build(TypeNode.NE, pos_j, pos_i_minus_1),
|
|
861
|
+
Node.build(TypeNode.EQ, prev_idx, j),
|
|
862
|
+
)
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
return result_var
|