diff --git a/scripts/imaging/jax_likelihood/mge_group.py b/scripts/imaging/jax_likelihood/mge_group.py index e9dba2a8..6fab3652 100644 --- a/scripts/imaging/jax_likelihood/mge_group.py +++ b/scripts/imaging/jax_likelihood/mge_group.py @@ -162,7 +162,16 @@ profile_list=bulge_gaussian_list, ) +# The main mass is anchored near the `simulator/simple.py` truth (einstein_radius=1.6) with a +# prior whose median is that truth. At the config-default prior median (einstein_radius=4.0) the +# positive-only solver zeroes the source's solved intensities and the vmap literal below is +# bit-identical for ANY source-plane mass structure (audit 2026-08-06, autolens_workspace_test#253) +# — the mass-sensitivity assertion after the literal only works on a retained source. + mass = af.Model(al.mp.Isothermal) +mass.centre = (0.0, 0.0) +mass.ell_comps = al.convert.ell_comps_from(axis_ratio=0.8, angle=45.0) +mass.einstein_radius = af.UniformPrior(lower_limit=1.1, upper_limit=2.1) shear = af.Model(al.mp.ExternalShear) @@ -313,12 +322,46 @@ np.testing.assert_allclose( np.array(result), - -29060.21511937, + -28830.547173, rtol=1e-4, err_msg="mge_group: JAX vmap likelihood mismatch", ) +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "imaging/mge_group: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 9.0, ( + f"imaging/mge_group: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + + """ __Path A: jit-wrap ``analysis.fit_from``__ """ diff --git a/scripts/interferometer/datacube/delaunay.py b/scripts/interferometer/datacube/delaunay.py index 1902c135..27d06789 100644 --- a/scripts/interferometer/datacube/delaunay.py +++ b/scripts/interferometer/datacube/delaunay.py @@ -257,6 +257,40 @@ ) +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "interferometer/datacube/delaunay: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 0.1, ( + f"interferometer/datacube/delaunay: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + + """ __Path A: jit-wrap parameter-vector entry point__ diff --git a/scripts/interferometer/datacube/rectangular.py b/scripts/interferometer/datacube/rectangular.py index 9173335a..520badff 100644 --- a/scripts/interferometer/datacube/rectangular.py +++ b/scripts/interferometer/datacube/rectangular.py @@ -239,6 +239,40 @@ ) +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "interferometer/datacube/rectangular: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 0.03, ( + f"interferometer/datacube/rectangular: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + + """ __Path A: jit-wrap parameter-vector entry point__ diff --git a/scripts/interferometer/jax_likelihood/delaunay.py b/scripts/interferometer/jax_likelihood/delaunay.py index 7b875ab3..bdf21f84 100644 --- a/scripts/interferometer/jax_likelihood/delaunay.py +++ b/scripts/interferometer/jax_likelihood/delaunay.py @@ -243,6 +243,40 @@ class in **PyAutoFit**, which pairs the model with likelihood. err_msg="interferometer/delaunay: JAX vmap likelihood mismatch", ) + +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "interferometer/delaunay: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 0.03, ( + f"interferometer/delaunay: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + # The sparse inversion path must obey the same lane-isolation contract as # imaging. Fitness sees the raw NaN after the forward pass and converts only # that lane to its configured resample value. diff --git a/scripts/interferometer/jax_likelihood/delaunay_mge.py b/scripts/interferometer/jax_likelihood/delaunay_mge.py index f7bfd62c..668dd5c0 100644 --- a/scripts/interferometer/jax_likelihood/delaunay_mge.py +++ b/scripts/interferometer/jax_likelihood/delaunay_mge.py @@ -252,6 +252,40 @@ class in **PyAutoFit**, which pairs the model with likelihood. ) +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "interferometer/delaunay_mge: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 0.007, ( + f"interferometer/delaunay_mge: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + + """ __Path A: jit-wrap ``analysis.fit_from``__ """ diff --git a/scripts/interferometer/jax_likelihood/mge_group.py b/scripts/interferometer/jax_likelihood/mge_group.py index 7675d9b7..f0c60e0b 100644 --- a/scripts/interferometer/jax_likelihood/mge_group.py +++ b/scripts/interferometer/jax_likelihood/mge_group.py @@ -133,6 +133,40 @@ err_msg="interferometer/mge_group: JAX vmap likelihood mismatch", ) + +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "interferometer/mge_group: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 0.005, ( + f"interferometer/mge_group: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + print("interferometer/mge_group.py checks passed.") diff --git a/scripts/interferometer/jax_likelihood/rectangular.py b/scripts/interferometer/jax_likelihood/rectangular.py index 78ac97b7..b1f34690 100644 --- a/scripts/interferometer/jax_likelihood/rectangular.py +++ b/scripts/interferometer/jax_likelihood/rectangular.py @@ -277,6 +277,40 @@ ) +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "interferometer/rectangular: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 0.008, ( + f"interferometer/rectangular: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + + """ __Path A: jit-wrap ``analysis.fit_from``__ """ diff --git a/scripts/interferometer/jax_likelihood/rectangular_dspl.py b/scripts/interferometer/jax_likelihood/rectangular_dspl.py index f062f8bd..b897ecb2 100644 --- a/scripts/interferometer/jax_likelihood/rectangular_dspl.py +++ b/scripts/interferometer/jax_likelihood/rectangular_dspl.py @@ -236,6 +236,40 @@ class in **PyAutoFit**, which pairs the model with likelihood. ) +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "interferometer/rectangular_dspl: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 0.009, ( + f"interferometer/rectangular_dspl: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + + """ __Path A: jit-wrap ``analysis.fit_from``__ """ diff --git a/scripts/interferometer/jax_likelihood/rectangular_mge.py b/scripts/interferometer/jax_likelihood/rectangular_mge.py index 002e996d..5925cb68 100644 --- a/scripts/interferometer/jax_likelihood/rectangular_mge.py +++ b/scripts/interferometer/jax_likelihood/rectangular_mge.py @@ -236,6 +236,40 @@ class in **PyAutoFit**, which pairs the model with likelihood. ) +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "interferometer/rectangular_mge: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 0.02, ( + f"interferometer/rectangular_mge: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + + """ __Path A: jit-wrap ``analysis.fit_from``__ """ diff --git a/scripts/interferometer/jax_likelihood/rectangular_sparse.py b/scripts/interferometer/jax_likelihood/rectangular_sparse.py index 03018c33..15d1ed4e 100644 --- a/scripts/interferometer/jax_likelihood/rectangular_sparse.py +++ b/scripts/interferometer/jax_likelihood/rectangular_sparse.py @@ -227,6 +227,40 @@ class in **PyAutoFit**, which pairs the model with likelihood. ) +""" +__Mass Sensitivity__ + +The literal above is evaluated at the model's prior medians, where a +5% change of +every lens mass parameter moves this likelihood by less than the literal's rtol +(audit 2026-08-06, autolens_workspace_test#253) — the literal alone would pass a +source-plane mass regression. This block pins mass sensitivity directly; the floor +is the audit-measured response divided by five (margin for platform drift). +""" +mass_indices = [ + i + for i, name in enumerate(model.model_component_and_parameter_names) + if ".mass." in name + and "centre" not in name + and "ell_comps" not in name + and "redshift" not in name +] +assert mass_indices, "interferometer/rectangular_sparse: no mass parameters found for sensitivity check" + +parameters_perturbed = np.array(model.physical_values_from_prior_medians) +for i in mass_indices: + parameters_perturbed[i] *= 1.05 + +ll_median = float(np.asarray(result).ravel()[0]) +ll_perturbed = float( + np.asarray(fitness._vmap(jnp.array(parameters_perturbed[None, :]))).ravel()[0] +) +assert abs(ll_perturbed - ll_median) > 0.008, ( + f"interferometer/rectangular_sparse: likelihood insensitive to a +5% lens-mass perturbation " + f"(median={ll_median}, perturbed={ll_perturbed}) — source-plane mass pipeline regression?" +) +print("PASS: mass-sensitivity floor exceeded.") + + """ __Path A: jit-wrap ``analysis.fit_from``__ """