Skip to content

feat: al.twins model builder and posterior recovery table (LensTwin 4a) - #811

Merged
Jammy2211 merged 2 commits into
mainfrom
feature/lenstwin-p4a-twin-fit-model
Oct 29, 2026
Merged

Jammy2211 merged 2 commits into
mainfrom
feature/lenstwin-p4a-twin-fit-model

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

HOLD — draft; do not merge until the human lifts the hold (human instruction 2026-10-29)

Closes #810

What it adds

al.twins.stellar_dark_model_from(twin, galaxy=None, *, source_model, free_scale_radius=True, free_inner_slope=None, free_shear=True, priors=None) -> af.Collection

This builds the model that fits a stellar-plus-dark twin with the same parameterization it was made with, so the model is not misspecified. It is the twin-based version of ag.analysis.chaining_util.mass_light_dark_basis_from. It reads the last ReplaceTotalMassWithStellarPlusDark record (requested["light"], requested["dark_name"] and truth) and the twin's tracer. If the twin has no such record, it raises ValueError.

  • The lens light basis becomes a Basis of af.Model(lmp.Gaussian). Each member's centre, ell_comps, sigma and intensity are fixed to the twin's values, and all members share one mass_to_light_ratio prior object.

  • The dark profile is af.Model(<the twin's dark class>), with its centre fixed to truth["dark_centre"]. kappa_s is free. scale_radius is free, or fixed to the truth when free_scale_radius=False. inner_slope is free only for classes that take it as a constructor argument (e.g. gNFWSph, not NFWSph).

  • Shear: an ExternalShear galaxy attribute becomes af.Model(ExternalShear). A tracer MassField is mirrored as fields=af.Model(al.MassField, redshift=<field z>, shear=af.Model(al.mp.ExternalShear)), which is the workspace and DR1 layout. Several fields give af.Collection(field_0=..., ...). With free_shear=False the shear is fixed to the twin's values.

  • source_model replaces the galaxy named "source" (otherwise the single highest-redshift galaxy). It can also be a {name: model} dict. All other galaxies stay as fixed instances under their names.

  • Default priors are broad, fixed and never derived from the truth (DEFAULT_PRIORS):

    • M/L: LogUniform over [1e-6, 1e6]
    • kappa_s: LogUniform over [1e-4, 10]
    • scale_radius: Uniform over [0.1, 30] arcsec
    • inner_slope: Uniform over [0, 2]

    You can override any of them with priors={truth_key: prior}.

al.twins.truth_paths_from(model, galaxy) maps each truth key (mass_to_light_ratio, kappa_s, scale_radius_arcsec, inner_slope) to the path of its free model parameter. The model builder and the recovery table both use this one map.

al.twins.recovery_table_from(samples, truth, paths=None, sigma=3.0) -> RecoveryTable

  • Rows hold name, path, truth, median, lower, upper, inside. The table has all_inside, to_csv, a readable __str__ and table["kappa_s"] lookup.
  • truth can be a dict, a twin.json path or the directory that holds it. From the file it uses the last stellar-dark entry of the "truth" block.
  • Medians come from median_pdf(as_instance=False) and bounds from values_at_sigma(sigma, as_instance=False). Columns are looked up through samples.paths, which groups the paths of tied priors, so the path of any member of the shared M/L finds its column.
  • A SamplesSummary is accepted for sigma 1 or 3.

Tests

  • test_autolens/twins/test_model.py (10 tests):
    • layout and prior counts
    • the instance at the truth reproduces the twin lens convergence and deflections to 1e-10
    • gNFW with a free and a fixed inner slope
    • shear as a MassField field, checked through AnalysisImaging.tracer_via_instance_from
    • overrides, the no-record error and the path map
  • test_autolens/twins/test_recovery.py (5 tests):
    • synthetic SamplesPDF with the truth inside and outside the bounds
    • lookup through the path of a tied member
    • truth read from twin.json and from a directory
    • SamplesSummary
    • an end-to-end run with PYAUTO_TEST_MODE=1: simulate, build the model, run real Nautilus on a reduced budget, write the table. It takes about 25 s with figure saving patched out (about 115 s otherwise).
  • pytest test_autolens/twins -n 4: 54 passed
  • Full PyAutoLens suite: 1013 passed, 1 xfailed

🤖 Generated with Claude Code

Add al.twins.model.stellar_dark_model_from, which builds the no-misspecification
inference model of a ReplaceTotalMassWithStellarPlusDark twin (fixed MGE light as
lmp.Gaussian with one shared mass_to_light_ratio, dark halo centred on the light,
free shear as a galaxy attribute or MassField field, broad fixed priors), and
al.twins.recovery.recovery_table_from, which compares posterior medians and sigma
bounds with the twin's truth. truth_paths_from is the single truth-key -> model-path
map both use.

Closes #810

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jammy2211 Jammy2211 added the pending-release PR queued for the next release build label Oct 29, 2026
ReplaceTotalMassWithStellarPlusDark only recognised bare ExternalShear tracer
fields; DR1-style tracers hold a MassField with a .shear component, so
keep_shear=False neither removed nor recorded it. Shear components inside a
MassField are now found by attribute, removed from the copy (the field is
dropped when no mass component remains) and recorded as fields.<i>.<key>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jammy2211
Jammy2211 marked this pull request as ready for review October 29, 2026 21:31
@Jammy2211
Jammy2211 merged commit bf18d5c into main Oct 29, 2026
4 checks passed
@Jammy2211
Jammy2211 deleted the feature/lenstwin-p4a-twin-fit-model branch October 29, 2026 21:31
@pyautolabs-bot pyautolabs-bot Bot removed the pending-release PR queued for the next release build label Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: al.twins model builder and posterior recovery table (LensTwin 4a)

1 participant