Skip to content

feat: twins stellar-plus-dark transformation and diagnostics (lenstwin 3) #805

Closed

Description

@Jammy2211

Overview

Phase 3 of the lenstwin epic (PyAutoMind draft/feature/autolens/lenstwin_epic.md; report lenstwin_epic_report.md §2.2–§2.4, §3 "Phase 3"). Phase 0 (PyAutoLens#804) made a fitted lens a seeded, provenance-carrying twin. Phase 2 (PyAutoGalaxy#676) added the mass-normalisation mathematics. This issue adds the first real transformation: replace a lens galaxy's total-mass profile with stellar mass that follows its MGE light, plus an NFW dark-matter halo, normalised inside the parent's Einstein radius. It also adds the diagnostics that say what the change does before any inference, and a library plotter for them. Rulings R3/R4 (convention) and R6 (plotter in the library) were taken on 2026-10-28.

Plan

  • 3A — transformation.
    • An Einstein-radius aperture: the circle of radius θ_E of the whole parent lens model, centred on the replaced galaxy.
    • transform.ReplaceTotalMassWithStellarPlusDark, which solves the split with ag.MassBudget, takes r_s in kpc and converts it with the setup's cosmology.
    • It keeps external shear, light, other galaxies and the source unchanged, and records the mass inside θ_E as the one deliberate preservation.
  • 3B — diagnostics.
    • al.twins.compare(parent, twin) returns convergence and deflection differences, enclosed-mass profiles, critical curves, both Einstein radii, and the noise-free Δχ²_image.
    • compare_to_data(twin, dataset) returns Δχ²_data against the observed image (parametric sources only).
    • A library plotter aplt.subplot_twin_comparison (R6).
  • The ground-truth parameters (M/L, κ_s, r_s, slope, f_dark) are written flat into twin.json, and the record round-trips.
  • Tests use the 7x7 fixtures; docs/api/twins.rst is extended.

Tier: judge — merge mode: human /prm

Detailed implementation plan

Affected Repositories

Branch Survey

Repository Current Branch Dirty?
PyAutoLens (origin/main 9a85d32, carries #804) main clean

No active.md claim on PyAutoLens.

Suggested branch: feature/lenstwin-p3-stellar-dark

Rulings

  • R3: the aperture is a circle of radius θ_E,parent. θ_E is computed for the whole lens model via LensCalc, and the circle is centred on the replaced galaxy's mass centre. M_ref is the projected mass of the replaced component(s) inside it, by grid integration. External shear and other galaxies are untouched and excluded from M_ref.
  • R4: the first halo is NFWSph, centred on the light, with a fixed physical r_s.
  • R6: the diagnostics plotter lives in the library and follows the aplt conventions.

Implementation Steps

  1. autolens/twins/aperture.py: einstein_radius_aperture_from(tracer, grid, centre, **integrator_settings) -> ag.CircularAperture. θ_E comes from ag.LensCalc.from_tracer(tracer).einstein_radius_from(grid); return θ_E alongside the aperture (or as an attribute) so the record can quote it.
  2. autolens/twins/transform/stellar_dark.py: ReplaceTotalMassWithStellarPlusDark(galaxy, light, f_dark=0.5 | budget, dark_cls=al.mp.NFWSph, scale_radius_kpc, keep_shear=True, aperture_settings=None, **dark_fixed).
    • Galaxy addressing: verify how galaxies are named in a tracer loaded from tracer.json, and reuse ReferenceModel's roles/naming.
    • light names the galaxy's MGE Basis attribute.
    • apply(twin) must not mutate anything. It deep-copies the tracer, removes the galaxy's total-mass profile(s) (every MassProfile attribute except ExternalShear), and adds a stellar Basis of lmp.Gaussian and the dark profile from ag.MassBudget(aperture, {galaxy: {...}}).solve(...).
    • r_s conversion: cosmology.arcsec_per_kpc_from(redshift=galaxy.redshift) * scale_radius_kpc.
    • gNFW needs coarse aperture_settings, because it does one quad per point.
    • The new tracer must serialise (al.to_dict).
  3. Record (TransformationRecord):
    • requested: all arguments.
    • procedure: convention text citing PyAutoGalaxy/docs/api/mass_budget.rst, the derived numbers from MassBudget.solve, θ_E,parent, r_s in kpc and in arcsec, redshift, and cosmology name.
    • changed: component paths.
    • side_effects: shear kept, the twin's own θ_E (reported, not asserted), and the dark centre tied to the light.
    • preserved: ["projected mass of <galaxy> inside theta_E,parent"].
    • The dicts must hold no None values (al.from_dict drops them; see the note in record.py).
    • Make sure the flat truth parameters (Υ, κ_s, r_s, inner slope, f_dark) appear in twin.json via the records.
  4. autolens/twins/compare.py: compare(parent, twin, grid=None) -> TwinComparison.
    • Fields: convergence difference, deflection difference (y, x), enclosed-mass profiles M(<R) for both (via ag.mass_angular_within_circle_via_grid_from on a radius list), tangential critical curves for both (LensCalc), θ_E for both, and delta_chi_squared_image. That last one is Σ(I_twin − I_parent)²/σ² over the parent setup's masked grid with the noise map, using Twin.noise_free_image_2d.
    • compare_to_data(twin, dataset) -> float: χ²(twin | data) − χ²(parent | data) via two al.FitImaging; raise for pixelised sources.
  5. autolens/twins/plot/twin_plots.py: subplot_twin_comparison(comparison, output_path=None, output_filename="twin_comparison", output_format="png"). Panels: parent image, twin image, (twin − parent)/σ, convergence difference, M(<R) for both, and critical curves overlaid. Follow autolens/lens/plot/tracer_plots.py (helpers from autogalaxy.util.plot_utils, output_figure). Export from autolens/plot/__init__.py.
  6. Exports: autolens/twins/__init__.py exports compare, compare_to_data, TwinComparison, einstein_radius_aperture_from, and the transformation under transform.
  7. Docs: extend docs/api/twins.rst.
  8. Tests (test_autolens/twins/test_stellar_dark.py, test_compare.py; NumPy only, coarse grids):
    • The witness: on a fixture SIE + shear + MGE-light reference with a 50/50 budget, the stellar and dark masses inside θ_E,parent are each 0.5 ± 1e-3 of M_ref, and the shear is unchanged.
    • The record round-trips through dict and through SimulatedTwin save/reload.
    • No mutation; kpc → arcsec conversion; a gNFW inner_slope round trip; a two-galaxy budget raises.
    • compare of a twin with itself is zero; Δχ²_image > 0 for the transformed twin; compare_to_data gives finite numbers on the 7x7 fixture.
    • The subplot writes a PNG.
  9. PR body: say the public API grew, and quote R3/R4/R6.

Key Files

  • autolens/twins/twin.py, record.py, transform/{__init__,identity}.py, reference.py (galaxy roles)
  • PyAutoGalaxy autogalaxy/operate/mass_budget.py, operate/lens_calc.py (LensCalc.from_tracer, einstein_radius_from, tangential_critical_curve_list_from)
  • autolens/lens/plot/tracer_plots.py, autolens/plot/__init__.py — plotting conventions
  • test_autolens/twins/test_twins.py — the fixture fit directory pattern

Out of scope

The identifiability experiment (phase 4), library ingestion and multi-galaxy normalisation (phase 5, R7), workspace and assistant (phase 6), other transformations.

Original Prompt

Click to expand starting prompt

PyAutoMind draft/feature/autolens/lenstwin_p3_stellar_dark_transform.md (Type: feature · Target: autolens · Difficulty: large · Consequence: judge · Epic: lenstwin · Phase: 3 · Filed: 2026-10-28).

Witness: on a fixture SIE(+shear) reference with an MGE lens light, twin.apply(transform.ReplaceTotalMassWithStellarPlusDark(...)) with a 50/50 budget yields a twin whose stellar and dark projected masses inside θ_E,parent are each 0.5 ± 1e-3 of M_ref, keeps the shear unchanged, and appends a TransformationRecord that round-trips through al.to_dict/al.from_dict and SimulatedTwin.output_to/from_directory; al.twins.compare(baseline, twin) returns Δχ²_image > 0 and Δχ²_image(baseline, baseline) == 0; aplt.subplot_twin_comparison writes a PNG.

What to build: (1) an Einstein-radius aperture helper in autolens/twins/; (2) transform.ReplaceTotalMassWithStellarPlusDark (galaxy, light basis, f_dark or ag.MassBudget, dark class default NFWSph, r_s in kpc via the setup cosmology, keep_shear, aperture settings), non-mutating, with a full TransformationRecord whose preserved names the aperture mass; (3) al.twins.compare (convergence and deflection differences, M(<R), critical curves, θ_E, Δχ²_image) and compare_to_data (Δχ²_data via FitImaging, parametric sources only); (4) the plotter autolens/twins/plot/twin_plots.py::subplot_twin_comparison, exported from aplt (R6); (5) docs in docs/api/twins.rst. Tests on the 7x7 fixtures. Out of scope: phases 4–6, other transformations. Say in the PR that the public API grew.

Activity

  1. Jammy2211 commented on Oct 28, 2026

    @Jammy2211
    CollaboratorAuthor

    Shipped

    #806 merged 2026-10-28 (merge commit b29ffcc). Head 9541c87 was green on every leg (unittest 3.12, 3.13, nojax; docs-build). Labelled pending-release.

    It adds:

    • al.twins.einstein_radius_aperture_from
    • al.twins.transform.ReplaceTotalMassWithStellarPlusDark
    • al.twins.compare and TwinComparison
    • al.twins.compare_to_data
    • aplt.subplot_twin_comparison
    • flat ground truth in twin.json

    Follow-up already in flight: gNFW convergence speed-up, PyAutoLabs/PyAutoGalaxy#677. The next epic phase (4, the first identifiability experiment) needs a release carrying phases 0, 2 and 3.


    Generated by Claude Code

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