From 48fae1acc3235dcae1ee85355b8efd397af4d480 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Tue, 14 Jul 2026 17:26:22 +0100 Subject: [PATCH] fix: test-mode-gate singular/non-PD inversion crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Return a benign dummy from the inversion reconstruction/log-det sites when PYAUTO_TEST_MODE is active, instead of raising on a singular/non-PD curvature-regularization matrix. In test mode the fitted model is fabricated / under-converged and the inversion output is discarded, so an unguarded test-mode path (search chaining, preloads, result construction) should not crash — while the real likelihood path already resamples such solutions via FitException. Normal operation is byte-for-byte unchanged: the four sites re-raise exactly as before (LinAlgError / InversionException), so real-mode numerics and the search resample behaviour are untouched. numpy-only: the JAX linalg path returns NaN rather than raising. Sites gated on autoconf.is_test_mode(): - inversion_util.reconstruction_positive_negative_from (xp.linalg.solve) - inversion_util.reconstruction_positive_only_from (fnnls except) - abstract.log_det_curvature_reg_matrix_term (cholesky) - abstract.log_det_regularization_matrix_term (cholesky) Fixes the flaky release-tail FAILs slam.py / cpu_fast_modeling.py (PyAutoArray#388; follow-up to PyAutoHeart#72, PyAutoLens#607). Co-Authored-By: Claude Opus 4.8 --- autoarray/inversion/inversion/abstract.py | 40 ++++++++++---- .../inversion/inversion/inversion_util.py | 20 ++++++- .../inversion/test_inversion_util.py | 53 +++++++++++++++++++ 3 files changed, 101 insertions(+), 12 deletions(-) diff --git a/autoarray/inversion/inversion/abstract.py b/autoarray/inversion/inversion/abstract.py index 147ddfe23..d6bef7992 100644 --- a/autoarray/inversion/inversion/abstract.py +++ b/autoarray/inversion/inversion/abstract.py @@ -3,7 +3,7 @@ import numpy as np from typing import Dict, List, Optional, Type, Union -from autoconf import cached_property +from autoconf import cached_property, is_test_mode from autoarray.dataset.imaging.dataset import Imaging from autoarray.dataset.interferometer.dataset import Interferometer @@ -712,13 +712,23 @@ def log_det_curvature_reg_matrix_term(self) -> float: if not self.has(cls=AbstractRegularization): return 0.0 - return 2.0 * self._xp.sum( - self._xp.log( - self._xp.diag( - self._xp.linalg.cholesky(self.curvature_reg_matrix_reduced) + try: + return 2.0 * self._xp.sum( + self._xp.log( + self._xp.diag( + self._xp.linalg.cholesky(self.curvature_reg_matrix_reduced) + ) ) ) - ) + except np.linalg.LinAlgError: + if is_test_mode(): + # Singular curvature-reg matrix from a fabricated test-mode model; + # this evidence term is discarded. Return a benign 0.0 so unguarded + # test-mode paths do not crash (matches the reconstruction guard in + # inversion_util). Normal runs re-raise unchanged. numpy-only: the + # JAX cholesky returns NaN rather than raising. + return 0.0 + raise @property def log_det_regularization_matrix_term(self) -> float: @@ -737,13 +747,21 @@ def log_det_regularization_matrix_term(self) -> float: if not self.has(cls=AbstractRegularization): return 0.0 - return 2.0 * self._xp.sum( - self._xp.log( - self._xp.diag( - self._xp.linalg.cholesky(self.regularization_matrix_reduced) + try: + return 2.0 * self._xp.sum( + self._xp.log( + self._xp.diag( + self._xp.linalg.cholesky(self.regularization_matrix_reduced) + ) ) ) - ) + except np.linalg.LinAlgError: + if is_test_mode(): + # Singular regularization matrix from a fabricated test-mode model; + # this evidence term is discarded. Benign 0.0 in test mode, unchanged + # (raise) in normal operation. numpy-only (JAX cholesky returns NaN). + return 0.0 + raise @property def reconstruction_noise_map_with_covariance(self) -> np.ndarray: diff --git a/autoarray/inversion/inversion/inversion_util.py b/autoarray/inversion/inversion/inversion_util.py index d02c66d99..1542486e4 100644 --- a/autoarray/inversion/inversion/inversion_util.py +++ b/autoarray/inversion/inversion/inversion_util.py @@ -2,6 +2,8 @@ from typing import List, Optional, Type +from autoconf import is_test_mode + from autoarray.settings import Settings from autoarray import exc @@ -221,7 +223,19 @@ def reconstruction_positive_negative_from( curvature_reg_matrix The curvature_matrix plus regularization matrix, overwriting the curvature_matrix in memory. """ - return xp.linalg.solve(curvature_reg_matrix, data_vector) + try: + return xp.linalg.solve(curvature_reg_matrix, data_vector) + except np.linalg.LinAlgError: + if is_test_mode(): + # Test-mode fits run fabricated / under-converged models whose + # curvature-regularization matrix can be singular; the reconstruction + # value is discarded. Return a benign dummy so unguarded test-mode + # paths (search chaining, preloads, result construction) do not crash. + # Normal runs re-raise unchanged and resample via the likelihood's + # FitException guard — real-mode numerics are untouched. (The JAX path + # returns NaN rather than raising, so this branch is numpy-only.) + return xp.ones_like(data_vector) + raise def reconstruction_positive_only_from( @@ -350,6 +364,10 @@ def reconstruction_positive_only_from( ) except (RuntimeError, np.linalg.LinAlgError, ValueError) as e: + if is_test_mode(): + # See reconstruction_positive_negative_from: benign dummy in test mode, + # unchanged (raise InversionException) in normal operation. + return xp.ones_like(data_vector) raise exc.InversionException() from e diff --git a/test_autoarray/inversion/inversion/test_inversion_util.py b/test_autoarray/inversion/inversion/test_inversion_util.py index 87964d82e..7aa5944fc 100644 --- a/test_autoarray/inversion/inversion/test_inversion_util.py +++ b/test_autoarray/inversion/inversion/test_inversion_util.py @@ -228,3 +228,56 @@ def test__preconditioner_matrix_via_mapping_matrix_from(): preconditioner_matrix == np.array([[5.0, 2.0, 3.0], [4.0, 9.0, 6.0], [7.0, 8.0, 13.0]]) ).all() + + +def test__reconstruction_positive_negative_from__singular_matrix_test_mode_returns_dummy( + monkeypatch, +): + # A rank-deficient curvature-regularization matrix makes np.linalg.solve + # raise a singular-matrix LinAlgError. + curvature_reg_matrix = np.array([[1.0, 1.0], [1.0, 1.0]]) + data_vector = np.array([1.0, 2.0]) + + # Normal operation: the singular solve raises, so the search resamples. + monkeypatch.delenv("PYAUTO_TEST_MODE", raising=False) + with pytest.raises(np.linalg.LinAlgError): + aa.util.inversion.reconstruction_positive_negative_from( + data_vector=data_vector, + curvature_reg_matrix=curvature_reg_matrix, + ) + + # Test mode: the fabricated model's reconstruction is discarded, so a benign + # finite dummy is returned instead of crashing an unguarded test-mode path. + monkeypatch.setenv("PYAUTO_TEST_MODE", "1") + reconstruction = aa.util.inversion.reconstruction_positive_negative_from( + data_vector=data_vector, + curvature_reg_matrix=curvature_reg_matrix, + ) + assert reconstruction.shape == data_vector.shape + assert np.all(np.isfinite(reconstruction)) + assert (reconstruction == 1.0).all() + + +def test__reconstruction_positive_only_from__singular_matrix_test_mode_returns_dummy( + monkeypatch, +): + curvature_reg_matrix = np.array([[1.0, 1.0], [1.0, 1.0]]) + data_vector = np.array([1.0, 2.0]) + + # Normal operation: the singular solve raises InversionException (resample). + monkeypatch.delenv("PYAUTO_TEST_MODE", raising=False) + with pytest.raises(aa.exc.InversionException): + aa.util.inversion.reconstruction_positive_only_from( + data_vector=data_vector, + curvature_reg_matrix=curvature_reg_matrix, + ) + + # Test mode: benign finite dummy instead of crashing. + monkeypatch.setenv("PYAUTO_TEST_MODE", "1") + reconstruction = aa.util.inversion.reconstruction_positive_only_from( + data_vector=data_vector, + curvature_reg_matrix=curvature_reg_matrix, + ) + assert reconstruction.shape == data_vector.shape + assert np.all(np.isfinite(reconstruction)) + assert (reconstruction == 1.0).all()