Overview
When jax_zero_contour isn't installed, the default-enabled
effective_einstein_radius latent crashes the post-fit metric write of
otherwise-converged searches (surfaced on autolens_profiling job
322552 against PyAutoLens #557). Fix with caller fallback + NaN
backstop: PyAutoLens's effective_einstein_radius detects the
missing dep and routes to the existing NumPy einstein_radius_from
path (user keeps a real value); PyAutoGalaxy's lens_calc.py
soft-fails to NaN / [] as a defensive backstop for any other direct
caller.
Plan
- PyAutoLens — caller fallback. In
effective_einstein_radius,
detect missing jax_zero_contour on the JAX path and route through
the existing NumPy einstein_radius_from(grid=fit.dataset.grids.lp)
branch with a one-time-per-process warning ("falling back to NumPy —
slower; install jax_zero_contour to enable JIT path").
- PyAutoGalaxy — backstop soft-fail. Add
_maybe_optional_dep_warn
helper in lens_calc.py; replace the two try/except → raise blocks
with NaN return (einstein_radius_jit_from) and [] return
(_critical_curve_list_via_zero_contour). Empty list matches the
existing ValueError → return [] path at line 1167.
- Tests in both repos. Mock the import failure and assert: PyAutoLens
fallback returns a real (finite) Einstein radius; PyAutoGalaxy
producer returns NaN / [] with exactly one warning per process per
feature.
Detailed implementation plan
Affected Repositories
- PyAutoGalaxy (primary — soft-fail backstop)
- PyAutoLens (caller fallback)
Work Classification
Library (both repos)
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoGalaxy |
main |
clean |
| ./PyAutoLens |
main |
clean |
Suggested branch: feature/optional-dep-latents-soft-fail (both repos)
Worktree root: ~/Code/PyAutoLabs-wt/optional-dep-latents-soft-fail/ (created later by /start_library)
Implementation Steps
PyAutoGalaxy first (backstop, no caller dependency).
-
autogalaxy/operate/lens_calc.py — add helper at module scope:
- Add
import importlib and _OPTIONAL_DEP_WARNED: set = set() near the top. logger already exists in this module.
- Add
_maybe_optional_dep_warn(import_name: str, feature_name: str) -> bool: tries importlib.import_module(import_name); returns False on success; on ModuleNotFoundError emits one logger.warning per feature_name and returns True. Message: "Optional dependency '%s' not installed; '%s' returning NaN/empty. pip install %s to enable it."
-
Replace _critical_curve_list_via_zero_contour (lines 1155–1161):
if _maybe_optional_dep_warn(
"jax_zero_contour", "critical_curve_list_via_zero_contour"
):
return []
from jax_zero_contour import ZeroSolver
-
Replace einstein_radius_jit_from (lines 1566–1572):
if _maybe_optional_dep_warn(
"jax_zero_contour", "einstein_radius_jit_from"
):
import jax.numpy as jnp
return jnp.nan
from jax_zero_contour import ZeroSolver
-
Add tests to test_autogalaxy/operate/test_deflections.py:
test__einstein_radius_jit_from__missing_jax_zero_contour__returns_nan_and_warns(monkeypatch, caplog) — patch importlib.import_module to raise ModuleNotFoundError for "jax_zero_contour", build a minimal LensCalc, assert return is NaN and one warning record matches feature_name.
test__tangential_critical_curve_list_via_zero_contour__missing_dep__returns_empty_list_and_warns(...) — same approach, assert [].
test__maybe_optional_dep_warn__logs_only_once_per_name(...) — call twice, assert one record.
- Each test resets
_OPTIONAL_DEP_WARNED.discard(name) at the start for determinism.
PyAutoLens second (caller fallback).
-
autolens/analysis/latent.py — add fallback logic to effective_einstein_radius:
- Add module-level
_JAX_ZERO_CONTOUR_FALLBACK_WARNED: bool = False.
- Add helper
_jax_zero_contour_available() -> bool that does importlib.import_module("jax_zero_contour") inside try/except and warns once when missing: "jax_zero_contour not installed; effective_einstein_radius falling back to NumPy path (slower). pip install jax_zero_contour to enable the JIT path."
- Modify the function (lines 218–228):
try:
lens_calc = LensCalc.from_mass_obj(fit.tracer)
if xp is not np and _jax_zero_contour_available():
import jax.numpy as jnp
init_guess = jnp.array(
[[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]]
)
return lens_calc.einstein_radius_jit_from(init_guess=init_guess)
return lens_calc.einstein_radius_from(grid=fit.dataset.grids.lp)
except (ValueError, AttributeError):
return xp.nan
- Net effect: JAX path with dep present → unchanged JIT path. JAX path without dep → falls through to NumPy path with a one-time warning. NumPy path → unchanged.
-
Add tests to test_autolens/analysis/test_latent.py:
test__effective_einstein_radius__jax_path__missing_jax_zero_contour__falls_back_to_numpy(monkeypatch, caplog) — patch importlib.import_module to raise for "jax_zero_contour", call effective_einstein_radius(fit=fit, magzero=None, xp=jnp), assert finite return (not NaN) and one warning emitted.
- Reset
_latent_module._JAX_ZERO_CONTOUR_FALLBACK_WARNED = False at start.
Key Files
PyAutoGalaxy/autogalaxy/operate/lens_calc.py — helper + replace 2 raise sites (~12 LOC net).
PyAutoGalaxy/test_autogalaxy/operate/test_deflections.py — 3 new unit tests.
PyAutoLens/autolens/analysis/latent.py — helper + reroute in effective_einstein_radius (~10 LOC net).
PyAutoLens/test_autolens/analysis/test_latent.py — 1 new fallback test.
Merge Order
PyAutoGalaxy PR first (the backstop). PyAutoLens PR second; does not strictly depend on the PyAutoGalaxy merge because the PyAutoLens fallback sidesteps the producer entirely. Library-first gate still applies.
Out of Scope
- Promoting
jax_zero_contour to a hard dep (alternative; not preferred per the prompt).
autogalaxy/util/plot_utils.py:337-360 — has its own ImportError handling for the plot helper, not reached by default latents.
- Auditing every optional-dep import in PyAutoGalaxy; scope is the latent-computation surface.
- Adding a fallback for
_critical_curve_list_via_zero_contour to the marching-squares sibling — its callers are plot utilities; [] (no curves drawn) is acceptable.
Performance Note
The NumPy fallback (einstein_radius_from) uses a dense ~250k-evaluation grid vs. the JIT'd contour tracer. Cost is post-fit, runs once per converged search, so order-of-seconds is acceptable. The fallback warning makes the cost discoverable to the user.
Original Prompt
Click to expand starting prompt
Optional-dep latents should soft-fail to NaN, not re-raise ModuleNotFoundError
Context
Discovered while validating the PyAutoLens #557 magzero fix on the
first-class A100 search profile (autolens_profiling job 322552). The
magzero _mujy latents now soft-fail correctly, but a second post-fit
latent crashed the same metric-JSON write path with a parallel bug:
File "PyAutoGalaxy/autogalaxy/operate/lens_calc.py", line 1567,
in einstein_radius_jit_from
from jax_zero_contour import ZeroSolver
ModuleNotFoundError: No module named 'jax_zero_contour'
The above exception was the direct cause of the following exception:
File "PyAutoLens/autolens/analysis/latent.py", line 225,
in effective_einstein_radius
return lens_calc.einstein_radius_jit_from(init_guess=init_guess)
raise ModuleNotFoundError(
"jax_zero_contour is required for einstein_radius_jit_from. "
"Install it with: pip install jax_zero_contour"
) from exc
The raise lives at PyAutoGalaxy:autogalaxy/operate/lens_calc.py:1566-1572:
try:
from jax_zero_contour import ZeroSolver
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
"jax_zero_contour is required for einstein_radius_jit_from. "
"Install it with: pip install jax_zero_contour"
) from exc
This is the same default-config-crashes-by-default pattern as the
magzero family (fixed in PyAutoLens #557 with _maybe_magzero_warn),
just keyed on an optional dependency instead of an Analysis kwarg.
The bug
effective_einstein_radius is enabled by default in
PyAutoLens:autolens/config/latent.yaml, and is computed during every
SearchUpdater._compute_latent_samples call as part of the standard
post-fit pipeline. When jax_zero_contour isn't installed (it isn't
in PyAuto's default pyproject.toml deps — it's a JAX-only optional
extra), every search.fit() that reaches the post-fit step crashes
with the message above. The search itself converged; the metric-JSON
write never happens.
This is the same failure shape as
PyAutoPrompt/autolens/magzero_required_latents_crash_search.md
(landed as PyAutoLens #557). The PyAutoLens fix only addressed the
magzero family of latents — the optional-dependency family was not
touched, and surfaces with the same symptoms.
Desired fix
Mirror PyAutoLens #557's _maybe_magzero_warn: replace the re-raise
with a per-process warning + xp.nan return. The "module is missing"
case is structurally identical to the "user kwarg is missing" case —
both mean the latent value is unknown, and the right behaviour is
return-NaN-and-warn, not kill the search.
Sketch (in PyAutoGalaxy:autogalaxy/operate/lens_calc.py or wherever
the latent function lives — PyAutoLens:autolens/analysis/latent.py:225
calls into this):
_OPTIONAL_DEP_WARNED: set[str] = set()
def _maybe_optional_dep_warn(import_name: str, name: str) -> bool:
"""Return True (and warn once) if the optional dependency is missing."""
try:
importlib.import_module(import_name)
return False
except ModuleNotFoundError:
if name not in _OPTIONAL_DEP_WARNED:
logger.warning(
"Optional dependency '%s' not installed; '%s' latent will "
"be NaN. pip install %s to enable it, or disable in "
"config/latent.yaml to silence this warning.",
import_name, name, import_name,
)
_OPTIONAL_DEP_WARNED.add(name)
return True
einstein_radius_jit_from (and any sibling that does the same
try/except ModuleNotFoundError → re-raise pattern) becomes:
def einstein_radius_jit_from(self, ...):
if _maybe_optional_dep_warn("jax_zero_contour", "effective_einstein_radius"):
return xp.nan
from jax_zero_contour import ZeroSolver
...
Test plan
- Unit test (mirror
test_autolens/analysis/test_latent.py's magzero
cases): mock jax_zero_contour import to raise, assert the latent
returns NaN and emits one warning per process.
- End-to-end on a node WITHOUT
jax_zero_contour installed: construct
AnalysisImaging(..., use_jax=True) with default
config/latent.yaml, run af.Nautilus(...).fit(...) in
PYAUTO_TEST_MODE=1. Confirm search completes, JSON write happens,
latent.csv has NaN values for the affected columns.
- Smoke check: re-run
autolens_profiling/searches/nautilus/imaging/mge.py
on a clean venv (no jax_zero_contour) and confirm no crash.
Affected callers / interaction surface
- Every PyAutoLens search using default latents on a venv without
jax_zero_contour — the HPC PyAutoNSS venv on Euclid SAAS in
particular, where jax_zero_contour was not installed at venv
creation time (added manually post-hoc to unblock job 322552).
- Sibling functions in
PyAutoGalaxy:autogalaxy/operate/lens_calc.py
that do the same try / except ModuleNotFoundError → raise pattern:
grep for raise ModuleNotFoundError in lens_calc.py and adjacent
modules; any that gate optional-dep imports for default-on latents
should switch to the soft-fail pattern.
Why not just add jax_zero_contour as a hard dep?
That's the pragmatic short-term fix (it's what unblocked job 322552
on the HPC). But it makes a JAX-only utility a required dep of every
PyAutoGalaxy install, including non-JAX numpy-only users — wider blast
radius than this fix needs. Soft-fail keeps the optional dep optional
while making default configs runnable.
Out of scope
- Promoting
jax_zero_contour to a hard dep (alternative; not the
preferred fix).
- Auditing every optional-dep code path in PyAutoGalaxy. Scope to
the latent-computation surface where default configs hit them.
- Re-reviewing PyAutoLens #557 — that fix is correct for what it
covered; this is a parallel-but-independent surface.
Cross-references
- PyAutoLens #557 — fixed the magzero family with the same soft-fail
pattern this prompt proposes for optional deps.
- PyAutoPrompt/autolens/magzero_required_latents_crash_search.md —
the upstream prompt for #557.
- autolens_profiling HPC job 322552 — the surfacing run; sampling
completed (64,200 evals, log_Z=+31690.49) but post-fit latent
crashed before metric JSON write.
Overview
When
jax_zero_contourisn't installed, the default-enabledeffective_einstein_radiuslatent crashes the post-fit metric write ofotherwise-converged searches (surfaced on
autolens_profilingjob322552 against PyAutoLens #557). Fix with caller fallback + NaN
backstop: PyAutoLens's
effective_einstein_radiusdetects themissing dep and routes to the existing NumPy
einstein_radius_frompath (user keeps a real value); PyAutoGalaxy's
lens_calc.pysoft-fails to NaN /
[]as a defensive backstop for any other directcaller.
Plan
effective_einstein_radius,detect missing
jax_zero_contouron the JAX path and route throughthe existing NumPy
einstein_radius_from(grid=fit.dataset.grids.lp)branch with a one-time-per-process warning ("falling back to NumPy —
slower; install jax_zero_contour to enable JIT path").
_maybe_optional_dep_warnhelper in
lens_calc.py; replace the twotry/except → raiseblockswith NaN return (
einstein_radius_jit_from) and[]return(
_critical_curve_list_via_zero_contour). Empty list matches theexisting
ValueError → return []path at line 1167.fallback returns a real (finite) Einstein radius; PyAutoGalaxy
producer returns NaN /
[]with exactly one warning per process perfeature.
Detailed implementation plan
Affected Repositories
Work Classification
Library (both repos)
Branch Survey
Suggested branch:
feature/optional-dep-latents-soft-fail(both repos)Worktree root:
~/Code/PyAutoLabs-wt/optional-dep-latents-soft-fail/(created later by/start_library)Implementation Steps
PyAutoGalaxy first (backstop, no caller dependency).
autogalaxy/operate/lens_calc.py— add helper at module scope:import importliband_OPTIONAL_DEP_WARNED: set = set()near the top.loggeralready exists in this module._maybe_optional_dep_warn(import_name: str, feature_name: str) -> bool: triesimportlib.import_module(import_name); returnsFalseon success; onModuleNotFoundErroremits onelogger.warningperfeature_nameand returnsTrue. Message:"Optional dependency '%s' not installed; '%s' returning NaN/empty. pip install %s to enable it."Replace
_critical_curve_list_via_zero_contour(lines 1155–1161):Replace
einstein_radius_jit_from(lines 1566–1572):Add tests to
test_autogalaxy/operate/test_deflections.py:test__einstein_radius_jit_from__missing_jax_zero_contour__returns_nan_and_warns(monkeypatch, caplog)— patchimportlib.import_moduleto raiseModuleNotFoundErrorfor"jax_zero_contour", build a minimalLensCalc, assert return is NaN and one warning record matchesfeature_name.test__tangential_critical_curve_list_via_zero_contour__missing_dep__returns_empty_list_and_warns(...)— same approach, assert[].test__maybe_optional_dep_warn__logs_only_once_per_name(...)— call twice, assert one record._OPTIONAL_DEP_WARNED.discard(name)at the start for determinism.PyAutoLens second (caller fallback).
autolens/analysis/latent.py— add fallback logic toeffective_einstein_radius:_JAX_ZERO_CONTOUR_FALLBACK_WARNED: bool = False._jax_zero_contour_available() -> boolthat doesimportlib.import_module("jax_zero_contour")inside try/except and warns once when missing:"jax_zero_contour not installed; effective_einstein_radius falling back to NumPy path (slower). pip install jax_zero_contour to enable the JIT path."Add tests to
test_autolens/analysis/test_latent.py:test__effective_einstein_radius__jax_path__missing_jax_zero_contour__falls_back_to_numpy(monkeypatch, caplog)— patchimportlib.import_moduleto raise for"jax_zero_contour", calleffective_einstein_radius(fit=fit, magzero=None, xp=jnp), assert finite return (not NaN) and one warning emitted._latent_module._JAX_ZERO_CONTOUR_FALLBACK_WARNED = Falseat start.Key Files
PyAutoGalaxy/autogalaxy/operate/lens_calc.py— helper + replace 2 raise sites (~12 LOC net).PyAutoGalaxy/test_autogalaxy/operate/test_deflections.py— 3 new unit tests.PyAutoLens/autolens/analysis/latent.py— helper + reroute ineffective_einstein_radius(~10 LOC net).PyAutoLens/test_autolens/analysis/test_latent.py— 1 new fallback test.Merge Order
PyAutoGalaxy PR first (the backstop). PyAutoLens PR second; does not strictly depend on the PyAutoGalaxy merge because the PyAutoLens fallback sidesteps the producer entirely. Library-first gate still applies.
Out of Scope
jax_zero_contourto a hard dep (alternative; not preferred per the prompt).autogalaxy/util/plot_utils.py:337-360— has its own ImportError handling for the plot helper, not reached by default latents._critical_curve_list_via_zero_contourto the marching-squares sibling — its callers are plot utilities;[](no curves drawn) is acceptable.Performance Note
The NumPy fallback (
einstein_radius_from) uses a dense ~250k-evaluation grid vs. the JIT'd contour tracer. Cost is post-fit, runs once per converged search, so order-of-seconds is acceptable. The fallback warning makes the cost discoverable to the user.Original Prompt
Click to expand starting prompt
Optional-dep latents should soft-fail to NaN, not re-raise ModuleNotFoundError
Context
Discovered while validating the PyAutoLens #557 magzero fix on the
first-class A100 search profile (
autolens_profilingjob 322552). Themagzero
_mujylatents now soft-fail correctly, but a second post-fitlatent crashed the same metric-JSON write path with a parallel bug:
The raise lives at
PyAutoGalaxy:autogalaxy/operate/lens_calc.py:1566-1572:This is the same default-config-crashes-by-default pattern as the
magzero family (fixed in PyAutoLens #557 with
_maybe_magzero_warn),just keyed on an optional dependency instead of an Analysis kwarg.
The bug
effective_einstein_radiusis enabled by default inPyAutoLens:autolens/config/latent.yaml, and is computed during everySearchUpdater._compute_latent_samplescall as part of the standardpost-fit pipeline. When
jax_zero_contourisn't installed (it isn'tin PyAuto's default
pyproject.tomldeps — it's a JAX-only optionalextra), every
search.fit()that reaches the post-fit step crasheswith the message above. The search itself converged; the metric-JSON
write never happens.
This is the same failure shape as
PyAutoPrompt/autolens/magzero_required_latents_crash_search.md(landed as PyAutoLens #557). The PyAutoLens fix only addressed the
magzero family of latents — the optional-dependency family was not
touched, and surfaces with the same symptoms.
Desired fix
Mirror PyAutoLens #557's
_maybe_magzero_warn: replace the re-raisewith a per-process warning +
xp.nanreturn. The "module is missing"case is structurally identical to the "user kwarg is missing" case —
both mean the latent value is unknown, and the right behaviour is
return-NaN-and-warn, not kill the search.
Sketch (in
PyAutoGalaxy:autogalaxy/operate/lens_calc.pyor whereverthe latent function lives —
PyAutoLens:autolens/analysis/latent.py:225calls into this):
einstein_radius_jit_from(and any sibling that does the sametry/except ModuleNotFoundError → re-raisepattern) becomes:Test plan
test_autolens/analysis/test_latent.py's magzerocases): mock
jax_zero_contourimport to raise, assert the latentreturns NaN and emits one warning per process.
jax_zero_contourinstalled: constructAnalysisImaging(..., use_jax=True)with defaultconfig/latent.yaml, runaf.Nautilus(...).fit(...)inPYAUTO_TEST_MODE=1. Confirm search completes, JSON write happens,latent.csvhas NaN values for the affected columns.autolens_profiling/searches/nautilus/imaging/mge.pyon a clean venv (no
jax_zero_contour) and confirm no crash.Affected callers / interaction surface
jax_zero_contour— the HPCPyAutoNSSvenv on Euclid SAAS inparticular, where
jax_zero_contourwas not installed at venvcreation time (added manually post-hoc to unblock job 322552).
PyAutoGalaxy:autogalaxy/operate/lens_calc.pythat do the same
try / except ModuleNotFoundError → raisepattern:grep for
raise ModuleNotFoundErrorin lens_calc.py and adjacentmodules; any that gate optional-dep imports for default-on latents
should switch to the soft-fail pattern.
Why not just add
jax_zero_contouras a hard dep?That's the pragmatic short-term fix (it's what unblocked job 322552
on the HPC). But it makes a JAX-only utility a required dep of every
PyAutoGalaxy install, including non-JAX numpy-only users — wider blast
radius than this fix needs. Soft-fail keeps the optional dep optional
while making default configs runnable.
Out of scope
jax_zero_contourto a hard dep (alternative; not thepreferred fix).
the latent-computation surface where default configs hit them.
covered; this is a parallel-but-independent surface.
Cross-references
pattern this prompt proposes for optional deps.
the upstream prompt for #557.
completed (64,200 evals, log_Z=+31690.49) but post-fit latent
crashed before metric JSON write.