openscvx 2.dev6__py3-none-any.whl → 2.dev7__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.
- openscvx/__init__.py +16 -1
- openscvx/_version.py +2 -2
- openscvx/loader.py +2 -2
- openscvx/lowered/cvxpy_constraints.py +4 -1
- openscvx/problem.py +19 -13
- openscvx/solvers/__init__.py +40 -23
- openscvx/solvers/base.py +117 -36
- openscvx/solvers/cvxpy_ptr_solver.py +908 -0
- openscvx/solvers/ptr_solver.py +94 -843
- openscvx/solvers/qpax_ptr_solver.py +776 -0
- openscvx/symbolic/lower.py +6 -7
- openscvx/utils/printing.py +9 -1
- {openscvx-2.dev6.dist-info → openscvx-2.dev7.dist-info}/METADATA +3 -1
- {openscvx-2.dev6.dist-info → openscvx-2.dev7.dist-info}/RECORD +18 -16
- {openscvx-2.dev6.dist-info → openscvx-2.dev7.dist-info}/WHEEL +0 -0
- {openscvx-2.dev6.dist-info → openscvx-2.dev7.dist-info}/entry_points.txt +0 -0
- {openscvx-2.dev6.dist-info → openscvx-2.dev7.dist-info}/licenses/LICENSE +0 -0
- {openscvx-2.dev6.dist-info → openscvx-2.dev7.dist-info}/top_level.txt +0 -0
openscvx/solvers/ptr_solver.py
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
"""Abstract base class for Penalized Trust-Region (PTR) convex subproblem solvers.
|
|
2
|
+
|
|
3
|
+
The PTR formulation — its variables, slack structure, cost terms, and
|
|
4
|
+
linearization contract — is shared across backends. This module defines that
|
|
5
|
+
contract; concrete backends (CVXPy, QPAX) live in sibling modules and
|
|
6
|
+
implement the assembly/dispatch that each backend's modeling layer requires.
|
|
7
|
+
|
|
8
|
+
Backends:
|
|
9
|
+
:class:`openscvx.solvers.cvxpy_ptr_solver.CVXPyPTRSolver`
|
|
10
|
+
DCP graph assembled via CVXPy, dispatched to a conic solver
|
|
11
|
+
(QOCO, CLARABEL, ...).
|
|
12
|
+
:class:`openscvx.solvers.qpax_ptr_solver.QPAXPTRSolver`
|
|
13
|
+
Flat ``(Q, q, A, b, G, h)`` assembled as JAX arrays and solved with
|
|
14
|
+
``qpax.solve_qp``. Enables an end-to-end JAX-differentiable SCP loop
|
|
15
|
+
in follow-up work.
|
|
6
16
|
"""
|
|
7
17
|
|
|
8
|
-
import
|
|
18
|
+
from abc import abstractmethod
|
|
9
19
|
from dataclasses import dataclass
|
|
10
|
-
from typing import TYPE_CHECKING, List,
|
|
20
|
+
from typing import TYPE_CHECKING, List, Tuple, Union
|
|
11
21
|
|
|
12
|
-
import cvxpy as cp
|
|
13
22
|
import numpy as np
|
|
14
23
|
|
|
15
|
-
from openscvx.config import Config
|
|
16
|
-
|
|
17
24
|
from .base import ConvexSolver
|
|
18
25
|
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from openscvx.lowered.unified import UnifiedControl, UnifiedState
|
|
28
|
+
|
|
19
29
|
|
|
20
30
|
@dataclass
|
|
21
31
|
class PTRSolveResult:
|
|
@@ -45,601 +55,25 @@ class PTRSolveResult:
|
|
|
45
55
|
status: str
|
|
46
56
|
|
|
47
57
|
|
|
48
|
-
if TYPE_CHECKING:
|
|
49
|
-
from openscvx.lowered import LoweredProblem
|
|
50
|
-
from openscvx.lowered.cvxpy_variables import CVXPyVariables
|
|
51
|
-
from openscvx.lowered.jax_constraints import LoweredJaxConstraints
|
|
52
|
-
from openscvx.lowered.unified import UnifiedControl, UnifiedState
|
|
53
|
-
|
|
54
|
-
# Optional cvxpygen import
|
|
55
|
-
try:
|
|
56
|
-
from cvxpygen import cpg
|
|
57
|
-
|
|
58
|
-
CVXPYGEN_AVAILABLE = True
|
|
59
|
-
except ImportError:
|
|
60
|
-
CVXPYGEN_AVAILABLE = False
|
|
61
|
-
cpg = None
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
## TODO: (fabio) add support for impulsive controls
|
|
65
|
-
|
|
66
|
-
|
|
67
58
|
class PTRSolver(ConvexSolver):
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
This solver uses CVXPy's modeling language to construct and solve the convex
|
|
71
|
-
subproblems generated at each SCP iteration. It supports multiple backend
|
|
72
|
-
solvers (CLARABEL, ECOS, MOSEK, etc.) and optional code generation via
|
|
73
|
-
cvxpygen for improved performance.
|
|
74
|
-
|
|
75
|
-
The solver builds the problem structure once during ``initialize()``, using
|
|
76
|
-
CVXPy Parameters for values that change each iteration. The ``solve()``
|
|
77
|
-
method then solves and returns a structured ``PTRSolveResult``.
|
|
78
|
-
|
|
79
|
-
The cost and constraint formulations are defined in the ``cost()`` and
|
|
80
|
-
``constraints()`` methods, which can be overridden in subclasses to
|
|
81
|
-
customize the convex subproblem. For example::
|
|
82
|
-
|
|
83
|
-
class MyPTRSolver(PTRSolver):
|
|
84
|
-
def cost(self, settings, lowered):
|
|
85
|
-
c = super().cost(settings, lowered)
|
|
86
|
-
c += my_extra_term(self._ocp_vars)
|
|
87
|
-
return c
|
|
88
|
-
|
|
89
|
-
!!! note "Future Backend Support"
|
|
90
|
-
|
|
91
|
-
When adding a new backend (QPAX, COCO, etc.), this class should be
|
|
92
|
-
refactored:
|
|
59
|
+
"""Abstract base class for Penalized Trust-Region convex subproblem solvers.
|
|
93
60
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
61
|
+
Defines the contract every PTR backend must satisfy: per-iteration entry
|
|
62
|
+
points for updating the linearization point, constraint gradients, penalty
|
|
63
|
+
weights, and boundary conditions, plus a ``solve()`` returning a
|
|
64
|
+
:class:`PTRSolveResult`. The set of variables (state, control, virtual
|
|
65
|
+
control ``nu``, per-constraint virtual buffer ``nu_vb``, cross-node slack
|
|
66
|
+
``nu_vb_cross``) is fixed by the PTR formulation; how each backend
|
|
67
|
+
realizes those variables is an implementation detail.
|
|
100
68
|
|
|
101
|
-
|
|
102
|
-
|
|
69
|
+
Subclasses must additionally implement :meth:`create_variables` and
|
|
70
|
+
:meth:`initialize` from :class:`ConvexSolver`.
|
|
103
71
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
solver = PTRSolver()
|
|
108
|
-
solver.create_variables(N, x_unified, u_unified, jax_constraints)
|
|
109
|
-
solver.initialize(lowered, settings)
|
|
110
|
-
|
|
111
|
-
# Each iteration (parameter updates done by algorithm):
|
|
112
|
-
result = solver.solve()
|
|
113
|
-
x_sol = result.x # Unscaled state trajectory
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
cvx_solver: CVXPY solver backend name. Defaults to ``"QOCO"``.
|
|
117
|
-
solver_args: Keyword arguments forwarded to the CVXPY solver
|
|
118
|
-
(e.g. tolerances). Defaults to
|
|
119
|
-
``{"abstol": 1e-6, "reltol": 1e-9, "enforce_dpp": True}``.
|
|
120
|
-
cvxpygen: Enable CVXPy code generation for faster solves.
|
|
121
|
-
Defaults to ``False``.
|
|
122
|
-
|
|
123
|
-
!!! warning
|
|
124
|
-
Enabling cvxpygen currently disables sparse parameter
|
|
125
|
-
declarations. cvxpygen does not yet support the N-D sparsity
|
|
126
|
-
indices used by OpenSCvx's tiled parameters, so all parameters
|
|
127
|
-
are created as dense when code generation is active. This may
|
|
128
|
-
increase the generated solver's memory footprint and compile
|
|
129
|
-
time but does not affect solution correctness.
|
|
130
|
-
cvxpygen_override: Overwrite existing generated solver directory
|
|
131
|
-
without prompting. Defaults to ``False``.
|
|
132
|
-
|
|
133
|
-
Attributes:
|
|
134
|
-
ocp_vars: The CVXPy variables and parameters (available after create_variables())
|
|
72
|
+
Implementations: :class:`openscvx.solvers.cvxpy_ptr_solver.CVXPyPTRSolver`
|
|
73
|
+
and :class:`openscvx.solvers.qpax_ptr_solver.QPAXPTRSolver`.
|
|
135
74
|
"""
|
|
136
75
|
|
|
137
|
-
|
|
138
|
-
self,
|
|
139
|
-
cvx_solver: str = "QOCO",
|
|
140
|
-
solver_args: Optional[dict] = None,
|
|
141
|
-
cvxpygen: bool = False,
|
|
142
|
-
cvxpygen_override: bool = False,
|
|
143
|
-
):
|
|
144
|
-
"""Initialize PTRSolver with solver configuration.
|
|
145
|
-
|
|
146
|
-
Call create_variables() then initialize() to build the problem structure.
|
|
147
|
-
"""
|
|
148
|
-
self.cvx_solver = cvx_solver
|
|
149
|
-
self.solver_args = (
|
|
150
|
-
solver_args
|
|
151
|
-
if solver_args is not None
|
|
152
|
-
else {"abstol": 1e-06, "reltol": 1e-09, "enforce_dpp": True}
|
|
153
|
-
)
|
|
154
|
-
self.cvxpygen = cvxpygen
|
|
155
|
-
self.cvxpygen_override = cvxpygen_override
|
|
156
|
-
|
|
157
|
-
self._ocp_vars: "CVXPyVariables" = None
|
|
158
|
-
self._problem: cp.Problem = None
|
|
159
|
-
self._solve_fn: callable = None
|
|
160
|
-
|
|
161
|
-
@property
|
|
162
|
-
def ocp_vars(self) -> "CVXPyVariables":
|
|
163
|
-
"""The CVXPy variables and parameters.
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
The CVXPyVariables dataclass, or None if create_variables() not called.
|
|
167
|
-
"""
|
|
168
|
-
return self._ocp_vars
|
|
169
|
-
|
|
170
|
-
def create_variables(
|
|
171
|
-
self,
|
|
172
|
-
N: int,
|
|
173
|
-
x_unified: "UnifiedState",
|
|
174
|
-
u_unified: "UnifiedControl",
|
|
175
|
-
jax_constraints: "LoweredJaxConstraints",
|
|
176
|
-
dynamics_sparsity: Optional[tuple] = None,
|
|
177
|
-
constraint_sparsity: Optional[list] = None,
|
|
178
|
-
) -> None:
|
|
179
|
-
"""Create CVXPy optimization variables.
|
|
180
|
-
|
|
181
|
-
Creates all CVXPy Variable and Parameter objects needed for the optimal
|
|
182
|
-
control problem. This includes state/control variables, dynamics parameters,
|
|
183
|
-
constraint linearization parameters, and scaling matrices.
|
|
184
|
-
|
|
185
|
-
Args:
|
|
186
|
-
N: Number of discretization nodes
|
|
187
|
-
x_unified: Unified state interface with dimensions and scaling bounds
|
|
188
|
-
u_unified: Unified control interface with dimensions and scaling bounds
|
|
189
|
-
jax_constraints: Lowered JAX constraints (for sizing linearization params)
|
|
190
|
-
dynamics_sparsity: Optional tuple ``(A_d, B_d, C_d)`` of boolean
|
|
191
|
-
ndarrays giving the discrete-time Jacobian sparsity patterns.
|
|
192
|
-
``A_d`` has shape ``(n_x, n_x)``; ``B_d`` and ``C_d`` have
|
|
193
|
-
shape ``(n_x, n_u)``.
|
|
194
|
-
constraint_sparsity: Optional list of ``(x_mask, u_mask)`` boolean
|
|
195
|
-
1-D arrays, one per nodal constraint.
|
|
196
|
-
"""
|
|
197
|
-
from openscvx.config import get_affine_scaling_matrices
|
|
198
|
-
from openscvx.symbolic.lower import _tile_sparsity, create_cvxpy_variables
|
|
199
|
-
|
|
200
|
-
n_states = len(x_unified.max)
|
|
201
|
-
n_controls = len(u_unified.max)
|
|
202
|
-
slice_cont = u_unified.slice_continuous
|
|
203
|
-
slice_imp = u_unified.slice_impulsive
|
|
204
|
-
n_controls_cont = int(slice_cont.stop - slice_cont.start)
|
|
205
|
-
n_controls_imp = int(slice_imp.stop - slice_imp.start)
|
|
206
|
-
if n_controls_cont + n_controls_imp != n_controls:
|
|
207
|
-
raise ValueError(
|
|
208
|
-
"Unified control slices are inconsistent with control dimension. "
|
|
209
|
-
f"continuous={n_controls_cont}, impulsive={n_controls_imp}, total={n_controls}."
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
# Compute scaling matrices from unified object bounds
|
|
213
|
-
if x_unified.scaling_min is not None:
|
|
214
|
-
lower_x = np.array(x_unified.scaling_min, dtype=float)
|
|
215
|
-
else:
|
|
216
|
-
lower_x = np.array(x_unified.min, dtype=float)
|
|
217
|
-
|
|
218
|
-
if x_unified.scaling_max is not None:
|
|
219
|
-
upper_x = np.array(x_unified.scaling_max, dtype=float)
|
|
220
|
-
else:
|
|
221
|
-
upper_x = np.array(x_unified.max, dtype=float)
|
|
222
|
-
|
|
223
|
-
S_x, c_x = get_affine_scaling_matrices(n_states, lower_x, upper_x)
|
|
224
|
-
|
|
225
|
-
if u_unified.scaling_min is not None:
|
|
226
|
-
lower_u = np.array(u_unified.scaling_min, dtype=float)
|
|
227
|
-
else:
|
|
228
|
-
lower_u = np.array(u_unified.min, dtype=float)
|
|
229
|
-
|
|
230
|
-
if u_unified.scaling_max is not None:
|
|
231
|
-
upper_u = np.array(u_unified.scaling_max, dtype=float)
|
|
232
|
-
else:
|
|
233
|
-
upper_u = np.array(u_unified.max, dtype=float)
|
|
234
|
-
|
|
235
|
-
S_u, c_u = get_affine_scaling_matrices(n_controls, lower_u, upper_u)
|
|
236
|
-
|
|
237
|
-
# Convert boolean sparsity patterns to CVXPY index format
|
|
238
|
-
A_d_sp = B_d_sp = C_d_sp = None
|
|
239
|
-
if dynamics_sparsity is not None:
|
|
240
|
-
A_d_pat, B_d_pat, C_d_pat = dynamics_sparsity
|
|
241
|
-
A_d_sp = _tile_sparsity(A_d_pat, N - 1)
|
|
242
|
-
B_d_sp = _tile_sparsity(B_d_pat, N - 1)
|
|
243
|
-
C_d_sp = _tile_sparsity(C_d_pat, N - 1)
|
|
244
|
-
|
|
245
|
-
# TODO: (griffin-norris) Remove once cvxpygen supports N-D sparsity
|
|
246
|
-
# indices. cvxpygen's handle_sparsity() assumes 2-D (rows, cols) but
|
|
247
|
-
# our tiled parameters produce 3-D indices (slices, rows, cols).
|
|
248
|
-
# Dropping sparsity here is safe — it only affects codegen performance.
|
|
249
|
-
if self.cvxpygen:
|
|
250
|
-
A_d_sp = B_d_sp = C_d_sp = None
|
|
251
|
-
constraint_sparsity = None
|
|
252
|
-
|
|
253
|
-
# Create all CVXPy variables for the OCP
|
|
254
|
-
self._ocp_vars = create_cvxpy_variables(
|
|
255
|
-
N=N,
|
|
256
|
-
n_states=n_states,
|
|
257
|
-
n_controls=n_controls,
|
|
258
|
-
S_x=S_x,
|
|
259
|
-
c_x=c_x,
|
|
260
|
-
S_u=S_u,
|
|
261
|
-
c_u=c_u,
|
|
262
|
-
n_nodal_constraints=len(jax_constraints.nodal),
|
|
263
|
-
n_cross_node_constraints=len(jax_constraints.cross_node),
|
|
264
|
-
A_d_sparsity=A_d_sp,
|
|
265
|
-
B_d_sparsity=B_d_sp,
|
|
266
|
-
C_d_sparsity=C_d_sp,
|
|
267
|
-
constraint_sparsity=constraint_sparsity,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
def initialize(
|
|
271
|
-
self,
|
|
272
|
-
lowered: "LoweredProblem",
|
|
273
|
-
settings: "Config",
|
|
274
|
-
) -> None:
|
|
275
|
-
"""Build the CVXPy optimal control problem.
|
|
276
|
-
|
|
277
|
-
Constructs the complete optimization problem by calling ``cost()`` and
|
|
278
|
-
``constraints()`` to build the objective and constraint formulations,
|
|
279
|
-
then assembles them into a CVXPy Problem.
|
|
280
|
-
|
|
281
|
-
If cvxpygen is enabled, generates compiled solver code for improved
|
|
282
|
-
performance.
|
|
283
|
-
|
|
284
|
-
Note:
|
|
285
|
-
``create_variables()`` must be called before this method.
|
|
286
|
-
|
|
287
|
-
Args:
|
|
288
|
-
lowered: Lowered problem containing:
|
|
289
|
-
- ``cvxpy_constraints``: Lowered convex constraints
|
|
290
|
-
- ``jax_constraints``: JAX constraint functions (for structure)
|
|
291
|
-
settings: Problem configuration (node count, scaling, etc.)
|
|
292
|
-
|
|
293
|
-
Raises:
|
|
294
|
-
RuntimeError: If create_variables() has not been called.
|
|
295
|
-
"""
|
|
296
|
-
if self._ocp_vars is None:
|
|
297
|
-
raise RuntimeError(
|
|
298
|
-
"PTRSolver.initialize() called before create_variables(). "
|
|
299
|
-
"Call create_variables() first to create optimization variables."
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
objective = self.cost(settings, lowered)
|
|
303
|
-
constr = self.constraints(settings, lowered)
|
|
304
|
-
prob = cp.Problem(cp.Minimize(objective), constr)
|
|
305
|
-
|
|
306
|
-
if self.cvxpygen:
|
|
307
|
-
if not CVXPYGEN_AVAILABLE:
|
|
308
|
-
raise ImportError(
|
|
309
|
-
"cvxpygen is required for code generation but not installed. "
|
|
310
|
-
"Install it with: pip install openscvx[cvxpygen] or pip install cvxpygen"
|
|
311
|
-
)
|
|
312
|
-
# Check to see if solver directory exists
|
|
313
|
-
if not os.path.exists("solver"):
|
|
314
|
-
cpg.generate_code(prob, solver=self.cvx_solver, code_dir="solver", wrapper=True)
|
|
315
|
-
else:
|
|
316
|
-
# Prompt the use to indicate if they wish to overwrite the solver
|
|
317
|
-
# directory or use the existing compiled solver
|
|
318
|
-
if self.cvxpygen_override:
|
|
319
|
-
cpg.generate_code(
|
|
320
|
-
prob,
|
|
321
|
-
solver=self.cvx_solver,
|
|
322
|
-
code_dir="solver",
|
|
323
|
-
wrapper=True,
|
|
324
|
-
)
|
|
325
|
-
else:
|
|
326
|
-
overwrite = input("Solver directory already exists. Overwrite? (y/n): ")
|
|
327
|
-
if overwrite.lower() == "y":
|
|
328
|
-
cpg.generate_code(
|
|
329
|
-
prob,
|
|
330
|
-
solver=self.cvx_solver,
|
|
331
|
-
code_dir="solver",
|
|
332
|
-
wrapper=True,
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
self._problem = prob
|
|
336
|
-
self._setup_solve_function()
|
|
337
|
-
|
|
338
|
-
def cost(
|
|
339
|
-
self,
|
|
340
|
-
settings: "Config",
|
|
341
|
-
lowered: "LoweredProblem",
|
|
342
|
-
) -> cp.Expression:
|
|
343
|
-
"""Build the cost expression for the convex subproblem.
|
|
344
|
-
|
|
345
|
-
Constructs the PTR objective function including:
|
|
346
|
-
|
|
347
|
-
- Boundary condition costs (Minimize/Maximize state components)
|
|
348
|
-
- Trust region penalty (deviation from linearization point)
|
|
349
|
-
- Virtual control penalty (dynamics defect relaxation)
|
|
350
|
-
- Virtual buffer penalty (nonconvex constraint violation relaxation)
|
|
351
|
-
|
|
352
|
-
Override this method in subclasses to customize the cost formulation.
|
|
353
|
-
Use ``super().cost(settings, lowered)`` to include the standard PTR
|
|
354
|
-
cost terms and add to them.
|
|
355
|
-
|
|
356
|
-
Args:
|
|
357
|
-
settings: Configuration object with solver settings
|
|
358
|
-
lowered: Lowered problem containing constraint structure
|
|
359
|
-
|
|
360
|
-
Returns:
|
|
361
|
-
CVXPy expression representing the total cost to minimize.
|
|
362
|
-
"""
|
|
363
|
-
ocp_vars = self._ocp_vars
|
|
364
|
-
jax_constraints = lowered.jax_constraints
|
|
365
|
-
|
|
366
|
-
lam_prox = ocp_vars.lam_prox
|
|
367
|
-
lam_cost = ocp_vars.lam_cost
|
|
368
|
-
lam_vc = ocp_vars.lam_vc
|
|
369
|
-
lam_vb_nodal = ocp_vars.lam_vb_nodal
|
|
370
|
-
lam_vb_cross = ocp_vars.lam_vb_cross
|
|
371
|
-
_ = ocp_vars.x_nonscaled
|
|
372
|
-
dx = ocp_vars.dx
|
|
373
|
-
du = ocp_vars.du
|
|
374
|
-
nu = ocp_vars.nu
|
|
375
|
-
nu_vb = ocp_vars.nu_vb
|
|
376
|
-
nu_vb_cross = ocp_vars.nu_vb_cross
|
|
377
|
-
|
|
378
|
-
cost = cp.sum(lam_cost) * 0
|
|
379
|
-
cost += cp.sum(lam_vb_nodal) * 0
|
|
380
|
-
cost += cp.sum(lam_vb_cross) * 0
|
|
381
|
-
|
|
382
|
-
# Boundary condition cost terms (use scaled x for numerical conditioning)
|
|
383
|
-
x = ocp_vars.x
|
|
384
|
-
for i in range(settings.sim.true_state_slice.start, settings.sim.true_state_slice.stop):
|
|
385
|
-
if settings.sim.x.initial_type[i] == "Minimize":
|
|
386
|
-
cost += lam_cost[i] * x[0][i]
|
|
387
|
-
if settings.sim.x.final_type[i] == "Minimize":
|
|
388
|
-
cost += lam_cost[i] * x[-1][i]
|
|
389
|
-
if settings.sim.x.initial_type[i] == "Maximize":
|
|
390
|
-
cost -= lam_cost[i] * x[0][i]
|
|
391
|
-
if settings.sim.x.final_type[i] == "Maximize":
|
|
392
|
-
cost -= lam_cost[i] * x[-1][i]
|
|
393
|
-
|
|
394
|
-
# Trust Region Cost (per-variable weighting)
|
|
395
|
-
cost += sum(
|
|
396
|
-
cp.sum(cp.multiply(lam_prox[i], cp.square(cp.hstack((dx[i], du[i])))))
|
|
397
|
-
for i in range(settings.sim.n)
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
# Virtual Control Slack
|
|
401
|
-
cost += sum(cp.sum(lam_vc[i - 1] * cp.abs(nu[i - 1])) for i in range(1, settings.sim.n))
|
|
402
|
-
|
|
403
|
-
# Virtual buffer penalty for nodal constraints (per-node weighting)
|
|
404
|
-
idx_ncvx = 0
|
|
405
|
-
if jax_constraints.nodal:
|
|
406
|
-
for constraint in jax_constraints.nodal:
|
|
407
|
-
cost += lam_vb_nodal[:, idx_ncvx] @ cp.pos(nu_vb[idx_ncvx])
|
|
408
|
-
idx_ncvx += 1
|
|
409
|
-
|
|
410
|
-
# Virtual slack penalty for cross-node constraints
|
|
411
|
-
idx_cross = 0
|
|
412
|
-
if jax_constraints.cross_node:
|
|
413
|
-
for constraint in jax_constraints.cross_node:
|
|
414
|
-
cost += lam_vb_cross[idx_cross] * cp.pos(nu_vb_cross[idx_cross])
|
|
415
|
-
idx_cross += 1
|
|
416
|
-
|
|
417
|
-
return cost
|
|
418
|
-
|
|
419
|
-
def constraints(
|
|
420
|
-
self,
|
|
421
|
-
settings: "Config",
|
|
422
|
-
lowered: "LoweredProblem",
|
|
423
|
-
) -> list:
|
|
424
|
-
"""Build the constraint list for the convex subproblem.
|
|
425
|
-
|
|
426
|
-
Constructs all PTR constraints including:
|
|
427
|
-
|
|
428
|
-
- Linearized nodal constraints (from JAX-lowered nonconvex constraints)
|
|
429
|
-
- Linearized cross-node constraints
|
|
430
|
-
- Convex constraints (already lowered to CVXPy)
|
|
431
|
-
- Boundary conditions (fixed initial/terminal states)
|
|
432
|
-
- Uniform time grid constraints
|
|
433
|
-
- State and control deviation definitions
|
|
434
|
-
- Linearized dynamics
|
|
435
|
-
- State and control box constraints
|
|
436
|
-
- CTCS constraints
|
|
437
|
-
|
|
438
|
-
Override this method in subclasses to customize the constraint
|
|
439
|
-
formulation. Use ``super().constraints(settings, lowered)`` to include
|
|
440
|
-
the standard PTR constraints and extend them.
|
|
441
|
-
|
|
442
|
-
Args:
|
|
443
|
-
settings: Configuration object with solver settings
|
|
444
|
-
lowered: Lowered problem containing lowered constraints
|
|
445
|
-
|
|
446
|
-
Returns:
|
|
447
|
-
List of CVXPy constraints.
|
|
448
|
-
"""
|
|
449
|
-
ocp_vars = self._ocp_vars
|
|
450
|
-
jax_constraints = lowered.jax_constraints
|
|
451
|
-
cvxpy_constraints = lowered.cvxpy_constraints
|
|
452
|
-
|
|
453
|
-
x = ocp_vars.x
|
|
454
|
-
dx = ocp_vars.dx
|
|
455
|
-
x_bar = ocp_vars.x_bar
|
|
456
|
-
x_init = ocp_vars.x_init
|
|
457
|
-
x_term = ocp_vars.x_term
|
|
458
|
-
u = ocp_vars.u
|
|
459
|
-
du = ocp_vars.du
|
|
460
|
-
u_bar = ocp_vars.u_bar
|
|
461
|
-
A_d = ocp_vars.A_d
|
|
462
|
-
B_d = ocp_vars.B_d
|
|
463
|
-
C_d = ocp_vars.C_d
|
|
464
|
-
x_prop = ocp_vars.x_prop
|
|
465
|
-
x_prop_plus = ocp_vars.x_prop_plus
|
|
466
|
-
E_d = ocp_vars.E_d
|
|
467
|
-
nu = ocp_vars.nu
|
|
468
|
-
g = ocp_vars.g
|
|
469
|
-
grad_g_x = ocp_vars.grad_g_x
|
|
470
|
-
grad_g_u = ocp_vars.grad_g_u
|
|
471
|
-
nu_vb = ocp_vars.nu_vb
|
|
472
|
-
g_cross = ocp_vars.g_cross
|
|
473
|
-
grad_g_X_cross = ocp_vars.grad_g_X_cross
|
|
474
|
-
grad_g_U_cross = ocp_vars.grad_g_U_cross
|
|
475
|
-
nu_vb_cross = ocp_vars.nu_vb_cross
|
|
476
|
-
inv_S_x = ocp_vars.inv_S_x
|
|
477
|
-
c_x = ocp_vars.c_x
|
|
478
|
-
inv_S_u = ocp_vars.inv_S_u
|
|
479
|
-
c_u = ocp_vars.c_u
|
|
480
|
-
x_nonscaled = ocp_vars.x_nonscaled
|
|
481
|
-
u_nonscaled = ocp_vars.u_nonscaled
|
|
482
|
-
dx_nonscaled = ocp_vars.dx_nonscaled
|
|
483
|
-
du_nonscaled = ocp_vars.du_nonscaled
|
|
484
|
-
slice_cont = settings.sim.u.slice_continuous
|
|
485
|
-
slice_imp = settings.sim.u.slice_impulsive
|
|
486
|
-
has_continuous = bool(slice_cont.stop > slice_cont.start)
|
|
487
|
-
has_impulsive = bool(slice_imp.stop > slice_imp.start)
|
|
488
|
-
|
|
489
|
-
constr = []
|
|
490
|
-
|
|
491
|
-
# Linearized nodal constraints (from JAX-lowered non-convex)
|
|
492
|
-
idx_ncvx = 0
|
|
493
|
-
if jax_constraints.nodal:
|
|
494
|
-
for constraint in jax_constraints.nodal:
|
|
495
|
-
# nodes should already be validated and normalized in preprocessing
|
|
496
|
-
nodes = constraint.nodes
|
|
497
|
-
for node in nodes:
|
|
498
|
-
residual = (
|
|
499
|
-
g[idx_ncvx][node]
|
|
500
|
-
+ grad_g_x[idx_ncvx][node] @ dx[node]
|
|
501
|
-
+ grad_g_u[idx_ncvx][node] @ du[node]
|
|
502
|
-
)
|
|
503
|
-
constr += [residual == nu_vb[idx_ncvx][node]]
|
|
504
|
-
idx_ncvx += 1
|
|
505
|
-
|
|
506
|
-
# Linearized cross-node constraints (from JAX-lowered non-convex)
|
|
507
|
-
idx_cross = 0
|
|
508
|
-
if jax_constraints.cross_node:
|
|
509
|
-
for constraint in jax_constraints.cross_node:
|
|
510
|
-
# Linearization: g(X_bar, U_bar) + ∇g_X @ dX + ∇g_U @ dU == nu_vb
|
|
511
|
-
# Sum over all trajectory nodes to couple multiple nodes
|
|
512
|
-
residual = g_cross[idx_cross]
|
|
513
|
-
for k in range(settings.sim.n):
|
|
514
|
-
# Contribution from state at node k
|
|
515
|
-
residual += grad_g_X_cross[idx_cross][k, :] @ dx[k]
|
|
516
|
-
# Contribution from control at node k
|
|
517
|
-
residual += grad_g_U_cross[idx_cross][k, :] @ du[k]
|
|
518
|
-
# Add constraint: residual == slack variable
|
|
519
|
-
constr += [residual == nu_vb_cross[idx_cross]]
|
|
520
|
-
idx_cross += 1
|
|
521
|
-
|
|
522
|
-
# Convex constraints (already lowered to CVXPy)
|
|
523
|
-
if cvxpy_constraints.constraints:
|
|
524
|
-
constr += cvxpy_constraints.constraints
|
|
525
|
-
|
|
526
|
-
# Boundary conditions (Fix)
|
|
527
|
-
for i in range(settings.sim.true_state_slice.start, settings.sim.true_state_slice.stop):
|
|
528
|
-
if settings.sim.x.initial_type[i] == "Fix":
|
|
529
|
-
if has_impulsive:
|
|
530
|
-
constr += [
|
|
531
|
-
x_nonscaled[0][i]
|
|
532
|
-
== x_prop_plus[0][i] + E_d[0][i, slice_imp] @ du_nonscaled[0][slice_imp]
|
|
533
|
-
]
|
|
534
|
-
else:
|
|
535
|
-
constr += [x_nonscaled[0][i] == x_init[i]] # Initial Boundary Conditions
|
|
536
|
-
if settings.sim.x.final_type[i] == "Fix":
|
|
537
|
-
constr += [x_nonscaled[-1][i] == x_term[i]] # Final Boundary Conditions
|
|
538
|
-
|
|
539
|
-
if settings.sim._uniform_time_grid:
|
|
540
|
-
S_u_inv_td = inv_S_u[settings.sim.time_dilation_slice, settings.sim.time_dilation_slice]
|
|
541
|
-
c_u_td = c_u[settings.sim.time_dilation_slice]
|
|
542
|
-
constr += [
|
|
543
|
-
S_u_inv_td @ (u_nonscaled[i][settings.sim.time_dilation_slice] - c_u_td)
|
|
544
|
-
== S_u_inv_td @ (u_nonscaled[i - 1][settings.sim.time_dilation_slice] - c_u_td)
|
|
545
|
-
for i in range(1, settings.sim.n)
|
|
546
|
-
]
|
|
547
|
-
|
|
548
|
-
constr += [
|
|
549
|
-
(x[i] - inv_S_x @ (x_bar[i] - c_x) - dx[i]) == 0 for i in range(settings.sim.n)
|
|
550
|
-
] # State Error
|
|
551
|
-
constr += [
|
|
552
|
-
(u[i] - inv_S_u @ (u_bar[i] - c_u) - du[i]) == 0 for i in range(settings.sim.n)
|
|
553
|
-
] # Control Error
|
|
554
|
-
|
|
555
|
-
constr += [
|
|
556
|
-
inv_S_x @ (x_nonscaled[i] - c_x)
|
|
557
|
-
== inv_S_x
|
|
558
|
-
@ (
|
|
559
|
-
A_d[i - 1] @ dx_nonscaled[i - 1]
|
|
560
|
-
+ (
|
|
561
|
-
B_d[i - 1][:, slice_cont] @ du_nonscaled[i - 1][slice_cont]
|
|
562
|
-
if has_continuous
|
|
563
|
-
else 0
|
|
564
|
-
)
|
|
565
|
-
+ (C_d[i - 1][:, slice_cont] @ du_nonscaled[i][slice_cont] if has_continuous else 0)
|
|
566
|
-
+ (E_d[i][:, slice_imp] @ du_nonscaled[i][slice_imp] if has_impulsive else 0)
|
|
567
|
-
+ (x_prop_plus[i] if has_impulsive else x_prop[i - 1])
|
|
568
|
-
- c_x
|
|
569
|
-
)
|
|
570
|
-
+ nu[i - 1]
|
|
571
|
-
for i in range(1, settings.sim.n)
|
|
572
|
-
] # Dynamics Constraint
|
|
573
|
-
|
|
574
|
-
constr += [
|
|
575
|
-
inv_S_u @ (u_nonscaled[i] - c_u) <= inv_S_u @ (settings.sim.u.max - c_u)
|
|
576
|
-
for i in range(settings.sim.n)
|
|
577
|
-
]
|
|
578
|
-
constr += [
|
|
579
|
-
inv_S_u @ (u_nonscaled[i] - c_u) >= inv_S_u @ (settings.sim.u.min - c_u)
|
|
580
|
-
for i in range(settings.sim.n)
|
|
581
|
-
] # Control Constraints
|
|
582
|
-
|
|
583
|
-
# TODO: (norrisg) formalize this
|
|
584
|
-
constr += [
|
|
585
|
-
inv_S_x @ (x_nonscaled[i][:] - c_x) <= inv_S_x @ (settings.sim.x.max - c_x)
|
|
586
|
-
for i in range(settings.sim.n)
|
|
587
|
-
]
|
|
588
|
-
constr += [
|
|
589
|
-
inv_S_x @ (x_nonscaled[i][:] - c_x) >= inv_S_x @ (settings.sim.x.min - c_x)
|
|
590
|
-
for i in range(settings.sim.n)
|
|
591
|
-
] # State Constraints (Also implemented in CTCS but included for numerical stability)
|
|
592
|
-
|
|
593
|
-
for idx, nodes in zip(
|
|
594
|
-
np.arange(settings.sim.ctcs_slice.start, settings.sim.ctcs_slice.stop),
|
|
595
|
-
settings.sim.ctcs_node_intervals,
|
|
596
|
-
):
|
|
597
|
-
start_idx = 1 if nodes[0] == 0 else nodes[0]
|
|
598
|
-
constr += [
|
|
599
|
-
cp.abs(x_nonscaled[i][idx] - x_nonscaled[i - 1][idx]) <= settings.sim.x.max[idx]
|
|
600
|
-
for i in range(start_idx, nodes[1])
|
|
601
|
-
]
|
|
602
|
-
constr += [x_nonscaled[0][idx] == 0]
|
|
603
|
-
|
|
604
|
-
return constr
|
|
605
|
-
|
|
606
|
-
def _setup_solve_function(self) -> None:
|
|
607
|
-
"""Configure the solve function based on solver settings.
|
|
608
|
-
|
|
609
|
-
Sets up either cvxpygen-based solving or standard CVXPy solving
|
|
610
|
-
based on the configuration.
|
|
611
|
-
"""
|
|
612
|
-
if self.cvxpygen:
|
|
613
|
-
try:
|
|
614
|
-
import pickle
|
|
615
|
-
|
|
616
|
-
from solver.cpg_solver import cpg_solve
|
|
617
|
-
|
|
618
|
-
with open("solver/problem.pickle", "rb") as f:
|
|
619
|
-
pickle.load(f)
|
|
620
|
-
self._problem.register_solve("CPG", cpg_solve)
|
|
621
|
-
solver_args = self.solver_args
|
|
622
|
-
self._solve_fn = lambda: self._problem.solve(method="CPG", **solver_args)
|
|
623
|
-
except ImportError:
|
|
624
|
-
raise ImportError(
|
|
625
|
-
"cvxpygen solver not found. Make sure cvxpygen is installed and code "
|
|
626
|
-
"generation has been run. Install with: pip install openscvx[cvxpygen]"
|
|
627
|
-
)
|
|
628
|
-
else:
|
|
629
|
-
solver = self.cvx_solver
|
|
630
|
-
solver_args = dict(self.solver_args)
|
|
631
|
-
|
|
632
|
-
def _solve_with_dpp_fallback():
|
|
633
|
-
try:
|
|
634
|
-
return self._problem.solve(solver=solver, **solver_args)
|
|
635
|
-
except cp.error.DPPError:
|
|
636
|
-
fallback_args = dict(solver_args)
|
|
637
|
-
fallback_args.pop("enforce_dpp", None)
|
|
638
|
-
fallback_args["ignore_dpp"] = True
|
|
639
|
-
return self._problem.solve(solver=solver, **fallback_args)
|
|
640
|
-
|
|
641
|
-
self._solve_fn = _solve_with_dpp_fallback
|
|
642
|
-
|
|
76
|
+
@abstractmethod
|
|
643
77
|
def update_dynamics_linearization(
|
|
644
78
|
self,
|
|
645
79
|
x_bar: np.ndarray,
|
|
@@ -652,61 +86,28 @@ class PTRSolver(ConvexSolver):
|
|
|
652
86
|
D_d: np.ndarray | None = None,
|
|
653
87
|
E_d: np.ndarray | None = None,
|
|
654
88
|
) -> None:
|
|
655
|
-
"""Update dynamics linearization point and matrices.
|
|
656
|
-
|
|
657
|
-
Sets the current linearization point (previous iterate) and the
|
|
658
|
-
discretized dynamics matrices for the convex subproblem.
|
|
89
|
+
"""Update dynamics linearization point and discrete-time matrices.
|
|
659
90
|
|
|
660
91
|
Args:
|
|
661
|
-
x_bar: Previous state trajectory, shape (N, n_states)
|
|
662
|
-
u_bar: Previous control trajectory, shape (N, n_controls)
|
|
663
|
-
A_d: Discretized state Jacobian, shape (N-1, n_states, n_states)
|
|
664
|
-
B_d: Discretized control Jacobian (current node),
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
92
|
+
x_bar: Previous state trajectory, shape (N, n_states).
|
|
93
|
+
u_bar: Previous control trajectory, shape (N, n_controls).
|
|
94
|
+
A_d: Discretized state Jacobian, shape (N-1, n_states, n_states).
|
|
95
|
+
B_d: Discretized control Jacobian (current node),
|
|
96
|
+
shape (N-1, n_states, n_controls).
|
|
97
|
+
C_d: Discretized control Jacobian (next node),
|
|
98
|
+
shape (N-1, n_states, n_controls).
|
|
99
|
+
x_prop: Propagated state from continuous dynamics,
|
|
100
|
+
shape (N-1, n_states).
|
|
101
|
+
x_prop_plus: Optional impulsive/discrete propagated state,
|
|
102
|
+
shape (N, n_states).
|
|
103
|
+
D_d: Optional impulsive/discrete Jacobian wrt state,
|
|
104
|
+
shape (N, n_states, n_states).
|
|
105
|
+
E_d: Optional impulsive/discrete Jacobian wrt control,
|
|
106
|
+
shape (N, n_states, n_controls).
|
|
670
107
|
"""
|
|
671
|
-
|
|
672
|
-
self._set_param("u_bar", u_bar)
|
|
673
|
-
|
|
674
|
-
A_eff = np.asarray(A_d)
|
|
675
|
-
B_eff = np.asarray(B_d)
|
|
676
|
-
C_eff = np.asarray(C_d)
|
|
677
|
-
|
|
678
|
-
if D_d is not None:
|
|
679
|
-
D_arr = np.asarray(D_d)
|
|
680
|
-
# Temporary DPP-safe workaround: absorb D_d into A/B/C numerically
|
|
681
|
-
# so the CVXPY graph has only Parameter@Variable products.
|
|
682
|
-
if D_arr.ndim == 3 and D_arr.shape[0] == A_eff.shape[0] + 1:
|
|
683
|
-
D_steps = D_arr[1:]
|
|
684
|
-
elif D_arr.ndim == 3 and D_arr.shape[0] == A_eff.shape[0]:
|
|
685
|
-
D_steps = D_arr
|
|
686
|
-
else:
|
|
687
|
-
raise ValueError(
|
|
688
|
-
"Unexpected D_d shape for dynamics update: "
|
|
689
|
-
f"{D_arr.shape}, expected "
|
|
690
|
-
f"{(A_eff.shape[0] + 1, A_eff.shape[1], A_eff.shape[2])} "
|
|
691
|
-
f"or {(A_eff.shape[0], A_eff.shape[1], A_eff.shape[2])}."
|
|
692
|
-
)
|
|
693
|
-
|
|
694
|
-
A_eff = np.einsum("kij,kjl->kil", D_steps, A_eff)
|
|
695
|
-
B_eff = np.einsum("kij,kjl->kil", D_steps, B_eff)
|
|
696
|
-
C_eff = np.einsum("kij,kjl->kil", D_steps, C_eff)
|
|
697
|
-
|
|
698
|
-
self._set_param("A_d", A_eff)
|
|
699
|
-
self._set_param("B_d", B_eff)
|
|
700
|
-
self._set_param("C_d", C_eff)
|
|
701
|
-
if "x_prop" in self._problem.param_dict:
|
|
702
|
-
self._set_param("x_prop", x_prop)
|
|
703
|
-
elif self._ocp_vars.x_prop is not None:
|
|
704
|
-
self._ocp_vars.x_prop.value = np.asarray(x_prop)
|
|
705
|
-
if x_prop_plus is not None and self._ocp_vars.x_prop_plus is not None:
|
|
706
|
-
self._ocp_vars.x_prop_plus.value = np.asarray(x_prop_plus)
|
|
707
|
-
if E_d is not None and self._ocp_vars.E_d is not None:
|
|
708
|
-
self._ocp_vars.E_d.value = np.asarray(E_d)
|
|
108
|
+
raise NotImplementedError
|
|
709
109
|
|
|
110
|
+
@abstractmethod
|
|
710
111
|
def update_constraint_linearizations(
|
|
711
112
|
self,
|
|
712
113
|
nodal: List[dict] = None,
|
|
@@ -714,31 +115,19 @@ class PTRSolver(ConvexSolver):
|
|
|
714
115
|
) -> None:
|
|
715
116
|
"""Update linearized constraint values and gradients.
|
|
716
117
|
|
|
717
|
-
Sets constraint function values and gradients at the current
|
|
718
|
-
linearization point for both nodal and cross-node constraints.
|
|
719
|
-
|
|
720
118
|
Args:
|
|
721
|
-
nodal: List of dicts
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
cross_node: List of dicts
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
119
|
+
nodal: List of dicts, one per nodal constraint, each containing:
|
|
120
|
+
``g`` (value at linearization point),
|
|
121
|
+
``grad_g_x`` (gradient w.r.t. state),
|
|
122
|
+
``grad_g_u`` (gradient w.r.t. control).
|
|
123
|
+
cross_node: List of dicts, one per cross-node constraint, each
|
|
124
|
+
containing: ``g``, ``grad_g_X`` (gradient w.r.t. full state
|
|
125
|
+
trajectory), ``grad_g_U`` (gradient w.r.t. full control
|
|
126
|
+
trajectory).
|
|
729
127
|
"""
|
|
730
|
-
|
|
731
|
-
for g_id, constraint_data in enumerate(nodal):
|
|
732
|
-
self._set_param(f"g_{g_id}", constraint_data["g"])
|
|
733
|
-
self._set_param(f"grad_g_x_{g_id}", constraint_data["grad_g_x"])
|
|
734
|
-
self._set_param(f"grad_g_u_{g_id}", constraint_data["grad_g_u"])
|
|
735
|
-
|
|
736
|
-
if cross_node:
|
|
737
|
-
for g_id, constraint_data in enumerate(cross_node):
|
|
738
|
-
self._set_param(f"g_cross_{g_id}", constraint_data["g"])
|
|
739
|
-
self._set_param(f"grad_g_X_cross_{g_id}", constraint_data["grad_g_X"])
|
|
740
|
-
self._set_param(f"grad_g_U_cross_{g_id}", constraint_data["grad_g_U"])
|
|
128
|
+
raise NotImplementedError
|
|
741
129
|
|
|
130
|
+
@abstractmethod
|
|
742
131
|
def update_penalties(
|
|
743
132
|
self,
|
|
744
133
|
lam_prox: np.ndarray,
|
|
@@ -749,197 +138,59 @@ class PTRSolver(ConvexSolver):
|
|
|
749
138
|
) -> None:
|
|
750
139
|
"""Update SCP penalty weights.
|
|
751
140
|
|
|
752
|
-
Sets the penalty weights that balance competing objectives in the
|
|
753
|
-
PTR convex subproblem.
|
|
754
|
-
|
|
755
141
|
Args:
|
|
756
|
-
lam_prox: Trust region weights, shape
|
|
757
|
-
lam_cost: Cost function weight. Scalar or
|
|
758
|
-
|
|
759
|
-
lam_vc: Virtual control penalty weights, shape (N-1, n_states)
|
|
142
|
+
lam_prox: Trust region weights, shape (N, n_states + n_controls).
|
|
143
|
+
lam_cost: Cost function weight. Scalar or shape (n_states,).
|
|
144
|
+
lam_vc: Virtual control penalty weights, shape (N-1, n_states).
|
|
760
145
|
lam_vb_nodal: Virtual buffer penalty weights for nodal constraints,
|
|
761
|
-
shape
|
|
146
|
+
shape (N, n_nodal_constraints).
|
|
762
147
|
lam_vb_cross: Virtual buffer penalty weights for cross-node
|
|
763
|
-
constraints, shape
|
|
148
|
+
constraints, shape (n_cross_node_constraints,).
|
|
764
149
|
"""
|
|
765
|
-
|
|
766
|
-
self._set_param("lam_cost", lam_cost)
|
|
767
|
-
self._set_param("lam_vc", lam_vc)
|
|
768
|
-
self._set_param("lam_vb_nodal", lam_vb_nodal)
|
|
769
|
-
self._set_param("lam_vb_cross", lam_vb_cross)
|
|
150
|
+
raise NotImplementedError
|
|
770
151
|
|
|
152
|
+
@abstractmethod
|
|
771
153
|
def update_boundary_conditions(
|
|
772
154
|
self,
|
|
773
155
|
x_init: np.ndarray = None,
|
|
774
156
|
x_term: np.ndarray = None,
|
|
775
157
|
) -> None:
|
|
776
|
-
"""Update
|
|
777
|
-
|
|
778
|
-
Sets initial and/or terminal state constraints. Only sets parameters
|
|
779
|
-
that exist in the problem (some problems may not have both).
|
|
158
|
+
"""Update initial and/or terminal state parameters.
|
|
780
159
|
|
|
781
160
|
Args:
|
|
782
161
|
x_init: Initial state vector, shape (n_states,). Optional.
|
|
783
162
|
x_term: Terminal state vector, shape (n_states,). Optional.
|
|
784
163
|
"""
|
|
785
|
-
|
|
786
|
-
self._set_param("x_init", x_init)
|
|
787
|
-
if x_term is not None and "x_term" in self._problem.param_dict:
|
|
788
|
-
self._set_param("x_term", x_term)
|
|
789
|
-
|
|
790
|
-
def get_stats(self) -> dict:
|
|
791
|
-
"""Get solver statistics for diagnostics and printing.
|
|
792
|
-
|
|
793
|
-
Returns:
|
|
794
|
-
Dict containing:
|
|
795
|
-
- ``n_variables``: Total number of optimization variables
|
|
796
|
-
- ``n_parameters``: Total number of parameters
|
|
797
|
-
- ``n_constraints``: Total number of constraints
|
|
798
|
-
"""
|
|
799
|
-
if self._problem is None:
|
|
800
|
-
return {"n_variables": 0, "n_parameters": 0, "n_constraints": 0}
|
|
801
|
-
|
|
802
|
-
return {
|
|
803
|
-
"n_variables": sum(var.size for var in self._problem.variables()),
|
|
804
|
-
"n_parameters": sum(param.size for param in self._problem.parameters()),
|
|
805
|
-
"n_constraints": sum(constraint.size for constraint in self._problem.constraints),
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
def _set_param(self, name: str, value: np.ndarray) -> None:
|
|
809
|
-
"""Set a CVXPy parameter with helpful error messages on failure.
|
|
810
|
-
|
|
811
|
-
Args:
|
|
812
|
-
name: The parameter name in problem.param_dict
|
|
813
|
-
value: The value to assign
|
|
814
|
-
|
|
815
|
-
Raises:
|
|
816
|
-
ValueError: If the value is not real, with diagnostic information.
|
|
817
|
-
"""
|
|
818
|
-
try:
|
|
819
|
-
param = self._problem.param_dict[name]
|
|
820
|
-
value_arr = np.asarray(value)
|
|
821
|
-
|
|
822
|
-
# Ensure the value shape matches the parameter shape exactly
|
|
823
|
-
# This is critical for Python 3.11+ where NumPy/CVXPy are stricter about shapes
|
|
824
|
-
if hasattr(param, "shape") and param.shape is not None:
|
|
825
|
-
expected_shape = param.shape
|
|
826
|
-
if value_arr.shape != expected_shape:
|
|
827
|
-
# Try to reshape if sizes match
|
|
828
|
-
if value_arr.size == np.prod(expected_shape):
|
|
829
|
-
value_arr = value_arr.reshape(expected_shape)
|
|
830
|
-
else:
|
|
831
|
-
# If sizes don't match, try squeezing extra dimensions first
|
|
832
|
-
value_arr = np.squeeze(value_arr)
|
|
833
|
-
if value_arr.shape != expected_shape and value_arr.size == np.prod(
|
|
834
|
-
expected_shape
|
|
835
|
-
):
|
|
836
|
-
value_arr = value_arr.reshape(expected_shape)
|
|
837
|
-
elif value_arr.shape != expected_shape:
|
|
838
|
-
raise ValueError(
|
|
839
|
-
f"Parameter '{name}' shape mismatch: expected {expected_shape}, "
|
|
840
|
-
f"got {value.shape} (after squeezing: {value_arr.shape})"
|
|
841
|
-
)
|
|
842
|
-
|
|
843
|
-
param.value = value_arr
|
|
844
|
-
except ValueError as e:
|
|
845
|
-
if "must be real" in str(e):
|
|
846
|
-
arr = np.asarray(value)
|
|
847
|
-
nan_mask = ~np.isfinite(arr)
|
|
848
|
-
nan_indices = np.argwhere(nan_mask)
|
|
849
|
-
|
|
850
|
-
index_value_strs = [
|
|
851
|
-
f" {tuple(int(i) for i in idx)} -> {arr[tuple(idx)]}"
|
|
852
|
-
for idx in nan_indices[:20]
|
|
853
|
-
]
|
|
854
|
-
if len(nan_indices) > 20:
|
|
855
|
-
index_value_strs.append(f" ... and {len(nan_indices) - 20} more")
|
|
856
|
-
|
|
857
|
-
arr_str = np.array2string(arr, threshold=200, edgeitems=3, max_line_width=120)
|
|
858
|
-
msg = (
|
|
859
|
-
f"Parameter '{name}' with shape {arr.shape} contains "
|
|
860
|
-
f"{len(nan_indices)} non-real value(s):\n"
|
|
861
|
-
+ "\n".join(index_value_strs)
|
|
862
|
-
+ f"\n\n{name} = {arr_str}"
|
|
863
|
-
)
|
|
864
|
-
raise ValueError(msg) from e
|
|
865
|
-
raise
|
|
164
|
+
raise NotImplementedError
|
|
866
165
|
|
|
166
|
+
@abstractmethod
|
|
867
167
|
def solve(self) -> PTRSolveResult:
|
|
868
|
-
"""Solve the convex subproblem and return
|
|
869
|
-
|
|
870
|
-
Call ``update_dynamics_linearization()``, ``update_constraint_linearizations()``,
|
|
871
|
-
and ``update_penalties()`` before calling this method.
|
|
168
|
+
"""Solve the convex subproblem and return a :class:`PTRSolveResult`.
|
|
872
169
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
cost, and solver status.
|
|
876
|
-
|
|
877
|
-
Raises:
|
|
878
|
-
RuntimeError: If initialize() has not been called.
|
|
170
|
+
Call the four ``update_*`` methods first to set the linearization
|
|
171
|
+
point, constraint gradients, penalties, and boundary conditions.
|
|
879
172
|
"""
|
|
880
|
-
|
|
881
|
-
raise RuntimeError(
|
|
882
|
-
"PTRSolver.solve() called before initialize(). "
|
|
883
|
-
"Call initialize() first to build the problem structure."
|
|
884
|
-
)
|
|
885
|
-
|
|
886
|
-
self._solve_fn()
|
|
173
|
+
raise NotImplementedError
|
|
887
174
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
c_u = self._ocp_vars.c_u
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _scaling(unified: Union["UnifiedState", "UnifiedControl"]) -> Tuple[np.ndarray, np.ndarray]:
|
|
177
|
+
"""Compute the affine scaling matrices ``(S, c)`` for a unified
|
|
178
|
+
state or control interface.
|
|
893
179
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
# Get virtual control slack
|
|
901
|
-
nu = self._problem.var_dict["nu"].value
|
|
902
|
-
|
|
903
|
-
# Get nodal constraint violation slacks
|
|
904
|
-
nu_vb = [var.value for var in self._ocp_vars.nu_vb]
|
|
905
|
-
|
|
906
|
-
# Get cross-node constraint violation slacks
|
|
907
|
-
nu_vb_cross = [var.value for var in self._ocp_vars.nu_vb_cross]
|
|
180
|
+
The PTR formulation works on scaled variables ``z = S⁻¹ (x - c)``;
|
|
181
|
+
``S`` and ``c`` are chosen so the scaled variables sit roughly on
|
|
182
|
+
``[-1, 1]``. Bounds come from ``scaling_min``/``scaling_max`` when
|
|
183
|
+
provided, otherwise from ``min``/``max``. Shared by every backend.
|
|
184
|
+
"""
|
|
185
|
+
from openscvx.config import get_affine_scaling_matrices
|
|
908
186
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
nu_vb=nu_vb,
|
|
914
|
-
nu_vb_cross=nu_vb_cross,
|
|
915
|
-
cost=self._problem.value,
|
|
916
|
-
status=self._problem.status,
|
|
187
|
+
n = len(unified.max)
|
|
188
|
+
lower = np.array(
|
|
189
|
+
unified.scaling_min if unified.scaling_min is not None else unified.min,
|
|
190
|
+
dtype=float,
|
|
917
191
|
)
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
List containing BibTeX entries for CVXPy and DCCP papers.
|
|
924
|
-
"""
|
|
925
|
-
return [
|
|
926
|
-
r"""@article{diamond2016cvxpy,
|
|
927
|
-
title={CVXPY: A Python-embedded modeling language for convex optimization},
|
|
928
|
-
author={Diamond, Steven and Boyd, Stephen},
|
|
929
|
-
journal={Journal of Machine Learning Research},
|
|
930
|
-
volume={17},
|
|
931
|
-
number={83},
|
|
932
|
-
pages={1--5},
|
|
933
|
-
year={2016}
|
|
934
|
-
}""",
|
|
935
|
-
r"""@article{agrawal2018rewriting,
|
|
936
|
-
title={A rewriting system for convex optimization problems},
|
|
937
|
-
author={Agrawal, Akshay and Verschueren, Robin and Diamond, Steven and Boyd, Stephen},
|
|
938
|
-
journal={Journal of Control and Decision},
|
|
939
|
-
volume={5},
|
|
940
|
-
number={1},
|
|
941
|
-
pages={42--60},
|
|
942
|
-
year={2018},
|
|
943
|
-
publisher={Taylor \& Francis}
|
|
944
|
-
}""",
|
|
945
|
-
]
|
|
192
|
+
upper = np.array(
|
|
193
|
+
unified.scaling_max if unified.scaling_max is not None else unified.max,
|
|
194
|
+
dtype=float,
|
|
195
|
+
)
|
|
196
|
+
return get_affine_scaling_matrices(n, lower, upper)
|