Skip to content

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

Closed

Description

@Jammy2211

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, …): an af.Collection built from the twin's stellar-dark record — stellar Basis of lmp.Gaussian with 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; priors override.
  • Add al.twins.recovery.recovery_table_from(samples, truth, sigma=3.0): truth, median PDF, lower/upper bound and an inside flag 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=1 end-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

  1. autolens/twins/model.py: locate the last ReplaceTotalMassWithStellarPlusDark record on the twin; read galaxy name, light attribute, dark attribute (requested["dark_name"]), dark_cls, light centre (truth["dark_centre"]). Build af.Model(lmp.Gaussian) per stellar member with fixed geometry and intensity, tie mass_to_light_ratio to member 0 (as mass_light_dark_basis_from), wrap in af.Model(Basis, profile_list=…); dark af.Model(dark_cls) with centre fixed, free kappa_s, scale_radius (or fixed via free_scale_radius=False), inner_slope free for gNFW when requested; shear af.Model(ExternalShear) when the twin has one; remaining galaxies as instances; source_model passed in. Publish TRUTH_PATHS mapping truth keys → model paths.
  2. autolens/twins/recovery.py: RecoveryRow/RecoveryTable (rows, all_inside, to_csv), recovery_table_from(samples, truth, paths=None, sigma=3.0) using samples.median_pdf(as_instance=False) and samples.values_at_sigma(sigma, as_instance=False) indexed via model.paths; truth may be a dict or a twin.json path.
  3. Export under al.twins; extend the module docstring.
  4. 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 — new
  • autolens/twins/transform/stellar_dark.py — record fields read by the builder
  • autogalaxy/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 an af.Collection whose lens galaxy carries a Basis of lmp.Gaussian models with centre, ell_comps, sigma and intensity fixed to the twin's solved values and ONE shared free mass_to_light_ratio, a dark profile (NFWSph by default, gNFWSph with free inner_slope on request) centred on the light with free kappa_s and scale_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 at sigma, with an inside flag; 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 on main). 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 last ReplaceTotalMassWithStellarPlusDark record's procedure, not by guessing attribute names), ties one mass_to_light_ratio across members exactly as mass_light_dark_basis_from does, 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 a priors override dict.
  • recovery.py: recovery_table_from(samples, truth, paths=None, sigma=3.0) → a list of rows (or a small RecoveryTable with to_csv and all_inside), mapping truth keys (mass_to_light_ratio, kappa_s, scale_radius_arcsec, inner_slope) to model paths through a default map that stellar_dark_model_from also publishes (one source of truth for the paths). Uses samples.median_pdf() and samples.values_at_sigma(sigma) from PyAutoFit; reads twin.json truth with al.twins.SimulatedTwin's loader if one exists, else plain JSON.
  • Export both under al.twins; tests under test_autolens/twins/; a short section in the al.twins module docstring.

Out of scope: the Science experiment (phase 4 proper), multi-galaxy models (phase 5), workspace example (phase 6).

Activity

  1. Jammy2211 commented on Oct 29, 2026

    @Jammy2211
    CollaboratorAuthor

    Shipped in #811 (merge bf18d5c, pending release): al.twins.stellar_dark_model_from, truth_paths_from, recovery_table_from, plus the keep_shear fix for shear held in a MassField (DR1 layout). Hold lifted by the human 2026-10-29.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions