Skip to content

Commit 1ff9028

Browse files
Jammy2211Jammy2211claude
authored
fix: test-mode-gate singular/non-PD inversion crash (#389)
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: Jammy2211 <JNightingale2211@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 59a7802 commit 1ff9028

3 files changed

Lines changed: 101 additions & 12 deletions

File tree

autoarray/inversion/inversion/abstract.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy as np
44
from typing import Dict, List, Optional, Type, Union
55

6-
from autoconf import cached_property
6+
from autoconf import cached_property, is_test_mode
77

88
from autoarray.dataset.imaging.dataset import Imaging
99
from autoarray.dataset.interferometer.dataset import Interferometer
@@ -712,13 +712,23 @@ def log_det_curvature_reg_matrix_term(self) -> float:
712712
if not self.has(cls=AbstractRegularization):
713713
return 0.0
714714

715-
return 2.0 * self._xp.sum(
716-
self._xp.log(
717-
self._xp.diag(
718-
self._xp.linalg.cholesky(self.curvature_reg_matrix_reduced)
715+
try:
716+
return 2.0 * self._xp.sum(
717+
self._xp.log(
718+
self._xp.diag(
719+
self._xp.linalg.cholesky(self.curvature_reg_matrix_reduced)
720+
)
719721
)
720722
)
721-
)
723+
except np.linalg.LinAlgError:
724+
if is_test_mode():
725+
# Singular curvature-reg matrix from a fabricated test-mode model;
726+
# this evidence term is discarded. Return a benign 0.0 so unguarded
727+
# test-mode paths do not crash (matches the reconstruction guard in
728+
# inversion_util). Normal runs re-raise unchanged. numpy-only: the
729+
# JAX cholesky returns NaN rather than raising.
730+
return 0.0
731+
raise
722732

723733
@property
724734
def log_det_regularization_matrix_term(self) -> float:
@@ -737,13 +747,21 @@ def log_det_regularization_matrix_term(self) -> float:
737747
if not self.has(cls=AbstractRegularization):
738748
return 0.0
739749

740-
return 2.0 * self._xp.sum(
741-
self._xp.log(
742-
self._xp.diag(
743-
self._xp.linalg.cholesky(self.regularization_matrix_reduced)
750+
try:
751+
return 2.0 * self._xp.sum(
752+
self._xp.log(
753+
self._xp.diag(
754+
self._xp.linalg.cholesky(self.regularization_matrix_reduced)
755+
)
744756
)
745757
)
746-
)
758+
except np.linalg.LinAlgError:
759+
if is_test_mode():
760+
# Singular regularization matrix from a fabricated test-mode model;
761+
# this evidence term is discarded. Benign 0.0 in test mode, unchanged
762+
# (raise) in normal operation. numpy-only (JAX cholesky returns NaN).
763+
return 0.0
764+
raise
747765

748766
@property
749767
def reconstruction_noise_map_with_covariance(self) -> np.ndarray:

autoarray/inversion/inversion/inversion_util.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from typing import List, Optional, Type
44

5+
from autoconf import is_test_mode
6+
57
from autoarray.settings import Settings
68

79
from autoarray import exc
@@ -221,7 +223,19 @@ def reconstruction_positive_negative_from(
221223
curvature_reg_matrix
222224
The curvature_matrix plus regularization matrix, overwriting the curvature_matrix in memory.
223225
"""
224-
return xp.linalg.solve(curvature_reg_matrix, data_vector)
226+
try:
227+
return xp.linalg.solve(curvature_reg_matrix, data_vector)
228+
except np.linalg.LinAlgError:
229+
if is_test_mode():
230+
# Test-mode fits run fabricated / under-converged models whose
231+
# curvature-regularization matrix can be singular; the reconstruction
232+
# value is discarded. Return a benign dummy so unguarded test-mode
233+
# paths (search chaining, preloads, result construction) do not crash.
234+
# Normal runs re-raise unchanged and resample via the likelihood's
235+
# FitException guard — real-mode numerics are untouched. (The JAX path
236+
# returns NaN rather than raising, so this branch is numpy-only.)
237+
return xp.ones_like(data_vector)
238+
raise
225239

226240

227241
def reconstruction_positive_only_from(
@@ -350,6 +364,10 @@ def reconstruction_positive_only_from(
350364
)
351365

352366
except (RuntimeError, np.linalg.LinAlgError, ValueError) as e:
367+
if is_test_mode():
368+
# See reconstruction_positive_negative_from: benign dummy in test mode,
369+
# unchanged (raise InversionException) in normal operation.
370+
return xp.ones_like(data_vector)
353371
raise exc.InversionException() from e
354372

355373

test_autoarray/inversion/inversion/test_inversion_util.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,56 @@ def test__preconditioner_matrix_via_mapping_matrix_from():
228228
preconditioner_matrix
229229
== np.array([[5.0, 2.0, 3.0], [4.0, 9.0, 6.0], [7.0, 8.0, 13.0]])
230230
).all()
231+
232+
233+
def test__reconstruction_positive_negative_from__singular_matrix_test_mode_returns_dummy(
234+
monkeypatch,
235+
):
236+
# A rank-deficient curvature-regularization matrix makes np.linalg.solve
237+
# raise a singular-matrix LinAlgError.
238+
curvature_reg_matrix = np.array([[1.0, 1.0], [1.0, 1.0]])
239+
data_vector = np.array([1.0, 2.0])
240+
241+
# Normal operation: the singular solve raises, so the search resamples.
242+
monkeypatch.delenv("PYAUTO_TEST_MODE", raising=False)
243+
with pytest.raises(np.linalg.LinAlgError):
244+
aa.util.inversion.reconstruction_positive_negative_from(
245+
data_vector=data_vector,
246+
curvature_reg_matrix=curvature_reg_matrix,
247+
)
248+
249+
# Test mode: the fabricated model's reconstruction is discarded, so a benign
250+
# finite dummy is returned instead of crashing an unguarded test-mode path.
251+
monkeypatch.setenv("PYAUTO_TEST_MODE", "1")
252+
reconstruction = aa.util.inversion.reconstruction_positive_negative_from(
253+
data_vector=data_vector,
254+
curvature_reg_matrix=curvature_reg_matrix,
255+
)
256+
assert reconstruction.shape == data_vector.shape
257+
assert np.all(np.isfinite(reconstruction))
258+
assert (reconstruction == 1.0).all()
259+
260+
261+
def test__reconstruction_positive_only_from__singular_matrix_test_mode_returns_dummy(
262+
monkeypatch,
263+
):
264+
curvature_reg_matrix = np.array([[1.0, 1.0], [1.0, 1.0]])
265+
data_vector = np.array([1.0, 2.0])
266+
267+
# Normal operation: the singular solve raises InversionException (resample).
268+
monkeypatch.delenv("PYAUTO_TEST_MODE", raising=False)
269+
with pytest.raises(aa.exc.InversionException):
270+
aa.util.inversion.reconstruction_positive_only_from(
271+
data_vector=data_vector,
272+
curvature_reg_matrix=curvature_reg_matrix,
273+
)
274+
275+
# Test mode: benign finite dummy instead of crashing.
276+
monkeypatch.setenv("PYAUTO_TEST_MODE", "1")
277+
reconstruction = aa.util.inversion.reconstruction_positive_only_from(
278+
data_vector=data_vector,
279+
curvature_reg_matrix=curvature_reg_matrix,
280+
)
281+
assert reconstruction.shape == data_vector.shape
282+
assert np.all(np.isfinite(reconstruction))
283+
assert (reconstruction == 1.0).all()

0 commit comments

Comments
 (0)