Skip to content

docs: adopt the flat fields=<MassField> model form across the workspace examples [draft until release] - #562

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/mass-field-flat-sweep
Sep 19, 2026
Merged

Jammy2211 merged 1 commit into
mainfrom
feature/mass-field-flat-sweep

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Adopts the flat fields= model form across autolens_workspace, following PyAutoLens #744 (478213e78), which made the model's fields= slot accept a bare al.MassField alongside a collection.

# before
fields=af.Collection(field=field)
# after
fields=field

Prior paths become fields.shear.gamma_1 instead of fields.field.shear.gamma_1.

Companion PR: autolens_workspace_test #322 (draft), which carries the same sweep for the test workspace.


What changed

autolens_workspace (this PR) — 143 model-side construction sites across 120 files (imaging 37, group + interferometer 50, multi_galaxy + point_source 30, guides + multi_dataset 26), plus:

  • Prose, __Model__ bullets and result-path docstrings that described the old container: "in its own fields collection" → "in the model's fields slot"; result.instance.fields.field.shear → result.instance.fields.shear.
  • The reader family, migrated in lockstep with the builders, so latent keys, the CSV column and aggregator paths still name a real path:
    • guides/results/_quick_fit.py — latent keys fields.shear.{magnitude,angle}, instance.fields.shear.gamma_{1,2}
    • guides/results/workflow/csv_make.py — add_variable(argument="fields.shear.magnitude")
    • guides/results/aggregator/samples.py — tuple path ("fields","shear","gamma_1") and its string form
  • 356 regenerated notebooks plus llms-full.txt and workspace_index.json, rebuilt from scripts/ with PyAutoHands generate.py autolens. Notebooks were never hand-edited.

autolens_workspace_test (PR #322) — 66 model-side collection sites found by an AST walk; 65 migrated, 1 deliberately retained (see below).

What was deliberately NOT changed

  • Chained fields=<result>.{model,instance}.fields stages — 170 in autolens_workspace, 19 in autolens_workspace_test. Chaining is shape-transparent: it carries whatever form the parent model had, and the free-vs-fixed distinction rides on .model vs .instance, not on the container. These work unchanged.
  • Tracer-side al.Tracer(..., fields=[field]) list literals — 112 and 16 respectively. Tracers take a list; this stays a list, mirroring the merged euclid pipeline reference implementation (euclid_strong_lens_modeling_pipeline#90, 9cdee7b).
  • Genuine multi-field sites (all_fields in the test workspace) — a collection there is not a single-entry wrapper.
  • The 13 intentionally field-less light-only stages — af.Collection(galaxies=...) stages that carry no fields=. None gained one.
  • misc/interop/coolest_round_trip.py — its .fields[0] reads are tracer-side list indexing, not model composition.
  • The collection-form regression witness in misc/mass/galaxy_attached_legacy.py (test workspace, second model at :189). With every other model in both workspaces now flat, this is the workspaces' last regression witness that the library still accepts a collection in that slot. Only a docstring saying so was added.
  • markdown/ mirrors — out of scope for this change.

⚠️ IDENTIFIER MIGRATION NOTE — read before merging

The flat form intentionally changes prior paths (fields.field.shear.* → fields.shear.*) and therefore PyAutoFit result identifiers.

  • Migrated scripts write to NEW output/ directories.
  • Existing result trees keyed on the old identifiers are ORPHANED and are NOT resumed. These scripts start new runs.
  • No identifier was aliased to an old value, and no result directory was renamed to fake continuity.

Example (imaging modeling): 68b52dd173952931699fb677e93b9489 → 38854f8a9d9cbde4fd1d27d2b54b7dc0.

Two pins in autolens_workspace_test (PR #322):

Script OLD NEW
scripts/multi_galaxy/composition_mge.py b99831e66dd27eee314113e8e58235b6 29f82bd3de24984b94657c328b64c3be
scripts/misc/mass/galaxy_attached_legacy.py 35ebe9353118bcc0c7b2d577ce2639ee UNCHANGED (backwards-compatibility regression — must not move)

The composition_mge.py pin was recomputed, not adjusted, from the autolens_workspace_test repository root — the same cwd every script there runs from. An identifier is a function of the model and the active PyAutoFit configuration, so it is config- and cwd-dependent; the script's docstring now records that.

Evidence

  • Equivalence, 8/8 representative models (one per folder, composed from the worktree with a byte-identical config/): prior_count unchanged everywhere (21/14/33/20/15/12/11/29); prior-path sets equal after normalising fields.field. → fields.; model.identifier changed (the intended consequence).
  • Log-likelihood bit-identical for the same instance under both forms — imaging -37796.54513586828 (0xc0e2749171c0c6c4).
  • autolens_workspace smoke: 38/38 scripts + 2/2 notebooks PASS.
  • autolens_workspace_test smoke: 32/32 PASS (979 s, profile_smoke.yaml), including composition_mge.py and both latent integration smokes.
  • slam_start_here.py hand-run PASS — all 5 stages carry fields.shear, free/fixed split intact.
  • slam_general_jax.py hand-run — FREE stages (source_lp[1] seed, source_pix[1] and mass_total[1] via .model.fields) show fields MassField (N=2) with the seed's own prior ids 25/26 propagated; FIXED stages (source_pix[2], light[1] via .instance.fields) show fields MassField (N=0) with gamma 0.0.
  • Notebook AST witness: 0 per-file mismatches. An AST walk of the notebook code cells matches scripts/ exactly (0 single-entry collections; 170 attr-chain / 128 bare-name / 25 flat-model / 112 list-literal on both sides).
  • Site census conserved in both repos. In autolens_workspace_test: 103 fields= sites overall (66 → 65 flat + 1 retained, 19 attr-chain, 16 tracer list-literals, 2 tracer all_fields); no file mixes the two forms; fields.field no longer appears anywhere, docstrings included.
  • Every changed file byte-compiles.

Local stack: PyAutoLens 478213e78, PyAutoGalaxy 90e757d3, PyAutoFit 7c0e79aab.

The subhalo sensitivity fix

Both hasattr guards in base_model_narrow_priors_from() — imaging/features/advanced/subhalo/sensitivity/slam_source_{parametric,pixelized}.py — moved from hasattr(base_model.fields.field, "shear") to hasattr(base_model.fields, "shear").

Under the flat form the OLD guard silently skipped a deliberate shear prior re-centring. base_model.fields.field raises AttributeError inside hasattr, which returns False — no error, no log, the re-centring simply never ran.

Proven to fire now, for a max-likelihood gamma_1=0.1234, gamma_2=-0.0567 at b=0.05:

  • gamma_1: UniformPrior(-0.3, 0.3) → UniformPrior(0.0734, 0.1734)
  • gamma_2: UniformPrior(-0.3, 0.3) → UniformPrior(-0.1067, -0.0067)

The guard stays defensive: no fields slot, or a field with no shear, still does nothing. These scripts remain parked in no_run.yaml and out of smoke_tests.txt.

SEPARATELY — a pre-existing bug fix, not part of the migration

This is called out in its own section so it is not read as part of the flat-form sweep.

scripts/multi_dataset/features/one_by_one/modeling.py, the "Second Dataset Offset" stage, re-collected:

af.Collection(dataset_model=..., galaxies=result.instance.galaxies)

dropping fields entirely — so a stage documented as holding all lens model parameters fixed to the first fit silently fitted with no external shear at all. The author's own commented-out "Mass Model Fixed" block directly above it already uses the correct fields=result.instance.fields.

One line added: fields=result.instance.fields,.

This is a pre-existing bug on main, independent of the flat form, and the fix was approved by the human.

PyAutoHeart ack

Heart was RED at ship time. Verdict, verbatim:

RELEASE READINESS  ✗ RED  score 60
  ✗ release validation FAILED (stage integrate)
  ! manifest drift: remote-session blocks (generated) — 2 mismatch(es) vs PyAutoMind/repos.yaml

Both conditions are pre-existing and unrelated to these two workspace repos.

The human was asked:

"Heart is RED (release validation FAILED, stage integrate) plus a YELLOW manifest drift — both pre-existing and unrelated to these two workspace repos. Do you authorize pushing the two commits and opening/updating the PRs despite the RED?"

and answered:

"Yes — push and open/update PRs"

That authorization covers push and PR-open/update only. Merge and release remain human-only and are NOT authorized by it.

This PR is therefore opened as DRAFT with pending-release: the flat form requires PyAutoLens #744 (478213e78), which is merged but not yet on PyPI.

PR #322's red CI leg is NOT this sweep

The red leg on the companion draft PR was bisected to a JAX 0.11.2 × unfixed-PyAutoGalaxy regression, not to the field migration:

TypeError: functools.partial ... is not a valid JAX type

raised in the zero-contour solver via the effective_einstein_radius latent. The unmodified main version of the script fails identically in a CI-matching venv. It is fixed by PyAutoGalaxy 90e757d3, which merged roughly 10.5 hours after that CI run, so a CI re-run should clear it. The flat form itself passes under JAX 0.11.2 on current mains — misc/latent/latent_integration_smoke_jax.py, the very script whose leg was red, PASSES under the flat form (56.4 s), writing exactly one files/latent/latent_summary.json with all 8 latents finite and non-zero.

Cross-references

Closes #561

🤖 Generated with Claude Code

Every single-entry `fields=af.Collection(field=X)` in `scripts/` now writes
`fields=X`, matching the merged euclid reference and the library's flat
`fields` slot. 143 model-side construction sites moved across 130 files
(imaging 37, group + interferometer 50, multi_galaxy + point_source 30,
guides + multi_dataset 26), together with the prose, `__Model__` bullets and
result-path docstrings that described the old container ("in its own `fields`
collection" -> "in the model's `fields` slot",
`result.instance.fields.field.shear` -> `result.instance.fields.shear`).

Deliberately unchanged:
* 170 chained `fields=<result>.model/.instance.fields` passes — shape
  transparent, the free-vs-fixed distinction rides on `.model` vs `.instance`.
* 112 tracer-side `al.Tracer(..., fields=[field])` lists — tracers take a list.
* light-only `af.Collection(galaxies=...)` stages that carry no `fields=` —
  none gained one.
* `markdown/` mirrors — out of scope for this change.

Reader family migrated in lockstep with the builders, so the latent keys, the
CSV column and the aggregator paths still name a real path:
`guides/results/_quick_fit.py` (latent keys `fields.shear.{magnitude,angle}`,
`instance.fields.shear.gamma_{1,2}`), `guides/results/workflow/csv_make.py`
(`add_variable(argument="fields.shear.magnitude")`) and
`guides/results/aggregator/samples.py` (tuple path `("fields","shear","gamma_1")`
and its string form).

Both subhalo sensitivity prior-reset guards in `base_model_narrow_priors_from()`
(`imaging/features/advanced/subhalo/sensitivity/slam_source_{parametric,pixelized}.py`)
moved from `hasattr(base_model.fields.field, "shear")` to
`hasattr(base_model.fields, "shear")`. Against a flat model the old guard was
False, so the re-centring silently never ran; under the new guard it fires:
gamma_1 UniformPrior(-0.3, 0.3) -> UniformPrior(0.0734, 0.1734) and
gamma_2 UniformPrior(-0.3, 0.3) -> UniformPrior(-0.1067, -0.0067) for a
max-likelihood gamma_1=0.1234, gamma_2=-0.0567 at b=0.05. The guard stays
defensive: no `fields` slot, or a field with no `shear`, still does nothing.

SEPARATELY — a pre-existing bug fix, not part of the migration:
`scripts/multi_dataset/features/one_by_one/modeling.py` "Second Dataset Offset"
re-collected only `galaxies=result.instance.galaxies`, dropping the external
shear the first fit had constrained, although its stated intent is to hold all
lens model parameters fixed to that fit (the commented-out "Mass Model Fixed"
block above it already carried `fields=result.instance.fields`). That stage now
carries `fields=result.instance.fields,`. This is a behaviour change on main,
independent of the flat form.

Equivalence witness (8 representative models, one per folder, composed from the
worktree with a byte-identical `config/`): `prior_count` unchanged everywhere
(21/14/33/20/15/12/11/29) and the prior-path sets are equal after normalising
`fields.field.` -> `fields.`; the same instance gives a bit-identical
log_likelihood under both forms (imaging: -37796.54513586828, 0xc0e2749171c0c6c4).
The `model.identifier` DOES change (e.g. imaging modeling
68b52dd173952931699fb677e93b9489 -> 38854f8a9d9cbde4fd1d27d2b54b7dc0) — that is
the intended consequence of the flat path. Existing `output/` trees are
orphaned: these scripts start new runs and do not resume the old ones.

`notebooks/`, `llms-full.txt` and `workspace_index.json` regenerated from
`scripts/` with PyAutoHands `generate.py autolens` (356 notebooks); notebooks
were never hand-edited. An AST walk of the notebook code cells matches the
scripts exactly (0 single-entry collections, 170 attr-chain / 128 bare-name /
25 flat-model / 112 list-literal on both sides, 0 per-file mismatches).

Issue: #561

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pending-release PR queued for the next release build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: adopt the flat (bare) fields= form in autolens_workspace + autolens_workspace_test

1 participant