@@ -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 )
0 commit comments