feat: Shape.contains / Shape.boundary for source-plane shapes (image-source-mappings P2a) - #518
Merged
Merged
Conversation
…e-source-mappings phase 2a) Point containment and closed boundary polygons for Circle, Triangle, Polygon and Square, documenting the (y, x) axis convention the triangle solvers already use. Fixes the reflected containment test in Triangle.triangle_contains_mask, which unpacked its vertices as (y, x) while testing a centroid built as (x, y). Square.contains / Square.boundary sort their bounds so they work for either top/bottom ordering; mask and area keep their documented assumption. Part of PyAutoLabs/PyAutoLens#719 (image-source-mappings epic, phase 2a). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011EVnQ4sEYrRM7GCvFyveUA
…e-source-mappings phase 2a)
`CoordinateArrayTriangles{,Np}.for_limits_and_scale` named its first pair of limits
`x_min`/`x_max` and tiled them along element 0 of every vertex, while element 0 is the
`y` coordinate everywhere else — `ArrayTrianglesNp.for_limits_and_scale`, every PyAuto
`(y, x)` grid, and the `element 0 <-> element 0` convention `Shape.contains` / `Shape.mask`
are documented with. Both the keyword caller (`AbstractSolver._initial_triangles`) and the
positional caller (`AbstractTriangles.for_grid`) therefore tiled the transposed rectangle.
A square grid hides this, which is why it shipped; on a 24x80 grid of 0.05" pixels
`PointSolver.solve` found one of an Isothermal's two images instead of both, the missing
one lying well inside the grid.
Also removes a stray `bbbb` statement (a `NameError` on every call) left in
`ArrayTrianglesNp.with_vertices` since 2025-11-13, uncaught because the solver only ever
reaches the `CoordinateArrayTrianglesNp` override of that name.
Found by the ShapeSolver validation suite of image-source-mappings phase 2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EVnQ4sEYrRM7GCvFyveUA
…mappings phase 2a) `plot_regions` drew one label per region entry, at the mean of all of that region's polygons. A region whose polygons are the multiple images of one lensed source has them on opposite sides of the lens, so the label landed at their midpoint -- on empty sky, labelling nothing, which is exactly the case the mappings overlay exists for. The label is now repeated once per polygon, at that polygon's own centre. A single-polygon region is unchanged: its polygon's mean is the region's mean. Found by the phase 2 verification renders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011EVnQ4sEYrRM7GCvFyveUA
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2a of the
image-source-mappingsepic: point containment and boundary polygons for the source-planeShapeclasses, plus three defects the Phase 2ShapeSolvervalidation suite exposed in PyAutoArray's triangle and overlay code. This is the PyAutoArray half of PyAutoLabs/PyAutoLens#719; the PyAutoLens PR (PyAutoLabs/PyAutoLens#720) depends on it — its CI is source-installed against PyAutoArraymain, so it goes green once this merges. Merge order: this PR first.Shape.contains(points) -> bool arrayon every shape:Circleradial,Trianglebarycentric,Polygonany-of its fan triangles (exact for convex polygons),Squarebounds;Point.containsraises, pointing atCircle/PointSolver.Shape.boundary(n=100) -> (n, 2)closed loop for drawing the source-plane region.Shapebase class:pointsuse the same axis order astriangles[..., 0]/[..., 1]thatmask()already reads, andPointSolver.solvebuildsPoint(*source_plane_coordinate)from a(y, x)tuple, so the attribute namedxholds the first (y) coordinate.Audit findings (all fixed here, each with a regression test)
Triangle.triangle_contains_masktested the reflected triangle: it unpacked vertices asy1, x1 = self.a(element 1 as "x") while testing a centroid built as(element 0, element 1). On the asymmetric triangle((0,0),(3,0),(0,1))the interior point(2.0, 0.2)read as outside and its reflection(0.2, 2.0)as inside. Every symmetric shape in the existingtest_polygons.pyhid it. All three barycentric copies now share one_barycentric_containshelper.b1f88f4atest_shape.py::test_triangle_contains_mask_is_not_reflected;::test_contains_uses_the_same_axis_order_as_the_triangle_arraypins the convention empiricallyCoordinateArrayTriangles{,Np}.for_limits_and_scalenamed its first pair of limitsx_min/x_maxand tiled them along element 0, while element 0 isyeverywhere else (ArrayTrianglesNp, every(y, x)grid, theShapeconvention). Both the keyword caller (AbstractSolver._initial_triangles) and the positional caller (AbstractTriangles.for_grid) tiled the transposed rectangle. Invisible on a square grid; on a 24×80 grid of 0.05" pixelsPointSolver.solvefound one of an Isothermal's two images instead of both, the missing one well inside the grid.67b1cb75test_coordinate.py::test_for_limits_and_scale__element_0_spans_the_y_limits,::test_for_grid__rectangular_grid_is_tiled_the_same_way_roundbbbbstatement inArrayTrianglesNp.with_vertices— aNameErroron every call since 2025-11, unreached because the solver only uses theCoordinateArrayTrianglesNpoverride.67b1cb75test_coordinate.py::test_array_triangles_with_vertices_returns_new_trianglesplot_regionsdrew one label per region entry at the mean of all its polygons, so a mapping's label for two multiple images landed on empty sky between them. Multi-polygon regions now carry the label once per polygon.a9a9120atest_autoarray/plot/(a two-polygon region with one label yields two text artists)Square.mask/Square.areaassumetop < bottomnumerically, consistent with their "coordinates from the top-left corner" docstring — not a bug. The newcontains/boundarysort the bounds so they also work for arcsec(y, x)squares (test_shape.py::test_square_contains_is_robust_to_coordinate_ordering).API Changes
Shape.contains(points)andShape.boundary(n=100)(abstract onShape; implemented onPoint,Circle,Triangle,Polygon,Square).CoordinateArrayTriangles.for_limits_and_scale/CoordinateArrayTrianglesNp.for_limits_and_scalesignature order from(x_min, x_max, y_min, y_max, scale)to(y_min, y_max, x_min, x_max, scale), matching the abstract base and the positional caller. Keyword callers are unaffected by the reorder and now get the rectangle they asked for. Behaviour change:PointSolveron a non-square grid now searches the grid's actual extent and can return multiple images it previously missed.plot_regionslabel placement for multi-polygon regions (one label per polygon).mask(triangles),area, the pytree methods andautoarray/__init__.pyare untouched.Tests
test_autoarray/structures/triangles/test_shape.py(19 tests) and additions totest_coordinate.py(3) andtest_autoarray/plot/(2).pytest test_autoarray -q: 1410 passed (Phase 1 shipped at 1382).Epic
image-source-mappingsphase 2a — ledgerPyAutoMind/draft/feature/autoarray/image_source_mappings_epic.md. Phase 1 was PyAutoArray#517. Labelpending-release.🤖 Generated with Claude Code
https://claude.ai/code/session_011EVnQ4sEYrRM7GCvFyveUA