Repository navigation
feat: al.twins model builder and posterior recovery table (LensTwin 4a) #810
Description
Overview
Phase 4a of the LensTwin epic. Phase 4's identifiability experiment fits simulated twins with the model they were simulated from (MGE stellar lmp basis with one free M/L, NFW/gNFW halo on the light, shear, MGE source). ag.chaining_util.mass_light_dark_basis_from builds that model only from a light-pipeline Result; a twin has an instance tracer and a flat truth instead. This adds the reusable builder and a truth-vs-posterior table so science scripts do not hand-roll either.
Draft PR only — human instruction 2026-10-29: nothing merges until the human says so.
Plan
- Add
al.twins.model.stellar_dark_model_from(twin, galaxy, *, source_model, …): anaf.Collectionbuilt from the twin's stellar-dark record — stellarBasisoflmp.Gaussianwith fixed centre/ell_comps/sigma/intensity and one shared free M/L; NFW (or gNFW with free inner slope) fixed to the light centre with free κ_s and r_s; free shear when present; other galaxies fixed; caller's source model. - Default priors broad, fixed, documented, never centred on the truth;
priorsoverride. - Add
al.twins.recovery.recovery_table_from(samples, truth, sigma=3.0): truth, median PDF, lower/upper bound and aninsideflag per parameter, mapped through one shared truth-key → model-path map. - Unit tests (truth instance reproduces the twin convergence; synthetic-samples recovery in/out) and a
PYAUTO_TEST_MODE=1end-to-end simulate → model → Nautilus → table.
Tier: judge — merge mode: human /prm (and held as a draft until the human lifts the hold)
Detailed implementation plan
Affected Repositories
- PyAutoLens (primary)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| lens/PyAutoLens | main | clean |
PyAutoLens is claimed by cosmos-web-ring-intro-visuals (its Lens PR #809 already merged; claim held for its assistant PR) — human approved concurrent work in a separate worktree 2026-10-29.
Suggested branch: feature/lenstwin-p4a-twin-fit-model
Implementation Steps
autolens/twins/model.py: locate the lastReplaceTotalMassWithStellarPlusDarkrecord on the twin; read galaxy name, light attribute, dark attribute (requested["dark_name"]),dark_cls, light centre (truth["dark_centre"]). Buildaf.Model(lmp.Gaussian)per stellar member with fixed geometry and intensity, tiemass_to_light_ratioto member 0 (asmass_light_dark_basis_from), wrap inaf.Model(Basis, profile_list=…); darkaf.Model(dark_cls)with centre fixed, freekappa_s,scale_radius(or fixed viafree_scale_radius=False),inner_slopefree for gNFW when requested; shearaf.Model(ExternalShear)when the twin has one; remaining galaxies as instances;source_modelpassed in. PublishTRUTH_PATHSmapping truth keys → model paths.autolens/twins/recovery.py:RecoveryRow/RecoveryTable(rows,all_inside,to_csv),recovery_table_from(samples, truth, paths=None, sigma=3.0)usingsamples.median_pdf(as_instance=False)andsamples.values_at_sigma(sigma, as_instance=False)indexed viamodel.paths; truth may be a dict or atwin.jsonpath.- Export under
al.twins; extend the module docstring. - Tests in
test_autolens/twins/test_model.py,test_recovery.py; end-to-end under test mode.
Key Files
autolens/twins/model.py,autolens/twins/recovery.py— newautolens/twins/transform/stellar_dark.py— record fields read by the builderautogalaxy/analysis/chaining_util.py— the Result-based twin of this builder
Original Prompt
Click to expand starting prompt
LensTwin phase 4a: build the inference model from a twin (al.twins.model) and compare posteriors with the injected truth (al.twins.recovery)
Type: feature
Target: autolens
Repos:
- PyAutoLens
Themes: - mge
Difficulty: medium
Autonomy: supervised
Priority: high
Status: formalised
Consequence: judge
Witness: (1)al.twins.model.stellar_dark_model_from(twin, galaxy="lens", ...)returns anaf.Collectionwhose lens galaxy carries aBasisoflmp.Gaussianmodels with centre, ell_comps, sigma and intensity fixed to the twin's solved values and ONE shared freemass_to_light_ratio, a dark profile (NFWSphby default,gNFWSphwith freeinner_slopeon request) centred on the light with freekappa_sandscale_radius, free shear when the twin has one, and the caller's source model;model.instance_from_prior_medians()builds, and the model instance at the truth values reproduces the twin tracer's convergence to 1e-10. (2)al.twins.recovery.recovery_table_from(samples, truth, sigma=3.0)lists each truth parameter's value, median PDF and the lower/upper bounds atsigma, with aninsideflag; a unit test on synthetic samples covers in and out. (3) A test-mode (PYAUTO_TEST_MODE=1) end-to-end run on a simulated reference (simulate → twin → model → Nautilus → recovery table) runs in under a minute
Review-minutes: 30
Unattended: never
Epic: lenstwin
Phase: 4
Parent: draft/feature/autolens/lenstwin_epic.md
Blocked-by: none (library phases 0, 2, 3 are onmain). Human instruction 2026-10-29: open the PR as a DRAFT and do not merge until the human says so
Filed: 2026-10-29
Library prep for phase 4 (draft/research/autolens/lenstwin_p4_identifiability.md, step 3 says "if it is a reusable gap, file a small library prompt rather than writing it into the science script"). It is a reusable gap: ag.analysis.chaining_util.mass_light_dark_basis_from builds the equivalent model from a light-pipeline Result, but a twin has no Result; it has an instance tracer and a flat truth. Without this, every science script hand-builds the same no-misspecification model.
Scope (PyAutoLens autolens/twins/)
model.py:stellar_dark_model_from(twin, galaxy, *, source_model, dark_cls=None, free_inner_slope=None, free_scale_radius=True, free_shear=True, priors=None). Reads the stellar basis and dark profile the stellar-dark transformation put on the twin (via the lastReplaceTotalMassWithStellarPlusDarkrecord'sprocedure, not by guessing attribute names), ties onemass_to_light_ratioacross members exactly asmass_light_dark_basis_fromdoes, fixes the dark centre to the light centre, and keeps every other galaxy as a fixed instance. Default priors are broad, fixed and documented in the docstring, and never centred on the truth (a prior that knows the answer defeats the experiment). Pick them from each parameter's units (e.g. M/L and κ_s log-uniform over several decades; r_s uniform over a physically sensible arc-second range; inner_slope uniform 0–2) and accept apriorsoverride dict.recovery.py:recovery_table_from(samples, truth, paths=None, sigma=3.0)→ a list of rows (or a smallRecoveryTablewithto_csvandall_inside), mapping truth keys (mass_to_light_ratio,kappa_s,scale_radius_arcsec,inner_slope) to model paths through a default map thatstellar_dark_model_fromalso publishes (one source of truth for the paths). Usessamples.median_pdf()andsamples.values_at_sigma(sigma)from PyAutoFit; readstwin.jsontruth withal.twins.SimulatedTwin's loader if one exists, else plain JSON.- Export both under
al.twins; tests undertest_autolens/twins/; a short section in theal.twinsmodule docstring.
Out of scope: the Science experiment (phase 4 proper), multi-galaxy models (phase 5), workspace example (phase 6).
Activity
Jammy2211 commentedon Oct 29, 2026
Shipped in #811 (merge bf18d5c, pending release):
al.twins.stellar_dark_model_from,truth_paths_from,recovery_table_from, plus thekeep_shearfix for shear held in aMassField(DR1 layout). Hold lifted by the human 2026-10-29.