ipax 0.3.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.3.0 → ipax-0.4.0}/PKG-INFO +1 -1
- ipax-0.4.0/benchmarks/corpus/_s2mpj_fast.py +413 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/corpus/s2mpj.py +85 -14
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/harness/__init__.py +34 -11
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/s2mpj.py +239 -91
- ipax-0.4.0/docs/benchmarks/s2mpj.md +199 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/concepts/linalg.md +25 -10
- {ipax-0.3.0 → ipax-0.4.0}/examples/radiotherapy_sparse.py +7 -4
- ipax-0.4.0/examples/sparse_operator_problem.py +82 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/__init__.py +9 -1
- {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/operators.py +445 -0
- ipax-0.4.0/ipax/backend/sparse/_canonical.py +207 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/_routing.py +24 -9
- {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/cupy.py +275 -63
- {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/numpy_scipy.py +134 -7
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/driver.py +151 -18
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/filter_ls.py +41 -3
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/hessian.py +57 -8
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/kkt.py +387 -24
- {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/dense.py +49 -8
- {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/krylov.py +199 -14
- {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/regularize.py +22 -1
- {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/solver.py +10 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/sparse.py +64 -8
- {ipax-0.3.0 → ipax-0.4.0}/ipax/options.py +82 -15
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/derivatives.py +20 -8
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/scaling.py +6 -0
- {ipax-0.3.0 → ipax-0.4.0}/pyproject.toml +1 -1
- {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_builtin_operator_contracts.py +69 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_operator_contract.py +11 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_benchmark_harness.py +39 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_callback_and_logging.py +8 -1
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_known_optima.py +263 -232
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_s2mpj_corpus.py +123 -0
- ipax-0.4.0/tests/integration/test_sparse_operator_problem.py +182 -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.3.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.3.0 → ipax-0.4.0}/tests/unit/test_hessian_lbfgs.py +67 -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_numpy_scipy_canonical_reuse.py +131 -0
- {ipax-0.3.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.3.0 → ipax-0.4.0}/tests/unit/test_operator_coo.py +136 -1
- ipax-0.4.0/tests/unit/test_operator_coo_values.py +115 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_operator_diagonal.py +27 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_options.py +46 -4
- {ipax-0.3.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.3.0 → ipax-0.4.0}/tests/unit/test_regularize.py +19 -1
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_s2mpj_adapter.py +24 -0
- ipax-0.4.0/tests/unit/test_s2mpj_fasteval.py +331 -0
- {ipax-0.3.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.3.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.3.0 → ipax-0.4.0}/tests/unit/test_step_failure_salvage.py +28 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_termination.py +8 -1
- ipax-0.3.0/docs/benchmarks/s2mpj.md +0 -129
- ipax-0.3.0/tests/unit/test_dense_solver.py +0 -63
- ipax-0.3.0/tests/unit/test_filter_ls.py +0 -75
- ipax-0.3.0/tests/unit/test_kkt.py +0 -162
- ipax-0.3.0/tests/unit/test_krylov.py +0 -442
- ipax-0.3.0/tests/unit/test_sparse_dispatch.py +0 -19
- {ipax-0.3.0 → ipax-0.4.0}/.gitignore +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/AGENTS.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/CLAUDE.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/CONTRIBUTING.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/LICENSE +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/NOTICE +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/README.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/README.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/asv.conf.json +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/baselines/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/corpus/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/corpus/external.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/generators/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/harness/plots.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/reports/.gitkeep +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/bench_solve.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/crosscheck.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/device_efficiency.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/micro/bench_kernels.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/qc.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/scaling.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/conftest.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/architecture.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/concepts/algorithm.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/concepts/problem.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/contributing.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/getting-started.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/guide/backends.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/guide/diagnostics.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/guide/options.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/guide/problems.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/guide/results.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/index.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/docs/reference.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/examples/README.md +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/examples/autodiff.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/examples/bound_and_inequality.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/examples/equality_constrained.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/examples/lbfgs_finite_diff.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/examples/matrix_free_krylov.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/examples/nonconvex_hs.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/examples/unconstrained_quadratic.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/_logging.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/namespace.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/jax.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/torch.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/barrier.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/breedveld_ls.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/corrections.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/init.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/restoration.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/step.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/termination.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/autodiff/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/autodiff/jax.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/autodiff/torch.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/base.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/finitediff.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/function.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/linear_ineq.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/py.typed +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/result.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/solve.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/testing/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/testing/backends.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/testing/problems.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/ipax/typing.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/_helpers.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/backends/test_cross_backend_conformance.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/conftest.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_builtin_problem_contracts.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_builtin_solver_contracts.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_builtin_sparse_contracts.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_problem_contract.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_solver_contract.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_sparse_contract.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_benchmark_crosscheck.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_benchmark_phase4.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_benchmark_scaling.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_corrections_solve.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_dense_nonconvex.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_device_efficiency.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_hock_schittkowski.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_krylov_solve.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_linear_inequalities.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_scaling_solve.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_sparse_solve.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_warm_start_solve.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/property/test_derivative_checks.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/regression/__init__.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/regression/test_doctest_collection_adapters.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/regression/test_fraction_to_boundary_no_elementwise_sync.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/test_purity_gate.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_autodiff.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_backend_namespace.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_barrier.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_breedveld.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_corrections.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_inertia_correction.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_init_bounds.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_linear_ineq_lowering.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_logging.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_restoration.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_result.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_solver_selection.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_sparse_operator_symmetry.py +0 -0
- {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_warm_start.py +0 -0
|
@@ -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/
|
|
@@ -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"]
|
|
@@ -14,6 +14,13 @@ model is evaluated on the host. This is for accuracy / cross-backend consistency
|
|
|
14
14
|
benchmarking; it forces a host sync per evaluation, so it is *not* for GPU
|
|
15
15
|
performance work (use the synthetic RT generator / TROTS surrogate there).
|
|
16
16
|
|
|
17
|
+
S2MPJ's generic ``evalgrsum`` evaluation loop is interpretive (per-element
|
|
18
|
+
``eval()`` dispatch, ``lil_matrix`` row assembly) and dominates sweep wall-time,
|
|
19
|
+
so the bridge routes ``fx/fgx/cx/cJx`` through the **precompiled evaluator** in
|
|
20
|
+
:mod:`benchmarks.corpus._s2mpj_fast` whenever it verifies against the original
|
|
21
|
+
methods at build time (else it falls back to the originals — see that module's
|
|
22
|
+
docstring for the mechanism and measured speedups).
|
|
23
|
+
|
|
17
24
|
S2MPJ has no license file, so its files are **not vendored**. Point
|
|
18
25
|
``IPAX_S2MPJ_DIR`` (or the ``directory`` argument) at a local checkout; the loader
|
|
19
26
|
returns ``[]`` when it is absent, mirroring the other external corpora.
|
|
@@ -123,6 +130,23 @@ class _S2MPJProblem(Problem):
|
|
|
123
130
|
m = int(getattr(instance, "m", 0))
|
|
124
131
|
self._m = m
|
|
125
132
|
|
|
133
|
+
# Precompiled evaluator, verified against the original methods at build
|
|
134
|
+
# time (None → keep the originals). Cached on the shared instance, so the
|
|
135
|
+
# per-config rebuilds of the same problem verify once.
|
|
136
|
+
from benchmarks.corpus._s2mpj_fast import fast_evaluator
|
|
137
|
+
|
|
138
|
+
self._fast = fast_evaluator(instance)
|
|
139
|
+
|
|
140
|
+
# Per-point memo of the full constraint value/Jacobian. S2MPJ bundles all
|
|
141
|
+
# constraints in one ``cx``/``cJx`` call (each ~10–16 ms of pure-Python
|
|
142
|
+
# ``eval`` + scipy-sparse assembly — ~100× the objective), and the IPM
|
|
143
|
+
# evaluates equalities and inequalities at the *same* point back-to-back, so
|
|
144
|
+
# a size-1 cache keyed on the point halves the dominant benchmark cost.
|
|
145
|
+
self._cx_key: bytes | None = None
|
|
146
|
+
self._cx_val: Any = None
|
|
147
|
+
self._cJx_key: bytes | None = None
|
|
148
|
+
self._cJx_val: Any = None
|
|
149
|
+
|
|
126
150
|
# ``clower``/``cupper`` exist only when the problem has constraints.
|
|
127
151
|
clower_attr = getattr(instance, "clower", None)
|
|
128
152
|
cupper_attr = getattr(instance, "cupper", None)
|
|
@@ -164,6 +188,19 @@ class _S2MPJProblem(Problem):
|
|
|
164
188
|
def bounds(self) -> tuple[Array | None, Array | None]:
|
|
165
189
|
return self._lower, self._upper
|
|
166
190
|
|
|
191
|
+
# -- evaluation dispatch: verified fast evaluator, else the original -----
|
|
192
|
+
def _eval_fx(self, x_np: Any) -> Any:
|
|
193
|
+
return (self._fast or self._inst).fx(x_np)
|
|
194
|
+
|
|
195
|
+
def _eval_fgx(self, x_np: Any) -> Any:
|
|
196
|
+
return (self._fast or self._inst).fgx(x_np)
|
|
197
|
+
|
|
198
|
+
def _eval_cx(self, x_np: Any) -> Any:
|
|
199
|
+
return (self._fast or self._inst).cx(x_np)
|
|
200
|
+
|
|
201
|
+
def _eval_cJx(self, x_np: Any) -> Any:
|
|
202
|
+
return (self._fast or self._inst).cJx(x_np)
|
|
203
|
+
|
|
167
204
|
def objective(self, x: Array) -> Scalar:
|
|
168
205
|
import numpy as np
|
|
169
206
|
|
|
@@ -176,7 +213,7 @@ class _S2MPJProblem(Problem):
|
|
|
176
213
|
# Python float) so the value keeps the backend dtype — a Python float
|
|
177
214
|
# re-cast via ``xp.asarray`` would silently drop to float32 on Torch.
|
|
178
215
|
try:
|
|
179
|
-
val = np.asarray(self.
|
|
216
|
+
val = np.asarray(self._eval_fx(_to_numpy(x)))
|
|
180
217
|
except (OverflowError, FloatingPointError):
|
|
181
218
|
val = np.asarray(np.inf)
|
|
182
219
|
return _from_numpy(self.xp, val)
|
|
@@ -187,25 +224,51 @@ class _S2MPJProblem(Problem):
|
|
|
187
224
|
if self._no_objective: # constant objective ⇒ zero gradient
|
|
188
225
|
return _from_numpy(self.xp, np.zeros((self._n,)))
|
|
189
226
|
try:
|
|
190
|
-
_f, g = self.
|
|
227
|
+
_f, g = self._eval_fgx(_to_numpy(x))
|
|
191
228
|
g = np.reshape(g, (-1,))
|
|
192
229
|
except (OverflowError, FloatingPointError):
|
|
193
230
|
g = np.full((self._n,), np.inf)
|
|
194
231
|
return _from_numpy(self.xp, g)
|
|
195
232
|
|
|
233
|
+
# -- shared per-point constraint value / Jacobian (memoized) -----------
|
|
234
|
+
def _cx(self, x_np: Any) -> Any:
|
|
235
|
+
"""S2MPJ full constraint vector ``c(x)``, memoized on the last point."""
|
|
236
|
+
import numpy as np
|
|
237
|
+
|
|
238
|
+
key = x_np.tobytes()
|
|
239
|
+
if key != self._cx_key:
|
|
240
|
+
self._cx_key = key
|
|
241
|
+
self._cx_val = np.reshape(
|
|
242
|
+
np.asarray(self._eval_cx(x_np), dtype=float), (-1,)
|
|
243
|
+
)
|
|
244
|
+
return self._cx_val
|
|
245
|
+
|
|
246
|
+
def _cJx(self, x_np: Any) -> Any:
|
|
247
|
+
"""S2MPJ constraint Jacobian ``(c, ∇c)``, memoized on the last point."""
|
|
248
|
+
import numpy as np
|
|
249
|
+
|
|
250
|
+
key = x_np.tobytes()
|
|
251
|
+
if key != self._cJx_key:
|
|
252
|
+
self._cJx_key = key
|
|
253
|
+
self._cJx_val = self._eval_cJx(x_np)
|
|
254
|
+
# cJx returns c(x) alongside the Jacobian; seed the value memo so the
|
|
255
|
+
# driver's same-point eq/ineq *value* requests skip a full cx call.
|
|
256
|
+
c, _jac = self._cJx_val
|
|
257
|
+
self._cx_key = key
|
|
258
|
+
self._cx_val = np.reshape(np.asarray(c, dtype=float), (-1,))
|
|
259
|
+
return self._cJx_val
|
|
260
|
+
|
|
196
261
|
# -- equalities (present only when S2MPJ has clower == cupper rows) ----
|
|
197
262
|
def eq_constraints(self, x: Array) -> Array:
|
|
198
263
|
if self._eq_idx.shape[0] == 0:
|
|
199
264
|
raise NotImplementedError
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
c = np.reshape(np.asarray(self._inst.cx(_to_numpy(x)), dtype=float), (-1,))
|
|
265
|
+
c = self._cx(_to_numpy(x))
|
|
203
266
|
return _from_numpy(self.xp, c[self._eq_idx] - self._eq_rhs)
|
|
204
267
|
|
|
205
268
|
def eq_jacobian(self, x: Array) -> Array | LinearOperator:
|
|
206
269
|
if self._eq_idx.shape[0] == 0:
|
|
207
270
|
raise NotImplementedError
|
|
208
|
-
_c, jac = self.
|
|
271
|
+
_c, jac = self._cJx(_to_numpy(x))
|
|
209
272
|
if self._sparse:
|
|
210
273
|
return self._sparse_op(jac.tocsr()[self._eq_idx, :])
|
|
211
274
|
return _from_numpy(self.xp, jac.toarray()[self._eq_idx, :])
|
|
@@ -216,7 +279,7 @@ class _S2MPJProblem(Problem):
|
|
|
216
279
|
raise NotImplementedError
|
|
217
280
|
import numpy as np
|
|
218
281
|
|
|
219
|
-
c =
|
|
282
|
+
c = self._cx(_to_numpy(x))
|
|
220
283
|
lo = self._lo_rhs - c[self._lo_idx]
|
|
221
284
|
up = c[self._up_idx] - self._up_rhs
|
|
222
285
|
return _from_numpy(self.xp, np.concatenate((lo, up)))
|
|
@@ -226,7 +289,7 @@ class _S2MPJProblem(Problem):
|
|
|
226
289
|
raise NotImplementedError
|
|
227
290
|
import numpy as np
|
|
228
291
|
|
|
229
|
-
_c, jac = self.
|
|
292
|
+
_c, jac = self._cJx(_to_numpy(x))
|
|
230
293
|
# Lower side ``clower − c`` contributes ``−∇c``; upper side ``c − cupper``
|
|
231
294
|
# contributes ``+∇c`` (matches the constraint-value lowering above).
|
|
232
295
|
if self._sparse:
|
|
@@ -279,18 +342,26 @@ class _S2MPJProblem(Problem):
|
|
|
279
342
|
) -> Any:
|
|
280
343
|
"""Assemble ``σ∇²f + Σ y·∇²c`` as a host ``scipy.sparse`` matrix.
|
|
281
344
|
|
|
282
|
-
``LgHxy`` returns ``∇²f + Σ Y_i ∇²c_i`` (no objective scaling)
|
|
283
|
-
``σ
|
|
284
|
-
|
|
285
|
-
|
|
345
|
+
``LgHxy`` returns ``∇²f + Σ Y_i ∇²c_i`` (no objective scaling). For the
|
|
346
|
+
common ``σ > 0`` case (gradient-based scaling passes ``σ = s_f`` through
|
|
347
|
+
the scaling wrapper) the identity ``σ∇²f + Σ Y∇²c = σ·(∇²f + Σ(Y/σ)∇²c)``
|
|
348
|
+
folds the objective scaling into the multipliers, so one ``LgHxy`` call
|
|
349
|
+
suffices — ``LgHxy`` costs a full interpretive Hessian assembly, so the
|
|
350
|
+
old two-call ``(σ−1)∇²f`` correction doubled the exact-Hessian bridge
|
|
351
|
+
cost. ``σ ≤ 0`` keeps the general two-call form.
|
|
286
352
|
"""
|
|
287
353
|
import numpy as np
|
|
288
354
|
|
|
289
355
|
s = float(sigma)
|
|
290
356
|
xcol = np.reshape(_to_numpy(x), (-1, 1))
|
|
291
357
|
Y = self._s2mpj_multipliers(y_eq, y_ineq)
|
|
292
|
-
|
|
293
|
-
|
|
358
|
+
if s == 1.0:
|
|
359
|
+
_lval, _grad, hess = self._inst.LgHxy(xcol, np.reshape(Y, (-1, 1)))
|
|
360
|
+
elif s > 0.0:
|
|
361
|
+
_lval, _grad, hess = self._inst.LgHxy(xcol, np.reshape(Y / s, (-1, 1)))
|
|
362
|
+
hess = s * hess
|
|
363
|
+
else:
|
|
364
|
+
_lval, _grad, hess = self._inst.LgHxy(xcol, np.reshape(Y, (-1, 1)))
|
|
294
365
|
_l0, _g0, hess_f = self._inst.LgHxy(xcol, np.zeros((self._m, 1)))
|
|
295
366
|
hess = hess + (s - 1.0) * hess_f
|
|
296
367
|
return hess
|