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,494 @@
|
|
|
1
|
+
"""
|
|
2
|
+
State functions for modeling discrete states over time.
|
|
3
|
+
|
|
4
|
+
A state function represents a resource that can be in different states
|
|
5
|
+
over time, with optional transition constraints between states.
|
|
6
|
+
|
|
7
|
+
Use cases:
|
|
8
|
+
- Machine modes (setup, processing, idle, maintenance)
|
|
9
|
+
- Room configurations (lecture, exam, meeting)
|
|
10
|
+
- Worker skills/roles
|
|
11
|
+
- Any resource with discrete, mutually exclusive states
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> machine_state = StateFunction(name="machine")
|
|
15
|
+
>>> # Task requires machine in state 1
|
|
16
|
+
>>> satisfy(always_equal(machine_state, task, 1))
|
|
17
|
+
>>> # Define valid transitions with durations
|
|
18
|
+
>>> transitions = TransitionMatrix([
|
|
19
|
+
... [0, 5, 10], # From state 0: 0->0=0, 0->1=5, 0->2=10
|
|
20
|
+
... [5, 0, 3], # From state 1: 1->0=5, 1->1=0, 1->2=3
|
|
21
|
+
... [10, 3, 0], # From state 2: 2->0=10, 2->1=3, 2->2=0
|
|
22
|
+
... ])
|
|
23
|
+
>>> machine_state = StateFunction(name="machine", transitions=transitions)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from enum import Enum, auto
|
|
30
|
+
from typing import TYPE_CHECKING, Sequence
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# Transition Matrix
|
|
38
|
+
# =============================================================================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class TransitionMatrix:
|
|
43
|
+
"""
|
|
44
|
+
Transition matrix defining valid state transitions and durations.
|
|
45
|
+
|
|
46
|
+
A transition matrix specifies the time required to transition from
|
|
47
|
+
one state to another. A value of -1 (or FORBIDDEN) indicates that
|
|
48
|
+
the transition is not allowed.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
matrix: 2D list of transition times. matrix[i][j] is the time
|
|
52
|
+
to transition from state i to state j.
|
|
53
|
+
name: Optional name for the matrix.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> # 3 states with symmetric transition times
|
|
57
|
+
>>> tm = TransitionMatrix([
|
|
58
|
+
... [0, 5, 10],
|
|
59
|
+
... [5, 0, 3],
|
|
60
|
+
... [10, 3, 0],
|
|
61
|
+
... ])
|
|
62
|
+
>>> tm[0, 1] # Time from state 0 to state 1
|
|
63
|
+
5
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
matrix: list[list[int]]
|
|
67
|
+
name: str | None = None
|
|
68
|
+
_id: int = field(default=-1, repr=False)
|
|
69
|
+
|
|
70
|
+
# Special value indicating forbidden transition
|
|
71
|
+
FORBIDDEN: int = -1
|
|
72
|
+
|
|
73
|
+
def __post_init__(self) -> None:
|
|
74
|
+
"""Validate and assign unique ID."""
|
|
75
|
+
self._validate()
|
|
76
|
+
if self._id == -1:
|
|
77
|
+
self._id = TransitionMatrix._get_next_id()
|
|
78
|
+
|
|
79
|
+
def _validate(self) -> None:
|
|
80
|
+
"""Validate the transition matrix."""
|
|
81
|
+
if not self.matrix:
|
|
82
|
+
raise ValueError("Transition matrix cannot be empty")
|
|
83
|
+
|
|
84
|
+
n = len(self.matrix)
|
|
85
|
+
for i, row in enumerate(self.matrix):
|
|
86
|
+
if len(row) != n:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Transition matrix must be square. "
|
|
89
|
+
f"Row {i} has {len(row)} elements, expected {n}"
|
|
90
|
+
)
|
|
91
|
+
for j, val in enumerate(row):
|
|
92
|
+
if not isinstance(val, int):
|
|
93
|
+
raise TypeError(
|
|
94
|
+
f"Transition matrix values must be integers, "
|
|
95
|
+
f"got {type(val).__name__} at [{i}][{j}]"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _get_next_id() -> int:
|
|
100
|
+
"""Get next unique ID."""
|
|
101
|
+
current = getattr(TransitionMatrix, "_id_counter", 0)
|
|
102
|
+
TransitionMatrix._id_counter = current + 1
|
|
103
|
+
return current
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def size(self) -> int:
|
|
107
|
+
"""Number of states (dimension of the matrix)."""
|
|
108
|
+
return len(self.matrix)
|
|
109
|
+
|
|
110
|
+
def __getitem__(self, key: tuple[int, int]) -> int:
|
|
111
|
+
"""Get transition time from state i to state j."""
|
|
112
|
+
i, j = key
|
|
113
|
+
return self.matrix[i][j]
|
|
114
|
+
|
|
115
|
+
def __setitem__(self, key: tuple[int, int], value: int) -> None:
|
|
116
|
+
"""Set transition time from state i to state j."""
|
|
117
|
+
i, j = key
|
|
118
|
+
self.matrix[i][j] = value
|
|
119
|
+
|
|
120
|
+
def is_forbidden(self, from_state: int, to_state: int) -> bool:
|
|
121
|
+
"""Check if transition from from_state to to_state is forbidden."""
|
|
122
|
+
return self.matrix[from_state][to_state] == self.FORBIDDEN
|
|
123
|
+
|
|
124
|
+
def get_row(self, state: int) -> list[int]:
|
|
125
|
+
"""Get all transition times from a given state."""
|
|
126
|
+
return self.matrix[state]
|
|
127
|
+
|
|
128
|
+
def get_column(self, state: int) -> list[int]:
|
|
129
|
+
"""Get all transition times to a given state."""
|
|
130
|
+
return [row[state] for row in self.matrix]
|
|
131
|
+
|
|
132
|
+
def __repr__(self) -> str:
|
|
133
|
+
"""String representation."""
|
|
134
|
+
if self.name:
|
|
135
|
+
return f"TransitionMatrix({self.name}, {self.size}x{self.size})"
|
|
136
|
+
return f"TransitionMatrix({self.size}x{self.size})"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# =============================================================================
|
|
140
|
+
# State Function
|
|
141
|
+
# =============================================================================
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class StateFunction:
|
|
146
|
+
"""
|
|
147
|
+
State function representing a discrete state over time.
|
|
148
|
+
|
|
149
|
+
A state function can be in different integer states at different times.
|
|
150
|
+
Tasks can require specific states during their execution, and transitions
|
|
151
|
+
between states can have associated times defined by a transition matrix.
|
|
152
|
+
|
|
153
|
+
Attributes:
|
|
154
|
+
name: Name of the state function.
|
|
155
|
+
transitions: Optional transition matrix defining transition times.
|
|
156
|
+
initial_state: Initial state at time 0 (default: no specific state).
|
|
157
|
+
states: Set of valid state values (inferred from transitions if not given).
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
>>> machine = StateFunction(name="machine_mode")
|
|
161
|
+
>>> # Machine must be in state 2 during task execution
|
|
162
|
+
>>> satisfy(always_equal(machine, task, 2))
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
name: str
|
|
166
|
+
transitions: TransitionMatrix | None = None
|
|
167
|
+
initial_state: int | None = None
|
|
168
|
+
states: set[int] | None = None
|
|
169
|
+
_id: int = field(default=-1, repr=False)
|
|
170
|
+
|
|
171
|
+
def __post_init__(self) -> None:
|
|
172
|
+
"""Initialize and validate."""
|
|
173
|
+
if self._id == -1:
|
|
174
|
+
self._id = StateFunction._get_next_id()
|
|
175
|
+
_register_state_function(self)
|
|
176
|
+
|
|
177
|
+
# Infer states from transition matrix if not provided
|
|
178
|
+
if self.states is None and self.transitions is not None:
|
|
179
|
+
self.states = set(range(self.transitions.size))
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _get_next_id() -> int:
|
|
183
|
+
"""Get next unique ID."""
|
|
184
|
+
current = getattr(StateFunction, "_id_counter", 0)
|
|
185
|
+
StateFunction._id_counter = current + 1
|
|
186
|
+
return current
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def num_states(self) -> int | None:
|
|
190
|
+
"""Number of valid states, if known."""
|
|
191
|
+
if self.states is not None:
|
|
192
|
+
return len(self.states)
|
|
193
|
+
if self.transitions is not None:
|
|
194
|
+
return self.transitions.size
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def __hash__(self) -> int:
|
|
198
|
+
"""Hash based on unique ID."""
|
|
199
|
+
return hash(self._id)
|
|
200
|
+
|
|
201
|
+
def __repr__(self) -> str:
|
|
202
|
+
"""String representation."""
|
|
203
|
+
parts = [f"StateFunction({self.name!r}"]
|
|
204
|
+
if self.transitions:
|
|
205
|
+
parts.append(f", transitions={self.transitions.size}x{self.transitions.size}")
|
|
206
|
+
if self.initial_state is not None:
|
|
207
|
+
parts.append(f", initial={self.initial_state}")
|
|
208
|
+
parts.append(")")
|
|
209
|
+
return "".join(parts)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# =============================================================================
|
|
213
|
+
# State Constraint Types
|
|
214
|
+
# =============================================================================
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class StateConstraintType(Enum):
|
|
218
|
+
"""Types of state constraints."""
|
|
219
|
+
|
|
220
|
+
ALWAYS_IN = auto() # State in range [min, max]
|
|
221
|
+
ALWAYS_EQUAL = auto() # State equals specific value
|
|
222
|
+
ALWAYS_CONSTANT = auto() # State doesn't change during interval
|
|
223
|
+
ALWAYS_NO_STATE = auto() # No state defined during interval
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class StateConstraint:
|
|
228
|
+
"""
|
|
229
|
+
Constraint on a state function during an interval.
|
|
230
|
+
|
|
231
|
+
Attributes:
|
|
232
|
+
state_func: The state function being constrained.
|
|
233
|
+
interval: The interval during which the constraint applies.
|
|
234
|
+
constraint_type: Type of constraint.
|
|
235
|
+
value: State value for ALWAYS_EQUAL.
|
|
236
|
+
min_value: Minimum state for ALWAYS_IN.
|
|
237
|
+
max_value: Maximum state for ALWAYS_IN.
|
|
238
|
+
is_start_aligned: Whether constraint starts exactly at interval start.
|
|
239
|
+
is_end_aligned: Whether constraint ends exactly at interval end.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
state_func: StateFunction
|
|
243
|
+
interval: IntervalVar
|
|
244
|
+
constraint_type: StateConstraintType
|
|
245
|
+
value: int | None = None
|
|
246
|
+
min_value: int | None = None
|
|
247
|
+
max_value: int | None = None
|
|
248
|
+
is_start_aligned: bool = True
|
|
249
|
+
is_end_aligned: bool = True
|
|
250
|
+
|
|
251
|
+
def __repr__(self) -> str:
|
|
252
|
+
"""String representation."""
|
|
253
|
+
interval_name = self.interval.name if self.interval else "?"
|
|
254
|
+
if self.constraint_type == StateConstraintType.ALWAYS_EQUAL:
|
|
255
|
+
return f"always_equal({self.state_func.name}, {interval_name}, {self.value})"
|
|
256
|
+
elif self.constraint_type == StateConstraintType.ALWAYS_IN:
|
|
257
|
+
return f"always_in({self.state_func.name}, {interval_name}, {self.min_value}, {self.max_value})"
|
|
258
|
+
elif self.constraint_type == StateConstraintType.ALWAYS_CONSTANT:
|
|
259
|
+
return f"always_constant({self.state_func.name}, {interval_name})"
|
|
260
|
+
elif self.constraint_type == StateConstraintType.ALWAYS_NO_STATE:
|
|
261
|
+
return f"always_no_state({self.state_func.name}, {interval_name})"
|
|
262
|
+
return f"StateConstraint({self.constraint_type})"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# =============================================================================
|
|
266
|
+
# State Constraint Functions
|
|
267
|
+
# =============================================================================
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def always_equal(
|
|
271
|
+
state_func: StateFunction,
|
|
272
|
+
interval: IntervalVar,
|
|
273
|
+
value: int,
|
|
274
|
+
is_start_aligned: bool = True,
|
|
275
|
+
is_end_aligned: bool = True,
|
|
276
|
+
) -> StateConstraint:
|
|
277
|
+
"""
|
|
278
|
+
Constrain state function to equal a specific value during interval.
|
|
279
|
+
|
|
280
|
+
The state function must be equal to the specified value throughout
|
|
281
|
+
the execution of the interval.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
state_func: The state function.
|
|
285
|
+
interval: The interval during which the constraint applies.
|
|
286
|
+
value: The required state value.
|
|
287
|
+
is_start_aligned: If True, state must equal value exactly at start.
|
|
288
|
+
is_end_aligned: If True, state must equal value exactly at end.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
A StateConstraint representing the always_equal constraint.
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
>>> machine = StateFunction(name="machine")
|
|
295
|
+
>>> # Machine must be in state 2 during task
|
|
296
|
+
>>> satisfy(always_equal(machine, task, 2))
|
|
297
|
+
"""
|
|
298
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
299
|
+
|
|
300
|
+
if not isinstance(state_func, StateFunction):
|
|
301
|
+
raise TypeError(
|
|
302
|
+
f"state_func must be a StateFunction, got {type(state_func).__name__}"
|
|
303
|
+
)
|
|
304
|
+
if not isinstance(interval, IntervalVar):
|
|
305
|
+
raise TypeError(
|
|
306
|
+
f"interval must be an IntervalVar, got {type(interval).__name__}"
|
|
307
|
+
)
|
|
308
|
+
if not isinstance(value, int):
|
|
309
|
+
raise TypeError(f"value must be an int, got {type(value).__name__}")
|
|
310
|
+
|
|
311
|
+
return StateConstraint(
|
|
312
|
+
state_func=state_func,
|
|
313
|
+
interval=interval,
|
|
314
|
+
constraint_type=StateConstraintType.ALWAYS_EQUAL,
|
|
315
|
+
value=value,
|
|
316
|
+
is_start_aligned=is_start_aligned,
|
|
317
|
+
is_end_aligned=is_end_aligned,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def always_in(
|
|
322
|
+
state_func: StateFunction,
|
|
323
|
+
interval: IntervalVar,
|
|
324
|
+
min_value: int,
|
|
325
|
+
max_value: int,
|
|
326
|
+
is_start_aligned: bool = True,
|
|
327
|
+
is_end_aligned: bool = True,
|
|
328
|
+
) -> StateConstraint:
|
|
329
|
+
"""
|
|
330
|
+
Constrain state function to be within a range during interval.
|
|
331
|
+
|
|
332
|
+
The state function must be within [min_value, max_value] throughout
|
|
333
|
+
the execution of the interval.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
state_func: The state function.
|
|
337
|
+
interval: The interval during which the constraint applies.
|
|
338
|
+
min_value: Minimum allowed state value.
|
|
339
|
+
max_value: Maximum allowed state value.
|
|
340
|
+
is_start_aligned: If True, constraint applies exactly at start.
|
|
341
|
+
is_end_aligned: If True, constraint applies exactly at end.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
A StateConstraint representing the always_in constraint.
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
>>> machine = StateFunction(name="machine")
|
|
348
|
+
>>> # Machine must be in state 1, 2, or 3 during task
|
|
349
|
+
>>> satisfy(always_in(machine, task, 1, 3))
|
|
350
|
+
"""
|
|
351
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
352
|
+
|
|
353
|
+
if not isinstance(state_func, StateFunction):
|
|
354
|
+
raise TypeError(
|
|
355
|
+
f"state_func must be a StateFunction, got {type(state_func).__name__}"
|
|
356
|
+
)
|
|
357
|
+
if not isinstance(interval, IntervalVar):
|
|
358
|
+
raise TypeError(
|
|
359
|
+
f"interval must be an IntervalVar, got {type(interval).__name__}"
|
|
360
|
+
)
|
|
361
|
+
if not isinstance(min_value, int) or not isinstance(max_value, int):
|
|
362
|
+
raise TypeError("min_value and max_value must be integers")
|
|
363
|
+
if min_value > max_value:
|
|
364
|
+
raise ValueError(
|
|
365
|
+
f"min_value ({min_value}) cannot exceed max_value ({max_value})"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return StateConstraint(
|
|
369
|
+
state_func=state_func,
|
|
370
|
+
interval=interval,
|
|
371
|
+
constraint_type=StateConstraintType.ALWAYS_IN,
|
|
372
|
+
min_value=min_value,
|
|
373
|
+
max_value=max_value,
|
|
374
|
+
is_start_aligned=is_start_aligned,
|
|
375
|
+
is_end_aligned=is_end_aligned,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def always_constant(
|
|
380
|
+
state_func: StateFunction,
|
|
381
|
+
interval: IntervalVar,
|
|
382
|
+
is_start_aligned: bool = True,
|
|
383
|
+
is_end_aligned: bool = True,
|
|
384
|
+
) -> StateConstraint:
|
|
385
|
+
"""
|
|
386
|
+
Constrain state function to remain constant during interval.
|
|
387
|
+
|
|
388
|
+
The state function must not change its value throughout the
|
|
389
|
+
execution of the interval.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
state_func: The state function.
|
|
393
|
+
interval: The interval during which the constraint applies.
|
|
394
|
+
is_start_aligned: If True, constant region starts exactly at start.
|
|
395
|
+
is_end_aligned: If True, constant region ends exactly at end.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
A StateConstraint representing the always_constant constraint.
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
>>> machine = StateFunction(name="machine")
|
|
402
|
+
>>> # Machine state cannot change during task
|
|
403
|
+
>>> satisfy(always_constant(machine, task))
|
|
404
|
+
"""
|
|
405
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
406
|
+
|
|
407
|
+
if not isinstance(state_func, StateFunction):
|
|
408
|
+
raise TypeError(
|
|
409
|
+
f"state_func must be a StateFunction, got {type(state_func).__name__}"
|
|
410
|
+
)
|
|
411
|
+
if not isinstance(interval, IntervalVar):
|
|
412
|
+
raise TypeError(
|
|
413
|
+
f"interval must be an IntervalVar, got {type(interval).__name__}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return StateConstraint(
|
|
417
|
+
state_func=state_func,
|
|
418
|
+
interval=interval,
|
|
419
|
+
constraint_type=StateConstraintType.ALWAYS_CONSTANT,
|
|
420
|
+
is_start_aligned=is_start_aligned,
|
|
421
|
+
is_end_aligned=is_end_aligned,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def always_no_state(
|
|
426
|
+
state_func: StateFunction,
|
|
427
|
+
interval: IntervalVar,
|
|
428
|
+
is_start_aligned: bool = True,
|
|
429
|
+
is_end_aligned: bool = True,
|
|
430
|
+
) -> StateConstraint:
|
|
431
|
+
"""
|
|
432
|
+
Constrain state function to have no defined state during interval.
|
|
433
|
+
|
|
434
|
+
The state function must not be in any state throughout the
|
|
435
|
+
execution of the interval (the resource is "unused").
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
state_func: The state function.
|
|
439
|
+
interval: The interval during which the constraint applies.
|
|
440
|
+
is_start_aligned: If True, no-state region starts exactly at start.
|
|
441
|
+
is_end_aligned: If True, no-state region ends exactly at end.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
A StateConstraint representing the always_no_state constraint.
|
|
445
|
+
|
|
446
|
+
Example:
|
|
447
|
+
>>> machine = StateFunction(name="machine")
|
|
448
|
+
>>> # Machine must be unused during maintenance
|
|
449
|
+
>>> satisfy(always_no_state(machine, maintenance_interval))
|
|
450
|
+
"""
|
|
451
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
452
|
+
|
|
453
|
+
if not isinstance(state_func, StateFunction):
|
|
454
|
+
raise TypeError(
|
|
455
|
+
f"state_func must be a StateFunction, got {type(state_func).__name__}"
|
|
456
|
+
)
|
|
457
|
+
if not isinstance(interval, IntervalVar):
|
|
458
|
+
raise TypeError(
|
|
459
|
+
f"interval must be an IntervalVar, got {type(interval).__name__}"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
return StateConstraint(
|
|
463
|
+
state_func=state_func,
|
|
464
|
+
interval=interval,
|
|
465
|
+
constraint_type=StateConstraintType.ALWAYS_NO_STATE,
|
|
466
|
+
is_start_aligned=is_start_aligned,
|
|
467
|
+
is_end_aligned=is_end_aligned,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# =============================================================================
|
|
472
|
+
# Registry for State Functions
|
|
473
|
+
# =============================================================================
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
_state_function_registry: list[StateFunction] = []
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _register_state_function(sf: StateFunction) -> None:
|
|
480
|
+
"""Register a state function."""
|
|
481
|
+
if sf not in _state_function_registry:
|
|
482
|
+
_state_function_registry.append(sf)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def get_registered_state_functions() -> list[StateFunction]:
|
|
486
|
+
"""Get all registered state functions."""
|
|
487
|
+
return list(_state_function_registry)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def clear_state_function_registry() -> None:
|
|
491
|
+
"""Clear the state function registry."""
|
|
492
|
+
_state_function_registry.clear()
|
|
493
|
+
StateFunction._id_counter = 0
|
|
494
|
+
TransitionMatrix._id_counter = 0
|