From e93595596c69cd17ff9217a94ae27cb9ac90123b Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:03:21 +0100 Subject: [PATCH 1/5] fix: repair broken recipes in data-prep and model-building skills al_custom_profile's light-profile recipe used three APIs that don't exist in the installed stack (Dictable mixin, _radii_from_grid, _b_n) and told users to write priors YAML into the installed PyAutoGalaxy config, crossing the source-edit boundary. Rewritten around the real base-class helpers (elliptical_radii_grid_from, explicit Ciotti-Bertin b_n), no mixin, workspace config/priors/, and the no-default-priors gotcha made explicit; the corrected recipe was executed against the installed stack. al_prepare_imaging_data's minimal recipe ended with a bare interactive plot call contradicting the plot-output rules; it now saves and prints the dataset.png path and holds for the real-data inspection. al_build_interferometer_model dropped a pointless settings=al.Settings() and documents the transformer choice where it actually lives (Interferometer.from_fits). Co-Authored-By: Claude Fable 5 --- skills/al_build_interferometer_model.md | 12 ++-- skills/al_custom_profile.md | 89 +++++++++++++++---------- skills/al_prepare_imaging_data.md | 14 +++- 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/skills/al_build_interferometer_model.md b/skills/al_build_interferometer_model.md index 3914c15..f2d1df1 100644 --- a/skills/al_build_interferometer_model.md +++ b/skills/al_build_interferometer_model.md @@ -48,6 +48,9 @@ dataset = al.Interferometer.from_fits( noise_map_path=dataset_path / "noise_map.fits", uv_wavelengths_path=dataset_path / "uv_wavelengths.fits", real_space_mask=real_space_mask, + # transformer_class defaults to al.TransformerNUFFT — the NUFFT that maps the + # model image to visibilities. al.TransformerDFT is exact but only viable for + # small visibility counts. ) # Identical model composition to the imaging case. @@ -64,12 +67,9 @@ source = af.Model( ) model = af.Collection(galaxies=af.Collection(lens=lens, source=source)) -# Difference from imaging: AnalysisInterferometer, plus a transformer class that -# converts the model image to visibilities via NUFFT. -analysis = al.AnalysisInterferometer( - dataset=dataset, - settings=al.Settings(), -) +# Difference from imaging: AnalysisInterferometer computes the likelihood in +# visibility space (the transformer was chosen on the dataset above). +analysis = al.AnalysisInterferometer(dataset=dataset) ``` Source citations: diff --git a/skills/al_custom_profile.md b/skills/al_custom_profile.md index 5cc3320..7d96026 100644 --- a/skills/al_custom_profile.md +++ b/skills/al_custom_profile.md @@ -24,18 +24,21 @@ Canonical reference: `autolens_workspace:scripts/guides/advanced/add_a_profile.p ## Branch — custom light profile -Subclass `LightProfile` (or `LightProfileLinear` for a profile whose intensity is -solved during the fit). Implement `image_2d_from(grid)`: +Subclass `ag.LightProfile` (or `LightProfileLinear` for a profile whose intensity is +solved during the fit). Implement `image_2d_from(grid)` — the base class provides the +geometry helpers (`elliptical_radii_grid_from` returns each coordinate's elliptical +radius from the profile centre; the returned object exposes the raw values as +`.array`). No serialisation mixin is needed: profiles serialise to `tracer.json` +automatically from their constructor arguments. ```python # scripts/my_profile.py import numpy as np import autogalaxy as ag -from autoconf.dictable import Dictable -class TwistedSersic(ag.LightProfile, Dictable): - """Sersic light profile with isophote position-angle twist with radius.""" +class TruncatedSersic(ag.LightProfile): + """Sersic light profile with an exponential outer truncation.""" def __init__( self, @@ -44,41 +47,47 @@ class TwistedSersic(ag.LightProfile, Dictable): intensity=1.0, effective_radius=1.0, sersic_index=2.0, - twist_rate=0.0, + truncation_radius=5.0, ): super().__init__(centre=centre, ell_comps=ell_comps) self.intensity = intensity self.effective_radius = effective_radius self.sersic_index = sersic_index - self.twist_rate = twist_rate + self.truncation_radius = truncation_radius + + @property + def sersic_constant(self): + # b_n via the Ciotti & Bertin (1999) expansion — the base class does not + # provide it for custom profiles, so state it explicitly. + n = self.sersic_index + return 2.0 * n - 1.0 / 3.0 + 4.0 / (405.0 * n) + 46.0 / (25515.0 * n**2) def image_2d_from(self, grid, **kwargs): - # Apply the elliptical + twisted transform manually, then evaluate Sersic. - radii = self._radii_from_grid(grid) # helper method - intensity = self.intensity * np.exp( - -self._b_n() * ((radii / self.effective_radius) ** (1.0 / self.sersic_index) - 1.0) + radii = self.elliptical_radii_grid_from(grid) + sersic = self.intensity * np.exp( + -self.sersic_constant + * ((radii.array / self.effective_radius) ** (1.0 / self.sersic_index) - 1.0) ) - return intensity + return sersic * np.exp(-((radii.array / self.truncation_radius) ** 2)) ``` Source citations: -- `PyAutoGalaxy:autogalaxy/profiles/light/abstract.py` — `LightProfile` base class - and required interface. +- `PyAutoGalaxy:autogalaxy/profiles/light/abstract.py` — `LightProfile` base class, + required interface, and the radii-grid helpers. - `PyAutoGalaxy:autogalaxy/profiles/light/standard/sersic.py` — Sersic implementation - to copy structure from. -- `PyAutoConf:autoconf/dictable.py` — `Dictable` mixin so the profile serialises to - `tracer.json`. + to copy structure from (note how it reads `grid_radii.array`). -Use in a model: +Use in a model (both files live in `scripts/`, so a sibling import works when the +script is run as `python scripts/build_model.py`): ```python import autofit as af -from work.my_profile import TwistedSersic +from my_profile import TruncatedSersic lens = af.Model( ag.Galaxy, redshift=0.5, - bulge=af.Model(TwistedSersic), # new profile, slots into the af.Model API + bulge=af.Model(TruncatedSersic), # new profile, slots into the af.Model API ) ``` @@ -89,8 +98,8 @@ Subclass `MassProfile` and implement at least `convergence_2d_from(grid)` and deflection has no closed form. ```python -class CustomNFW(ag.mp.MassProfile, Dictable): - """NFW with a custom inner-slope tweak.""" +class CustomNFW(ag.mp.MassProfile): + """NFW with a custom inner-slope tweak (spherical).""" def __init__(self, centre=(0.0, 0.0), kappa_s=1.0, scale_radius=1.0, inner_slope=1.0): super().__init__(centre=centre, ell_comps=(0.0, 0.0)) @@ -99,7 +108,7 @@ class CustomNFW(ag.mp.MassProfile, Dictable): self.inner_slope = inner_slope def convergence_2d_from(self, grid, **kwargs): - r = self._radii_from_grid(grid) / self.scale_radius + r = self.radial_grid_from(grid).array / self.scale_radius return self.kappa_s / (r ** self.inner_slope * (1.0 + r) ** (3.0 - self.inner_slope)) def deflections_yx_2d_from(self, grid, **kwargs): @@ -123,7 +132,7 @@ class that interpolates on the grid: ```python from scipy.interpolate import RegularGridInterpolator -class TabulatedConvergence(ag.mp.MassProfile, Dictable): +class TabulatedConvergence(ag.mp.MassProfile): def __init__(self, centre, table_path): super().__init__(centre=centre, ell_comps=(0.0, 0.0)) self._interp = self._load_table(table_path) @@ -137,17 +146,25 @@ it numerically but at a runtime cost. Profile your fit if this is slow. ## Priors and defaults -For the new profile to play nicely in `af.Model`, give each parameter a sensible -default prior. Either set it in the model declaration: +A custom profile has **no default priors** — `af.Model(TruncatedSersic)` reports +`prior_count == 0` and cannot be fit until every free parameter is given one. Set them +explicitly in the model declaration: ```python -profile = af.Model(TwistedSersic) -profile.twist_rate = af.UniformPrior(lower_limit=-1.0, upper_limit=1.0) +profile = af.Model(TruncatedSersic) +profile.centre.centre_0 = af.GaussianPrior(mean=0.0, sigma=0.1) +profile.centre.centre_1 = af.GaussianPrior(mean=0.0, sigma=0.1) +profile.intensity = af.LogUniformPrior(lower_limit=1e-4, upper_limit=1e2) +profile.effective_radius = af.UniformPrior(lower_limit=0.0, upper_limit=5.0) +profile.sersic_index = af.UniformPrior(lower_limit=0.5, upper_limit=8.0) +profile.truncation_radius = af.UniformPrior(lower_limit=0.5, upper_limit=10.0) ``` -Or — for repeated use — add a default-priors YAML under -`PyAutoGalaxy:autogalaxy/config/priors/` keyed by class name. The conf system reads -it automatically. +Or — for repeated use — add a default-priors YAML under **this workspace's** +`config/priors/` (mirror the schema of the existing files in `config/priors/light/`), +which the conf system reads automatically. Never write the YAML into the installed +PyAutoGalaxy's own config — that edits library source, which the source-edit boundary +forbids, and it is lost on the next upgrade. ## Verification @@ -155,13 +172,15 @@ it automatically. # Sanity: image_2d_from returns the right shape and finite values. import autoarray as aa grid = aa.Grid2D.uniform(shape_native=(50, 50), pixel_scales=0.05) -prof = TwistedSersic(centre=(0.0, 0.0), intensity=1.0, effective_radius=0.5, sersic_index=2.0, twist_rate=0.1) +prof = TruncatedSersic(centre=(0.0, 0.0), intensity=1.0, effective_radius=0.5, sersic_index=2.0) img = prof.image_2d_from(grid=grid) -assert img.shape == (2500,) and np.isfinite(img).all() +assert np.asarray(img).shape == (2500,) and np.isfinite(np.asarray(img)).all() ``` -For mass profiles, also sanity-check `convergence_2d_from` against a known case (set -`twist_rate=0`, your custom Sersic should match the built-in Sersic). +Also sanity-check against a known case: with the custom behaviour switched off +(here, `truncation_radius` set very large), the profile should match the built-in it +extends (`ag.lp.Sersic` with the same parameters). For mass profiles do the same with +`convergence_2d_from` against the canonical implementation. ## Combine diff --git a/skills/al_prepare_imaging_data.md b/skills/al_prepare_imaging_data.md index 99f8396..e121c26 100644 --- a/skills/al_prepare_imaging_data.md +++ b/skills/al_prepare_imaging_data.md @@ -84,9 +84,21 @@ over_sample_size = al.util.over_sample.over_sample_size_via_radial_bins_from( ) dataset = dataset.apply_over_sampling(over_sample_size_lp=over_sample_size) -aplt.subplot_imaging_dataset(dataset=dataset) +# Save the inspection plot — never rely on interactive display. +plot_dir = Path("scripts/scratch") / dataset_path.name +plot_dir.mkdir(parents=True, exist_ok=True) +aplt.subplot_imaging_dataset( + dataset=dataset, + output_path=str(plot_dir), + output_filename="dataset", + output_format="png", +) +print(f"Dataset plot saved to: {plot_dir.resolve()}") ``` +After running, quote the printed `dataset.png` path to the user and hold for their +confirmation that they have inspected it (per the real-data gate) before moving on. + Source citations: - `PyAutoArray:autoarray/dataset/imaging/dataset.py` — `Imaging.from_fits`, `apply_mask`, `apply_over_sampling`. From cb026ed1f594fe4bd02a370aab6763e3a51b1664 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:07:21 +0100 Subject: [PATCH 2/5] fix: repair search-config drift and the fabricated SLaM recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit al_configure_search: iterations_per_update no longer exists on Nautilus (now iterations_per_quick/full_update); UltraNest and PySwarms are not exposed by the installed autofit, so the other-searches menu now lists what actually ships (DynestyDynamic, Zeus, BFGS/LBFGS, Drawer). al_run_slam_pipeline: the recipe imported a slam module and slam_pipeline_main driver that have never existed — replaced with the real pattern grounded in slam_start_here.py (stages are inline functions chained by af.Result; copy the template via init-slam and adapt), with the positions-likelihood wiring verified against the installed API. Co-Authored-By: Claude Fable 5 --- skills/al_configure_search.md | 13 ++++--- skills/al_run_slam_pipeline.md | 71 ++++++++++++++-------------------- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/skills/al_configure_search.md b/skills/al_configure_search.md index 642fc8e..2683a9f 100644 --- a/skills/al_configure_search.md +++ b/skills/al_configure_search.md @@ -1,6 +1,6 @@ --- name: al_configure_search -description: Pick and tune a non-linear search for a lens-modelling fit. Defaults to Nautilus (nested sampling, the recommended choice); covers Dynesty, Emcee, Zeus, UltraNest, and the gradient/swarm options for completeness. Sets sampler-specific knobs (live points, walkers, tolerance) and the output `path_prefix` / `name` that determine where results land. Pairs with `al_run_search`, which actually calls `search.fit`. +description: Pick and tune a non-linear search for a lens-modelling fit. Defaults to Nautilus (nested sampling, the recommended choice); covers Dynesty, Emcee, Zeus, and the gradient/optimizer options for completeness. Sets sampler-specific knobs (live points, walkers, tolerance) and the output `path_prefix` / `name` that determine where results land. Pairs with `al_run_search`, which actually calls `search.fit`. --- # Configuring a non-linear search @@ -19,7 +19,7 @@ search (what nested sampling does, what MCMC does, what gradient descent does), - *"How complex is the model?"* — number of free parameters and whether any are expected to be multi-modal. Multi-modality strongly favours nested sampling - (Nautilus / Dynesty / UltraNest) over MCMC. + (Nautilus / Dynesty) over MCMC. - *"How fast does the likelihood evaluate?"* — fast (<1s with JAX) → can afford more live points or walkers. Slow → tighten the search. - *"Is this a first exploratory fit, or a final production run?"* — exploratory fits @@ -41,7 +41,7 @@ search = af.Nautilus( name="modeling_sie_sersic", unique_tag="", n_live=200, # 100 for exploration, 200 for production, 400+ for complex models - iterations_per_update=2500, + iterations_per_full_update=2500, number_of_cores=4, # parallel likelihood evaluations ) ``` @@ -52,7 +52,8 @@ Knobs to know: - `n_live` — more = more accurate posterior, slower. Start at 200; go to 400+ only if the posterior looks multi-modal or thin. - `number_of_cores` — set to your CPU core count for parallel likelihood eval. -- `iterations_per_update` — how often the search writes intermediate samples to disk. +- `iterations_per_full_update` / `iterations_per_quick_update` — how often the search + writes full output (samples, visualisation) vs quick intermediate updates to disk. ## Branch — Dynesty @@ -88,8 +89,8 @@ Source: `PyAutoFit:autofit/non_linear/search/mcmc/emcee.py`. ## Branch — Other searches -Zeus (ensemble slice), UltraNest (nested sampling alternative), PySwarms (particle swarm), -BFGS (gradient descent for MLE), Drawer (random prior draws — debugging only). See +Zeus (ensemble slice MCMC), DynestyDynamic (dynamic nested sampling), BFGS / LBFGS +(gradient descent for MLE), Drawer (random prior draws — debugging only). See [`wiki/core/api/searches.md`](../wiki/core/api/searches.md) for the comparison table. ## Output folder layout diff --git a/skills/al_run_slam_pipeline.md b/skills/al_run_slam_pipeline.md index a8a01b9..a2ea6b4 100644 --- a/skills/al_run_slam_pipeline.md +++ b/skills/al_run_slam_pipeline.md @@ -59,53 +59,42 @@ For the full conceptual treatment of SLaM, ## Branch — galaxy-scale imaging SLaM -The SLaM API lives in `autolens_workspace:slam/` (a sibling module in the workspace -that ships SLaM pipeline functions). The workspace doesn't ship its own copy; the recipe -below mirrors `slam_start_here.py` and depends on the workspace pipelines being -available. +There is **no importable SLaM module**: each SLaM script defines its stages as plain +Python functions inline (`source_lp`, `source_pix_1`, `source_pix_2`, `light_lp`, +`mass_total`, …) and chains them by passing each stage's `af.Result` into the next. +`slam_start_here.py` is the canonical, fully-documented instance of that pattern — +so the recipe is *copy and adapt the template*, not write from scratch: + +```bash +# The `init-slam` skill automates this copy; manually it is: +cp /scripts/guides/modeling/slam_start_here.py scripts/slam_pipeline.py +``` + +Then adapt the template's clearly-marked sections to the user's lens — dataset path +and pixel scale, mask radius, lens/source redshifts, the LIGHT LP parameterisation, +and the final MASS TOTAL model. The chaining skeleton you are editing looks like: ```python -# scripts/slam_pipeline.py -from autoconf import jax_wrapper -from pathlib import Path -import autofit as af -import autolens as al -from autolens_workspace.slam import slam_pipeline_main # workspace dep - -dataset_path = Path("dataset/imaging/") -dataset = al.Imaging.from_fits( - data_path=dataset_path / "data.fits", - noise_map_path=dataset_path / "noise_map.fits", - psf_path=dataset_path / "psf.fits", - pixel_scales=0.06, -) -mask = al.Mask2D.circular( - shape_native=dataset.shape_native, pixel_scales=dataset.pixel_scales, radius=3.0 -) -dataset = dataset.apply_mask(mask=mask) - -# Optional: image positions to penalise unphysical mass models. -positions = al.from_json(file_path=dataset_path / "positions.json") # returns a Grid2DIrregular -positions_likelihood = al.PositionsLH(positions=positions, threshold=0.5) +# Inside scripts/slam_pipeline.py (structure of the copied template, abridged): +source_lp_result = source_lp(analysis=analysis, ...) +source_pix_result_1 = source_pix_1(analysis=analysis, source_lp_result=source_lp_result) +source_pix_result_2 = source_pix_2(analysis, source_lp_result, source_pix_result_1) +light_result = light_lp(analysis, source_pix_result_1, source_pix_result_2) +mass_result = mass_total(analysis, source_pix_result_1, source_pix_result_2, light_result) +``` + +Image positions (recommended for pixelised sources, to penalise unphysical +demagnified solutions) load from JSON and wire into the stage functions via +`positions_likelihood_from` / `al.PositionsLH`: -# Driver — the actual SLaM pipeline functions live in autolens_workspace/slam/. -# See slam_start_here.py for the canonical invocation: SOURCE LP → SOURCE PIX → -# LIGHT LP → MASS TOTAL with their chaining of `result_*` objects. -slam_pipeline_main( - dataset=dataset, - redshift_lens=0.5, - redshift_source=1.0, - path_prefix="imaging//slam", - positions_likelihood=positions_likelihood, - mesh_shape=(35, 35), - # ... per-stage settings; defer to slam_start_here.py for the full list. -) +```python +positions = al.from_json(file_path=dataset_path / "positions.json") # Grid2DIrregular +positions_likelihood = al.PositionsLH(positions=positions, threshold=0.5) ``` Source citations: -- `autolens_workspace:scripts/guides/modeling/slam_start_here.py` — canonical invocation. -- `autolens_workspace:slam/` — pipeline function definitions (the actual SLaM code is - in the workspace, not in PyAutoLens itself). +- `autolens_workspace:scripts/guides/modeling/slam_start_here.py` — the canonical + template: stage functions, their chaining, and every per-stage setting. - `PyAutoLens:autolens/analysis/positions.py` — `PositionsLH`. ## Branch — interferometer SLaM From 8b82ff2180f64beb8b6b22ec991a47c59a0a2021 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:10:56 +0100 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20results-skill=20drift=20=E2=80=94=20?= =?UTF-8?q?LensCalc,=20debug=20load=20pattern,=20plane=20plotter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracer no longer carries einstein_mass_angular_from (lensing derived quantities moved to al.LensCalc.from_tracer/from_mass_obj) — al_load_results now shows the LensCalc route with a source citation. al_debug_fit_failure's residual-inspection snippet mixed aggregator generations with undefined names; replaced with the al_load_results- consistent tracer.json + FitImaging pattern with explicit imports and saved (not interactive) plot output. al_plot_tracer's multi-plane branch referenced the retired PlanePlotter, contradicting its own flat-function-API preamble. Every symbol in both plotting skills verified against the installed stack. Co-Authored-By: Claude Fable 5 --- skills/al_debug_fit_failure.md | 23 ++++++++++++++++------- skills/al_load_results.md | 9 +++++++-- skills/al_plot_tracer.md | 7 ++++--- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/skills/al_debug_fit_failure.md b/skills/al_debug_fit_failure.md index 907ecd6..ac272c1 100644 --- a/skills/al_debug_fit_failure.md +++ b/skills/al_debug_fit_failure.md @@ -35,18 +35,27 @@ Read the search log: `output//.../search.log`. Common causes: ## Branch — search completed but residuals are huge -Load the result and look at the fit: +Load the result and look at the fit (same pattern as +[`al_load_results`](./al_load_results.md) — load the max-log-likelihood tracer from +JSON, reload the dataset, rebuild the fit): ```python -import autofit as af +from pathlib import Path +from autoconf.dictable import from_json +import autolens as al +import autolens.plot as aplt -agg = af.Aggregator(af.db.open_database("sqlite://")) -agg.add_directory("output/imaging//") -result = list(agg.values("samples"))[-1] # most recent run +out = Path("output/imaging///") +tracer = from_json(file_path=out / "files" / "tracer.json") + +# Reload the dataset exactly as the fit script did (path, pixel_scales, mask). +dataset = ... # see al_prepare_imaging_data -tracer = result.max_log_likelihood_tracer fit = al.FitImaging(dataset=dataset, tracer=tracer) -aplt.subplot_fit_imaging(fit=fit) +aplt.subplot_fit_imaging( + fit=fit, output_path="scripts/scratch/debug/", output_filename="fit", + output_format="png", +) ``` Look at the residual map. Common signatures: diff --git a/skills/al_load_results.md b/skills/al_load_results.md index 958b2b8..581a9b4 100644 --- a/skills/al_load_results.md +++ b/skills/al_load_results.md @@ -79,10 +79,15 @@ in the model. ```python lens = tracer.galaxies[0] einstein_radius_arcsec = lens.mass.einstein_radius # for parametric mass -# Or compute it numerically from the convergence: -einstein_mass = tracer.einstein_mass_angular_from(grid=dataset.grid) +# Or compute numerically — lensing calculations live on al.LensCalc: +lens_calc = al.LensCalc.from_tracer(tracer=tracer) +einstein_mass = lens_calc.einstein_mass_angular_from(grid=dataset.grid) ``` +Source: `PyAutoGalaxy:autogalaxy/operate/lens_calc.py` — `LensCalc.from_tracer` / +`from_mass_obj` and the derived-quantity methods (Einstein radius/mass, shear, +magnification). + For the API surface, read [`wiki/core/concepts/galaxy_and_plane.md`](../wiki/core/concepts/galaxy_and_plane.md). ## Branch — posterior (`Samples`) diff --git a/skills/al_plot_tracer.md b/skills/al_plot_tracer.md index 41c01df..9ed290c 100644 --- a/skills/al_plot_tracer.md +++ b/skills/al_plot_tracer.md @@ -103,9 +103,10 @@ caustics directly — build the magnification map from the deflection Jacobian ## Branch — multi-plane systems (>2 redshifts) When the `Tracer` has more than two lens planes (e.g. cluster lenses), the source -plane is the *final* plane. Plot each intermediate plane via -`tracer.planes[i].galaxies[j]` and the dedicated `PlanePlotter`. See -`PyAutoLens:autolens/lens/plot/`. +plane is the *final* plane. There is no dedicated plane plotter in the flat function +API — extract each intermediate plane's galaxies via `tracer.planes[i]`, evaluate +their images on the traced grid for that plane, and pass the arrays to +`aplt.plot_array`. See `PyAutoLens:autolens/lens/plot/`. [`wiki/core/concepts/tracer.md`](../wiki/core/concepts/tracer.md) — multi-plane ray tracing explanation. From 2537ab73686593a1861e988295ae7cf3a6c8f6bc Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:12:43 +0100 Subject: [PATCH 4/5] fix: inversion attribute drift + surface the #332 regression where users hit it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit al_inspect_source_reconstruction's snippet used inversion attributes that never existed (reconstruction_dict_of_mapper, reconstructed_data_dict); replaced with the verified 2026.7 names (reconstruction, reconstruction_dict, mapped_reconstructed_data) and lead with subplot_fit_imaging whose final panel is the reconstruction. al_chain_searches' phase-2 recipe used exactly the Delaunay + ConstantSplit combination that open regression PyAutoArray#332 crashes on, with no warning — switched to RectangularUniform + Constant with the regression note inline; the warning block in the inspection skill now names both broken combinations and is dated to the current stack. Co-Authored-By: Claude Fable 5 --- skills/al_chain_searches.md | 12 ++++--- skills/al_inspect_source_reconstruction.md | 41 +++++++++++++--------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/skills/al_chain_searches.md b/skills/al_chain_searches.md index 244ca48..cbc86c6 100644 --- a/skills/al_chain_searches.md +++ b/skills/al_chain_searches.md @@ -77,8 +77,8 @@ result_1 = search_1.fit(model=model_1, analysis=analysis) """ __Phase 2__ -Swap in a pixelised source (`al.mesh.Delaunay` + `al.reg.ConstantSplit`) for maximum source -flexibility, inheriting the lens mass and shear from phase 1's posterior so the search +Swap in a pixelised source for maximum source flexibility, +inheriting the lens mass and shear from phase 1's posterior so the search starts from a good region of parameter space rather than the prior. `result_1.model` returns a new `af.Model` whose priors are the previous search's posterior, keeping the parameters free to vary. @@ -95,8 +95,12 @@ source_2 = af.Model( redshift=1.0, pixelization=af.Model( al.Pixelization, - mesh=al.mesh.Delaunay, - regularization=al.reg.ConstantSplit, + # NOTE: Delaunay + ConstantSplit is the standard production choice, but a + # known regression (PyAutoArray #332, still open) crashes Delaunay inside + # FitImaging and breaks ConstantSplit on RectangularUniform. Until it is + # fixed, use the combination below. + mesh=al.mesh.RectangularUniform, + regularization=al.reg.Constant, ), ) model_2 = af.Collection(galaxies=af.Collection(lens=lens_2, source=source_2)) diff --git a/skills/al_inspect_source_reconstruction.md b/skills/al_inspect_source_reconstruction.md index 55fa6df..6c86f55 100644 --- a/skills/al_inspect_source_reconstruction.md +++ b/skills/al_inspect_source_reconstruction.md @@ -45,41 +45,48 @@ After running, the agent quotes `PLOT_DIR.resolve()` and offers function-style plot API is documented in [`wiki/core/api/plotting.md`](../wiki/core/api/plotting.md). -> ⚠️ **Known regression in `2026.5.21.1`.** `Delaunay` and `KNNBarycentric` -> currently crash inside `FitImaging` (`'NoneType' object has no attribute -> 'array'`). Use `al.mesh.RectangularUniform` for now; tracking: +> ⚠️ **Known regression (still open as of `2026.7.6`).** `Delaunay` and +> `KNNBarycentric` crash inside `FitImaging` (`'NoneType' object has no attribute +> 'array'`), and `ConstantSplit` is broken on `RectangularUniform`. Use +> `al.mesh.RectangularUniform` + `al.reg.Constant` for now; tracking: > . ## Branch — source-plane reconstruction Rebuild the fit so the inversion is computed, then plot the components: +The quickest full view is the fit subplot — when the fit uses an inversion its +final panel *is* the source-plane reconstruction: + ```python # `dataset` from al_prepare_imaging_data, `tracer` from al_load_results. fit = al.FitImaging(dataset=dataset, tracer=tracer) + +aplt.subplot_fit_imaging( + fit=fit, output_path=str(PLOT_DIR), output_format="png", +) +``` + +For component-level access, the inversion exposes (names verified against the +2026.7 stack): + +```python inv = fit.inversion -# Source-plane reconstruction on the pixelisation mesh: -aplt.plot_array(array=inv.reconstruction_dict_of_mapper(mapper_index=0), +# Image-plane image the linear solution reconstructs (Array2D — plottable): +aplt.plot_array(array=inv.mapped_reconstructed_data, output_path=str(PLOT_DIR), - output_filename="reconstruction", + output_filename="mapped_reconstruction", output_format="png") -# Source-plane signal map (per-pixel inferred surface brightness): -aplt.plot_array(array=inv.reconstructed_data_dict[next(iter(inv.reconstructed_data_dict))], - output_path=str(PLOT_DIR), - output_filename="reconstructed_data", - output_format="png") +# Source-plane solution: a 1D vector of per-source-pixel intensities +# (keyed per mapper in inv.reconstruction_dict). For a RectangularUniform +# mesh, reshape to the mesh's native 2D shape to plot it. +reconstruction = inv.reconstruction print(f"Saved to: {PLOT_DIR.resolve()}") ``` -> The exact attribute names on `Inversion` (`reconstruction_dict_of_mapper`, -> `reconstructed_data_dict`, etc.) evolved alongside the API rewrite — -> `dir(inv)` is the canonical reference for which arrays the current release -> exposes. The inversion plot helpers in `aplt.*` themselves are no longer -> public. - Source: `PyAutoArray:autoarray/inversion/inversion/`. ## Branch — mesh + regularisation diagnostics From 4e455e3cd327575ef5088c845b02a5390c0102b9 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:14:43 +0100 Subject: [PATCH 5/5] docs: platform-aware plot-open offers and a style-guide typo The plot-path announcement hardcoded the macOS-only 'open' command in AGENTS.md and twice in _style.md; the offer is now platform-aware (open / xdg-open / explorer.exe-wslview). Fixes a doubled 'the' in _style.md's Iteration section. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 3 ++- skills/_style.md | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dc6038a..e840328 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,7 +180,8 @@ When **not** in maintainer mode, commit at natural checkpoints (a script + its straight to the `aplt.*` plotting call (e.g. `aplt.subplot_imaging_dataset`, `aplt.subplot_fit_imaging`) — there is no separate `aplt.Output`/`MatPlot2D` object. Then `print(...)` the absolute path, and after running **quote that absolute path** and - offer *"want me to `open `?"* — don't just say "plot saved". One offer per plot. + offer to open it (platform opener: `open` on macOS, `xdg-open` on Linux, + `explorer.exe`/`wslview` from WSL) — don't just say "plot saved". One offer per plot. --- diff --git a/skills/_style.md b/skills/_style.md index 24a1961..d3316c1 100644 --- a/skills/_style.md +++ b/skills/_style.md @@ -279,8 +279,8 @@ the user-facing content above should read like a conversation arc, not a recipe. - Point at the wiki by relative path every time you teach a concept. - For newcomers, surface the relevant HowToLens notebook before the code block, not after. See "Newcomer mode" in Adaptive depth above. -- When a script produces plot files, quote the absolute path and offer to - `open ` (macOS). See "Plot output and path announcement" below. +- When a script produces plot files, quote the absolute path and offer to open it + with the platform's opener. See "Plot output and path announcement" below. **Don't** @@ -338,9 +338,9 @@ Skills that produce visualisations save them through the function-style because each `aplt.*` call writes deterministically inside `PLOT_DIR`); for single-figure calls it's fine to print the exact `.png` path instead. 3. **The agent quotes the path back** to the user after running the script - and offers *"want me to `open `?"* — one offer per plot run, not - nagging. `open` on macOS opens a directory in Finder or a PNG in - Preview. + and offers to open it — one offer per plot run, not nagging. Use the + platform's opener: `open ` on macOS, `xdg-open ` on Linux, + `explorer.exe` (or `wslview`) from WSL. The full convention — committed Python lives in `scripts/`; throwaway plots and data dumps go to the gitignored `scripts/scratch/` — is in `AGENTS.md` @@ -376,6 +376,6 @@ Rules: ## Iteration -This guide is the the workspace v1 writing guide. As patterns emerge, update this file in +This guide is the workspace v1 writing guide. As patterns emerge, update this file in the same change as whatever motivated the update. Note the change at the top of that PR description.