elastiqp 0.2.0__cp39-cp39-win_amd64.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.
- elastiqp/__init__.py +124 -0
- elastiqp/_core.cp39-win_amd64.pyd +0 -0
- elastiqp/elastiqp_jax_ffi.dll +0 -0
- elastiqp/jax.py +501 -0
- elastiqp/torch.py +521 -0
- elastiqp-0.2.0.dist-info/METADATA +427 -0
- elastiqp-0.2.0.dist-info/RECORD +10 -0
- elastiqp-0.2.0.dist-info/WHEEL +5 -0
- elastiqp-0.2.0.dist-info/licenses/LICENSE +190 -0
- elastiqp-0.2.0.dist-info/licenses/NOTICE +101 -0
elastiqp/__init__.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""ElastiQP: an always-feasible QP solver for constrained robot control"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _import_core(): # compiled nanobind core
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
# Let ELASTIQP_BUILD_DIR take priority for ctest
|
|
8
|
+
env_dir = os.environ.get("ELASTIQP_BUILD_DIR")
|
|
9
|
+
if env_dir:
|
|
10
|
+
import importlib.machinery
|
|
11
|
+
import importlib.util
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
for suffix in importlib.machinery.EXTENSION_SUFFIXES:
|
|
15
|
+
path = os.path.join(env_dir, "_core" + suffix)
|
|
16
|
+
if not os.path.exists(path):
|
|
17
|
+
continue
|
|
18
|
+
spec = importlib.util.spec_from_file_location("elastiqp._core", path)
|
|
19
|
+
core = importlib.util.module_from_spec(spec)
|
|
20
|
+
sys.modules["elastiqp._core"] = core
|
|
21
|
+
spec.loader.exec_module(core)
|
|
22
|
+
return core
|
|
23
|
+
try:
|
|
24
|
+
from elastiqp import _core
|
|
25
|
+
|
|
26
|
+
return _core
|
|
27
|
+
except ImportError:
|
|
28
|
+
pass
|
|
29
|
+
# Un-configured source checkout: the extension defaults to <repo>/build.
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
build = str(Path(__file__).resolve().parents[2] / "build")
|
|
33
|
+
if os.path.isdir(build) and build not in __path__:
|
|
34
|
+
__path__.append(build)
|
|
35
|
+
try:
|
|
36
|
+
from elastiqp import _core
|
|
37
|
+
except ImportError as e:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"elastiqp._core extension not found. Install the package "
|
|
40
|
+
"(pip install .) or build from source (cmake -B build . && "
|
|
41
|
+
"cmake --build build), optionally pointing ELASTIQP_BUILD_DIR "
|
|
42
|
+
"at the build directory."
|
|
43
|
+
) from e
|
|
44
|
+
return _core
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
import sys as _sys
|
|
48
|
+
|
|
49
|
+
_core = _import_core()
|
|
50
|
+
|
|
51
|
+
Solution = _core.Solution
|
|
52
|
+
Status = _core.Status
|
|
53
|
+
solve = _core.solve
|
|
54
|
+
|
|
55
|
+
# One submodule per backend, mirroring the C++ namespaces: each has its own
|
|
56
|
+
# Settings and Solver (das.RowState too). Registered in sys.modules so that
|
|
57
|
+
# `import elastiqp.das` works as well as `elastiqp.das`.
|
|
58
|
+
das = _core.das
|
|
59
|
+
pdal = _core.pdal
|
|
60
|
+
ipm = _core.ipm
|
|
61
|
+
for _name, _mod in (("das", das), ("pdal", pdal), ("ipm", ipm)):
|
|
62
|
+
_sys.modules[f"elastiqp.{_name}"] = _mod
|
|
63
|
+
RowState = das.RowState
|
|
64
|
+
|
|
65
|
+
METHODS = ("das", "pdal", "ipm")
|
|
66
|
+
_BACKENDS = {"das": das, "pdal": pdal, "ipm": ipm}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _lookup(table, method):
|
|
70
|
+
try:
|
|
71
|
+
return table[method]
|
|
72
|
+
except KeyError:
|
|
73
|
+
raise ValueError(f"method must be one of {METHODS}, got {method!r}") from None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def Solver(method="das"):
|
|
77
|
+
"""Factory: a solver object for the given backend.
|
|
78
|
+
|
|
79
|
+
method is "das" (dual active set, the default), "pdal" (primal-dual
|
|
80
|
+
augmented Lagrangian) or "ipm" (interior point); the returned object is
|
|
81
|
+
an elastiqp.das.Solver / pdal.Solver / ipm.Solver. All three share
|
|
82
|
+
setup(), update(), set_*(), solve(), relax() (the differentiation point)
|
|
83
|
+
and the Solution type.
|
|
84
|
+
"""
|
|
85
|
+
return _lookup(_BACKENDS, method).Solver()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Per-backend outer iteration budget (the jax/torch wrappers' max_iter=None),
|
|
89
|
+
# read from each backend's Settings so it cannot drift from C++.
|
|
90
|
+
_DEFAULT_MAX_ITER = {
|
|
91
|
+
"das": das.Settings().max_iter,
|
|
92
|
+
"pdal": pdal.Settings().max_outer_iter,
|
|
93
|
+
"ipm": ipm.Settings().max_iter,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def Settings(method="das"):
|
|
98
|
+
"""Factory: a default settings object for the given backend
|
|
99
|
+
(elastiqp.das.Settings / pdal.Settings / ipm.Settings); assign to
|
|
100
|
+
solver.settings."""
|
|
101
|
+
return _lookup(_BACKENDS, method).Settings()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"METHODS",
|
|
106
|
+
"das",
|
|
107
|
+
"pdal",
|
|
108
|
+
"ipm",
|
|
109
|
+
"RowState",
|
|
110
|
+
"Settings",
|
|
111
|
+
"Solution",
|
|
112
|
+
"Solver",
|
|
113
|
+
"Status",
|
|
114
|
+
"solve",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
from importlib.metadata import version as _version
|
|
119
|
+
|
|
120
|
+
__version__ = _version("elastiqp")
|
|
121
|
+
except Exception: # not installed (source checkout)
|
|
122
|
+
__version__ = "0.0.0.dev0"
|
|
123
|
+
|
|
124
|
+
del _core, _import_core, _name, _mod
|
|
Binary file
|
|
Binary file
|
elastiqp/jax.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""ElastiQP JAX FFI wrapper
|
|
2
|
+
|
|
3
|
+
Supported:
|
|
4
|
+
- Different methods (das, pdal, ipm)
|
|
5
|
+
- jit, vmap, scan
|
|
6
|
+
- grad
|
|
7
|
+
|
|
8
|
+
Notes:
|
|
9
|
+
- vmap is sequential mode
|
|
10
|
+
- Requires float64
|
|
11
|
+
- Warm-starting is not differentiable
|
|
12
|
+
- Warm-starting requires explicit passing of the previous result
|
|
13
|
+
- Recommended default kappa for differentiability: 1e-3
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import ctypes
|
|
17
|
+
from functools import partial
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import NamedTuple
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import jax
|
|
23
|
+
except ImportError as e:
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
if sys.version_info < (3, 11):
|
|
27
|
+
raise ImportError(
|
|
28
|
+
"elastiqp.jax requires Python >= 3.11 (for jax>=0.9.1)"
|
|
29
|
+
) from e
|
|
30
|
+
raise ImportError("elastiqp.jax requires jax to be installed") from e
|
|
31
|
+
|
|
32
|
+
from jax import Array
|
|
33
|
+
import jax.numpy as jnp
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
from elastiqp import _DEFAULT_MAX_ITER, _core
|
|
37
|
+
|
|
38
|
+
__all__ = ["solve", "Result", "METHODS"]
|
|
39
|
+
|
|
40
|
+
METHODS = ("das", "pdal", "ipm")
|
|
41
|
+
_METHOD_ID = {"das": 0, "pdal": 1, "ipm": 2}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _find_library():
|
|
45
|
+
# The FFI library is built and installed next to the compiled core, so
|
|
46
|
+
# look wherever elastiqp._core was found (installed wheel, editable
|
|
47
|
+
# install, ELASTIQP_BUILD_DIR, or <repo>/build).
|
|
48
|
+
directory = Path(_core.__file__).resolve().parent
|
|
49
|
+
for name in (
|
|
50
|
+
"libelastiqp_jax_ffi.so",
|
|
51
|
+
"elastiqp_jax_ffi.so",
|
|
52
|
+
"elastiqp_jax_ffi.dll",
|
|
53
|
+
"libelastiqp_jax_ffi.dylib",
|
|
54
|
+
):
|
|
55
|
+
path = directory / name
|
|
56
|
+
if path.exists():
|
|
57
|
+
return path
|
|
58
|
+
raise ImportError(
|
|
59
|
+
f"elastiqp_jax_ffi shared library not found in {directory} (next to "
|
|
60
|
+
"elastiqp._core). Reinstall the package, or for source checkouts "
|
|
61
|
+
"build it first (cmake --build build)."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_lib = ctypes.cdll.LoadLibrary(str(_find_library()))
|
|
66
|
+
jax.ffi.register_ffi_target(
|
|
67
|
+
"elastiqp_solve", jax.ffi.pycapsule(_lib.ElastiqpSolve), platform="cpu"
|
|
68
|
+
)
|
|
69
|
+
jax.ffi.register_ffi_target(
|
|
70
|
+
"elastiqp_solve_warm",
|
|
71
|
+
jax.ffi.pycapsule(_lib.ElastiqpSolveWarm),
|
|
72
|
+
platform="cpu",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Result(NamedTuple):
|
|
77
|
+
x: jax.Array
|
|
78
|
+
t: jax.Array
|
|
79
|
+
y: jax.Array
|
|
80
|
+
z_t: jax.Array
|
|
81
|
+
z: jax.Array
|
|
82
|
+
converged: jax.Array
|
|
83
|
+
iters: jax.Array
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _ffi_solve(
|
|
87
|
+
Q,
|
|
88
|
+
q,
|
|
89
|
+
A,
|
|
90
|
+
b,
|
|
91
|
+
G,
|
|
92
|
+
h,
|
|
93
|
+
penalty,
|
|
94
|
+
eps_abs,
|
|
95
|
+
max_iter,
|
|
96
|
+
ruiz,
|
|
97
|
+
target_kappa,
|
|
98
|
+
method,
|
|
99
|
+
):
|
|
100
|
+
n = Q.shape[-1]
|
|
101
|
+
m = b.shape[-1]
|
|
102
|
+
p = h.shape[-1]
|
|
103
|
+
batch = Q.shape[:-2]
|
|
104
|
+
vec = lambda d: jax.ShapeDtypeStruct(batch + (d,), jnp.float64)
|
|
105
|
+
# (x, t, y, z_t, z) tight solution, then the kappa-relaxed central
|
|
106
|
+
# point (identical to the tight block when target_kappa <= 0), then
|
|
107
|
+
# info = [converged, iters, relax_converged].
|
|
108
|
+
out_types = [
|
|
109
|
+
vec(n),
|
|
110
|
+
vec(p),
|
|
111
|
+
vec(m),
|
|
112
|
+
vec(p),
|
|
113
|
+
vec(p),
|
|
114
|
+
vec(n),
|
|
115
|
+
vec(p),
|
|
116
|
+
vec(m),
|
|
117
|
+
vec(p),
|
|
118
|
+
vec(p),
|
|
119
|
+
vec(3),
|
|
120
|
+
]
|
|
121
|
+
call = jax.ffi.ffi_call("elastiqp_solve", out_types, vmap_method="sequential")
|
|
122
|
+
return call(
|
|
123
|
+
Q,
|
|
124
|
+
q,
|
|
125
|
+
A,
|
|
126
|
+
b,
|
|
127
|
+
G,
|
|
128
|
+
h,
|
|
129
|
+
penalty,
|
|
130
|
+
eps_abs=np.float64(eps_abs),
|
|
131
|
+
max_iter=np.int64(max_iter),
|
|
132
|
+
ruiz=np.int64(bool(ruiz)),
|
|
133
|
+
target_kappa=np.float64(target_kappa),
|
|
134
|
+
method=np.int64(_METHOD_ID[method]),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _ffi_solve_warm(
|
|
139
|
+
Q,
|
|
140
|
+
q,
|
|
141
|
+
A,
|
|
142
|
+
b,
|
|
143
|
+
G,
|
|
144
|
+
h,
|
|
145
|
+
penalty,
|
|
146
|
+
x0,
|
|
147
|
+
y0,
|
|
148
|
+
z0,
|
|
149
|
+
eps_abs,
|
|
150
|
+
max_iter,
|
|
151
|
+
ruiz,
|
|
152
|
+
method,
|
|
153
|
+
):
|
|
154
|
+
n = Q.shape[-1]
|
|
155
|
+
m = b.shape[-1]
|
|
156
|
+
p = h.shape[-1]
|
|
157
|
+
batch = Q.shape[:-2]
|
|
158
|
+
vec = lambda d: jax.ShapeDtypeStruct(batch + (d,), jnp.float64)
|
|
159
|
+
# (x, t, y, z_t, z), then info = [converged, iters].
|
|
160
|
+
out_types = [vec(n), vec(p), vec(m), vec(p), vec(p), vec(2)]
|
|
161
|
+
call = jax.ffi.ffi_call("elastiqp_solve_warm", out_types, vmap_method="sequential")
|
|
162
|
+
return call(
|
|
163
|
+
Q,
|
|
164
|
+
q,
|
|
165
|
+
A,
|
|
166
|
+
b,
|
|
167
|
+
G,
|
|
168
|
+
h,
|
|
169
|
+
penalty,
|
|
170
|
+
x0,
|
|
171
|
+
y0,
|
|
172
|
+
z0,
|
|
173
|
+
eps_abs=np.float64(eps_abs),
|
|
174
|
+
max_iter=np.int64(max_iter),
|
|
175
|
+
ruiz=np.int64(bool(ruiz)),
|
|
176
|
+
method=np.int64(_METHOD_ID[method]),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@partial(jax.custom_vjp, nondiff_argnums=(10, 11, 12, 13))
|
|
181
|
+
def _solve_warm(
|
|
182
|
+
Q,
|
|
183
|
+
q,
|
|
184
|
+
A,
|
|
185
|
+
b,
|
|
186
|
+
G,
|
|
187
|
+
h,
|
|
188
|
+
penalty,
|
|
189
|
+
x0,
|
|
190
|
+
y0,
|
|
191
|
+
z0,
|
|
192
|
+
eps_abs,
|
|
193
|
+
max_iter,
|
|
194
|
+
ruiz,
|
|
195
|
+
method,
|
|
196
|
+
):
|
|
197
|
+
return _ffi_solve_warm(
|
|
198
|
+
Q,
|
|
199
|
+
q,
|
|
200
|
+
A,
|
|
201
|
+
b,
|
|
202
|
+
G,
|
|
203
|
+
h,
|
|
204
|
+
penalty,
|
|
205
|
+
x0,
|
|
206
|
+
y0,
|
|
207
|
+
z0,
|
|
208
|
+
eps_abs,
|
|
209
|
+
max_iter,
|
|
210
|
+
ruiz,
|
|
211
|
+
method,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _solve_warm_fwd(*args):
|
|
216
|
+
raise TypeError("elastiqp.jax.solve is not differentiable with warm_start")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _solve_warm_bwd(*args):
|
|
220
|
+
raise TypeError("elastiqp.jax.solve is not differentiable with warm_start")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
_solve_warm.defvjp(_solve_warm_fwd, _solve_warm_bwd)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _outer(a, b):
|
|
227
|
+
return a[..., :, None] * b[..., None, :]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Note: this funciton is a port of equivalent logic in qpax
|
|
231
|
+
# See also: kkt_vjp.hpp (c++ version) and _vjp_torch in torch.py
|
|
232
|
+
def _kkt_bwd(res, ct):
|
|
233
|
+
"""Implicit differentiation of the elastic KKT conditions."""
|
|
234
|
+
Q, A, G, h, x, t, y, z_t, z = res
|
|
235
|
+
xb, tb, yb, z_tb, zb, _ = ct
|
|
236
|
+
n = Q.shape[-1]
|
|
237
|
+
m = A.shape[-2]
|
|
238
|
+
|
|
239
|
+
Qs = 0.5 * (Q + jnp.swapaxes(Q, -1, -2))
|
|
240
|
+
Gt = jnp.swapaxes(G, -1, -2)
|
|
241
|
+
At = jnp.swapaxes(A, -1, -2)
|
|
242
|
+
mv = lambda M, v: jnp.einsum("...ij,...j->...i", M, v)
|
|
243
|
+
|
|
244
|
+
D = mv(G, x) - t - h
|
|
245
|
+
E = D - z * t / z_t
|
|
246
|
+
|
|
247
|
+
# Eliminating the t, z_t, z rows reduces the adjoint system to (x, y).
|
|
248
|
+
# Hard rows (penalty = inf: t = 0, z_t = inf) have no t block.
|
|
249
|
+
rt = jnp.where(jnp.isfinite(z_t), -z_t * z_tb + t * tb, 0.0)
|
|
250
|
+
rh = z * zb + z * rt / z_t
|
|
251
|
+
rhs_x = xb - mv(Gt, rh / E)
|
|
252
|
+
|
|
253
|
+
H = Qs + jnp.einsum("...ji,...j,...jk->...ik", G, -z / E, G)
|
|
254
|
+
KKT = jnp.concatenate(
|
|
255
|
+
[
|
|
256
|
+
jnp.concatenate([H, At], axis=-1),
|
|
257
|
+
jnp.concatenate([A, jnp.zeros(A.shape[:-2] + (m, m), Q.dtype)], axis=-1),
|
|
258
|
+
],
|
|
259
|
+
axis=-2,
|
|
260
|
+
)
|
|
261
|
+
vxy = jnp.linalg.solve(KKT, jnp.concatenate([rhs_x, yb], axis=-1))
|
|
262
|
+
vx = vxy[..., :n]
|
|
263
|
+
vy = vxy[..., n:]
|
|
264
|
+
|
|
265
|
+
vh = (rh - z * mv(G, vx)) / E
|
|
266
|
+
vpen = (rt + t * vh) / z_t
|
|
267
|
+
|
|
268
|
+
qb = -vx
|
|
269
|
+
penalty_b = -vpen
|
|
270
|
+
bb = vy
|
|
271
|
+
hb = vh
|
|
272
|
+
Qb = -0.5 * (_outer(vx, x) + _outer(x, vx))
|
|
273
|
+
Ab = -(_outer(y, vx) + _outer(vy, x))
|
|
274
|
+
Gb = -(_outer(z, vx) + _outer(vh, x))
|
|
275
|
+
return Qb, qb, Ab, bb, Gb, hb, penalty_b
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@partial(jax.custom_vjp, nondiff_argnums=(7, 8, 9, 10, 11))
|
|
279
|
+
def _solve(
|
|
280
|
+
Q,
|
|
281
|
+
q,
|
|
282
|
+
A,
|
|
283
|
+
b,
|
|
284
|
+
G,
|
|
285
|
+
h,
|
|
286
|
+
penalty,
|
|
287
|
+
eps_abs,
|
|
288
|
+
max_iter,
|
|
289
|
+
ruiz,
|
|
290
|
+
target_kappa,
|
|
291
|
+
method,
|
|
292
|
+
):
|
|
293
|
+
# Tight solution when not differentiating
|
|
294
|
+
out = _ffi_solve(
|
|
295
|
+
Q,
|
|
296
|
+
q,
|
|
297
|
+
A,
|
|
298
|
+
b,
|
|
299
|
+
G,
|
|
300
|
+
h,
|
|
301
|
+
penalty,
|
|
302
|
+
eps_abs,
|
|
303
|
+
max_iter,
|
|
304
|
+
ruiz,
|
|
305
|
+
0.0,
|
|
306
|
+
method,
|
|
307
|
+
)
|
|
308
|
+
return tuple(out[:5]) + (out[10],) # solution + info
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _solve_fwd(
|
|
312
|
+
Q,
|
|
313
|
+
q,
|
|
314
|
+
A,
|
|
315
|
+
b,
|
|
316
|
+
G,
|
|
317
|
+
h,
|
|
318
|
+
penalty,
|
|
319
|
+
eps_abs,
|
|
320
|
+
max_iter,
|
|
321
|
+
ruiz,
|
|
322
|
+
target_kappa,
|
|
323
|
+
method,
|
|
324
|
+
):
|
|
325
|
+
if not target_kappa > 0:
|
|
326
|
+
raise TypeError(
|
|
327
|
+
"elastiqp.jax.solve is not differentiable with target_kappa=0: "
|
|
328
|
+
"Set target_kappa > 0 (e.g. 1e-3) for smoothed gradients"
|
|
329
|
+
)
|
|
330
|
+
out = _ffi_solve(
|
|
331
|
+
Q,
|
|
332
|
+
q,
|
|
333
|
+
A,
|
|
334
|
+
b,
|
|
335
|
+
G,
|
|
336
|
+
h,
|
|
337
|
+
penalty,
|
|
338
|
+
eps_abs,
|
|
339
|
+
max_iter,
|
|
340
|
+
ruiz,
|
|
341
|
+
target_kappa,
|
|
342
|
+
method,
|
|
343
|
+
)
|
|
344
|
+
x, t, y, z_t, z, xr, tr, yr, z_t_r, z_r, info = out
|
|
345
|
+
return (x, t, y, z_t, z, info), (Q, A, G, h, xr, tr, yr, z_t_r, z_r)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _solve_bwd(eps_abs, max_iter, ruiz, target_kappa, method, res, ct):
|
|
349
|
+
return _kkt_bwd(res, ct)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
_solve.defvjp(_solve_fwd, _solve_bwd)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def solve(
|
|
356
|
+
Q: Array,
|
|
357
|
+
q: Array,
|
|
358
|
+
G: Array,
|
|
359
|
+
h: Array,
|
|
360
|
+
penalty: Array | float,
|
|
361
|
+
*,
|
|
362
|
+
A: Array | None = None,
|
|
363
|
+
b: Array | None = None,
|
|
364
|
+
method: str = "das",
|
|
365
|
+
eps_abs: float = 1e-6,
|
|
366
|
+
max_iter: int | None = None,
|
|
367
|
+
ruiz: bool = True,
|
|
368
|
+
target_kappa: float = 1e-3,
|
|
369
|
+
warm_start: tuple[Array, Array, Array] | Result | None = None,
|
|
370
|
+
) -> Result:
|
|
371
|
+
"""Solve the elastic QP
|
|
372
|
+
|
|
373
|
+
min 0.5 x'Qx + q'x + penalty' t
|
|
374
|
+
s.t. A x == b, G x - t <= h, t >= 0
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
Q (Array): Quadratic cost matrix, shape (n, n)
|
|
378
|
+
q (Array): Quadratic cost vector, shape (n,)
|
|
379
|
+
G (Array): Linear inequality constraint matrix, shape (p, n)
|
|
380
|
+
h (Array): Linear inequality constraint vector, shape (p,)
|
|
381
|
+
penalty (Array | float): L1 penalty, shape (p,) if vector
|
|
382
|
+
A (Array, optional): Linear equality constraint matrix, shape (m, n).
|
|
383
|
+
Defaults to None.
|
|
384
|
+
b (Array, optional): Linear equality constraint vector, shape (m,).
|
|
385
|
+
Defaults to None.
|
|
386
|
+
method (str, optional): Backend (das/pdal/ipm). Defaults to "das".
|
|
387
|
+
eps_abs (float, optional): Absolute KKT tolerance (same meaning for
|
|
388
|
+
every backend). Defaults to 1e-6.
|
|
389
|
+
max_iter (int, optional): Outer iteration budget; what it counts
|
|
390
|
+
depends on the backend (das: active-set iterations, pdal: BCL
|
|
391
|
+
rounds, ipm: interior-point iterations). Defaults to None (the
|
|
392
|
+
backend's Settings default).
|
|
393
|
+
ruiz (bool, optional): Whether to use ruiz equilibration.
|
|
394
|
+
Defaults to True.
|
|
395
|
+
target_kappa (float, optional): Kappa-relaxation parameter for smooth
|
|
396
|
+
derivatives. Defaults to 1e-3.
|
|
397
|
+
warm_start (tuple | Result, optional): Explicit warm start, either a
|
|
398
|
+
previous Result or a (x, y, z) tuple, (das and pdal only).
|
|
399
|
+
Defaults to None.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Result: Solution to the elastic QP
|
|
403
|
+
"""
|
|
404
|
+
if method not in METHODS:
|
|
405
|
+
raise ValueError(f"method must be one of {METHODS}, got {method!r}")
|
|
406
|
+
if not jax.config.jax_enable_x64:
|
|
407
|
+
raise RuntimeError(
|
|
408
|
+
"elastiqp.jax.solve requires double precision. Enable it via "
|
|
409
|
+
"jax.config.update('jax_enable_x64', True) "
|
|
410
|
+
"or set JAX_ENABLE_X64=1 in your environment."
|
|
411
|
+
)
|
|
412
|
+
if max_iter is None:
|
|
413
|
+
max_iter = _DEFAULT_MAX_ITER[method]
|
|
414
|
+
Q = jnp.asarray(Q)
|
|
415
|
+
q = jnp.asarray(q)
|
|
416
|
+
G = jnp.asarray(G)
|
|
417
|
+
h = jnp.asarray(h)
|
|
418
|
+
if Q.ndim != 2 or q.ndim != 1:
|
|
419
|
+
raise ValueError("solve takes unbatched problems; use jax.vmap for batching")
|
|
420
|
+
n = Q.shape[-1]
|
|
421
|
+
if (A is None) != (b is None):
|
|
422
|
+
raise ValueError("A and b must be provided together")
|
|
423
|
+
if A is None:
|
|
424
|
+
A = jnp.zeros(Q.shape[:-2] + (0, n), dtype=jnp.float64)
|
|
425
|
+
b = jnp.zeros(Q.shape[:-2] + (0,), dtype=jnp.float64)
|
|
426
|
+
else:
|
|
427
|
+
A = jnp.asarray(A)
|
|
428
|
+
b = jnp.asarray(b)
|
|
429
|
+
# penalty may be a scalar or a per-constraint vector of length p
|
|
430
|
+
penalty = jnp.broadcast_to(jnp.asarray(penalty, dtype=jnp.float64), h.shape)
|
|
431
|
+
|
|
432
|
+
if warm_start is not None:
|
|
433
|
+
if method == "ipm":
|
|
434
|
+
raise ValueError(
|
|
435
|
+
"warm_start is not supported with method='ipm'; use 'das' or 'pdal'"
|
|
436
|
+
)
|
|
437
|
+
if isinstance(warm_start, Result):
|
|
438
|
+
x0, y0, z0 = warm_start.x, warm_start.y, warm_start.z
|
|
439
|
+
else:
|
|
440
|
+
x0, y0, z0 = warm_start
|
|
441
|
+
x0 = jnp.asarray(x0, dtype=jnp.float64)
|
|
442
|
+
y0 = jnp.asarray(y0, dtype=jnp.float64)
|
|
443
|
+
z0 = jnp.asarray(z0, dtype=jnp.float64)
|
|
444
|
+
if x0.shape != q.shape or y0.shape != b.shape or z0.shape != h.shape:
|
|
445
|
+
raise ValueError(
|
|
446
|
+
"warm_start (x, y, z) must have shapes "
|
|
447
|
+
f"{q.shape}, {b.shape}, {h.shape}; got "
|
|
448
|
+
f"{x0.shape}, {y0.shape}, {z0.shape}"
|
|
449
|
+
)
|
|
450
|
+
x, t, y, z_t, z, info = _solve_warm(
|
|
451
|
+
Q,
|
|
452
|
+
q,
|
|
453
|
+
A,
|
|
454
|
+
b,
|
|
455
|
+
G,
|
|
456
|
+
h,
|
|
457
|
+
penalty,
|
|
458
|
+
x0,
|
|
459
|
+
y0,
|
|
460
|
+
z0,
|
|
461
|
+
float(eps_abs),
|
|
462
|
+
int(max_iter),
|
|
463
|
+
bool(ruiz),
|
|
464
|
+
method,
|
|
465
|
+
)
|
|
466
|
+
return Result(
|
|
467
|
+
x=x,
|
|
468
|
+
t=t,
|
|
469
|
+
y=y,
|
|
470
|
+
z_t=z_t,
|
|
471
|
+
z=z,
|
|
472
|
+
converged=info[..., 0].astype(jnp.int32),
|
|
473
|
+
iters=info[..., 1].astype(jnp.int32),
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
x, t, y, z_t, z, info = _solve(
|
|
477
|
+
Q,
|
|
478
|
+
q,
|
|
479
|
+
A,
|
|
480
|
+
b,
|
|
481
|
+
G,
|
|
482
|
+
h,
|
|
483
|
+
penalty,
|
|
484
|
+
float(eps_abs),
|
|
485
|
+
int(max_iter),
|
|
486
|
+
bool(ruiz),
|
|
487
|
+
float(target_kappa),
|
|
488
|
+
method,
|
|
489
|
+
)
|
|
490
|
+
# Recall: info = [converged, iters, relax_converged]. The converged
|
|
491
|
+
# flag here considers both forward and backward (relax)
|
|
492
|
+
converged = jnp.minimum(info[..., 0], info[..., 2])
|
|
493
|
+
return Result(
|
|
494
|
+
x=x,
|
|
495
|
+
t=t,
|
|
496
|
+
y=y,
|
|
497
|
+
z_t=z_t,
|
|
498
|
+
z=z,
|
|
499
|
+
converged=converged.astype(jnp.int32),
|
|
500
|
+
iters=info[..., 1].astype(jnp.int32),
|
|
501
|
+
)
|