Skip to content

feat: MassField in PyAutoLens — Tracer(fields=), analysis slot, COOLEST, LOS, model_util - #742

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/mass-field-integration
Sep 17, 2026
Merged

Jammy2211 merged 1 commit into
mainfrom
feature/mass-field-integration

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

Teaches PyAutoLens about ag.MassField (PyAutoGalaxy#621), phase 2 of the mass-field epic. A field is its own thing: Tracer(galaxies, cosmology=None, fields=None) stores it in tracer.fields, and only the tracer's planes merge galaxies and fields at each redshift (members / members_ascending_redshift). tracer.galaxies never contains a field, so every script that indexes galaxies positionally keeps working, and every per-galaxy surface (galaxy_image_2d_dict_from, the fit plane images, the galaxy subplots) skips fields explicitly. Lensing quantities (deflections, convergence, potential, ray-tracing, has, cls_list_from) run over members.

Around the tracer: the analysis folds a model's fields collection (AnalysisLens.tracer_via_instance_from, the aggregator tracer builder, Result.max_log_likelihood_fields); COOLEST exports each field as its own MassField entity and imports MassField entities as al.MassField in tracer.fields; the line-of-sight sampler gains fields_from() / galaxies_and_fields_from() with its legacy single-list return kept as the default; and al.model_util.mass_field_from(lens, shear=True, mass_sheet=False, potential=False, redshift=None) composes the field model with the MassSheet / ExternalPotential centre tied to lens.mass.centre as a shared prior. al.MassField is re-exported; API docs and the start-here overview gain the new surfaces.

Legacy galaxy-attached sheets are untouched: no warning, no behaviour change, and the COOLEST export of a galaxy-attached tracer is pinned byte-identical against a fixture captured on main. The PyAutoFit identifier of a galaxy-attached lens/source model is asserted unchanged. Closes #741.

API Changes

Added: Tracer(..., fields=), Tracer.fields, Tracer.members, Tracer.members_ascending_redshift, Tracer.sliced_tracer_from(..., fields=), Result.max_log_likelihood_fields, LOSSampler.fields_from() / galaxies_and_fields_from() / galaxies_from(sheets_as_fields=False), model_util.mass_field_from, and the al.MassField re-export. One behaviour change on COOLEST import only: a MassField entity now lands in tracer.fields as an al.MassField rather than as an extra Galaxy in tracer.galaxies (export is unchanged). LOSSampler now caches its sampled population, so repeated galaxies_from() calls describe one line of sight. No removals or renames.
See full details below.

Test Plan

  • pytest test_autolens -q -n auto: 710 passed, 1 xfailed (pre-existing); baseline on main 653 passed, 1 xfailed — 57 new tests, no regressions.
  • Identifier pin: af.Collection(galaxies=af.Collection(lens=af.Model(al.Galaxy, redshift=0.5, mass=af.Model(al.mp.Isothermal), shear=af.Model(al.mp.ExternalShear)), source=af.Model(al.Galaxy, redshift=1.0, bulge=af.Model(al.lp.Sersic)))).identifier == "fef2697b5c32ba56bb18a7baecb7b0f6" on main (223132c) and on this branch, asserted in a subprocess (the suite's conftest pushes a test config that changes identifiers, so the pinned value is evaluated under the shipped config).
  • Witness: Tracer(galaxies=[lens, source], fields=[MassField(redshift=0.5, shear=...)]).traced_grid_2d_list_from(grid) allclose to the same shear on lens; tracer.galaxies == [lens, source]; a model with fields=af.Collection(field=af.Model(al.MassField, ...)) builds through AnalysisImaging with the field in tracer.fields and its log likelihood equal to the galaxy-attached form at the same values; COOLEST round trip returns an al.MassField with parameters equal to 1e-12; legacy galaxy-attached export byte-identical to test_autolens/interop/fixtures/legacy_galaxy_attached.json; mass_field_from(lens=lens_model, potential=True).potential.centre is lens_model.mass.centre.
  • Per-galaxy surfaces: FitImaging.galaxy_model_image_dict keys are the galaxies only; model_images_of_planes_list equals the galaxy-attached equivalent; subplot_fit, subplot_of_planes, subplot_tracer, subplot_galaxies_images render with a field; a pixelized-source fit and FitPointDataset.figure_of_merit with a field equal the galaxy-attached values.
  • JAX three-way probe (NumPy / xp=jnp / jax.jit with the tracer traced and the grid closed over) on traced_grid_2d_list_from with a field: all allclose at atol=1e-6 (the 7.5e-8 float32 residual is pre-existing and identical for a galaxy-attached shear); the jit result with the field differs from without. Unit tests stay NumPy-only per repo policy.
  • git diff --name-only touches nothing under autolens/config/; no warnings.warn added; LF endings.
  • CI green on Python 3.12 and 3.13.
Full API Changes (for automation & release notes)

Added

  • Tracer.__init__(galaxies, cosmology=None, fields=None) — fields is last, so positional Tracer(galaxies, cosmology) callers are unaffected; validated by _validate_fields (mirrors _validate_galaxies, names MassField, rejects a Galaxy in fields); stored as tracer.fields (a list, [] by default; serialises via autonerves like galaxies).
  • Tracer.members / Tracer.members_ascending_redshift — galaxies followed by fields; plane_redshifts, planes, grid_2d_at_redshift_from, deflections_yx_2d_from, convergence_2d_from, potential_2d_from, time_delays_from, has, cls_list_from, extract_attribute, extract_profile now run over members. galaxies_ascending_redshift, extract_attributes_of_galaxies and galaxy_image_2d_dict_from (and everything keyed off it) stay galaxies-only.
  • Tracer.sliced_tracer_from(..., fields=None) — fields snapped to the nearest sliced plane like line_of_sight_galaxies.
  • _warn_if_no_light_is_behind_any_mass(galaxies, fields=None) — fields are mass-bearing for the ordering check.
  • Result.max_log_likelihood_fields — [] when the model declares no fields; passed to the tracer built for the positions threshold.
  • LOSSampler.fields_from() -> List[ag.MassField], LOSSampler.galaxies_and_fields_from() -> (halos, sheets), LOSSampler.galaxies_from(sheets_as_fields=False); sampling moved into a cached _sample_population().
  • autolens.analysis.model_util.mass_field_from(lens, shear=True, mass_sheet=False, potential=False, redshift=None) -> af.Model — ValueError when lens has no mass; mass_sheet.centre / potential.centre are the same TuplePrior object as lens.mass.centre.
  • autolens.MassField re-export (from autogalaxy.galaxy.mass_field import MassField).
  • autolens.jax.register_tracer_classes registers MassField with no_flatten=("redshift",) and walks tracer.fields.

Changed Behaviour

  • from_coolest: a COOLEST MassField entity becomes an al.MassField in tracer.fields, not a Galaxy in tracer.galaxies. Lensing quantities of the returned tracer are unchanged; only where the profiles land differs. Two existing tests that asserted the old landing place were updated minimally (test__round_trip__relative_file_path, test__from_coolest__skipped_template_returns_mass_profiles_only).
  • to_coolest: a tracer's fields are exported as field_{j} entities; the legacy peel of galaxy-attached sheets keeps its mass_field_{i} names and output (byte-identity test).
  • FitImaging.model_images_of_planes_list / FitInterferometer.model_visibilities_of_planes_list skip MassField plane members (they are not keys of the galaxy dictionary).
  • LOSSampler: the sampled population is cached on the instance, so galaxies_from() followed by fields_from() describe a single line of sight (previously each galaxies_from() call with seed=None redrew).

Migration

  • None required. Galaxy(redshift=..., shear=ExternalShear(...)) scripts and models keep working with the same result identifier. To adopt fields: al.Tracer(galaxies=[lens, source], fields=[al.MassField(redshift=0.5, shear=al.mp.ExternalShear(...))]), or in a model fields=af.Collection(field=al.model_util.mass_field_from(lens=lens)). Workspace adoption is the epic's phases 3–5.

Docs

  • docs/api/galaxy.rst (MassField, Tracer(fields=)), docs/api/modeling.rst (new "Model Utilities" block incl. mass_field_from), docs/overview/overview_1_start_here.md ("External fields").

Fixture

  • test_autolens/interop/fixtures/legacy_galaxy_attached.json — captured from main before this change; lives under fixtures/ because the repo's .gitignore excludes every directory named files.

Generated by the PyAutoLabs agent workflow.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PqfNuorRbom8ZjD9i9hvWo


Generated by Claude Code

…ST, LOS, model_util

Phase 2 of the mass-field epic. `Tracer(galaxies, cosmology=None,
fields=None)` stores external fields in `tracer.fields`; only the planes
merge galaxies and fields (`members` / `members_ascending_redshift`), so
`tracer.galaxies` never holds a field and per-galaxy surfaces (the galaxy
image dictionary, fit plane images, subplots) skip fields explicitly while
every lensing quantity runs over members.

The analysis folds a model's `fields` collection (AnalysisLens, the
aggregator tracer builder, Result.max_log_likelihood_fields); COOLEST
exports fields as their own MassField entities and imports MassField
entities into `tracer.fields` (the one import behaviour change; export of
legacy galaxy-attached sheets is pinned byte-identical); the LOS sampler
gains fields_from()/galaxies_and_fields_from() with its legacy return kept
as the default; model_util.mass_field_from ties the MassSheet /
ExternalPotential centre to the lens mass centre as a shared prior.
al.MassField is re-exported; API docs and the overview updated.

Legacy galaxy-attached sheets: no warning, no behaviour change, identifier
pinned (fef2697b5c32ba56bb18a7baecb7b0f6). No prior config changes.
Closes #741.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqfNuorRbom8ZjD9i9hvWo
@Jammy2211

Copy link
Copy Markdown
Collaborator Author

Workspace PRs (phase 3 of the MassField epic, drafts held until this lands in a release): PyAutoLabs/autolens_workspace#560, PyAutoLabs/autolens_workspace_test#322. Issue: PyAutoLabs/autolens_workspace#559

@Jammy2211 Jammy2211 removed the pending-release PR queued for the next release build label Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: MassField in PyAutoLens — Tracer(fields=), analysis slot, COOLEST, LOS, model_util

2 participants