11import numpy as np
22import scipy .spatial
3- from scipy .spatial import cKDTree
3+ from scipy .spatial import cKDTree , Delaunay
44from typing import List , Union , Optional , Tuple
55
66from autoconf import cached_property
1616
1717def scipy_delaunay (points_np , query_points_np , max_simplices ):
1818 """Compute Delaunay simplices (simplices_padded) and Voronoi areas in one call."""
19- from scipy .spatial import Delaunay
20- import numpy as np
2119
2220 # --- Delaunay mesh using source plane data grid ---
2321 tri = Delaunay (points_np )
@@ -331,6 +329,16 @@ def geometry(self):
331329 scaled_minima = scaled_minima ,
332330 )
333331
332+ @cached_property
333+ def mesh_grid_xy (self ):
334+ """
335+ The default convention in `scipy.spatial` is to represent 2D coordinates as (x,y) pairs, whereas PyAutoArray
336+ represents 2D coordinates as (y,x) pairs.
337+
338+ Therefore, this property simply converts the (y,x) grid of irregular coordinates into an (x,y) grid.
339+ """
340+ return self ._xp .stack ([self .array [:, 1 ], self .array [:, 0 ]]).T
341+
334342 @cached_property
335343 def delaunay (self ) -> "scipy.spatial.Delaunay" :
336344 """
@@ -345,22 +353,19 @@ def delaunay(self) -> "scipy.spatial.Delaunay":
345353 to compute the Voronoi mesh are ill posed. These exceptions are caught and combined into a single
346354 `MeshException`, which helps exception handling in the `inversion` package.
347355 """
348-
349- mesh_grid = self ._xp .stack ([self .array [:, 0 ], self .array [:, 1 ]]).T
350-
351356 if self ._xp .__name__ .startswith ("jax" ):
352357
353358 import jax .numpy as jnp
354359
355360 points , simplices , mappings , split_points , splitted_mappings = jax_delaunay (
356- points = mesh_grid ,
361+ points = self . mesh_grid_xy ,
357362 query_points = self ._source_plane_data_grid_over_sampled
358363 )
359364
360365 else :
361366
362367 points , simplices , mappings , split_points , splitted_mappings = scipy_delaunay (
363- points_np = mesh_grid ,
368+ points = self . mesh_grid_xy ,
364369 query_points_np = self .source_plane_data_grid_over_sampled
365370 )
366371
@@ -415,35 +420,11 @@ def neighbors(self) -> Neighbors:
415420
416421 return Neighbors (arr = neighbors .astype ("int" ), sizes = sizes .astype ("int" ))
417422
418- @property
419- def voronoi (self ) -> "scipy.spatial.Voronoi" :
420- """
421- Returns a `scipy.spatial.Voronoi` object from the 2D (y,x) grid of irregular coordinates, which correspond to
422- the centre of every Voronoi pixel.
423-
424- This object contains numerous attributes describing a Voronoi mesh. PyAutoArray uses
425- the `vertex_neighbor_vertices` attribute to determine the neighbors of every Delaunay triangle.
426-
427- There are numerous exceptions that `scipy.spatial.Delaunay` may raise when the input grid of coordinates used
428- to compute the Delaunay triangulation are ill posed. These exceptions are caught and combined into a single
429- `MeshException`, which helps exception handling in the `inversion` package.
430- """
431- import scipy .spatial
432- from scipy .spatial import QhullError
433-
434- try :
435- return scipy .spatial .Voronoi (
436- np .asarray ([self .array [:, 1 ], self .array [:, 0 ]]).T ,
437- qhull_options = "Qbb Qc Qx Qm" ,
438- )
439- except (ValueError , OverflowError , QhullError ) as e :
440- raise exc .MeshException () from e
441-
442423 def interpolated_array_from (
443- self ,
444- values : np .ndarray ,
445- shape_native : Tuple [int , int ] = (401 , 401 ),
446- extent : Optional [Tuple [float , float , float , float ]] = None ,
424+ self ,
425+ values : np .ndarray ,
426+ shape_native : Tuple [int , int ] = (401 , 401 ),
427+ extent : Optional [Tuple [float , float , float , float ]] = None ,
447428 ) -> Array2D :
448429 """
449430 The reconstruction of data on a `Delaunay` triangulation (e.g. the `reconstruction` output from an `Inversion`)
@@ -470,21 +451,72 @@ def interpolated_array_from(
470451 The (x0, x1, y0, y1) extent of the grid in scaled coordinates over which the grid is created if it
471452 is input.
472453 """
454+ # Uses find simplex so recomputes delaunay internally
455+ delaunay = Delaunay (self .mesh_grid_xy )
456+
473457 interpolation_grid = self .interpolation_grid_from (
474458 shape_native = shape_native , extent = extent
475459 )
476460
477461 interpolated_array = mesh_numba_util .delaunay_interpolated_array_from (
478462 shape_native = shape_native ,
479463 interpolation_grid_slim = np .array (interpolation_grid .slim .array ),
480- delaunay = self . delaunay ,
464+ delaunay = delaunay ,
481465 pixel_values = values ,
482466 )
483467
484468 return Array2D .no_mask (
485469 values = interpolated_array , pixel_scales = interpolation_grid .pixel_scales
486470 )
487471
472+ @cached_property
473+ def voronoi (self ) -> "scipy.spatial.Voronoi" :
474+ """
475+ Returns a `scipy.spatial.Voronoi` object from the 2D (y,x) grid of irregular coordinates, which correspond to
476+ the centre of every Voronoi pixel.
477+
478+ This object contains numerous attributes describing a Voronoi mesh. PyAutoArray uses
479+ the `vertex_neighbor_vertices` attribute to determine the neighbors of every Delaunay triangle.
480+
481+ There are numerous exceptions that `scipy.spatial.Delaunay` may raise when the input grid of coordinates used
482+ to compute the Delaunay triangulation are ill posed. These exceptions are caught and combined into a single
483+ `MeshException`, which helps exception handling in the `inversion` package.
484+ """
485+ import scipy .spatial
486+ from scipy .spatial import QhullError
487+
488+ try :
489+ return scipy .spatial .Voronoi (
490+ self .mesh_grid_xy ,
491+ qhull_options = "Qbb Qc Qx Qm" ,
492+ )
493+ except (ValueError , OverflowError , QhullError ) as e :
494+ raise exc .MeshException () from e
495+
496+ @property
497+ def voronoi_areas (self ):
498+
499+ N = self .mesh_grid_xy .shape [0 ]
500+
501+ voronoi_areas = np .zeros (N )
502+
503+ for i in range (N ):
504+ region_vertices_indexes = self .voronoi .regions [self .voronoi .point_region [i ]]
505+ if - 1 in region_vertices_indexes :
506+ voronoi_areas [i ] = - 1
507+ else :
508+
509+ points_of_region = self .voronoivertices [region_vertices_indexes ]
510+
511+ x = points_of_region [:, 1 ]
512+ y = points_of_region [:, 0 ]
513+
514+ voronoi_areas [i ] = 0.5 * np .abs (
515+ np .dot (x , np .roll (y , 1 )) - np .dot (y , np .roll (x , 1 ))
516+ )
517+
518+ return voronoi_areas
519+
488520 @property
489521 def areas_for_magnification (self ) -> np .ndarray :
490522 """
@@ -494,7 +526,7 @@ def areas_for_magnification(self) -> np.ndarray:
494526 calculation. This method therefore sets their areas to zero so they do not impact the magnification
495527 calculation.
496528 """
497- areas = self .delaunay . voronoi_areas
529+ areas = self .voronoi_areas
498530
499531 areas [areas == - 1 ] = 0.0
500532
0 commit comments