Skip to content

Commit c177391

Browse files
Jammy2211claude
authored andcommitted
fix: rectangular-mesh plot edges follow the mapper's node convention
The bilinear interpolation mapper places reconstruction value (row r, col c) at node U_y=(n_y-r-1)/(n_y-3), U_x=(c-1)/(n_x-3) in unit CDF space (guard ring at the border, rows flipped by the mapper's row = n - i), but edges_transformed drew a uniform [0,1] partition — pcolormesh plots put flux up to ~1.5 mesh pixels from where the mapper scatters it (the "small offsets" of issue #372, confirmed by a delta-function reproduction). The edges are now the node midpoints pushed through the same CDF machinery (guard-safe denominator for degenerate n<=3 meshes; border guard cells clamp to the data span). Both consumers inherit the fix: the pcolormesh plot path and rectangular_rotated's warped-centre reconstruction; spline and linear CDFs share the upstream index convention. Plot geometry only — the mapper is untouched, so inversions and likelihoods are byte-identical. #372 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e0cb2fa commit c177391

2 files changed

Lines changed: 71 additions & 8 deletions

File tree

autoarray/inversion/mesh/mesh_geometry/rectangular.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -510,16 +510,29 @@ def areas_for_magnification(self):
510510
@property
511511
def edges_transformed(self):
512512
"""
513-
A class packing the ndarrays describing the neighbors of every pixel in the rectangular pixelization (see
514-
`Neighbors` for a complete description of the neighboring scheme).
515-
516-
The neighbors of a rectangular pixelization are computed by exploiting the uniform and symmetric nature of the
517-
rectangular grid, as described in the method `rectangular_neighbors_from`.
513+
The source-plane cell edges of every mesh pixel, transformed through
514+
the adaptive CDF — one more edge than pixels per axis, shape
515+
``(n + 1, 2)`` packing (y_edges, x_edges).
516+
517+
The interpolation mapper is node-based: reconstruction value
518+
``(row r, col c)`` lives at node ``U_y = (n_y - r - 1)/(n_y - 3)``,
519+
``U_x = (c - 1)/(n_x - 3)`` in unit CDF space (a guard ring occupies
520+
the border; rows are flipped by the mapper's ``row = n - i``). The
521+
cell edges are therefore the node midpoints — a uniform ``[0, 1]``
522+
partition drew every cell up to ~1.5 mesh pixels away from where the
523+
mapper scatters its flux (issue #372). Guard-node edges fall outside
524+
the CDF domain and clamp to the data span in the inverse interp,
525+
squashing the border guard cells to the region they actually cover.
518526
"""
519527

520-
# edges defined in 0 -> 1 space, there is one more edge than pixel centers on each side
521-
edges_y = self._xp.linspace(1, 0, self.shape_native[0] + 1)
522-
edges_x = self._xp.linspace(0, 1, self.shape_native[1] + 1)
528+
# Node-midpoint edges in unit CDF space (see docstring); the guard
529+
# denominator keeps the degenerate n <= 3 meshes finite (their
530+
# interpolator maps every point to the single interior node anyway).
531+
n_y, n_x = self.shape_native
532+
rows = self._xp.arange(n_y + 1)
533+
cols = self._xp.arange(n_x + 1)
534+
edges_y = (n_y - rows - 0.5) / max(n_y - 3, 1)
535+
edges_x = (cols - 1.5) / max(n_x - 3, 1)
523536

524537
edges_reshaped = self._xp.stack([edges_y, edges_x]).T
525538

test_autoarray/inversion/pixelization/mesh_geometry/test_rectangular.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,53 @@ def test__edges_transformed(mask_2d_7x7):
200200
),
201201
abs=1e-8,
202202
)
203+
204+
205+
def test__edges_transformed__aligned_with_interpolation_node_convention():
206+
"""
207+
Regression test for issue #372: the pcolormesh plotting path draws value
208+
(row r, col c) inside the cell bounded by edges_transformed — that cell
209+
must be centred on the interpolation mapper's node for that value, not on
210+
a uniform [0, 1] partition (which shifted plots by ~1.5 mesh pixels).
211+
212+
A delta function scattered through the mapper must land in plotted cells
213+
whose weighted centroid matches the input point to sub-cell precision.
214+
"""
215+
from autoarray.inversion.mesh.interpolator.rectangular import (
216+
adaptive_rectangular_mappings_weights_via_interpolation_from,
217+
adaptive_rectangular_transformed_grid_from,
218+
)
219+
220+
n = 10
221+
rng = np.random.default_rng(0)
222+
data_grid = rng.uniform(-1.0, 1.0, (5000, 2))
223+
test_point = np.array([[0.3, -0.2]])
224+
225+
flat_indices, weights = (
226+
adaptive_rectangular_mappings_weights_via_interpolation_from(
227+
source_grid_size=n,
228+
data_grid=data_grid,
229+
data_grid_over_sampled=test_point,
230+
)
231+
)
232+
233+
# The node-midpoint unit-space edges (what edges_transformed now builds),
234+
# pushed through the same CDF transform.
235+
rows = np.arange(n + 1)
236+
edges_y = (n - rows - 0.5) / (n - 3)
237+
edges_x = (rows - 1.5) / (n - 3)
238+
edges = np.stack([edges_y, edges_x]).T
239+
edges_t = adaptive_rectangular_transformed_grid_from(data_grid, edges)
240+
y_edges, x_edges = edges_t.T
241+
242+
centroid_y = 0.0
243+
centroid_x = 0.0
244+
for flat, weight in zip(flat_indices[0], weights[0]):
245+
r, c = flat // n, flat % n
246+
centroid_y += weight * 0.5 * (y_edges[r] + y_edges[r + 1])
247+
centroid_x += weight * 0.5 * (x_edges[c] + x_edges[c + 1])
248+
249+
# Half a mesh cell in these units is ~0.15; the pre-fix uniform edges
250+
# missed by ~0.4 in y.
251+
assert centroid_y == pytest.approx(test_point[0, 0], abs=0.1)
252+
assert centroid_x == pytest.approx(test_point[0, 1], abs=0.1)

0 commit comments

Comments
 (0)