Repository navigation
Description
Overview
Restore and improve image-plane ↔ source-plane mapping visualization in PyAutoArray. The colour-matched mapping feature (source pixels highlighted in one colour, their image-plane counterparts in the same colour) died in 0cb75ebd (2025-07-21) and the plotter stack followed in 2026-03; today aplt.subplot_mappings computes peak source pixels and discards them. This phase lands plotting-agnostic Mapping / ImageRegion result objects, source clump finding on the mesh, a generic regions= polygon overlay for plot_array / plot_inversion_reconstruction, and a restored subplot_mappings that shows, in one look, how the brightest source clumps map to their multiple images. Phase 1 of 3 of the image-source-mappings epic (PyAutoMind ledger draft/feature/autoarray/image_source_mappings_epic.md); Phase 2 (PyAutoLens, ShapeSolver engine for parametric sources) and Phase 3 (workspace guide + tutorial rewrite) depend on this merging and releasing first.
Plan
- Add a new
autoarray/inversion/mappings/package withImageRegionandMappingdataclasses plus the pure-numpy helpers that build image-plane connected regions from a mapper's mapping matrix and turn them into arcsec-space boundary polygons. - Add
Inversion.source_clumps_from(threshold × max, connected components over the mesh neighbour graph, min-pixels filter, brightest-first ordering, explicitpix_indexesbypass) andInversion.mappings_fromcomposing it with the image-plane regions. - Add a
regions/region_colors/region_alpha/region_labelsoverlay toplot_arrayandplot_inversion_reconstruction, drawn as filled polygons with outlines in arcsec space sozoom_arraycropping cannot misalign them. - Rewrite
subplot_mappingsas a 2×2 figure (data-subtracted + regions, reconstructed image + regions, zoomed and unzoomed source reconstruction + clumps) with matched colours and numbered labels; keep its name,pixelization_indexkwarg andmappings_{i}filename. - Give bare
Mapperobjects amappings_from(pix_indexes)helper and letsubplot_image_and_mapperacceptregionsso tutorials without an inversion can draw mappings. - Replace the dead
total_mappings_pixelsconfig key withtotal_mappings,mappings_threshold,mappings_min_pixels(old key read as a fallback for one release). - Unit tests for the graph/region helpers, clump finding, the overlays and the rewritten subplot, mirroring the existing
plot_path/plot_patchfixture pattern.
Detailed implementation plan
Affected Repositories
- PyAutoArray (primary)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./PyAutoArray | main (302d5df) | clean |
PyAutoArray is also claimed by numpy-deflections-p1 (#514, worktree ~/Code/PyAutoLabs-wt/numpy-deflections-p1, no commits yet, edits the grid over-sampling decorator / tracer path). File sets are disjoint (this task: autoarray/inversion/, autoarray/plot/, autoarray/config/visualize/), so a parallel worktree was approved by the human on 2026-09-02.
Suggested branch: feature/image-source-mappings-p1
Verified facts that constrain the implementation
Mapper.slim_indexes_for_pix_indexes(autoarray/inversion/mappers/abstract.py:504) returns sub-slim (over-sampled) indexes despite its name — never use it to build an image-plane pixel region. UseMapper.mapping_matrix(:256, shape[mask_pixels, mesh_pixels], already folded over sub-pixels), the idiom inautolens_workspace/scripts/imaging/features/pixelization/delaunay.py:1305.plot_arraycallszoom_array(array)(autoarray/plot/array.py:124) before drawing, so overlays must be arcsec-space polygons, not boolean pixel arrays. All existing overlays are coordinate-space._plot_delaunay(autoarray/plot/inversion.py) uses flattripcolor; a Delaunay "pixel" is a vertex whose natural polygon is its Voronoi cell (mesh_geometry/delaunay.py:167 voronoi). Rectangular cells come from the mesh geometry.scipy.ndimageis already a hard dependency (autoarray/mask/mask_2d_util.py:609).Neighbors(autoarray/inversion/linear_obj/neighbors.py) is an ndarray with-1sentinels and.sizes.max_pixel_list_from(autoarray/inversion/inversion/abstract.py:1226) returns ONE flat peak list, never per-clump groups; leave it as is.- Under
PYAUTO_SMALL_DATASETSmeshes are capped (256 pixels) — never hard-code pix indexes in library code; tests userectangular_inversion_7x7_3x3/delaunay_mapper_9_3x3withmin_pixels=1.
Implementation Steps
- New package
autoarray/inversion/mappings/{__init__,mapping}.py.@dataclass(frozen=True) ImageRegion:slim_indexes: np.ndarray,mask: Mask2D(False = inside),contours: List[np.ndarray]((N,2) yx arcsec closed loops),centre: Tuple[float, float]; methodsarea(),brightest_coordinate_from(array),centroid_from(array)(flux-weighted),flux_from(array).@dataclass(frozen=True) Mapping:pix_indexes: np.ndarray,source_contours: List[np.ndarray],source_centre,image_regions: List[ImageRegion],peak_value: Optional[float].connected_components_from(indexes, neighbors) -> List[np.ndarray]— BFS over the mesh neighbour graph restricted toindexes.image_regions_from_slim_mask(mask2d, slim_bool, min_pixels=1) -> List[ImageRegion]— lift slim bool to native viaArray2D(values=..., mask=...).native,scipy.ndimage.labelwith 8-connectivity, drop small labels, contours viacontours_from_bool_native.image_regions_from(mapper, pix_indexes, weight_threshold=0.0, min_pixels=1)—w = np.asarray(mapper.mapping_matrix)[:, pix_indexes].sum(axis=1),slim_bool = w > weight_threshold, then the function above.source_contours_from(mapper, pix_indexes) -> List[np.ndarray]— rectangular meshes: cell corner polygons from the mesh geometry; Delaunay/KNN: the vertices' Voronoi regions (mesh_geometry.voronoi), clipped to the mesh extent.contours_from_bool_native(bool_native, geometry) -> List[np.ndarray]— pixel-edge boundary loops (assemble the outer edges of the True pixels into closed loops), converted to arcsec via the geometry's scaled-coordinate helpers. Keep it small and pure numpy.
autoarray/inversion/inversion/abstract.py:source_clumps_from(mapper_index=0, threshold=0.5, min_pixels=3, total_clumps=None, pix_indexes=None) -> List[np.ndarray]andmappings_from(mapper_index=0, weight_threshold=0.0, **clump_kwargs) -> List[Mapping].np.asarraythe reconstruction (may be a JAX array); non-positive max →[];pix_indexesgiven → returned unchanged as arrays (tutorial path).autoarray/inversion/mappers/abstract.py:Mapper.mappings_from(pix_indexes, weight_threshold=0.0, min_pixels=1) -> List[Mapping](thin wrapper:image_regions_from+source_contours_from,peak_value=None).autoarray/plot/array.py,autoarray/plot/inversion.py: parametersregions: Optional[List[List[np.ndarray]]],region_colors,region_alpha=0.25,region_labels; per mappingax.fill(x, y, color, alpha)+ax.plotoutline + optionalax.annotate(label)at the region centre, drawn afterlines; colour cycle["r","g","b","m","c","y"]shared withplot_grid. Update docstrings.plot_grid(indexes=...)untouched.autoarray/inversion/plot/inversion_plots.py::subplot_mappings: rewrite body aroundinversion.mappings_from(...); new kwargsthreshold,min_pixels,total_clumps,pix_indexes,weight_threshold; 2×2 layout; keep the existingVisibilities → transformer.image_fromconversion for interferometer inversions.autoarray/inversion/plot/mapper_plots.py::subplot_image_and_mapper: acceptregions/region_colors/region_labelspass-through.- Config
autoarray/config/visualize/general.yaml:inversion: total_mappings: 5,mappings_threshold: 0.5,mappings_min_pixels: 3; readtotal_mappings_pixelsas fallback if present. - Exports:
Mapping,ImageRegionfromautoarray/__init__.py(subplot_mappingsis already exported fromautoarray/plot/__init__.py). - Tests (fixtures
rectangular_inversion_7x7_3x3,rectangular_mapper_7x7_3x3,delaunay_mapper_9_3x3;plot_path/plot_patchpattern):test_autoarray/inversion/mappings/test_mapping.py—connected_components_fromon a syntheticNeighbors;image_regions_fromsplitting a bimodal mapping matrix into two regions; contours closed (first == last point);brightest_coordinate_from/centroid_fromon a known array.test_autoarray/inversion/inversion/test_abstract.py—source_clumps_from: two groups for a two-peak vector, one at low threshold,pix_indexespassthrough,total_clumpstruncation, all-negative →[].test_autoarray/inversion/plot/test_inversion_plotters.py— existingsubplot_mappingsassertion kept; add Delaunay andpix_indexes=cases.test_autoarray/plot/—regions=smoke onplot_arrayandplot_inversion_reconstruction;subplot_image_and_mapper(regions=...).
Key Files
autoarray/inversion/mappings/mapping.py— new: result objects + region/contour helpersautoarray/inversion/inversion/abstract.py—source_clumps_from,mappings_fromautoarray/inversion/mappers/abstract.py—Mapper.mappings_fromautoarray/plot/array.py,autoarray/plot/inversion.py—regionsoverlayautoarray/inversion/plot/inversion_plots.py—subplot_mappingsrewriteautoarray/inversion/plot/mapper_plots.py—subplot_image_and_mapperpass-throughautoarray/config/visualize/general.yaml— config keys
Verification
source activate.sh && pytest test_autoarray/inversion test_autoarray/plot -q; then run autogalaxy_workspace/scripts/imaging/features/pixelization/plot.py and autolens_workspace/scripts/imaging/features/pixelization/fit.py under the smoke profile and inspect mappings_0.png: distinct colours per clump, each clump's images outlined in the same colour in both image panels, labels matching across planes.
Original Prompt
Click to expand starting prompt
Image-plane region mappings, source clump finding and a restored subplot_mappings in PyAutoArray
Type: feature
Target: PyAutoArray
Repos:
- PyAutoArray
Difficulty: large
Autonomy: supervised
Priority: high
Status: formalised
Consequence: judge
Review-minutes: 25
Unattended: needs-slicing
Epic: image-source-mappings
Phase: 1
Filed: 2026-09-02
Image-plane region mappings, source clump finding and a restored subplot_mappings in PyAutoArray.
Phase 1 of the
image-source-mappingsepic — ledger
draft/feature/autoarray/image_source_mappings_epic.md, which holds the context (the feature died in
0cb75ebd, 2025-07-21), the user decisions and the original request verbatim. This phase lands the
plotting-agnostic result objects, the pixelized-source engine (engine A) and the genericregions=
overlay, so that Phase 2 (PyAutoLens, ShapeSolver) and Phase 3 (guide + tutorials) have something to
build on. Must merge AND release before Phase 2 opens.
Architecture (this phase's half)
A mapping = one source-plane region + the list of image-plane connected regions (the multiple images) it maps to,
sharing one colour. Two engines produce the same result objects; rendering is one generic regions= overlay on
plot_array and plot_inversion_reconstruction. Numpy-only, never jitted (plotting/diagnostic code).
autoarray/inversion/mappings/mapping.py (new package — not inside mappers/)
@dataclass ImageRegion: slim_indexes, mask(Mask2D), contours[(N,2) yx arcsec], centre,
area(), brightest_coordinate_from(array), centroid_from(array), flux_from(array)
@dataclass Mapping: pix_indexes, source_contours, source_centre, image_regions[ImageRegion], peak_value
connected_components_from(indexes, neighbors) -> List[np.ndarray] # BFS on mesh graph
image_regions_from_slim_mask(mask2d, slim_bool, min_pixels) -> List[ImageRegion] # ndimage.label(8-conn) + contours
image_regions_from(mapper, pix_indexes, weight_threshold=0.0, min_pixels=1) -> List[ImageRegion] # via mapping_matrix
source_contours_from(mapper, pix_indexes) -> List[np.ndarray] # rectangular cells / Delaunay Voronoi cells
contours_from_bool_native(bool_native, geometry) -> List[np.ndarray] # pixel-edge boundary loops → arcsec
Engine A (pixelized, this phase): Inversion.source_clumps_from(...) thresholds the reconstruction at
threshold * max, takes connected components over mapper.neighbors, drops < min_pixels, sorts by peak, truncates to
total_clumps; pix_indexes=[[...],[...]] bypasses clump finding (the tutorial path). Inversion.mappings_from(...)
composes it with image_regions_from. The threshold is the documented knob: ~0.5 one smooth source, ~0.2 merges two
galaxies into one, ~0.8 splits star-forming knots.
Rendering: plot_array / plot_inversion_reconstruction gain regions, region_colors, region_alpha=0.25,
region_labels; each polygon is ax.fill + ax.plot outline + optional numbered label; colour cycle reuses plot_grid's
["r","g","b","m","c","y"]. plot_grid(indexes=...) (point-level) is untouched.
Verified facts (do not re-derive)
Mapper.slim_indexes_for_pix_indexes(mappers/abstract.py:504) returns sub-slim (over-sampled) indexes despite its
name — never use it to build an image-plane pixel region. UseMapper.mapping_matrix(:256, shape
[mask_pixels, mesh_pixels], already folded over sub-pixels) — the same idiom aspixelization/delaunay.py:1305.plot_arraycallszoom_array(array)(plot/array.py:124) before drawing, so overlays must be arcsec-space polygons,
not boolean pixel arrays. All existing overlays (mask,border,positions,lines) are coordinate-space._plot_delaunayuses flattripcolor; a Delaunay "pixel" is a vertex, its natural polygon is its Voronoi cell
(mesh_geometry/delaunay.py:167 voronoi). Rectangular cells come from the mesh geometry edges.scipy.ndimageis already a hard dependency (mask_2d_util.py:609).Neighbors(inversion/linear_obj/neighbors.py)
is an ndarray with-1sentinels and.sizes.
(Numbering matches the ledger's list; facts 3, 5 and 7 belong to Phases 2/3.)
Files
- new
autoarray/inversion/mappings/{__init__,mapping}.py— the objects + functions above. autoarray/inversion/inversion/abstract.py— add
source_clumps_from(mapper_index=0, threshold=0.5, min_pixels=3, total_clumps=None, pix_indexes=None) -> List[np.ndarray]
andmappings_from(mapper_index=0, weight_threshold=0.0, **clump_kwargs) -> List[Mapping].np.asarraythe
reconstruction (may be a jax array). Empty/non-positive max →[]. Leavemax_pixel_list_fromas is.autoarray/plot/array.py,autoarray/plot/inversion.py— theregionsoverlay (drawn afterlines).autoarray/inversion/plot/inversion_plots.py::subplot_mappings— rewrite the body; keep the name,pixelization_index
and themappings_{i}filename. Addthreshold,min_pixels,total_clumps,pix_indexes,weight_threshold.
Layout 2×2: data-subtracted + image regions | reconstructed image + image regions | source recon zoomed + clumps |
source recon unzoomed + clumps; matched colours, labels 1..N in both planes. Interferometer: keep the existing
Visibilities→transformer.image_fromconversion; regions are real-space so they work unchanged.autoarray/inversion/plot/mapper_plots.py::subplot_image_and_mapper— acceptregions/region_colorspass-through so a
bareMapper(no inversion — the tutorial_2 case) can showpix_indexesmappings: helper
mapper.mappings_from(pix_indexes) -> List[Mapping]onMapper(thin wrapper overimage_regions_from+
source_contours_from).autoarray/config/visualize/general.yaml— replacetotal_mappings_pixelswithinversion: total_mappings: 5,
mappings_threshold: 0.5,mappings_min_pixels: 3; read the old key as a fallback for one release.autoarray/plot/__init__.py— export nothing new beyond what exists (subplot_mappingsis already exported); export
Mapping,ImageRegionfromautoarray/__init__.py.
Optional — ship here if timing allows
autoarray/structures/triangles/shape.py — add Shape.contains(points_yx) -> bool array (Circle radial, Triangle
barycentric, Polygon even-odd, Square bounds, Point raises pointing at Circle/PointSolver) and
Shape.boundary(n=100) -> (N,2) yx. Takes (y, x); the docstring must note the legacy (x, y) order of
mask(triangles) (verified fact 3: the Shape classes today have mask(triangles) only, no point containment).
This is Phase 2's first bullet — a small PyAutoArray change. Ship it in this PR if timing allows; otherwise it becomes a
tiny Phase 2a PyAutoArray PR that must precede the PyAutoLens one.
Tests
Existing plot_path/plot_patch fixture pattern; fixtures rectangular_inversion_7x7_3x3, delaunay_mapper_9_3x3.
test_autoarray/inversion/mappings/test_mapping.py—connected_components_fromon a syntheticNeighbors;
image_regions_fromsplitting a bimodal mapping matrix into two regions; contour loops closed;
brightest_coordinate_from.test_autoarray/inversion/inversion/test_abstract.py—source_clumps_from: two groups for a two-peak vector, one at
low threshold,pix_indexespassthrough,total_clumpstruncation.test_inversion_plotters.py— keep the existingsubplot_mappingsassertion; add a Delaunay case and apix_indexes=case.test_autoarray/plot/—regions=smoke onplot_arrayandplot_inversion_reconstruction.
Verification
source activate.sh && pytest test_autoarray/inversion test_autoarray/plot -q; then run
autogalaxy_workspace/scripts/imaging/features/pixelization/plot.py and
autolens_workspace/scripts/imaging/features/pixelization/fit.py under the smoke profile and eyeball
mappings_0.png: a two-clump source must show distinct colours, each clump's images outlined in the same colour in both
image panels, and labels matching across the planes.
Activity
Jammy2211 commentedon Sep 2, 2026
Library PR Created
PR: #517
Branch:
feature/image-source-mappings-p1(worktree~/Code/PyAutoLabs-wt/image-source-mappings-p1)Tests:
pytest test_autoarray— 1382 passed, 0 failed.Heart readiness at ship time was YELLOW, acknowledged by the human (2026-09-02): "PyAutoArray: open PR 10d old"; "release validation incomplete: no rehearsal for current source". Neither concerns this branch.
Workspace impact: (i) new demos — existing
subplot_mappings(inversion=..., pixelization_index=0)calls keep working and now render the mapping figure; prose/guide/tutorial updates are Phase 3 of the epic (draft/docs/autolens_workspace/mappings_guide_and_tutorial_rewrite.md). Phase 2 (PyAutoLens) waits for this PR to merge and release.Merge via
/prmonce CI is green.Jammy2211 commentedon Sep 2, 2026
Shipped
PR: #517 — MERGED 2026-09-02 (merge commit
501c373fdb0f1cd0545ccdc593b2f74d35fbac0d, head36b75e0).CI: Tests workflow green on all 3 legs (3.12, 3.13, nojax). Local:
pytest test_autoarray— 1382 passed.Label:
pending-release— the change is onmainand ships with the next PyAutoArray release.What shipped
autoarray/inversion/mappings/package:Mapping/ImageRegionresult objects, mesh-graph connected components, image-plane regions derived from the mapping matrix, and arcsec boundary polygons.Inversion.source_clumps_fromwith a threshold knob, plusInversion.mappings_fromandMapper.mappings_from.regions=polygon overlay onplot_arrayandplot_inversion_reconstruction.subplot_mappingsrewritten as the one-look 2x2 colour-matched figure.Known leftovers (not regressions from this PR)
except: passguards remain insubplot_mappings.blackreformatted ~40 unrelated lines in the touched plot files.total_mappings_pixelskey; the library reads it as a fallback, and Phase 3 of theimage-source-mappingsepic sweeps it.Epic
image-source-mappingsphase 1 of 3 is done. Phase 2 (PyAutoLens) is gated on the PyAutoArray release landing; Phase 3 is the workspace follow-up.