ipax 0.2.0__tar.gz → 0.4.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.
- {ipax-0.2.0 → ipax-0.4.0}/AGENTS.md +40 -2
- {ipax-0.2.0 → ipax-0.4.0}/PKG-INFO +1 -1
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/corpus/__init__.py +39 -0
- ipax-0.4.0/benchmarks/corpus/_s2mpj_fast.py +413 -0
- ipax-0.4.0/benchmarks/corpus/s2mpj.py +569 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/harness/__init__.py +82 -17
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/runners/qc.py +2 -0
- ipax-0.4.0/benchmarks/runners/s2mpj.py +608 -0
- ipax-0.4.0/docs/benchmarks/s2mpj.md +199 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/concepts/linalg.md +29 -0
- {ipax-0.2.0 → ipax-0.4.0}/examples/radiotherapy_sparse.py +7 -4
- ipax-0.4.0/examples/sparse_operator_problem.py +82 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/__init__.py +9 -1
- {ipax-0.2.0 → ipax-0.4.0}/ipax/_logging.py +37 -10
- ipax-0.4.0/ipax/backend/operators.py +970 -0
- ipax-0.4.0/ipax/backend/sparse/_canonical.py +207 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/backend/sparse/_routing.py +27 -9
- {ipax-0.2.0 → ipax-0.4.0}/ipax/backend/sparse/cupy.py +308 -66
- {ipax-0.2.0 → ipax-0.4.0}/ipax/backend/sparse/numpy_scipy.py +203 -24
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/driver.py +236 -89
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/filter_ls.py +59 -7
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/hessian.py +57 -8
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/init.py +41 -1
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/kkt.py +401 -24
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/restoration.py +23 -1
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/termination.py +11 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/linalg/dense.py +49 -8
- {ipax-0.2.0 → ipax-0.4.0}/ipax/linalg/krylov.py +199 -14
- {ipax-0.2.0 → ipax-0.4.0}/ipax/linalg/regularize.py +22 -1
- {ipax-0.2.0 → ipax-0.4.0}/ipax/linalg/solver.py +10 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/linalg/sparse.py +70 -8
- {ipax-0.2.0 → ipax-0.4.0}/ipax/options.py +90 -21
- {ipax-0.2.0 → ipax-0.4.0}/ipax/problem/derivatives.py +20 -8
- ipax-0.4.0/ipax/problem/linear_ineq.py +196 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/problem/scaling.py +6 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/solve.py +11 -4
- {ipax-0.2.0 → ipax-0.4.0}/ipax/testing/problems.py +253 -0
- {ipax-0.2.0 → ipax-0.4.0}/pyproject.toml +1 -1
- {ipax-0.2.0 → ipax-0.4.0}/tests/contracts/test_builtin_operator_contracts.py +81 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/contracts/test_operator_contract.py +11 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_benchmark_harness.py +51 -1
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_callback_and_logging.py +79 -1
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_hock_schittkowski.py +52 -1
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_known_optima.py +263 -199
- ipax-0.4.0/tests/integration/test_linear_inequalities.py +224 -0
- ipax-0.4.0/tests/integration/test_s2mpj_corpus.py +349 -0
- ipax-0.4.0/tests/integration/test_sparse_operator_problem.py +182 -0
- ipax-0.4.0/tests/property/test_derivative_checks.py +189 -0
- ipax-0.4.0/tests/regression/test_delta_c_rank_deficient_constraints.py +83 -0
- ipax-0.4.0/tests/regression/test_hs7_lbfgs_no_false_infeasible.py +80 -0
- ipax-0.4.0/tests/regression/test_hs9_krylov_zero_hessian.py +47 -0
- ipax-0.4.0/tests/regression/test_lbfgs_gradient_overshoot.py +60 -0
- ipax-0.4.0/tests/regression/test_sparse_non_finite_kkt_no_crash.py +59 -0
- ipax-0.4.0/tests/regression/test_unbounded_detection.py +90 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_cupy_sparse_adapter.py +232 -4
- ipax-0.4.0/tests/unit/test_dense_solver.py +327 -0
- ipax-0.4.0/tests/unit/test_filter_ls.py +186 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_hessian_lbfgs.py +67 -0
- ipax-0.4.0/tests/unit/test_init_bounds.py +32 -0
- ipax-0.4.0/tests/unit/test_kkt.py +669 -0
- ipax-0.4.0/tests/unit/test_krylov.py +838 -0
- ipax-0.4.0/tests/unit/test_linear_ineq_lowering.py +105 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_logging.py +52 -0
- ipax-0.4.0/tests/unit/test_numpy_scipy_canonical_reuse.py +131 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_numpy_scipy_feral.py +40 -1
- ipax-0.4.0/tests/unit/test_operator_batched_hooks.py +79 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_operator_coo.py +167 -1
- ipax-0.4.0/tests/unit/test_operator_coo_values.py +115 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_operator_diagonal.py +27 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_options.py +46 -4
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_problem_derivatives.py +37 -0
- ipax-0.4.0/tests/unit/test_public_sparse_operators.py +218 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_regularize.py +19 -1
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_restoration.py +43 -0
- ipax-0.4.0/tests/unit/test_s2mpj_adapter.py +226 -0
- ipax-0.4.0/tests/unit/test_s2mpj_fasteval.py +331 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_scaling.py +14 -0
- ipax-0.4.0/tests/unit/test_sparse_canonical.py +151 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_sparse_device_routing.py +14 -0
- ipax-0.4.0/tests/unit/test_sparse_dispatch.py +78 -0
- ipax-0.4.0/tests/unit/test_sparse_facade_structure_cache.py +112 -0
- ipax-0.4.0/tests/unit/test_sparse_operator_symmetry.py +64 -0
- ipax-0.4.0/tests/unit/test_step_failure_salvage.py +110 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_termination.py +8 -1
- ipax-0.2.0/ipax/backend/operators.py +0 -429
- ipax-0.2.0/tests/property/test_derivative_checks.py +0 -94
- ipax-0.2.0/tests/unit/test_dense_solver.py +0 -63
- ipax-0.2.0/tests/unit/test_filter_ls.py +0 -54
- ipax-0.2.0/tests/unit/test_kkt.py +0 -162
- ipax-0.2.0/tests/unit/test_krylov.py +0 -442
- ipax-0.2.0/tests/unit/test_sparse_dispatch.py +0 -19
- {ipax-0.2.0 → ipax-0.4.0}/.gitignore +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/CLAUDE.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/CONTRIBUTING.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/LICENSE +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/NOTICE +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/README.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/README.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/asv.conf.json +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/baselines/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/corpus/external.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/generators/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/harness/plots.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/reports/.gitkeep +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/runners/bench_solve.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/runners/crosscheck.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/runners/device_efficiency.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/runners/micro/bench_kernels.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/benchmarks/runners/scaling.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/conftest.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/architecture.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/concepts/algorithm.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/concepts/problem.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/contributing.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/getting-started.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/guide/backends.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/guide/diagnostics.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/guide/options.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/guide/problems.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/guide/results.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/index.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/docs/reference.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/examples/README.md +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/examples/autodiff.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/examples/bound_and_inequality.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/examples/equality_constrained.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/examples/lbfgs_finite_diff.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/examples/matrix_free_krylov.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/examples/nonconvex_hs.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/examples/unconstrained_quadratic.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/backend/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/backend/namespace.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/backend/sparse/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/backend/sparse/jax.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/backend/sparse/torch.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/barrier.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/breedveld_ls.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/corrections.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/ipm/step.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/linalg/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/problem/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/problem/autodiff/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/problem/autodiff/jax.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/problem/autodiff/torch.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/problem/base.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/problem/finitediff.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/problem/function.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/py.typed +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/result.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/testing/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/testing/backends.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/ipax/typing.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/_helpers.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/backends/test_cross_backend_conformance.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/conftest.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/contracts/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/contracts/test_builtin_problem_contracts.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/contracts/test_builtin_solver_contracts.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/contracts/test_builtin_sparse_contracts.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/contracts/test_problem_contract.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/contracts/test_solver_contract.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/contracts/test_sparse_contract.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_benchmark_crosscheck.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_benchmark_phase4.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_benchmark_scaling.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_corrections_solve.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_dense_nonconvex.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_device_efficiency.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_krylov_solve.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_scaling_solve.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_sparse_solve.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/integration/test_warm_start_solve.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/regression/__init__.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/regression/test_doctest_collection_adapters.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/regression/test_fraction_to_boundary_no_elementwise_sync.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/test_purity_gate.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_autodiff.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_backend_namespace.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_barrier.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_breedveld.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_corrections.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_inertia_correction.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_result.py +0 -0
- {ipax-0.2.0 → ipax-0.4.0}/tests/unit/test_solver_selection.py +0 -0
- {ipax-0.2.0 → ipax-0.4.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
|
-
|
|
183
|
-
hardware
|
|
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.
|
|
3
|
+
Version: 0.4.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,413 @@
|
|
|
1
|
+
"""Precompiled S2MPJ evaluator (benchmark-side, NumPy/SciPy allowed).
|
|
2
|
+
|
|
3
|
+
S2MPJ's generic ``CUTEst_problem.evalgrsum`` re-interprets the group-partially-
|
|
4
|
+
separable problem structure on **every** evaluation: each element and group
|
|
5
|
+
function is dispatched through ``eval('self.'+name+'(...)')`` (a string compile
|
|
6
|
+
per call), the Jacobian is assembled row-by-row into a ``lil_matrix`` (with each
|
|
7
|
+
group's dense gradient converted to ``lil`` just to write one row), and the
|
|
8
|
+
linear term is sliced out of the sparse ``A`` one group at a time. Profiling the
|
|
9
|
+
S2MPJ sweep shows ~90% of solve wall-time in that interpretive loop — tiny
|
|
10
|
+
problems like ACOPP14 (n=38) cost ~10 ms per constraint evaluation.
|
|
11
|
+
|
|
12
|
+
:class:`FastS2MPJEval` compiles the same structure **once per instance**: element
|
|
13
|
+
and group functions are resolved to callables via ``getattr``, all linear terms
|
|
14
|
+
become one vectorized ``A @ x − gconst`` product, and the Jacobian is built as
|
|
15
|
+
COO triplets on each group's precomputed sparsity support. The element/group
|
|
16
|
+
functions themselves — the actual math S2MPJ generated from the SIF file — are
|
|
17
|
+
untouched, so values match the original to floating-point roundoff (summation
|
|
18
|
+
order differs). Measured: 8–130× per ``cx`` call, 6–37× end-to-end solves.
|
|
19
|
+
|
|
20
|
+
:func:`fast_evaluator` is the safe entry point: it builds the fast evaluator and
|
|
21
|
+
**verifies it against the original methods** at the start point (and a perturbed
|
|
22
|
+
point) before use, returning ``None`` — meaning "use the original methods" — on
|
|
23
|
+
any unsupported feature, construction error, or numerical mismatch. A corpus
|
|
24
|
+
oddity can therefore never silently corrupt benchmark scores. The result is
|
|
25
|
+
cached on the instance, so the per-config problem rebuilds share one
|
|
26
|
+
verification.
|
|
27
|
+
|
|
28
|
+
Scope: values, gradients, and Jacobians (``fx/fgx/cx/cJx``, ``nargout ≤ 2``).
|
|
29
|
+
The Lagrangian-Hessian path (``LgHxy``) keeps the original implementation.
|
|
30
|
+
Problems that declare *partial* derivative levels (``objderlvl``/``conderlvl``
|
|
31
|
+
< 2, where the original substitutes NaNs) are rejected at construction and fall
|
|
32
|
+
back to the original methods.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
import numpy as np
|
|
40
|
+
import scipy.sparse as sp
|
|
41
|
+
|
|
42
|
+
# Element entry: (element function, elemental variable indices, weight or None,
|
|
43
|
+
# element index). Group entry: (elements, group function or None for TRIVIAL).
|
|
44
|
+
_Element = tuple[Any, np.ndarray, float | None, int]
|
|
45
|
+
_Group = tuple[list[_Element], Any]
|
|
46
|
+
|
|
47
|
+
# Agreement gates for verify-at-build: a genuine reimplementation bug is orders
|
|
48
|
+
# of magnitude larger than the summation-order roundoff these absorb.
|
|
49
|
+
_VERIFY_RTOL = 1e-7
|
|
50
|
+
_VERIFY_ATOL = 1e-9
|
|
51
|
+
|
|
52
|
+
_FAST_ATTR = "_ipax_fast_eval"
|
|
53
|
+
_UNSET = object()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FastS2MPJEval:
|
|
57
|
+
"""Precompiled ``fx/fgx/cx/cJx`` for one S2MPJ ``CUTEst_problem`` instance.
|
|
58
|
+
|
|
59
|
+
Raises ``NotImplementedError`` (or any construction error) when the instance
|
|
60
|
+
uses a feature outside the replicated ``evalgrsum`` semantics; callers should
|
|
61
|
+
then use the original methods (see :func:`fast_evaluator`).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, instance: Any) -> None:
|
|
65
|
+
self.inst = instance
|
|
66
|
+
getglobs = getattr(instance, "getglobs", None)
|
|
67
|
+
if getglobs is not None: # set global element/group parameters once
|
|
68
|
+
getglobs()
|
|
69
|
+
n = int(instance.n)
|
|
70
|
+
self.n = n
|
|
71
|
+
|
|
72
|
+
# Partial derivative levels make evalgrsum emit NaN rows; keep those on
|
|
73
|
+
# the original path rather than replicating the NaN bookkeeping.
|
|
74
|
+
if int(getattr(instance, "objderlvl", 2)) < 2:
|
|
75
|
+
raise NotImplementedError("partial objective derivatives")
|
|
76
|
+
conderlvl = getattr(instance, "conderlvl", None)
|
|
77
|
+
if conderlvl is not None and any(int(d) < 2 for d in conderlvl):
|
|
78
|
+
raise NotImplementedError("partial constraint derivatives")
|
|
79
|
+
|
|
80
|
+
self.objgrps = np.asarray(getattr(instance, "objgrps", ()), dtype=int).reshape(
|
|
81
|
+
-1
|
|
82
|
+
)
|
|
83
|
+
self.congrps = np.asarray(getattr(instance, "congrps", ()), dtype=int).reshape(
|
|
84
|
+
-1
|
|
85
|
+
)
|
|
86
|
+
n_groups = int(max([*self.objgrps, *self.congrps, -1])) + 1
|
|
87
|
+
|
|
88
|
+
self.gconst = self._dense_per_group(
|
|
89
|
+
getattr(instance, "gconst", None), n_groups, default=0.0
|
|
90
|
+
)
|
|
91
|
+
gscale = self._dense_per_group(
|
|
92
|
+
getattr(instance, "gscale", None), n_groups, default=1.0
|
|
93
|
+
)
|
|
94
|
+
# evalgrsum treats |scale| ≤ 1e-15 as "no scaling"
|
|
95
|
+
gscale[np.abs(gscale) <= 1.0e-15] = 1.0
|
|
96
|
+
self.gscale = gscale
|
|
97
|
+
|
|
98
|
+
# Linear term: one padded CSR so every group's row is A[ig].
|
|
99
|
+
if hasattr(instance, "A"):
|
|
100
|
+
A = sp.csr_matrix(instance.A, dtype=float)
|
|
101
|
+
if A.shape[1] > n: # evalgrsum requires sA2 ≤ n (gin[:sA2] scatter)
|
|
102
|
+
raise NotImplementedError("linear term wider than n")
|
|
103
|
+
A.resize((max(A.shape[0], n_groups), n))
|
|
104
|
+
else:
|
|
105
|
+
A = sp.csr_matrix((n_groups, n))
|
|
106
|
+
self.A = A
|
|
107
|
+
|
|
108
|
+
self.obj_groups = [self._compile_group(int(ig)) for ig in self.objgrps]
|
|
109
|
+
self.con_groups = [self._compile_group(int(ig)) for ig in self.congrps]
|
|
110
|
+
|
|
111
|
+
# Per-constraint-group Jacobian support: A-row indices ∪ element vars.
|
|
112
|
+
self.con_support: list[np.ndarray] = []
|
|
113
|
+
for k, ig in enumerate(self.congrps):
|
|
114
|
+
cols = set(A.indices[A.indptr[ig] : A.indptr[ig + 1]].tolist())
|
|
115
|
+
for _fn, idx, _w, _iel in self.con_groups[k][0]:
|
|
116
|
+
cols.update(idx.tolist())
|
|
117
|
+
self.con_support.append(
|
|
118
|
+
np.fromiter(sorted(cols), dtype=int, count=len(cols))
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.H = getattr(instance, "H", None)
|
|
122
|
+
self._has_objective = len(self.objgrps) > 0 or self.H is not None
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _dense_per_group(values: Any, n_groups: int, *, default: float) -> np.ndarray:
|
|
126
|
+
"""Densify a lazily-padded per-group S2MPJ array (None entries → default)."""
|
|
127
|
+
out = np.full(n_groups, default)
|
|
128
|
+
if values is None:
|
|
129
|
+
return out
|
|
130
|
+
for ig in range(min(len(values), n_groups)):
|
|
131
|
+
v = values[ig]
|
|
132
|
+
if v is not None:
|
|
133
|
+
out[ig] = float(np.asarray(v).reshape(-1)[0])
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
def _compile_group(self, ig: int) -> _Group:
|
|
137
|
+
"""Resolve one group's element list and group function to callables."""
|
|
138
|
+
inst = self.inst
|
|
139
|
+
elements: list[_Element] = []
|
|
140
|
+
grelt = getattr(inst, "grelt", None)
|
|
141
|
+
if grelt is not None and ig < len(grelt) and grelt[ig] is not None:
|
|
142
|
+
grelw = getattr(inst, "grelw", None)
|
|
143
|
+
weights = (
|
|
144
|
+
grelw[ig]
|
|
145
|
+
if grelw is not None and ig < len(grelw) and grelw[ig] is not None
|
|
146
|
+
else None
|
|
147
|
+
)
|
|
148
|
+
for pos, iel in enumerate(grelt[ig]):
|
|
149
|
+
iel = int(iel)
|
|
150
|
+
fn = getattr(inst, inst.elftype[iel])
|
|
151
|
+
idx = np.asarray([int(iv) for iv in inst.elvar[iel]], dtype=int)
|
|
152
|
+
w = None if weights is None else float(np.asarray(weights[pos]))
|
|
153
|
+
elements.append((fn, idx, w, iel))
|
|
154
|
+
gname = "TRIVIAL"
|
|
155
|
+
grftype = getattr(inst, "grftype", None)
|
|
156
|
+
if grftype is not None and ig < len(grftype) and grftype[ig] is not None:
|
|
157
|
+
gname = grftype[ig]
|
|
158
|
+
gfn = None if gname == "TRIVIAL" else getattr(inst, gname)
|
|
159
|
+
return elements, gfn
|
|
160
|
+
|
|
161
|
+
# -- constraints --------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def cx(self, x: Any) -> np.ndarray:
|
|
164
|
+
"""Constraint values, shape ``(m, 1)`` (mirrors the original ``cx``)."""
|
|
165
|
+
x = np.asarray(x, dtype=float).reshape(-1)
|
|
166
|
+
xc = x.reshape(-1, 1)
|
|
167
|
+
fin = (self.A @ x)[self.congrps] - self.gconst[self.congrps]
|
|
168
|
+
out = np.empty((len(self.congrps), 1))
|
|
169
|
+
for k, (elements, gfn) in enumerate(self.con_groups):
|
|
170
|
+
f = fin[k]
|
|
171
|
+
for fn, idx, w, iel in elements:
|
|
172
|
+
fiel = fn(self.inst, 1, xc[idx], iel)
|
|
173
|
+
f = f + (w * fiel if w is not None else fiel)
|
|
174
|
+
ig = int(self.congrps[k])
|
|
175
|
+
if gfn is not None:
|
|
176
|
+
f = gfn(self.inst, 1, f, ig)
|
|
177
|
+
out[k, 0] = float(np.asarray(f).reshape(-1)[0]) / self.gscale[ig]
|
|
178
|
+
return out
|
|
179
|
+
|
|
180
|
+
def cJx(self, x: Any) -> tuple[np.ndarray, sp.csr_matrix]:
|
|
181
|
+
"""Constraint values and Jacobian ``(c, J)`` with ``J`` in CSR."""
|
|
182
|
+
x = np.asarray(x, dtype=float).reshape(-1)
|
|
183
|
+
xc = x.reshape(-1, 1)
|
|
184
|
+
m = len(self.congrps)
|
|
185
|
+
fin = (self.A @ x)[self.congrps] - self.gconst[self.congrps]
|
|
186
|
+
A = self.A
|
|
187
|
+
cvals = np.empty((m, 1))
|
|
188
|
+
rows: list[np.ndarray] = []
|
|
189
|
+
cols: list[np.ndarray] = []
|
|
190
|
+
vals: list[np.ndarray] = []
|
|
191
|
+
for k, (elements, gfn) in enumerate(self.con_groups):
|
|
192
|
+
ig = int(self.congrps[k])
|
|
193
|
+
support = self.con_support[k]
|
|
194
|
+
gin = np.zeros(len(support))
|
|
195
|
+
a0, a1 = A.indptr[ig], A.indptr[ig + 1]
|
|
196
|
+
if a1 > a0:
|
|
197
|
+
gin[np.searchsorted(support, A.indices[a0:a1])] += A.data[a0:a1]
|
|
198
|
+
f = fin[k]
|
|
199
|
+
for fn, idx, w, iel in elements:
|
|
200
|
+
fiel, giel = fn(self.inst, 2, xc[idx], iel)
|
|
201
|
+
giel = np.asarray(giel, dtype=float).reshape(-1)
|
|
202
|
+
pos = np.searchsorted(support, idx)
|
|
203
|
+
if w is not None:
|
|
204
|
+
f = f + w * fiel
|
|
205
|
+
np.add.at(gin, pos, w * giel)
|
|
206
|
+
else:
|
|
207
|
+
f = f + fiel
|
|
208
|
+
np.add.at(gin, pos, giel)
|
|
209
|
+
if gfn is not None:
|
|
210
|
+
f, grada = gfn(self.inst, 2, f, ig)
|
|
211
|
+
gin = grada * gin
|
|
212
|
+
gsc = self.gscale[ig]
|
|
213
|
+
cvals[k, 0] = float(np.asarray(f).reshape(-1)[0]) / gsc
|
|
214
|
+
if len(support):
|
|
215
|
+
rows.append(np.full(len(support), k))
|
|
216
|
+
cols.append(support)
|
|
217
|
+
vals.append(np.asarray(gin, dtype=float).reshape(-1) / gsc)
|
|
218
|
+
if rows:
|
|
219
|
+
J = sp.csr_matrix(
|
|
220
|
+
(np.concatenate(vals), (np.concatenate(rows), np.concatenate(cols))),
|
|
221
|
+
shape=(m, self.n),
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
J = sp.csr_matrix((m, self.n))
|
|
225
|
+
return cvals, J
|
|
226
|
+
|
|
227
|
+
# -- objective ----------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def fx(self, x: Any) -> float:
|
|
230
|
+
return self._obj(x, want_grad=False) # type: ignore[return-value]
|
|
231
|
+
|
|
232
|
+
def fgx(self, x: Any) -> tuple[float, np.ndarray]:
|
|
233
|
+
return self._obj(x, want_grad=True) # type: ignore[return-value]
|
|
234
|
+
|
|
235
|
+
def _obj(self, x: Any, *, want_grad: bool) -> float | tuple[float, np.ndarray]:
|
|
236
|
+
if not self._has_objective:
|
|
237
|
+
raise ValueError("S2MPJ problem has no objective function")
|
|
238
|
+
x = np.asarray(x, dtype=float).reshape(-1)
|
|
239
|
+
xc = x.reshape(-1, 1)
|
|
240
|
+
n = self.n
|
|
241
|
+
fx = 0.0
|
|
242
|
+
gx = np.zeros(n) if want_grad else None
|
|
243
|
+
if self.H is not None:
|
|
244
|
+
Hx = np.asarray(self.H @ x).reshape(-1)
|
|
245
|
+
fx += 0.5 * float(x @ Hx)
|
|
246
|
+
if want_grad:
|
|
247
|
+
gx += Hx
|
|
248
|
+
fin = (
|
|
249
|
+
(self.A @ x)[self.objgrps] - self.gconst[self.objgrps]
|
|
250
|
+
if len(self.objgrps)
|
|
251
|
+
else np.zeros(0)
|
|
252
|
+
)
|
|
253
|
+
A = self.A
|
|
254
|
+
for k, (elements, gfn) in enumerate(self.obj_groups):
|
|
255
|
+
ig = int(self.objgrps[k])
|
|
256
|
+
f = fin[k]
|
|
257
|
+
if want_grad:
|
|
258
|
+
gin = np.zeros(n)
|
|
259
|
+
a0, a1 = A.indptr[ig], A.indptr[ig + 1]
|
|
260
|
+
if a1 > a0:
|
|
261
|
+
gin[A.indices[a0:a1]] += A.data[a0:a1]
|
|
262
|
+
for fn, idx, w, iel in elements:
|
|
263
|
+
if want_grad:
|
|
264
|
+
fiel, giel = fn(self.inst, 2, xc[idx], iel)
|
|
265
|
+
giel = np.asarray(giel, dtype=float).reshape(-1)
|
|
266
|
+
if w is not None:
|
|
267
|
+
f = f + w * fiel
|
|
268
|
+
np.add.at(gin, idx, w * giel)
|
|
269
|
+
else:
|
|
270
|
+
f = f + fiel
|
|
271
|
+
np.add.at(gin, idx, giel)
|
|
272
|
+
else:
|
|
273
|
+
fiel = fn(self.inst, 1, xc[idx], iel)
|
|
274
|
+
f = f + (w * fiel if w is not None else fiel)
|
|
275
|
+
if gfn is not None:
|
|
276
|
+
if want_grad:
|
|
277
|
+
f, grada = gfn(self.inst, 2, f, ig)
|
|
278
|
+
gin = grada * gin
|
|
279
|
+
else:
|
|
280
|
+
f = gfn(self.inst, 1, f, ig)
|
|
281
|
+
gsc = self.gscale[ig]
|
|
282
|
+
fx += float(np.asarray(f).reshape(-1)[0]) / gsc
|
|
283
|
+
if want_grad:
|
|
284
|
+
gx += gin / gsc
|
|
285
|
+
if want_grad:
|
|
286
|
+
return fx, gx.reshape(-1, 1)
|
|
287
|
+
return fx
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# -- verify-at-build entry point ----------------------------------------------
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def fast_evaluator(instance: Any) -> FastS2MPJEval | None:
|
|
294
|
+
"""The verified fast evaluator for ``instance``, or ``None`` to use the original.
|
|
295
|
+
|
|
296
|
+
Builds :class:`FastS2MPJEval` and compares every replicated method against
|
|
297
|
+
the instance's original one at the start point and a perturbed point. Any
|
|
298
|
+
construction failure, evaluation mismatch, or inability to verify at least
|
|
299
|
+
one method yields ``None``. The outcome is cached on the instance so the
|
|
300
|
+
per-config problem rebuilds (which share the lru-cached instance) verify
|
|
301
|
+
only once.
|
|
302
|
+
"""
|
|
303
|
+
cached = getattr(instance, _FAST_ATTR, _UNSET)
|
|
304
|
+
if cached is not _UNSET:
|
|
305
|
+
return cached # type: ignore[return-value]
|
|
306
|
+
fast = _build_and_verify(instance)
|
|
307
|
+
try:
|
|
308
|
+
setattr(instance, _FAST_ATTR, fast)
|
|
309
|
+
except Exception: # exotic instance forbids attributes — just skip caching
|
|
310
|
+
pass
|
|
311
|
+
return fast
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _build_and_verify(instance: Any) -> FastS2MPJEval | None:
|
|
315
|
+
try:
|
|
316
|
+
fast = FastS2MPJEval(instance)
|
|
317
|
+
x0 = np.asarray(instance.x0, dtype=float).reshape(-1)
|
|
318
|
+
except Exception:
|
|
319
|
+
return None
|
|
320
|
+
# Two deterministic probe points: x0 itself and a relative perturbation
|
|
321
|
+
# (x0 is often special — e.g. all zeros kills every linear term).
|
|
322
|
+
probe = x0 + 1e-4 * (1.0 + np.abs(x0)) * np.cos(np.arange(x0.shape[0]))
|
|
323
|
+
verified = 0
|
|
324
|
+
for x in (x0, probe):
|
|
325
|
+
agree = _agrees_at(instance, fast, x)
|
|
326
|
+
if agree is False:
|
|
327
|
+
return None
|
|
328
|
+
verified += agree is True
|
|
329
|
+
return fast if verified else None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _agrees_at(instance: Any, fast: FastS2MPJEval, x: np.ndarray) -> bool | None:
|
|
333
|
+
"""Tri-state agreement at one point: True/False, or None when uncheckable.
|
|
334
|
+
|
|
335
|
+
A raising *original* method skips that comparison (the original is the
|
|
336
|
+
authority on exceptions, and the solve would fail there anyway); a raising
|
|
337
|
+
or mismatching *fast* method where the original succeeded is a failure.
|
|
338
|
+
"""
|
|
339
|
+
checked = 0
|
|
340
|
+
with np.errstate(all="ignore"):
|
|
341
|
+
if fast._has_objective:
|
|
342
|
+
for ref_fn, fast_fn, is_pair in (
|
|
343
|
+
(getattr(instance, "fx", None), fast.fx, False),
|
|
344
|
+
(getattr(instance, "fgx", None), fast.fgx, True),
|
|
345
|
+
):
|
|
346
|
+
if ref_fn is None:
|
|
347
|
+
continue
|
|
348
|
+
try:
|
|
349
|
+
ref = ref_fn(x.copy())
|
|
350
|
+
except Exception:
|
|
351
|
+
continue
|
|
352
|
+
try:
|
|
353
|
+
new = fast_fn(x)
|
|
354
|
+
except Exception:
|
|
355
|
+
return False
|
|
356
|
+
if is_pair:
|
|
357
|
+
if not (_close(ref[0], new[0]) and _close(ref[1], new[1])):
|
|
358
|
+
return False
|
|
359
|
+
elif not _close(ref, new):
|
|
360
|
+
return False
|
|
361
|
+
checked += 1
|
|
362
|
+
if len(fast.congrps):
|
|
363
|
+
ref_cx = getattr(instance, "cx", None)
|
|
364
|
+
if ref_cx is not None:
|
|
365
|
+
try:
|
|
366
|
+
ref = ref_cx(x.copy())
|
|
367
|
+
except Exception:
|
|
368
|
+
ref = None
|
|
369
|
+
if ref is not None:
|
|
370
|
+
try:
|
|
371
|
+
if not _close(ref, fast.cx(x)):
|
|
372
|
+
return False
|
|
373
|
+
except Exception:
|
|
374
|
+
return False
|
|
375
|
+
checked += 1
|
|
376
|
+
ref_cJx = getattr(instance, "cJx", None)
|
|
377
|
+
if ref_cJx is not None:
|
|
378
|
+
try:
|
|
379
|
+
c_ref, J_ref = ref_cJx(x.copy())
|
|
380
|
+
except Exception:
|
|
381
|
+
c_ref = J_ref = None
|
|
382
|
+
if c_ref is not None:
|
|
383
|
+
try:
|
|
384
|
+
c_new, J_new = fast.cJx(x)
|
|
385
|
+
if not (_close(c_ref, c_new) and _jac_close(J_ref, J_new)):
|
|
386
|
+
return False
|
|
387
|
+
except Exception:
|
|
388
|
+
return False
|
|
389
|
+
checked += 1
|
|
390
|
+
return True if checked else None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _close(a: Any, b: Any) -> bool:
|
|
394
|
+
a = np.asarray(a, dtype=float).reshape(-1)
|
|
395
|
+
b = np.asarray(b, dtype=float).reshape(-1)
|
|
396
|
+
if a.shape != b.shape:
|
|
397
|
+
return False
|
|
398
|
+
return bool(np.allclose(a, b, rtol=_VERIFY_RTOL, atol=_VERIFY_ATOL, equal_nan=True))
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _jac_close(ref: Any, new: Any) -> bool:
|
|
402
|
+
ref = sp.csr_matrix(ref)
|
|
403
|
+
new = sp.csr_matrix(new)
|
|
404
|
+
if ref.shape != new.shape:
|
|
405
|
+
return False
|
|
406
|
+
diff = ref - new
|
|
407
|
+
if diff.nnz == 0:
|
|
408
|
+
return True
|
|
409
|
+
scale = max(1.0, float(np.max(np.abs(ref.data))) if ref.nnz else 1.0)
|
|
410
|
+
return float(np.max(np.abs(diff.data))) <= _VERIFY_RTOL * scale
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
__all__ = ["FastS2MPJEval", "fast_evaluator"]
|