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,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]