Skip to content

feat: image↔source mapping regions, clumps, subplot_mappings (P1) #515

Closed

Description

@Jammy2211

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 with ImageRegion and Mapping dataclasses 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, explicit pix_indexes bypass) and Inversion.mappings_from composing it with the image-plane regions.
  • Add a regions / region_colors / region_alpha / region_labels overlay to plot_array and plot_inversion_reconstruction, drawn as filled polygons with outlines in arcsec space so zoom_array cropping cannot misalign them.
  • Rewrite subplot_mappings as 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_index kwarg and mappings_{i} filename.
  • Give bare Mapper objects a mappings_from(pix_indexes) helper and let subplot_image_and_mapper accept regions so tutorials without an inversion can draw mappings.
  • Replace the dead total_mappings_pixels config key with total_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_patch fixture 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

  1. 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. Use Mapper.mapping_matrix (:256, shape [mask_pixels, mesh_pixels], already folded over sub-pixels), the idiom in autolens_workspace/scripts/imaging/features/pixelization/delaunay.py:1305.
  2. plot_array calls zoom_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.
  3. _plot_delaunay (autoarray/plot/inversion.py) uses flat tripcolor; 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.
  4. scipy.ndimage is already a hard dependency (autoarray/mask/mask_2d_util.py:609). Neighbors (autoarray/inversion/linear_obj/neighbors.py) is an ndarray with -1 sentinels and .sizes.
  5. max_pixel_list_from (autoarray/inversion/inversion/abstract.py:1226) returns ONE flat peak list, never per-clump groups; leave it as is.
  6. Under PYAUTO_SMALL_DATASETS meshes are capped (256 pixels) — never hard-code pix indexes in library code; tests use rectangular_inversion_7x7_3x3 / delaunay_mapper_9_3x3 with min_pixels=1.

Implementation Steps

  1. 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]; methods area(), 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 to indexes.
    • image_regions_from_slim_mask(mask2d, slim_bool, min_pixels=1) -> List[ImageRegion] — lift slim bool to native via Array2D(values=..., mask=...).native, scipy.ndimage.label with 8-connectivity, drop small labels, contours via contours_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.
  2. 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] and mappings_from(mapper_index=0, weight_threshold=0.0, **clump_kwargs) -> List[Mapping]. np.asarray the reconstruction (may be a JAX array); non-positive max → []; pix_indexes given → returned unchanged as arrays (tutorial path).
  3. 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).
  4. autoarray/plot/array.py, autoarray/plot/inversion.py: parameters regions: Optional[List[List[np.ndarray]]], region_colors, region_alpha=0.25, region_labels; per mapping ax.fill(x, y, color, alpha) + ax.plot outline + optional ax.annotate(label) at the region centre, drawn after lines; colour cycle ["r","g","b","m","c","y"] shared with plot_grid. Update docstrings. plot_grid(indexes=...) untouched.
  5. autoarray/inversion/plot/inversion_plots.py::subplot_mappings: rewrite body around inversion.mappings_from(...); new kwargs threshold, min_pixels, total_clumps, pix_indexes, weight_threshold; 2×2 layout; keep the existing Visibilities → transformer.image_from conversion for interferometer inversions. autoarray/inversion/plot/mapper_plots.py::subplot_image_and_mapper: accept regions/region_colors/region_labels pass-through.
  6. Config autoarray/config/visualize/general.yaml: inversion: total_mappings: 5, mappings_threshold: 0.5, mappings_min_pixels: 3; read total_mappings_pixels as fallback if present.
  7. Exports: Mapping, ImageRegion from autoarray/__init__.py (subplot_mappings is already exported from autoarray/plot/__init__.py).
  8. Tests (fixtures rectangular_inversion_7x7_3x3, rectangular_mapper_7x7_3x3, delaunay_mapper_9_3x3; plot_path/plot_patch pattern):
    • test_autoarray/inversion/mappings/test_mapping.py — connected_components_from on a synthetic Neighbors; image_regions_from splitting a bimodal mapping matrix into two regions; contours closed (first == last point); brightest_coordinate_from / centroid_from on 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_indexes passthrough, total_clumps truncation, all-negative → [].
    • test_autoarray/inversion/plot/test_inversion_plotters.py — existing subplot_mappings assertion kept; add Delaunay and pix_indexes= cases.
    • test_autoarray/plot/ — regions= smoke on plot_array and plot_inversion_reconstruction; subplot_image_and_mapper(regions=...).

Key Files

  • autoarray/inversion/mappings/mapping.py — new: result objects + region/contour helpers
  • autoarray/inversion/inversion/abstract.py — source_clumps_from, mappings_from
  • autoarray/inversion/mappers/abstract.py — Mapper.mappings_from
  • autoarray/plot/array.py, autoarray/plot/inversion.py — regions overlay
  • autoarray/inversion/plot/inversion_plots.py — subplot_mappings rewrite
  • autoarray/inversion/plot/mapper_plots.py — subplot_image_and_mapper pass-through
  • autoarray/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-mappings epic — 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 generic regions=
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)

  1. 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. Use Mapper.mapping_matrix (:256, shape
    [mask_pixels, mesh_pixels], already folded over sub-pixels) — the same idiom as pixelization/delaunay.py:1305.
  2. plot_array calls zoom_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.
  3. _plot_delaunay uses flat tripcolor; 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.
  4. scipy.ndimage is already a hard dependency (mask_2d_util.py:609). Neighbors (inversion/linear_obj/neighbors.py)
    is an ndarray with -1 sentinels 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]
    and mappings_from(mapper_index=0, weight_threshold=0.0, **clump_kwargs) -> List[Mapping]. np.asarray the
    reconstruction (may be a jax array). Empty/non-positive max → []. Leave max_pixel_list_from as is.
  • autoarray/plot/array.py, autoarray/plot/inversion.py — the regions overlay (drawn after lines).
  • autoarray/inversion/plot/inversion_plots.py::subplot_mappings — rewrite the body; keep the name, pixelization_index
    and the mappings_{i} filename. Add threshold, 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_from conversion; regions are real-space so they work unchanged.
  • autoarray/inversion/plot/mapper_plots.py::subplot_image_and_mapper — accept regions/region_colors pass-through so a
    bare Mapper (no inversion — the tutorial_2 case) can show pix_indexes mappings: helper
    mapper.mappings_from(pix_indexes) -> List[Mapping] on Mapper (thin wrapper over image_regions_from +
    source_contours_from).
  • autoarray/config/visualize/general.yaml — replace total_mappings_pixels with inversion: 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_mappings is already exported); export
    Mapping, ImageRegion from autoarray/__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_from on a synthetic Neighbors;
    image_regions_from splitting 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_indexes passthrough, total_clumps truncation.
  • test_inversion_plotters.py — keep the existing subplot_mappings assertion; add a Delaunay case and a pix_indexes= case.
  • test_autoarray/plot/ — regions= smoke on plot_array and plot_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

  1. Jammy2211 commented on Sep 2, 2026

    @Jammy2211
    CollaboratorAuthor

    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 /prm once CI is green.

  2. Jammy2211 commented on Sep 2, 2026

    @Jammy2211
    CollaboratorAuthor

    Shipped

    PR: #517 — MERGED 2026-09-02 (merge commit 501c373fdb0f1cd0545ccdc593b2f74d35fbac0d, head 36b75e0).
    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 on main and ships with the next PyAutoArray release.

    What shipped

    • New autoarray/inversion/mappings/ package: Mapping / ImageRegion result objects, mesh-graph connected components, image-plane regions derived from the mapping matrix, and arcsec boundary polygons.
    • Inversion.source_clumps_from with a threshold knob, plus Inversion.mappings_from and Mapper.mappings_from.
    • A regions= polygon overlay on plot_array and plot_inversion_reconstruction.
    • subplot_mappings rewritten as the one-look 2x2 colour-matched figure.

    Known leftovers (not regressions from this PR)

    • Pre-existing silent except: pass guards remain in subplot_mappings.
    • black reformatted ~40 unrelated lines in the touched plot files.
    • Workspace configs still carry the old total_mappings_pixels key; the library reads it as a fallback, and Phase 3 of the image-source-mappings epic sweeps it.

    Epic image-source-mappings phase 1 of 3 is done. Phase 2 (PyAutoLens) is gated on the PyAutoArray release landing; Phase 3 is the workspace follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions