Repository navigation
feat: twins stellar-plus-dark transformation and diagnostics (lenstwin 3) - #806
Merged
Merged
Conversation
…n 3) Public API grew: al.twins.einstein_radius_aperture_from, al.twins.transform.ReplaceTotalMassWithStellarPlusDark, al.twins.compare, al.twins.TwinComparison, al.twins.compare_to_data, aplt.subplot_twin_comparison, TransformationRecord.truth and a flat "truth" list in twin.json. Nothing removed. Issue #805. Rulings honoured (2026-10-28): R3 (aperture = circle of radius theta_E,parent from the whole tracer, centred on the replaced galaxy; M_ref = the replaced total-mass components only, shear excluded), R4 (NFWSph on the light centre, fixed r_s given in kpc and converted with the setup cosmology) and R6 (library plotter, aplt conventions). The split is solved by ag.MassBudget (PyAutoGalaxy#676). The light basis is re-expressed as lmp.Gaussian in place, so the light is unchanged and not double-counted. Also makes test__simulate__observed_route_leaves_global_random_state order-independent: the first import of jax in a process draws from NumPy's global RNG (jax/_src/clusters/k8s_cluster.py, reached lazily via autoarray AbstractNDArray.__getitem__), so the test warms the route before seeding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016nhQ8NtZ6QtuqxKKAnhhdg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3 of the LensTwin epic (closes #805). It adds the first real twin transformation: replace a lens galaxy's total-mass profile with stellar mass that follows its MGE light, plus a dark-matter halo, normalised inside the parent's Einstein radius. It also adds the diagnostics that say what a transformation changes before any inference is run.
3A — transformation
al.twins.einstein_radius_aperture_from(tracer, grid, centre, ...)measures θ_E for the whole lens model (viaLensCalc) and returns anag.CircularApertureof that radius, centred on the replaced galaxy.al.twins.transform.ReplaceTotalMassWithStellarPlusDark(galaxy, light, *, scale_radius_kpc, f_dark=0.5, ...):galaxyis addressed by model name (e.g."lens") or by the reference's role.dark_clsdefaults toNFWSph;gNFWSphtakesinner_slope.lmp.Gaussianbasis in place, with identical light. A test shows the lens image is bit-identical, so the light is not counted twice.ag.MassBudget(PyAutoGalaxy#676).TransformationRecordholds:requested: the arguments.procedure: convention text citingPyAutoGalaxy/docs/api/mass_budget.rst, plus the solved numbers — θ_E of parent and twin, M_ref, Υ, κ_s, r_s in kpc and arcsec, cosmology, aperture geometry.changed: the replaced component paths.side_effects: shear kept; the twin's own θ_E (reported, not asserted).preserved:["projected mass of <galaxy> inside theta_E,parent"]."truth"intwin.json, via the newTransformationRecord.truth.3B — diagnostics and plotter (R6)
al.twins.compare(parent, twin, ...)returns aTwinComparisonholding:al.twins.compare_to_data(twin, dataset)returns Δχ²_data = χ²(twin | data) − χ²(parent | data), from twoFitImagingobjects. It handles parametric sources only and raises for pixelised ones. Δχ²_image and Δχ²_data are different quantities and are named separately.aplt.subplot_twin_comparison(comparison, ...)draws a 2×3 panel: parent image, twin image, normalised residual (with Δχ²_image), convergence difference, M(<R) with both θ_E, and both critical curves.Rulings honoured (2026-10-28)
NFWSphon the light centre, with a fixed physical r_s.apltconventions.Fixes a test that failed depending on run order.
test__simulate__observed_route_leaves_global_random_statefailed whenever it was the first test in a process to run the observed route, for example underpytest -n 4. The cause is upstream:import jaxin a process draws 13 numbers from NumPy's global random state.jax/_src/clusters/k8s_cluster.pyevaluatesnp.random.rand(...)at import time.AbstractNDArray.__getitem__.Twin.simulateitself draws only fromdefault_rng(seed). The test now runs the route once before seeding.API Changes
The public API grew.
al.twins:einstein_radius_aperture_from,compare,compare_to_data,TwinComparisonandtransform.ReplaceTotalMassWithStellarPlusDark.aplt:subplot_twin_comparison.TransformationRecord.truthand a"truth"key intwin.json.Nothing existing was removed or changed in behaviour.
See full details below.
Test Plan
test_autolens/twins/test_stellar_dark.py(13 tests):al.to_dict/from_dictand throughSimulatedTwin.output_to/from_directory.gNFWSphinner_sloperound trip.keep_shear=False.test_autolens/twins/test_compare.py(7 tests):compare_to_datareturns finite values on the 7x7 fixture.subplot_twin_comparisonwrites a PNG.python -m pytest test_autolens/twins -n 4: 39 passed.python -m pytest test_autolens -n 8: 973 passed, 1 skipped, 1 xfailed.Full API Changes (for automation & release notes)
Added
al.twins.einstein_radius_aperture_from(tracer, grid, centre, pixel_scale=0.05, **integrator_settings) -> ag.CircularAperture(θ_E is.radius).al.twins.transform.ReplaceTotalMassWithStellarPlusDark(galaxy, light, *, scale_radius_kpc, f_dark=0.5, budget=None, dark_cls=None, keep_shear=True, aperture_settings=None, einstein_radius_pixel_scale=0.05, grid=None, dark_name="dark", **dark_fixed), withapply(twin) -> (tracer, setup, record).al.twins.compare(parent, twin, grid=None, radii=None, centre=None, pixel_scale=0.05, aperture_settings=None) -> TwinComparison.al.twins.TwinComparison:radii,mass_enclosed_parent/twin,tangential_critical_curves_parent/twin,einstein_radius_parent/twin,image_parent/twin,noise_map,centre,grid.convergence_difference,deflections_difference,deflections_y/x_difference,mass_enclosed_difference,normalized_residual_map,delta_chi_squared_image.al.twins.compare_to_data(twin, dataset, parent=None) -> float.aplt.subplot_twin_comparison(comparison, output_path=None, output_filename="twin_comparison", output_format=None, colormap=None, title_prefix=None).TransformationRecord.truth(property);SimulatedTwin.twin_json()["truth"].docs/api/twins.rst: galaxy addressing, the transformation, the diagnostics and the plotter.Removed
Migration
Generated by the PyAutoLabs agent workflow.
Generated by Claude Code