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,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Expression functions for interval variables.
|
|
3
|
+
|
|
4
|
+
This module provides accessor functions that return expressions from interval variables:
|
|
5
|
+
- start_of, end_of, size_of, length_of, presence_of
|
|
6
|
+
- overlap_length
|
|
7
|
+
- expr_min, expr_max
|
|
8
|
+
- Sequence accessors: start_of_next, start_of_prev, etc.
|
|
9
|
+
- Element expressions for array indexing: element, element2d, TransitionMatrix
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pycsp3_scheduling.expressions.interval_expr import (
|
|
15
|
+
ExprType,
|
|
16
|
+
IntervalExpr,
|
|
17
|
+
end_of,
|
|
18
|
+
expr_max,
|
|
19
|
+
expr_min,
|
|
20
|
+
length_of,
|
|
21
|
+
overlap_length,
|
|
22
|
+
presence_of,
|
|
23
|
+
size_of,
|
|
24
|
+
start_of,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from pycsp3_scheduling.expressions.sequence_expr import (
|
|
28
|
+
start_of_next,
|
|
29
|
+
start_of_prev,
|
|
30
|
+
end_of_next,
|
|
31
|
+
end_of_prev,
|
|
32
|
+
size_of_next,
|
|
33
|
+
size_of_prev,
|
|
34
|
+
length_of_next,
|
|
35
|
+
length_of_prev,
|
|
36
|
+
type_of_next,
|
|
37
|
+
type_of_prev,
|
|
38
|
+
clear_sequence_expr_cache,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from pycsp3_scheduling.expressions.element import (
|
|
42
|
+
ElementMatrix,
|
|
43
|
+
element,
|
|
44
|
+
element2d,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Expression class
|
|
49
|
+
"IntervalExpr",
|
|
50
|
+
"ExprType",
|
|
51
|
+
# Basic accessors
|
|
52
|
+
"start_of",
|
|
53
|
+
"end_of",
|
|
54
|
+
"size_of",
|
|
55
|
+
"length_of",
|
|
56
|
+
"presence_of",
|
|
57
|
+
# Overlap
|
|
58
|
+
"overlap_length",
|
|
59
|
+
# Utilities
|
|
60
|
+
"expr_min",
|
|
61
|
+
"expr_max",
|
|
62
|
+
# Sequence accessors - Next
|
|
63
|
+
"start_of_next",
|
|
64
|
+
"end_of_next",
|
|
65
|
+
"size_of_next",
|
|
66
|
+
"length_of_next",
|
|
67
|
+
"type_of_next",
|
|
68
|
+
# Sequence accessors - Prev
|
|
69
|
+
"start_of_prev",
|
|
70
|
+
"end_of_prev",
|
|
71
|
+
"size_of_prev",
|
|
72
|
+
"length_of_prev",
|
|
73
|
+
"type_of_prev",
|
|
74
|
+
# Element expressions (array indexing with variables)
|
|
75
|
+
"ElementMatrix",
|
|
76
|
+
"element",
|
|
77
|
+
"element2d",
|
|
78
|
+
# Cache management
|
|
79
|
+
"clear_sequence_expr_cache",
|
|
80
|
+
]
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Element expressions for array indexing with variable indices.
|
|
3
|
+
|
|
4
|
+
This module provides the ability to index into arrays using expressions
|
|
5
|
+
(like type_of_next), similar to CP Optimizer's IloNumArray2 pattern.
|
|
6
|
+
|
|
7
|
+
The key use case is building objectives like:
|
|
8
|
+
sum(M[type_i][type_of_next(route, visit[i], last, abs)] for i in intervals)
|
|
9
|
+
|
|
10
|
+
Where M is a transition cost matrix indexed by interval types.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Sequence, Union
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pycsp3_scheduling.variables.interval import IntervalVar
|
|
20
|
+
from pycsp3_scheduling.variables.sequence import SequenceVar
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ElementMatrix:
|
|
25
|
+
"""
|
|
26
|
+
A 2D matrix that can be indexed with expressions.
|
|
27
|
+
|
|
28
|
+
This class wraps a 2D list of values and provides element-style indexing
|
|
29
|
+
where indices can be pycsp3 variables or expressions. It's designed to
|
|
30
|
+
work with type_of_next() for computing transition costs in scheduling.
|
|
31
|
+
|
|
32
|
+
The matrix supports two special values for boundary cases:
|
|
33
|
+
- last_value: Used when an interval is the last in its sequence
|
|
34
|
+
- absent_value: Used when an interval is absent (optional and not selected)
|
|
35
|
+
|
|
36
|
+
Example (CP Optimizer pattern):
|
|
37
|
+
# Travel distance matrix indexed by customer types
|
|
38
|
+
M = ElementMatrix(
|
|
39
|
+
matrix=travel_times, # 2D list [from_type][to_type]
|
|
40
|
+
last_value=depot_distances, # 1D list or scalar for return to depot
|
|
41
|
+
absent_value=0, # 0 cost if interval is absent
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Objective: minimize total travel distance
|
|
45
|
+
for k in vehicles:
|
|
46
|
+
for i in intervals:
|
|
47
|
+
cost = M[type_i, type_of_next(route[k], visit[k][i])]
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
matrix: 2D list of transition costs [from_type][to_type].
|
|
51
|
+
last_value: Value(s) when next is "last" (end of sequence).
|
|
52
|
+
Can be a scalar or 1D list indexed by from_type.
|
|
53
|
+
absent_value: Value when interval is absent (scalar or 1D list).
|
|
54
|
+
n_rows: Number of rows (from_types).
|
|
55
|
+
n_cols: Number of columns (to_types).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
matrix: list[list[int | float]]
|
|
59
|
+
last_value: int | float | list[int | float] = 0
|
|
60
|
+
absent_value: int | float | list[int | float] = 0
|
|
61
|
+
_flat_vars: Any = field(default=None, repr=False, compare=False)
|
|
62
|
+
_n_rows: int = field(default=-1, repr=False, compare=False)
|
|
63
|
+
_n_cols: int = field(default=-1, repr=False, compare=False)
|
|
64
|
+
_last_type: int = field(default=-1, repr=False, compare=False)
|
|
65
|
+
_absent_type: int = field(default=-1, repr=False, compare=False)
|
|
66
|
+
|
|
67
|
+
def __post_init__(self) -> None:
|
|
68
|
+
"""Validate and setup the matrix."""
|
|
69
|
+
if not self.matrix:
|
|
70
|
+
raise ValueError("Matrix cannot be empty")
|
|
71
|
+
|
|
72
|
+
# Validate rectangular matrix
|
|
73
|
+
self._n_rows = len(self.matrix)
|
|
74
|
+
self._n_cols = len(self.matrix[0])
|
|
75
|
+
for row in self.matrix:
|
|
76
|
+
if len(row) != self._n_cols:
|
|
77
|
+
raise ValueError("Matrix must be rectangular")
|
|
78
|
+
|
|
79
|
+
# Special type indices: last = n_cols, absent = n_cols + 1
|
|
80
|
+
self._last_type = self._n_cols
|
|
81
|
+
self._absent_type = self._n_cols + 1
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def n_rows(self) -> int:
|
|
85
|
+
"""Number of rows (from_types)."""
|
|
86
|
+
return self._n_rows
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def n_cols(self) -> int:
|
|
90
|
+
"""Number of columns (to_types), excluding special types."""
|
|
91
|
+
return self._n_cols
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def last_type(self) -> int:
|
|
95
|
+
"""Type index representing 'last' (end of sequence)."""
|
|
96
|
+
return self._last_type
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def absent_type(self) -> int:
|
|
100
|
+
"""Type index representing 'absent' (interval not scheduled)."""
|
|
101
|
+
return self._absent_type
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def total_cols(self) -> int:
|
|
105
|
+
"""Total columns including special types (last, absent)."""
|
|
106
|
+
return self._n_cols + 2
|
|
107
|
+
|
|
108
|
+
def _get_last_value(self, row: int) -> int | float:
|
|
109
|
+
"""Get the last_value for a given row."""
|
|
110
|
+
if isinstance(self.last_value, (int, float)):
|
|
111
|
+
return self.last_value
|
|
112
|
+
return self.last_value[row]
|
|
113
|
+
|
|
114
|
+
def _get_absent_value(self, row: int) -> int | float:
|
|
115
|
+
"""Get the absent_value for a given row."""
|
|
116
|
+
if isinstance(self.absent_value, (int, float)):
|
|
117
|
+
return self.absent_value
|
|
118
|
+
return self.absent_value[row]
|
|
119
|
+
|
|
120
|
+
def build_extended_matrix(self) -> list[list[int | float]]:
|
|
121
|
+
"""
|
|
122
|
+
Build the full matrix including last and absent columns.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Matrix of shape [n_rows][n_cols + 2] where:
|
|
126
|
+
- columns 0..n_cols-1 are regular transitions
|
|
127
|
+
- column n_cols is the "last" value
|
|
128
|
+
- column n_cols+1 is the "absent" value
|
|
129
|
+
"""
|
|
130
|
+
extended = []
|
|
131
|
+
for i, row in enumerate(self.matrix):
|
|
132
|
+
new_row = list(row) + [self._get_last_value(i), self._get_absent_value(i)]
|
|
133
|
+
extended.append(new_row)
|
|
134
|
+
return extended
|
|
135
|
+
|
|
136
|
+
def _ensure_flat_vars(self) -> None:
|
|
137
|
+
"""Create flattened pycsp3 VarArray for element constraints (lazy)."""
|
|
138
|
+
if self._flat_vars is not None:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
from pycsp3 import VarArray
|
|
143
|
+
except ImportError:
|
|
144
|
+
raise ImportError("pycsp3 is required for element constraints")
|
|
145
|
+
|
|
146
|
+
# Build extended matrix with last/absent columns
|
|
147
|
+
extended = self.build_extended_matrix()
|
|
148
|
+
|
|
149
|
+
# Flatten to 1D for element constraint
|
|
150
|
+
flat = [val for row in extended for val in row]
|
|
151
|
+
|
|
152
|
+
# Create VarArray with singleton domains (constants)
|
|
153
|
+
# Use a unique name based on id to avoid conflicts
|
|
154
|
+
# Note: XCSP3 IDs must start with a letter, not underscore
|
|
155
|
+
var_id = f"tm{id(self)}"
|
|
156
|
+
self._flat_vars = VarArray(
|
|
157
|
+
size=len(flat),
|
|
158
|
+
dom=lambda k: {int(flat[k])}, # Singleton domain = constant
|
|
159
|
+
id=var_id,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def __getitem__(self, indices: tuple) -> Any:
|
|
163
|
+
"""
|
|
164
|
+
Index the matrix with expressions.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
indices: Tuple of (row_index, col_index) where each can be:
|
|
168
|
+
- An integer constant
|
|
169
|
+
- A pycsp3 variable or expression
|
|
170
|
+
- The result of type_of_next() etc.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
A pycsp3 element expression that evaluates to matrix[row][col].
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
M = TransitionMatrix(travel_times)
|
|
177
|
+
cost = M[type_i, type_of_next(route, interval)]
|
|
178
|
+
"""
|
|
179
|
+
if not isinstance(indices, tuple) or len(indices) != 2:
|
|
180
|
+
raise TypeError("TransitionMatrix requires 2D indexing: M[row, col]")
|
|
181
|
+
|
|
182
|
+
row_idx, col_idx = indices
|
|
183
|
+
|
|
184
|
+
# Ensure flat vars exist
|
|
185
|
+
self._ensure_flat_vars()
|
|
186
|
+
|
|
187
|
+
# Compute linear index: row * total_cols + col
|
|
188
|
+
total_cols = self.total_cols
|
|
189
|
+
|
|
190
|
+
# Build the linear index expression
|
|
191
|
+
try:
|
|
192
|
+
linear_idx = row_idx * total_cols + col_idx
|
|
193
|
+
except TypeError:
|
|
194
|
+
# If indices don't support arithmetic, wrap them
|
|
195
|
+
from pycsp3.classes.nodes import Node, TypeNode
|
|
196
|
+
row_node = row_idx if hasattr(row_idx, 'cnt') else row_idx
|
|
197
|
+
col_node = col_idx if hasattr(col_idx, 'cnt') else col_idx
|
|
198
|
+
linear_idx = Node.build(
|
|
199
|
+
TypeNode.ADD,
|
|
200
|
+
Node.build(TypeNode.MUL, row_node, total_cols),
|
|
201
|
+
col_node
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Return element expression
|
|
205
|
+
return self._flat_vars[linear_idx]
|
|
206
|
+
|
|
207
|
+
def get_value(self, row: int, col: int) -> int | float:
|
|
208
|
+
"""
|
|
209
|
+
Get a constant value from the matrix (no expression).
|
|
210
|
+
|
|
211
|
+
Use this for debugging or when indices are known constants.
|
|
212
|
+
For variable indices, use __getitem__ instead.
|
|
213
|
+
"""
|
|
214
|
+
if col == self._last_type:
|
|
215
|
+
return self._get_last_value(row)
|
|
216
|
+
elif col == self._absent_type:
|
|
217
|
+
return self._get_absent_value(row)
|
|
218
|
+
else:
|
|
219
|
+
return self.matrix[row][col]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def element(array: Sequence, index: Any) -> Any:
|
|
223
|
+
"""
|
|
224
|
+
Create an element expression for array indexing with a variable index.
|
|
225
|
+
|
|
226
|
+
This provides a clean way to access array[index] where index is a
|
|
227
|
+
pycsp3 variable or expression.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
array: A list of values or VarArray.
|
|
231
|
+
index: A pycsp3 variable, expression, or integer.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
A pycsp3 element expression.
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
costs = [10, 20, 30, 40, 50]
|
|
238
|
+
idx = type_of_next(route, interval)
|
|
239
|
+
cost = element(costs, idx) # Returns costs[type_of_next(...)]
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
from pycsp3 import VarArray
|
|
243
|
+
from pycsp3.classes.main.variables import Variable
|
|
244
|
+
except ImportError:
|
|
245
|
+
raise ImportError("pycsp3 is required for element constraints")
|
|
246
|
+
|
|
247
|
+
# If index is a constant integer, just return the value
|
|
248
|
+
if isinstance(index, int):
|
|
249
|
+
return array[index]
|
|
250
|
+
|
|
251
|
+
# If array is already a VarArray, use it directly
|
|
252
|
+
if hasattr(array, '__getitem__') and hasattr(array[0] if array else None, 'dom'):
|
|
253
|
+
return array[index]
|
|
254
|
+
|
|
255
|
+
# Convert constant list to VarArray with singleton domains
|
|
256
|
+
# Note: XCSP3 IDs must start with a letter, not underscore
|
|
257
|
+
var_id = f"elem{id(array)}"
|
|
258
|
+
var_array = VarArray(
|
|
259
|
+
size=len(array),
|
|
260
|
+
dom=lambda k: {int(array[k])},
|
|
261
|
+
id=var_id,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return var_array[index]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def element2d(matrix: Sequence[Sequence], row_idx: Any, col_idx: Any) -> Any:
|
|
268
|
+
"""
|
|
269
|
+
Create an element expression for 2D array indexing with variable indices.
|
|
270
|
+
|
|
271
|
+
This provides matrix[row][col] access where row and col can be expressions.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
matrix: A 2D list of values.
|
|
275
|
+
row_idx: Row index (variable or expression).
|
|
276
|
+
col_idx: Column index (variable or expression).
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
A pycsp3 element expression.
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
travel = [[0, 10, 20], [10, 0, 15], [20, 15, 0]]
|
|
283
|
+
i = type_of(interval_from)
|
|
284
|
+
j = type_of_next(route, interval_from)
|
|
285
|
+
cost = element2d(travel, i, j)
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
from pycsp3 import VarArray
|
|
289
|
+
except ImportError:
|
|
290
|
+
raise ImportError("pycsp3 is required for element constraints")
|
|
291
|
+
|
|
292
|
+
# If both indices are constants, return the value directly
|
|
293
|
+
if isinstance(row_idx, int) and isinstance(col_idx, int):
|
|
294
|
+
return matrix[row_idx][col_idx]
|
|
295
|
+
|
|
296
|
+
# Flatten the matrix
|
|
297
|
+
n_rows = len(matrix)
|
|
298
|
+
n_cols = len(matrix[0])
|
|
299
|
+
flat = [matrix[i][j] for i in range(n_rows) for j in range(n_cols)]
|
|
300
|
+
|
|
301
|
+
# Create VarArray for flattened matrix
|
|
302
|
+
# Note: XCSP3 IDs must start with a letter, not underscore
|
|
303
|
+
var_id = f"elem2d{id(matrix)}"
|
|
304
|
+
flat_vars = VarArray(
|
|
305
|
+
size=len(flat),
|
|
306
|
+
dom=lambda k: {int(flat[k])},
|
|
307
|
+
id=var_id,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Compute linear index
|
|
311
|
+
linear_idx = row_idx * n_cols + col_idx
|
|
312
|
+
|
|
313
|
+
return flat_vars[linear_idx]
|