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,450 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interval variable implementation for scheduling models.
|
|
3
|
+
|
|
4
|
+
An interval variable represents a task/activity with:
|
|
5
|
+
- start: start time (can be a range [min, max])
|
|
6
|
+
- end: end time (can be a range [min, max])
|
|
7
|
+
- size: duration/size (can be a range [min, max])
|
|
8
|
+
- length: length (can differ from size with intensity)
|
|
9
|
+
- intensity: stepwise function relating size to length
|
|
10
|
+
- granularity: scale for the intensity function
|
|
11
|
+
- optional: whether the interval can be absent
|
|
12
|
+
- name: identifier for the variable
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import warnings
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Union
|
|
21
|
+
|
|
22
|
+
# Type aliases for bounds and stepwise functions
|
|
23
|
+
Bound = Union[int, tuple[int, int]]
|
|
24
|
+
Step = tuple[int, int]
|
|
25
|
+
|
|
26
|
+
# Constants for default bounds
|
|
27
|
+
INTERVAL_MIN = 0
|
|
28
|
+
INTERVAL_MAX = 2**30 - 1 # Large but not overflow-prone
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class IntervalVar:
|
|
33
|
+
"""
|
|
34
|
+
Represents an interval variable for scheduling.
|
|
35
|
+
|
|
36
|
+
An interval variable represents a task or activity with a start time,
|
|
37
|
+
end time, and duration. The interval can be optional (may be absent
|
|
38
|
+
from the solution).
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
name: Unique identifier for this interval variable.
|
|
42
|
+
start: Start time bound as int or (min, max) tuple.
|
|
43
|
+
end: End time bound as int or (min, max) tuple.
|
|
44
|
+
size: Duration/size bound as int or (min, max) tuple.
|
|
45
|
+
length: Length bound (can differ from size with intensity functions).
|
|
46
|
+
intensity: Stepwise function relating size to length.
|
|
47
|
+
granularity: Scale for the intensity function.
|
|
48
|
+
optional: If True, the interval may be absent from the solution.
|
|
49
|
+
_id: Internal unique identifier.
|
|
50
|
+
|
|
51
|
+
Note:
|
|
52
|
+
When using intensity functions, you should explicitly set ``length`` bounds.
|
|
53
|
+
At lower intensity values, more elapsed time (length) is needed to complete
|
|
54
|
+
the same amount of work (size). For example, at 50% intensity, a task with
|
|
55
|
+
size=10 needs length=20. If length is not set, it defaults to size, which
|
|
56
|
+
may be too restrictive. A warning is issued in this case.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> task = IntervalVar(size=10, name="task1")
|
|
60
|
+
>>> optional_task = IntervalVar(size=(5, 20), optional=True, name="opt")
|
|
61
|
+
>>> bounded_task = IntervalVar(start=(0, 100), end=(10, 200), size=15)
|
|
62
|
+
>>> # With intensity: explicitly set length bounds to allow larger values
|
|
63
|
+
>>> intensity = [(INTERVAL_MIN, 100), (10, 50)] # 100% until t=10, then 50%
|
|
64
|
+
>>> variable_rate = IntervalVar(
|
|
65
|
+
... size=10,
|
|
66
|
+
... length=(10, 30), # Allow length up to 30 for lower intensity
|
|
67
|
+
... intensity=intensity,
|
|
68
|
+
... granularity=100,
|
|
69
|
+
... name="variable_rate"
|
|
70
|
+
... )
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
name: str | None = None
|
|
74
|
+
start: Bound = field(default_factory=lambda: (INTERVAL_MIN, INTERVAL_MAX))
|
|
75
|
+
end: Bound = field(default_factory=lambda: (INTERVAL_MIN, INTERVAL_MAX))
|
|
76
|
+
size: Bound = field(default_factory=lambda: (0, INTERVAL_MAX))
|
|
77
|
+
length: Bound | None = None
|
|
78
|
+
intensity: list[Step] | None = None
|
|
79
|
+
granularity: int = 1
|
|
80
|
+
optional: bool = False
|
|
81
|
+
_id: int = field(default=-1, repr=False, compare=False)
|
|
82
|
+
|
|
83
|
+
# Class-level counter for unique IDs
|
|
84
|
+
_counter: int = field(default=0, init=False, repr=False, compare=False)
|
|
85
|
+
|
|
86
|
+
def __post_init__(self) -> None:
|
|
87
|
+
"""Normalize bounds and assign unique ID."""
|
|
88
|
+
# Normalize bounds to tuples
|
|
89
|
+
self.start = self._normalize_bound(self.start)
|
|
90
|
+
self.end = self._normalize_bound(self.end)
|
|
91
|
+
self.size = self._normalize_bound(self.size)
|
|
92
|
+
# Track if length was explicitly provided
|
|
93
|
+
length_was_explicit = self.length is not None
|
|
94
|
+
if self.length is not None:
|
|
95
|
+
self.length = self._normalize_bound(self.length)
|
|
96
|
+
else:
|
|
97
|
+
# Length defaults to size if not specified
|
|
98
|
+
self.length = self.size
|
|
99
|
+
if self.intensity is not None:
|
|
100
|
+
self.intensity = self._normalize_intensity(self.intensity)
|
|
101
|
+
# Warn if intensity is set but length was not explicitly provided
|
|
102
|
+
# With intensity, the required length often exceeds size (at low intensity)
|
|
103
|
+
if not length_was_explicit and self.intensity:
|
|
104
|
+
warnings.warn(
|
|
105
|
+
f"IntervalVar '{self.name or 'unnamed'}' has intensity but no explicit "
|
|
106
|
+
f"length bounds. With variable intensity, length may need to exceed size. "
|
|
107
|
+
f"Consider setting length=(min, max) to allow larger values. "
|
|
108
|
+
f"Current length defaults to size={self.size}.",
|
|
109
|
+
UserWarning,
|
|
110
|
+
stacklevel=2,
|
|
111
|
+
)
|
|
112
|
+
self._validate_granularity()
|
|
113
|
+
|
|
114
|
+
# Assign unique ID if not set
|
|
115
|
+
if self._id == -1:
|
|
116
|
+
self._id = IntervalVar._get_next_id()
|
|
117
|
+
|
|
118
|
+
# Generate name if not provided
|
|
119
|
+
if self.name is None:
|
|
120
|
+
self.name = f"_interval_{self._id}"
|
|
121
|
+
|
|
122
|
+
# Validate bounds
|
|
123
|
+
self._validate_bounds()
|
|
124
|
+
|
|
125
|
+
# Register for model compilation/interop helpers
|
|
126
|
+
register_interval(self)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _get_next_id() -> int:
|
|
130
|
+
"""Get next unique ID for interval variables."""
|
|
131
|
+
# Use a module-level counter stored as class attribute
|
|
132
|
+
current = getattr(IntervalVar, "_id_counter", 0)
|
|
133
|
+
IntervalVar._id_counter = current + 1
|
|
134
|
+
return current
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _normalize_bound(bound: Bound) -> tuple[int, int]:
|
|
138
|
+
"""Convert bound to (min, max) tuple."""
|
|
139
|
+
if isinstance(bound, int):
|
|
140
|
+
return (bound, bound)
|
|
141
|
+
if isinstance(bound, tuple) and len(bound) == 2:
|
|
142
|
+
return (int(bound[0]), int(bound[1]))
|
|
143
|
+
raise ValueError(f"Invalid bound: {bound}. Expected int or (min, max) tuple.")
|
|
144
|
+
|
|
145
|
+
def _validate_bounds(self) -> None:
|
|
146
|
+
"""Validate that bounds are consistent."""
|
|
147
|
+
start_min, start_max = self.start
|
|
148
|
+
end_min, end_max = self.end
|
|
149
|
+
size_min, size_max = self.size
|
|
150
|
+
|
|
151
|
+
if start_min > start_max:
|
|
152
|
+
raise ValueError(f"Invalid start bounds: min={start_min} > max={start_max}")
|
|
153
|
+
if end_min > end_max:
|
|
154
|
+
raise ValueError(f"Invalid end bounds: min={end_min} > max={end_max}")
|
|
155
|
+
if size_min > size_max:
|
|
156
|
+
raise ValueError(f"Invalid size bounds: min={size_min} > max={size_max}")
|
|
157
|
+
if size_min < 0:
|
|
158
|
+
raise ValueError(f"Size cannot be negative: min={size_min}")
|
|
159
|
+
|
|
160
|
+
# Check feasibility: end >= start + size
|
|
161
|
+
if end_max < start_min + size_min:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"Infeasible bounds: end_max={end_max} < start_min + size_min="
|
|
164
|
+
f"{start_min + size_min}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _normalize_intensity(intensity: Sequence[Step]) -> list[Step] | None:
|
|
169
|
+
"""Normalize stepwise intensity to minimal consecutive steps."""
|
|
170
|
+
if not isinstance(intensity, Sequence) or isinstance(intensity, (str, bytes)):
|
|
171
|
+
raise TypeError("Intensity must be a sequence of (x, value) pairs")
|
|
172
|
+
steps: list[Step] = []
|
|
173
|
+
last_x: int | None = None
|
|
174
|
+
for step in intensity:
|
|
175
|
+
if not isinstance(step, (tuple, list)) or len(step) != 2:
|
|
176
|
+
raise ValueError("Intensity step must be a pair (x, value)")
|
|
177
|
+
x, val = step
|
|
178
|
+
if not isinstance(x, int) or not isinstance(val, int):
|
|
179
|
+
raise TypeError("Intensity step coordinates must be integers")
|
|
180
|
+
x = int(x)
|
|
181
|
+
val = int(val)
|
|
182
|
+
if val < 0:
|
|
183
|
+
raise ValueError("Intensity values must be non-negative")
|
|
184
|
+
if last_x is not None and x <= last_x:
|
|
185
|
+
raise ValueError("Intensity steps must be strictly increasing in x")
|
|
186
|
+
if steps and steps[-1][1] == val:
|
|
187
|
+
last_x = x
|
|
188
|
+
continue
|
|
189
|
+
steps.append((x, val))
|
|
190
|
+
last_x = x
|
|
191
|
+
if steps and steps[0][1] == 0:
|
|
192
|
+
steps.pop(0)
|
|
193
|
+
return steps if steps else None
|
|
194
|
+
|
|
195
|
+
def _validate_granularity(self) -> None:
|
|
196
|
+
"""Validate intensity granularity."""
|
|
197
|
+
if not isinstance(self.granularity, int):
|
|
198
|
+
raise TypeError("granularity must be an int")
|
|
199
|
+
if self.granularity <= 0:
|
|
200
|
+
raise ValueError("granularity must be positive")
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def start_min(self) -> int:
|
|
204
|
+
"""Minimum possible start time."""
|
|
205
|
+
return self.start[0]
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def start_max(self) -> int:
|
|
209
|
+
"""Maximum possible start time."""
|
|
210
|
+
return self.start[1]
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def end_min(self) -> int:
|
|
214
|
+
"""Minimum possible end time."""
|
|
215
|
+
return self.end[0]
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def end_max(self) -> int:
|
|
219
|
+
"""Maximum possible end time."""
|
|
220
|
+
return self.end[1]
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def size_min(self) -> int:
|
|
224
|
+
"""Minimum possible size/duration."""
|
|
225
|
+
return self.size[0]
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def size_max(self) -> int:
|
|
229
|
+
"""Maximum possible size/duration."""
|
|
230
|
+
return self.size[1]
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def length_min(self) -> int:
|
|
234
|
+
"""Minimum possible length."""
|
|
235
|
+
assert self.length is not None
|
|
236
|
+
return self.length[0]
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def length_max(self) -> int:
|
|
240
|
+
"""Maximum possible length."""
|
|
241
|
+
assert self.length is not None
|
|
242
|
+
return self.length[1]
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def is_optional(self) -> bool:
|
|
246
|
+
"""Whether this interval can be absent."""
|
|
247
|
+
return self.optional
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def is_present(self) -> bool:
|
|
251
|
+
"""Whether this interval must be present (not optional)."""
|
|
252
|
+
return not self.optional
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def is_fixed_size(self) -> bool:
|
|
256
|
+
"""Whether the size is fixed (not a range)."""
|
|
257
|
+
return self.size[0] == self.size[1]
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def is_fixed_start(self) -> bool:
|
|
261
|
+
"""Whether the start is fixed."""
|
|
262
|
+
return self.start[0] == self.start[1]
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def is_fixed_end(self) -> bool:
|
|
266
|
+
"""Whether the end is fixed."""
|
|
267
|
+
return self.end[0] == self.end[1]
|
|
268
|
+
|
|
269
|
+
def __hash__(self) -> int:
|
|
270
|
+
"""Hash based on unique ID."""
|
|
271
|
+
return hash(self._id)
|
|
272
|
+
|
|
273
|
+
def __eq__(self, other: object) -> bool:
|
|
274
|
+
"""Equality based on unique ID."""
|
|
275
|
+
if not isinstance(other, IntervalVar):
|
|
276
|
+
return NotImplemented
|
|
277
|
+
return self._id == other._id
|
|
278
|
+
|
|
279
|
+
def __repr__(self) -> str:
|
|
280
|
+
"""String representation."""
|
|
281
|
+
parts = [f"IntervalVar({self.name!r}"]
|
|
282
|
+
if self.is_fixed_size:
|
|
283
|
+
parts.append(f"size={self.size_min}")
|
|
284
|
+
else:
|
|
285
|
+
parts.append(f"size={self.size}")
|
|
286
|
+
if not self.is_fixed_start or self.start_min != INTERVAL_MIN:
|
|
287
|
+
parts.append(f"start={self.start}")
|
|
288
|
+
if not self.is_fixed_end or self.end_max != INTERVAL_MAX:
|
|
289
|
+
parts.append(f"end={self.end}")
|
|
290
|
+
if self.intensity is not None:
|
|
291
|
+
parts.append(f"intensity={self.intensity}")
|
|
292
|
+
if self.granularity != 1:
|
|
293
|
+
parts.append(f"granularity={self.granularity}")
|
|
294
|
+
if self.optional:
|
|
295
|
+
parts.append("optional=True")
|
|
296
|
+
return ", ".join(parts) + ")"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def IntervalVarArray(
|
|
300
|
+
size: int | Sequence[int],
|
|
301
|
+
*,
|
|
302
|
+
start: Bound | None = None,
|
|
303
|
+
end: Bound | None = None,
|
|
304
|
+
size_range: Bound | None = None,
|
|
305
|
+
length: Bound | None = None,
|
|
306
|
+
intensity: Sequence[Step] | None = None,
|
|
307
|
+
granularity: int = 1,
|
|
308
|
+
optional: bool = False,
|
|
309
|
+
name: str | None = None,
|
|
310
|
+
) -> list[IntervalVar]:
|
|
311
|
+
"""
|
|
312
|
+
Create an array of interval variables.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
size: Number of intervals, or tuple for multi-dimensional array.
|
|
316
|
+
start: Start bound for all intervals.
|
|
317
|
+
end: End bound for all intervals.
|
|
318
|
+
size_range: Size/duration bound for all intervals (named size_range
|
|
319
|
+
to avoid conflict with the size parameter).
|
|
320
|
+
length: Length bound for all intervals.
|
|
321
|
+
intensity: Stepwise intensity function for all intervals.
|
|
322
|
+
granularity: Scale for the intensity function.
|
|
323
|
+
optional: Whether all intervals are optional.
|
|
324
|
+
name: Base name for intervals (will be suffixed with index).
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
List of IntervalVar objects (nested list for multi-dimensional).
|
|
328
|
+
|
|
329
|
+
Example:
|
|
330
|
+
>>> tasks = IntervalVarArray(5, size_range=10, name="task")
|
|
331
|
+
>>> ops = IntervalVarArray((3, 4), size_range=(5, 20), optional=True)
|
|
332
|
+
"""
|
|
333
|
+
# Handle single dimension
|
|
334
|
+
if isinstance(size, int):
|
|
335
|
+
dims = [size]
|
|
336
|
+
else:
|
|
337
|
+
dims = list(size)
|
|
338
|
+
|
|
339
|
+
base_name = name or "_interval"
|
|
340
|
+
|
|
341
|
+
def create_recursive(dims: list[int], indices: list[int]) -> list:
|
|
342
|
+
"""Recursively create nested array structure."""
|
|
343
|
+
if len(dims) == 1:
|
|
344
|
+
# Base case: create actual interval variables
|
|
345
|
+
result = []
|
|
346
|
+
for i in range(dims[0]):
|
|
347
|
+
idx = indices + [i]
|
|
348
|
+
var_name = f"{base_name}[{']['.join(map(str, idx))}]"
|
|
349
|
+
kwargs: dict = {"name": var_name, "optional": optional}
|
|
350
|
+
if start is not None:
|
|
351
|
+
kwargs["start"] = start
|
|
352
|
+
if end is not None:
|
|
353
|
+
kwargs["end"] = end
|
|
354
|
+
if size_range is not None:
|
|
355
|
+
kwargs["size"] = size_range
|
|
356
|
+
if length is not None:
|
|
357
|
+
kwargs["length"] = length
|
|
358
|
+
if intensity is not None:
|
|
359
|
+
kwargs["intensity"] = intensity
|
|
360
|
+
if granularity != 1:
|
|
361
|
+
kwargs["granularity"] = granularity
|
|
362
|
+
result.append(IntervalVar(**kwargs))
|
|
363
|
+
return result
|
|
364
|
+
else:
|
|
365
|
+
# Recursive case: create nested list
|
|
366
|
+
return [
|
|
367
|
+
create_recursive(dims[1:], indices + [i])
|
|
368
|
+
for i in range(dims[0])
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
return create_recursive(dims, [])
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def IntervalVarDict(
|
|
375
|
+
keys: Sequence,
|
|
376
|
+
*,
|
|
377
|
+
start: Bound | None = None,
|
|
378
|
+
end: Bound | None = None,
|
|
379
|
+
size_range: Bound | None = None,
|
|
380
|
+
length: Bound | None = None,
|
|
381
|
+
intensity: Sequence[Step] | None = None,
|
|
382
|
+
granularity: int = 1,
|
|
383
|
+
optional: bool = False,
|
|
384
|
+
name: str | None = None,
|
|
385
|
+
) -> dict:
|
|
386
|
+
"""
|
|
387
|
+
Create a dictionary of interval variables indexed by keys.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
keys: Sequence of keys for the dictionary.
|
|
391
|
+
start: Start bound for all intervals.
|
|
392
|
+
end: End bound for all intervals.
|
|
393
|
+
size_range: Size/duration bound for all intervals.
|
|
394
|
+
length: Length bound for all intervals.
|
|
395
|
+
intensity: Stepwise intensity function for all intervals.
|
|
396
|
+
granularity: Scale for the intensity function.
|
|
397
|
+
optional: Whether all intervals are optional.
|
|
398
|
+
name: Base name for intervals.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Dictionary mapping keys to IntervalVar objects.
|
|
402
|
+
|
|
403
|
+
Example:
|
|
404
|
+
>>> tasks = IntervalVarDict(["A", "B", "C"], size_range=10, name="task")
|
|
405
|
+
>>> tasks["A"].size_min # 10
|
|
406
|
+
"""
|
|
407
|
+
base_name = name or "_interval"
|
|
408
|
+
result = {}
|
|
409
|
+
for key in keys:
|
|
410
|
+
var_name = f"{base_name}[{key}]"
|
|
411
|
+
kwargs: dict = {"name": var_name, "optional": optional}
|
|
412
|
+
if start is not None:
|
|
413
|
+
kwargs["start"] = start
|
|
414
|
+
if end is not None:
|
|
415
|
+
kwargs["end"] = end
|
|
416
|
+
if size_range is not None:
|
|
417
|
+
kwargs["size"] = size_range
|
|
418
|
+
if length is not None:
|
|
419
|
+
kwargs["length"] = length
|
|
420
|
+
if intensity is not None:
|
|
421
|
+
kwargs["intensity"] = intensity
|
|
422
|
+
if granularity != 1:
|
|
423
|
+
kwargs["granularity"] = granularity
|
|
424
|
+
result[key] = IntervalVar(**kwargs)
|
|
425
|
+
return result
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# Registry for all interval variables (for model compilation)
|
|
429
|
+
# Uses set for O(1) membership check, list for insertion order
|
|
430
|
+
_interval_registry_set: set[IntervalVar] = set()
|
|
431
|
+
_interval_registry_ordered: list[IntervalVar] = []
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def register_interval(interval: IntervalVar) -> None:
|
|
435
|
+
"""Register an interval variable for model compilation."""
|
|
436
|
+
if interval not in _interval_registry_set: # O(1) lookup
|
|
437
|
+
_interval_registry_set.add(interval)
|
|
438
|
+
_interval_registry_ordered.append(interval)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def get_registered_intervals() -> list[IntervalVar]:
|
|
442
|
+
"""Get all registered interval variables in registration order."""
|
|
443
|
+
return list(_interval_registry_ordered)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def clear_interval_registry() -> None:
|
|
447
|
+
"""Clear the interval variable registry."""
|
|
448
|
+
_interval_registry_set.clear()
|
|
449
|
+
_interval_registry_ordered.clear()
|
|
450
|
+
IntervalVar._id_counter = 0
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sequence variable implementation for scheduling models.
|
|
3
|
+
|
|
4
|
+
A sequence variable represents an ordered sequence of interval variables,
|
|
5
|
+
typically used to model a disjunctive resource (machine) where intervals
|
|
6
|
+
must be totally ordered and non-overlapping.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SequenceVar:
|
|
22
|
+
"""
|
|
23
|
+
Represents a sequence variable for scheduling.
|
|
24
|
+
|
|
25
|
+
A sequence variable represents an ordered set of interval variables.
|
|
26
|
+
It is typically used to model a disjunctive resource where only one
|
|
27
|
+
interval can execute at a time.
|
|
28
|
+
|
|
29
|
+
Each interval in the sequence can optionally have an associated type
|
|
30
|
+
(integer identifier) used for transition matrix constraints.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
intervals: List of interval variables in this sequence.
|
|
34
|
+
types: Optional list of type identifiers (one per interval).
|
|
35
|
+
Used for transition constraints (setup times).
|
|
36
|
+
name: Unique identifier for this sequence variable.
|
|
37
|
+
_id: Internal unique identifier.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> task1 = IntervalVar(size=10, name="task1")
|
|
41
|
+
>>> task2 = IntervalVar(size=15, name="task2")
|
|
42
|
+
>>> task3 = IntervalVar(size=8, name="task3")
|
|
43
|
+
>>> seq = SequenceVar(intervals=[task1, task2, task3], name="machine1")
|
|
44
|
+
|
|
45
|
+
>>> # With types for transition matrix
|
|
46
|
+
>>> seq = SequenceVar(
|
|
47
|
+
... intervals=[task1, task2, task3],
|
|
48
|
+
... types=[0, 1, 0],
|
|
49
|
+
... name="machine1"
|
|
50
|
+
... )
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
intervals: list[IntervalVar] = field(default_factory=list)
|
|
54
|
+
types: list[int] | None = None
|
|
55
|
+
name: str | None = None
|
|
56
|
+
_id: int = field(default=-1, repr=False, compare=False)
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
"""Validate and initialize the sequence variable."""
|
|
60
|
+
# Convert intervals to list if needed
|
|
61
|
+
if not isinstance(self.intervals, list):
|
|
62
|
+
self.intervals = list(self.intervals)
|
|
63
|
+
|
|
64
|
+
# Validate types length matches intervals
|
|
65
|
+
if self.types is not None:
|
|
66
|
+
if not isinstance(self.types, list):
|
|
67
|
+
self.types = list(self.types)
|
|
68
|
+
if len(self.types) != len(self.intervals):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Length of types ({len(self.types)}) must match "
|
|
71
|
+
f"length of intervals ({len(self.intervals)})"
|
|
72
|
+
)
|
|
73
|
+
# Validate types are non-negative integers
|
|
74
|
+
for i, t in enumerate(self.types):
|
|
75
|
+
if not isinstance(t, int) or t < 0:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Type at index {i} must be a non-negative integer, got {t}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Assign unique ID if not set
|
|
81
|
+
if self._id == -1:
|
|
82
|
+
self._id = SequenceVar._get_next_id()
|
|
83
|
+
|
|
84
|
+
# Generate name if not provided
|
|
85
|
+
if self.name is None:
|
|
86
|
+
self.name = f"_sequence_{self._id}"
|
|
87
|
+
|
|
88
|
+
# Register for model compilation/interop helpers
|
|
89
|
+
register_sequence(self)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _get_next_id() -> int:
|
|
93
|
+
"""Get next unique ID for sequence variables."""
|
|
94
|
+
current = getattr(SequenceVar, "_id_counter", 0)
|
|
95
|
+
SequenceVar._id_counter = current + 1
|
|
96
|
+
return current
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def size(self) -> int:
|
|
100
|
+
"""Number of intervals in this sequence."""
|
|
101
|
+
return len(self.intervals)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def has_types(self) -> bool:
|
|
105
|
+
"""Whether this sequence has type identifiers."""
|
|
106
|
+
return self.types is not None
|
|
107
|
+
|
|
108
|
+
def get_interval(self, index: int) -> IntervalVar:
|
|
109
|
+
"""Get interval at given index."""
|
|
110
|
+
return self.intervals[index]
|
|
111
|
+
|
|
112
|
+
def get_type(self, index: int) -> int | None:
|
|
113
|
+
"""Get type identifier at given index, or None if no types."""
|
|
114
|
+
if self.types is None:
|
|
115
|
+
return None
|
|
116
|
+
return self.types[index]
|
|
117
|
+
|
|
118
|
+
def get_intervals_by_type(self, type_id: int) -> list[IntervalVar]:
|
|
119
|
+
"""Get all intervals with the given type identifier."""
|
|
120
|
+
if self.types is None:
|
|
121
|
+
return []
|
|
122
|
+
return [
|
|
123
|
+
interval
|
|
124
|
+
for interval, t in zip(self.intervals, self.types)
|
|
125
|
+
if t == type_id
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
def __len__(self) -> int:
|
|
129
|
+
"""Number of intervals in the sequence."""
|
|
130
|
+
return len(self.intervals)
|
|
131
|
+
|
|
132
|
+
def __iter__(self):
|
|
133
|
+
"""Iterate over intervals."""
|
|
134
|
+
return iter(self.intervals)
|
|
135
|
+
|
|
136
|
+
def __getitem__(self, index: int) -> IntervalVar:
|
|
137
|
+
"""Get interval by index."""
|
|
138
|
+
return self.intervals[index]
|
|
139
|
+
|
|
140
|
+
def __hash__(self) -> int:
|
|
141
|
+
"""Hash based on unique ID."""
|
|
142
|
+
return hash(self._id)
|
|
143
|
+
|
|
144
|
+
def __eq__(self, other: object) -> bool:
|
|
145
|
+
"""Equality based on unique ID."""
|
|
146
|
+
if not isinstance(other, SequenceVar):
|
|
147
|
+
return NotImplemented
|
|
148
|
+
return self._id == other._id
|
|
149
|
+
|
|
150
|
+
def __repr__(self) -> str:
|
|
151
|
+
"""String representation."""
|
|
152
|
+
interval_names = [iv.name for iv in self.intervals]
|
|
153
|
+
parts = [f"SequenceVar({self.name!r}"]
|
|
154
|
+
parts.append(f"intervals={interval_names}")
|
|
155
|
+
if self.types is not None:
|
|
156
|
+
parts.append(f"types={self.types}")
|
|
157
|
+
return ", ".join(parts) + ")"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def SequenceVarArray(
|
|
161
|
+
size: int | Sequence[int],
|
|
162
|
+
intervals_per_sequence: list[list[IntervalVar]] | None = None,
|
|
163
|
+
*,
|
|
164
|
+
types_per_sequence: list[list[int]] | None = None,
|
|
165
|
+
name: str | None = None,
|
|
166
|
+
) -> list[SequenceVar]:
|
|
167
|
+
"""
|
|
168
|
+
Create an array of sequence variables.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
size: Number of sequences, or tuple for multi-dimensional array.
|
|
172
|
+
intervals_per_sequence: List of interval lists, one per sequence.
|
|
173
|
+
types_per_sequence: Optional list of type lists, one per sequence.
|
|
174
|
+
name: Base name for sequences (will be suffixed with index).
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
List of SequenceVar objects (nested list for multi-dimensional).
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> # Create sequences for 3 machines
|
|
181
|
+
>>> ops_m0 = [IntervalVar(size=10) for _ in range(5)]
|
|
182
|
+
>>> ops_m1 = [IntervalVar(size=15) for _ in range(5)]
|
|
183
|
+
>>> ops_m2 = [IntervalVar(size=8) for _ in range(5)]
|
|
184
|
+
>>> sequences = SequenceVarArray(
|
|
185
|
+
... 3,
|
|
186
|
+
... intervals_per_sequence=[ops_m0, ops_m1, ops_m2],
|
|
187
|
+
... name="machine"
|
|
188
|
+
... )
|
|
189
|
+
"""
|
|
190
|
+
# Handle single dimension
|
|
191
|
+
if isinstance(size, int):
|
|
192
|
+
n = size
|
|
193
|
+
else:
|
|
194
|
+
if len(size) != 1:
|
|
195
|
+
raise ValueError("SequenceVarArray only supports 1D arrays")
|
|
196
|
+
n = size[0]
|
|
197
|
+
|
|
198
|
+
if intervals_per_sequence is not None and len(intervals_per_sequence) != n:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Length of intervals_per_sequence ({len(intervals_per_sequence)}) "
|
|
201
|
+
f"must match size ({n})"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if types_per_sequence is not None and len(types_per_sequence) != n:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"Length of types_per_sequence ({len(types_per_sequence)}) "
|
|
207
|
+
f"must match size ({n})"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
base_name = name or "_sequence"
|
|
211
|
+
result = []
|
|
212
|
+
|
|
213
|
+
for i in range(n):
|
|
214
|
+
seq_name = f"{base_name}[{i}]"
|
|
215
|
+
intervals = intervals_per_sequence[i] if intervals_per_sequence else []
|
|
216
|
+
types = types_per_sequence[i] if types_per_sequence else None
|
|
217
|
+
result.append(SequenceVar(intervals=intervals, types=types, name=seq_name))
|
|
218
|
+
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Registry for all sequence variables (for model compilation)
|
|
223
|
+
# Uses set for O(1) membership check, list for insertion order
|
|
224
|
+
_sequence_registry_set: set[SequenceVar] = set()
|
|
225
|
+
_sequence_registry_ordered: list[SequenceVar] = []
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def register_sequence(sequence: SequenceVar) -> None:
|
|
229
|
+
"""Register a sequence variable for model compilation."""
|
|
230
|
+
if sequence not in _sequence_registry_set: # O(1) lookup
|
|
231
|
+
_sequence_registry_set.add(sequence)
|
|
232
|
+
_sequence_registry_ordered.append(sequence)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_registered_sequences() -> list[SequenceVar]:
|
|
236
|
+
"""Get all registered sequence variables in registration order."""
|
|
237
|
+
return list(_sequence_registry_ordered)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def clear_sequence_registry() -> None:
|
|
241
|
+
"""Clear the sequence variable registry."""
|
|
242
|
+
_sequence_registry_set.clear()
|
|
243
|
+
_sequence_registry_ordered.clear()
|
|
244
|
+
SequenceVar._id_counter = 0
|