openscvx 0.3.2.dev170__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.
Potentially problematic release.
This version of openscvx might be problematic. Click here for more details.
- openscvx/__init__.py +123 -0
- openscvx/_version.py +34 -0
- openscvx/algorithms/__init__.py +92 -0
- openscvx/algorithms/autotuning.py +24 -0
- openscvx/algorithms/base.py +351 -0
- openscvx/algorithms/optimization_results.py +215 -0
- openscvx/algorithms/penalized_trust_region.py +384 -0
- openscvx/config.py +437 -0
- openscvx/discretization/__init__.py +47 -0
- openscvx/discretization/discretization.py +236 -0
- openscvx/expert/__init__.py +23 -0
- openscvx/expert/byof.py +326 -0
- openscvx/expert/lowering.py +419 -0
- openscvx/expert/validation.py +357 -0
- openscvx/integrators/__init__.py +48 -0
- openscvx/integrators/runge_kutta.py +281 -0
- openscvx/lowered/__init__.py +30 -0
- openscvx/lowered/cvxpy_constraints.py +23 -0
- openscvx/lowered/cvxpy_variables.py +124 -0
- openscvx/lowered/dynamics.py +34 -0
- openscvx/lowered/jax_constraints.py +133 -0
- openscvx/lowered/parameters.py +54 -0
- openscvx/lowered/problem.py +70 -0
- openscvx/lowered/unified.py +718 -0
- openscvx/plotting/__init__.py +63 -0
- openscvx/plotting/plotting.py +756 -0
- openscvx/plotting/scp_iteration.py +299 -0
- openscvx/plotting/viser/__init__.py +126 -0
- openscvx/plotting/viser/animated.py +605 -0
- openscvx/plotting/viser/plotly_integration.py +333 -0
- openscvx/plotting/viser/primitives.py +355 -0
- openscvx/plotting/viser/scp.py +459 -0
- openscvx/plotting/viser/server.py +112 -0
- openscvx/problem.py +734 -0
- openscvx/propagation/__init__.py +60 -0
- openscvx/propagation/post_processing.py +104 -0
- openscvx/propagation/propagation.py +248 -0
- openscvx/solvers/__init__.py +51 -0
- openscvx/solvers/cvxpy.py +226 -0
- openscvx/symbolic/__init__.py +9 -0
- openscvx/symbolic/augmentation.py +630 -0
- openscvx/symbolic/builder.py +492 -0
- openscvx/symbolic/constraint_set.py +92 -0
- openscvx/symbolic/expr/__init__.py +222 -0
- openscvx/symbolic/expr/arithmetic.py +517 -0
- openscvx/symbolic/expr/array.py +632 -0
- openscvx/symbolic/expr/constraint.py +796 -0
- openscvx/symbolic/expr/control.py +135 -0
- openscvx/symbolic/expr/expr.py +720 -0
- openscvx/symbolic/expr/lie/__init__.py +87 -0
- openscvx/symbolic/expr/lie/adjoint.py +357 -0
- openscvx/symbolic/expr/lie/se3.py +172 -0
- openscvx/symbolic/expr/lie/so3.py +138 -0
- openscvx/symbolic/expr/linalg.py +279 -0
- openscvx/symbolic/expr/math.py +699 -0
- openscvx/symbolic/expr/spatial.py +209 -0
- openscvx/symbolic/expr/state.py +607 -0
- openscvx/symbolic/expr/stl.py +136 -0
- openscvx/symbolic/expr/variable.py +321 -0
- openscvx/symbolic/hashing.py +112 -0
- openscvx/symbolic/lower.py +760 -0
- openscvx/symbolic/lowerers/__init__.py +106 -0
- openscvx/symbolic/lowerers/cvxpy.py +1302 -0
- openscvx/symbolic/lowerers/jax.py +1382 -0
- openscvx/symbolic/preprocessing.py +757 -0
- openscvx/symbolic/problem.py +110 -0
- openscvx/symbolic/time.py +116 -0
- openscvx/symbolic/unified.py +420 -0
- openscvx/utils/__init__.py +20 -0
- openscvx/utils/cache.py +131 -0
- openscvx/utils/caching.py +210 -0
- openscvx/utils/printing.py +301 -0
- openscvx/utils/profiling.py +37 -0
- openscvx/utils/utils.py +100 -0
- openscvx-0.3.2.dev170.dist-info/METADATA +350 -0
- openscvx-0.3.2.dev170.dist-info/RECORD +79 -0
- openscvx-0.3.2.dev170.dist-info/WHEEL +5 -0
- openscvx-0.3.2.dev170.dist-info/licenses/LICENSE +201 -0
- openscvx-0.3.2.dev170.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from .variable import Variable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BoundaryType(str, Enum):
|
|
10
|
+
"""Enumeration of boundary condition types for state variables.
|
|
11
|
+
|
|
12
|
+
This enum allows users to specify boundary conditions using plain strings
|
|
13
|
+
while maintaining type safety internally. Boundary conditions control how
|
|
14
|
+
the optimizer handles initial and final state values.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
FIXED (str): State value is fixed to a specific value
|
|
18
|
+
FREE (str): State value is free to be optimized within bounds
|
|
19
|
+
MINIMIZE (str): Objective term to minimize the state value
|
|
20
|
+
MAXIMIZE (str): Objective term to maximize the state value
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
Can use either enum or string:
|
|
24
|
+
|
|
25
|
+
BoundaryType.FIXED
|
|
26
|
+
"fixed" # Equivalent
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
FIXED = "fixed"
|
|
30
|
+
FREE = "free"
|
|
31
|
+
MINIMIZE = "minimize"
|
|
32
|
+
MAXIMIZE = "maximize"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def Free(guess):
|
|
36
|
+
"""Create a free boundary condition tuple.
|
|
37
|
+
|
|
38
|
+
This is a convenience function that returns a tuple ("free", guess) which
|
|
39
|
+
can be used to specify free boundary conditions for State or Time objects.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
guess: Initial guess value for the free variable.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
tuple: ("free", guess) tuple suitable for use in State.initial, State.final,
|
|
46
|
+
or Time.initial, Time.final.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
```python
|
|
50
|
+
pos = ox.State("pos", (3,))
|
|
51
|
+
pos.final = [ox.Free(5.0), ox.Free(3.0), 10] # First two free, third fixed
|
|
52
|
+
|
|
53
|
+
time = ox.Time(
|
|
54
|
+
initial=0.0,
|
|
55
|
+
final=ox.Free(10.0),
|
|
56
|
+
min=0.0,
|
|
57
|
+
max=20.0
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
"""
|
|
61
|
+
return ("free", guess)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def Fixed(value):
|
|
65
|
+
"""Create a fixed boundary condition tuple.
|
|
66
|
+
|
|
67
|
+
This is a convenience function that returns a tuple ("fixed", value) which
|
|
68
|
+
can be used to explicitly specify fixed boundary conditions for State or Time objects.
|
|
69
|
+
Note that plain numbers default to fixed, so this is mainly for clarity.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
value: Fixed value for the boundary condition.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
tuple: ("fixed", value) tuple suitable for use in State.initial, State.final,
|
|
76
|
+
or Time.initial, Time.final.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
```python
|
|
80
|
+
pos = ox.State("pos", (3,))
|
|
81
|
+
pos.final = [ox.Fixed(10.0), ox.Free(5.0), ox.Fixed(2.0)]
|
|
82
|
+
|
|
83
|
+
# Equivalent to:
|
|
84
|
+
pos.final = [10.0, ox.Free(5.0), 2.0] # Plain numbers default to fixed
|
|
85
|
+
```
|
|
86
|
+
"""
|
|
87
|
+
return ("fixed", value)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def Minimize(guess):
|
|
91
|
+
"""Create a minimize boundary condition tuple.
|
|
92
|
+
|
|
93
|
+
This is a convenience function that returns a tuple ("minimize", guess) which
|
|
94
|
+
can be used to specify that a boundary value should be minimized in the objective
|
|
95
|
+
function for State or Time objects.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
guess: Initial guess value for the variable to be minimized.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
tuple: ("minimize", guess) tuple suitable for use in State.initial, State.final,
|
|
102
|
+
or Time.initial, Time.final.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
```python
|
|
106
|
+
time = ox.Time(
|
|
107
|
+
initial=0.0,
|
|
108
|
+
final=ox.Minimize(10.0), # Minimize final time
|
|
109
|
+
min=0.0,
|
|
110
|
+
max=20.0
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
fuel = ox.State("fuel", (1,))
|
|
114
|
+
fuel.final = [ox.Minimize(0)] # Minimize final fuel consumption
|
|
115
|
+
```
|
|
116
|
+
"""
|
|
117
|
+
return ("minimize", guess)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def Maximize(guess):
|
|
121
|
+
"""Create a maximize boundary condition tuple.
|
|
122
|
+
|
|
123
|
+
This is a convenience function that returns a tuple ("maximize", guess) which
|
|
124
|
+
can be used to specify that a boundary value should be maximized in the objective
|
|
125
|
+
function for State or Time objects.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
guess: Initial guess value for the variable to be maximized.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
tuple: ("maximize", guess) tuple suitable for use in State.initial, State.final,
|
|
132
|
+
or Time.initial, Time.final.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
```python
|
|
136
|
+
altitude = ox.State("altitude", (1,))
|
|
137
|
+
altitude.final = [ox.Maximize(100.0)] # Maximize final altitude
|
|
138
|
+
|
|
139
|
+
time = ox.Time(
|
|
140
|
+
initial=ox.Maximize(0.0), # Maximize initial time
|
|
141
|
+
final=10.0,
|
|
142
|
+
min=0.0,
|
|
143
|
+
max=20.0
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
"""
|
|
147
|
+
return ("maximize", guess)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class State(Variable):
|
|
151
|
+
"""State variable with boundary conditions for trajectory optimization.
|
|
152
|
+
|
|
153
|
+
State represents a dynamic state variable in a trajectory optimization problem.
|
|
154
|
+
Unlike control inputs, states evolve according to dynamics constraints and can
|
|
155
|
+
have boundary conditions specified at the initial and final time points.
|
|
156
|
+
Like all Variables, States also support min/max bounds and initial trajectory
|
|
157
|
+
guesses to help guide the optimization solver toward good solutions.
|
|
158
|
+
|
|
159
|
+
States support four types of boundary conditions:
|
|
160
|
+
|
|
161
|
+
- **fixed**: State value is constrained to a specific value
|
|
162
|
+
- **free**: State value is optimized within the specified bounds
|
|
163
|
+
- **minimize**: Adds a term to the objective function to minimize the state value
|
|
164
|
+
- **maximize**: Adds a term to the objective function to maximize the state value
|
|
165
|
+
|
|
166
|
+
Each element of a multi-dimensional state can have different boundary condition
|
|
167
|
+
types, allowing for fine-grained control over the optimization.
|
|
168
|
+
|
|
169
|
+
Attributes:
|
|
170
|
+
name (str): Unique name identifier for this state variable
|
|
171
|
+
_shape (tuple[int, ...]): Shape of the state vector (typically 1D like (3,) for 3D position)
|
|
172
|
+
_slice (slice | None): Internal slice information for variable indexing
|
|
173
|
+
_min (np.ndarray | None): Minimum bounds for state variables
|
|
174
|
+
_max (np.ndarray | None): Maximum bounds for state variables
|
|
175
|
+
_guess (np.ndarray | None): Initial trajectory guess
|
|
176
|
+
_initial (np.ndarray | None): Initial state values with boundary condition types
|
|
177
|
+
initial_type (np.ndarray | None): Array of boundary condition types for initial state
|
|
178
|
+
_final (np.ndarray | None): Final state values with boundary condition types
|
|
179
|
+
final_type (np.ndarray | None): Array of boundary condition types for final state
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
Scalar time state with fixed initial time, minimize final time:
|
|
183
|
+
|
|
184
|
+
time = State("time", (1,))
|
|
185
|
+
time.min = [0.0]
|
|
186
|
+
time.max = [10.0]
|
|
187
|
+
time.initial = [("fixed", 0.0)]
|
|
188
|
+
time.final = [("minimize", 5.0)]
|
|
189
|
+
|
|
190
|
+
3D position state with mixed boundary conditions:
|
|
191
|
+
|
|
192
|
+
pos = State("pos", (3,))
|
|
193
|
+
pos.min = [0, 0, 10]
|
|
194
|
+
pos.max = [10, 10, 200]
|
|
195
|
+
pos.initial = [0, ("free", 1), 50] # x fixed, y free, z fixed
|
|
196
|
+
pos.final = [10, ("free", 5), ("maximize", 150)] # Maximize final altitude
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, name, shape):
|
|
200
|
+
"""Initialize a State object.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
name: Name identifier for the state variable
|
|
204
|
+
shape: Shape of the state vector (typically 1D tuple)
|
|
205
|
+
"""
|
|
206
|
+
super().__init__(name, shape)
|
|
207
|
+
self._initial = None
|
|
208
|
+
self.initial_type = None
|
|
209
|
+
self._final = None
|
|
210
|
+
self.final_type = None
|
|
211
|
+
self._scaling_min = None
|
|
212
|
+
self._scaling_max = None
|
|
213
|
+
|
|
214
|
+
def _hash_into(self, hasher: "hashlib._Hash") -> None:
|
|
215
|
+
"""Hash State including boundary condition types.
|
|
216
|
+
|
|
217
|
+
Extends Variable._hash_into to include the structural metadata that
|
|
218
|
+
affects the compiled problem: boundary condition types (fixed, free,
|
|
219
|
+
minimize, maximize). Values are not hashed as they are runtime parameters.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
hasher: A hashlib hash object to update
|
|
223
|
+
"""
|
|
224
|
+
# Hash the base Variable attributes (class name, shape, slice)
|
|
225
|
+
super()._hash_into(hasher)
|
|
226
|
+
# Hash boundary condition types (these affect constraint structure)
|
|
227
|
+
if self.initial_type is not None:
|
|
228
|
+
hasher.update(b"initial_type:")
|
|
229
|
+
hasher.update(str(self.initial_type.tolist()).encode())
|
|
230
|
+
if self.final_type is not None:
|
|
231
|
+
hasher.update(b"final_type:")
|
|
232
|
+
hasher.update(str(self.final_type.tolist()).encode())
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def min(self):
|
|
236
|
+
"""Get the minimum bounds for the state variables.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Array of minimum values for each state variable element.
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
Get lower bounds:
|
|
243
|
+
|
|
244
|
+
pos = State("pos", (3,))
|
|
245
|
+
pos.min = [0, 0, 10]
|
|
246
|
+
print(pos.min) # [0. 0. 10.]
|
|
247
|
+
"""
|
|
248
|
+
return self._min
|
|
249
|
+
|
|
250
|
+
@min.setter
|
|
251
|
+
def min(self, val):
|
|
252
|
+
"""Set the minimum bounds for the state variables.
|
|
253
|
+
|
|
254
|
+
Bounds are validated against any fixed initial/final conditions to ensure
|
|
255
|
+
consistency.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
val: Array of minimum values, must match the state shape exactly
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ValueError: If the shape doesn't match the state shape, or if fixed
|
|
262
|
+
boundary conditions violate the bounds
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
Set lower bounds:
|
|
266
|
+
|
|
267
|
+
pos = State("pos", (3,))
|
|
268
|
+
pos.min = [0, 0, 10]
|
|
269
|
+
pos.initial = [0, 5, 15] # Must satisfy: 0>=0, 5>=0, 15>=10
|
|
270
|
+
"""
|
|
271
|
+
val = np.asarray(val, dtype=float)
|
|
272
|
+
if val.shape != self.shape:
|
|
273
|
+
raise ValueError(f"Min shape {val.shape} does not match State shape {self.shape}")
|
|
274
|
+
self._min = val
|
|
275
|
+
self._check_bounds_against_initial_final()
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def max(self):
|
|
279
|
+
"""Get the maximum bounds for the state variables.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Array of maximum values for each state variable element.
|
|
283
|
+
|
|
284
|
+
Example:
|
|
285
|
+
Get upper bounds:
|
|
286
|
+
|
|
287
|
+
vel = State("vel", (3,))
|
|
288
|
+
vel.max = [10, 10, 5]
|
|
289
|
+
print(vel.max) # [10. 10. 5.]
|
|
290
|
+
"""
|
|
291
|
+
return self._max
|
|
292
|
+
|
|
293
|
+
@max.setter
|
|
294
|
+
def max(self, val):
|
|
295
|
+
"""Set the maximum bounds for the state variables.
|
|
296
|
+
|
|
297
|
+
Bounds are validated against any fixed initial/final conditions to ensure
|
|
298
|
+
consistency.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
val: Array of maximum values, must match the state shape exactly
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
ValueError: If the shape doesn't match the state shape, or if fixed
|
|
305
|
+
boundary conditions violate the bounds
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
Set upper bounds:
|
|
309
|
+
|
|
310
|
+
vel = State("vel", (3,))
|
|
311
|
+
vel.max = [10, 10, 5]
|
|
312
|
+
vel.final = [8, 9, 4] # Must satisfy: 8<=10, 9<=10, 4<=5
|
|
313
|
+
"""
|
|
314
|
+
val = np.asarray(val, dtype=float)
|
|
315
|
+
if val.shape != self.shape:
|
|
316
|
+
raise ValueError(f"Max shape {val.shape} does not match State shape {self.shape}")
|
|
317
|
+
self._max = val
|
|
318
|
+
self._check_bounds_against_initial_final()
|
|
319
|
+
|
|
320
|
+
def _check_bounds_against_initial_final(self):
|
|
321
|
+
"""Validate that fixed boundary conditions respect min/max bounds.
|
|
322
|
+
|
|
323
|
+
This internal method is automatically called when bounds or boundary
|
|
324
|
+
conditions are set to ensure consistency.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
ValueError: If any fixed initial or final value violates the min/max bounds
|
|
328
|
+
"""
|
|
329
|
+
for field_name, data, types in [
|
|
330
|
+
("initial", self._initial, self.initial_type),
|
|
331
|
+
("final", self._final, self.final_type),
|
|
332
|
+
]:
|
|
333
|
+
if data is None or types is None:
|
|
334
|
+
continue
|
|
335
|
+
for i, val in np.ndenumerate(data):
|
|
336
|
+
if types[i] != "Fix":
|
|
337
|
+
continue
|
|
338
|
+
min_i = self._min[i] if self._min is not None else -np.inf
|
|
339
|
+
max_i = self._max[i] if self._max is not None else np.inf
|
|
340
|
+
if val < min_i:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
f"{field_name.capitalize()} Fixed value at index {i[0]} is lower then the "
|
|
343
|
+
f"min: {val} < {min_i}"
|
|
344
|
+
)
|
|
345
|
+
if val > max_i:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f"{field_name.capitalize()} Fixed value at index {i[0]} is greater then "
|
|
348
|
+
f"the max: {val} > {max_i}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def initial(self):
|
|
353
|
+
"""Get the initial state boundary condition values.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Array of initial state values (regardless of boundary condition type),
|
|
357
|
+
or None if not set.
|
|
358
|
+
|
|
359
|
+
Note:
|
|
360
|
+
Use `initial_type` to see the boundary condition types for each element.
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
Get initial state boundary conditions:
|
|
364
|
+
|
|
365
|
+
x = State("x", (2,))
|
|
366
|
+
x.initial = [0, ("free", 1)]
|
|
367
|
+
print(x.initial) # [0. 1.]
|
|
368
|
+
print(x.initial_type) # ['Fix' 'Free']
|
|
369
|
+
"""
|
|
370
|
+
return self._initial
|
|
371
|
+
|
|
372
|
+
@initial.setter
|
|
373
|
+
def initial(self, arr):
|
|
374
|
+
"""Set the initial state boundary conditions.
|
|
375
|
+
|
|
376
|
+
Each element can be specified as either a simple number (defaults to "fixed")
|
|
377
|
+
or a tuple of (type, value) where type specifies the boundary condition.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
arr: Array-like of initial conditions. Each element can be:
|
|
381
|
+
- A number: Defaults to fixed boundary condition at that value
|
|
382
|
+
- A tuple (type, value): Where type is one of:
|
|
383
|
+
- "fixed": Constrain state to this exact value
|
|
384
|
+
- "free": Let optimizer choose within bounds, initialize at value
|
|
385
|
+
- "minimize": Add objective term to minimize, initialize at value
|
|
386
|
+
- "maximize": Add objective term to maximize, initialize at value
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
ValueError: If the shape doesn't match the state shape, if boundary
|
|
390
|
+
condition type is invalid, or if fixed values violate bounds
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
Set initial state boundary conditions:
|
|
394
|
+
|
|
395
|
+
pos = State("pos", (3,))
|
|
396
|
+
pos.min = [0, 0, 0]
|
|
397
|
+
pos.max = [10, 10, 10]
|
|
398
|
+
# x fixed at 0, y free (starts at 5), z fixed at 2
|
|
399
|
+
pos.initial = [0, ("free", 5), 2]
|
|
400
|
+
|
|
401
|
+
Can also minimize/maximize boundary values:
|
|
402
|
+
|
|
403
|
+
time = State("t", (1,))
|
|
404
|
+
time.initial = [("minimize", 0)] # Minimize initial time
|
|
405
|
+
"""
|
|
406
|
+
# Convert to list first to handle mixed types properly
|
|
407
|
+
if not isinstance(arr, (list, tuple)):
|
|
408
|
+
arr = np.asarray(arr)
|
|
409
|
+
if arr.shape != self.shape:
|
|
410
|
+
raise ValueError(f"Shape mismatch: {arr.shape} != {self.shape}")
|
|
411
|
+
arr = arr.tolist()
|
|
412
|
+
|
|
413
|
+
# Ensure we have the right number of elements
|
|
414
|
+
if len(arr) != self.shape[0]:
|
|
415
|
+
raise ValueError(f"Length mismatch: got {len(arr)} elements, expected {self.shape[0]}")
|
|
416
|
+
|
|
417
|
+
self._initial = np.zeros(self.shape, dtype=float)
|
|
418
|
+
self.initial_type = np.full(self.shape, "Fix", dtype=object)
|
|
419
|
+
|
|
420
|
+
for i, v in enumerate(arr):
|
|
421
|
+
if isinstance(v, tuple) and len(v) == 2:
|
|
422
|
+
# Tuple API: (type, value)
|
|
423
|
+
bc_type_str, bc_value = v
|
|
424
|
+
try:
|
|
425
|
+
bc_type = BoundaryType(bc_type_str) # Validates the string
|
|
426
|
+
except ValueError:
|
|
427
|
+
valid_types = [t.value for t in BoundaryType]
|
|
428
|
+
raise ValueError(
|
|
429
|
+
f"Invalid boundary condition type: {bc_type_str}. "
|
|
430
|
+
f"Valid types are: {valid_types}"
|
|
431
|
+
)
|
|
432
|
+
self._initial[i] = float(bc_value)
|
|
433
|
+
self.initial_type[i] = bc_type.value.capitalize()
|
|
434
|
+
elif isinstance(v, (int, float, np.number)):
|
|
435
|
+
# Simple number defaults to fixed
|
|
436
|
+
self._initial[i] = float(v)
|
|
437
|
+
self.initial_type[i] = "Fix"
|
|
438
|
+
else:
|
|
439
|
+
raise ValueError(
|
|
440
|
+
f"Invalid boundary condition format: {v}. "
|
|
441
|
+
f"Use a number (defaults to fixed) or tuple ('type', value) "
|
|
442
|
+
f"where type is 'fixed', 'free', 'minimize', or 'maximize'."
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
self._check_bounds_against_initial_final()
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def final(self):
|
|
449
|
+
"""Get the final state boundary condition values.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Array of final state values (regardless of boundary condition type),
|
|
453
|
+
or None if not set.
|
|
454
|
+
|
|
455
|
+
Note:
|
|
456
|
+
Use `final_type` to see the boundary condition types for each element.
|
|
457
|
+
|
|
458
|
+
Example:
|
|
459
|
+
Get final state boundary conditions:
|
|
460
|
+
|
|
461
|
+
x = State("x", (2,))
|
|
462
|
+
x.final = [10, ("minimize", 0)]
|
|
463
|
+
print(x.final) # [10. 0.]
|
|
464
|
+
print(x.final_type) # ['Fix' 'Minimize']
|
|
465
|
+
"""
|
|
466
|
+
return self._final
|
|
467
|
+
|
|
468
|
+
@final.setter
|
|
469
|
+
def final(self, arr):
|
|
470
|
+
"""Set the final state boundary conditions.
|
|
471
|
+
|
|
472
|
+
Each element can be specified as either a simple number (defaults to "fixed")
|
|
473
|
+
or a tuple of (type, value) where type specifies the boundary condition.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
arr: Array-like of final conditions. Each element can be:
|
|
477
|
+
- A number: Defaults to fixed boundary condition at that value
|
|
478
|
+
- A tuple (type, value): Where type is one of:
|
|
479
|
+
- "fixed": Constrain state to this exact value
|
|
480
|
+
- "free": Let optimizer choose within bounds, initialize at value
|
|
481
|
+
- "minimize": Add objective term to minimize, initialize at value
|
|
482
|
+
- "maximize": Add objective term to maximize, initialize at value
|
|
483
|
+
|
|
484
|
+
Raises:
|
|
485
|
+
ValueError: If the shape doesn't match the state shape, if boundary
|
|
486
|
+
condition type is invalid, or if fixed values violate bounds
|
|
487
|
+
|
|
488
|
+
Example:
|
|
489
|
+
Set final state boundary conditionis:
|
|
490
|
+
|
|
491
|
+
pos = State("pos", (3,))
|
|
492
|
+
pos.min = [0, 0, 0]
|
|
493
|
+
pos.max = [10, 10, 10]
|
|
494
|
+
# x fixed at 10, y free (starts at 5), z maximize altitude
|
|
495
|
+
pos.final = [10, ("free", 5), ("maximize", 8)]
|
|
496
|
+
|
|
497
|
+
Minimize final time in time-optimal problem:
|
|
498
|
+
|
|
499
|
+
time = State("t", (1,))
|
|
500
|
+
time.final = [("minimize", 10)]
|
|
501
|
+
"""
|
|
502
|
+
# Convert to list first to handle mixed types properly
|
|
503
|
+
if not isinstance(arr, (list, tuple)):
|
|
504
|
+
arr = np.asarray(arr)
|
|
505
|
+
if arr.shape != self.shape:
|
|
506
|
+
raise ValueError(f"Shape mismatch: {arr.shape} != {self.shape}")
|
|
507
|
+
arr = arr.tolist()
|
|
508
|
+
|
|
509
|
+
# Ensure we have the right number of elements
|
|
510
|
+
if len(arr) != self.shape[0]:
|
|
511
|
+
raise ValueError(f"Length mismatch: got {len(arr)} elements, expected {self.shape[0]}")
|
|
512
|
+
|
|
513
|
+
self._final = np.zeros(self.shape, dtype=float)
|
|
514
|
+
self.final_type = np.full(self.shape, "Fix", dtype=object)
|
|
515
|
+
|
|
516
|
+
for i, v in enumerate(arr):
|
|
517
|
+
if isinstance(v, tuple) and len(v) == 2:
|
|
518
|
+
# Tuple API: (type, value)
|
|
519
|
+
bc_type_str, bc_value = v
|
|
520
|
+
try:
|
|
521
|
+
bc_type = BoundaryType(bc_type_str) # Validates the string
|
|
522
|
+
except ValueError:
|
|
523
|
+
valid_types = [t.value for t in BoundaryType]
|
|
524
|
+
raise ValueError(
|
|
525
|
+
f"Invalid boundary condition type: {bc_type_str}. "
|
|
526
|
+
f"Valid types are: {valid_types}"
|
|
527
|
+
)
|
|
528
|
+
self._final[i] = float(bc_value)
|
|
529
|
+
self.final_type[i] = bc_type.value.capitalize()
|
|
530
|
+
elif isinstance(v, (int, float, np.number)):
|
|
531
|
+
# Simple number defaults to fixed
|
|
532
|
+
self._final[i] = float(v)
|
|
533
|
+
self.final_type[i] = "Fix"
|
|
534
|
+
else:
|
|
535
|
+
raise ValueError(
|
|
536
|
+
f"Invalid boundary condition format: {v}. "
|
|
537
|
+
f"Use a number (defaults to fixed) or tuple ('type', value) "
|
|
538
|
+
f"where type is 'fixed', 'free', 'minimize', or 'maximize'."
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
self._check_bounds_against_initial_final()
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def scaling_min(self):
|
|
545
|
+
"""Get the scaling minimum bounds for the state variables.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Array of scaling minimum values for each state variable element, or None if not set.
|
|
549
|
+
"""
|
|
550
|
+
return self._scaling_min
|
|
551
|
+
|
|
552
|
+
@scaling_min.setter
|
|
553
|
+
def scaling_min(self, val):
|
|
554
|
+
"""Set the scaling minimum bounds for the state variables.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
val: Array of scaling minimum values, must match the state shape exactly
|
|
558
|
+
|
|
559
|
+
Raises:
|
|
560
|
+
ValueError: If the shape doesn't match the state shape
|
|
561
|
+
"""
|
|
562
|
+
if val is None:
|
|
563
|
+
self._scaling_min = None
|
|
564
|
+
return
|
|
565
|
+
val = np.asarray(val, dtype=float)
|
|
566
|
+
if val.shape != self.shape:
|
|
567
|
+
raise ValueError(
|
|
568
|
+
f"Scaling min shape {val.shape} does not match State shape {self.shape}"
|
|
569
|
+
)
|
|
570
|
+
self._scaling_min = val
|
|
571
|
+
|
|
572
|
+
@property
|
|
573
|
+
def scaling_max(self):
|
|
574
|
+
"""Get the scaling maximum bounds for the state variables.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
Array of scaling maximum values for each state variable element, or None if not set.
|
|
578
|
+
"""
|
|
579
|
+
return self._scaling_max
|
|
580
|
+
|
|
581
|
+
@scaling_max.setter
|
|
582
|
+
def scaling_max(self, val):
|
|
583
|
+
"""Set the scaling maximum bounds for the state variables.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
val: Array of scaling maximum values, must match the state shape exactly
|
|
587
|
+
|
|
588
|
+
Raises:
|
|
589
|
+
ValueError: If the shape doesn't match the state shape
|
|
590
|
+
"""
|
|
591
|
+
if val is None:
|
|
592
|
+
self._scaling_max = None
|
|
593
|
+
return
|
|
594
|
+
val = np.asarray(val, dtype=float)
|
|
595
|
+
if val.shape != self.shape:
|
|
596
|
+
raise ValueError(
|
|
597
|
+
f"Scaling max shape {val.shape} does not match State shape {self.shape}"
|
|
598
|
+
)
|
|
599
|
+
self._scaling_max = val
|
|
600
|
+
|
|
601
|
+
def __repr__(self):
|
|
602
|
+
"""String representation of the State object.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
Concise string showing the state name and shape.
|
|
606
|
+
"""
|
|
607
|
+
return f"State('{self.name}', shape={self.shape})"
|