elastiqp 0.1.0__tar.gz → 0.2.0__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.
Files changed (53) hide show
  1. {elastiqp-0.1.0 → elastiqp-0.2.0}/.github/workflows/wheels.yml +14 -4
  2. {elastiqp-0.1.0 → elastiqp-0.2.0}/CMakeLists.txt +46 -2
  3. {elastiqp-0.1.0 → elastiqp-0.2.0}/PKG-INFO +31 -14
  4. {elastiqp-0.1.0 → elastiqp-0.2.0}/README.md +30 -13
  5. {elastiqp-0.1.0 → elastiqp-0.2.0}/bindings/bindings.cc +12 -68
  6. {elastiqp-0.1.0 → elastiqp-0.2.0}/bindings/jax_ffi.cc +20 -33
  7. elastiqp-0.2.0/cmake/elastiqpConfig.cmake.in +18 -0
  8. {elastiqp-0.1.0 → elastiqp-0.2.0}/cmake/elastiqp_install.cmake +8 -1
  9. {elastiqp-0.1.0 → elastiqp-0.2.0}/examples/basic_usage.py +1 -2
  10. {elastiqp-0.1.0 → elastiqp-0.2.0}/examples/jax_usage.py +1 -5
  11. {elastiqp-0.1.0 → elastiqp-0.2.0}/examples/torch_usage.py +1 -5
  12. {elastiqp-0.1.0 → elastiqp-0.2.0}/include/elastiqp/common.hpp +26 -2
  13. {elastiqp-0.1.0 → elastiqp-0.2.0}/include/elastiqp/das.hpp +401 -68
  14. elastiqp-0.2.0/include/elastiqp/elastiqp_c.h +233 -0
  15. {elastiqp-0.1.0 → elastiqp-0.2.0}/include/elastiqp/ipm.hpp +91 -140
  16. {elastiqp-0.1.0 → elastiqp-0.2.0}/include/elastiqp/kkt_vjp.hpp +7 -1
  17. {elastiqp-0.1.0 → elastiqp-0.2.0}/include/elastiqp/pdal.hpp +56 -119
  18. {elastiqp-0.1.0 → elastiqp-0.2.0}/package.xml +1 -1
  19. {elastiqp-0.1.0 → elastiqp-0.2.0}/python/elastiqp/__init__.py +11 -2
  20. {elastiqp-0.1.0 → elastiqp-0.2.0}/python/elastiqp/jax.py +76 -124
  21. {elastiqp-0.1.0 → elastiqp-0.2.0}/python/elastiqp/torch.py +20 -39
  22. elastiqp-0.2.0/src/elastiqp_c.cc +534 -0
  23. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/support/drift_traj.hpp +1 -1
  24. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/support/test_util.hpp +5 -2
  25. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_bindings.py +35 -17
  26. elastiqp-0.2.0/tests/test_c_api.c +425 -0
  27. elastiqp-0.2.0/tests/test_c_parity.cc +234 -0
  28. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_das.cc +298 -0
  29. elastiqp-0.2.0/tests/test_das_alloc.cc +397 -0
  30. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_ipm.cc +7 -122
  31. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_jax_ffi.py +130 -94
  32. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_pdal.cc +18 -85
  33. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_solvers.cc +3 -3
  34. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_torch.py +105 -37
  35. elastiqp-0.1.0/cmake/elastiqpConfig.cmake.in +0 -8
  36. {elastiqp-0.1.0 → elastiqp-0.2.0}/.clang-format +0 -0
  37. {elastiqp-0.1.0 → elastiqp-0.2.0}/.gitignore +0 -0
  38. {elastiqp-0.1.0 → elastiqp-0.2.0}/LICENSE +0 -0
  39. {elastiqp-0.1.0 → elastiqp-0.2.0}/NOTICE +0 -0
  40. {elastiqp-0.1.0 → elastiqp-0.2.0}/examples/constraint_conflict_demo.py +0 -0
  41. {elastiqp-0.1.0 → elastiqp-0.2.0}/include/elastiqp/elastiqp.hpp +0 -0
  42. {elastiqp-0.1.0 → elastiqp-0.2.0}/pyproject.toml +0 -0
  43. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/data/warmstart_dual_overshoot.npz +0 -0
  44. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/data/warmstart_stale_duals.npz +0 -0
  45. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/support/problem_gen.hpp +0 -0
  46. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_bcl_creep.cc +0 -0
  47. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_gap_creep.cc +0 -0
  48. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_lp.cc +0 -0
  49. {elastiqp-0.1.0 → elastiqp-0.2.0}/tests/test_ruiz.cc +0 -0
  50. {elastiqp-0.1.0 → elastiqp-0.2.0}/third_party/xla_ffi/README.md +0 -0
  51. {elastiqp-0.1.0 → elastiqp-0.2.0}/third_party/xla_ffi/xla/ffi/api/api.h +0 -0
  52. {elastiqp-0.1.0 → elastiqp-0.2.0}/third_party/xla_ffi/xla/ffi/api/c_api.h +0 -0
  53. {elastiqp-0.1.0 → elastiqp-0.2.0}/third_party/xla_ffi/xla/ffi/api/ffi.h +0 -0
@@ -1,6 +1,7 @@
1
- # Build manylinux + macOS wheels and an sdist, then publish to PyPI on a version tag.
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-14]
34
+ os: [ubuntu-latest, ubuntu-24.04-arm, macos-14, 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
- install(TARGETS elastiqp_jax_ffi LIBRARY DESTINATION elastiqp)
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.0
3
+ Version: 0.2.0
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
  [![Paper](http://img.shields.io/badge/arXiv-2609.19080-B31B1B.svg)](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
- // If you are solving multiple times in a control loop:
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 on the pdal / ipm backends
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, method="ipm")
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
  [![Paper](http://img.shields.io/badge/arXiv-2609.19080-B31B1B.svg)](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
- // If you are solving multiple times in a control loop:
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 on the pdal / ipm backends
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, method="ipm")
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
  }
@@ -305,7 +299,9 @@ NB_MODULE(_core, m) {
305
299
  .def_rw("check_eq_consistency", &S::check_eq_consistency)
306
300
  .def_rw("progress_tol", &S::progress_tol)
307
301
  .def_rw("cycle_tol", &S::cycle_tol)
308
- .def_rw("refactor_tol", &S::refactor_tol);
302
+ .def_rw("refactor_tol", &S::refactor_tol)
303
+ .def_rw("relax_reg", &S::relax_reg)
304
+ .def_rw("relax_factor_retries", &S::relax_factor_retries);
309
305
 
310
306
  using Sv = elastiqp::das::Solver;
311
307
  nb::enum_<Sv::RowState>(das, "RowState")
@@ -317,6 +313,7 @@ NB_MODULE(_core, m) {
317
313
  auto cls =
318
314
  nb::class_<Sv>(das, "Solver", "Dual active-set backend (default)");
319
315
  def_common(cls);
316
+ def_relax(cls, 50);
320
317
  cls.def("row_state", &Sv::row_state, nb::arg("i"),
321
318
  "RowState of inequality row i")
322
319
  .def("proximal", &Sv::proximal, "True if Q is proximally shifted")
@@ -369,45 +366,28 @@ NB_MODULE(_core, m) {
369
366
  .def_rw("bcl_release_jump", &S::bcl_release_jump)
370
367
  .def_rw("bcl_release_jump_horizon", &S::bcl_release_jump_horizon)
371
368
  .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
369
  .def_rw("safe_guard", &S::safe_guard)
377
370
  .def_rw("ruiz", &S::ruiz)
378
371
  .def_rw("ruiz_max_iter", &S::ruiz_max_iter)
379
372
  .def_rw("ruiz_tol", &S::ruiz_tol)
380
373
  .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);
374
+ .def_rw("relax_reg", &S::relax_reg);
384
375
 
385
376
  using Sv = elastiqp::pdal::Solver;
386
377
  auto cls = nb::class_<Sv>(pdal, "Solver",
387
378
  "Primal-dual augmented Lagrangian backend");
388
379
  def_common(cls);
380
+ def_relax(cls, 50);
389
381
  cls.def(
390
382
  "solution", [](const Sv& s) -> Solution { return s.solution(); },
391
383
  "Result of the last solve() or relax()")
392
384
  .def("factorizations", &Sv::factorizations,
393
385
  "KKT factorizations in the last solve()")
394
- .def("cold_resets", &Sv::cold_resets,
395
- "BCL cold resets in the last solve()")
396
386
  .def("reequilibrate", &Sv::reequilibrate,
397
387
  "Recompute the Ruiz scaling in place; solve() does this when "
398
388
  "drift exceeds settings.ruiz_refresh_ratio")
399
389
  .def("scaling_drift", &Sv::scaling_drift,
400
390
  "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
391
  .def(
412
392
  "set_warm_start",
413
393
  [](Sv& s, const Eigen::VectorXd& x, const Eigen::VectorXd& y,
@@ -431,7 +411,6 @@ NB_MODULE(_core, m) {
431
411
  .def_rw("eps_duality_gap_abs", &S::eps_duality_gap_abs)
432
412
  .def_rw("eps_duality_gap_rel", &S::eps_duality_gap_rel)
433
413
  .def_rw("max_factor_retries", &S::max_factor_retries)
434
- .def_rw("warm_start", &S::warm_start)
435
414
  .def_rw("check_eq_consistency", &S::check_eq_consistency)
436
415
  .def_rw("max_iter", &S::max_iter)
437
416
  .def_rw("rho_init", &S::rho_init)
@@ -444,9 +423,6 @@ NB_MODULE(_core, m) {
444
423
  .def_rw("reg_finetune_dual_update_threshold",
445
424
  &S::reg_finetune_dual_update_threshold)
446
425
  .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
426
  .def_rw("ruiz", &S::ruiz)
451
427
  .def_rw("ruiz_max_iter", &S::ruiz_max_iter)
452
428
  .def_rw("ruiz_tol", &S::ruiz_tol);
@@ -455,35 +431,8 @@ NB_MODULE(_core, m) {
455
431
  auto cls = nb::class_<Sv>(ipm, "Solver", "Proximal interior-point backend");
456
432
  def_common(cls);
457
433
  def_relax(cls, 30);
458
- cls.def(
459
- "solution", [](const Sv& s) -> Solution { return s.solution(); },
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.");
434
+ cls.def("solution", [](const Sv& s) -> Solution { return s.solution(); },
435
+ "Result of the last solve() or relax()");
487
436
  }
488
437
 
489
438
  // --- one-shot solve ------------------------------------------------------
@@ -530,7 +479,7 @@ NB_MODULE(_core, m) {
530
479
  "method: 'das' (default), 'pdal' or 'ipm'. eps_abs, max_iter (outer "
531
480
  "budget) and ruiz override the backend defaults; settings= passes a "
532
481
  "full Settings object for that method.\n\n"
533
- "For gradients use pdal.Solver or ipm.Solver with relax().";
482
+ "For gradients use a Solver object with relax().";
534
483
 
535
484
  m.def(
536
485
  "solve",
@@ -585,11 +534,6 @@ NB_MODULE(_core, m) {
585
534
  bool ruiz, double target_kappa, const std::string& method) {
586
535
  const Method mth = parse_method(method);
587
536
  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
537
  Solution sol, rsol;
594
538
  // Gradient accuracy is set by the relax residual, so cap it at 1e-6.
595
539
  const double rtol = std::min(eps_abs, 1e-6);
@@ -598,7 +542,7 @@ NB_MODULE(_core, m) {
598
542
  apply_options(solver.settings, eps_abs, max_iter, ruiz);
599
543
  solver.setup(Q, q, A, b, G, h, penalty);
600
544
  sol = solver.solve();
601
- rsol = sol;
545
+ rsol = want_relax ? solver.relax(target_kappa, rtol, 50) : sol;
602
546
  } else if (mth == Method::kPDAL) {
603
547
  elastiqp::pdal::Solver solver;
604
548
  apply_options(solver.settings, eps_abs, max_iter, ruiz);
@@ -4,14 +4,13 @@
4
4
  // ElastiqpSolve: (Q, q, A, b, G, h, penalty)
5
5
  // -> (x, t, y, z_t, z, xr, tr, yr, z_t_r, z_r, info)
6
6
  // ElastiqpSolveWarm: (Q, q, A, b, G, h, penalty, x0, y0, z0)
7
- // -> (x, t, y, z_t, z, info)
7
+ // -> (x, t, y, z_t, z, info) (das and pdal only)
8
8
  //
9
9
  // method: 0 = das, 1 = pdal, 2 = ipm. max_iter is the backend's outer budget.
10
10
  // The *r block is the kappa-relaxed point the Python wrappers differentiate
11
- // at; it copies the tight solution when target_kappa <= 0, and das cannot
12
- // relax. info = [converged, iters, relax_converged], without the last entry
13
- // for the warm handler. A has 0 rows and b 0 entries when there are no
14
- // equalities.
11
+ // at; it copies the tight solution when target_kappa <= 0. info = [converged,
12
+ // iters, relax_converged], without the last entry for the warm handler. A has 0
13
+ // rows and b 0 entries when there are no equalities.
15
14
 
16
15
  #include <algorithm>
17
16
  #include <cstdint>
@@ -46,7 +45,6 @@ template <>
46
45
  void apply_options(elastiqp::pdal::Solver& solver, double eps_abs,
47
46
  int64_t max_iter, int64_t ruiz) {
48
47
  solver.settings.eps_abs = eps_abs;
49
- solver.settings.eps_duality_gap_abs = eps_abs;
50
48
  solver.settings.max_outer_iter = static_cast<int>(max_iter);
51
49
  solver.settings.ruiz = ruiz != 0;
52
50
  }
@@ -54,22 +52,12 @@ template <>
54
52
  void apply_options(elastiqp::ipm::Solver& solver, double eps_abs,
55
53
  int64_t max_iter, int64_t ruiz) {
56
54
  solver.settings.eps_abs = eps_abs;
57
- solver.settings.eps_duality_gap_abs = eps_abs;
58
55
  solver.settings.max_iter = static_cast<int>(max_iter);
59
56
  solver.settings.ruiz = ruiz != 0;
60
57
  }
61
58
 
62
- template <typename SolverT>
63
- void apply_warm_start(SolverT& solver, const VectorXd& x0, const VectorXd& y0,
64
- const VectorXd& z0) {
65
- if constexpr (std::is_same_v<SolverT, elastiqp::ipm::Solver>) {
66
- solver.warm_start_from(x0, y0, z0); // reconstructs + floors the slacks
67
- } else {
68
- solver.set_warm_start(x0, y0, z0);
69
- }
70
- }
71
-
72
- // x0 != nullptr seeds the solve from (x0, y0, z0).
59
+ // x0 != nullptr seeds the solve from (x0, y0, z0); the IPM has no warm
60
+ // start (dispatch rejects it).
73
61
  template <typename SolverT>
74
62
  void run_solver(double eps_abs, int64_t max_iter, int64_t ruiz,
75
63
  double target_kappa, int64_t n, int64_t m, int64_t p,
@@ -83,19 +71,18 @@ void run_solver(double eps_abs, int64_t max_iter, int64_t ruiz,
83
71
  solver.setup(MapMatrix(Q, n, n), MapVector(q, n), MapMatrix(A, m, n),
84
72
  MapVector(b, m), MapMatrix(G, p, n), MapVector(h, p),
85
73
  MapVector(penalty, p));
86
- if (x0 != nullptr) {
87
- apply_warm_start(solver, VectorXd(MapVector(x0, n)),
88
- VectorXd(MapVector(y0, m)), VectorXd(MapVector(z0, p)));
74
+ if constexpr (!std::is_same_v<SolverT, elastiqp::ipm::Solver>) {
75
+ if (x0 != nullptr) {
76
+ solver.set_warm_start(VectorXd(MapVector(x0, n)),
77
+ VectorXd(MapVector(y0, m)),
78
+ VectorXd(MapVector(z0, p)));
79
+ }
89
80
  }
90
81
  sol = solver.solve();
91
82
  // Gradient accuracy is set by the relax residual, so cap it at 1e-6.
92
- if constexpr (std::is_same_v<SolverT, elastiqp::das::Solver>) {
93
- rsol = sol;
94
- } else {
95
- rsol = (target_kappa > 0 && p > 0)
96
- ? solver.relax(target_kappa, std::min(eps_abs, 1e-6), 50)
97
- : sol;
98
- }
83
+ rsol = (target_kappa > 0 && p > 0)
84
+ ? solver.relax(target_kappa, std::min(eps_abs, 1e-6), 50)
85
+ : sol;
99
86
  }
100
87
 
101
88
  ffi::Error dispatch(int64_t method, double eps_abs, int64_t max_iter,
@@ -107,11 +94,6 @@ ffi::Error dispatch(int64_t method, double eps_abs, int64_t max_iter,
107
94
  elastiqp::Solution& rsol) {
108
95
  switch (method) {
109
96
  case 0:
110
- if (target_kappa > 0 && p > 0) {
111
- return ffi::Error(ffi::ErrorCode::kInvalidArgument,
112
- "method='das' has no kappa relaxation (not "
113
- "differentiable); use method='pdal' or 'ipm'");
114
- }
115
97
  run_solver<elastiqp::das::Solver>(eps_abs, max_iter, ruiz, target_kappa,
116
98
  n, m, p, Q, q, A, b, G, h, penalty, x0,
117
99
  y0, z0, sol, rsol);
@@ -122,6 +104,11 @@ ffi::Error dispatch(int64_t method, double eps_abs, int64_t max_iter,
122
104
  y0, z0, sol, rsol);
123
105
  return ffi::Error::Success();
124
106
  case 2:
107
+ if (x0 != nullptr) {
108
+ return ffi::Error(ffi::ErrorCode::kInvalidArgument,
109
+ "method='ipm' has no warm start; use method='das' "
110
+ "or 'pdal'");
111
+ }
125
112
  run_solver<elastiqp::ipm::Solver>(eps_abs, max_iter, ruiz, target_kappa,
126
113
  n, m, p, Q, q, A, b, G, h, penalty, x0,
127
114
  y0, z0, sol, rsol);
@@ -0,0 +1,18 @@
1
+ @PACKAGE_INIT@
2
+
3
+ include(CMakeFindDependencyMacro)
4
+ find_dependency(Eigen3)
5
+
6
+ include("${CMAKE_CURRENT_LIST_DIR}/elastiqpTargets.cmake")
7
+
8
+ # The static C interface requires the C++ linker and runtime.
9
+ if(TARGET elastiqp::elastiqp_c)
10
+ get_target_property(_elastiqp_c_type elastiqp::elastiqp_c TYPE)
11
+ get_property(_elastiqp_languages GLOBAL PROPERTY ENABLED_LANGUAGES)
12
+ if(_elastiqp_c_type STREQUAL "STATIC_LIBRARY"
13
+ AND NOT "CXX" IN_LIST _elastiqp_languages)
14
+ enable_language(CXX)
15
+ endif()
16
+ endif()
17
+
18
+ check_required_components(elastiqp)
@@ -3,7 +3,14 @@ include(CMakePackageConfigHelpers)
3
3
 
4
4
  install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
5
5
 
6
- install(TARGETS elastiqp EXPORT elastiqpTargets)
6
+ set(_elastiqp_install_targets elastiqp)
7
+ if(ELASTIQP_BUILD_C)
8
+ list(APPEND _elastiqp_install_targets elastiqp_c)
9
+ endif()
10
+ install(TARGETS ${_elastiqp_install_targets} EXPORT elastiqpTargets
11
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
12
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
13
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
7
14
  install(EXPORT elastiqpTargets
8
15
  NAMESPACE elastiqp::
9
16
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/elastiqp)
@@ -121,8 +121,7 @@ def main():
121
121
  print("\nNote that this speedup can be significant on robot control problems!")
122
122
 
123
123
  print("\n--- Differentiability ---")
124
- print("\nThe 'pdal' and 'ipm' backends can relax() a solution to the")
125
- print("kappa-smoothed differentiation point; see the JAX and PyTorch examples")
124
+ print("For smooth derivative evaluation, see the JAX and PyTorch examples")
126
125
 
127
126
 
128
127
  if __name__ == "__main__":
@@ -68,9 +68,6 @@ def main():
68
68
  print("\nAs in solvers like qpax, we relax the solution to some kappa > 0")
69
69
  print("smoothed value for well-conditioned gradients")
70
70
 
71
- print("\nNote: Currently, differentiation is only supported in the `ipm`")
72
- print("and `pdal` backends")
73
-
74
71
  def loss(q_, kappa=1e-6):
75
72
  sol = elastiqp.jax.solve(
76
73
  Q,
@@ -80,7 +77,6 @@ def main():
80
77
  penalty,
81
78
  A=A,
82
79
  b=b,
83
- method="pdal",
84
80
  target_kappa=kappa,
85
81
  eps_abs=1e-10,
86
82
  )
@@ -108,7 +104,7 @@ def main():
108
104
  def x0(h0, kappa):
109
105
  h_mod = h.at[0].set(h0)
110
106
  return elastiqp.jax.solve(
111
- Q, q, G, h_mod, penalty, A=A, b=b, method="pdal", target_kappa=kappa
107
+ Q, q, G, h_mod, penalty, A=A, b=b, target_kappa=kappa
112
108
  ).x[0]
113
109
 
114
110
  print("\nTo see this, let's compare d(x0)/d(h0): the sensitivity of x0 to its")