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.
Files changed (54) hide show
  1. {elastiqp-0.1.0 → elastiqp-0.2.1}/.github/workflows/wheels.yml +14 -4
  2. {elastiqp-0.1.0 → elastiqp-0.2.1}/CMakeLists.txt +46 -2
  3. {elastiqp-0.1.0 → elastiqp-0.2.1}/PKG-INFO +31 -14
  4. {elastiqp-0.1.0 → elastiqp-0.2.1}/README.md +30 -13
  5. {elastiqp-0.1.0 → elastiqp-0.2.1}/bindings/bindings.cc +13 -68
  6. {elastiqp-0.1.0 → elastiqp-0.2.1}/bindings/jax_ffi.cc +41 -97
  7. elastiqp-0.2.1/cmake/elastiqpConfig.cmake.in +18 -0
  8. {elastiqp-0.1.0 → elastiqp-0.2.1}/cmake/elastiqp_install.cmake +8 -1
  9. {elastiqp-0.1.0 → elastiqp-0.2.1}/examples/basic_usage.py +1 -2
  10. {elastiqp-0.1.0 → elastiqp-0.2.1}/examples/jax_usage.py +1 -5
  11. {elastiqp-0.1.0 → elastiqp-0.2.1}/examples/torch_usage.py +1 -5
  12. {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/common.hpp +26 -2
  13. {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/das.hpp +434 -73
  14. elastiqp-0.2.1/include/elastiqp/elastiqp_c.h +236 -0
  15. {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/ipm.hpp +91 -140
  16. {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/kkt_vjp.hpp +7 -1
  17. {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/pdal.hpp +56 -119
  18. {elastiqp-0.1.0 → elastiqp-0.2.1}/package.xml +1 -1
  19. {elastiqp-0.1.0 → elastiqp-0.2.1}/python/elastiqp/__init__.py +11 -2
  20. elastiqp-0.2.1/python/elastiqp/jax.py +413 -0
  21. {elastiqp-0.1.0 → elastiqp-0.2.1}/python/elastiqp/torch.py +20 -39
  22. elastiqp-0.2.1/src/elastiqp_c.cc +538 -0
  23. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/support/drift_traj.hpp +1 -1
  24. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/support/test_util.hpp +5 -2
  25. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_bindings.py +35 -17
  26. elastiqp-0.2.1/tests/test_c_api.c +425 -0
  27. elastiqp-0.2.1/tests/test_c_parity.cc +235 -0
  28. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_das.cc +355 -3
  29. elastiqp-0.2.1/tests/test_das_alloc.cc +397 -0
  30. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_ipm.cc +7 -122
  31. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_jax_ffi.py +219 -113
  32. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_pdal.cc +18 -85
  33. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_solvers.cc +3 -3
  34. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_torch.py +105 -37
  35. elastiqp-0.1.0/cmake/elastiqpConfig.cmake.in +0 -8
  36. elastiqp-0.1.0/python/elastiqp/jax.py +0 -549
  37. {elastiqp-0.1.0 → elastiqp-0.2.1}/.clang-format +0 -0
  38. {elastiqp-0.1.0 → elastiqp-0.2.1}/.gitignore +0 -0
  39. {elastiqp-0.1.0 → elastiqp-0.2.1}/LICENSE +0 -0
  40. {elastiqp-0.1.0 → elastiqp-0.2.1}/NOTICE +0 -0
  41. {elastiqp-0.1.0 → elastiqp-0.2.1}/examples/constraint_conflict_demo.py +0 -0
  42. {elastiqp-0.1.0 → elastiqp-0.2.1}/include/elastiqp/elastiqp.hpp +0 -0
  43. {elastiqp-0.1.0 → elastiqp-0.2.1}/pyproject.toml +0 -0
  44. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/data/warmstart_dual_overshoot.npz +0 -0
  45. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/data/warmstart_stale_duals.npz +0 -0
  46. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/support/problem_gen.hpp +0 -0
  47. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_bcl_creep.cc +0 -0
  48. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_gap_creep.cc +0 -0
  49. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_lp.cc +0 -0
  50. {elastiqp-0.1.0 → elastiqp-0.2.1}/tests/test_ruiz.cc +0 -0
  51. {elastiqp-0.1.0 → elastiqp-0.2.1}/third_party/xla_ffi/README.md +0 -0
  52. {elastiqp-0.1.0 → elastiqp-0.2.1}/third_party/xla_ffi/xla/ffi/api/api.h +0 -0
  53. {elastiqp-0.1.0 → elastiqp-0.2.1}/third_party/xla_ffi/xla/ffi/api/c_api.h +0 -0
  54. {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 + 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-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
- 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.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
  [![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
  }
@@ -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
- "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.");
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 pdal.Solver or ipm.Solver with relax().";
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);