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,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,7 @@
1
+ """
2
+ Solver-specific adapters.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ __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
+ ]