Repository navigation
feat: model figures phase 3 — lens solved/missing semantics + cookbook figures #736
Description
Overview
Phase 3 of the model-figures epic (PyAutoMind ledger draft/feature/autofit/model_figures_epic.md; phases 1–2 shipped as PyAutoFit#1606 and PyAutoFit#1614 + autofit_workspace#152). The renderer draws any model truthfully but cannot know which lens parameters a fit solves rather than samples. This phase adds a class-declared __solved_parameters__ protocol consumed by PyAutoFit's graph_spec (no lens class names inside PyAutoFit), declares it on LightProfileLinear, PointSolved and Pixelization, renders the three lens acceptance models with real classes, and puts model figures into the lens and galaxy cookbooks and docs pages. Two prompt statements were stale against the installed stack and the plan follows the code: Pixelization is two-deep and areas_factor is a Delaunay mesh parameter (genuinely missing from the null priors config).
Plan
- PyAutoFit:
__solved_parameters__protocol read fromobj.clsingraph_spec, additivesolved_paths, rule R7 widened to float subclasses (freeRedshiftis a pill on the galaxy card),solvedcount/text in the presentation; tests. - PyAutoArray:
Pixelization.__solved_parameters__ = ("reconstruction",). PyAutoGalaxy:LightProfileLinear(intensity),PointSolved(centre);Basisdeclares nothing (members carry the solved amplitudes); real-class semantics tests. - PyAutoLens: real-class acceptance tests for the simple lens, MGE 2×30 + Delaunay pixelization and the 8-extra-galaxy group model; committed renders;
docs/general/model_cookbook.mdre-synced to the script with figures;overview_3_features.mdMGE/pixelization/point figures. PyAutoGalaxy docs likewise. - Ship four library PRs on one same-named branch, merged PyAutoFit → PyAutoArray → PyAutoGalaxy → PyAutoLens.
- Workspaces: figures at every
model.infostage of both modeling cookbooks, a fixed Redshift Free stage and a new Solved Parameters stage in the lens cookbook, notebooks regenerated,model_figure: falsein bothconfig/output.yaml.
Detailed implementation plan
Scope note (widening the prompt's repo list)
The prompt lists PyAutoLens, PyAutoGalaxy and the two workspaces. The survey shows the protocol needs PyAutoFit (extractor consumes it) and PyAutoArray (Pixelization is autoarray's class). Both changes are small (≈40 and 1 lines) and are the only honest way to meet the prompt's "registry or protocol, not class-name strings inside PyAutoFit" rule. Six repos, one same-named branch, library PRs merged in dependency order.
Branch survey (step 4)
All six repos are on main, clean, level with origin. worktree_check_conflict fires only for PyAutoFit (still remove-parallel-ep-optimiser, PR #1612 open, no local worktree, EP-optimiser files only) — same disjoint-file parallel-claim waiver as phase 2, to be recorded on the active.md row. PyAutoArray, PyAutoGalaxy, PyAutoLens, autolens_workspace, autogalaxy_workspace: no claims.
- Task
model-figures-lens· branchfeature/model-figures-lens(identical name across all six repos solib-tests.ymlchecks out the same-named dependency branches) · worktree~/Code/PyAutoLabs-wt/model-figures-lens/. - Issue:
feat: model figures phase 3 — lens solved/missing semantics + cookbook figureson PyAutoLens (the prompt's target).
Decisions (recommendations)
- Protocol = class attribute
__solved_parameters__(a tuple of attribute names), read fromobj.clsby the extractor exactly as it already reads__exclude_identifier_fields__(graph_spec.py:610-625). Declared on:LightProfileLinear(("intensity",)),Basis(nothing — it has no model-level amplitude; the solved amplitudes are one per member Gaussian, and the docs say so),PointSolved(("centre",)),aa.Pixelization(("reconstruction",)— the name of the solved quantity oninversion.reconstruction). Subclasses inherit it (linear Gaussian/Sersic/… getintensityfrom the base). solved_pathswidened to synthesise a row when unmatched (never silently ignored); a warning is not needed — the row appears,in_model_info=False.- Free redshift: extend rule R7 so
af.Model(cls)withclsa subclass ofint/float(al.Redshift(float)) is a row on its owner, not a one-pill child card (_is_dropped_model,graph_spec.py:581-586→issubclass). A free redshift then renders as the ordinary sampledredshiftpill on the Galaxy card; a fixed one stays theredshift = 0.5subtitle. - Presentation: solved pills read
name · solved; footer gainsN solved during fitting;counts["solved"]added; legend wording unchanged ("solved during fitting"). - Tint: documented as nesting depth, decorative, in both
model_cookbook.mdpages and the legend note. No code change. - Images follow each repo's live convention
docs/<section>/images/<page>/…referenced by raw URL, not a newdocs/images/. - Acceptance renders with real classes are built in
test_autolens(autofit may not import autolens): (a)imaging/modeling.py:269-285shape, (b) the MGE compositionmulti_gaussian_expansion/modeling.py:209-261with aPixelization(Delaunay(pixels=…, zeroed_pixels=…), ConstantSplit)source (assembled — no workspace script has both), (e)group/modeling.py:285-348with eight synthetic extra-galaxy centres (the shipped dataset has one; the epic's count is eight and phase 1's doubles already use eight). Counts are pinned as measured with the epic targets in comments; the structural invariants (two30 componentsplates split onell_comps,intensity · solvedinside the plate,areas_factor · missing, per-galaxy fixed centres) are the assertions. - Lens cookbook: the prompt's "6 stages" are 7
print(model.info)calls (:82, :117, :166, :200, :256, :319, :373); the last re-prints the JSON reload. Figures go after the six distinct stages; the JSON stage gets one sentence ("the reloaded model draws identically"). Two additions so the page actually shows the semantics this phase exists for: the__Redshift Free__stage builds and prints its model (today it builds nothing) and gets a figure; a new__Solved Parameters__stage after it composeslp_linearbulge +Pixelization(Delaunay, ConstantSplit)source +ps.PointSolvedand shows thesolved/missingencodings with the many-to-manymodel.infonote. The galaxy cookbook gets figures at its 8 stages (7 distinct + the JSON reload sentence) — no new stage.
High-level plan
- PyAutoFit:
__solved_parameters__protocol + additivesolved_paths+ R7 for float subclasses + solved counts/text; tests. - PyAutoArray + PyAutoGalaxy: declare the protocol on
Pixelization,LightProfileLinear,PointSolved; PyAutoGalaxy tests over real classes (linear intensity solved, Basis no amplitude, PointSolved centre solved, Delaunayareas_factormissing, free redshift pill, fixed redshift subtitle). - PyAutoLens: real-class acceptance tests for (a)/(b)/(e), the
make_figures.py+ committed PNGs,docs/general/model_cookbook.mdrefreshed to the script (it has drifted toal.lp.*) with figures,docs/overview/overview_3_features.mdMGE + pixelization sections each gain the model figure. PyAutoGalaxy docsmodel_cookbook.mdlikewise. - Ship four library PRs (
pending-release), merged in order PyAutoFit → PyAutoArray → PyAutoGalaxy → PyAutoLens. - Workspaces:
autolens_workspace/scripts/guides/modeling/cookbook.py(figures at 6 stages + Redshift Free fix + Solved Parameters stage),autogalaxy_workspace/scripts/guides/modeling/cookbook.py(figures at 7 stages), notebooks regenerated,model_figure: falseadded to bothconfig/output.yaml(they carry the full key list, so this is the phase-6 rollout for these two repos done early and cheaply). Two workspace PRs behind the library-first gate.
Detailed plan — libraries
PyAutoFit (autofit/graph_spec.py, autofit/model_figure/presentation.py)
_solved_parameter_names(obj):getattr(getattr(obj, "cls", None), "__solved_parameters__", ()), tuple of str; documented in the module docstring next to the other protocol probes. In_node, after the__dict__walk: for each declared name not already a row/child at that path, appendParamRow(name, path+(name,), sampling="solved", provenance=Provenance("solved-by-fit"), in_model_info=False); a declared name that IS a slot (e.g. a subclass that re-exposesintensity) is re-tagged, not duplicated.Provenance.kinddocstring gainssolved-by-fit.solved_paths: keep the re-tag; after the tree is built, any remaining unmatched path is attached as a synthesised solved row on its owner node (owner missing → root), reusing theattach_latentsshape (graph_spec.py:1226-1264; factor the shared "attach rows by owner path" helper).- R7:
_is_dropped_model→cls in (int, float) or (isinstance(cls, type) and issubclass(cls, (int, float))); the row keeps the prior/constant state and the class name is not shown (documented). _counts: add"solved"(tuple components counted per scalar likemissing).path_indexentries for solved rows: empty tuple (already thein_model_info=Falserule).- Presentation:
_text→f"{name} · solved"; footer_plural(counts["solved"], "parameter solved during fitting", "parameters solved during fitting"); legend line unchanged. Platerepeatsline: a solved member row readsintensity solved. - Tests (
test_autofit/graph_spec/+test_autofit/model_figure/): a double class with__solved_parameters__ = ("intensity",)and a subclass; apassclass with("centre",); unmatchedsolved_pathssynthesises; declared name that is a slot re-tags;Model(FloatSubclass)is a row; determinism andto_dictstability; footer/text. Updatelens_doubles.pydocstring lines 26-30 to sayareas_factornow lives on theDelaunaymesh (the double keeps the old shape deliberately).
PyAutoArray — autoarray/inversion/pixelization.py
class Pixelization: __solved_parameters__ = ("reconstruction",)with a 3-line comment naming the protocol and PyAutoFit'sgraph_spec.
PyAutoGalaxy
autogalaxy/profiles/light/linear/abstract.pyLightProfileLinear.__solved_parameters__ = ("intensity",);autogalaxy/profiles/point_sources.pyPointSolved.__solved_parameters__ = ("centre",);Basisuntouched (comment inbasis.pydocstring: no amplitude of its own — members carry the solved intensities).test_autogalaxy/profiles/test_model_figure_semantics.py(Agg): the six real-class assertions in decision 2 viaaf.GraphSpec.from_model+af.ModelPlotter(...).presentation();af.Model(ag.mesh.Delaunay)yields threemissingrows (pixels,zeroed_pixels,areas_factor) andDelaunay(pixels=…, zeroed_pixels=…)leaves exactlyareas_factormissing.docs/general/model_cookbook.md: figures after the stages that mirror the galaxy cookbook, PNGs underdocs/general/images/model_cookbook/viamake_figures.pythere; tint note; many-to-many note where the MGE appears indocs/overview/overview_3_features.md(:46region) — add the MGE model figure there.
PyAutoLens
test_autolens/model_figure/test_acceptance_lens.py(Agg): builds (a)/(b)/(e) with real classes (helpers intest_autolens/model_figure/lens_models.py), asserts the invariants + pinned counts, renders totmp_path, checks widths ≤ 1400 px and determinism of the layout dict.docs/general/images/model_cookbook/make_figures.py+ PNGs:simple_lens,complex_lens,four_galaxies,concise,prior_custom,model_custom,redshift_free,solved_parameters(cookbook stages) and the acceptance trioacceptance_simple_lens,acceptance_mge_pixelized,acceptance_group_scale;docs/overview/images/overview_3/gainsmge_model.png,pixelization_model.png,point_model.png.docs/general/model_cookbook.md: re-synced to the script (lp_linear, the Redshift Free and Solved Parameters stages, drop-or-mark the script-less "Multi Galaxy, Group and Cluster Models" section as pointing atgroup/modeling.pywith the group acceptance figure), figures embedded after eachmodel.infoblock, plus a "Reading the figure" subsection: map vs legend, solved/missing are additional information absent frommodel.info, the plate-vs-model.infogrouping paragraph verbatim from the prompt, tint = nesting depth.docs/overview/overview_3_features.md: model figure beside the MGE (:128), pixelization and point-source sections.- Sphinx warning counts ≤ each repo's baseline.
Detailed plan — workspaces (after the four library PRs are open)
autolens_workspace/scripts/guides/modeling/cookbook.py: prose cell +af.ModelPlotter(model).figure()after:82, :117, :166, :200, :256, :319; one sentence at:373; Redshift Free buildsmodel, prints info, figure; new__Solved Parameters__section (lp_linear bulge,Pixelization(Delaunay(pixels=500, zeroed_pixels=0), ConstantSplit)source, aps.PointSolvedgalaxy) with the many-to-many/solved/missingprose. Runs headless underprofile_smoke.yamldefaults (explicit.figure()calls are unaffected byPYAUTO_SKIP_VISUALIZATION; no dataset, soPYAUTO_SMALL_DATASETSis moot).autogalaxy_workspace/scripts/guides/modeling/cookbook.py: figures after:50, :76, :107, :127, :149, :194; sentence at:61(repeat) and:227(JSON).- Both
config/output.yaml:model_figure: falseblock aftermodel_graph. - Regenerate only the two cookbook notebooks (
generate.py autolens/autogalaxy, run from inside each workspace); revert any drift twins. - Prose is tutorial prose → Opus; architect reviews it.
Delegation (Opus, one subagent per leg, heartbeat files + Monitor)
A. PyAutoFit protocol + tests. B. PyAutoArray + PyAutoGalaxy + PyAutoLens library edits, tests, acceptance renders, docs pages (starts after A's API is in the worktree; same worktree, disjoint repos). C. Test gates + ship_library for the four repos (four PRs, cross-referenced). D. Two workspace cookbooks + notebooks + config. E. ship_workspace (two PRs). Architect reviews the acceptance PNGs after B and the prose after D.
Verification
- Serial pytest:
test_autofit/graph_spec test_autofit/model_figure,test_autogalaxy/profiles/test_model_figure_semantics.py,test_autolens/model_figure; full suites of the four libraries before ship; black/pyflakes on touched files. - Committed PNGs ≤ 1400 px; MGE render shows two
30 componentsplates withintensity · solvedpills andareas_factor · missing; group render shows 8-member plate withcentre · fixed, varies by member. - Both cookbooks run headless from their workspace worktree roots with the smoke env; notebooks regenerated.
- CI:
lib-testson all four libraries (same-named dependency branches resolve automatically), docs builds, workspace smoke on the same-named library branches. Merge order PyAutoFit → PyAutoArray → PyAutoGalaxy → PyAutoLens → workspaces via/prm.
Out of scope
Flipping model_figure to default-true (separate human decision, ledger), phases 4–6, dotted lens latent keys (analysis= placement of magnification etc. on the root card — noted as a follow-up prompt), the autofit>= floor bump in PyAutoGalaxy (only needed once a release depends on the protocol; noted in the PR).
Original Prompt
Click to expand starting prompt
Model figures phase 3 — lens semantics and cookbooks
Type: feature
Target: PyAutoLens
Repos:
- PyAutoLens
- PyAutoGalaxy
- autolens_workspace
- autogalaxy_workspace
Themes: - visualization
- mge
- pixelization
Difficulty: medium
Autonomy: supervised
Priority: normal
Status: formalised
Consequence: judge
Review-minutes: 25
Unattended: ready
Epic: model-figures
Phase: 3
Filed: 2026-09-10
Phase 2 shipped 2026-09-11 — complete/2026/09/model-figures-renderer.md (PyAutoFit#1614 merged, autofit_workspace#152 merged); this phase is unblocked.
(Deliberately not in a Blocked-by: header: that key is graded against GitHub
refs and cannot name a Mind prompt path.)
Phase 3 of the model-figures epic. Ledger (brief, full design record, full
independent review): draft/feature/autofit/model_figures_epic.md. By this
phase the renderer is already correct on the acceptance models; phase 3 supplies
the lens domain semantics it renders and puts the figures into the lens
documentation.
Domain semantics hooks
PyAutoFit cannot know which parameters a lens fit solves rather than samples.
Supply that from PyAutoGalaxy / PyAutoLens as data the phase-1 spec consumes —
a registry or a small protocol, not hard-coded class-name strings inside
PyAutoFit:
- Linear light profiles (
al.lp_linear.*) andal.lp_basis.Basis—
intensityis absent from the model entirely; it is solved by the
inversion. It must be emitted as asolvedpill with the legend wording
"solved during fitting", or the figure silently misreports the profile as
having no amplitude. Beware the review's caveat: aBasis.intensitydrawn
alongside member intensities risks implying an additional solved amplitude —
decide and document where it belongs. - Pixelization — the source pixels are solved;
al.Pixelizationis a
3-deep chain (image_mesh/mesh/regularization) whose only free
parameters are the regularization coefficients.mesh_shapeandpixelsare
plain scalar attributes, not priors: rows on their owner, never boxes. al.ps.PointSolved— zero parameters; the centre is solved analytically.
al.ps.Pointhas two. Without the annotation the box looks empty and wrong.areas_factor— themissingstate. It is unset configuration, not a
solved quantity and not an absent one. This is an explicit review finding
(the prototype labelled aDelaunay.pixelspill "solved" when the real gap
wasareas_factor) and it is an epic acceptance case.- Redshift — fixed redshift renders as
redshift = 0.5under the galaxy
header (a presentation convenience, not a different semantic rule); a free
redshift (af.Model(al.Redshift)) keeps the ordinary sampled pill.
Class-family header tint — decide it here
The review's nit: either explain class-family header colours (light / mass /
pixelization) in the legend, or keep them strictly decorative and say so.
Do not ship an unexplained semantic-looking encoding. Whichever way it goes,
record the decision in the docs page.
Documentation deliverables
autolens_workspace/scripts/guides/modeling/cookbook.py— 442 lines with 6
model.infostages; each gains its figure.@PyAutoLens/docs/general/model_cookbook.mdand
@PyAutoLens/docs/overview/overview_3_features.md— PNGs committed under
docs/images/and referenced by raw URL, matching the existing pattern.- The equivalent PyAutoGalaxy /
autogalaxy_workspacecookbook stages where
they mirror the lens ones.
Document the plate-vs-model.info grouping difference
This is the one place the promised correspondence is genuinely many-to-many and
it must be written down for readers, not papered over:
The figure partitions by component: the MGE model shows two
30 components
plates, split because each basis holds its ownell_compspair.model.info
groups per parameter: it prints a single0 - 59block forcentre
spanning both figure plates, two separate ellipticity blocks, and individual
sigmablocks.
The contract that does hold, and which the docs should state: every displayed
element resolves to a path or grouped paths in model.info; every omission and
every added annotation (solved, missing) is explicit. Solved annotations
have no counterpart in model.info at all and must be identified as additional
information.
Acceptance
- The three lens acceptance figures from the ledger render truthfully with lens
semantics attached, and the renders are committed. - All 6 cookbook stages carry a figure; the docs pages build with the images.
- Once these pass, the per-search
output.yamlkeymodel_figure(phase 2) may
be proposed for default-true— a separate human decision, recorded in
the ledger, not taken inside this phase.
🤖 Generated with Claude Code
Activity
Jammy2211 commentedon Sep 11, 2026
Library PRs Created
Merge order (library-first):
Next: /start_workspace → autolens_workspace + autogalaxy_workspace cookbooks (in flight)
Jammy2211 commentedon Sep 11, 2026
Workspace PRs Created
Close-out: /prm — merge order PyAutoFit#1615 → PyAutoArray#550 → PyAutoGalaxy#616 → PyAutoLens#737 → autogalaxy_workspace → autolens_workspace (library-first gate; workspace CI runs on the same-named library branches).
Merge pull request #737 from PyAutoLabs/feature/model-figures-lens
Jammy2211 commentedon Sep 11, 2026
Shipped
Merged, in order: PyAutoFit#1615 · PyAutoArray#550 · PyAutoGalaxy#616 · PyAutoLens#737 · autogalaxy_workspace#240 · autolens_workspace#541 (all
pending-release).Class-declared
__solved_parameters__protocol in PyAutoFit'sgraph_spec(additive solved rows; R7 for float subclasses;Modelno longer leaksclsfrom a custom__new__), declared onLightProfileLinear,PointSolvedandPixelization; real-class lens acceptance tests + committed renders; model figures in both model cookbooks (docs pages and workspace scripts + notebooks),model_figure: falseadded to both workspace configs. Follow-ups filed in the Mind:af.Model.from_jsondrops zero-free-parameter components; tuple-component relations not yet annotated on pills.Epic
model-figures: phase 3 of 6 done; phase 4 (graphical plates) unblocked. Flippingmodel_figureto default-true stays a separate human decision.