_spherical_voronoi.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. """
  2. Spherical Voronoi Code
  3. .. versionadded:: 0.18.0
  4. """
  5. #
  6. # Copyright (C) Tyler Reddy, Ross Hemsley, Edd Edmondson,
  7. # Nikolai Nowaczyk, Joe Pitt-Francis, 2015.
  8. #
  9. # Distributed under the same BSD license as Scipy.
  10. #
  11. import numpy as np
  12. import scipy
  13. import itertools
  14. from . import _voronoi
  15. from scipy.spatial.distance import pdist
  16. __all__ = ['SphericalVoronoi']
  17. def sphere_check(points, radius, center):
  18. """ Determines distance of generators from theoretical sphere
  19. surface.
  20. """
  21. actual_squared_radii = (((points[...,0] - center[0]) ** 2) +
  22. ((points[...,1] - center[1]) ** 2) +
  23. ((points[...,2] - center[2]) ** 2))
  24. max_discrepancy = (np.sqrt(actual_squared_radii) - radius).max()
  25. return abs(max_discrepancy)
  26. def calc_circumcenters(tetrahedrons):
  27. """ Calculates the cirumcenters of the circumspheres of tetrahedrons.
  28. An implementation based on
  29. http://mathworld.wolfram.com/Circumsphere.html
  30. Parameters
  31. ----------
  32. tetrahedrons : an array of shape (N, 4, 3)
  33. consisting of N tetrahedrons defined by 4 points in 3D
  34. Returns
  35. ----------
  36. circumcenters : an array of shape (N, 3)
  37. consisting of the N circumcenters of the tetrahedrons in 3D
  38. """
  39. num = tetrahedrons.shape[0]
  40. a = np.concatenate((tetrahedrons, np.ones((num, 4, 1))), axis=2)
  41. sums = np.sum(tetrahedrons ** 2, axis=2)
  42. d = np.concatenate((sums[:, :, np.newaxis], a), axis=2)
  43. dx = np.delete(d, 1, axis=2)
  44. dy = np.delete(d, 2, axis=2)
  45. dz = np.delete(d, 3, axis=2)
  46. dx = np.linalg.det(dx)
  47. dy = -np.linalg.det(dy)
  48. dz = np.linalg.det(dz)
  49. a = np.linalg.det(a)
  50. nominator = np.vstack((dx, dy, dz))
  51. denominator = 2*a
  52. return (nominator / denominator).T
  53. def project_to_sphere(points, center, radius):
  54. """
  55. Projects the elements of points onto the sphere defined
  56. by center and radius.
  57. Parameters
  58. ----------
  59. points : array of floats of shape (npoints, ndim)
  60. consisting of the points in a space of dimension ndim
  61. center : array of floats of shape (ndim,)
  62. the center of the sphere to project on
  63. radius : float
  64. the radius of the sphere to project on
  65. returns: array of floats of shape (npoints, ndim)
  66. the points projected onto the sphere
  67. """
  68. lengths = scipy.spatial.distance.cdist(points, np.array([center]))
  69. return (points - center) / lengths * radius + center
  70. class SphericalVoronoi:
  71. """ Voronoi diagrams on the surface of a sphere.
  72. .. versionadded:: 0.18.0
  73. Parameters
  74. ----------
  75. points : ndarray of floats, shape (npoints, 3)
  76. Coordinates of points to construct a spherical
  77. Voronoi diagram from
  78. radius : float, optional
  79. Radius of the sphere (Default: 1)
  80. center : ndarray of floats, shape (3,)
  81. Center of sphere (Default: origin)
  82. threshold : float
  83. Threshold for detecting duplicate points and
  84. mismatches between points and sphere parameters.
  85. (Default: 1e-06)
  86. Attributes
  87. ----------
  88. points : double array of shape (npoints, 3)
  89. the points in 3D to generate the Voronoi diagram from
  90. radius : double
  91. radius of the sphere
  92. Default: None (forces estimation, which is less precise)
  93. center : double array of shape (3,)
  94. center of the sphere
  95. Default: None (assumes sphere is centered at origin)
  96. vertices : double array of shape (nvertices, 3)
  97. Voronoi vertices corresponding to points
  98. regions : list of list of integers of shape (npoints, _ )
  99. the n-th entry is a list consisting of the indices
  100. of the vertices belonging to the n-th point in points
  101. Raises
  102. ------
  103. ValueError
  104. If there are duplicates in `points`.
  105. If the provided `radius` is not consistent with `points`.
  106. Notes
  107. ----------
  108. The spherical Voronoi diagram algorithm proceeds as follows. The Convex
  109. Hull of the input points (generators) is calculated, and is equivalent to
  110. their Delaunay triangulation on the surface of the sphere [Caroli]_.
  111. A 3D Delaunay tetrahedralization is obtained by including the origin of
  112. the coordinate system as the fourth vertex of each simplex of the Convex
  113. Hull. The circumcenters of all tetrahedra in the system are calculated and
  114. projected to the surface of the sphere, producing the Voronoi vertices.
  115. The Delaunay tetrahedralization neighbour information is then used to
  116. order the Voronoi region vertices around each generator. The latter
  117. approach is substantially less sensitive to floating point issues than
  118. angle-based methods of Voronoi region vertex sorting.
  119. The surface area of spherical polygons is calculated by decomposing them
  120. into triangles and using L'Huilier's Theorem to calculate the spherical
  121. excess of each triangle [Weisstein]_. The sum of the spherical excesses is
  122. multiplied by the square of the sphere radius to obtain the surface area
  123. of the spherical polygon. For nearly-degenerate spherical polygons an area
  124. of approximately 0 is returned by default, rather than attempting the
  125. unstable calculation.
  126. Empirical assessment of spherical Voronoi algorithm performance suggests
  127. quadratic time complexity (loglinear is optimal, but algorithms are more
  128. challenging to implement). The reconstitution of the surface area of the
  129. sphere, measured as the sum of the surface areas of all Voronoi regions,
  130. is closest to 100 % for larger (>> 10) numbers of generators.
  131. References
  132. ----------
  133. .. [Caroli] Caroli et al. Robust and Efficient Delaunay triangulations of
  134. points on or close to a sphere. Research Report RR-7004, 2009.
  135. .. [Weisstein] "L'Huilier's Theorem." From MathWorld -- A Wolfram Web
  136. Resource. http://mathworld.wolfram.com/LHuiliersTheorem.html
  137. See Also
  138. --------
  139. Voronoi : Conventional Voronoi diagrams in N dimensions.
  140. Examples
  141. --------
  142. >>> from matplotlib import colors
  143. >>> from mpl_toolkits.mplot3d.art3d import Poly3DCollection
  144. >>> import matplotlib.pyplot as plt
  145. >>> from scipy.spatial import SphericalVoronoi
  146. >>> from mpl_toolkits.mplot3d import proj3d
  147. >>> # set input data
  148. >>> points = np.array([[0, 0, 1], [0, 0, -1], [1, 0, 0],
  149. ... [0, 1, 0], [0, -1, 0], [-1, 0, 0], ])
  150. >>> center = np.array([0, 0, 0])
  151. >>> radius = 1
  152. >>> # calculate spherical Voronoi diagram
  153. >>> sv = SphericalVoronoi(points, radius, center)
  154. >>> # sort vertices (optional, helpful for plotting)
  155. >>> sv.sort_vertices_of_regions()
  156. >>> # generate plot
  157. >>> fig = plt.figure()
  158. >>> ax = fig.add_subplot(111, projection='3d')
  159. >>> # plot the unit sphere for reference (optional)
  160. >>> u = np.linspace(0, 2 * np.pi, 100)
  161. >>> v = np.linspace(0, np.pi, 100)
  162. >>> x = np.outer(np.cos(u), np.sin(v))
  163. >>> y = np.outer(np.sin(u), np.sin(v))
  164. >>> z = np.outer(np.ones(np.size(u)), np.cos(v))
  165. >>> ax.plot_surface(x, y, z, color='y', alpha=0.1)
  166. >>> # plot generator points
  167. >>> ax.scatter(points[:, 0], points[:, 1], points[:, 2], c='b')
  168. >>> # plot Voronoi vertices
  169. >>> ax.scatter(sv.vertices[:, 0], sv.vertices[:, 1], sv.vertices[:, 2],
  170. ... c='g')
  171. >>> # indicate Voronoi regions (as Euclidean polygons)
  172. >>> for region in sv.regions:
  173. ... random_color = colors.rgb2hex(np.random.rand(3))
  174. ... polygon = Poly3DCollection([sv.vertices[region]], alpha=1.0)
  175. ... polygon.set_color(random_color)
  176. ... ax.add_collection3d(polygon)
  177. >>> plt.show()
  178. """
  179. def __init__(self, points, radius=None, center=None, threshold=1e-06):
  180. """
  181. Initializes the object and starts the computation of the Voronoi
  182. diagram.
  183. points : The generator points of the Voronoi diagram assumed to be
  184. all on the sphere with radius supplied by the radius parameter and
  185. center supplied by the center parameter.
  186. radius : The radius of the sphere. Will default to 1 if not supplied.
  187. center : The center of the sphere. Will default to the origin if not
  188. supplied.
  189. """
  190. self.points = points
  191. if np.any(center):
  192. self.center = center
  193. else:
  194. self.center = np.zeros(3)
  195. if radius:
  196. self.radius = radius
  197. else:
  198. self.radius = 1
  199. if pdist(self.points).min() <= threshold * self.radius:
  200. raise ValueError("Duplicate generators present.")
  201. max_discrepancy = sphere_check(self.points,
  202. self.radius,
  203. self.center)
  204. if max_discrepancy >= threshold * self.radius:
  205. raise ValueError("Radius inconsistent with generators.")
  206. self.vertices = None
  207. self.regions = None
  208. self._tri = None
  209. self._calc_vertices_regions()
  210. def _calc_vertices_regions(self):
  211. """
  212. Calculates the Voronoi vertices and regions of the generators stored
  213. in self.points. The vertices will be stored in self.vertices and the
  214. regions in self.regions.
  215. This algorithm was discussed at PyData London 2015 by
  216. Tyler Reddy, Ross Hemsley and Nikolai Nowaczyk
  217. """
  218. # perform 3D Delaunay triangulation on data set
  219. # (here ConvexHull can also be used, and is faster)
  220. self._tri = scipy.spatial.ConvexHull(self.points)
  221. # add the center to each of the simplices in tri to get the same
  222. # tetrahedrons we'd have gotten from Delaunay tetrahedralization
  223. # tetrahedrons will have shape: (2N-4, 4, 3)
  224. tetrahedrons = self._tri.points[self._tri.simplices]
  225. tetrahedrons = np.insert(
  226. tetrahedrons,
  227. 3,
  228. np.array([self.center]),
  229. axis=1
  230. )
  231. # produce circumcenters of tetrahedrons from 3D Delaunay
  232. # circumcenters will have shape: (2N-4, 3)
  233. circumcenters = calc_circumcenters(tetrahedrons)
  234. # project tetrahedron circumcenters to the surface of the sphere
  235. # self.vertices will have shape: (2N-4, 3)
  236. self.vertices = project_to_sphere(
  237. circumcenters,
  238. self.center,
  239. self.radius
  240. )
  241. # calculate regions from triangulation
  242. # simplex_indices will have shape: (2N-4,)
  243. simplex_indices = np.arange(self._tri.simplices.shape[0])
  244. # tri_indices will have shape: (6N-12,)
  245. tri_indices = np.column_stack([simplex_indices, simplex_indices,
  246. simplex_indices]).ravel()
  247. # point_indices will have shape: (6N-12,)
  248. point_indices = self._tri.simplices.ravel()
  249. # array_associations will have shape: (6N-12, 2)
  250. array_associations = np.dstack((point_indices, tri_indices))[0]
  251. array_associations = array_associations[np.lexsort((
  252. array_associations[...,1],
  253. array_associations[...,0]))]
  254. array_associations = array_associations.astype(np.intp)
  255. # group by generator indices to produce
  256. # unsorted regions in nested list
  257. groups = []
  258. for k, g in itertools.groupby(array_associations,
  259. lambda t: t[0]):
  260. groups.append(list(list(zip(*list(g)))[1]))
  261. self.regions = groups
  262. def sort_vertices_of_regions(self):
  263. """
  264. For each region in regions, it sorts the indices of the Voronoi
  265. vertices such that the resulting points are in a clockwise or
  266. counterclockwise order around the generator point.
  267. This is done as follows: Recall that the n-th region in regions
  268. surrounds the n-th generator in points and that the k-th
  269. Voronoi vertex in vertices is the projected circumcenter of the
  270. tetrahedron obtained by the k-th triangle in _tri.simplices (and the
  271. origin). For each region n, we choose the first triangle (=Voronoi
  272. vertex) in _tri.simplices and a vertex of that triangle not equal to
  273. the center n. These determine a unique neighbor of that triangle,
  274. which is then chosen as the second triangle. The second triangle
  275. will have a unique vertex not equal to the current vertex or the
  276. center. This determines a unique neighbor of the second triangle,
  277. which is then chosen as the third triangle and so forth. We proceed
  278. through all the triangles (=Voronoi vertices) belonging to the
  279. generator in points and obtain a sorted version of the vertices
  280. of its surrounding region.
  281. """
  282. _voronoi.sort_vertices_of_regions(self._tri.simplices,
  283. self.regions)