You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Builds on the PyAutoGalaxy latent infrastructure shipped in PyAutoGalaxy #441 by adding the lensing-specific latents that need a Tracer. PyAutoLens's AnalysisImaging does NOT inherit the PyAutoGalaxy LATENT_KEYS / compute_latent_variables via MRO — they live on parallel imaging Analysis classes (autolens.imaging.model.AnalysisImaging vs autogalaxy.imaging.model.AnalysisImaging), not the shared dataset base — so this PR adds an explicit dispatch layer in PyAutoLens with its own registry.
After this ships, sub-prompt #3 (euclid migration) can drop the bespoke LATENT_KEYS + compute_latent_variables in euclid_strong_lens_modeling_pipeline/util.py:306-490 and inherit the library catalogue.
Plan
Add autolens/analysis/latent.py for the one tracer-derived latent (effective_einstein_radius, delegating to PyAutoGalaxy's LensCalc.einstein_radius_jit_from so its closure cache is reused).
Add autolens/imaging/model/latent.py for image-derived lensing latents — total_lens_flux_mujy, total_source_flux_mujy, total_lensed_source_flux_mujy, magnification. Helpers ab_mag_via_flux_from / flux_mujy_via_ab_mag_from are imported from PyAutoGalaxy (no duplication).
Wire AnalysisImaging with a LATENT_KEYS@property reading from conf.instance["latent"] and a compute_latent_variables(parameters, model) method dispatching through a local LATENT_FUNCTIONS registry. PyAutoLens defines its own registry (no re-export of PyAutoGalaxy's total_galaxy_0_flux_mujy — the lens-light equivalent is total_lens_flux_mujy, named in the lensing vocabulary).
Preserve LATENT_BATCH_MODE = "jit" (inherited via autogalaxy/analysis/analysis/dataset.py:28 — ZeroSolver in the Einstein-radius helper is vmap-incompatible).
Unit tests for each latent function + AnalysisImaging end-to-end + a spy/mock test confirming effective_einstein_radius actually calls LensCalc.einstein_radius_jit_from.
Cache the (closure, solver) pair properly per memory feedback_jax_closure_cache_busts. The cache lives inside LensCalc (lines 1580-1586) — just don't construct a fresh LensCalc each call within the same Analysis.
Create autolens/imaging/model/latent.py with the four image-derived latents. Each function takes (fit, magzero, xp=np). Use from autogalaxy.imaging.model.latent import ab_mag_via_flux_from, flux_mujy_via_ab_mag_from.
total_lens_flux_mujy(fit, magzero, xp=np) — fit.galaxy_image_dict[fit.galaxies[0]], sum, → AB mag → µJy. Raises ValueError if magzero is None (memory feedback_no_silent_guards).
total_lensed_source_flux_mujy(fit, magzero, xp=np) — fit.galaxy_image_dict[fit.tracer.galaxies[-1]] (image-plane image of source after lensing).
magnification(fit, magzero, xp=np) — ratio of total_lensed_source_flux_mujy / total_source_flux_mujy. magzero param accepted but unused; dimensionless ratio.
Create autolens/config/latent.yaml — all 5 keys default false. Each key gets a comment explaining what it computes and that it requires magzero (except magnification which is dimensionless).
Build a single LATENT_FUNCTIONS registry: {**lens_analysis_latents, **lens_imaging_latents} where the two source modules each export a per-module dict.
LATENT_KEYS@property: reads conf.instance["latent"] (lens-side latent.yaml), filters to enabled keys present in LATENT_FUNCTIONS.
compute_latent_variables(parameters, model): raises NotImplementedError when LATENT_KEYS is empty; otherwise dispatches through LATENT_FUNCTIONS[k](**context) for k in LATENT_KEYS, returning a tuple aligned to keys. Context dict: {"fit": fit_from(instance_from_vector(parameters)), "magzero": self.kwargs.get("magzero"), "xp": self._xp}.
Unit tests at test_autolens/analysis/test_latent.py and test_autolens/imaging/model/test_latent.py:
Per-latent against known toy models (numpy only — memory feedback_no_jax_in_unit_tests).
effective_einstein_radius spy test asserting it calls into LensCalc.einstein_radius_jit_from / einstein_radius_from (mock or monkeypatch).
On/off filtering via explicit yaml_config dict.
Order preservation across registry → LATENT_KEYS → return tuple.
End-to-end through AnalysisImaging.compute_latent_variables with at least one key enabled in the test config mirror.
compute_latent_variables raises NotImplementedError when LATENT_KEYS == [].
test_autolens/config/latent.yaml mirror with at least one latent enabled so the end-to-end test exercises the dispatch path.
Key Files
autolens/analysis/latent.py — new module (effective_einstein_radius)
autolens/imaging/model/latent.py — new module (4 image-derived latents)
Reuse LensCalc.einstein_radius_jit_from rather than reimplementing the ZeroSolver loop.
Original Prompt
Click to expand starting prompt
Add first-class lensing latent-variable modules to PyAutoLens
Context
Parent epic: PyAutoPrompt/z_features/latent_refactor.md.
Depends on the PyAutoGalaxy spine in autogalaxy/latent_module.md (shipped as PyAutoGalaxy #441).
PyAutoGalaxy gets galaxy-level / image-flux latents. This task adds the lensing-specific latents that need a Tracer (mass model + sources): magnification, effective Einstein radius, lensed source flux, total source flux. Today these live in euclid_strong_lens_modeling_pipeline/util.py:306-490 as part of a bespoke AnalysisImaging subclass. After this task lands, the euclid pipeline (sub-prompt #3) can drop its custom subclass and inherit the curated set.
Task
Create autolens/analysis/latent.py — tracer-derived, dataset-agnostic latents. effective_einstein_radius must delegate to LensCalc.einstein_radius_jit_from() at PyAutoGalaxy/autogalaxy/operate/lens_calc.py:1520.
Create autolens/imaging/model/latent.py — lensing imaging-derived latents that need a FitImaging: total_lens_flux_mujy, total_lensed_source_flux_mujy, total_source_flux_mujy, magnification. Use ab_mag_via_flux_from and flux_mujy_via_ab_mag_from imported from PyAutoGalaxy (shipped in Drop Binder URLs, pin Colab URLs to workspace tag 2026.4.13.6 #441).
Wire AnalysisImaging (autolens/imaging/model/analysis.py): read config/latent.yaml, build LATENT_KEYS via @property, dispatch compute_latent_variables through a local LATENT_FUNCTIONS registry composed from the analysis-latent module and the imaging-latent module. Preserve LATENT_BATCH_MODE = "jit" (inherited via autogalaxy/analysis/analysis/dataset.py:28). Raise NotImplementedError when LATENT_KEYS == [] so autofit skips cleanly.
Unit tests at test_autolens/analysis/test_latent.py and test_autolens/imaging/model/test_latent.py. Spy/mock test that effective_einstein_radius calls into LensCalc.einstein_radius_jit_from. No JAX in unit tests.
Where to look
PyAutoFit hook (do not modify): autofit/non_linear/analysis/analysis.py:34, 170, 285.
Einstein radius JAX helper: PyAutoGalaxy/autogalaxy/operate/lens_calc.py:1520-1537 and the closure cache at 1580-1586.
Lens-side latent is named total_lens_flux_mujy (not total_galaxy_0_flux_mujy from PyAutoGalaxy). Per-user direction during /start_dev — PyAutoLens uses lensing-vocabulary names, doesn't re-export PyAutoGalaxy's registry.
Per memory feedback_jax_closure_cache_busts: when delegating to einstein_radius_jit_from, ensure the same (closure, solver) pair is reused across calls.
Per memory feedback_no_silent_guards: if magzero is missing when an enabled latent needs it, raise loudly.
Overview
Builds on the PyAutoGalaxy latent infrastructure shipped in PyAutoGalaxy #441 by adding the lensing-specific latents that need a
Tracer. PyAutoLens'sAnalysisImagingdoes NOT inherit the PyAutoGalaxyLATENT_KEYS/compute_latent_variablesvia MRO — they live on parallel imaging Analysis classes (autolens.imaging.model.AnalysisImagingvsautogalaxy.imaging.model.AnalysisImaging), not the shared dataset base — so this PR adds an explicit dispatch layer in PyAutoLens with its own registry.After this ships, sub-prompt #3 (euclid migration) can drop the bespoke
LATENT_KEYS+compute_latent_variablesineuclid_strong_lens_modeling_pipeline/util.py:306-490and inherit the library catalogue.Plan
autolens/analysis/latent.pyfor the one tracer-derived latent (effective_einstein_radius, delegating to PyAutoGalaxy'sLensCalc.einstein_radius_jit_fromso its closure cache is reused).autolens/imaging/model/latent.pyfor image-derived lensing latents —total_lens_flux_mujy,total_source_flux_mujy,total_lensed_source_flux_mujy,magnification. Helpersab_mag_via_flux_from/flux_mujy_via_ab_mag_fromare imported from PyAutoGalaxy (no duplication).autolens/config/latent.yamlwith all 5 keys default OFF (same reasoning as Drop Binder URLs, pin Colab URLs to workspace tag 2026.4.13.6 #441:magzerorequirement +compute_latent_samplesruns on every fit, so default-on would crash existing users).AnalysisImagingwith aLATENT_KEYS@propertyreading fromconf.instance["latent"]and acompute_latent_variables(parameters, model)method dispatching through a localLATENT_FUNCTIONSregistry. PyAutoLens defines its own registry (no re-export of PyAutoGalaxy'stotal_galaxy_0_flux_mujy— the lens-light equivalent istotal_lens_flux_mujy, named in the lensing vocabulary).NotImplementedError-when-empty short-circuit as Drop Binder URLs, pin Colab URLs to workspace tag 2026.4.13.6 #441 so autofit'sexcept NotImplementedError: return Noneskips the latent pipeline cleanly.LATENT_BATCH_MODE = "jit"(inherited viaautogalaxy/analysis/analysis/dataset.py:28—ZeroSolverin the Einstein-radius helper is vmap-incompatible).effective_einstein_radiusactually callsLensCalc.einstein_radius_jit_from.Detailed implementation plan
Affected Repositories
Work Classification
Library
Branch Survey
Suggested branch:
feature/latent-module-autolensWorktree root:
~/Code/PyAutoLabs-wt/latent-module-autolens/(created later by/start_library)Implementation Steps
Create
autolens/analysis/latent.pywitheffective_einstein_radius(fit, magzero, xp=np):LensCalc.from_mass_obj(fit.tracer).einstein_radius_jit_from(init_guess=fixed_fan)wherefixed_fan = jnp.array([[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]])(matches euclidutil.py:502-509).LensCalc.from_mass_obj(fit.tracer).einstein_radius_from(grid=fit.dataset.grids.lp).xp.nanonValueError/AttributeError.(closure, solver)pair properly per memoryfeedback_jax_closure_cache_busts. The cache lives insideLensCalc(lines 1580-1586) — just don't construct a freshLensCalceach call within the same Analysis.Create
autolens/imaging/model/latent.pywith the four image-derived latents. Each function takes(fit, magzero, xp=np). Usefrom autogalaxy.imaging.model.latent import ab_mag_via_flux_from, flux_mujy_via_ab_mag_from.total_lens_flux_mujy(fit, magzero, xp=np)—fit.galaxy_image_dict[fit.galaxies[0]], sum, → AB mag → µJy. RaisesValueErrorifmagzerois None (memoryfeedback_no_silent_guards).total_lensed_source_flux_mujy(fit, magzero, xp=np)—fit.galaxy_image_dict[fit.tracer.galaxies[-1]](image-plane image of source after lensing).total_source_flux_mujy(fit, magzero, xp=np)—fit.tracer.galaxies[-1].image_2d_from(grid=fit.dataset.grids.lp)(source-plane intrinsic image).magnification(fit, magzero, xp=np)— ratio oftotal_lensed_source_flux_mujy / total_source_flux_mujy. magzero param accepted but unused; dimensionless ratio.Create
autolens/config/latent.yaml— all 5 keys defaultfalse. Each key gets a comment explaining what it computes and that it requiresmagzero(exceptmagnificationwhich is dimensionless).Wire
AnalysisImaging(autolens/imaging/model/analysis.py):LATENT_FUNCTIONSregistry:{**lens_analysis_latents, **lens_imaging_latents}where the two source modules each export a per-module dict.LATENT_KEYS@property: readsconf.instance["latent"](lens-side latent.yaml), filters to enabled keys present inLATENT_FUNCTIONS.compute_latent_variables(parameters, model): raisesNotImplementedErrorwhenLATENT_KEYSis empty; otherwise dispatches throughLATENT_FUNCTIONS[k](**context)forkinLATENT_KEYS, returning a tuple aligned to keys. Context dict:{"fit": fit_from(instance_from_vector(parameters)), "magzero": self.kwargs.get("magzero"), "xp": self._xp}.Unit tests at
test_autolens/analysis/test_latent.pyandtest_autolens/imaging/model/test_latent.py:feedback_no_jax_in_unit_tests).effective_einstein_radiusspy test asserting it calls intoLensCalc.einstein_radius_jit_from/einstein_radius_from(mock or monkeypatch).yaml_configdict.AnalysisImaging.compute_latent_variableswith at least one key enabled in the test config mirror.compute_latent_variablesraisesNotImplementedErrorwhenLATENT_KEYS == [].test_autolens/config/latent.yamlmirror with at least one latent enabled so the end-to-end test exercises the dispatch path.Key Files
autolens/analysis/latent.py— new module (effective_einstein_radius)autolens/imaging/model/latent.py— new module (4 image-derived latents)autolens/config/latent.yaml— new configautolens/imaging/model/analysis.py— wireAnalysisImaging(parallel to PyAutoGalaxy's PR Drop Binder URLs, pin Colab URLs to workspace tag 2026.4.13.6 #441 pattern)test_autolens/config/latent.yaml— test mirrortest_autolens/analysis/test_latent.py— new teststest_autolens/imaging/model/test_latent.py— new testsConstraints to preserve
LATENT_BATCH_MODE = "jit"inherited viaautogalaxy/analysis/analysis/dataset.py:28— do not change.feedback_autoconf_lowercases_yaml_keys).magzeroregression that Drop Binder URLs, pin Colab URLs to workspace tag 2026.4.13.6 #441 caught.LensCalc.einstein_radius_jit_fromrather than reimplementing theZeroSolverloop.Original Prompt
Click to expand starting prompt
Add first-class lensing latent-variable modules to PyAutoLens
Context
Parent epic:
PyAutoPrompt/z_features/latent_refactor.md.Depends on the PyAutoGalaxy spine in
autogalaxy/latent_module.md(shipped as PyAutoGalaxy #441).PyAutoGalaxy gets galaxy-level / image-flux latents. This task adds the lensing-specific latents that need a
Tracer(mass model + sources): magnification, effective Einstein radius, lensed source flux, total source flux. Today these live ineuclid_strong_lens_modeling_pipeline/util.py:306-490as part of a bespokeAnalysisImagingsubclass. After this task lands, the euclid pipeline (sub-prompt #3) can drop its custom subclass and inherit the curated set.Task
Create
autolens/analysis/latent.py— tracer-derived, dataset-agnostic latents.effective_einstein_radiusmust delegate toLensCalc.einstein_radius_jit_from()atPyAutoGalaxy/autogalaxy/operate/lens_calc.py:1520.Create
autolens/imaging/model/latent.py— lensing imaging-derived latents that need aFitImaging:total_lens_flux_mujy,total_lensed_source_flux_mujy,total_source_flux_mujy,magnification. Useab_mag_via_flux_fromandflux_mujy_via_ab_mag_fromimported from PyAutoGalaxy (shipped in Drop Binder URLs, pin Colab URLs to workspace tag 2026.4.13.6 #441).Create
autolens/config/latent.yaml— flat dict oflatent_key: bool. Mirrorautolens/config/output.yaml. Per Drop Binder URLs, pin Colab URLs to workspace tag 2026.4.13.6 #441's regression learning, default everything OFF.Wire
AnalysisImaging(autolens/imaging/model/analysis.py): readconfig/latent.yaml, buildLATENT_KEYSvia@property, dispatchcompute_latent_variablesthrough a localLATENT_FUNCTIONSregistry composed from the analysis-latent module and the imaging-latent module. PreserveLATENT_BATCH_MODE = "jit"(inherited viaautogalaxy/analysis/analysis/dataset.py:28). RaiseNotImplementedErrorwhenLATENT_KEYS == []so autofit skips cleanly.Unit tests at
test_autolens/analysis/test_latent.pyandtest_autolens/imaging/model/test_latent.py. Spy/mock test thateffective_einstein_radiuscalls intoLensCalc.einstein_radius_jit_from. No JAX in unit tests.Where to look
autofit/non_linear/analysis/analysis.py:34, 170, 285.PyAutoGalaxy/autogalaxy/operate/lens_calc.py:1520-1537and the closure cache at 1580-1586.LATENT_BATCH_MODEconstraint:PyAutoGalaxy/autogalaxy/analysis/analysis/dataset.py:28.euclid_strong_lens_modeling_pipeline/util.py:306-490.PyAutoGalaxy/autogalaxy/imaging/model/latent.py,autogalaxy/config/latent.yaml,autogalaxy/imaging/model/analysis.pyLATENT_KEYSproperty andcompute_latent_variablesmethod.Suggested branch
feature/latent-module-autolensNotes
total_lens_flux_mujy(nottotal_galaxy_0_flux_mujyfrom PyAutoGalaxy). Per-user direction during /start_dev — PyAutoLens uses lensing-vocabulary names, doesn't re-export PyAutoGalaxy's registry.magzeroregression that PyAutoGalaxy Drop Binder URLs, pin Colab URLs to workspace tag 2026.4.13.6 #441 caught.feedback_jax_closure_cache_busts: when delegating toeinstein_radius_jit_from, ensure the same(closure, solver)pair is reused across calls.feedback_no_silent_guards: ifmagzerois missing when an enabled latent needs it, raise loudly.