Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/scipy/spatial/_spherical_voronoi.py: 16%

79 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-12 06:31 +0000

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 numpy as np 

15import scipy 

16from . import _voronoi 

17from scipy.spatial import cKDTree 

18 

19__all__ = ['SphericalVoronoi'] 

20 

21 

22def calculate_solid_angles(R): 

23 """Calculates the solid angles of plane triangles. Implements the method of 

24 Van Oosterom and Strackee [VanOosterom]_ with some modifications. Assumes 

25 that input points have unit norm.""" 

26 # Original method uses a triple product `R1 . (R2 x R3)` for the numerator. 

27 # This is equal to the determinant of the matrix [R1 R2 R3], which can be 

28 # computed with better stability. 

29 numerator = np.linalg.det(R) 

30 denominator = 1 + (np.einsum('ij,ij->i', R[:, 0], R[:, 1]) + 

31 np.einsum('ij,ij->i', R[:, 1], R[:, 2]) + 

32 np.einsum('ij,ij->i', R[:, 2], R[:, 0])) 

33 return np.abs(2 * np.arctan2(numerator, denominator)) 

34 

35 

36class SphericalVoronoi: 

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

38 

39 .. versionadded:: 0.18.0 

40 

41 Parameters 

42 ---------- 

43 points : ndarray of floats, shape (npoints, ndim) 

44 Coordinates of points from which to construct a spherical 

45 Voronoi diagram. 

46 radius : float, optional 

47 Radius of the sphere (Default: 1) 

48 center : ndarray of floats, shape (ndim,) 

49 Center of sphere (Default: origin) 

50 threshold : float 

51 Threshold for detecting duplicate points and 

52 mismatches between points and sphere parameters. 

53 (Default: 1e-06) 

54 

55 Attributes 

56 ---------- 

57 points : double array of shape (npoints, ndim) 

58 the points in `ndim` dimensions to generate the Voronoi diagram from 

59 radius : double 

60 radius of the sphere 

61 center : double array of shape (ndim,) 

62 center of the sphere 

63 vertices : double array of shape (nvertices, ndim) 

64 Voronoi vertices corresponding to points 

65 regions : list of list of integers of shape (npoints, _ ) 

66 the n-th entry is a list consisting of the indices 

67 of the vertices belonging to the n-th point in points 

68 

69 Methods 

70 ------- 

71 calculate_areas 

72 Calculates the areas of the Voronoi regions. For 2D point sets, the 

73 regions are circular arcs. The sum of the areas is `2 * pi * radius`. 

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

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

76 

77 Raises 

78 ------ 

79 ValueError 

80 If there are duplicates in `points`. 

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

82 

83 Notes 

84 ----- 

85 The spherical Voronoi diagram algorithm proceeds as follows. The Convex 

86 Hull of the input points (generators) is calculated, and is equivalent to 

87 their Delaunay triangulation on the surface of the sphere [Caroli]_. 

88 The Convex Hull neighbour information is then used to 

89 order the Voronoi region vertices around each generator. The latter 

90 approach is substantially less sensitive to floating point issues than 

91 angle-based methods of Voronoi region vertex sorting. 

92 

93 Empirical assessment of spherical Voronoi algorithm performance suggests 

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

95 challenging to implement). 

96 

97 References 

98 ---------- 

99 .. [Caroli] Caroli et al. Robust and Efficient Delaunay triangulations of 

100 points on or close to a sphere. Research Report RR-7004, 2009. 

101 

102 .. [VanOosterom] Van Oosterom and Strackee. The solid angle of a plane 

103 triangle. IEEE Transactions on Biomedical Engineering, 

104 2, 1983, pp 125--126. 

105 

106 See Also 

107 -------- 

108 Voronoi : Conventional Voronoi diagrams in N dimensions. 

109 

110 Examples 

111 -------- 

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

113 

114 >>> import numpy as np 

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 raise ValueError('`radius` is `None`. ' 

171 'Please provide a floating point number ' 

172 '(i.e. `radius=1`).') 

173 

174 self.radius = float(radius) 

175 self.points = np.array(points).astype(np.double) 

176 self._dim = self.points.shape[1] 

177 if center is None: 

178 self.center = np.zeros(self._dim) 

179 else: 

180 self.center = np.array(center, dtype=float) 

181 

182 # test degenerate input 

183 self._rank = np.linalg.matrix_rank(self.points - self.points[0], 

184 tol=threshold * self.radius) 

185 if self._rank < self._dim: 

186 raise ValueError("Rank of input points must be at least {0}".format(self._dim)) 

187 

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

189 raise ValueError("Duplicate generators present.") 

190 

191 radii = np.linalg.norm(self.points - self.center, axis=1) 

192 max_discrepancy = np.abs(radii - self.radius).max() 

193 if max_discrepancy >= threshold * self.radius: 

194 raise ValueError("Radius inconsistent with generators.") 

195 

196 self._calc_vertices_regions() 

197 

198 def _calc_vertices_regions(self): 

199 """ 

200 Calculates the Voronoi vertices and regions of the generators stored 

201 in self.points. The vertices will be stored in self.vertices and the 

202 regions in self.regions. 

203 

204 This algorithm was discussed at PyData London 2015 by 

205 Tyler Reddy, Ross Hemsley and Nikolai Nowaczyk 

206 """ 

207 # get Convex Hull 

208 conv = scipy.spatial.ConvexHull(self.points) 

209 # get circumcenters of Convex Hull triangles from facet equations 

210 # for 3D input circumcenters will have shape: (2N-4, 3) 

211 self.vertices = self.radius * conv.equations[:, :-1] + self.center 

212 self._simplices = conv.simplices 

213 # calculate regions from triangulation 

214 # for 3D input simplex_indices will have shape: (2N-4,) 

215 simplex_indices = np.arange(len(self._simplices)) 

216 # for 3D input tri_indices will have shape: (6N-12,) 

217 tri_indices = np.column_stack([simplex_indices] * self._dim).ravel() 

218 # for 3D input point_indices will have shape: (6N-12,) 

219 point_indices = self._simplices.ravel() 

220 # for 3D input indices will have shape: (6N-12,) 

221 indices = np.argsort(point_indices, kind='mergesort') 

222 # for 3D input flattened_groups will have shape: (6N-12,) 

223 flattened_groups = tri_indices[indices].astype(np.intp) 

224 # intervals will have shape: (N+1,) 

225 intervals = np.cumsum(np.bincount(point_indices + 1)) 

226 # split flattened groups to get nested list of unsorted regions 

227 groups = [list(flattened_groups[intervals[i]:intervals[i + 1]]) 

228 for i in range(len(intervals) - 1)] 

229 self.regions = groups 

230 

231 def sort_vertices_of_regions(self): 

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

233 

234 Raises 

235 ------ 

236 TypeError 

237 If the points are not three-dimensional. 

238 

239 Notes 

240 ----- 

241 For each region in regions, it sorts the indices of the Voronoi 

242 vertices such that the resulting points are in a clockwise or 

243 counterclockwise order around the generator point. 

244 

245 This is done as follows: Recall that the n-th region in regions 

246 surrounds the n-th generator in points and that the k-th 

247 Voronoi vertex in vertices is the circumcenter of the k-th triangle 

248 in self._simplices. For each region n, we choose the first triangle 

249 (=Voronoi vertex) in self._simplices and a vertex of that triangle 

250 not equal to the center n. These determine a unique neighbor of that 

251 triangle, which is then chosen as the second triangle. The second 

252 triangle will have a unique vertex not equal to the current vertex or 

253 the center. This determines a unique neighbor of the second triangle, 

254 which is then chosen as the third triangle and so forth. We proceed 

255 through all the triangles (=Voronoi vertices) belonging to the 

256 generator in points and obtain a sorted version of the vertices 

257 of its surrounding region. 

258 """ 

259 if self._dim != 3: 

260 raise TypeError("Only supported for three-dimensional point sets") 

261 _voronoi.sort_vertices_of_regions(self._simplices, self.regions) 

262 

263 def _calculate_areas_3d(self): 

264 self.sort_vertices_of_regions() 

265 sizes = [len(region) for region in self.regions] 

266 csizes = np.cumsum(sizes) 

267 num_regions = csizes[-1] 

268 

269 # We create a set of triangles consisting of one point and two Voronoi 

270 # vertices. The vertices of each triangle are adjacent in the sorted 

271 # regions list. 

272 point_indices = [i for i, size in enumerate(sizes) 

273 for j in range(size)] 

274 

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

276 

277 # The calculation of nbrs2 is a vectorized version of: 

278 # np.array([r for region in self.regions for r in np.roll(region, 1)]) 

279 nbrs2 = np.roll(nbrs1, 1) 

280 indices = np.roll(csizes, 1) 

281 indices[0] = 0 

282 nbrs2[indices] = nbrs1[csizes - 1] 

283 

284 # Normalize points and vertices. 

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

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

287 

288 # Create the complete set of triangles and calculate their solid angles 

289 triangles = np.hstack([pnormalized[point_indices], 

290 vnormalized[nbrs1], 

291 vnormalized[nbrs2] 

292 ]).reshape((num_regions, 3, 3)) 

293 triangle_solid_angles = calculate_solid_angles(triangles) 

294 

295 # Sum the solid angles of the triangles in each region 

296 solid_angles = np.cumsum(triangle_solid_angles)[csizes - 1] 

297 solid_angles[1:] -= solid_angles[:-1] 

298 

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

300 return solid_angles * self.radius**2 

301 

302 def _calculate_areas_2d(self): 

303 # Find start and end points of arcs 

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

305 

306 # Calculate the angle subtended by arcs 

307 cosine = np.einsum('ij,ij->i', arcs[:, 0], arcs[:, 1]) 

308 sine = np.abs(np.linalg.det(arcs)) 

309 theta = np.arctan2(sine, cosine) 

310 

311 # Get areas using A = r * theta 

312 areas = self.radius * theta 

313 

314 # Correct arcs which go the wrong way (single-hemisphere inputs) 

315 signs = np.sign(np.einsum('ij,ij->i', arcs[:, 0], 

316 self.vertices - self.center)) 

317 indices = np.where(signs < 0) 

318 areas[indices] = 2 * np.pi * self.radius - areas[indices] 

319 return areas 

320 

321 def calculate_areas(self): 

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

323 

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

325 is `2 * pi * radius`. 

326 

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

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

329 

330 .. versionadded:: 1.5.0 

331 

332 Returns 

333 ------- 

334 areas : double array of shape (npoints,) 

335 The areas of the Voronoi regions. 

336 """ 

337 if self._dim == 2: 

338 return self._calculate_areas_2d() 

339 elif self._dim == 3: 

340 return self._calculate_areas_3d() 

341 else: 

342 raise TypeError("Only supported for 2D and 3D point sets")