Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions autolens/analysis/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import os
from typing import NoReturn

import autofit as af


def raise_fit_exception(exception: Exception) -> NoReturn:
"""
Re-raise a numpy-path fit failure as an ``af.exc.FitException`` so the
non-linear search discards and resamples the model.

This is path-parity with the JAX branch: there, a pathological model (e.g. a
non-positive-definite inversion) yields ``NaN`` which ``autofit`` maps to a
resample, whereas the numpy branch raises (e.g. ``numpy.linalg.LinAlgError``).
Wrapping the raise in ``FitException`` — which ``autofit`` catches and
resamples — makes both branches behave identically for real fits.

The original exception is preserved as the ``__cause__`` (via ``raise ...
from``), so the true failure remains visible in the traceback rather than
being masked behind a bare ``FitException``.

Set the environment variable ``PYAUTO_RAISE_ANALYSIS_EXCEPTIONS=1`` to
re-raise the original exception unchanged instead of wrapping it. This is a
debugging aid for surfacing the real cause of a masked failure — for example
under ``PYAUTO_TEST_MODE``, where a single likelihood evaluation is not
absorbed by a sampler and the wrapped ``FitException`` would otherwise hide
the underlying error. Off by default so production searches keep resampling.

Parameters
----------
exception
The original exception raised while evaluating the numpy-path fit.
"""
if os.environ.get("PYAUTO_RAISE_ANALYSIS_EXCEPTIONS", "0") == "1":
raise exception
raise af.exc.FitException from exception
3 changes: 2 additions & 1 deletion autolens/imaging/model/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from autonerves.fitsable import hdu_list_for_output_from

from autolens.analysis.analysis.dataset import AnalysisDataset
from autolens.analysis.exceptions import raise_fit_exception
from autolens.analysis.latent import LatentLens
from autolens.imaging.model.result import ResultImaging
from autolens.imaging.model.visualizer import VisualizerImaging
Expand Down Expand Up @@ -141,7 +142,7 @@ def log_likelihood_function(self, instance: af.ModelInstance, shared=None) -> fl
- log_likelihood_penalty
)
except Exception as e:
raise af.exc.FitException
raise_fit_exception(e)

def shared_state_from(self, instance: af.ModelInstance):
"""
Expand Down
3 changes: 2 additions & 1 deletion autolens/interferometer/model/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import autogalaxy as ag

from autolens.analysis.analysis.dataset import AnalysisDataset
from autolens.analysis.exceptions import raise_fit_exception
from autolens.analysis.positions import PositionsLH
from autolens.interferometer.model.result import ResultInterferometer
from autolens.interferometer.model.visualizer import VisualizerInterferometer
Expand Down Expand Up @@ -179,7 +180,7 @@ def log_likelihood_function(self, instance, shared=None):
- log_likelihood_penalty
)
except Exception as e:
raise af.exc.FitException
raise_fit_exception(e)

def shared_state_from(self, instance: af.ModelInstance):
"""
Expand Down
3 changes: 2 additions & 1 deletion autolens/point/model/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from autogalaxy.analysis.analysis.analysis import Analysis as AgAnalysis

from autolens.analysis.analysis.lens import AnalysisLens
from autolens.analysis.exceptions import raise_fit_exception
from autolens.point.fit.positions.image.pair_repeat import FitPositionsImagePairRepeat
from autolens.point.fit.dataset import FitPointDataset
from autolens.point.dataset import PointDataset
Expand Down Expand Up @@ -136,7 +137,7 @@ def log_likelihood_function(self, instance):
try:
return self.fit_from(instance=instance).log_likelihood
except Exception as e:
raise af.exc.FitException
raise_fit_exception(e)

def fit_from(
self,
Expand Down
38 changes: 38 additions & 0 deletions test_autolens/analysis/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest

import autofit as af
from autolens.analysis.exceptions import raise_fit_exception


class _Sentinel(Exception):
"""A distinctive error standing in for a real numpy-path fit failure."""


def test__wraps_as_fit_exception_and_preserves_cause():
sentinel = _Sentinel("non-PD inversion")

with pytest.raises(af.exc.FitException) as exc_info:
raise_fit_exception(sentinel)

# The search resamples on FitException, but the true failure must remain
# visible in the traceback via __cause__ rather than being masked.
assert exc_info.value.__cause__ is sentinel


def test__env_flag_reraises_original_unwrapped(monkeypatch):
monkeypatch.setenv("PYAUTO_RAISE_ANALYSIS_EXCEPTIONS", "1")
sentinel = _Sentinel("non-PD inversion")

with pytest.raises(_Sentinel) as exc_info:
raise_fit_exception(sentinel)

assert exc_info.value is sentinel


def test__env_flag_zero_still_wraps(monkeypatch):
# Only "1" enables raise-through; "0" must keep wrapping (guards against the
# "0"-is-a-truthy-string trap).
monkeypatch.setenv("PYAUTO_RAISE_ANALYSIS_EXCEPTIONS", "0")

with pytest.raises(af.exc.FitException):
raise_fit_exception(_Sentinel())
Loading