Hide keyboard shortcuts

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 

3 

4.. versionadded:: 0.18.0 

5 

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# 

13 

14import warnings 

15import numpy as np 

16import scipy 

17from . import _voronoi 

18from scipy.spatial import cKDTree 

19 

20__all__ = ['SphericalVoronoi'] 

21 

22 

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)) 

35 

36 

37class SphericalVoronoi: 

38 """ Voronoi diagrams on the surface of a sphere. 

39 

40 .. versionadded:: 0.18.0 

41 

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) 

55 

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 

69 

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`. 

77 

78 Raises 

79 ------ 

80 ValueError 

81 If there are duplicates in `points`. 

82 If the provided `radius` is not consistent with `points`. 

83 

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. 

93 

94 Empirical assessment of spherical Voronoi algorithm performance suggests 

95 quadratic time complexity (loglinear is optimal, but algorithms are more 

96 challenging to implement). 

97 

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. 

102 

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. 

106 

107 See Also 

108 -------- 

109 Voronoi : Conventional Voronoi diagrams in N dimensions. 

110 

111 Examples 

112 -------- 

113 Do some imports and take some points on a cube: 

114 

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], ]) 

121 

122 Calculate the spherical Voronoi diagram: 

123 

124 >>> radius = 1 

125 >>> center = np.array([0, 0, 0]) 

126 >>> sv = SphericalVoronoi(points, radius, center) 

127 

128 Generate plot: 

129 

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() 

165 

166 """ 

167 def __init__(self, points, radius=1, center=None, threshold=1e-06): 

168 

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) 

176 

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) 

184 

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)) 

190 

191 if cKDTree(self.points).query_pairs(threshold * self.radius): 

192 raise ValueError("Duplicate generators present.") 

193 

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.") 

198 

199 self._calc_vertices_regions() 

200 

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. 

206 

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 

233 

234 def sort_vertices_of_regions(self): 

235 """Sort indices of the vertices to be (counter-)clockwise ordered. 

236 

237 Raises 

238 ------ 

239 TypeError 

240 If the points are not three-dimensional. 

241 

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. 

247 

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) 

265 

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] 

271 

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)] 

277 

278 nbrs1 = np.array([r for region in self.regions for r in region]) 

279 

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] 

286 

287 # Normalize points and vertices. 

288 pnormalized = (self.points - self.center) / self.radius 

289 vnormalized = (self.vertices - self.center) / self.radius 

290 

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) 

297 

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] 

301 

302 # Get polygon areas using A = omega * r**2 

303 return solid_angles * self.radius**2 

304 

305 def _calculate_areas_2d(self): 

306 # Find start and end points of arcs 

307 arcs = self.points[self._simplices] - self.center 

308 

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) 

313 

314 # Get areas using A = r * theta 

315 areas = self.radius * theta 

316 

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 

323 

324 def calculate_areas(self): 

325 """Calculates the areas of the Voronoi regions. 

326 

327 For 2D point sets, the regions are circular arcs. The sum of the areas 

328 is `2 * pi * radius`. 

329 

330 For 3D point sets, the regions are spherical polygons. The sum of the 

331 areas is `4 * pi * radius**2`. 

332 

333 .. versionadded:: 1.5.0 

334 

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")