|
| 1 | +import importlib |
1 | 2 | import logging |
2 | 3 | from types import SimpleNamespace |
3 | 4 | from unittest.mock import MagicMock |
@@ -283,6 +284,61 @@ def einstein_radius_jit_from(self, init_guess): |
283 | 284 | assert calls["grid"] == "sentinel_grid" |
284 | 285 |
|
285 | 286 |
|
| 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 | + |
286 | 342 | def test_effective_einstein_radius_returns_nan_on_value_error(monkeypatch): |
287 | 343 | def _raise(cls, tracer): |
288 | 344 | raise ValueError("singular mass model") |
|
0 commit comments