ipax 0.2.0__tar.gz → 0.3.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 (164) hide show
  1. {ipax-0.2.0 → ipax-0.3.0}/AGENTS.md +40 -2
  2. {ipax-0.2.0 → ipax-0.3.0}/PKG-INFO +1 -1
  3. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/corpus/__init__.py +39 -0
  4. ipax-0.3.0/benchmarks/corpus/s2mpj.py +498 -0
  5. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/harness/__init__.py +51 -9
  6. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/runners/qc.py +2 -0
  7. ipax-0.3.0/benchmarks/runners/s2mpj.py +460 -0
  8. ipax-0.3.0/docs/benchmarks/s2mpj.md +129 -0
  9. {ipax-0.2.0 → ipax-0.3.0}/docs/concepts/linalg.md +14 -0
  10. {ipax-0.2.0 → ipax-0.3.0}/ipax/__init__.py +1 -1
  11. {ipax-0.2.0 → ipax-0.3.0}/ipax/_logging.py +37 -10
  12. {ipax-0.2.0 → ipax-0.3.0}/ipax/backend/operators.py +96 -0
  13. {ipax-0.2.0 → ipax-0.3.0}/ipax/backend/sparse/_routing.py +4 -1
  14. {ipax-0.2.0 → ipax-0.3.0}/ipax/backend/sparse/cupy.py +35 -5
  15. {ipax-0.2.0 → ipax-0.3.0}/ipax/backend/sparse/numpy_scipy.py +72 -20
  16. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/driver.py +85 -71
  17. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/filter_ls.py +18 -4
  18. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/init.py +41 -1
  19. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/kkt.py +14 -0
  20. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/restoration.py +23 -1
  21. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/termination.py +11 -0
  22. {ipax-0.2.0 → ipax-0.3.0}/ipax/linalg/sparse.py +7 -1
  23. {ipax-0.2.0 → ipax-0.3.0}/ipax/options.py +8 -6
  24. ipax-0.3.0/ipax/problem/linear_ineq.py +196 -0
  25. {ipax-0.2.0 → ipax-0.3.0}/ipax/solve.py +11 -4
  26. {ipax-0.2.0 → ipax-0.3.0}/ipax/testing/problems.py +253 -0
  27. {ipax-0.2.0 → ipax-0.3.0}/tests/contracts/test_builtin_operator_contracts.py +12 -0
  28. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_benchmark_harness.py +12 -1
  29. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_callback_and_logging.py +71 -0
  30. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_hock_schittkowski.py +52 -1
  31. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_known_optima.py +33 -0
  32. ipax-0.3.0/tests/integration/test_linear_inequalities.py +224 -0
  33. ipax-0.3.0/tests/integration/test_s2mpj_corpus.py +226 -0
  34. ipax-0.3.0/tests/property/test_derivative_checks.py +189 -0
  35. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_filter_ls.py +21 -0
  36. ipax-0.3.0/tests/unit/test_init_bounds.py +32 -0
  37. ipax-0.3.0/tests/unit/test_linear_ineq_lowering.py +105 -0
  38. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_logging.py +52 -0
  39. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_operator_coo.py +31 -0
  40. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_restoration.py +43 -0
  41. ipax-0.3.0/tests/unit/test_s2mpj_adapter.py +202 -0
  42. ipax-0.3.0/tests/unit/test_sparse_operator_symmetry.py +64 -0
  43. ipax-0.3.0/tests/unit/test_step_failure_salvage.py +82 -0
  44. ipax-0.2.0/tests/property/test_derivative_checks.py +0 -94
  45. {ipax-0.2.0 → ipax-0.3.0}/.gitignore +0 -0
  46. {ipax-0.2.0 → ipax-0.3.0}/CLAUDE.md +0 -0
  47. {ipax-0.2.0 → ipax-0.3.0}/CONTRIBUTING.md +0 -0
  48. {ipax-0.2.0 → ipax-0.3.0}/LICENSE +0 -0
  49. {ipax-0.2.0 → ipax-0.3.0}/NOTICE +0 -0
  50. {ipax-0.2.0 → ipax-0.3.0}/README.md +0 -0
  51. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/README.md +0 -0
  52. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/asv.conf.json +0 -0
  53. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/baselines/__init__.py +0 -0
  54. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/corpus/external.py +0 -0
  55. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/generators/__init__.py +0 -0
  56. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/harness/plots.py +0 -0
  57. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/reports/.gitkeep +0 -0
  58. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/runners/bench_solve.py +0 -0
  59. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/runners/crosscheck.py +0 -0
  60. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/runners/device_efficiency.py +0 -0
  61. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/runners/micro/bench_kernels.py +0 -0
  62. {ipax-0.2.0 → ipax-0.3.0}/benchmarks/runners/scaling.py +0 -0
  63. {ipax-0.2.0 → ipax-0.3.0}/conftest.py +0 -0
  64. {ipax-0.2.0 → ipax-0.3.0}/docs/architecture.md +0 -0
  65. {ipax-0.2.0 → ipax-0.3.0}/docs/concepts/algorithm.md +0 -0
  66. {ipax-0.2.0 → ipax-0.3.0}/docs/concepts/problem.md +0 -0
  67. {ipax-0.2.0 → ipax-0.3.0}/docs/contributing.md +0 -0
  68. {ipax-0.2.0 → ipax-0.3.0}/docs/getting-started.md +0 -0
  69. {ipax-0.2.0 → ipax-0.3.0}/docs/guide/backends.md +0 -0
  70. {ipax-0.2.0 → ipax-0.3.0}/docs/guide/diagnostics.md +0 -0
  71. {ipax-0.2.0 → ipax-0.3.0}/docs/guide/options.md +0 -0
  72. {ipax-0.2.0 → ipax-0.3.0}/docs/guide/problems.md +0 -0
  73. {ipax-0.2.0 → ipax-0.3.0}/docs/guide/results.md +0 -0
  74. {ipax-0.2.0 → ipax-0.3.0}/docs/index.md +0 -0
  75. {ipax-0.2.0 → ipax-0.3.0}/docs/reference.md +0 -0
  76. {ipax-0.2.0 → ipax-0.3.0}/examples/README.md +0 -0
  77. {ipax-0.2.0 → ipax-0.3.0}/examples/autodiff.py +0 -0
  78. {ipax-0.2.0 → ipax-0.3.0}/examples/bound_and_inequality.py +0 -0
  79. {ipax-0.2.0 → ipax-0.3.0}/examples/equality_constrained.py +0 -0
  80. {ipax-0.2.0 → ipax-0.3.0}/examples/lbfgs_finite_diff.py +0 -0
  81. {ipax-0.2.0 → ipax-0.3.0}/examples/matrix_free_krylov.py +0 -0
  82. {ipax-0.2.0 → ipax-0.3.0}/examples/nonconvex_hs.py +0 -0
  83. {ipax-0.2.0 → ipax-0.3.0}/examples/radiotherapy_sparse.py +0 -0
  84. {ipax-0.2.0 → ipax-0.3.0}/examples/unconstrained_quadratic.py +0 -0
  85. {ipax-0.2.0 → ipax-0.3.0}/ipax/backend/__init__.py +0 -0
  86. {ipax-0.2.0 → ipax-0.3.0}/ipax/backend/namespace.py +0 -0
  87. {ipax-0.2.0 → ipax-0.3.0}/ipax/backend/sparse/__init__.py +0 -0
  88. {ipax-0.2.0 → ipax-0.3.0}/ipax/backend/sparse/jax.py +0 -0
  89. {ipax-0.2.0 → ipax-0.3.0}/ipax/backend/sparse/torch.py +0 -0
  90. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/__init__.py +0 -0
  91. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/barrier.py +0 -0
  92. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/breedveld_ls.py +0 -0
  93. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/corrections.py +0 -0
  94. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/hessian.py +0 -0
  95. {ipax-0.2.0 → ipax-0.3.0}/ipax/ipm/step.py +0 -0
  96. {ipax-0.2.0 → ipax-0.3.0}/ipax/linalg/__init__.py +0 -0
  97. {ipax-0.2.0 → ipax-0.3.0}/ipax/linalg/dense.py +0 -0
  98. {ipax-0.2.0 → ipax-0.3.0}/ipax/linalg/krylov.py +0 -0
  99. {ipax-0.2.0 → ipax-0.3.0}/ipax/linalg/regularize.py +0 -0
  100. {ipax-0.2.0 → ipax-0.3.0}/ipax/linalg/solver.py +0 -0
  101. {ipax-0.2.0 → ipax-0.3.0}/ipax/problem/__init__.py +0 -0
  102. {ipax-0.2.0 → ipax-0.3.0}/ipax/problem/autodiff/__init__.py +0 -0
  103. {ipax-0.2.0 → ipax-0.3.0}/ipax/problem/autodiff/jax.py +0 -0
  104. {ipax-0.2.0 → ipax-0.3.0}/ipax/problem/autodiff/torch.py +0 -0
  105. {ipax-0.2.0 → ipax-0.3.0}/ipax/problem/base.py +0 -0
  106. {ipax-0.2.0 → ipax-0.3.0}/ipax/problem/derivatives.py +0 -0
  107. {ipax-0.2.0 → ipax-0.3.0}/ipax/problem/finitediff.py +0 -0
  108. {ipax-0.2.0 → ipax-0.3.0}/ipax/problem/function.py +0 -0
  109. {ipax-0.2.0 → ipax-0.3.0}/ipax/problem/scaling.py +0 -0
  110. {ipax-0.2.0 → ipax-0.3.0}/ipax/py.typed +0 -0
  111. {ipax-0.2.0 → ipax-0.3.0}/ipax/result.py +0 -0
  112. {ipax-0.2.0 → ipax-0.3.0}/ipax/testing/__init__.py +0 -0
  113. {ipax-0.2.0 → ipax-0.3.0}/ipax/testing/backends.py +0 -0
  114. {ipax-0.2.0 → ipax-0.3.0}/ipax/typing.py +0 -0
  115. {ipax-0.2.0 → ipax-0.3.0}/pyproject.toml +0 -0
  116. {ipax-0.2.0 → ipax-0.3.0}/tests/__init__.py +0 -0
  117. {ipax-0.2.0 → ipax-0.3.0}/tests/_helpers.py +0 -0
  118. {ipax-0.2.0 → ipax-0.3.0}/tests/backends/test_cross_backend_conformance.py +0 -0
  119. {ipax-0.2.0 → ipax-0.3.0}/tests/conftest.py +0 -0
  120. {ipax-0.2.0 → ipax-0.3.0}/tests/contracts/__init__.py +0 -0
  121. {ipax-0.2.0 → ipax-0.3.0}/tests/contracts/test_builtin_problem_contracts.py +0 -0
  122. {ipax-0.2.0 → ipax-0.3.0}/tests/contracts/test_builtin_solver_contracts.py +0 -0
  123. {ipax-0.2.0 → ipax-0.3.0}/tests/contracts/test_builtin_sparse_contracts.py +0 -0
  124. {ipax-0.2.0 → ipax-0.3.0}/tests/contracts/test_operator_contract.py +0 -0
  125. {ipax-0.2.0 → ipax-0.3.0}/tests/contracts/test_problem_contract.py +0 -0
  126. {ipax-0.2.0 → ipax-0.3.0}/tests/contracts/test_solver_contract.py +0 -0
  127. {ipax-0.2.0 → ipax-0.3.0}/tests/contracts/test_sparse_contract.py +0 -0
  128. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_benchmark_crosscheck.py +0 -0
  129. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_benchmark_phase4.py +0 -0
  130. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_benchmark_scaling.py +0 -0
  131. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_corrections_solve.py +0 -0
  132. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_dense_nonconvex.py +0 -0
  133. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_device_efficiency.py +0 -0
  134. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_krylov_solve.py +0 -0
  135. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_scaling_solve.py +0 -0
  136. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_sparse_solve.py +0 -0
  137. {ipax-0.2.0 → ipax-0.3.0}/tests/integration/test_warm_start_solve.py +0 -0
  138. {ipax-0.2.0 → ipax-0.3.0}/tests/regression/__init__.py +0 -0
  139. {ipax-0.2.0 → ipax-0.3.0}/tests/regression/test_doctest_collection_adapters.py +0 -0
  140. {ipax-0.2.0 → ipax-0.3.0}/tests/regression/test_fraction_to_boundary_no_elementwise_sync.py +0 -0
  141. {ipax-0.2.0 → ipax-0.3.0}/tests/test_purity_gate.py +0 -0
  142. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_autodiff.py +0 -0
  143. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_backend_namespace.py +0 -0
  144. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_barrier.py +0 -0
  145. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_breedveld.py +0 -0
  146. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_corrections.py +0 -0
  147. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_cupy_sparse_adapter.py +0 -0
  148. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_dense_solver.py +0 -0
  149. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_hessian_lbfgs.py +0 -0
  150. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_inertia_correction.py +0 -0
  151. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_kkt.py +0 -0
  152. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_krylov.py +0 -0
  153. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_numpy_scipy_feral.py +0 -0
  154. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_operator_diagonal.py +0 -0
  155. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_options.py +0 -0
  156. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_problem_derivatives.py +0 -0
  157. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_regularize.py +0 -0
  158. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_result.py +0 -0
  159. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_scaling.py +0 -0
  160. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_solver_selection.py +0 -0
  161. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_sparse_device_routing.py +0 -0
  162. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_sparse_dispatch.py +0 -0
  163. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_termination.py +0 -0
  164. {ipax-0.2.0 → ipax-0.3.0}/tests/unit/test_warm_start.py +0 -0
@@ -177,10 +177,14 @@ Claude Code config lives in `.claude/` and is checked in:
177
177
  - **`/verify`** (`.claude/commands/verify.md`) — run `scripts/check.py` and summarize.
178
178
  - **`/tdd`** (`.claude/commands/tdd.md`) — drive a change through the mandated
179
179
  red→green→verify→regression loop, multi-backend and invariant-aware.
180
+ - **`/release`** (`.claude/commands/release.md`) — drive the release workflow
181
+ (`rc/vX.Y.Z` branch → PR onto `main` → review loop → version/changelog bump → tag →
182
+ sync `develop`), stopping at each human/CI gate. Follows the **Releasing** section.
180
183
 
181
184
  The auditor's rubric is the static half of GPU performance work; the measured half is
182
- a future `benchmarks/runners/device_efficiency.py` (sync/kernel profiling on real
183
- hardware), deferred until GPU CI exists.
185
+ `benchmarks/runners/device_efficiency.py` (host-sync counting + per-iteration timing
186
+ on real hardware, kernel-launch counts best-effort via `nsys`). It is GPU-gated, so it
187
+ is a no-op in CI until GPU CI exists; run it locally on a CUDA backend.
184
188
 
185
189
  ---
186
190
 
@@ -250,6 +254,40 @@ core must remain backend-agnostic.
250
254
 
251
255
  ---
252
256
 
257
+ ## Releasing
258
+
259
+ Releases follow a release-candidate PR onto `main`, then a tag. Conventions to know
260
+ before starting:
261
+
262
+ - **Version is single-sourced** in `ipax/__init__.py` (`__version__`); `pyproject.toml`
263
+ derives it via `[tool.hatch.version]`. Bump in **exactly that one place**.
264
+ - **`CHANGELOG.md` follows Keep a Changelog** and is enforced by the `kacl-verify`
265
+ pre-commit/CI hook. Keep entries under `## [Unreleased]` as work lands.
266
+ - **Tags are `vX.Y.Z`**; pushing a tag triggers `release.yml` (PyPI Trusted Publishing
267
+ + a GitHub release whose notes come from the tag annotation and the changelog).
268
+
269
+ Steps:
270
+
271
+ 1. **Branch.** From an up-to-date `develop`, create `rc/vX.Y.Z`.
272
+ 2. **Open the PR.** Push the branch and open a PR with **base `main`, head `rc/vX.Y.Z`**.
273
+ **Do not bump the version yet** — review may change the contents.
274
+ 3. **Wait for CI + code review** (CI gates + Copilot/human review).
275
+ 4. **Address review.** Push fixes to `rc/vX.Y.Z`; repeat 3–4 until the PR is approved.
276
+ Avoid `git add -A` (it sweeps unrelated working-tree edits into the commit) — stage
277
+ the files you changed.
278
+ 5. **Release bump (only once approved).** Confirm `CHANGELOG.md` covers the release:
279
+ add `## [X.Y.Z] - YYYY-MM-DD` (move the `[Unreleased]` items into it) and update the
280
+ compare links at the bottom. Bump `ipax.__version__`. Run `python scripts/check.py`
281
+ and `pre-commit run kacl-verify --files CHANGELOG.md`. Push; let CI go green.
282
+ 6. **Merge** the PR into `main`.
283
+ 7. **Tag and sync.** Sync `main` (`git switch main && git pull --ff-only`), create the
284
+ annotated tag (`git tag -a vX.Y.Z main` with a short changelog-derived summary),
285
+ merge `main` back into `develop` (`--no-ff`), then push **both** `develop` and the
286
+ tag (`git push origin develop && git push origin vX.Y.Z`). The tag push runs
287
+ `release.yml` — confirm that workflow succeeds.
288
+
289
+ ---
290
+
253
291
  ## References
254
292
 
255
293
  - Wächter, A. & Biegler, L. T. (2006). "On the implementation of an interior-point
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipax
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Array-API-conformant primal–dual interior-point solver for large-scale nonlinear constrained optimization.
5
5
  Project-URL: Homepage, https://github.com/wahln/ipax
6
6
  Project-URL: Documentation, https://ipax.readthedocs.io/
@@ -21,8 +21,12 @@ from ipax.testing.problems import (
21
21
  HS6,
22
22
  HS7,
23
23
  HS8,
24
+ HS9,
25
+ HS21,
26
+ HS28,
24
27
  HS35,
25
28
  HS43,
29
+ HS71,
26
30
  BoundConstrainedQP,
27
31
  EqualityConstrainedQP,
28
32
  UnconstrainedQuadratic,
@@ -61,6 +65,7 @@ class BenchmarkProblem:
61
65
  default=lambda _problem: None, repr=False
62
66
  )
63
67
  backends: tuple[str, ...] | None = None
68
+ exclude_configs: tuple[str, ...] = () # config labels the QC sweep skips here
64
69
 
65
70
 
66
71
  def _known(problem: Problem) -> Array | None:
@@ -149,6 +154,40 @@ def default_corpus() -> list[BenchmarkProblem]:
149
154
  tags=("eq", "nonlinear"),
150
155
  build=lambda xp: (HS8(xp), _arr(xp, [2.0, 1.0])),
151
156
  ),
157
+ BenchmarkProblem(
158
+ name="hs9",
159
+ kind="NLP",
160
+ tags=("eq", "nonlinear"),
161
+ build=lambda xp: (HS9(xp), _arr(xp, [0.0, 0.0])),
162
+ ),
163
+ BenchmarkProblem(
164
+ name="hs21",
165
+ kind="QP",
166
+ tags=("bounds", "ineq"),
167
+ build=lambda xp: (HS21(xp), _arr(xp, [3.0, 1.0])),
168
+ optimum=_known,
169
+ ),
170
+ BenchmarkProblem(
171
+ name="hs28",
172
+ kind="QP",
173
+ tags=("eq",),
174
+ build=lambda xp: (HS28(xp), _arr(xp, [-1.0, 0.5, 0.5])),
175
+ optimum=_known,
176
+ ),
177
+ BenchmarkProblem(
178
+ name="hs71",
179
+ kind="NLP",
180
+ tags=("eq", "ineq", "bounds", "nonlinear"),
181
+ build=lambda xp: (HS71(xp), _arr(xp, [1.0, 5.0, 5.0, 1.0])),
182
+ optimum=_known,
183
+ # The Mehrotra/Gondzio correctors sit at HS71's convergence edge on this
184
+ # nonconvex problem and stall on some backends/platforms (e.g. CI's
185
+ # Torch build) while converging on others — a known corrector-robustness
186
+ # gap, not a per-PR regression. Exclude those configs here so the gate is
187
+ # deterministic; HS71 is still swept on every stable route, and covered
188
+ # under the default solve by the integration tests.
189
+ exclude_configs=("exact/dense+mehrotra", "exact/dense+gondzio"),
190
+ ),
152
191
  _rt_case(),
153
192
  ]
154
193
 
@@ -0,0 +1,498 @@
1
+ """S2MPJ problem loader (download-gated, NumPy-evaluated, backend-bridged).
2
+
3
+ `S2MPJ <https://github.com/GrattonToint/S2MPJ>`_ translates the CUTEst SIF test
4
+ collection (including the full Hock-Schittkowski set) into **pure Python** problem
5
+ files — no Fortran, no SIF decoder, no compilation, cross-platform. Each problem
6
+ exposes value/gradient/Jacobian/Hessian through a ``CUTEst_problem`` base class
7
+ (``fgx``, ``cJx``, ``LHxyv`` …) using NumPy + SciPy.
8
+
9
+ Because that evaluation is hardcoded to NumPy/SciPy, the problems cannot run
10
+ *natively* under an arbitrary Array-API backend. :class:`_S2MPJProblem` instead
11
+ **bridges**: it evaluates in NumPy and converts inputs/outputs to/from the target
12
+ namespace, so ipax's solver runs its linear algebra on any (CPU) backend while the
13
+ model is evaluated on the host. This is for accuracy / cross-backend consistency
14
+ benchmarking; it forces a host sync per evaluation, so it is *not* for GPU
15
+ performance work (use the synthetic RT generator / TROTS surrogate there).
16
+
17
+ S2MPJ has no license file, so its files are **not vendored**. Point
18
+ ``IPAX_S2MPJ_DIR`` (or the ``directory`` argument) at a local checkout; the loader
19
+ returns ``[]`` when it is absent, mirroring the other external corpora.
20
+
21
+ S2MPJ stores each constraint as a two-sided range ``clower ≤ c(x) ≤ cupper`` with
22
+ an equality when the two coincide. The adapter maps that onto ipax's eq/ineq
23
+ split, lowering finite inequality sides to one-sided rows (``c − cupper ≤ 0`` and
24
+ ``clower − c ≤ 0``) exactly as the native ``linear_ineq`` lowering does.
25
+
26
+ S2MPJ also exposes the **exact Lagrangian Hessian** (``LgHxy``/``LHxyv``, convention
27
+ ``L = f + yᵀc``), so the adapter can drive ipax's exact-Hessian route — not only the
28
+ default L-BFGS. :class:`_S2MPJExactProblem` implements ``lagrangian_hessian`` by
29
+ mapping ipax's ``(σ, y_eq, y_ineq)`` onto S2MPJ's single multiplier vector ``Y``
30
+ (equality rows ``+y_eq``; lowered lower-side rows ``−y_ineq`` because their curvature
31
+ is ``−∇²c``; lowered upper-side rows ``+y_ineq``) and honoring ``σ`` on the objective
32
+ term (so it stays correct under gradient-based scaling, where ``σ ≠ 1``). With
33
+ ``sparse=True`` the Jacobians and the Hessian are returned as
34
+ :class:`~ipax.backend.sparse.numpy_scipy.SparseOperator` (true COO sparsity, for the
35
+ sparse-direct route) rather than densified arrays.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import functools
41
+ import importlib
42
+ import os
43
+ import re
44
+ import sys
45
+ from typing import TYPE_CHECKING, Any
46
+
47
+ from ipax.problem.base import Problem
48
+
49
+ if TYPE_CHECKING:
50
+ from collections.abc import Sequence
51
+
52
+ from benchmarks.corpus import BenchmarkProblem
53
+ from ipax.backend.operators import LinearOperator
54
+ from ipax.typing import Array, Namespace, Scalar
55
+
56
+
57
+ def _to_numpy(x: Array) -> Any:
58
+ """Convert an Array-API array to a 1-D NumPy array (host bridge)."""
59
+ import numpy as np
60
+
61
+ if isinstance(x, np.ndarray):
62
+ return np.reshape(x, (-1,))
63
+ try:
64
+ return np.reshape(np.from_dlpack(x), (-1,))
65
+ except (TypeError, RuntimeError, BufferError, ValueError):
66
+ return np.reshape(np.asarray(x), (-1,))
67
+
68
+
69
+ def _from_numpy(xp: Namespace, arr: Any) -> Array:
70
+ """Convert a NumPy array back into the target namespace as float64 (1-/2-D)."""
71
+ import numpy as np
72
+
73
+ return xp.asarray(np.asarray(arr, dtype=np.float64))
74
+
75
+
76
+ class _S2MPJProblem(Problem):
77
+ """Bridge a NumPy-evaluated S2MPJ problem onto an Array-API namespace.
78
+
79
+ The wrapped ``instance`` is a CUTEst_problem from S2MPJ; ``xp`` is the target
80
+ namespace. Equality vs inequality constraints and the finite inequality sides
81
+ are precomputed once from ``clower``/``cupper``.
82
+
83
+ ``sparse`` controls how Jacobians (and, in :class:`_S2MPJExactProblem`, the
84
+ Hessian) cross the bridge: ``False`` densifies to Array-API arrays; ``True``
85
+ wraps the native ``scipy.sparse`` structure in a ``SparseOperator`` so the
86
+ sparse-direct route factors true sparsity. This base class supplies no
87
+ Lagrangian Hessian, so the solver uses its default L-BFGS.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ instance: Any,
93
+ xp: Namespace,
94
+ *,
95
+ sparse: bool = False,
96
+ feasibility: bool = False,
97
+ ) -> None:
98
+ import numpy as np
99
+
100
+ self._sparse = sparse
101
+ # Dataset-sourced expected outcome (set by the loader from the source file):
102
+ # the documented ``LO SOLTN`` objective, the ``infeasible`` marker, and the
103
+ # CUTEst classification. Defaults here so a directly-constructed instance is
104
+ # still valid; the loader overwrites them.
105
+ self.expected_objective: float | None = None
106
+ self.expected_infeasible: bool = False
107
+ self.pbclass: str | None = None
108
+
109
+ # A problem with no objective group is a *feasibility* / nonlinear-equations
110
+ # system, not a minimization problem (S2MPJ's ``fx`` would error). By default
111
+ # we reject it; with ``feasibility=True`` we run it as ``min 0`` s.t. the
112
+ # constraints, turning the IPM into a feasibility finder. ``H`` is the
113
+ # optional explicit quadratic objective.
114
+ self._no_objective = len(getattr(instance, "objgrps", ())) == 0 and not hasattr(
115
+ instance, "H"
116
+ )
117
+ if self._no_objective and not feasibility:
118
+ raise NotImplementedError("S2MPJ problem has no objective function")
119
+
120
+ self._inst = instance
121
+ self.xp = xp
122
+ self._n = int(instance.n)
123
+ m = int(getattr(instance, "m", 0))
124
+ self._m = m
125
+
126
+ # ``clower``/``cupper`` exist only when the problem has constraints.
127
+ clower_attr = getattr(instance, "clower", None)
128
+ cupper_attr = getattr(instance, "cupper", None)
129
+ if m > 0 and clower_attr is not None and cupper_attr is not None:
130
+ clower = np.reshape(np.asarray(clower_attr, dtype=float), (-1,))
131
+ cupper = np.reshape(np.asarray(cupper_attr, dtype=float), (-1,))
132
+ finite_l = np.isfinite(clower)
133
+ finite_u = np.isfinite(cupper)
134
+ eq_mask = finite_l & finite_u & (clower == cupper)
135
+ self._eq_idx = np.where(eq_mask)[0]
136
+ self._eq_rhs = clower[self._eq_idx]
137
+ self._lo_idx = np.where(finite_l & ~eq_mask)[0] # clower ≤ c ⇒ clower−c ≤ 0
138
+ self._up_idx = np.where(finite_u & ~eq_mask)[0] # c ≤ cupper ⇒ c−cupper ≤ 0
139
+ self._lo_rhs = clower[self._lo_idx]
140
+ self._up_rhs = cupper[self._up_idx]
141
+ else:
142
+ empty_i = np.zeros((0,), dtype=int)
143
+ empty_f = np.zeros((0,), dtype=float)
144
+ self._eq_idx = self._lo_idx = self._up_idx = empty_i
145
+ self._eq_rhs = self._lo_rhs = self._up_rhs = empty_f
146
+
147
+ lower_attr = getattr(instance, "xlower", None)
148
+ upper_attr = getattr(instance, "xupper", None)
149
+ self._lower = (
150
+ None
151
+ if lower_attr is None
152
+ else _from_numpy(xp, np.reshape(np.asarray(lower_attr, dtype=float), (-1,)))
153
+ )
154
+ self._upper = (
155
+ None
156
+ if upper_attr is None
157
+ else _from_numpy(xp, np.reshape(np.asarray(upper_attr, dtype=float), (-1,)))
158
+ )
159
+
160
+ @property
161
+ def n_vars(self) -> int:
162
+ return self._n
163
+
164
+ def bounds(self) -> tuple[Array | None, Array | None]:
165
+ return self._lower, self._upper
166
+
167
+ def objective(self, x: Array) -> Scalar:
168
+ import numpy as np
169
+
170
+ if self._no_objective: # feasibility problem: minimize a constant 0
171
+ return _from_numpy(self.xp, np.asarray(0.0))
172
+ # S2MPJ's auto-generated evaluations use Python ``float**`` and can raise
173
+ # OverflowError on the wild trial points a line search probes (e.g.
174
+ # LUKVLE4C's ``100*GVAR**6``). Return +inf so the solver simply rejects
175
+ # the trial rather than crashing. Also return an xp float64 0-d (not a
176
+ # Python float) so the value keeps the backend dtype — a Python float
177
+ # re-cast via ``xp.asarray`` would silently drop to float32 on Torch.
178
+ try:
179
+ val = np.asarray(self._inst.fx(_to_numpy(x)))
180
+ except (OverflowError, FloatingPointError):
181
+ val = np.asarray(np.inf)
182
+ return _from_numpy(self.xp, val)
183
+
184
+ def gradient(self, x: Array) -> Array:
185
+ import numpy as np
186
+
187
+ if self._no_objective: # constant objective ⇒ zero gradient
188
+ return _from_numpy(self.xp, np.zeros((self._n,)))
189
+ try:
190
+ _f, g = self._inst.fgx(_to_numpy(x))
191
+ g = np.reshape(g, (-1,))
192
+ except (OverflowError, FloatingPointError):
193
+ g = np.full((self._n,), np.inf)
194
+ return _from_numpy(self.xp, g)
195
+
196
+ # -- equalities (present only when S2MPJ has clower == cupper rows) ----
197
+ def eq_constraints(self, x: Array) -> Array:
198
+ if self._eq_idx.shape[0] == 0:
199
+ raise NotImplementedError
200
+ import numpy as np
201
+
202
+ c = np.reshape(np.asarray(self._inst.cx(_to_numpy(x)), dtype=float), (-1,))
203
+ return _from_numpy(self.xp, c[self._eq_idx] - self._eq_rhs)
204
+
205
+ def eq_jacobian(self, x: Array) -> Array | LinearOperator:
206
+ if self._eq_idx.shape[0] == 0:
207
+ raise NotImplementedError
208
+ _c, jac = self._inst.cJx(_to_numpy(x))
209
+ if self._sparse:
210
+ return self._sparse_op(jac.tocsr()[self._eq_idx, :])
211
+ return _from_numpy(self.xp, jac.toarray()[self._eq_idx, :])
212
+
213
+ # -- inequalities (lowered finite sides of the two-sided ranges) -------
214
+ def ineq_constraints(self, x: Array) -> Array:
215
+ if self._lo_idx.shape[0] == 0 and self._up_idx.shape[0] == 0:
216
+ raise NotImplementedError
217
+ import numpy as np
218
+
219
+ c = np.reshape(np.asarray(self._inst.cx(_to_numpy(x)), dtype=float), (-1,))
220
+ lo = self._lo_rhs - c[self._lo_idx]
221
+ up = c[self._up_idx] - self._up_rhs
222
+ return _from_numpy(self.xp, np.concatenate((lo, up)))
223
+
224
+ def ineq_jacobian(self, x: Array) -> Array | LinearOperator:
225
+ if self._lo_idx.shape[0] == 0 and self._up_idx.shape[0] == 0:
226
+ raise NotImplementedError
227
+ import numpy as np
228
+
229
+ _c, jac = self._inst.cJx(_to_numpy(x))
230
+ # Lower side ``clower − c`` contributes ``−∇c``; upper side ``c − cupper``
231
+ # contributes ``+∇c`` (matches the constraint-value lowering above).
232
+ if self._sparse:
233
+ import scipy.sparse as sp
234
+
235
+ jcsr = jac.tocsr()
236
+ rows = sp.vstack((-jcsr[self._lo_idx, :], jcsr[self._up_idx, :]))
237
+ return self._sparse_op(rows)
238
+ dense = jac.toarray()
239
+ rows_d = np.concatenate(
240
+ (-dense[self._lo_idx, :], dense[self._up_idx, :]), axis=0
241
+ )
242
+ return _from_numpy(self.xp, rows_d)
243
+
244
+ # -- shared Hessian helpers (used by the exact subclass) ---------------
245
+ def _sparse_op(self, matrix: Any) -> LinearOperator:
246
+ """Wrap a host ``scipy.sparse`` matrix as a COO-exposing operator."""
247
+ from ipax.backend.sparse.numpy_scipy import SparseOperator
248
+
249
+ return SparseOperator(matrix, self.xp)
250
+
251
+ def _s2mpj_multipliers(self, y_eq: Array, y_ineq: Array) -> Any:
252
+ """Map ipax ``(y_eq, y_ineq)`` onto S2MPJ's per-constraint vector ``Y``.
253
+
254
+ S2MPJ's Lagrangian is ``f + Σ_i Y_i c_i`` over the *original* constraint
255
+ rows, so ``Σ_i Y_i ∇²c_i`` must reproduce ipax's curvature term
256
+ ``Σ y_eq·∇²c + Σ y_ineq·∇²g``. Equality rows take ``+y_eq``; lowered
257
+ lower-side rows take ``−y_ineq`` (their ``g = clower − c`` has
258
+ ``∇²g = −∇²c``); lowered upper-side rows take ``+y_ineq``. A range
259
+ constraint that appears in both blocks accumulates both contributions.
260
+ """
261
+ import numpy as np
262
+
263
+ Y = np.zeros((self._m,), dtype=float)
264
+ if self._m == 0:
265
+ return Y
266
+ yeq = _to_numpy(y_eq)
267
+ yineq = _to_numpy(y_ineq)
268
+ n_lo = int(self._lo_idx.shape[0])
269
+ if self._eq_idx.shape[0]:
270
+ np.add.at(Y, self._eq_idx, yeq)
271
+ if n_lo:
272
+ np.add.at(Y, self._lo_idx, -yineq[:n_lo])
273
+ if self._up_idx.shape[0]:
274
+ np.add.at(Y, self._up_idx, yineq[n_lo:])
275
+ return Y
276
+
277
+ def _lagrangian_hessian_matrix(
278
+ self, x: Array, y_eq: Array, y_ineq: Array, sigma: Scalar
279
+ ) -> Any:
280
+ """Assemble ``σ∇²f + Σ y·∇²c`` as a host ``scipy.sparse`` matrix.
281
+
282
+ ``LgHxy`` returns ``∇²f + Σ Y_i ∇²c_i`` (no objective scaling); the
283
+ ``σ ≠ 1`` correction adds ``(σ−1)∇²f`` via a constraint-free call, so the
284
+ result is correct under gradient-based scaling, where the driver passes
285
+ ``σ = s_f`` through the scaling wrapper.
286
+ """
287
+ import numpy as np
288
+
289
+ s = float(sigma)
290
+ xcol = np.reshape(_to_numpy(x), (-1, 1))
291
+ Y = self._s2mpj_multipliers(y_eq, y_ineq)
292
+ _lval, _grad, hess = self._inst.LgHxy(xcol, np.reshape(Y, (-1, 1)))
293
+ if s != 1.0:
294
+ _l0, _g0, hess_f = self._inst.LgHxy(xcol, np.zeros((self._m, 1)))
295
+ hess = hess + (s - 1.0) * hess_f
296
+ return hess
297
+
298
+
299
+ class _S2MPJExactProblem(_S2MPJProblem):
300
+ """S2MPJ bridge that also supplies the **exact Lagrangian Hessian**.
301
+
302
+ Defining ``lagrangian_hessian`` on the class advertises it through ipax's
303
+ derivative resolution (``_provides`` compares against the base ``Problem``),
304
+ so the solver takes the exact-Hessian route instead of L-BFGS. The matrix is
305
+ returned dense or as a ``SparseOperator`` per the ``sparse`` flag.
306
+ """
307
+
308
+ def lagrangian_hessian(
309
+ self,
310
+ x: Array,
311
+ y_eq: Array,
312
+ y_ineq: Array,
313
+ sigma: Scalar = 1.0,
314
+ ) -> Array | LinearOperator:
315
+ import numpy as np
316
+
317
+ hess = self._lagrangian_hessian_matrix(x, y_eq, y_ineq, sigma)
318
+ if self._sparse:
319
+ import scipy.sparse as sp
320
+
321
+ return self._sparse_op(sp.csr_matrix(hess))
322
+ return _from_numpy(self.xp, np.asarray(hess.toarray()))
323
+
324
+
325
+ def s2mpj_dir(directory: str | None = None) -> str | None:
326
+ """Resolve the S2MPJ checkout directory, or ``None`` if unavailable.
327
+
328
+ Uses ``directory`` if given, else the ``IPAX_S2MPJ_DIR`` environment variable.
329
+ A directory qualifies only if it holds ``s2mpjlib.py`` and ``python_problems``.
330
+ """
331
+ root = directory or os.environ.get("IPAX_S2MPJ_DIR")
332
+ if not root:
333
+ return None
334
+ if not os.path.isfile(os.path.join(root, "s2mpjlib.py")):
335
+ return None
336
+ if not os.path.isdir(os.path.join(root, "python_problems")):
337
+ return None
338
+ return root
339
+
340
+
341
+ def list_s2mpj_problems(directory: str | None = None) -> list[str]:
342
+ """All S2MPJ problem names available in the checkout (sorted).
343
+
344
+ Globs ``python_problems/*.py`` (the actual files present, more reliable than
345
+ the repo's ``list_of_python_problems`` which can include editor backups),
346
+ dropping the ``s2mpjlib`` support module. Returns ``[]`` when no checkout is
347
+ found. Note: not every name is benchmarkable — objective-free problems are
348
+ rejected at build time, and the runner caps problem size.
349
+ """
350
+ import glob
351
+
352
+ root = s2mpj_dir(directory)
353
+ if root is None:
354
+ return []
355
+ pattern = os.path.join(root, "python_problems", "*.py")
356
+ names = sorted(
357
+ os.path.splitext(os.path.basename(path))[0]
358
+ for path in glob.glob(pattern)
359
+ if os.path.basename(path) != "s2mpjlib.py"
360
+ )
361
+ return names
362
+
363
+
364
+ @functools.lru_cache(maxsize=2048)
365
+ def _problem_metadata(root: str, name: str) -> tuple[str | None, float | None, bool]:
366
+ """Parse ``(pbclass, expected_objective, expected_infeasible)`` from the source.
367
+
368
+ These come from the dataset itself, not our own judgement: ``self.pbclass`` is
369
+ the CUTEst classification; ``# LO SOLTN <value>`` is the SIF author's documented
370
+ solution objective (present on ~72% of the corpus); and an explicit
371
+ ``Solution (infeasible)`` / ``Source: an infeasible problem`` comment marks the
372
+ deliberately-infeasible problems (e.g. BURKEHAN). Missing fields → ``None`` /
373
+ ``False``.
374
+ """
375
+ path = os.path.join(root, "python_problems", name + ".py")
376
+ try:
377
+ with open(path, encoding="utf-8", errors="replace") as fh:
378
+ text = fh.read()
379
+ except OSError:
380
+ return None, None, False
381
+
382
+ pbclass = None
383
+ match = re.search(r'self\.pbclass\s*=\s*"([^"]+)"', text)
384
+ if match:
385
+ pbclass = match.group(1)
386
+
387
+ expected_objective: float | None = None
388
+ match = re.search(r"(?im)^\s*#\s*LO\s+SOLTN\s+([-+0-9.eEdD]+)", text)
389
+ if match:
390
+ try: # SIF may use Fortran 'D' exponents
391
+ expected_objective = float(
392
+ match.group(1).replace("D", "E").replace("d", "e")
393
+ )
394
+ except ValueError:
395
+ expected_objective = None
396
+
397
+ infeasible = bool(
398
+ re.search(r"(?i)solution\s*\(\s*infeasible\s*\)", text)
399
+ or re.search(r"(?i)source:\s*an?\s+infeasible", text)
400
+ )
401
+ return pbclass, expected_objective, infeasible
402
+
403
+
404
+ def _ensure_on_path(root: str) -> None:
405
+ problems = os.path.join(root, "python_problems")
406
+ for entry in (root, problems):
407
+ if entry not in sys.path:
408
+ sys.path.insert(0, entry)
409
+
410
+
411
+ @functools.lru_cache(maxsize=8)
412
+ def _instantiate(root: str, name: str, size: int | None = None) -> Any:
413
+ """Instantiate an S2MPJ problem, optionally at a target variable count.
414
+
415
+ Cached (small LRU) because the runner builds the same problem once per config
416
+ (gate + every linear-solver route), and S2MPJ's pure-Python construction is the
417
+ sweep's bottleneck — sharing the read-only instance across the per-problem
418
+ config fan-out turns ~5 builds into one. ``size`` selects a scalable problem's
419
+ dimension (``PROBLEM(N)``); a problem that is not size-parametrized (or rejects
420
+ ``N``) falls back to its SIF default, so callers may request a size uniformly.
421
+ """
422
+ _ensure_on_path(root)
423
+ cls = getattr(importlib.import_module(name), name)
424
+ if size is not None:
425
+ try:
426
+ return cls(size)
427
+ except Exception: # not scalable / invalid N → SIF default size
428
+ return cls()
429
+ return cls()
430
+
431
+
432
+ # A small, curated default selection: constrained Hock-Schittkowski problems that
433
+ # the L-BFGS path should solve. The loader accepts any S2MPJ problem name.
434
+ _DEFAULT_NAMES = ("HS21", "HS35", "HS71", "HS6", "HS7", "HS8", "HS28")
435
+
436
+
437
+ def s2mpj_problems(
438
+ names: Sequence[str] | None = None,
439
+ *,
440
+ directory: str | None = None,
441
+ backends: tuple[str, ...] = ("numpy",),
442
+ hessian: str = "lbfgs",
443
+ sparse: bool = False,
444
+ size: int | None = None,
445
+ feasibility: bool = False,
446
+ ) -> list[BenchmarkProblem]:
447
+ """Return :class:`BenchmarkProblem`s for the named S2MPJ problems.
448
+
449
+ Returns ``[]`` when no S2MPJ checkout is available (so callers may extend the
450
+ corpus unconditionally). ``backends`` restricts each case to host-bridgeable
451
+ namespaces (CPU NumPy/Torch); the default is NumPy only. ``hessian="exact"``
452
+ builds problems that supply the analytic Lagrangian Hessian (else default
453
+ L-BFGS); ``sparse=True`` returns Jacobians/Hessian as ``SparseOperator`` for
454
+ the sparse-direct route. ``size`` requests a target variable count for the
455
+ scalable problems (others keep their SIF default) — the lever for a
456
+ scaling-focused sweep that reaches the sparse route's intended regime.
457
+ ``feasibility=True`` admits the objective-free problems (CUTEst feasibility /
458
+ nonlinear-equation systems), running them as ``min 0`` subject to the
459
+ constraints instead of rejecting them. Each built problem carries the
460
+ dataset's expected outcome (``expected_objective``/``expected_infeasible``/
461
+ ``pbclass``) for authoritative scoring.
462
+ """
463
+ from benchmarks.corpus import BenchmarkProblem
464
+
465
+ root = s2mpj_dir(directory)
466
+ if root is None:
467
+ return []
468
+
469
+ selected = tuple(names) if names is not None else _DEFAULT_NAMES
470
+ cls = _S2MPJExactProblem if hessian == "exact" else _S2MPJProblem
471
+
472
+ def _make(name: str) -> BenchmarkProblem:
473
+ def build(xp: Namespace) -> tuple[Problem, Array]:
474
+ import numpy as np
475
+
476
+ instance = _instantiate(root, name, size)
477
+ problem = cls(instance, xp, sparse=sparse, feasibility=feasibility)
478
+ pbclass, expected_objective, expected_infeasible = _problem_metadata(
479
+ root, name
480
+ )
481
+ problem.pbclass = pbclass
482
+ problem.expected_objective = expected_objective
483
+ problem.expected_infeasible = expected_infeasible
484
+ x0 = _from_numpy(xp, np.reshape(instance.x0, (-1,)))
485
+ return problem, x0
486
+
487
+ return BenchmarkProblem(
488
+ name=f"s2mpj/{name}",
489
+ kind="NLP",
490
+ tags=("s2mpj", "cutest"),
491
+ build=build,
492
+ backends=backends,
493
+ )
494
+
495
+ return [_make(name) for name in selected]
496
+
497
+
498
+ __all__ = ["list_s2mpj_problems", "s2mpj_dir", "s2mpj_problems"]