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,718 @@
|
|
|
1
|
+
"""Unified state and control dataclasses for the lowered representation.
|
|
2
|
+
|
|
3
|
+
This module contains the UnifiedState and UnifiedControl dataclasses that describe
|
|
4
|
+
the structure of the monolithic state and control vectors used in numerical optimization.
|
|
5
|
+
|
|
6
|
+
In the symbolic world, users define many named State and Control objects (position,
|
|
7
|
+
velocity, thrust, etc.). In the lowered world, these are aggregated into single
|
|
8
|
+
monolithic x and u vectors. UnifiedState and UnifiedControl hold the metadata
|
|
9
|
+
describing this aggregation: bounds, guesses, boundary conditions, and slices
|
|
10
|
+
for extracting individual components.
|
|
11
|
+
|
|
12
|
+
See Also:
|
|
13
|
+
- openscvx.symbolic.unified: Contains unify_states() and unify_controls()
|
|
14
|
+
functions that create these dataclasses from symbolic State/Control objects.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import TYPE_CHECKING, Optional
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from openscvx.symbolic.expr.control import Control
|
|
24
|
+
from openscvx.symbolic.expr.state import State
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class UnifiedState:
|
|
29
|
+
"""Unified state vector aggregating multiple State objects.
|
|
30
|
+
|
|
31
|
+
UnifiedState is a drop-in replacement for individual State objects that holds
|
|
32
|
+
aggregated data from multiple State instances. It maintains compatibility with
|
|
33
|
+
optimization infrastructure while providing access to individual state components
|
|
34
|
+
through slicing.
|
|
35
|
+
|
|
36
|
+
The unified state separates user-defined "true" states from augmented states
|
|
37
|
+
added internally (e.g., for CTCS constraints or time variables). This separation
|
|
38
|
+
allows clean access to physical states while supporting advanced features.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
name (str): Name identifier for the unified state vector
|
|
42
|
+
shape (tuple): Combined shape (total_dim,) of all aggregated states
|
|
43
|
+
min (np.ndarray): Lower bounds for all state variables, shape (total_dim,)
|
|
44
|
+
max (np.ndarray): Upper bounds for all state variables, shape (total_dim,)
|
|
45
|
+
guess (np.ndarray): Initial guess trajectory, shape (num_nodes, total_dim)
|
|
46
|
+
initial (np.ndarray): Initial boundary conditions, shape (total_dim,)
|
|
47
|
+
final (np.ndarray): Final boundary conditions, shape (total_dim,)
|
|
48
|
+
_initial (np.ndarray): Internal initial values, shape (total_dim,)
|
|
49
|
+
_final (np.ndarray): Internal final values, shape (total_dim,)
|
|
50
|
+
initial_type (np.ndarray): Boundary condition types at t0 ("Fix" or "Free"),
|
|
51
|
+
shape (total_dim,), dtype=object
|
|
52
|
+
final_type (np.ndarray): Boundary condition types at tf ("Fix" or "Free"),
|
|
53
|
+
shape (total_dim,), dtype=object
|
|
54
|
+
_true_dim (int): Number of user-defined state dimensions (excludes augmented)
|
|
55
|
+
_true_slice (slice): Slice for extracting true states from unified vector
|
|
56
|
+
_augmented_slice (slice): Slice for extracting augmented states
|
|
57
|
+
time_slice (Optional[slice]): Slice for time state variable, if present
|
|
58
|
+
ctcs_slice (Optional[slice]): Slice for CTCS augmented states, if present
|
|
59
|
+
|
|
60
|
+
Properties:
|
|
61
|
+
true: Returns UnifiedState view containing only true (user-defined) states
|
|
62
|
+
augmented: Returns UnifiedState view containing only augmented states
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
Creating a unified state from multiple State objects::
|
|
66
|
+
|
|
67
|
+
from openscvx.symbolic.unified import unify_states
|
|
68
|
+
|
|
69
|
+
position = ox.State("pos", shape=(3,), min=-10, max=10)
|
|
70
|
+
velocity = ox.State("vel", shape=(3,), min=-5, max=5)
|
|
71
|
+
|
|
72
|
+
unified = unify_states([position, velocity], name="x")
|
|
73
|
+
print(unified.shape) # (6,)
|
|
74
|
+
print(unified.min) # [-10, -10, -10, -5, -5, -5]
|
|
75
|
+
print(unified.true.shape) # (6,) - all are true states
|
|
76
|
+
print(unified.augmented.shape) # (0,) - no augmented states
|
|
77
|
+
|
|
78
|
+
Appending states dynamically::
|
|
79
|
+
|
|
80
|
+
unified = UnifiedState(name="x", shape=(0,), _true_dim=0)
|
|
81
|
+
unified.append(min=-1, max=1, guess=0.5) # Add scalar state
|
|
82
|
+
print(unified.shape) # (1,)
|
|
83
|
+
|
|
84
|
+
See Also:
|
|
85
|
+
- unify_states(): Factory function for creating UnifiedState from State list
|
|
86
|
+
- State: Individual symbolic state variable
|
|
87
|
+
- UnifiedControl: Analogous unified control vector
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
name: str
|
|
91
|
+
shape: tuple
|
|
92
|
+
min: Optional[np.ndarray] = None
|
|
93
|
+
max: Optional[np.ndarray] = None
|
|
94
|
+
guess: Optional[np.ndarray] = None
|
|
95
|
+
initial: Optional[np.ndarray] = None
|
|
96
|
+
final: Optional[np.ndarray] = None
|
|
97
|
+
_initial: Optional[np.ndarray] = None
|
|
98
|
+
_final: Optional[np.ndarray] = None
|
|
99
|
+
initial_type: Optional[np.ndarray] = None
|
|
100
|
+
final_type: Optional[np.ndarray] = None
|
|
101
|
+
_true_dim: int = 0
|
|
102
|
+
_true_slice: Optional[slice] = None
|
|
103
|
+
_augmented_slice: Optional[slice] = None
|
|
104
|
+
time_slice: Optional[slice] = None # Slice for time state
|
|
105
|
+
ctcs_slice: Optional[slice] = None # Slice for CTCS augmented states
|
|
106
|
+
scaling_min: Optional[np.ndarray] = None # Scaling minimum bounds for unified state
|
|
107
|
+
scaling_max: Optional[np.ndarray] = None # Scaling maximum bounds for unified state
|
|
108
|
+
|
|
109
|
+
def __post_init__(self):
|
|
110
|
+
"""Initialize slices after dataclass creation."""
|
|
111
|
+
if self._true_slice is None:
|
|
112
|
+
self._true_slice = slice(0, self._true_dim)
|
|
113
|
+
if self._augmented_slice is None:
|
|
114
|
+
self._augmented_slice = slice(self._true_dim, self.shape[0])
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def true(self) -> "UnifiedState":
|
|
118
|
+
"""Get the true (user-defined) state variables.
|
|
119
|
+
|
|
120
|
+
Returns a view of the unified state containing only user-defined states,
|
|
121
|
+
excluding internal augmented states added for CTCS, time, etc.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
UnifiedState: Sliced view containing only true state variables
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
Get true user-defined state::
|
|
128
|
+
|
|
129
|
+
unified = unify_states([position, velocity, ctcs_aug], name="x")
|
|
130
|
+
true_states = unified.true # Only position and velocity
|
|
131
|
+
true_states.shape # (6,) if position and velocity are 3D each
|
|
132
|
+
"""
|
|
133
|
+
return self[self._true_slice]
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def augmented(self) -> "UnifiedState":
|
|
137
|
+
"""Get the augmented (internal) state variables.
|
|
138
|
+
|
|
139
|
+
Returns a view of the unified state containing only augmented states
|
|
140
|
+
added internally by the optimization framework (e.g., CTCS penalty states,
|
|
141
|
+
time variables).
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
UnifiedState: Sliced view containing only augmented state variables
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
Get augmented state::
|
|
148
|
+
|
|
149
|
+
unified = unify_states([position, ctcs_aug], name="x")
|
|
150
|
+
aug_states = unified.augmented # Only CTCS states
|
|
151
|
+
"""
|
|
152
|
+
return self[self._augmented_slice]
|
|
153
|
+
|
|
154
|
+
def append(
|
|
155
|
+
self,
|
|
156
|
+
other: "Optional[State | UnifiedState]" = None,
|
|
157
|
+
*,
|
|
158
|
+
min=-np.inf,
|
|
159
|
+
max=np.inf,
|
|
160
|
+
guess=0.0,
|
|
161
|
+
initial=0.0,
|
|
162
|
+
final=0.0,
|
|
163
|
+
augmented=False,
|
|
164
|
+
):
|
|
165
|
+
"""Append another state or create a new state variable.
|
|
166
|
+
|
|
167
|
+
This method allows dynamic extension of the unified state, either by appending
|
|
168
|
+
another State/UnifiedState object or by creating a new scalar state variable
|
|
169
|
+
with specified properties. Modifies the unified state in-place.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
other (Optional[State | UnifiedState]): State object to append. If None,
|
|
173
|
+
creates a new scalar state variable with properties from keyword args.
|
|
174
|
+
min (float): Lower bound for new scalar state (default: -inf)
|
|
175
|
+
max (float): Upper bound for new scalar state (default: inf)
|
|
176
|
+
guess (float): Initial guess value for new scalar state (default: 0.0)
|
|
177
|
+
initial (float): Initial boundary condition for new scalar state (default: 0.0)
|
|
178
|
+
final (float): Final boundary condition for new scalar state (default: 0.0)
|
|
179
|
+
augmented (bool): Whether the appended state is augmented (internal) rather
|
|
180
|
+
than true (user-defined). Affects _true_dim tracking. Default: False
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
None: Modifies the unified state in-place
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
Appending a State object::
|
|
187
|
+
|
|
188
|
+
unified = unify_states([position], name="x")
|
|
189
|
+
velocity = ox.State("vel", shape=(3,), min=-5, max=5)
|
|
190
|
+
unified.append(velocity)
|
|
191
|
+
print(unified.shape) # (6,) - position (3) + velocity (3)
|
|
192
|
+
|
|
193
|
+
Creating new scalar state variables::
|
|
194
|
+
|
|
195
|
+
unified = UnifiedState(name="x", shape=(0,), _true_dim=0)
|
|
196
|
+
unified.append(min=-1, max=1, guess=0.5) # Add scalar state
|
|
197
|
+
unified.append(min=-2, max=2, augmented=True) # Add augmented state
|
|
198
|
+
print(unified.shape) # (2,)
|
|
199
|
+
print(unified._true_dim) # 1 (only first is true)
|
|
200
|
+
|
|
201
|
+
Note:
|
|
202
|
+
Maintains the invariant that true states appear before augmented states
|
|
203
|
+
in the unified vector. When appending augmented states, they are added
|
|
204
|
+
to the end but don't increment _true_dim.
|
|
205
|
+
"""
|
|
206
|
+
# Import here to avoid circular imports at module level
|
|
207
|
+
from openscvx.symbolic.expr.state import State
|
|
208
|
+
|
|
209
|
+
if isinstance(other, (State, UnifiedState)):
|
|
210
|
+
# Append another state object
|
|
211
|
+
new_shape = (self.shape[0] + other.shape[0],)
|
|
212
|
+
|
|
213
|
+
# Update bounds
|
|
214
|
+
if self.min is not None and other.min is not None:
|
|
215
|
+
new_min = np.concatenate([self.min, other.min])
|
|
216
|
+
else:
|
|
217
|
+
new_min = self.min
|
|
218
|
+
|
|
219
|
+
if self.max is not None and other.max is not None:
|
|
220
|
+
new_max = np.concatenate([self.max, other.max])
|
|
221
|
+
else:
|
|
222
|
+
new_max = self.max
|
|
223
|
+
|
|
224
|
+
# Update guess
|
|
225
|
+
if self.guess is not None and other.guess is not None:
|
|
226
|
+
new_guess = np.concatenate([self.guess, other.guess], axis=1)
|
|
227
|
+
else:
|
|
228
|
+
new_guess = self.guess
|
|
229
|
+
|
|
230
|
+
# Update initial/final conditions
|
|
231
|
+
if self.initial is not None and other.initial is not None:
|
|
232
|
+
new_initial = np.concatenate([self.initial, other.initial])
|
|
233
|
+
else:
|
|
234
|
+
new_initial = self.initial
|
|
235
|
+
|
|
236
|
+
if self.final is not None and other.final is not None:
|
|
237
|
+
new_final = np.concatenate([self.final, other.final])
|
|
238
|
+
else:
|
|
239
|
+
new_final = self.final
|
|
240
|
+
|
|
241
|
+
# Update internal arrays
|
|
242
|
+
if self._initial is not None and other._initial is not None:
|
|
243
|
+
new__initial = np.concatenate([self._initial, other._initial])
|
|
244
|
+
else:
|
|
245
|
+
new__initial = self._initial
|
|
246
|
+
|
|
247
|
+
if self._final is not None and other._final is not None:
|
|
248
|
+
new__final = np.concatenate([self._final, other._final])
|
|
249
|
+
else:
|
|
250
|
+
new__final = self._final
|
|
251
|
+
|
|
252
|
+
# Update types
|
|
253
|
+
if self.initial_type is not None and other.initial_type is not None:
|
|
254
|
+
new_initial_type = np.concatenate([self.initial_type, other.initial_type])
|
|
255
|
+
else:
|
|
256
|
+
new_initial_type = self.initial_type
|
|
257
|
+
|
|
258
|
+
if self.final_type is not None and other.final_type is not None:
|
|
259
|
+
new_final_type = np.concatenate([self.final_type, other.final_type])
|
|
260
|
+
else:
|
|
261
|
+
new_final_type = self.final_type
|
|
262
|
+
|
|
263
|
+
# Update scaling bounds (if present)
|
|
264
|
+
if (
|
|
265
|
+
self.scaling_min is not None
|
|
266
|
+
and hasattr(other, "scaling_min")
|
|
267
|
+
and other.scaling_min is not None
|
|
268
|
+
):
|
|
269
|
+
new_scaling_min = np.concatenate([self.scaling_min, other.scaling_min])
|
|
270
|
+
else:
|
|
271
|
+
new_scaling_min = self.scaling_min
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
self.scaling_max is not None
|
|
275
|
+
and hasattr(other, "scaling_max")
|
|
276
|
+
and other.scaling_max is not None
|
|
277
|
+
):
|
|
278
|
+
new_scaling_max = np.concatenate([self.scaling_max, other.scaling_max])
|
|
279
|
+
else:
|
|
280
|
+
new_scaling_max = self.scaling_max
|
|
281
|
+
|
|
282
|
+
# Update true dimension
|
|
283
|
+
if not augmented:
|
|
284
|
+
new_true_dim = self._true_dim + getattr(other, "_true_dim", other.shape[0])
|
|
285
|
+
else:
|
|
286
|
+
new_true_dim = self._true_dim
|
|
287
|
+
|
|
288
|
+
# Update all attributes in place
|
|
289
|
+
self.shape = new_shape
|
|
290
|
+
self.min = new_min
|
|
291
|
+
self.max = new_max
|
|
292
|
+
self.guess = new_guess
|
|
293
|
+
self.initial = new_initial
|
|
294
|
+
self.final = new_final
|
|
295
|
+
self._initial = new__initial
|
|
296
|
+
self._final = new__final
|
|
297
|
+
self.initial_type = new_initial_type
|
|
298
|
+
self.final_type = new_final_type
|
|
299
|
+
self.scaling_min = new_scaling_min
|
|
300
|
+
self.scaling_max = new_scaling_max
|
|
301
|
+
self._true_dim = new_true_dim
|
|
302
|
+
self._true_slice = slice(0, self._true_dim)
|
|
303
|
+
self._augmented_slice = slice(self._true_dim, self.shape[0])
|
|
304
|
+
|
|
305
|
+
else:
|
|
306
|
+
# Create a single new variable
|
|
307
|
+
new_shape = (self.shape[0] + 1,)
|
|
308
|
+
|
|
309
|
+
# Extend arrays
|
|
310
|
+
if self.min is not None:
|
|
311
|
+
self.min = np.concatenate([self.min, np.array([min])])
|
|
312
|
+
if self.max is not None:
|
|
313
|
+
self.max = np.concatenate([self.max, np.array([max])])
|
|
314
|
+
if self.guess is not None:
|
|
315
|
+
guess_arr = np.full((self.guess.shape[0], 1), guess)
|
|
316
|
+
self.guess = np.concatenate([self.guess, guess_arr], axis=1)
|
|
317
|
+
if self.initial is not None:
|
|
318
|
+
self.initial = np.concatenate([self.initial, np.array([initial])])
|
|
319
|
+
if self.final is not None:
|
|
320
|
+
self.final = np.concatenate([self.final, np.array([final])])
|
|
321
|
+
if self._initial is not None:
|
|
322
|
+
self._initial = np.concatenate([self._initial, np.array([initial])])
|
|
323
|
+
if self._final is not None:
|
|
324
|
+
self._final = np.concatenate([self._final, np.array([final])])
|
|
325
|
+
if self.initial_type is not None:
|
|
326
|
+
self.initial_type = np.concatenate(
|
|
327
|
+
[self.initial_type, np.array(["Fix"], dtype=object)]
|
|
328
|
+
)
|
|
329
|
+
if self.final_type is not None:
|
|
330
|
+
self.final_type = np.concatenate([self.final_type, np.array(["Fix"], dtype=object)])
|
|
331
|
+
if self.scaling_min is not None:
|
|
332
|
+
self.scaling_min = np.concatenate([self.scaling_min, np.array([min])])
|
|
333
|
+
if self.scaling_max is not None:
|
|
334
|
+
self.scaling_max = np.concatenate([self.scaling_max, np.array([max])])
|
|
335
|
+
|
|
336
|
+
# Update dimensions
|
|
337
|
+
self.shape = new_shape
|
|
338
|
+
if not augmented:
|
|
339
|
+
self._true_dim += 1
|
|
340
|
+
self._true_slice = slice(0, self._true_dim)
|
|
341
|
+
self._augmented_slice = slice(self._true_dim, self.shape[0])
|
|
342
|
+
|
|
343
|
+
def __getitem__(self, idx):
|
|
344
|
+
"""Get a subset of the unified state variables.
|
|
345
|
+
|
|
346
|
+
Enables slicing of the unified state to extract subsets of state variables.
|
|
347
|
+
Returns a new UnifiedState containing only the sliced dimensions.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
idx (slice): Slice object specifying which state dimensions to extract.
|
|
351
|
+
Only simple slices with step=1 are supported.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
UnifiedState: New unified state containing only the sliced dimensions
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
NotImplementedError: If idx is not a slice, or if step != 1
|
|
358
|
+
|
|
359
|
+
Example:
|
|
360
|
+
Generate unified state object::
|
|
361
|
+
|
|
362
|
+
unified = unify_states([position, velocity], name="x")
|
|
363
|
+
|
|
364
|
+
position has shape (3,), velocity has shape (3,)::
|
|
365
|
+
|
|
366
|
+
first_three = unified[0:3] # Extract position only
|
|
367
|
+
print(first_three.shape) # (3,)
|
|
368
|
+
last_three = unified[3:6] # Extract velocity only
|
|
369
|
+
print(last_three.shape) # (3,)
|
|
370
|
+
|
|
371
|
+
Note:
|
|
372
|
+
The sliced state maintains all properties (bounds, guesses, etc.) for
|
|
373
|
+
the selected dimensions. The _true_dim is recalculated based on which
|
|
374
|
+
dimensions fall within the original true state range.
|
|
375
|
+
"""
|
|
376
|
+
if isinstance(idx, slice):
|
|
377
|
+
start, stop, step = idx.indices(self.shape[0])
|
|
378
|
+
if step != 1:
|
|
379
|
+
raise NotImplementedError("Step slicing not supported")
|
|
380
|
+
|
|
381
|
+
new_shape = (stop - start,)
|
|
382
|
+
new_name = f"{self.name}[{start}:{stop}]"
|
|
383
|
+
|
|
384
|
+
# Slice all arrays
|
|
385
|
+
new_min = self.min[idx] if self.min is not None else None
|
|
386
|
+
new_max = self.max[idx] if self.max is not None else None
|
|
387
|
+
new_guess = self.guess[:, idx] if self.guess is not None else None
|
|
388
|
+
new_initial = self.initial[idx] if self.initial is not None else None
|
|
389
|
+
new_final = self.final[idx] if self.final is not None else None
|
|
390
|
+
new__initial = self._initial[idx] if self._initial is not None else None
|
|
391
|
+
new__final = self._final[idx] if self._final is not None else None
|
|
392
|
+
new_initial_type = self.initial_type[idx] if self.initial_type is not None else None
|
|
393
|
+
new_final_type = self.final_type[idx] if self.final_type is not None else None
|
|
394
|
+
|
|
395
|
+
# Calculate new true dimension
|
|
396
|
+
new_true_dim = max(0, min(stop, self._true_dim) - max(start, 0))
|
|
397
|
+
|
|
398
|
+
return UnifiedState(
|
|
399
|
+
name=new_name,
|
|
400
|
+
shape=new_shape,
|
|
401
|
+
min=new_min,
|
|
402
|
+
max=new_max,
|
|
403
|
+
guess=new_guess,
|
|
404
|
+
initial=new_initial,
|
|
405
|
+
final=new_final,
|
|
406
|
+
_initial=new__initial,
|
|
407
|
+
_final=new__final,
|
|
408
|
+
initial_type=new_initial_type,
|
|
409
|
+
final_type=new_final_type,
|
|
410
|
+
_true_dim=new_true_dim,
|
|
411
|
+
_true_slice=slice(0, new_true_dim),
|
|
412
|
+
_augmented_slice=slice(new_true_dim, new_shape[0]),
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
raise NotImplementedError("Only slice indexing is supported")
|
|
416
|
+
|
|
417
|
+
def __repr__(self):
|
|
418
|
+
"""String representation of the UnifiedState object."""
|
|
419
|
+
return f"UnifiedState('{self.name}', shape={self.shape})"
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@dataclass
|
|
423
|
+
class UnifiedControl:
|
|
424
|
+
"""Unified control vector aggregating multiple Control objects.
|
|
425
|
+
|
|
426
|
+
UnifiedControl is a drop-in replacement for individual Control objects that holds
|
|
427
|
+
aggregated data from multiple Control instances. It maintains compatibility with
|
|
428
|
+
optimization infrastructure while providing access to individual control components
|
|
429
|
+
through slicing.
|
|
430
|
+
|
|
431
|
+
The unified control separates user-defined "true" controls from augmented controls
|
|
432
|
+
added internally (e.g., for time dilation). This separation allows clean access to
|
|
433
|
+
physical control inputs while supporting advanced features.
|
|
434
|
+
|
|
435
|
+
Attributes:
|
|
436
|
+
name (str): Name identifier for the unified control vector
|
|
437
|
+
shape (tuple): Combined shape (total_dim,) of all aggregated controls
|
|
438
|
+
min (np.ndarray): Lower bounds for all control variables, shape (total_dim,)
|
|
439
|
+
max (np.ndarray): Upper bounds for all control variables, shape (total_dim,)
|
|
440
|
+
guess (np.ndarray): Initial guess trajectory, shape (num_nodes, total_dim)
|
|
441
|
+
_true_dim (int): Number of user-defined control dimensions (excludes augmented)
|
|
442
|
+
_true_slice (slice): Slice for extracting true controls from unified vector
|
|
443
|
+
_augmented_slice (slice): Slice for extracting augmented controls
|
|
444
|
+
time_dilation_slice (Optional[slice]): Slice for time dilation control, if present
|
|
445
|
+
|
|
446
|
+
Properties:
|
|
447
|
+
true: Returns UnifiedControl view containing only true (user-defined) controls
|
|
448
|
+
augmented: Returns UnifiedControl view containing only augmented controls
|
|
449
|
+
|
|
450
|
+
Example:
|
|
451
|
+
Creating a unified control from multiple Control objects::
|
|
452
|
+
|
|
453
|
+
from openscvx.symbolic.unified import unify_controls
|
|
454
|
+
|
|
455
|
+
thrust = ox.Control("thrust", shape=(3,), min=0, max=10)
|
|
456
|
+
torque = ox.Control("torque", shape=(3,), min=-1, max=1)
|
|
457
|
+
|
|
458
|
+
unified = unify_controls([thrust, torque], name="u")
|
|
459
|
+
print(unified.shape) # (6,)
|
|
460
|
+
print(unified.min) # [0, 0, 0, -1, -1, -1]
|
|
461
|
+
print(unified.true.shape) # (6,) - all are true controls
|
|
462
|
+
print(unified.augmented.shape) # (0,) - no augmented controls
|
|
463
|
+
|
|
464
|
+
Appending controls dynamically::
|
|
465
|
+
|
|
466
|
+
unified = UnifiedControl(name="u", shape=(0,), _true_dim=0)
|
|
467
|
+
unified.append(min=-1, max=1, guess=0.0) # Add scalar control
|
|
468
|
+
print(unified.shape) # (1,)
|
|
469
|
+
|
|
470
|
+
See Also:
|
|
471
|
+
- unify_controls(): Factory function for creating UnifiedControl from Control list
|
|
472
|
+
- Control: Individual symbolic control variable
|
|
473
|
+
- UnifiedState: Analogous unified state vector
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
name: str
|
|
477
|
+
shape: tuple
|
|
478
|
+
min: Optional[np.ndarray] = None
|
|
479
|
+
max: Optional[np.ndarray] = None
|
|
480
|
+
guess: Optional[np.ndarray] = None
|
|
481
|
+
_true_dim: int = 0
|
|
482
|
+
_true_slice: Optional[slice] = None
|
|
483
|
+
_augmented_slice: Optional[slice] = None
|
|
484
|
+
time_dilation_slice: Optional[slice] = None # Slice for time dilation control
|
|
485
|
+
scaling_min: Optional[np.ndarray] = None # Scaling minimum bounds for unified control
|
|
486
|
+
scaling_max: Optional[np.ndarray] = None # Scaling maximum bounds for unified control
|
|
487
|
+
|
|
488
|
+
def __post_init__(self):
|
|
489
|
+
"""Initialize slices after dataclass creation."""
|
|
490
|
+
if self._true_slice is None:
|
|
491
|
+
self._true_slice = slice(0, self._true_dim)
|
|
492
|
+
if self._augmented_slice is None:
|
|
493
|
+
self._augmented_slice = slice(self._true_dim, self.shape[0])
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def true(self) -> "UnifiedControl":
|
|
497
|
+
"""Get the true (user-defined) control variables.
|
|
498
|
+
|
|
499
|
+
Returns a view of the unified control containing only user-defined controls,
|
|
500
|
+
excluding internal augmented controls added for time dilation, etc.
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
UnifiedControl: Sliced view containing only true control variables
|
|
504
|
+
|
|
505
|
+
Example:
|
|
506
|
+
Get true user defined controls::
|
|
507
|
+
|
|
508
|
+
unified = unify_controls([thrust, torque, time_dilation], name="u")
|
|
509
|
+
true_controls = unified.true # Only thrust and torque
|
|
510
|
+
"""
|
|
511
|
+
return self[self._true_slice]
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def augmented(self) -> "UnifiedControl":
|
|
515
|
+
"""Get the augmented (internal) control variables.
|
|
516
|
+
|
|
517
|
+
Returns a view of the unified control containing only augmented controls
|
|
518
|
+
added internally by the optimization framework (e.g., time dilation control).
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
UnifiedControl: Sliced view containing only augmented control variables
|
|
522
|
+
|
|
523
|
+
Example:
|
|
524
|
+
Get augmented controls::
|
|
525
|
+
|
|
526
|
+
unified = unify_controls([thrust, time_dilation], name="u")
|
|
527
|
+
aug_controls = unified.augmented # Only time dilation
|
|
528
|
+
"""
|
|
529
|
+
return self[self._augmented_slice]
|
|
530
|
+
|
|
531
|
+
def append(
|
|
532
|
+
self,
|
|
533
|
+
other: "Optional[Control | UnifiedControl]" = None,
|
|
534
|
+
*,
|
|
535
|
+
min=-np.inf,
|
|
536
|
+
max=np.inf,
|
|
537
|
+
guess=0.0,
|
|
538
|
+
augmented=False,
|
|
539
|
+
):
|
|
540
|
+
"""Append another control or create a new control variable.
|
|
541
|
+
|
|
542
|
+
This method allows dynamic extension of the unified control, either by appending
|
|
543
|
+
another Control/UnifiedControl object or by creating a new scalar control variable
|
|
544
|
+
with specified properties. Modifies the unified control in-place.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
other (Optional[Control | UnifiedControl]): Control object to append. If None,
|
|
548
|
+
creates a new scalar control variable with properties from keyword args.
|
|
549
|
+
min (float): Lower bound for new scalar control (default: -inf)
|
|
550
|
+
max (float): Upper bound for new scalar control (default: inf)
|
|
551
|
+
guess (float): Initial guess value for new scalar control (default: 0.0)
|
|
552
|
+
augmented (bool): Whether the appended control is augmented (internal) rather
|
|
553
|
+
than true (user-defined). Affects _true_dim tracking. Default: False
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
None: Modifies the unified control in-place
|
|
557
|
+
|
|
558
|
+
Example:
|
|
559
|
+
Appending a Control object::
|
|
560
|
+
|
|
561
|
+
unified = unify_controls([thrust], name="u")
|
|
562
|
+
torque = ox.Control("torque", shape=(3,), min=-1, max=1)
|
|
563
|
+
unified.append(torque)
|
|
564
|
+
print(unified.shape) # (6,) - thrust (3) + torque (3)
|
|
565
|
+
|
|
566
|
+
Creating new scalar control variables::
|
|
567
|
+
|
|
568
|
+
unified = UnifiedControl(name="u", shape=(0,), _true_dim=0)
|
|
569
|
+
unified.append(min=-1, max=1, guess=0.0) # Add scalar control
|
|
570
|
+
print(unified.shape) # (1,)
|
|
571
|
+
"""
|
|
572
|
+
# Import here to avoid circular imports at module level
|
|
573
|
+
from openscvx.symbolic.expr.control import Control
|
|
574
|
+
|
|
575
|
+
if isinstance(other, (Control, UnifiedControl)):
|
|
576
|
+
# Append another control object
|
|
577
|
+
new_shape = (self.shape[0] + other.shape[0],)
|
|
578
|
+
|
|
579
|
+
# Update bounds
|
|
580
|
+
if self.min is not None and other.min is not None:
|
|
581
|
+
new_min = np.concatenate([self.min, other.min])
|
|
582
|
+
else:
|
|
583
|
+
new_min = self.min
|
|
584
|
+
|
|
585
|
+
if self.max is not None and other.max is not None:
|
|
586
|
+
new_max = np.concatenate([self.max, other.max])
|
|
587
|
+
else:
|
|
588
|
+
new_max = self.max
|
|
589
|
+
|
|
590
|
+
# Update guess
|
|
591
|
+
if self.guess is not None and other.guess is not None:
|
|
592
|
+
new_guess = np.concatenate([self.guess, other.guess], axis=1)
|
|
593
|
+
else:
|
|
594
|
+
new_guess = self.guess
|
|
595
|
+
|
|
596
|
+
# Update scaling bounds (if present)
|
|
597
|
+
if (
|
|
598
|
+
self.scaling_min is not None
|
|
599
|
+
and hasattr(other, "scaling_min")
|
|
600
|
+
and other.scaling_min is not None
|
|
601
|
+
):
|
|
602
|
+
new_scaling_min = np.concatenate([self.scaling_min, other.scaling_min])
|
|
603
|
+
else:
|
|
604
|
+
new_scaling_min = self.scaling_min
|
|
605
|
+
|
|
606
|
+
if (
|
|
607
|
+
self.scaling_max is not None
|
|
608
|
+
and hasattr(other, "scaling_max")
|
|
609
|
+
and other.scaling_max is not None
|
|
610
|
+
):
|
|
611
|
+
new_scaling_max = np.concatenate([self.scaling_max, other.scaling_max])
|
|
612
|
+
else:
|
|
613
|
+
new_scaling_max = self.scaling_max
|
|
614
|
+
|
|
615
|
+
# Update true dimension
|
|
616
|
+
if not augmented:
|
|
617
|
+
new_true_dim = self._true_dim + getattr(other, "_true_dim", other.shape[0])
|
|
618
|
+
else:
|
|
619
|
+
new_true_dim = self._true_dim
|
|
620
|
+
|
|
621
|
+
# Update all attributes in place
|
|
622
|
+
self.shape = new_shape
|
|
623
|
+
self.min = new_min
|
|
624
|
+
self.max = new_max
|
|
625
|
+
self.guess = new_guess
|
|
626
|
+
self.scaling_min = new_scaling_min
|
|
627
|
+
self.scaling_max = new_scaling_max
|
|
628
|
+
self._true_dim = new_true_dim
|
|
629
|
+
self._true_slice = slice(0, self._true_dim)
|
|
630
|
+
self._augmented_slice = slice(self._true_dim, self.shape[0])
|
|
631
|
+
|
|
632
|
+
else:
|
|
633
|
+
# Create a single new variable
|
|
634
|
+
new_shape = (self.shape[0] + 1,)
|
|
635
|
+
|
|
636
|
+
# Extend arrays
|
|
637
|
+
if self.min is not None:
|
|
638
|
+
self.min = np.concatenate([self.min, np.array([min])])
|
|
639
|
+
if self.max is not None:
|
|
640
|
+
self.max = np.concatenate([self.max, np.array([max])])
|
|
641
|
+
if self.guess is not None:
|
|
642
|
+
guess_arr = np.full((self.guess.shape[0], 1), guess)
|
|
643
|
+
self.guess = np.concatenate([self.guess, guess_arr], axis=1)
|
|
644
|
+
if self.scaling_min is not None:
|
|
645
|
+
self.scaling_min = np.concatenate([self.scaling_min, np.array([min])])
|
|
646
|
+
if self.scaling_max is not None:
|
|
647
|
+
self.scaling_max = np.concatenate([self.scaling_max, np.array([max])])
|
|
648
|
+
|
|
649
|
+
# Update dimensions
|
|
650
|
+
self.shape = new_shape
|
|
651
|
+
if not augmented:
|
|
652
|
+
self._true_dim += 1
|
|
653
|
+
self._true_slice = slice(0, self._true_dim)
|
|
654
|
+
self._augmented_slice = slice(self._true_dim, self.shape[0])
|
|
655
|
+
|
|
656
|
+
def __getitem__(self, idx):
|
|
657
|
+
"""Get a subset of the unified control variables.
|
|
658
|
+
|
|
659
|
+
Enables slicing of the unified control to extract subsets of control variables.
|
|
660
|
+
Returns a new UnifiedControl containing only the sliced dimensions.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
idx (slice): Slice object specifying which control dimensions to extract.
|
|
664
|
+
Only simple slices with step=1 are supported.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
UnifiedControl: New unified control containing only the sliced dimensions
|
|
668
|
+
|
|
669
|
+
Raises:
|
|
670
|
+
NotImplementedError: If idx is not a slice, or if step != 1
|
|
671
|
+
|
|
672
|
+
Example:
|
|
673
|
+
Generate unified control object::
|
|
674
|
+
|
|
675
|
+
unified = unify_controls([thrust, torque], name="u")
|
|
676
|
+
|
|
677
|
+
thrust has shape (3,), torque has shape (3,)::
|
|
678
|
+
|
|
679
|
+
first_three = unified[0:3] # Extract thrust only
|
|
680
|
+
print(first_three.shape) # (3,)
|
|
681
|
+
|
|
682
|
+
Note:
|
|
683
|
+
The sliced control maintains all properties (bounds, guesses, etc.) for
|
|
684
|
+
the selected dimensions. The _true_dim is recalculated based on which
|
|
685
|
+
dimensions fall within the original true control range.
|
|
686
|
+
"""
|
|
687
|
+
if isinstance(idx, slice):
|
|
688
|
+
start, stop, step = idx.indices(self.shape[0])
|
|
689
|
+
if step != 1:
|
|
690
|
+
raise NotImplementedError("Step slicing not supported")
|
|
691
|
+
|
|
692
|
+
new_shape = (stop - start,)
|
|
693
|
+
new_name = f"{self.name}[{start}:{stop}]"
|
|
694
|
+
|
|
695
|
+
# Slice all arrays
|
|
696
|
+
new_min = self.min[idx] if self.min is not None else None
|
|
697
|
+
new_max = self.max[idx] if self.max is not None else None
|
|
698
|
+
new_guess = self.guess[:, idx] if self.guess is not None else None
|
|
699
|
+
|
|
700
|
+
# Calculate new true dimension
|
|
701
|
+
new_true_dim = max(0, min(stop, self._true_dim) - max(start, 0))
|
|
702
|
+
|
|
703
|
+
return UnifiedControl(
|
|
704
|
+
name=new_name,
|
|
705
|
+
shape=new_shape,
|
|
706
|
+
min=new_min,
|
|
707
|
+
max=new_max,
|
|
708
|
+
guess=new_guess,
|
|
709
|
+
_true_dim=new_true_dim,
|
|
710
|
+
_true_slice=slice(0, new_true_dim),
|
|
711
|
+
_augmented_slice=slice(new_true_dim, new_shape[0]),
|
|
712
|
+
)
|
|
713
|
+
else:
|
|
714
|
+
raise NotImplementedError("Only slice indexing is supported")
|
|
715
|
+
|
|
716
|
+
def __repr__(self):
|
|
717
|
+
"""String representation of the UnifiedControl object."""
|
|
718
|
+
return f"UnifiedControl('{self.name}', shape={self.shape})"
|