Skip to content

perf: reuse one Cholesky factor of F+H for the solve and the log det - #634

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/reuse-cholesky-factor
Oct 15, 2026
Merged

Jammy2211 merged 1 commit into
mainfrom
feature/reuse-cholesky-factor

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

On the NumPy backend with use_positive_only_solver=False, every likelihood call factorized the same SPD matrix F + H twice: np.linalg.solve (LU) for reconstruction, then np.linalg.cholesky for log_det_curvature_reg_matrix_term. A new cached_property _curvature_reg_matrix_cho_factor computes one scipy.linalg.cho_factor, which serves both. reconstruction solves with cho_solve. The log det is 2 * sum(log(diag(L))) from the same factor, used when every linear object is regularized. The factor is built on a copy, so curvature_reg_matrix is never overwritten.

The factor is None, and the code takes exactly the old routes, in these cases: the JAX backend, the positive-only solver, log_det_method="slogdet", a LinAlgError (scipy's class is numpy's), or a non-finite factor diagonal. Failure behaviour is therefore unchanged:

  • An indefinite but nonsingular F + H is still solved by the LU, and the dense Cholesky log det still raises it (or returns 0.0 in test mode).
  • A singular F + H still makes the solve raise (or return its test-mode dummy).

Campaign: PyAutoLabs/PyAutoPulse#62 (task interferometer_large_mask_aris).

Closes #633

API Changes

None; the changes are internal only. The new _curvature_reg_matrix_cho_factor is private. The positive-negative NumPy reconstruction now comes from a Cholesky solve rather than an LU. It agrees with the LU to round-off on well-conditioned systems. On ill-conditioned ones it agrees to within the system's conditioning, and is more accurate there (see Witness).

Witness (laptop, single-threaded, Aris SPT2319-55 125×125 interferometer, Delaunay + AdaptSplit, numba route, use_positive_only_solver=False)

vertices run load avg full call median, before → after change solve + log det, before → after (breakdown medians) logL before / after log det reconstruction max-abs-rel
1,405 run 1 (nrep 6) 2.2 0.418 → 0.386 s −7.7% 144.5 → 50.9 ms identical (14090054.632080655545) identical 5.2e-14
1,405 repeat (nrep 12) 1.3 0.710 → 0.551 s −22.4% 215.5 → 99.1 ms identical (14095945.676112052053) identical (same as run 1)
1,405 cProfile (3 calls) 2.2 1.441 → 1.091 s −24.3% solve+chol 0.425 s → cho 0.157 s — — —
2,947 run 1 (nrep 6) 2.1 1.635 → 0.801 s −51.0% 1267.6 → 372.7 ms 14080490.696629 / 14080490.696473 (1.1e-11 rel) identical 1.6e-3

Pass condition: ≥15% at 1,405 and ≥35% at 2,947.

  • At 2,947 vertices: clear pass.
  • At 1,405 vertices: run 1's full-call median (−7.7%) fell short. The repeat (−22.4%) and the profiler (−24.3%) pass. The breakdown removes about 94 ms per call in both runs. The full-call median is dominated by numba curvature_matrix noise on a shared machine (227 → 254 ms and 339 → 445 ms between legs with no code change).
  • The "before" breakdown's fom_rest (78 / 122 / 544 ms) is a second dense Cholesky: log_det_curvature_reg_matrix_term is a plain property, so figure_of_merit re-ran it after the breakdown's own tick.

Reconstruction clause ("unchanged to 1e-10 relative") is not met at 2,947 vertices, and cannot be met there. At the witness vector, cond(F + H) = 7.67e14, so eps·cond ≈ 0.17. Measured against a longdouble-residual, iteratively refined reference:

solve forward error (max-rel) relative residual
old LU 1.49e-3 1.8e-11
new Cholesky 5.1e-4 2.9e-12

So the 1.6e-3 change sits inside the conditioning bound, and the new solve is closer to the true solution. The independent reviewer reproduced these numbers. The log det is bit-identical and logL agrees to 1.1e-11 relative. At 1,405 vertices the clause holds (5.2e-14). The reviewer suggests rewording the Witness to "within conditioning (eps·cond); log det bit-identical; logL within 1e-10 relative". Flagged for the human merge decision.

Test Plan

  • New test_autoarray/inversion/inversion/test_positive_negative_shared_cholesky.py (10 tests):
    • imaging and interferometer inversions match the LU solve (rtol 1e-10) and the dense Cholesky log det (1e-9), with one cho_factor call and no mutation of F + H;
    • random SPD system;
    • indefinite matrix: LU kept, log det raises / test-mode 0.0;
    • singular matrix: solve raises / test-mode dummy;
    • non-finite matrix: no factor;
    • slogdet unchanged, no factor;
    • positive-only: no factor;
    • partially regularized: solve uses the factor, log det takes the dense reduced route.
    • No JAX. The file passes under a fake jax that raises ImportError.
  • PyAutoArray pytest test_autoarray -x -q: 2065 passed, 4 xfailed
  • PyAutoGalaxy pytest test_autogalaxy -x -q (worktree source): 1357 passed
  • PyAutoLens pytest test_autolens -x -q (worktree source): 832 passed, 1 xfailed
  • Smoke (pyauto-heart smoke, all workspaces, worktree source): 165/165 passed, 0 failed (autofit 8 scripts + 2 notebooks, autogalaxy 14+2, autolens 41+2, autolens_test 37, euclid 9, howtolens 50); autocti/autocti_test not run (smoke env build failed locally: arcticpy needs libgsl-dev, same as PyAutoArray#632)
Full API Changes (for automation & release notes)

Added

  • AbstractInversion._curvature_reg_matrix_cho_factor (private cached_property): scipy.linalg.cho_factor tuple of curvature_reg_matrix, or None.

Changed Behaviour

  • AbstractInversion.reconstruction (NumPy, positive-negative, log_det_method="cholesky"): cho_solve with the shared factor in place of np.linalg.solve; falls back to the unchanged LU when the factor is None.
  • AbstractInversion.log_det_curvature_reg_matrix_term (same gate, all linear objects regularized): read off the shared factor; it does not evaluate the reconstruction.
  • Memory: one extra (n, n) array per inversion (~70 MB at n ≈ 3,000), documented in the docstring.

Validation checklist (--auto run — plan was not pre-approved)

  • Effective level: safe (header: safe, cap: feature → safe)
  • Plan: on the issue (perf: reuse one Cholesky factor of F+H for the solve and the log det #633), written at start, unmodified since
  • Gate: tests PyAutoArray 2065 passed/4 xfailed, PyAutoGalaxy 1357 passed, PyAutoLens 832 passed/1 xfailed · smoke 165/165 passed, 0 failed (autofit 8 scripts + 2 notebooks, autogalaxy 14+2, autolens 41+2, autolens_test 37, euclid 9, howtolens 50); autocti/autocti_test not run (smoke env build failed locally: arcticpy needs libgsl-dev, same as PyAutoArray#632) · review CLEAN (independent Opus reader; 6 claims dispositioned basis-cited, incl. the 2,947 reconstruction-accuracy claim; one non-finding edge noted: a numerically singular PSD matrix with tiny-but-positive Cholesky pivots, where the LU hits an exact zero pivot, now returns a large solution instead of raising) · Heart RED, under the development-only override below
  • Witness: speed PASS (2,947: −51%; 1,405: −22.4% repeat / −24.3% profile, run 1 −7.7% under load); log det bit-identical; logL ≤1.1e-11 rel; reconstruction 1e-10 clause FAILS as written at 2,947 (cond 7.7e14), new solve measured more accurate
  • Human: plan sound in hindsight?
  • Human: diff matches plan (no scope creep)?
  • Human: accept the reworded reconstruction witness (within conditioning)?
  • Human: merge, amend, or reject — then log the outcome

Heart RED development override

Heart was RED at launch and at ship (Heart: RED — Release RED; monitoring RED · 10/100 · 415 unresolved; reasons as listed on #633 / PyAutoArray#626). pyauto-heart readiness --json at ship time reported stale ("release validation stale: source moved since rehearsal (PyAutoFit)"). The live human authorized the development-only override in session 3e3dea38: AskUserQuestion "L1 now under override (Recommended)", 2026-10-15 late. It permits commit, push and PR-open. Merge is a separate human /prm. This PR stops at PR-open; no auto-merge.

Generated by the PyAutoLabs agent workflow.

🤖 Generated with Claude Code

On the NumPy backend with use_positive_only_solver=False, a likelihood call
factorized the same SPD matrix F + H twice: np.linalg.solve (LU) for the
reconstruction and np.linalg.cholesky for log_det_curvature_reg_matrix_term.

A new cached_property `_curvature_reg_matrix_cho_factor` computes one
scipy.linalg.cho_factor (a copy; curvature_reg_matrix is not overwritten).
`reconstruction` solves with cho_solve, and the log det reads
2 * sum(log(diag(L))) off the same factor when every linear object is
regularized.

The factor is None (unchanged routes) on JAX, with the positive-only solver,
under log_det_method="slogdet", on LinAlgError, or when its diagonal is not
finite. An indefinite-but-nonsingular F + H is therefore still solved by the
LU and still makes the log det raise (or return 0.0 in test mode), and a
singular one still makes the solve raise (or return its test-mode dummy).
Tests pin all of these against the two-factorization values.

Closes #633

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jammy2211 Jammy2211 added the pending-release PR queued for the next release build label Oct 15, 2026
@Jammy2211
Jammy2211 merged commit e59d5fa into main Oct 15, 2026
3 checks passed
@Jammy2211
Jammy2211 deleted the feature/reuse-cholesky-factor branch October 15, 2026 23:27
@Jammy2211 Jammy2211 removed the pending-release PR queued for the next release build label Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: reuse one Cholesky factor of F+H for the solve and the log det

1 participant