From 511085bda417f04965cbadfb64d2f526309f8372 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 21 May 2026 18:11:20 +0100 Subject: [PATCH] fix(viz): make _compute_critical_curve_lines failures loud, not silent The bare `except Exception: return None, None, None, None` in _compute_critical_curve_lines silently swallowed ZeroSolver failures during model fits, which is the root cause behind two recent viz default reverts: - PyAutoGalaxy abd7b717 (2026-04-19): zero_contour YAML default reverted because critical curves silently vanished on HPC. - PyAutoFit #1280 (2026-05-17): use_jax_for_visualization=True default reverted because Euclid source planes wrote all-zero. Both shared the same shape - a JAX-trace failure inside the viz path that the broad except masked. Tighten so: - ModuleNotFoundError (jax_zero_contour missing): silent (already warned upstream in plot_utils). - ValueError (no zero crossings in eigenvalue grid): silent (the curves genuinely do not exist for the model). - Anything else: WARNING log with exc_info=True so the next regression of this class fails loud. Closes Jammy2211/PyAutoGalaxy#433 alongside the PyAutoGalaxy companion PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- autolens/imaging/plot/fit_imaging_plots.py | 15 +++++ .../imaging/plot/test_fit_imaging_plots.py | 58 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/autolens/imaging/plot/fit_imaging_plots.py b/autolens/imaging/plot/fit_imaging_plots.py index efc4dec63..b9692cf93 100644 --- a/autolens/imaging/plot/fit_imaging_plots.py +++ b/autolens/imaging/plot/fit_imaging_plots.py @@ -49,7 +49,22 @@ def _compute_critical_curve_lines(tracer, grid): ["white"] * len(_tan_ca_lines) + ["yellow"] * len(_rad_ca_lines) ) return image_plane_lines, image_plane_line_colors, source_plane_lines, source_plane_line_colors + except (ModuleNotFoundError, ValueError): + # ModuleNotFoundError: jax_zero_contour missing — already warned upstream in + # plot_utils._critical_curves_method(). + # ValueError: no zero crossings in the eigenvalue grid (e.g. slope >= 2 + # isothermal where lambda_r > 0 everywhere). Curves don't exist for this + # model, so rendering without overlays is correct. + return None, None, None, None except Exception: + # Anything else — log loudly with traceback so the next regression of the + # "ZeroSolver raised inside model-fit, viz fell back to all-zero" failure + # mode (PyAutoGalaxy abd7b717, PyAutoFit #1280) does not stay silent. + logger.warning( + "Critical-curve computation failed unexpectedly; rendering without " + "overlays. Investigate — this used to be a silent fallback.", + exc_info=True, + ) return None, None, None, None diff --git a/test_autolens/imaging/plot/test_fit_imaging_plots.py b/test_autolens/imaging/plot/test_fit_imaging_plots.py index ca6665ff2..019f929e5 100644 --- a/test_autolens/imaging/plot/test_fit_imaging_plots.py +++ b/test_autolens/imaging/plot/test_fit_imaging_plots.py @@ -1,8 +1,11 @@ +import logging from pathlib import Path import pytest +from autolens.imaging.plot import fit_imaging_plots from autolens.imaging.plot.fit_imaging_plots import ( + _compute_critical_curve_lines, subplot_fit, subplot_fit_log10, subplot_fit_x1_plane, @@ -156,3 +159,58 @@ def test__subplot_fit_combined_log10__list_of_two_fits__output_file_created( output_format="png", ) assert str(plot_path / "fit_combined_log10.png") in plot_patch.paths + + +@pytest.mark.parametrize( + "exc_cls", + [ModuleNotFoundError, ValueError], + ids=["jax_zero_contour_missing", "no_zero_crossings"], +) +def test__compute_critical_curve_lines__known_recoverable_exceptions__silent( + monkeypatch, caplog, exc_cls +): + """ + Two failure modes are expected and pre-handled upstream: ``jax_zero_contour`` + not installed (``ModuleNotFoundError``) and a model with no zero crossings + (``ValueError`` raised by ``_init_guess_from_coarse_grid``). These must + fall through silently — no WARNING log — so plot-time noise stays clean + when the absence of critical curves is the correct rendering. + """ + def boom(*args, **kwargs): + raise exc_cls("synthetic failure for test") + + monkeypatch.setattr(fit_imaging_plots, "_critical_curves_from", boom) + + with caplog.at_level(logging.WARNING, logger=fit_imaging_plots.__name__): + result = _compute_critical_curve_lines(tracer=None, grid=None) + + assert result == (None, None, None, None) + assert caplog.records == [], ( + "known-recoverable failure must not emit a WARNING log" + ) + + +def test__compute_critical_curve_lines__unexpected_exception__logs_warning( + monkeypatch, caplog +): + """ + Anything OTHER than ``ModuleNotFoundError`` / ``ValueError`` is treated as + an unexpected failure (the silent failure mode that caused the + 2026-04-19 PyAutoGalaxy zero_contour revert and the 2026-05-16 Euclid + pipeline regression). Such failures must surface as a WARNING log with + a traceback — never silently swallowed. + """ + def boom(*args, **kwargs): + raise RuntimeError("synthetic unexpected failure for test") + + monkeypatch.setattr(fit_imaging_plots, "_critical_curves_from", boom) + + with caplog.at_level(logging.WARNING, logger=fit_imaging_plots.__name__): + result = _compute_critical_curve_lines(tracer=None, grid=None) + + assert result == (None, None, None, None) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelno == logging.WARNING + assert record.exc_info is not None, "traceback must be attached" + assert isinstance(record.exc_info[1], RuntimeError)