From 10fb7313387c80904e97136433f09aa5238f68f1 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Fri, 30 Oct 2026 15:19:34 +0000 Subject: [PATCH] feat: magnification latent from raw fluxes, no magzero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit magnification = total_lensed_source_flux / total_source_flux (was the µJy latents, NaN without magzero; identical for positive fluxes since the µJy factor cancels). Stays default false (opt-in); latent.yaml documents it and why per-image magnifications are not latents. Closes #812 Co-Authored-By: Claude Opus 5.5 --- autolens/analysis/latent.py | 18 ++++++++++------ autolens/config/latent.yaml | 12 +++++------ test_autolens/analysis/test_latent.py | 31 ++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/autolens/analysis/latent.py b/autolens/analysis/latent.py index fe53c611a..576307cc7 100644 --- a/autolens/analysis/latent.py +++ b/autolens/analysis/latent.py @@ -387,12 +387,17 @@ def total_source_flux_mujy(fit, magzero, xp=np): ) -def magnification(fit, magzero, xp=np): +def magnification(fit, magzero=None, xp=np): """ Ratio of image-plane to source-plane source flux — the integrated magnification implied by the lens model and the source's light. - The denominator is :func:`total_source_flux_mujy`: light-profile flux + Computed from the raw-flux latents :func:`total_lensed_source_flux` and + :func:`total_source_flux`, so it needs no ``magzero`` (the ratio is + dimensionless; a µJy conversion would only multiply both fluxes by the + same factor). + + The denominator is :func:`total_source_flux`: light-profile flux plus, for a pixelized source, the inversion's reconstruction integrated with its ``areas_for_magnification`` (barycentric dual areas for a Delaunay mesh — PyAutoArray#524 — transformed cell areas for a rectangular @@ -402,12 +407,11 @@ def magnification(fit, magzero, xp=np): scale. A source whose only light model is a ``Pixelization`` therefore has a finite magnification rather than an infinite one (PyAutoLens#726). - ``magzero`` is accepted but unused (the µJy conversions cancel in the - ratio). It's still required in the signature so the dispatcher can - pass a uniform context dict to every latent function. + ``magzero`` is accepted but unused, so the dispatcher can pass a uniform + context dict to every latent function. """ - lensed = total_lensed_source_flux_mujy(fit=fit, magzero=magzero, xp=xp) - intrinsic = total_source_flux_mujy(fit=fit, magzero=magzero, xp=xp) + lensed = total_lensed_source_flux(fit=fit, xp=xp) + intrinsic = total_source_flux(fit=fit, xp=xp) return lensed / intrinsic diff --git a/autolens/config/latent.yaml b/autolens/config/latent.yaml index b67da377b..5fdfa38bd 100644 --- a/autolens/config/latent.yaml +++ b/autolens/config/latent.yaml @@ -51,12 +51,12 @@ total_lensed_source_flux_mujy: false total_source_flux_mujy: false # `magnification` — ratio of image-plane lensed source flux to source-plane -# intrinsic source flux. Dimensionless; `magzero` is accepted but unused. -# Default `false` because it routes through the `_mujy` latents internally -# (the µJy conversions cancel in the ratio) — to flip on, also flip on -# `total_lensed_source_flux_mujy` and `total_source_flux_mujy` and supply -# a `magzero`. A follow-up could rewire `magnification` to the raw-flux -# latents so it's universally enable-able. +# intrinsic source flux, from the raw-flux latents above. Dimensionless and +# needs no `magzero`. Works for light-profile and pixelized sources. Default +# `false` (opt-in): set it `true` to get its posterior from the fit's samples. +# Magnifications at individual image positions are not latents, because the +# number of images differs between lenses; compute them from posterior draws +# as the `source_science.py` examples do. magnification: false # `effective_einstein_radius` — effective Einstein radius in arcseconds, diff --git a/test_autolens/analysis/test_latent.py b/test_autolens/analysis/test_latent.py index f353153cb..036f6693a 100644 --- a/test_autolens/analysis/test_latent.py +++ b/test_autolens/analysis/test_latent.py @@ -242,7 +242,6 @@ def test_maybe_magzero_warn_logs_only_once_per_name(caplog): def test_magnification_is_lensed_over_intrinsic(): # Image-plane lensed source flux = 10, source-plane intrinsic = 2. - # The µJy conversions cancel in the ratio, so the result is 5.0. class _FakeSourceGalaxy: # Plain class so instances are hashable (used as galaxy_image_dict key). @@ -974,3 +973,33 @@ def test_latent_lens_variables_traces_under_jit_with_model_assertion( assert np.allclose( [float(v) for v in values], [float(v) for v in values_np], rtol=1e-6 ) + + +def test_magnification_needs_no_magzero(): + """The magnification latent is the raw-flux ratio, so it is finite without a + ``magzero`` and equal to the ratio of the raw-flux latents.""" + from autolens.analysis.latent import ( + total_lensed_source_flux, + total_source_flux, + ) + + class _FakeSourceGalaxy: + def image_2d_from(self, grid, xp=np): + return SimpleNamespace(array=np.array([2.0])) + + source = _FakeSourceGalaxy() + galaxies_namespace = SimpleNamespace(galaxies=[object(), source]) + fit = SimpleNamespace( + tracer=galaxies_namespace, + tracer_linear_light_profiles_to_light_profiles=galaxies_namespace, + galaxy_image_dict={source: SimpleNamespace(array=np.array([10.0]))}, + dataset=SimpleNamespace(grids=SimpleNamespace(lp=object())), + ) + + value = magnification(fit=fit, magzero=None) + + assert np.isfinite(value) + assert value == pytest.approx( + total_lensed_source_flux(fit=fit) / total_source_flux(fit=fit) + ) + assert magnification(fit=fit, magzero=25.0) == pytest.approx(value)