Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/scipy/spatial/_spherical_voronoi.py : 17%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Spherical Voronoi Code
4.. versionadded:: 0.18.0
6"""
7#
8# Copyright (C) Tyler Reddy, Ross Hemsley, Edd Edmondson,
9# Nikolai Nowaczyk, Joe Pitt-Francis, 2015.
10#
11# Distributed under the same BSD license as SciPy.
12#
14import warnings
15import numpy as np
16import scipy
17from . import _voronoi
18from scipy.spatial import cKDTree
20__all__ = ['SphericalVoronoi']
23def calculate_solid_angles(R):
24 """Calculates the solid angles of plane triangles. Implements the method of
25 Van Oosterom and Strackee [VanOosterom]_ with some modifications. Assumes
26 that input points have unit norm."""
27 # Original method uses a triple product `R1 . (R2 x R3)` for the numerator.
28 # This is equal to the determinant of the matrix [R1 R2 R3], which can be
29 # computed with better stability.
30 numerator = np.linalg.det(R)
31 denominator = 1 + (np.einsum('ij,ij->i', R[:, 0], R[:, 1]) +
32 np.einsum('ij,ij->i', R[:, 1], R[:, 2]) +
33 np.einsum('ij,ij->i', R[:, 2], R[:, 0]))
34 return np.abs(2 * np.arctan2(numerator, denominator))
37class SphericalVoronoi:
38 """ Voronoi diagrams on the surface of a sphere.
40 .. versionadded:: 0.18.0
42 Parameters
43 ----------
44 points : ndarray of floats, shape (npoints, ndim)
45 Coordinates of points from which to construct a spherical
46 Voronoi diagram.
47 radius : float, optional
48 Radius of the sphere (Default: 1)
49 center : ndarray of floats, shape (ndim,)
50 Center of sphere (Default: origin)
51 threshold : float
52 Threshold for detecting duplicate points and
53 mismatches between points and sphere parameters.
54 (Default: 1e-06)
56 Attributes
57 ----------
58 points : double array of shape (npoints, ndim)
59 the points in `ndim` dimensions to generate the Voronoi diagram from
60 radius : double
61 radius of the sphere
62 center : double array of shape (ndim,)
63 center of the sphere
64 vertices : double array of shape (nvertices, ndim)
65 Voronoi vertices corresponding to points
66 regions : list of list of integers of shape (npoints, _ )
67 the n-th entry is a list consisting of the indices
68 of the vertices belonging to the n-th point in points
70 Methods
71 ----------
72 calculate_areas
73 Calculates the areas of the Voronoi regions. For 2D point sets, the
74 regions are circular arcs. The sum of the areas is `2 * pi * radius`.
75 For 3D point sets, the regions are spherical polygons. The sum of the
76 areas is `4 * pi * radius**2`.
78 Raises
79 ------
80 ValueError
81 If there are duplicates in `points`.
82 If the provided `radius` is not consistent with `points`.
84 Notes
85 -----
86 The spherical Voronoi diagram algorithm proceeds as follows. The Convex
87 Hull of the input points (generators) is calculated, and is equivalent to
88 their Delaunay triangulation on the surface of the sphere [Caroli]_.
89 The Convex Hull neighbour information is then used to
90 order the Voronoi region vertices around each generator. The latter
91 approach is substantially less sensitive to floating point issues than
92 angle-based methods of Voronoi region vertex sorting.
94 Empirical assessment of spherical Voronoi algorithm performance suggests
95 quadratic time complexity (loglinear is optimal, but algorithms are more
96 challenging to implement).
98 References
99 ----------
100 .. [Caroli] Caroli et al. Robust and Efficient Delaunay triangulations of
101 points on or close to a sphere. Research Report RR-7004, 2009.
103 .. [VanOosterom] Van Oosterom and Strackee. The solid angle of a plane
104 triangle. IEEE Transactions on Biomedical Engineering,
105 2, 1983, pp 125--126.
107 See Also
108 --------
109 Voronoi : Conventional Voronoi diagrams in N dimensions.
111 Examples
112 --------
113 Do some imports and take some points on a cube:
115 >>> import matplotlib.pyplot as plt
116 >>> from scipy.spatial import SphericalVoronoi, geometric_slerp
117 >>> from mpl_toolkits.mplot3d import proj3d
118 >>> # set input data
119 >>> points = np.array([[0, 0, 1], [0, 0, -1], [1, 0, 0],
120 ... [0, 1, 0], [0, -1, 0], [-1, 0, 0], ])
122 Calculate the spherical Voronoi diagram:
124 >>> radius = 1
125 >>> center = np.array([0, 0, 0])
126 >>> sv = SphericalVoronoi(points, radius, center)
128 Generate plot:
130 >>> # sort vertices (optional, helpful for plotting)
131 >>> sv.sort_vertices_of_regions()
132 >>> t_vals = np.linspace(0, 1, 2000)
133 >>> fig = plt.figure()
134 >>> ax = fig.add_subplot(111, projection='3d')
135 >>> # plot the unit sphere for reference (optional)
136 >>> u = np.linspace(0, 2 * np.pi, 100)
137 >>> v = np.linspace(0, np.pi, 100)
138 >>> x = np.outer(np.cos(u), np.sin(v))
139 >>> y = np.outer(np.sin(u), np.sin(v))
140 >>> z = np.outer(np.ones(np.size(u)), np.cos(v))
141 >>> ax.plot_surface(x, y, z, color='y', alpha=0.1)
142 >>> # plot generator points
143 >>> ax.scatter(points[:, 0], points[:, 1], points[:, 2], c='b')
144 >>> # plot Voronoi vertices
145 >>> ax.scatter(sv.vertices[:, 0], sv.vertices[:, 1], sv.vertices[:, 2],
146 ... c='g')
147 >>> # indicate Voronoi regions (as Euclidean polygons)
148 >>> for region in sv.regions:
149 ... n = len(region)
150 ... for i in range(n):
151 ... start = sv.vertices[region][i]
152 ... end = sv.vertices[region][(i + 1) % n]
153 ... result = geometric_slerp(start, end, t_vals)
154 ... ax.plot(result[..., 0],
155 ... result[..., 1],
156 ... result[..., 2],
157 ... c='k')
158 >>> ax.azim = 10
159 >>> ax.elev = 40
160 >>> _ = ax.set_xticks([])
161 >>> _ = ax.set_yticks([])
162 >>> _ = ax.set_zticks([])
163 >>> fig.set_size_inches(4, 4)
164 >>> plt.show()
166 """
167 def __init__(self, points, radius=1, center=None, threshold=1e-06):
169 if radius is None:
170 radius = 1.
171 warnings.warn('`radius` is `None`. '
172 'This will raise an error in a future version. '
173 'Please provide a floating point number '
174 '(i.e. `radius=1`).',
175 DeprecationWarning)
177 self.radius = float(radius)
178 self.points = np.array(points).astype(np.double)
179 self._dim = len(points[0])
180 if center is None:
181 self.center = np.zeros(self._dim)
182 else:
183 self.center = np.array(center, dtype=float)
185 # test degenerate input
186 self._rank = np.linalg.matrix_rank(self.points - self.points[0],
187 tol=threshold * self.radius)
188 if self._rank < self._dim:
189 raise ValueError("Rank of input points must be at least {0}".format(self._dim))
191 if cKDTree(self.points).query_pairs(threshold * self.radius):
192 raise ValueError("Duplicate generators present.")
194 radii = np.linalg.norm(self.points - self.center, axis=1)
195 max_discrepancy = np.abs(radii - self.radius).max()
196 if max_discrepancy >= threshold * self.radius:
197 raise ValueError("Radius inconsistent with generators.")
199 self._calc_vertices_regions()
201 def _calc_vertices_regions(self):
202 """
203 Calculates the Voronoi vertices and regions of the generators stored
204 in self.points. The vertices will be stored in self.vertices and the
205 regions in self.regions.
207 This algorithm was discussed at PyData London 2015 by
208 Tyler Reddy, Ross Hemsley and Nikolai Nowaczyk
209 """
210 # get Convex Hull
211 conv = scipy.spatial.ConvexHull(self.points)
212 # get circumcenters of Convex Hull triangles from facet equations
213 # for 3D input circumcenters will have shape: (2N-4, 3)
214 self.vertices = self.radius * conv.equations[:, :-1] + self.center
215 self._simplices = conv.simplices
216 # calculate regions from triangulation
217 # for 3D input simplex_indices will have shape: (2N-4,)
218 simplex_indices = np.arange(len(self._simplices))
219 # for 3D input tri_indices will have shape: (6N-12,)
220 tri_indices = np.column_stack([simplex_indices] * self._dim).ravel()
221 # for 3D input point_indices will have shape: (6N-12,)
222 point_indices = self._simplices.ravel()
223 # for 3D input indices will have shape: (6N-12,)
224 indices = np.argsort(point_indices, kind='mergesort')
225 # for 3D input flattened_groups will have shape: (6N-12,)
226 flattened_groups = tri_indices[indices].astype(np.intp)
227 # intervals will have shape: (N+1,)
228 intervals = np.cumsum(np.bincount(point_indices + 1))
229 # split flattened groups to get nested list of unsorted regions
230 groups = [list(flattened_groups[intervals[i]:intervals[i + 1]])
231 for i in range(len(intervals) - 1)]
232 self.regions = groups
234 def sort_vertices_of_regions(self):
235 """Sort indices of the vertices to be (counter-)clockwise ordered.
237 Raises
238 ------
239 TypeError
240 If the points are not three-dimensional.
242 Notes
243 -----
244 For each region in regions, it sorts the indices of the Voronoi
245 vertices such that the resulting points are in a clockwise or
246 counterclockwise order around the generator point.
248 This is done as follows: Recall that the n-th region in regions
249 surrounds the n-th generator in points and that the k-th
250 Voronoi vertex in vertices is the circumcenter of the k-th triangle
251 in self._simplices. For each region n, we choose the first triangle
252 (=Voronoi vertex) in self._simplices and a vertex of that triangle
253 not equal to the center n. These determine a unique neighbor of that
254 triangle, which is then chosen as the second triangle. The second
255 triangle will have a unique vertex not equal to the current vertex or
256 the center. This determines a unique neighbor of the second triangle,
257 which is then chosen as the third triangle and so forth. We proceed
258 through all the triangles (=Voronoi vertices) belonging to the
259 generator in points and obtain a sorted version of the vertices
260 of its surrounding region.
261 """
262 if self._dim != 3:
263 raise TypeError("Only supported for three-dimensional point sets")
264 _voronoi.sort_vertices_of_regions(self._simplices, self.regions)
266 def _calculate_areas_3d(self):
267 self.sort_vertices_of_regions()
268 sizes = [len(region) for region in self.regions]
269 csizes = np.cumsum(sizes)
270 num_regions = csizes[-1]
272 # We create a set of triangles consisting of one point and two Voronoi
273 # vertices. The vertices of each triangle are adjacent in the sorted
274 # regions list.
275 point_indices = [i for i, size in enumerate(sizes)
276 for j in range(size)]
278 nbrs1 = np.array([r for region in self.regions for r in region])
280 # The calculation of nbrs2 is a vectorized version of:
281 # np.array([r for region in self.regions for r in np.roll(region, 1)])
282 nbrs2 = np.roll(nbrs1, 1)
283 indices = np.roll(csizes, 1)
284 indices[0] = 0
285 nbrs2[indices] = nbrs1[csizes - 1]
287 # Normalize points and vertices.
288 pnormalized = (self.points - self.center) / self.radius
289 vnormalized = (self.vertices - self.center) / self.radius
291 # Create the complete set of triangles and calculate their solid angles
292 triangles = np.hstack([pnormalized[point_indices],
293 vnormalized[nbrs1],
294 vnormalized[nbrs2]
295 ]).reshape((num_regions, 3, 3))
296 triangle_solid_angles = calculate_solid_angles(triangles)
298 # Sum the solid angles of the triangles in each region
299 solid_angles = np.cumsum(triangle_solid_angles)[csizes - 1]
300 solid_angles[1:] -= solid_angles[:-1]
302 # Get polygon areas using A = omega * r**2
303 return solid_angles * self.radius**2
305 def _calculate_areas_2d(self):
306 # Find start and end points of arcs
307 arcs = self.points[self._simplices] - self.center
309 # Calculate the angle subtended by arcs
310 cosine = np.einsum('ij,ij->i', arcs[:, 0], arcs[:, 1])
311 sine = np.abs(np.linalg.det(arcs))
312 theta = np.arctan2(sine, cosine)
314 # Get areas using A = r * theta
315 areas = self.radius * theta
317 # Correct arcs which go the wrong way (single-hemisphere inputs)
318 signs = np.sign(np.einsum('ij,ij->i', arcs[:, 0],
319 self.vertices - self.center))
320 indices = np.where(signs < 0)
321 areas[indices] = 2 * np.pi * self.radius - areas[indices]
322 return areas
324 def calculate_areas(self):
325 """Calculates the areas of the Voronoi regions.
327 For 2D point sets, the regions are circular arcs. The sum of the areas
328 is `2 * pi * radius`.
330 For 3D point sets, the regions are spherical polygons. The sum of the
331 areas is `4 * pi * radius**2`.
333 .. versionadded:: 1.5.0
335 Returns
336 -------
337 areas : double array of shape (npoints,)
338 The areas of the Voronoi regions.
339 """
340 if self._dim == 2:
341 return self._calculate_areas_2d()
342 elif self._dim == 3:
343 return self._calculate_areas_3d()
344 else:
345 raise TypeError("Only supported for 2D and 3D point sets")