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.
Files changed (186) hide show
  1. {ipax-0.3.0 → ipax-0.4.0}/PKG-INFO +1 -1
  2. ipax-0.4.0/benchmarks/corpus/_s2mpj_fast.py +413 -0
  3. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/corpus/s2mpj.py +85 -14
  4. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/harness/__init__.py +34 -11
  5. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/s2mpj.py +239 -91
  6. ipax-0.4.0/docs/benchmarks/s2mpj.md +199 -0
  7. {ipax-0.3.0 → ipax-0.4.0}/docs/concepts/linalg.md +25 -10
  8. {ipax-0.3.0 → ipax-0.4.0}/examples/radiotherapy_sparse.py +7 -4
  9. ipax-0.4.0/examples/sparse_operator_problem.py +82 -0
  10. {ipax-0.3.0 → ipax-0.4.0}/ipax/__init__.py +9 -1
  11. {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/operators.py +445 -0
  12. ipax-0.4.0/ipax/backend/sparse/_canonical.py +207 -0
  13. {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/_routing.py +24 -9
  14. {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/cupy.py +275 -63
  15. {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/numpy_scipy.py +134 -7
  16. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/driver.py +151 -18
  17. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/filter_ls.py +41 -3
  18. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/hessian.py +57 -8
  19. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/kkt.py +387 -24
  20. {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/dense.py +49 -8
  21. {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/krylov.py +199 -14
  22. {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/regularize.py +22 -1
  23. {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/solver.py +10 -0
  24. {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/sparse.py +64 -8
  25. {ipax-0.3.0 → ipax-0.4.0}/ipax/options.py +82 -15
  26. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/derivatives.py +20 -8
  27. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/scaling.py +6 -0
  28. {ipax-0.3.0 → ipax-0.4.0}/pyproject.toml +1 -1
  29. {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_builtin_operator_contracts.py +69 -0
  30. {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_operator_contract.py +11 -0
  31. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_benchmark_harness.py +39 -0
  32. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_callback_and_logging.py +8 -1
  33. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_known_optima.py +263 -232
  34. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_s2mpj_corpus.py +123 -0
  35. ipax-0.4.0/tests/integration/test_sparse_operator_problem.py +182 -0
  36. ipax-0.4.0/tests/regression/test_delta_c_rank_deficient_constraints.py +83 -0
  37. ipax-0.4.0/tests/regression/test_hs7_lbfgs_no_false_infeasible.py +80 -0
  38. ipax-0.4.0/tests/regression/test_hs9_krylov_zero_hessian.py +47 -0
  39. ipax-0.4.0/tests/regression/test_lbfgs_gradient_overshoot.py +60 -0
  40. ipax-0.4.0/tests/regression/test_sparse_non_finite_kkt_no_crash.py +59 -0
  41. ipax-0.4.0/tests/regression/test_unbounded_detection.py +90 -0
  42. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_cupy_sparse_adapter.py +232 -4
  43. ipax-0.4.0/tests/unit/test_dense_solver.py +327 -0
  44. ipax-0.4.0/tests/unit/test_filter_ls.py +186 -0
  45. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_hessian_lbfgs.py +67 -0
  46. ipax-0.4.0/tests/unit/test_kkt.py +669 -0
  47. ipax-0.4.0/tests/unit/test_krylov.py +838 -0
  48. ipax-0.4.0/tests/unit/test_numpy_scipy_canonical_reuse.py +131 -0
  49. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_numpy_scipy_feral.py +40 -1
  50. ipax-0.4.0/tests/unit/test_operator_batched_hooks.py +79 -0
  51. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_operator_coo.py +136 -1
  52. ipax-0.4.0/tests/unit/test_operator_coo_values.py +115 -0
  53. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_operator_diagonal.py +27 -0
  54. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_options.py +46 -4
  55. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_problem_derivatives.py +37 -0
  56. ipax-0.4.0/tests/unit/test_public_sparse_operators.py +218 -0
  57. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_regularize.py +19 -1
  58. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_s2mpj_adapter.py +24 -0
  59. ipax-0.4.0/tests/unit/test_s2mpj_fasteval.py +331 -0
  60. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_scaling.py +14 -0
  61. ipax-0.4.0/tests/unit/test_sparse_canonical.py +151 -0
  62. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_sparse_device_routing.py +14 -0
  63. ipax-0.4.0/tests/unit/test_sparse_dispatch.py +78 -0
  64. ipax-0.4.0/tests/unit/test_sparse_facade_structure_cache.py +112 -0
  65. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_step_failure_salvage.py +28 -0
  66. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_termination.py +8 -1
  67. ipax-0.3.0/docs/benchmarks/s2mpj.md +0 -129
  68. ipax-0.3.0/tests/unit/test_dense_solver.py +0 -63
  69. ipax-0.3.0/tests/unit/test_filter_ls.py +0 -75
  70. ipax-0.3.0/tests/unit/test_kkt.py +0 -162
  71. ipax-0.3.0/tests/unit/test_krylov.py +0 -442
  72. ipax-0.3.0/tests/unit/test_sparse_dispatch.py +0 -19
  73. {ipax-0.3.0 → ipax-0.4.0}/.gitignore +0 -0
  74. {ipax-0.3.0 → ipax-0.4.0}/AGENTS.md +0 -0
  75. {ipax-0.3.0 → ipax-0.4.0}/CLAUDE.md +0 -0
  76. {ipax-0.3.0 → ipax-0.4.0}/CONTRIBUTING.md +0 -0
  77. {ipax-0.3.0 → ipax-0.4.0}/LICENSE +0 -0
  78. {ipax-0.3.0 → ipax-0.4.0}/NOTICE +0 -0
  79. {ipax-0.3.0 → ipax-0.4.0}/README.md +0 -0
  80. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/README.md +0 -0
  81. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/asv.conf.json +0 -0
  82. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/baselines/__init__.py +0 -0
  83. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/corpus/__init__.py +0 -0
  84. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/corpus/external.py +0 -0
  85. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/generators/__init__.py +0 -0
  86. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/harness/plots.py +0 -0
  87. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/reports/.gitkeep +0 -0
  88. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/bench_solve.py +0 -0
  89. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/crosscheck.py +0 -0
  90. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/device_efficiency.py +0 -0
  91. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/micro/bench_kernels.py +0 -0
  92. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/qc.py +0 -0
  93. {ipax-0.3.0 → ipax-0.4.0}/benchmarks/runners/scaling.py +0 -0
  94. {ipax-0.3.0 → ipax-0.4.0}/conftest.py +0 -0
  95. {ipax-0.3.0 → ipax-0.4.0}/docs/architecture.md +0 -0
  96. {ipax-0.3.0 → ipax-0.4.0}/docs/concepts/algorithm.md +0 -0
  97. {ipax-0.3.0 → ipax-0.4.0}/docs/concepts/problem.md +0 -0
  98. {ipax-0.3.0 → ipax-0.4.0}/docs/contributing.md +0 -0
  99. {ipax-0.3.0 → ipax-0.4.0}/docs/getting-started.md +0 -0
  100. {ipax-0.3.0 → ipax-0.4.0}/docs/guide/backends.md +0 -0
  101. {ipax-0.3.0 → ipax-0.4.0}/docs/guide/diagnostics.md +0 -0
  102. {ipax-0.3.0 → ipax-0.4.0}/docs/guide/options.md +0 -0
  103. {ipax-0.3.0 → ipax-0.4.0}/docs/guide/problems.md +0 -0
  104. {ipax-0.3.0 → ipax-0.4.0}/docs/guide/results.md +0 -0
  105. {ipax-0.3.0 → ipax-0.4.0}/docs/index.md +0 -0
  106. {ipax-0.3.0 → ipax-0.4.0}/docs/reference.md +0 -0
  107. {ipax-0.3.0 → ipax-0.4.0}/examples/README.md +0 -0
  108. {ipax-0.3.0 → ipax-0.4.0}/examples/autodiff.py +0 -0
  109. {ipax-0.3.0 → ipax-0.4.0}/examples/bound_and_inequality.py +0 -0
  110. {ipax-0.3.0 → ipax-0.4.0}/examples/equality_constrained.py +0 -0
  111. {ipax-0.3.0 → ipax-0.4.0}/examples/lbfgs_finite_diff.py +0 -0
  112. {ipax-0.3.0 → ipax-0.4.0}/examples/matrix_free_krylov.py +0 -0
  113. {ipax-0.3.0 → ipax-0.4.0}/examples/nonconvex_hs.py +0 -0
  114. {ipax-0.3.0 → ipax-0.4.0}/examples/unconstrained_quadratic.py +0 -0
  115. {ipax-0.3.0 → ipax-0.4.0}/ipax/_logging.py +0 -0
  116. {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/__init__.py +0 -0
  117. {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/namespace.py +0 -0
  118. {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/__init__.py +0 -0
  119. {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/jax.py +0 -0
  120. {ipax-0.3.0 → ipax-0.4.0}/ipax/backend/sparse/torch.py +0 -0
  121. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/__init__.py +0 -0
  122. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/barrier.py +0 -0
  123. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/breedveld_ls.py +0 -0
  124. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/corrections.py +0 -0
  125. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/init.py +0 -0
  126. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/restoration.py +0 -0
  127. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/step.py +0 -0
  128. {ipax-0.3.0 → ipax-0.4.0}/ipax/ipm/termination.py +0 -0
  129. {ipax-0.3.0 → ipax-0.4.0}/ipax/linalg/__init__.py +0 -0
  130. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/__init__.py +0 -0
  131. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/autodiff/__init__.py +0 -0
  132. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/autodiff/jax.py +0 -0
  133. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/autodiff/torch.py +0 -0
  134. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/base.py +0 -0
  135. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/finitediff.py +0 -0
  136. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/function.py +0 -0
  137. {ipax-0.3.0 → ipax-0.4.0}/ipax/problem/linear_ineq.py +0 -0
  138. {ipax-0.3.0 → ipax-0.4.0}/ipax/py.typed +0 -0
  139. {ipax-0.3.0 → ipax-0.4.0}/ipax/result.py +0 -0
  140. {ipax-0.3.0 → ipax-0.4.0}/ipax/solve.py +0 -0
  141. {ipax-0.3.0 → ipax-0.4.0}/ipax/testing/__init__.py +0 -0
  142. {ipax-0.3.0 → ipax-0.4.0}/ipax/testing/backends.py +0 -0
  143. {ipax-0.3.0 → ipax-0.4.0}/ipax/testing/problems.py +0 -0
  144. {ipax-0.3.0 → ipax-0.4.0}/ipax/typing.py +0 -0
  145. {ipax-0.3.0 → ipax-0.4.0}/tests/__init__.py +0 -0
  146. {ipax-0.3.0 → ipax-0.4.0}/tests/_helpers.py +0 -0
  147. {ipax-0.3.0 → ipax-0.4.0}/tests/backends/test_cross_backend_conformance.py +0 -0
  148. {ipax-0.3.0 → ipax-0.4.0}/tests/conftest.py +0 -0
  149. {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/__init__.py +0 -0
  150. {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_builtin_problem_contracts.py +0 -0
  151. {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_builtin_solver_contracts.py +0 -0
  152. {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_builtin_sparse_contracts.py +0 -0
  153. {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_problem_contract.py +0 -0
  154. {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_solver_contract.py +0 -0
  155. {ipax-0.3.0 → ipax-0.4.0}/tests/contracts/test_sparse_contract.py +0 -0
  156. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_benchmark_crosscheck.py +0 -0
  157. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_benchmark_phase4.py +0 -0
  158. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_benchmark_scaling.py +0 -0
  159. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_corrections_solve.py +0 -0
  160. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_dense_nonconvex.py +0 -0
  161. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_device_efficiency.py +0 -0
  162. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_hock_schittkowski.py +0 -0
  163. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_krylov_solve.py +0 -0
  164. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_linear_inequalities.py +0 -0
  165. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_scaling_solve.py +0 -0
  166. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_sparse_solve.py +0 -0
  167. {ipax-0.3.0 → ipax-0.4.0}/tests/integration/test_warm_start_solve.py +0 -0
  168. {ipax-0.3.0 → ipax-0.4.0}/tests/property/test_derivative_checks.py +0 -0
  169. {ipax-0.3.0 → ipax-0.4.0}/tests/regression/__init__.py +0 -0
  170. {ipax-0.3.0 → ipax-0.4.0}/tests/regression/test_doctest_collection_adapters.py +0 -0
  171. {ipax-0.3.0 → ipax-0.4.0}/tests/regression/test_fraction_to_boundary_no_elementwise_sync.py +0 -0
  172. {ipax-0.3.0 → ipax-0.4.0}/tests/test_purity_gate.py +0 -0
  173. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_autodiff.py +0 -0
  174. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_backend_namespace.py +0 -0
  175. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_barrier.py +0 -0
  176. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_breedveld.py +0 -0
  177. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_corrections.py +0 -0
  178. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_inertia_correction.py +0 -0
  179. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_init_bounds.py +0 -0
  180. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_linear_ineq_lowering.py +0 -0
  181. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_logging.py +0 -0
  182. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_restoration.py +0 -0
  183. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_result.py +0 -0
  184. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_solver_selection.py +0 -0
  185. {ipax-0.3.0 → ipax-0.4.0}/tests/unit/test_sparse_operator_symmetry.py +0 -0
  186. {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.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._inst.fx(_to_numpy(x)))
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._inst.fgx(_to_numpy(x))
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
- import numpy as np
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._inst.cJx(_to_numpy(x))
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 = np.reshape(np.asarray(self._inst.cx(_to_numpy(x)), dtype=float), (-1,))
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._inst.cJx(_to_numpy(x))
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); 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.
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
- _lval, _grad, hess = self._inst.LgHxy(xcol, np.reshape(Y, (-1, 1)))
293
- if s != 1.0:
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