Skip to content

Commit e71feaf

Browse files
authored
Merge pull request #666 from PyAutoLabs/feature/mesh-magnification-knn-areas
feat: exact kNN mesh magnification areas (arc phase 4b)
2 parents 7fb3805 + 0bc03d4 commit e71feaf

3 files changed

Lines changed: 117 additions & 7 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,25 @@ class InterpolatorKNearestNeighbor(InterpolatorDelaunay):
150150
# Excluding them would change pixel signals broadly and add a jump in the likelihood (PyAutoArray#627).
151151
exclude_zero_weight_mappings = False
152152

153+
@cached_property
154+
def mesh_geometry(self):
155+
"""
156+
The kNN geometry: Delaunay vertex geometry whose
157+
`areas_for_magnification` integrates this interpolator's own basis
158+
functions (see `MeshGeometryKNearestNeighbor`).
159+
"""
160+
from autoarray.inversion.mesh.mesh_geometry.delaunay import (
161+
MeshGeometryKNearestNeighbor,
162+
)
163+
164+
return MeshGeometryKNearestNeighbor(
165+
mesh=self.mesh,
166+
mesh_grid=self.mesh_grid,
167+
data_grid=self.data_grid,
168+
interpolator_cls=type(self),
169+
xp=self._xp,
170+
)
171+
153172
@property
154173
def dual_areas(self):
155174
"""

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,87 @@ def areas_for_magnification(self) -> np.ndarray:
271271
scipy.spatial.Delaunay(mesh_grid_xy).simplices,
272272
xp=np,
273273
)
274+
275+
276+
# Quadrature cells per axis for `MeshGeometryKNearestNeighbor.areas_for_magnification`.
277+
# On a 144-vertex mesh the identity-lens magnification moved by < 3e-5 between
278+
# 200, 400 and 800 cells per axis (PyAutoArray#665); 256 costs ~1 s there.
279+
KNN_AREA_QUADRATURE_PIXELS: int = 256
280+
281+
282+
class MeshGeometryKNearestNeighbor(MeshGeometryDelaunay):
283+
def __init__(self, mesh, mesh_grid, data_grid, interpolator_cls, xp=np, **kwargs):
284+
"""
285+
The geometry of the kNN meshes (`KNearestNeighbor`, `KNNBarycentric`):
286+
Delaunay vertex geometry, but magnification areas that integrate the
287+
kNN interpolant itself.
288+
289+
Parameters
290+
----------
291+
interpolator_cls
292+
The kNN interpolator class that built this geometry; its basis
293+
functions are evaluated on a quadrature grid to compute
294+
`areas_for_magnification`.
295+
"""
296+
super().__init__(
297+
mesh=mesh,
298+
mesh_grid=mesh_grid,
299+
data_grid=data_grid,
300+
dual_areas=None,
301+
xp=xp,
302+
**kwargs,
303+
)
304+
305+
self.interpolator_cls = interpolator_cls
306+
307+
@cached_property
308+
def areas_for_magnification(self) -> np.ndarray:
309+
"""
310+
The integral of every vertex's interpolation basis function over the
311+
convex hull of the mesh, so `sum(reconstruction * areas_for_magnification)`
312+
is the integral of the reconstructed source.
313+
314+
The Delaunay barycentric dual areas are these integrals only for the linear
315+
barycentric interpolant. The Wendland-kNN and 3-nearest barycentric
316+
interpolants are not linear on the Delaunay triangles, and weighting by the
317+
dual areas biased their magnification by up to ~6% (PyAutoArray#665).
318+
319+
The integral is a midpoint quadrature: the interpolator's own mapping
320+
weights are evaluated at the centres of a uniform
321+
`KNN_AREA_QUADRATURE_PIXELS`^2 grid over the vertices' bounding box, the
322+
centres outside the convex hull are dropped, and the weights times the
323+
cell area are summed per vertex. The total is rescaled to the hull area,
324+
which removes the cells straddling the hull edge from the sum (the same
325+
domain the Delaunay dual areas tile).
326+
327+
Computed host-side on first access, like the Delaunay dual-area fallback.
328+
"""
329+
import scipy.spatial
330+
331+
nodes = np.asarray(self.mesh_grid_xy)
332+
lo = nodes.min(axis=0)
333+
hi = nodes.max(axis=0)
334+
335+
n = KNN_AREA_QUADRATURE_PIXELS
336+
u = (np.arange(n) + 0.5) / n
337+
grid_0, grid_1 = np.meshgrid(
338+
lo[0] + u * (hi[0] - lo[0]), lo[1] + u * (hi[1] - lo[1]), indexing="ij"
339+
)
340+
points = np.stack([grid_0.ravel(), grid_1.ravel()], axis=1)
341+
points = points[scipy.spatial.Delaunay(nodes).find_simplex(points) >= 0]
342+
343+
interpolator = self.interpolator_cls(
344+
mesh=self.mesh,
345+
mesh_grid=self.mesh_grid,
346+
data_grid=points,
347+
xp=np,
348+
)
349+
mappings, _, weights = interpolator._mappings_sizes_weights
350+
351+
areas = np.bincount(
352+
np.asarray(mappings).ravel(),
353+
weights=np.asarray(weights).ravel(),
354+
minlength=nodes.shape[0],
355+
)
356+
357+
return areas * scipy.spatial.ConvexHull(nodes).volume / np.sum(areas)

‎test_autoarray/inversion/pixelization/mesh_geometry/test_areas_for_magnification.py‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,18 @@
7676
# not the integral of the node's hat once the CDF bends across the cell (the
7777
# rank CDF's staircase on tied coordinates; a steep adapt-image CDF).
7878
#
79-
# Documented approximations (flagged in PyAutoArray#659; phase 4b):
80-
# - DelaunayNN / KNearestNeighbor / KNNBarycentric inherit the Delaunay dual
81-
# areas, which are exact only for the linear interpolant; for the Sibson,
82-
# Wendland-kNN and 3-nearest-barycentric interpolants they are an approximation
83-
# [-4.3e-3, +1.6e-2, -4.4e-2].
79+
# - KNearestNeighbor / KNNBarycentric: the integral of each vertex's kNN basis
80+
# function over the convex hull, by midpoint quadrature of the interpolator's
81+
# own weights (`MeshGeometryKNearestNeighbor`, PyAutoArray#665)
82+
# [+1.3e-5, +1.8e-3; KNNBarycentric's clipped weights are discontinuous, so
83+
# the coarse sub-pixel `Q` is the limit there]. The Delaunay dual areas they
84+
# used before measured [+1.6e-2, -4.4e-2]. The quadrature integrates whatever
85+
# neighbours the interpolator selects, so it stays exact with or without the
86+
# open partial-block neighbour bug PyAutoArray#609.
87+
#
88+
# Documented approximation:
89+
# - DelaunayNN inherits the Delaunay dual areas, exact only for the linear
90+
# interpolant; for its Sibson interpolant they are an approximation [-4.3e-3].
8491
MAGNIFICATION_TOLERANCE = {
8592
"RectangularUniform": 2.5e-3,
8693
"RectangularBilinearAdaptDensity": 1.0e-3,
@@ -89,8 +96,8 @@
8996
"RectangularRTUAdaptImage": 1.0e-2,
9097
"Delaunay": 3.0e-4,
9198
"DelaunayNN": 1.0e-2,
92-
"KNearestNeighbor": 3.0e-2,
93-
"KNNBarycentric": 7.0e-2,
99+
"KNearestNeighbor": 1.0e-4,
100+
"KNNBarycentric": 4.0e-3,
94101
}
95102

96103
RECTANGULAR_ADAPTIVE = [

0 commit comments

Comments
 (0)