elastiqp 0.1.0__tar.gz → 0.2.1__tar.gz
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-0.1.0 → elastiqp-0.2.1}/.github/workflows/wheels.yml +14 -4
- {elastiqp-0.1.0 → elastiqp-0.2.1}/CMakeLists.txt +46 -2
- {elastiqp-0.1.0 → elastiqp-0.2.1}/PKG-INFO +31 -14
- {elastiqp-0.1.0 → elastiqp-0.2.1}/README.md +30 -13
- {elastiqp-0.1.0 → elastiqp-0.2.1}/bindings/bindings.cc +13 -68
- {elastiqp-0.1.0 → elastiqp-0.2.1}/bindings/jax_ffi.cc +41 -97
- elastiqp-0.2.1/cmake/elastiqpConfig.cmake.in +18 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/cmake/elastiqp_install.cmake +8 -1
- {elastiqp-0.1.0 → elastiqp-0.2.1}/examples/basic_usage.py +1 -2
- {elastiqp-0.1.0 → elastiqp-0.2.1}/examples/jax_usage.py +1 -5
- {elastiqp-0.1.0 → elastiqp-0.2.1}/examples/torch_usage.py +1 -5
- {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/common.hpp +26 -2
- {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/das.hpp +434 -73
- elastiqp-0.2.1/include/elastiqp/elastiqp_c.h +236 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/ipm.hpp +91 -140
- {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/kkt_vjp.hpp +7 -1
- {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/pdal.hpp +56 -119
- {elastiqp-0.1.0 → elastiqp-0.2.1}/package.xml +1 -1
- {elastiqp-0.1.0 → elastiqp-0.2.1}/python/elastiqp/__init__.py +11 -2
- elastiqp-0.2.1/python/elastiqp/jax.py +413 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/python/elastiqp/torch.py +20 -39
- elastiqp-0.2.1/src/elastiqp_c.cc +538 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/support/drift_traj.hpp +1 -1
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/support/test_util.hpp +5 -2
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_bindings.py +35 -17
- elastiqp-0.2.1/tests/test_c_api.c +425 -0
- elastiqp-0.2.1/tests/test_c_parity.cc +235 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_das.cc +355 -3
- elastiqp-0.2.1/tests/test_das_alloc.cc +397 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_ipm.cc +7 -122
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_jax_ffi.py +219 -113
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_pdal.cc +18 -85
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_solvers.cc +3 -3
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_torch.py +105 -37
- elastiqp-0.1.0/cmake/elastiqpConfig.cmake.in +0 -8
- elastiqp-0.1.0/python/elastiqp/jax.py +0 -549
- {elastiqp-0.1.0 → elastiqp-0.2.1}/.clang-format +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/.gitignore +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/LICENSE +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/NOTICE +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/examples/constraint_conflict_demo.py +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/elastiqp.hpp +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/pyproject.toml +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/data/warmstart_dual_overshoot.npz +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/data/warmstart_stale_duals.npz +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/support/problem_gen.hpp +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_bcl_creep.cc +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_gap_creep.cc +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_lp.cc +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_ruiz.cc +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/third_party/xla_ffi/README.md +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/third_party/xla_ffi/xla/ffi/api/api.h +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/third_party/xla_ffi/xla/ffi/api/c_api.h +0 -0
- {elastiqp-0.1.0 → elastiqp-0.2.1}/third_party/xla_ffi/xla/ffi/api/ffi.h +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
# Build manylinux
|
|
1
|
+
# Build manylinux, macOS, and Windows wheels and an sdist, then publish to PyPI on a version tag.
|
|
2
2
|
#
|
|
3
|
-
# Manual runs (workflow_dispatch) build everything and publish to TestPyPI
|
|
3
|
+
# Manual runs (workflow_dispatch) build everything, and publish to TestPyPI
|
|
4
|
+
# only if the "publish" box is ticked.
|
|
4
5
|
# Tag pushes matching v* publish to PyPI.
|
|
5
6
|
#
|
|
6
7
|
# Both publish jobs use PyPI trusted publishing (no API token): register this
|
|
@@ -17,6 +18,11 @@ on:
|
|
|
17
18
|
- "pyproject.toml"
|
|
18
19
|
- "CMakeLists.txt"
|
|
19
20
|
workflow_dispatch:
|
|
21
|
+
inputs:
|
|
22
|
+
publish:
|
|
23
|
+
description: "Publish to TestPyPI"
|
|
24
|
+
type: boolean
|
|
25
|
+
default: false
|
|
20
26
|
|
|
21
27
|
jobs:
|
|
22
28
|
build_wheels:
|
|
@@ -25,7 +31,7 @@ jobs:
|
|
|
25
31
|
strategy:
|
|
26
32
|
fail-fast: false
|
|
27
33
|
matrix:
|
|
28
|
-
os: [ubuntu-latest, ubuntu-24.04-arm, macos-
|
|
34
|
+
os: [ubuntu-latest, ubuntu-24.04-arm, macos-15, windows-latest]
|
|
29
35
|
steps:
|
|
30
36
|
- uses: actions/checkout@v5
|
|
31
37
|
|
|
@@ -33,6 +39,7 @@ jobs:
|
|
|
33
39
|
env:
|
|
34
40
|
CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*"
|
|
35
41
|
CIBW_SKIP: "*-musllinux_* *_i686"
|
|
42
|
+
CIBW_ARCHS_WINDOWS: AMD64
|
|
36
43
|
# AlmaLinux 8 based image: glibc 2.28, broad compatibility.
|
|
37
44
|
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
|
|
38
45
|
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
|
|
@@ -40,6 +47,9 @@ jobs:
|
|
|
40
47
|
CIBW_BEFORE_ALL_LINUX: dnf install -y epel-release && dnf install -y eigen3-devel
|
|
41
48
|
CIBW_BEFORE_ALL_MACOS: brew install eigen
|
|
42
49
|
CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=11.0
|
|
50
|
+
# vcpkg is preinstalled at C:\vcpkg on GitHub's Windows runners.
|
|
51
|
+
CIBW_BEFORE_ALL_WINDOWS: vcpkg install eigen3:x64-windows
|
|
52
|
+
CIBW_ENVIRONMENT_WINDOWS: CMAKE_PREFIX_PATH=C:/vcpkg/installed/x64-windows
|
|
43
53
|
# Smoke-test each wheel from a clean venv (no ELASTIQP_BUILD_DIR set,
|
|
44
54
|
# so the test imports the installed elastiqp._core).
|
|
45
55
|
CIBW_TEST_COMMAND: python {project}/tests/test_bindings.py
|
|
@@ -67,7 +77,7 @@ jobs:
|
|
|
67
77
|
|
|
68
78
|
publish_testpypi:
|
|
69
79
|
name: publish to TestPyPI
|
|
70
|
-
if: github.event_name == 'workflow_dispatch'
|
|
80
|
+
if: github.event_name == 'workflow_dispatch' && inputs.publish
|
|
71
81
|
needs: [build_wheels, build_sdist]
|
|
72
82
|
runs-on: ubuntu-latest
|
|
73
83
|
environment: testpypi
|
|
@@ -18,8 +18,10 @@ else()
|
|
|
18
18
|
endif()
|
|
19
19
|
option(ELASTIQP_BUILD_TESTS "Build the elastiqp test suite" ${_elastiqp_top_level})
|
|
20
20
|
option(ELASTIQP_BUILD_PYTHON "Build the Python bindings and JAX FFI" ${_elastiqp_top_level})
|
|
21
|
+
option(ELASTIQP_BUILD_C "Build the C interface library (elastiqp_c)" ${_elastiqp_top_level})
|
|
21
22
|
if(SKBUILD) # pip wheel build: bindings only, no install/export
|
|
22
23
|
set(ELASTIQP_BUILD_TESTS OFF)
|
|
24
|
+
set(ELASTIQP_BUILD_C OFF)
|
|
23
25
|
set(ELASTIQP_BUILD_PYTHON ON)
|
|
24
26
|
endif()
|
|
25
27
|
|
|
@@ -34,6 +36,27 @@ target_include_directories(elastiqp INTERFACE
|
|
|
34
36
|
target_link_libraries(elastiqp INTERFACE Eigen3::Eigen)
|
|
35
37
|
target_compile_features(elastiqp INTERFACE cxx_std_17)
|
|
36
38
|
|
|
39
|
+
# C interface library
|
|
40
|
+
if(ELASTIQP_BUILD_C)
|
|
41
|
+
enable_language(C)
|
|
42
|
+
add_library(elastiqp_c src/elastiqp_c.cc)
|
|
43
|
+
add_library(elastiqp::elastiqp_c ALIAS elastiqp_c)
|
|
44
|
+
target_link_libraries(elastiqp_c PRIVATE elastiqp::elastiqp)
|
|
45
|
+
target_include_directories(elastiqp_c PUBLIC
|
|
46
|
+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
47
|
+
$<INSTALL_INTERFACE:include>)
|
|
48
|
+
target_compile_definitions(elastiqp_c PRIVATE ELASTIQP_C_BUILDING)
|
|
49
|
+
if(BUILD_SHARED_LIBS)
|
|
50
|
+
target_compile_definitions(elastiqp_c PUBLIC ELASTIQP_C_SHARED)
|
|
51
|
+
endif()
|
|
52
|
+
set_target_properties(elastiqp_c PROPERTIES
|
|
53
|
+
CXX_VISIBILITY_PRESET hidden
|
|
54
|
+
VISIBILITY_INLINES_HIDDEN ON
|
|
55
|
+
POSITION_INDEPENDENT_CODE ON
|
|
56
|
+
VERSION ${PROJECT_VERSION}
|
|
57
|
+
SOVERSION ${PROJECT_VERSION_MAJOR})
|
|
58
|
+
endif()
|
|
59
|
+
|
|
37
60
|
# Headers for tests (never installed)
|
|
38
61
|
add_library(elastiqp_testing INTERFACE)
|
|
39
62
|
add_library(elastiqp::testing ALIAS elastiqp_testing)
|
|
@@ -55,6 +78,9 @@ if(ELASTIQP_BUILD_TESTS)
|
|
|
55
78
|
elastiqp_test(test_das)
|
|
56
79
|
add_test(NAME elastiqp.das COMMAND test_das)
|
|
57
80
|
|
|
81
|
+
elastiqp_test(test_das_alloc)
|
|
82
|
+
add_test(NAME elastiqp.das_alloc COMMAND test_das_alloc)
|
|
83
|
+
|
|
58
84
|
elastiqp_test(test_pdal)
|
|
59
85
|
add_test(NAME elastiqp.pdal COMMAND test_pdal)
|
|
60
86
|
|
|
@@ -72,16 +98,34 @@ if(ELASTIQP_BUILD_TESTS)
|
|
|
72
98
|
|
|
73
99
|
elastiqp_test(test_lp)
|
|
74
100
|
add_test(NAME elastiqp.lp COMMAND test_lp)
|
|
101
|
+
|
|
102
|
+
if(ELASTIQP_BUILD_C)
|
|
103
|
+
add_executable(test_c_api tests/test_c_api.c)
|
|
104
|
+
target_link_libraries(test_c_api PRIVATE elastiqp::elastiqp_c)
|
|
105
|
+
if(NOT MSVC)
|
|
106
|
+
target_link_libraries(test_c_api PRIVATE m)
|
|
107
|
+
endif()
|
|
108
|
+
add_test(NAME elastiqp.c_api COMMAND test_c_api)
|
|
109
|
+
|
|
110
|
+
elastiqp_test(test_c_parity)
|
|
111
|
+
target_link_libraries(test_c_parity PRIVATE elastiqp::elastiqp_c)
|
|
112
|
+
add_test(NAME elastiqp.c_parity COMMAND test_c_parity)
|
|
113
|
+
endif()
|
|
75
114
|
endif()
|
|
76
115
|
|
|
77
116
|
# Python bindings and JAX FFI
|
|
78
117
|
if(ELASTIQP_BUILD_PYTHON)
|
|
79
118
|
add_library(elastiqp_jax_ffi SHARED bindings/jax_ffi.cc)
|
|
80
|
-
target_include_directories(elastiqp_jax_ffi PRIVATE
|
|
119
|
+
target_include_directories(elastiqp_jax_ffi SYSTEM PRIVATE
|
|
81
120
|
${CMAKE_CURRENT_SOURCE_DIR}/third_party/xla_ffi)
|
|
82
121
|
target_link_libraries(elastiqp_jax_ffi PRIVATE elastiqp::elastiqp)
|
|
122
|
+
# The XLA FFI handler symbols carry no dllexport; export them for ctypes.
|
|
123
|
+
set_target_properties(elastiqp_jax_ffi PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
|
83
124
|
if(SKBUILD)
|
|
84
|
-
|
|
125
|
+
# A Windows DLL is a RUNTIME artifact, not a LIBRARY one.
|
|
126
|
+
install(TARGETS elastiqp_jax_ffi
|
|
127
|
+
LIBRARY DESTINATION elastiqp
|
|
128
|
+
RUNTIME DESTINATION elastiqp)
|
|
85
129
|
endif()
|
|
86
130
|
|
|
87
131
|
find_package(Python 3.8 COMPONENTS Interpreter Development.Module QUIET)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: elastiqp
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: An always-feasible QP solver for constrained robot control
|
|
5
5
|
Author-Email: Daniel Morton <danielpmorton@gmail.com>
|
|
6
6
|
License: Copyright 2026 Daniel Morton
|
|
@@ -213,14 +213,14 @@ Provides-Extra: all
|
|
|
213
213
|
Requires-Dist: elastiqp[dev,examples,jax,torch]; extra == "all"
|
|
214
214
|
Description-Content-Type: text/markdown
|
|
215
215
|
|
|
216
|
+
<div align="center">
|
|
217
|
+
<img src="https://github.com/user-attachments/assets/12b43719-2b23-478d-9aaa-5481911741b4" alt="logo" width="400" height="73"></img>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
216
220
|
# ElastiQP
|
|
217
221
|
|
|
218
222
|
[](https://arxiv.org/abs/2609.19080)
|
|
219
223
|
|
|
220
|
-
> [!WARNING]
|
|
221
|
-
> Currently under construction, apologies for any rough edges
|
|
222
|
-
|
|
223
|
-
|
|
224
224
|
An always-feasible QP solver for constrained robot control.
|
|
225
225
|
|
|
226
226
|
ElastiQP solves the following problem:
|
|
@@ -281,9 +281,6 @@ If you've installed with CMake, you can also `find_package(elastiqp)`
|
|
|
281
281
|
|
|
282
282
|
#### From PyPI
|
|
283
283
|
|
|
284
|
-
> [!WARNING]
|
|
285
|
-
> The code is not yet available on PyPI but I will upload a copy shortly
|
|
286
|
-
|
|
287
284
|
```
|
|
288
285
|
pip install elastiqp
|
|
289
286
|
```
|
|
@@ -312,7 +309,7 @@ Note: if using UV, you can directly replace the above `pip` commands with `uv pi
|
|
|
312
309
|
elastiqp::Solution sol = elastiqp::Solve(Q, q, A, b, G, h, penalty);
|
|
313
310
|
// or without equalities: elastiqp::Solve(Q, q, G, h, penalty)
|
|
314
311
|
|
|
315
|
-
//
|
|
312
|
+
// Repeated solves
|
|
316
313
|
elastiqp::Solver solver;
|
|
317
314
|
solver.setup(Q, q, A, b, G, h, penalty);
|
|
318
315
|
while (running) {
|
|
@@ -342,9 +339,6 @@ sol = solver.solve()
|
|
|
342
339
|
|
|
343
340
|
### JAX / PyTorch
|
|
344
341
|
|
|
345
|
-
> [!WARNING]
|
|
346
|
-
> Differentiability support is still in beta, and is not implemented in the default (active set) backend
|
|
347
|
-
|
|
348
342
|
```python
|
|
349
343
|
import jax
|
|
350
344
|
jax.config.update("jax_enable_x64", True)
|
|
@@ -353,9 +347,9 @@ import elastiqp.jax
|
|
|
353
347
|
# Same API as Python, just with elastiqp.jax
|
|
354
348
|
sol = elastiqp.jax.solve(Q, q, G, h, penalty, A=A, b=b)
|
|
355
349
|
|
|
356
|
-
# Compatible with jax.grad and vjp
|
|
350
|
+
# Compatible with jax.grad and vjp
|
|
357
351
|
def loss(q_):
|
|
358
|
-
sol = elastiqp.jax.solve(Q, q_, G, h, penalty, A=A, b=b
|
|
352
|
+
sol = elastiqp.jax.solve(Q, q_, G, h, penalty, A=A, b=b)
|
|
359
353
|
return jnp.sum(sol.x**2)
|
|
360
354
|
|
|
361
355
|
grad_q = jax.grad(loss)(q)
|
|
@@ -379,6 +373,29 @@ _, xs = jax.lax.scan(step, init, qs)
|
|
|
379
373
|
|
|
380
374
|
For runnable Python/JAX/PyTorch examples, see the `examples` folder
|
|
381
375
|
|
|
376
|
+
### C
|
|
377
|
+
|
|
378
|
+
A new experimental C wrapper has recently been added for compatibility with libraries like [CasADi](https://github.com/casadi/casadi)
|
|
379
|
+
|
|
380
|
+
```c
|
|
381
|
+
#include "elastiqp/elastiqp_c.h"
|
|
382
|
+
|
|
383
|
+
ElastiQPProblem qp = {.n = n, .m = m, .p = p, .Q = Q, .q = q, .A = A, .b = b,
|
|
384
|
+
.G = G, .h = h, .penalty = penalty};
|
|
385
|
+
ElastiQPSolution res = {.x = x, .z = z};
|
|
386
|
+
elastiqp_solve_once(&res, &qp, NULL); // NULL: default settings
|
|
387
|
+
|
|
388
|
+
// Repeated solves
|
|
389
|
+
ElastiQPWorkspace* work;
|
|
390
|
+
elastiqp_setup(&work, &qp, NULL);
|
|
391
|
+
while (running) {
|
|
392
|
+
elastiqp_set_q(work, q_k); elastiqp_set_h(work, h_k); elastiqp_set_b(work, b_k);
|
|
393
|
+
elastiqp_set_G(work, G_k);
|
|
394
|
+
if (elastiqp_solve(&res, work) != ELASTIQP_SOLVED) { /* ... */ }
|
|
395
|
+
}
|
|
396
|
+
elastiqp_free(work);
|
|
397
|
+
```
|
|
398
|
+
|
|
382
399
|
## Benchmarks
|
|
383
400
|
|
|
384
401
|
> [!WARNING]
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://github.com/user-attachments/assets/12b43719-2b23-478d-9aaa-5481911741b4" alt="logo" width="400" height="73"></img>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
1
5
|
# ElastiQP
|
|
2
6
|
|
|
3
7
|
[](https://arxiv.org/abs/2609.19080)
|
|
4
8
|
|
|
5
|
-
> [!WARNING]
|
|
6
|
-
> Currently under construction, apologies for any rough edges
|
|
7
|
-
|
|
8
|
-
|
|
9
9
|
An always-feasible QP solver for constrained robot control.
|
|
10
10
|
|
|
11
11
|
ElastiQP solves the following problem:
|
|
@@ -66,9 +66,6 @@ If you've installed with CMake, you can also `find_package(elastiqp)`
|
|
|
66
66
|
|
|
67
67
|
#### From PyPI
|
|
68
68
|
|
|
69
|
-
> [!WARNING]
|
|
70
|
-
> The code is not yet available on PyPI but I will upload a copy shortly
|
|
71
|
-
|
|
72
69
|
```
|
|
73
70
|
pip install elastiqp
|
|
74
71
|
```
|
|
@@ -97,7 +94,7 @@ Note: if using UV, you can directly replace the above `pip` commands with `uv pi
|
|
|
97
94
|
elastiqp::Solution sol = elastiqp::Solve(Q, q, A, b, G, h, penalty);
|
|
98
95
|
// or without equalities: elastiqp::Solve(Q, q, G, h, penalty)
|
|
99
96
|
|
|
100
|
-
//
|
|
97
|
+
// Repeated solves
|
|
101
98
|
elastiqp::Solver solver;
|
|
102
99
|
solver.setup(Q, q, A, b, G, h, penalty);
|
|
103
100
|
while (running) {
|
|
@@ -127,9 +124,6 @@ sol = solver.solve()
|
|
|
127
124
|
|
|
128
125
|
### JAX / PyTorch
|
|
129
126
|
|
|
130
|
-
> [!WARNING]
|
|
131
|
-
> Differentiability support is still in beta, and is not implemented in the default (active set) backend
|
|
132
|
-
|
|
133
127
|
```python
|
|
134
128
|
import jax
|
|
135
129
|
jax.config.update("jax_enable_x64", True)
|
|
@@ -138,9 +132,9 @@ import elastiqp.jax
|
|
|
138
132
|
# Same API as Python, just with elastiqp.jax
|
|
139
133
|
sol = elastiqp.jax.solve(Q, q, G, h, penalty, A=A, b=b)
|
|
140
134
|
|
|
141
|
-
# Compatible with jax.grad and vjp
|
|
135
|
+
# Compatible with jax.grad and vjp
|
|
142
136
|
def loss(q_):
|
|
143
|
-
sol = elastiqp.jax.solve(Q, q_, G, h, penalty, A=A, b=b
|
|
137
|
+
sol = elastiqp.jax.solve(Q, q_, G, h, penalty, A=A, b=b)
|
|
144
138
|
return jnp.sum(sol.x**2)
|
|
145
139
|
|
|
146
140
|
grad_q = jax.grad(loss)(q)
|
|
@@ -164,6 +158,29 @@ _, xs = jax.lax.scan(step, init, qs)
|
|
|
164
158
|
|
|
165
159
|
For runnable Python/JAX/PyTorch examples, see the `examples` folder
|
|
166
160
|
|
|
161
|
+
### C
|
|
162
|
+
|
|
163
|
+
A new experimental C wrapper has recently been added for compatibility with libraries like [CasADi](https://github.com/casadi/casadi)
|
|
164
|
+
|
|
165
|
+
```c
|
|
166
|
+
#include "elastiqp/elastiqp_c.h"
|
|
167
|
+
|
|
168
|
+
ElastiQPProblem qp = {.n = n, .m = m, .p = p, .Q = Q, .q = q, .A = A, .b = b,
|
|
169
|
+
.G = G, .h = h, .penalty = penalty};
|
|
170
|
+
ElastiQPSolution res = {.x = x, .z = z};
|
|
171
|
+
elastiqp_solve_once(&res, &qp, NULL); // NULL: default settings
|
|
172
|
+
|
|
173
|
+
// Repeated solves
|
|
174
|
+
ElastiQPWorkspace* work;
|
|
175
|
+
elastiqp_setup(&work, &qp, NULL);
|
|
176
|
+
while (running) {
|
|
177
|
+
elastiqp_set_q(work, q_k); elastiqp_set_h(work, h_k); elastiqp_set_b(work, b_k);
|
|
178
|
+
elastiqp_set_G(work, G_k);
|
|
179
|
+
if (elastiqp_solve(&res, work) != ELASTIQP_SOLVED) { /* ... */ }
|
|
180
|
+
}
|
|
181
|
+
elastiqp_free(work);
|
|
182
|
+
```
|
|
183
|
+
|
|
167
184
|
## Benchmarks
|
|
168
185
|
|
|
169
186
|
> [!WARNING]
|
|
@@ -43,20 +43,14 @@ inline void apply_options(elastiqp::pdal::Settings& s,
|
|
|
43
43
|
std::optional<double> eps,
|
|
44
44
|
std::optional<int> max_iter,
|
|
45
45
|
std::optional<bool> ruiz) {
|
|
46
|
-
if (eps)
|
|
47
|
-
s.eps_abs = *eps;
|
|
48
|
-
s.eps_duality_gap_abs = *eps;
|
|
49
|
-
}
|
|
46
|
+
if (eps) s.eps_abs = *eps;
|
|
50
47
|
if (max_iter) s.max_outer_iter = *max_iter;
|
|
51
48
|
if (ruiz) s.ruiz = *ruiz;
|
|
52
49
|
}
|
|
53
50
|
inline void apply_options(elastiqp::ipm::Settings& s, std::optional<double> eps,
|
|
54
51
|
std::optional<int> max_iter,
|
|
55
52
|
std::optional<bool> ruiz) {
|
|
56
|
-
if (eps)
|
|
57
|
-
s.eps_abs = *eps;
|
|
58
|
-
s.eps_duality_gap_abs = *eps;
|
|
59
|
-
}
|
|
53
|
+
if (eps) s.eps_abs = *eps;
|
|
60
54
|
if (max_iter) s.max_iter = *max_iter;
|
|
61
55
|
if (ruiz) s.ruiz = *ruiz;
|
|
62
56
|
}
|
|
@@ -291,6 +285,7 @@ NB_MODULE(_core, m) {
|
|
|
291
285
|
.def_rw("eps_rel", &S::eps_rel)
|
|
292
286
|
.def_rw("sing_tol", &S::sing_tol)
|
|
293
287
|
.def_rw("zero_tol", &S::zero_tol)
|
|
288
|
+
.def_rw("cond_iters", &S::cond_iters)
|
|
294
289
|
.def_rw("eps_prox", &S::eps_prox)
|
|
295
290
|
.def_rw("eta_prox", &S::eta_prox)
|
|
296
291
|
.def_rw("prox_relaxation", &S::prox_relaxation)
|
|
@@ -305,7 +300,9 @@ NB_MODULE(_core, m) {
|
|
|
305
300
|
.def_rw("check_eq_consistency", &S::check_eq_consistency)
|
|
306
301
|
.def_rw("progress_tol", &S::progress_tol)
|
|
307
302
|
.def_rw("cycle_tol", &S::cycle_tol)
|
|
308
|
-
.def_rw("refactor_tol", &S::refactor_tol)
|
|
303
|
+
.def_rw("refactor_tol", &S::refactor_tol)
|
|
304
|
+
.def_rw("relax_reg", &S::relax_reg)
|
|
305
|
+
.def_rw("relax_factor_retries", &S::relax_factor_retries);
|
|
309
306
|
|
|
310
307
|
using Sv = elastiqp::das::Solver;
|
|
311
308
|
nb::enum_<Sv::RowState>(das, "RowState")
|
|
@@ -317,6 +314,7 @@ NB_MODULE(_core, m) {
|
|
|
317
314
|
auto cls =
|
|
318
315
|
nb::class_<Sv>(das, "Solver", "Dual active-set backend (default)");
|
|
319
316
|
def_common(cls);
|
|
317
|
+
def_relax(cls, 50);
|
|
320
318
|
cls.def("row_state", &Sv::row_state, nb::arg("i"),
|
|
321
319
|
"RowState of inequality row i")
|
|
322
320
|
.def("proximal", &Sv::proximal, "True if Q is proximally shifted")
|
|
@@ -369,45 +367,28 @@ NB_MODULE(_core, m) {
|
|
|
369
367
|
.def_rw("bcl_release_jump", &S::bcl_release_jump)
|
|
370
368
|
.def_rw("bcl_release_jump_horizon", &S::bcl_release_jump_horizon)
|
|
371
369
|
.def_rw("bcl_warm_eta", &S::bcl_warm_eta)
|
|
372
|
-
.def_rw("cold_reset_mu", &S::cold_reset_mu)
|
|
373
|
-
.def_rw("cold_reset_threshold", &S::cold_reset_threshold)
|
|
374
|
-
.def_rw("cold_reset_residual", &S::cold_reset_residual)
|
|
375
|
-
.def_rw("cold_reset_limit", &S::cold_reset_limit)
|
|
376
370
|
.def_rw("safe_guard", &S::safe_guard)
|
|
377
371
|
.def_rw("ruiz", &S::ruiz)
|
|
378
372
|
.def_rw("ruiz_max_iter", &S::ruiz_max_iter)
|
|
379
373
|
.def_rw("ruiz_tol", &S::ruiz_tol)
|
|
380
374
|
.def_rw("ruiz_refresh_ratio", &S::ruiz_refresh_ratio)
|
|
381
|
-
.def_rw("relax_reg", &S::relax_reg)
|
|
382
|
-
.def_rw("relax_warm_budget", &S::relax_warm_budget)
|
|
383
|
-
.def_rw("relax_warm_flip_tol", &S::relax_warm_flip_tol);
|
|
375
|
+
.def_rw("relax_reg", &S::relax_reg);
|
|
384
376
|
|
|
385
377
|
using Sv = elastiqp::pdal::Solver;
|
|
386
378
|
auto cls = nb::class_<Sv>(pdal, "Solver",
|
|
387
379
|
"Primal-dual augmented Lagrangian backend");
|
|
388
380
|
def_common(cls);
|
|
381
|
+
def_relax(cls, 50);
|
|
389
382
|
cls.def(
|
|
390
383
|
"solution", [](const Sv& s) -> Solution { return s.solution(); },
|
|
391
384
|
"Result of the last solve() or relax()")
|
|
392
385
|
.def("factorizations", &Sv::factorizations,
|
|
393
386
|
"KKT factorizations in the last solve()")
|
|
394
|
-
.def("cold_resets", &Sv::cold_resets,
|
|
395
|
-
"BCL cold resets in the last solve()")
|
|
396
387
|
.def("reequilibrate", &Sv::reequilibrate,
|
|
397
388
|
"Recompute the Ruiz scaling in place; solve() does this when "
|
|
398
389
|
"drift exceeds settings.ruiz_refresh_ratio")
|
|
399
390
|
.def("scaling_drift", &Sv::scaling_drift,
|
|
400
391
|
"Largest factor a scaled row/column max-norm has drifted from 1")
|
|
401
|
-
.def(
|
|
402
|
-
"relax",
|
|
403
|
-
[](Sv& s, double kappa, double tol, int max_iter, bool warm)
|
|
404
|
-
-> Solution { return s.relax(kappa, tol, max_iter, warm); },
|
|
405
|
-
nb::arg("kappa"), nb::arg("tol") = 1e-6, nb::arg("max_iter") = 50,
|
|
406
|
-
nb::arg("warm") = true,
|
|
407
|
-
"Move the solve() result to the kappa-relaxed point (s.z = kappa) "
|
|
408
|
-
"for differentiation; the solver's own iterate stays tight. With "
|
|
409
|
-
"warm=True, repeated calls continue from the previous relaxed "
|
|
410
|
-
"point.")
|
|
411
392
|
.def(
|
|
412
393
|
"set_warm_start",
|
|
413
394
|
[](Sv& s, const Eigen::VectorXd& x, const Eigen::VectorXd& y,
|
|
@@ -431,7 +412,6 @@ NB_MODULE(_core, m) {
|
|
|
431
412
|
.def_rw("eps_duality_gap_abs", &S::eps_duality_gap_abs)
|
|
432
413
|
.def_rw("eps_duality_gap_rel", &S::eps_duality_gap_rel)
|
|
433
414
|
.def_rw("max_factor_retries", &S::max_factor_retries)
|
|
434
|
-
.def_rw("warm_start", &S::warm_start)
|
|
435
415
|
.def_rw("check_eq_consistency", &S::check_eq_consistency)
|
|
436
416
|
.def_rw("max_iter", &S::max_iter)
|
|
437
417
|
.def_rw("rho_init", &S::rho_init)
|
|
@@ -444,9 +424,6 @@ NB_MODULE(_core, m) {
|
|
|
444
424
|
.def_rw("reg_finetune_dual_update_threshold",
|
|
445
425
|
&S::reg_finetune_dual_update_threshold)
|
|
446
426
|
.def_rw("tau", &S::tau)
|
|
447
|
-
.def_rw("warm_start_fraction", &S::warm_start_fraction)
|
|
448
|
-
.def_rw("warm_start_min_floor", &S::warm_start_min_floor)
|
|
449
|
-
.def_rw("warm_start_max_floor", &S::warm_start_max_floor)
|
|
450
427
|
.def_rw("ruiz", &S::ruiz)
|
|
451
428
|
.def_rw("ruiz_max_iter", &S::ruiz_max_iter)
|
|
452
429
|
.def_rw("ruiz_tol", &S::ruiz_tol);
|
|
@@ -455,35 +432,8 @@ NB_MODULE(_core, m) {
|
|
|
455
432
|
auto cls = nb::class_<Sv>(ipm, "Solver", "Proximal interior-point backend");
|
|
456
433
|
def_common(cls);
|
|
457
434
|
def_relax(cls, 30);
|
|
458
|
-
cls.def(
|
|
459
|
-
|
|
460
|
-
"Result of the last solve() or relax()")
|
|
461
|
-
.def(
|
|
462
|
-
"set_warm_start",
|
|
463
|
-
[](Sv& s, const Eigen::VectorXd& x, const Eigen::VectorXd& t,
|
|
464
|
-
const Eigen::VectorXd& y, const Eigen::VectorXd& s_t,
|
|
465
|
-
const Eigen::VectorXd& s_ineq, const Eigen::VectorXd& z_t,
|
|
466
|
-
const Eigen::VectorXd& z, double rho, double delta) {
|
|
467
|
-
s.set_warm_start(x, t, y, s_t, s_ineq, z_t, z, rho, delta);
|
|
468
|
-
},
|
|
469
|
-
nb::arg("x"), nb::arg("t"), nb::arg("y"), nb::arg("s_t"),
|
|
470
|
-
nb::arg("s_ineq"), nb::arg("z_t"), nb::arg("z"), nb::kw_only(),
|
|
471
|
-
nb::arg("rho") = 0.0, nb::arg("delta") = 0.0,
|
|
472
|
-
"Seed the next solve() with a strictly interior iterate, used as "
|
|
473
|
-
"given.")
|
|
474
|
-
.def(
|
|
475
|
-
"warm_start_from",
|
|
476
|
-
[](Sv& s, const Solution& sol) { s.warm_start_from(sol); },
|
|
477
|
-
nb::arg("solution"),
|
|
478
|
-
"Seed the next solve() from a Solution; slacks are rebuilt and "
|
|
479
|
-
"floored off the boundary.")
|
|
480
|
-
.def(
|
|
481
|
-
"warm_start_from",
|
|
482
|
-
[](Sv& s, const Eigen::VectorXd& x, const Eigen::VectorXd& y,
|
|
483
|
-
const Eigen::VectorXd& z) { s.warm_start_from(x, y, z); },
|
|
484
|
-
nb::arg("x"), nb::arg("y"), nb::arg("z"),
|
|
485
|
-
"Seed the next solve() from a user-frame (x, y, z); slacks are "
|
|
486
|
-
"rebuilt and floored off the boundary.");
|
|
435
|
+
cls.def("solution", [](const Sv& s) -> Solution { return s.solution(); },
|
|
436
|
+
"Result of the last solve() or relax()");
|
|
487
437
|
}
|
|
488
438
|
|
|
489
439
|
// --- one-shot solve ------------------------------------------------------
|
|
@@ -530,7 +480,7 @@ NB_MODULE(_core, m) {
|
|
|
530
480
|
"method: 'das' (default), 'pdal' or 'ipm'. eps_abs, max_iter (outer "
|
|
531
481
|
"budget) and ruiz override the backend defaults; settings= passes a "
|
|
532
482
|
"full Settings object for that method.\n\n"
|
|
533
|
-
"For gradients use
|
|
483
|
+
"For gradients use a Solver object with relax().";
|
|
534
484
|
|
|
535
485
|
m.def(
|
|
536
486
|
"solve",
|
|
@@ -585,11 +535,6 @@ NB_MODULE(_core, m) {
|
|
|
585
535
|
bool ruiz, double target_kappa, const std::string& method) {
|
|
586
536
|
const Method mth = parse_method(method);
|
|
587
537
|
const bool want_relax = target_kappa > 0 && h.size() > 0;
|
|
588
|
-
if (want_relax && mth == Method::kDAS) {
|
|
589
|
-
throw std::invalid_argument(
|
|
590
|
-
"method='das' has no kappa relaxation (not differentiable); "
|
|
591
|
-
"use method='pdal' or 'ipm' for gradients");
|
|
592
|
-
}
|
|
593
538
|
Solution sol, rsol;
|
|
594
539
|
// Gradient accuracy is set by the relax residual, so cap it at 1e-6.
|
|
595
540
|
const double rtol = std::min(eps_abs, 1e-6);
|
|
@@ -598,7 +543,7 @@ NB_MODULE(_core, m) {
|
|
|
598
543
|
apply_options(solver.settings, eps_abs, max_iter, ruiz);
|
|
599
544
|
solver.setup(Q, q, A, b, G, h, penalty);
|
|
600
545
|
sol = solver.solve();
|
|
601
|
-
rsol = sol;
|
|
546
|
+
rsol = want_relax ? solver.relax(target_kappa, rtol, 50) : sol;
|
|
602
547
|
} else if (mth == Method::kPDAL) {
|
|
603
548
|
elastiqp::pdal::Solver solver;
|
|
604
549
|
apply_options(solver.settings, eps_abs, max_iter, ruiz);
|