Skip to content

Commit 08881e7

Browse files
Jammy2211Jammy2211
authored andcommitted
fix: effective_einstein_radius falls back to NumPy when jax_zero_contour missing
Add caller-side fallback to the NumPy einstein_radius_from(grid) path when xp is not np but jax_zero_contour isn't installed, with one fallback warning per process. User keeps a real Einstein radius value instead of NaN. Sibling to PyAutoGalaxy backstop in #464.
1 parent 0f67334 commit 08881e7

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

autolens/analysis/latent.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
which would kill the post-fit metric write of an otherwise-converged
1818
search).
1919
"""
20+
import importlib
2021
import logging
2122
from typing import Callable, Dict, List, Optional
2223

@@ -35,6 +36,33 @@
3536
# the many fit evaluations a single search performs.
3637
_MAGZERO_WARNED: set = set()
3738

39+
# Set to True the first time ``effective_einstein_radius`` falls back from
40+
# the JAX path to the NumPy path because ``jax_zero_contour`` is missing.
41+
# Deduplicates the fallback warning across the many fit evaluations a
42+
# single search performs.
43+
_JAX_ZERO_CONTOUR_FALLBACK_WARNED: bool = False
44+
45+
46+
def _jax_zero_contour_available() -> bool:
47+
"""
48+
Return True if ``jax_zero_contour`` can be imported; False otherwise.
49+
The first False return emits one warning per process noting that
50+
``effective_einstein_radius`` will use the slower NumPy path.
51+
"""
52+
global _JAX_ZERO_CONTOUR_FALLBACK_WARNED
53+
try:
54+
importlib.import_module("jax_zero_contour")
55+
return True
56+
except ModuleNotFoundError:
57+
if not _JAX_ZERO_CONTOUR_FALLBACK_WARNED:
58+
logger.warning(
59+
"jax_zero_contour not installed; effective_einstein_radius "
60+
"falling back to NumPy path (slower). "
61+
"pip install jax_zero_contour to enable the JIT path."
62+
)
63+
_JAX_ZERO_CONTOUR_FALLBACK_WARNED = True
64+
return False
65+
3866

3967
def _maybe_magzero_warn(magzero, name) -> bool:
4068
"""
@@ -217,7 +245,7 @@ def effective_einstein_radius(fit, magzero, xp=np):
217245

218246
try:
219247
lens_calc = LensCalc.from_mass_obj(fit.tracer)
220-
if xp is not np:
248+
if xp is not np and _jax_zero_contour_available():
221249
import jax.numpy as jnp
222250
init_guess = jnp.array(
223251
[[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]]

test_autolens/analysis/test_latent.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import importlib
12
import logging
23
from types import SimpleNamespace
34
from unittest.mock import MagicMock
@@ -283,6 +284,61 @@ def einstein_radius_jit_from(self, init_guess):
283284
assert calls["grid"] == "sentinel_grid"
284285

285286

287+
def test_effective_einstein_radius_jax_path_falls_back_to_numpy_when_dep_missing(
288+
monkeypatch, caplog
289+
):
290+
"""
291+
When ``xp is not np`` but ``jax_zero_contour`` isn't installed, the
292+
function must fall through to ``einstein_radius_from`` (the NumPy path)
293+
instead of crashing or returning NaN — caller-side fallback yields a
294+
real Einstein radius value. One warning is emitted per process.
295+
"""
296+
_latent_module._JAX_ZERO_CONTOUR_FALLBACK_WARNED = False
297+
298+
real_import = importlib.import_module
299+
300+
def fake_import(name, *args, **kwargs):
301+
if name == "jax_zero_contour":
302+
raise ModuleNotFoundError(f"No module named '{name}'")
303+
return real_import(name, *args, **kwargs)
304+
305+
monkeypatch.setattr(_latent_module.importlib, "import_module", fake_import)
306+
307+
calls = {}
308+
309+
class _SpyLensCalc:
310+
def einstein_radius_from(self, grid):
311+
calls["grid"] = grid
312+
return 5.678
313+
314+
def einstein_radius_jit_from(self, init_guess):
315+
raise AssertionError(
316+
"jit path must not run when jax_zero_contour is missing"
317+
)
318+
319+
monkeypatch.setattr(
320+
"autogalaxy.operate.lens_calc.LensCalc.from_mass_obj",
321+
classmethod(lambda cls, tracer: _SpyLensCalc()),
322+
)
323+
fit = SimpleNamespace(
324+
tracer=object(),
325+
dataset=SimpleNamespace(grids=SimpleNamespace(lp="sentinel_grid")),
326+
)
327+
328+
sentinel_xp = MagicMock() # truthy `xp is not np`
329+
with caplog.at_level(logging.WARNING, logger=_latent_module.__name__):
330+
value = effective_einstein_radius(
331+
fit=fit, magzero=None, xp=sentinel_xp
332+
)
333+
334+
assert value == pytest.approx(5.678)
335+
assert calls["grid"] == "sentinel_grid"
336+
fallback_warnings = [
337+
r for r in caplog.records if "falling back to NumPy" in r.message
338+
]
339+
assert len(fallback_warnings) == 1
340+
341+
286342
def test_effective_einstein_radius_returns_nan_on_value_error(monkeypatch):
287343
def _raise(cls, tracer):
288344
raise ValueError("singular mass model")

0 commit comments

Comments
 (0)