Skip to content

Commit e52782f

Browse files
Jammy2211claude
andcommitted
fix: adaptive rectangular meshes report their true pixel centres
`mapper.source_plane_mesh_grid` for the four adaptive rectangular meshes returned the uniform overlay the interpolator is built from, not the source-plane positions where the mapper's interpolant takes each reconstruction value (the regular CDF lattice mapped back through the inverse CDF). Anything placing the reconstruction from those positions was wrong: the reconstruction CSV, `max_pixel_centre` (used by PyAutoLens automatic position updating), plot zoom (`extent_from`), PyAutoLens potential correction and workspace interpolation of the source. - `adaptive_rectangular_node_centres_from` + `MeshGeometryRectangular.node_centres` - `InterpolatorRectangular.source_plane_mesh_grid` returns the node centres - kernel regularizations and MaternSparse keep measuring distances on the lattice, now explicitly via `regularization_mesh_grid` / `regularization_mesh_grid_from` (likelihoods unchanged; true-distance kernels are a separate research question) Closes #667 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e71feaf commit e52782f

13 files changed

Lines changed: 415 additions & 22 deletions

File tree

‎autoarray/inversion/mappers/abstract.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,11 @@ def source_plane_data_grid(self):
140140

141141
@property
142142
def source_plane_mesh_grid(self):
143-
return self.interpolator.mesh_grid
143+
return self.interpolator.source_plane_mesh_grid
144+
145+
@property
146+
def regularization_mesh_grid(self):
147+
return self.interpolator.regularization_mesh_grid
144148

145149
@property
146150
def adapt_data(self):

‎autoarray/inversion/mesh/interpolator/abstract.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ def _xp(self):
3030
return jnp
3131
return np
3232

33+
@property
34+
def source_plane_mesh_grid(self):
35+
"""
36+
The source-plane (y,x) position of every mesh pixel, in the order of the reconstruction. For most meshes
37+
this is the `mesh_grid` the interpolator was built from; the adaptive rectangular interpolator overrides it.
38+
"""
39+
return self.mesh_grid
40+
41+
@property
42+
def regularization_mesh_grid(self):
43+
"""
44+
The (y,x) positions kernel regularizations measure pixel-to-pixel distances on. For most meshes these are
45+
the source-plane pixel positions. For the adaptive rectangular meshes they are the regular lattice
46+
`mesh_grid`, so distances are measured in the mesh's stretched (CDF) coordinates, see
47+
`InterpolatorRectangular.source_plane_mesh_grid`.
48+
"""
49+
return self.mesh_grid
50+
3351
@property
3452
def _mappings_sizes_weights(self):
3553
raise NotImplementedError(

‎autoarray/inversion/mesh/interpolator/rectangular.py‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,73 @@ def inv_full(U):
478478
return xp.abs(xp.outer(dy, dx).flatten())
479479

480480

481+
def adaptive_rectangular_node_centres_from(
482+
source_grid_shape,
483+
data_grid,
484+
mesh_weight_map=None,
485+
bandwidth: float = KERNEL_CDF_DEFAULT_BANDWIDTH,
486+
n_knots: int = KERNEL_CDF_DEFAULT_KNOTS,
487+
transform: str = "kernel",
488+
xp=np,
489+
):
490+
"""
491+
The source-plane (y,x) centre of every node of the adaptive rectangular mesh,
492+
in the mapper's pixel order — the position where node ``(r, c)``'s
493+
interpolation basis function peaks, so that reconstruction value ``k`` lives
494+
at centre ``k``.
495+
496+
The mapper of ``adaptive_rectangular_mappings_weights_via_interpolation_from``
497+
places the source plane in index space via ``g = (source_grid_size - 3) * U + 1``
498+
with ``U`` the forward CDF transform, and its ``flatten`` puts y index-space
499+
node ``k`` on row ``n - k`` and x node ``k`` on column ``k``. Node ``(r, c)``
500+
is therefore at unit-square coordinate
501+
502+
U_y = (n - r - 1) / (n - 3), U_x = (c - 1) / (n - 3),
503+
504+
mapped back through the SAME inverse CDF. The transform is separable, so each
505+
axis is inverted once and the centres are their outer grid (row-major from
506+
the top-left, like the uniform overlay this replaces).
507+
508+
Guard nodes (rows 0 and 1, columns 0 and ``n - 1``) have ``U`` outside
509+
``[0, 1]``; the inverse CDF clamps them to the edge of the data span, so they
510+
sit on the border of the region the mapper samples (they carry zero
511+
interpolation weight and zero area).
512+
513+
The mapper indexes a square mesh; for a non-square ``source_grid_shape`` each
514+
axis uses its own pixel count, so there is still one centre per pixel.
515+
"""
516+
n_y, n_x = source_grid_shape
517+
518+
mu = data_grid.mean(axis=0)
519+
scale = data_grid.std(axis=0).min()
520+
source_grid_scaled = (data_grid - mu) / scale
521+
522+
_, inv_transform = _transforms_from(
523+
source_grid_scaled,
524+
mesh_pixels=n_y,
525+
mesh_weight_map=mesh_weight_map,
526+
bandwidth=bandwidth,
527+
n_knots=n_knots,
528+
transform=transform,
529+
xp=xp,
530+
)
531+
532+
# the transform is per axis, so invert each axis's node coordinates in its own
533+
# column (the other column is unused)
534+
rows = xp.arange(n_y)
535+
cols = xp.arange(n_x)
536+
unit_y = (n_y - rows - 1) / (n_y - 3)
537+
unit_x = (cols - 1) / (n_x - 3)
538+
539+
y_nodes = inv_transform(xp.stack([unit_y, xp.zeros(n_y)], axis=1))[:, 0]
540+
x_nodes = inv_transform(xp.stack([xp.zeros(n_x), unit_x], axis=1))[:, 1]
541+
542+
y = xp.repeat(y_nodes * scale + mu[0], n_x)
543+
x = xp.tile(x_nodes * scale + mu[1], n_y)
544+
545+
return xp.stack([y, x], axis=1)
546+
547+
481548
# Samples per axis of the 1D quadrature in `adaptive_rectangular_hat_areas_from`.
482549
# Against 40001 samples on a 12x12 mesh (PyAutoArray#659) the per-node error is
483550
# ~1e-6 on the smooth kernel CDF and ~6e-4 on the rank CDF's staircase over an
@@ -765,6 +832,23 @@ def mesh_geometry(self):
765832
xp=self._xp,
766833
)
767834

835+
@cached_property
836+
def source_plane_mesh_grid(self):
837+
"""
838+
The source-plane (y,x) centre of every mesh pixel, where the interpolant takes each reconstruction value
839+
(``mesh_geometry.node_centres``). Use these to place, plot, export or interpolate the reconstruction.
840+
841+
``mesh_grid`` is the uniform lattice before the adaptive CDF is applied. It is kept for the mesh
842+
geometry's extent and is the ``regularization_mesh_grid``: kernel regularizations (``MaternKernel``,
843+
``GaussianKernel``, ...) and ``MaternSparse`` measure distances on it, i.e. in the mesh's stretched
844+
coordinates, so a kernel's ``scale`` shrinks in the source plane where the mesh packs its pixels.
845+
Distances on the true pixel centres gave a lower Bayesian evidence on a test lens, so the lattice is the
846+
deliberate choice (PyAutoArray#667).
847+
"""
848+
from autoarray.structures.grids.irregular_2d import Grid2DIrregular
849+
850+
return Grid2DIrregular(values=self.mesh_geometry.node_centres)
851+
768852
@cached_property
769853
def _mappings_sizes_weights(self):
770854
mappings, weights = (

‎autoarray/inversion/mesh/mesh_geometry/rectangular.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,47 @@ def areas_for_magnification(self):
496496
xp=self._xp,
497497
)
498498

499+
@property
500+
def node_centres(self):
501+
"""
502+
The source-plane (y,x) centre of every pixel, in the order of the
503+
reconstruction: the position at which the mapper's interpolant takes
504+
each pixel's value.
505+
506+
For the adaptive meshes these are the nodes of the regular lattice in
507+
CDF space mapped back through the mapper's inverse CDF (see
508+
``adaptive_rectangular_node_centres_from``), so they bunch up where the
509+
mesh adapts. The uniform overlay ``mesh_grid`` is the lattice before
510+
that mapping and is not where the values live. For
511+
``RectangularUniform`` the two are the same.
512+
"""
513+
if self.transform == "uniform":
514+
return self.mesh_grid.array
515+
516+
from autoarray.inversion.mesh.interpolator.rectangular import (
517+
KERNEL_CDF_DEFAULT_BANDWIDTH,
518+
KERNEL_CDF_DEFAULT_KNOTS,
519+
adaptive_rectangular_node_centres_from,
520+
)
521+
522+
return adaptive_rectangular_node_centres_from(
523+
source_grid_shape=self.shape_native,
524+
data_grid=self.data_grid.array,
525+
mesh_weight_map=self.mesh_weight_map,
526+
bandwidth=(
527+
self.kernel_bandwidth
528+
if self.kernel_bandwidth is not None
529+
else KERNEL_CDF_DEFAULT_BANDWIDTH
530+
),
531+
n_knots=(
532+
self.kernel_knots
533+
if self.kernel_knots is not None
534+
else KERNEL_CDF_DEFAULT_KNOTS
535+
),
536+
transform=self.transform,
537+
xp=self._xp,
538+
)
539+
499540
@property
500541
def edges_transformed(self):
501542
"""

‎autoarray/inversion/mock/mock_mapper.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ def source_plane_mesh_grid(self):
5151
return super().source_plane_mesh_grid
5252
return self._source_plane_mesh_grid
5353

54+
@property
55+
def regularization_mesh_grid(self):
56+
if self._source_plane_mesh_grid is None:
57+
return super().regularization_mesh_grid
58+
return self._source_plane_mesh_grid
59+
5460
@property
5561
def adapt_data(self):
5662
if self._adapt_data is None:

‎autoarray/inversion/regularization/abstract.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ def validate_coefficient(coefficient, name: str = "coefficient"):
4444
)
4545

4646

47+
def regularization_mesh_grid_from(linear_obj):
48+
"""
49+
The (y,x) positions a kernel regularization measures pixel-to-pixel distances on.
50+
51+
Mappers expose `regularization_mesh_grid` (the regular lattice for the adaptive rectangular meshes, see
52+
`InterpolatorRectangular.source_plane_mesh_grid`). Other linear objects, including duck-typed ones in
53+
downstream libraries, only define `source_plane_mesh_grid`, which is used for them.
54+
"""
55+
grid = getattr(linear_obj, "regularization_mesh_grid", None)
56+
57+
if grid is None:
58+
return linear_obj.source_plane_mesh_grid
59+
60+
return grid
61+
62+
4763
class AbstractRegularization:
4864
is_split_regularization = False
4965
"""

‎autoarray/inversion/regularization/exponential_kernel.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from autoarray.inversion.regularization.abstract import AbstractRegularization
99
from autoarray.inversion.regularization.abstract import validate_coefficient
10+
from autoarray.inversion.regularization.abstract import regularization_mesh_grid_from
1011

1112

1213
def exp_cov_matrix_from(
@@ -161,7 +162,7 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray
161162

162163
covariance_matrix = exp_cov_matrix_from(
163164
scale=self.scale,
164-
pixel_points=linear_obj.source_plane_mesh_grid.array,
165+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
165166
jitter=self.jitter_value,
166167
jitter_relative=self.jitter_relative,
167168
xp=xp,
@@ -185,7 +186,7 @@ def log_det_regularization_matrix_term_from(
185186
"""
186187
covariance_matrix = exp_cov_matrix_from(
187188
scale=self.scale,
188-
pixel_points=linear_obj.source_plane_mesh_grid.array,
189+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
189190
jitter=self.jitter_value,
190191
jitter_relative=self.jitter_relative,
191192
xp=xp,
@@ -217,7 +218,7 @@ def regularization_term_from(
217218

218219
covariance_matrix = exp_cov_matrix_from(
219220
scale=self.scale,
220-
pixel_points=linear_obj.source_plane_mesh_grid.array,
221+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
221222
jitter=self.jitter_value,
222223
jitter_relative=self.jitter_relative,
223224
xp=xp,

‎autoarray/inversion/regularization/gaussian_kernel.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from autoarray.inversion.regularization.abstract import AbstractRegularization
99
from autoarray.inversion.regularization.abstract import validate_coefficient
10+
from autoarray.inversion.regularization.abstract import regularization_mesh_grid_from
1011

1112

1213
def gauss_cov_matrix_from(
@@ -150,7 +151,7 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray
150151

151152
covariance_matrix = gauss_cov_matrix_from(
152153
scale=self.scale,
153-
pixel_points=linear_obj.source_plane_mesh_grid.array,
154+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
154155
jitter=self.jitter_value,
155156
jitter_relative=self.jitter_relative,
156157
xp=xp,
@@ -198,7 +199,7 @@ def log_det_regularization_matrix_term_from(
198199
"""
199200
covariance_matrix = gauss_cov_matrix_from(
200201
scale=self.scale,
201-
pixel_points=linear_obj.source_plane_mesh_grid.array,
202+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
202203
jitter=self.jitter_value,
203204
jitter_relative=self.jitter_relative,
204205
xp=xp,
@@ -236,7 +237,7 @@ def regularization_term_from(
236237

237238
covariance_matrix = gauss_cov_matrix_from(
238239
scale=self.scale,
239-
pixel_points=linear_obj.source_plane_mesh_grid.array,
240+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
240241
jitter=self.jitter_value,
241242
jitter_relative=self.jitter_relative,
242243
xp=xp,

‎autoarray/inversion/regularization/matern_adapt_kernel.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from autoarray.inversion.regularization.adapt import adapt_regularization_weights_from
1717
from autoarray.inversion.regularization.abstract import validate_coefficient
18+
from autoarray.inversion.regularization.abstract import regularization_mesh_grid_from
1819

1920

2021
class MaternAdaptKernel(MaternKernel):
@@ -146,7 +147,7 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray
146147
linear_obj=linear_obj, xp=xp
147148
)
148149

149-
pixel_points = linear_obj.source_plane_mesh_grid.array
150+
pixel_points = regularization_mesh_grid_from(linear_obj).array
150151

151152
covariance_matrix = matern_cov_matrix_from(
152153
scale=self.scale,
@@ -178,7 +179,7 @@ def log_det_regularization_matrix_term_from(
178179

179180
covariance_matrix = matern_cov_matrix_from(
180181
scale=self.scale,
181-
pixel_points=linear_obj.source_plane_mesh_grid.array,
182+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
182183
nu=self.nu,
183184
weights=kernel_weights,
184185
jitter=self.jitter_value,
@@ -210,7 +211,7 @@ def regularization_term_from(
210211

211212
covariance_matrix = matern_cov_matrix_from(
212213
scale=self.scale,
213-
pixel_points=linear_obj.source_plane_mesh_grid.array,
214+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
214215
nu=self.nu,
215216
weights=kernel_weights,
216217
jitter=self.jitter_value,

‎autoarray/inversion/regularization/matern_kernel.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from autoarray.inversion.regularization.abstract import AbstractRegularization
1010
from autoarray.inversion.regularization.abstract import validate_coefficient
11+
from autoarray.inversion.regularization.abstract import regularization_mesh_grid_from
1112

1213

1314
def kv_xp(v, z, xp=np):
@@ -367,7 +368,7 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray
367368
"""
368369
covariance_matrix = matern_cov_matrix_from(
369370
scale=self.scale,
370-
pixel_points=linear_obj.source_plane_mesh_grid.array,
371+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
371372
nu=self.nu,
372373
jitter=self.jitter_value,
373374
jitter_relative=self.jitter_relative,
@@ -390,7 +391,7 @@ def log_det_regularization_matrix_term_from(
390391
"""
391392
covariance_matrix = matern_cov_matrix_from(
392393
scale=self.scale,
393-
pixel_points=linear_obj.source_plane_mesh_grid.array,
394+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
394395
nu=self.nu,
395396
jitter=self.jitter_value,
396397
jitter_relative=self.jitter_relative,
@@ -419,7 +420,7 @@ def regularization_term_from(
419420
"""
420421
covariance_matrix = matern_cov_matrix_from(
421422
scale=self.scale,
422-
pixel_points=linear_obj.source_plane_mesh_grid.array,
423+
pixel_points=regularization_mesh_grid_from(linear_obj).array,
423424
nu=self.nu,
424425
jitter=self.jitter_value,
425426
jitter_relative=self.jitter_relative,

0 commit comments

Comments
 (0)