Repository navigation
Description
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 withag.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.rstis extended.
Tier: judge — merge mode: human /prm
Detailed implementation plan
Affected Repositories
- PyAutoLens (primary; only repo). It depends on PyAutoGalaxy
maincarrying fix: LM damping parity + stall guards for potential-correction iterative engines #676 (merged 7701202).
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
apltconventions.
Implementation Steps
autolens/twins/aperture.py:einstein_radius_aperture_from(tracer, grid, centre, **integrator_settings) -> ag.CircularAperture. θ_E comes fromag.LensCalc.from_tracer(tracer).einstein_radius_from(grid); return θ_E alongside the aperture (or as an attribute) so the record can quote it.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 reuseReferenceModel's roles/naming. lightnames the galaxy's MGEBasisattribute.apply(twin)must not mutate anything. It deep-copies the tracer, removes the galaxy's total-mass profile(s) (everyMassProfileattribute exceptExternalShear), and adds a stellarBasisoflmp.Gaussianand the dark profile fromag.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 onequadper point. - The new tracer must serialise (
al.to_dict).
- Galaxy addressing: verify how galaxies are named in a tracer loaded from
- Record (
TransformationRecord):requested: all arguments.procedure: convention text citingPyAutoGalaxy/docs/api/mass_budget.rst, thederivednumbers fromMassBudget.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
Nonevalues (al.from_dictdrops them; see the note inrecord.py). - Make sure the flat truth parameters (Υ, κ_s, r_s, inner slope, f_dark) appear in
twin.jsonvia the records.
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_fromon a radius list), tangential critical curves for both (LensCalc), θ_E for both, anddelta_chi_squared_image. That last one is Σ(I_twin − I_parent)²/σ² over the parent setup's masked grid with the noise map, usingTwin.noise_free_image_2d. compare_to_data(twin, dataset) -> float: χ²(twin | data) − χ²(parent | data) via twoal.FitImaging; raise for pixelised sources.
- Fields: convergence difference, deflection difference (y, x), enclosed-mass profiles M(<R) for both (via
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. Followautolens/lens/plot/tracer_plots.py(helpers fromautogalaxy.util.plot_utils,output_figure). Export fromautolens/plot/__init__.py.- Exports:
autolens/twins/__init__.pyexportscompare,compare_to_data,TwinComparison,einstein_radius_aperture_from, and the transformation undertransform. - Docs: extend
docs/api/twins.rst. - 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
SimulatedTwinsave/reload. - No mutation; kpc → arcsec conversion; a gNFW
inner_sloperound trip; a two-galaxy budget raises. compareof a twin with itself is zero; Δχ²_image > 0 for the transformed twin;compare_to_datagives finite numbers on the 7x7 fixture.- The subplot writes a PNG.
- 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 conventionstest_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
Jammy2211 commentedon Oct 28, 2026
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_fromal.twins.transform.ReplaceTotalMassWithStellarPlusDarkal.twins.compareandTwinComparisonal.twins.compare_to_dataaplt.subplot_twin_comparisontwin.jsonFollow-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