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,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interop helpers to build pycsp3 expressions from IntervalVar.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pycsp3_scheduling.constraints._pycsp3 import length_value, presence_var, start_var
|
|
12
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def start_time(interval: IntervalVar):
|
|
16
|
+
"""Return a pycsp3 variable representing the start time."""
|
|
17
|
+
if not isinstance(interval, IntervalVar):
|
|
18
|
+
raise TypeError("start_time expects an IntervalVar")
|
|
19
|
+
return start_var(interval)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def end_time(interval: IntervalVar):
|
|
23
|
+
"""Return a pycsp3 expression representing the end time (start + length)."""
|
|
24
|
+
if not isinstance(interval, IntervalVar):
|
|
25
|
+
raise TypeError("end_time expects an IntervalVar")
|
|
26
|
+
return start_var(interval) + length_value(interval)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def presence_time(interval: IntervalVar):
|
|
30
|
+
"""Return a pycsp3 variable representing presence (0/1) for optional intervals."""
|
|
31
|
+
if not isinstance(interval, IntervalVar):
|
|
32
|
+
raise TypeError("presence_time expects an IntervalVar")
|
|
33
|
+
return presence_var(interval)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class IntervalValue(Mapping[str, int | bool | str | None]):
|
|
38
|
+
"""Solved interval values with dict-like and attribute access."""
|
|
39
|
+
|
|
40
|
+
start: int
|
|
41
|
+
length: int
|
|
42
|
+
present: bool = True
|
|
43
|
+
name: str | None = None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def end(self) -> int:
|
|
47
|
+
"""End time of the interval."""
|
|
48
|
+
return self.start + self.length
|
|
49
|
+
|
|
50
|
+
def __getitem__(self, key: str) -> int | bool | str | None:
|
|
51
|
+
if key == "start":
|
|
52
|
+
return self.start
|
|
53
|
+
if key == "end":
|
|
54
|
+
return self.end
|
|
55
|
+
if key == "length":
|
|
56
|
+
return self.length
|
|
57
|
+
if key == "present":
|
|
58
|
+
return self.present
|
|
59
|
+
if key == "name":
|
|
60
|
+
return self.name
|
|
61
|
+
raise KeyError(key)
|
|
62
|
+
|
|
63
|
+
def __iter__(self) -> Iterator[str]:
|
|
64
|
+
yield from ("start", "end", "length", "present", "name")
|
|
65
|
+
|
|
66
|
+
def __len__(self) -> int:
|
|
67
|
+
return 5
|
|
68
|
+
|
|
69
|
+
def __repr__(self) -> str:
|
|
70
|
+
if self.name is not None:
|
|
71
|
+
return (
|
|
72
|
+
f"IntervalValue(name={self.name!r}, start="
|
|
73
|
+
f"{self.start}, end={self.end}, length={self.length}, present={self.present})"
|
|
74
|
+
)
|
|
75
|
+
return (
|
|
76
|
+
"IntervalValue(start="
|
|
77
|
+
f"{self.start}, end={self.end}, length={self.length}, present={self.present})"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict[str, int | bool | str | None]:
|
|
81
|
+
"""Return a plain dict representation."""
|
|
82
|
+
return {
|
|
83
|
+
"start": self.start,
|
|
84
|
+
"end": self.end,
|
|
85
|
+
"length": self.length,
|
|
86
|
+
"present": self.present,
|
|
87
|
+
"name": self.name,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class ModelStatistics(Mapping[str, int]):
|
|
93
|
+
"""Statistics about the scheduling model."""
|
|
94
|
+
|
|
95
|
+
nb_interval_vars: int
|
|
96
|
+
nb_optional_interval_vars: int
|
|
97
|
+
nb_sequences: int
|
|
98
|
+
nb_sequences_with_types: int
|
|
99
|
+
nb_cumul_functions: int
|
|
100
|
+
nb_state_functions: int
|
|
101
|
+
|
|
102
|
+
def __getitem__(self, key: str) -> int:
|
|
103
|
+
if key == "nb_interval_vars":
|
|
104
|
+
return self.nb_interval_vars
|
|
105
|
+
if key == "nb_optional_interval_vars":
|
|
106
|
+
return self.nb_optional_interval_vars
|
|
107
|
+
if key == "nb_sequences":
|
|
108
|
+
return self.nb_sequences
|
|
109
|
+
if key == "nb_sequences_with_types":
|
|
110
|
+
return self.nb_sequences_with_types
|
|
111
|
+
if key == "nb_cumul_functions":
|
|
112
|
+
return self.nb_cumul_functions
|
|
113
|
+
if key == "nb_state_functions":
|
|
114
|
+
return self.nb_state_functions
|
|
115
|
+
raise KeyError(key)
|
|
116
|
+
|
|
117
|
+
def __iter__(self) -> Iterator[str]:
|
|
118
|
+
yield from (
|
|
119
|
+
"nb_interval_vars",
|
|
120
|
+
"nb_optional_interval_vars",
|
|
121
|
+
"nb_sequences",
|
|
122
|
+
"nb_sequences_with_types",
|
|
123
|
+
"nb_cumul_functions",
|
|
124
|
+
"nb_state_functions",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def __len__(self) -> int:
|
|
128
|
+
return 6
|
|
129
|
+
|
|
130
|
+
def __repr__(self) -> str:
|
|
131
|
+
return (
|
|
132
|
+
"ModelStatistics("
|
|
133
|
+
f"nb_interval_vars={self.nb_interval_vars}, "
|
|
134
|
+
f"nb_optional_interval_vars={self.nb_optional_interval_vars}, "
|
|
135
|
+
f"nb_sequences={self.nb_sequences}, "
|
|
136
|
+
f"nb_sequences_with_types={self.nb_sequences_with_types}, "
|
|
137
|
+
f"nb_cumul_functions={self.nb_cumul_functions}, "
|
|
138
|
+
f"nb_state_functions={self.nb_state_functions})"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> dict[str, int]:
|
|
142
|
+
"""Return a plain dict representation."""
|
|
143
|
+
return {
|
|
144
|
+
"nb_interval_vars": self.nb_interval_vars,
|
|
145
|
+
"nb_optional_interval_vars": self.nb_optional_interval_vars,
|
|
146
|
+
"nb_sequences": self.nb_sequences,
|
|
147
|
+
"nb_sequences_with_types": self.nb_sequences_with_types,
|
|
148
|
+
"nb_cumul_functions": self.nb_cumul_functions,
|
|
149
|
+
"nb_state_functions": self.nb_state_functions,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class SolutionStatistics(Mapping[str, object]):
|
|
155
|
+
"""Statistics about the solved schedule."""
|
|
156
|
+
|
|
157
|
+
status: object | None
|
|
158
|
+
objective_value: int | float | None
|
|
159
|
+
solve_time: float | None
|
|
160
|
+
nb_interval_vars: int
|
|
161
|
+
nb_intervals_present: int
|
|
162
|
+
nb_intervals_absent: int
|
|
163
|
+
min_start: int | None
|
|
164
|
+
max_end: int | None
|
|
165
|
+
makespan: int | None
|
|
166
|
+
span: int | None
|
|
167
|
+
|
|
168
|
+
def __getitem__(self, key: str) -> object:
|
|
169
|
+
if key == "status":
|
|
170
|
+
return self.status
|
|
171
|
+
if key == "objective_value":
|
|
172
|
+
return self.objective_value
|
|
173
|
+
if key == "solve_time":
|
|
174
|
+
return self.solve_time
|
|
175
|
+
if key == "nb_interval_vars":
|
|
176
|
+
return self.nb_interval_vars
|
|
177
|
+
if key == "nb_intervals_present":
|
|
178
|
+
return self.nb_intervals_present
|
|
179
|
+
if key == "nb_intervals_absent":
|
|
180
|
+
return self.nb_intervals_absent
|
|
181
|
+
if key == "min_start":
|
|
182
|
+
return self.min_start
|
|
183
|
+
if key == "max_end":
|
|
184
|
+
return self.max_end
|
|
185
|
+
if key == "makespan":
|
|
186
|
+
return self.makespan
|
|
187
|
+
if key == "span":
|
|
188
|
+
return self.span
|
|
189
|
+
raise KeyError(key)
|
|
190
|
+
|
|
191
|
+
def __iter__(self) -> Iterator[str]:
|
|
192
|
+
yield from (
|
|
193
|
+
"status",
|
|
194
|
+
"objective_value",
|
|
195
|
+
"solve_time",
|
|
196
|
+
"nb_interval_vars",
|
|
197
|
+
"nb_intervals_present",
|
|
198
|
+
"nb_intervals_absent",
|
|
199
|
+
"min_start",
|
|
200
|
+
"max_end",
|
|
201
|
+
"makespan",
|
|
202
|
+
"span",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def __len__(self) -> int:
|
|
206
|
+
return 10
|
|
207
|
+
|
|
208
|
+
def __repr__(self) -> str:
|
|
209
|
+
return (
|
|
210
|
+
"SolutionStatistics("
|
|
211
|
+
f"status={self.status}, "
|
|
212
|
+
f"objective_value={self.objective_value}, "
|
|
213
|
+
f"solve_time={self.solve_time}, "
|
|
214
|
+
f"nb_interval_vars={self.nb_interval_vars}, "
|
|
215
|
+
f"nb_intervals_present={self.nb_intervals_present}, "
|
|
216
|
+
f"nb_intervals_absent={self.nb_intervals_absent}, "
|
|
217
|
+
f"min_start={self.min_start}, "
|
|
218
|
+
f"max_end={self.max_end}, "
|
|
219
|
+
f"makespan={self.makespan}, "
|
|
220
|
+
f"span={self.span})"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def to_dict(self) -> dict[str, object]:
|
|
224
|
+
"""Return a plain dict representation."""
|
|
225
|
+
return {
|
|
226
|
+
"status": self.status,
|
|
227
|
+
"objective_value": self.objective_value,
|
|
228
|
+
"solve_time": self.solve_time,
|
|
229
|
+
"nb_interval_vars": self.nb_interval_vars,
|
|
230
|
+
"nb_intervals_present": self.nb_intervals_present,
|
|
231
|
+
"nb_intervals_absent": self.nb_intervals_absent,
|
|
232
|
+
"min_start": self.min_start,
|
|
233
|
+
"max_end": self.max_end,
|
|
234
|
+
"makespan": self.makespan,
|
|
235
|
+
"span": self.span,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def interval_value(interval: IntervalVar) -> IntervalValue | None:
|
|
240
|
+
"""
|
|
241
|
+
Extract the solution values for an interval after solving.
|
|
242
|
+
|
|
243
|
+
Returns an IntervalValue with 'start', 'end', 'length', 'present' fields,
|
|
244
|
+
or None if the interval is absent (for optional intervals).
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
interval: The interval variable to extract values from.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
IntervalValue with start/end/length/present values, or None if absent.
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
>>> task = IntervalVar(size=10, name="task")
|
|
254
|
+
>>> # ... add constraints and solve ...
|
|
255
|
+
>>> vals = interval_value(task)
|
|
256
|
+
>>> print(f"start={vals.start}, end={vals.end}")
|
|
257
|
+
"""
|
|
258
|
+
from pycsp3 import value
|
|
259
|
+
|
|
260
|
+
if not isinstance(interval, IntervalVar):
|
|
261
|
+
raise TypeError("interval_value expects an IntervalVar")
|
|
262
|
+
|
|
263
|
+
# Check presence for optional intervals
|
|
264
|
+
if interval.optional:
|
|
265
|
+
pres = presence_var(interval)
|
|
266
|
+
if value(pres) == 0:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
start = value(start_var(interval))
|
|
270
|
+
length_val = length_value(interval)
|
|
271
|
+
if isinstance(length_val, int):
|
|
272
|
+
length = length_val
|
|
273
|
+
else:
|
|
274
|
+
length = value(length_val)
|
|
275
|
+
|
|
276
|
+
return IntervalValue(start=start, length=length, present=True, name=interval.name)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def model_statistics() -> ModelStatistics:
|
|
280
|
+
"""Return statistics about the current scheduling model."""
|
|
281
|
+
from pycsp3_scheduling.functions.cumul_functions import get_registered_cumuls
|
|
282
|
+
from pycsp3_scheduling.functions.state_functions import (
|
|
283
|
+
get_registered_state_functions,
|
|
284
|
+
)
|
|
285
|
+
from pycsp3_scheduling.variables.interval import get_registered_intervals
|
|
286
|
+
from pycsp3_scheduling.variables.sequence import get_registered_sequences
|
|
287
|
+
|
|
288
|
+
intervals = get_registered_intervals()
|
|
289
|
+
sequences = get_registered_sequences()
|
|
290
|
+
nb_optional = sum(1 for interval in intervals if interval.optional)
|
|
291
|
+
nb_typed_sequences = sum(1 for seq in sequences if seq.has_types)
|
|
292
|
+
|
|
293
|
+
return ModelStatistics(
|
|
294
|
+
nb_interval_vars=len(intervals),
|
|
295
|
+
nb_optional_interval_vars=nb_optional,
|
|
296
|
+
nb_sequences=len(sequences),
|
|
297
|
+
nb_sequences_with_types=nb_typed_sequences,
|
|
298
|
+
nb_cumul_functions=len(get_registered_cumuls()),
|
|
299
|
+
nb_state_functions=len(get_registered_state_functions()),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def solution_statistics(
|
|
304
|
+
intervals: Sequence[IntervalVar] | None = None,
|
|
305
|
+
*,
|
|
306
|
+
status: object | None = None,
|
|
307
|
+
objective: Any | None = None,
|
|
308
|
+
solve_time: float | None = None,
|
|
309
|
+
) -> SolutionStatistics:
|
|
310
|
+
"""
|
|
311
|
+
Return statistics about the current solution.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
intervals: Optional list of intervals to analyze. Defaults to the
|
|
315
|
+
registered intervals.
|
|
316
|
+
status: Optional solve status from pycsp3 (SAT, OPTIMUM, UNSAT, etc.).
|
|
317
|
+
objective: Optional objective expression or value to evaluate.
|
|
318
|
+
solve_time: Optional elapsed solve time in seconds.
|
|
319
|
+
"""
|
|
320
|
+
if intervals is None:
|
|
321
|
+
from pycsp3_scheduling.variables.interval import get_registered_intervals
|
|
322
|
+
|
|
323
|
+
intervals = get_registered_intervals()
|
|
324
|
+
|
|
325
|
+
interval_values = [interval_value(interval) for interval in intervals]
|
|
326
|
+
present = [val for val in interval_values if val is not None]
|
|
327
|
+
|
|
328
|
+
min_start = min((val.start for val in present), default=None)
|
|
329
|
+
max_end = max((val.end for val in present), default=None)
|
|
330
|
+
makespan = max_end
|
|
331
|
+
span = None if min_start is None or max_end is None else max_end - min_start
|
|
332
|
+
|
|
333
|
+
objective_value: int | float | None
|
|
334
|
+
if objective is None:
|
|
335
|
+
objective_value = None
|
|
336
|
+
elif isinstance(objective, (int, float)):
|
|
337
|
+
objective_value = objective
|
|
338
|
+
else:
|
|
339
|
+
from pycsp3 import value
|
|
340
|
+
try:
|
|
341
|
+
objective_value = value(objective)
|
|
342
|
+
except (AssertionError, TypeError):
|
|
343
|
+
objective_value = None
|
|
344
|
+
|
|
345
|
+
return SolutionStatistics(
|
|
346
|
+
status=status,
|
|
347
|
+
objective_value=objective_value,
|
|
348
|
+
solve_time=solve_time,
|
|
349
|
+
nb_interval_vars=len(intervals),
|
|
350
|
+
nb_intervals_present=len(present),
|
|
351
|
+
nb_intervals_absent=len(intervals) - len(present),
|
|
352
|
+
min_start=min_start,
|
|
353
|
+
max_end=max_end,
|
|
354
|
+
makespan=makespan,
|
|
355
|
+
span=span,
|
|
356
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output generators for scheduling models.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- XCSP3 scheduling extension output generator
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# To be implemented:
|
|
11
|
+
# from pycsp3_scheduling.output.xcsp3_scheduling import generate_xcsp3_scheduling
|
|
12
|
+
|
|
13
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solver adapters for scheduling models.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- Decomposition layer for standard CP solvers
|
|
6
|
+
- Adapters for ACE, Choco, and other solvers
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
# To be implemented:
|
|
12
|
+
# from pycsp3_scheduling.solvers.decomposition import decompose_scheduling_model
|
|
13
|
+
|
|
14
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Variable types for scheduling models.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- IntervalVar: Represents a task/activity with start, end, and duration
|
|
6
|
+
- SequenceVar: Represents an ordered sequence of intervals on a resource
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pycsp3_scheduling.variables.interval import (
|
|
12
|
+
INTERVAL_MAX,
|
|
13
|
+
INTERVAL_MIN,
|
|
14
|
+
IntervalVar,
|
|
15
|
+
IntervalVarArray,
|
|
16
|
+
IntervalVarDict,
|
|
17
|
+
clear_interval_registry,
|
|
18
|
+
get_registered_intervals,
|
|
19
|
+
register_interval,
|
|
20
|
+
)
|
|
21
|
+
from pycsp3_scheduling.variables.sequence import (
|
|
22
|
+
SequenceVar,
|
|
23
|
+
SequenceVarArray,
|
|
24
|
+
clear_sequence_registry,
|
|
25
|
+
get_registered_sequences,
|
|
26
|
+
register_sequence,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Interval variables
|
|
31
|
+
"IntervalVar",
|
|
32
|
+
"IntervalVarArray",
|
|
33
|
+
"IntervalVarDict",
|
|
34
|
+
"INTERVAL_MIN",
|
|
35
|
+
"INTERVAL_MAX",
|
|
36
|
+
"register_interval",
|
|
37
|
+
"get_registered_intervals",
|
|
38
|
+
"clear_interval_registry",
|
|
39
|
+
# Sequence variables
|
|
40
|
+
"SequenceVar",
|
|
41
|
+
"SequenceVarArray",
|
|
42
|
+
"register_sequence",
|
|
43
|
+
"get_registered_sequences",
|
|
44
|
+
"clear_sequence_registry",
|
|
45
|
+
]
|