Coverage for src/thunderlab/voronoi.py: 98%

460 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-20 15:01 +0000

1"""Analyze Voronoi diagrams based on scipy.spatial. 

2 

3## Classes 

4 

5- `class Voronoi`: Compute and analyse Voronoi diagrams. 

6""" 

7 

8 

9import numpy as np 

10import scipy.spatial as sp 

11try: 

12 import matplotlib.pyplot as plt 

13except ImportError: 

14 pass 

15 

16 

17def cross2d(x, y): 

18 # numpy version 2.5 up require 3-dim vectors for np.cross()... 

19 return x[..., 0] * y[..., 1] - x[..., 1] * y[..., 0] 

20 

21 

22class Voronoi(object): 

23 """ 

24 Parameters 

25 ---------- 

26 points: ndarray of floats, shape (npoints, 2) 

27 List of point coordinates. 

28 radius: float or None 

29 Radius for computing far points of infinite ridges. 

30 If None twice the maximum extent of the input points is used. 

31 qhull_options: string or None 

32 Options to be passed on to QHull. From the manual: 

33  

34 - Qbb - scale last coordinate to [0,m] for Delaunay triangulations 

35 - Qc - keep coplanar points with nearest facet  

36 - Qx - exact pre-merges (skips coplanar and anglomaniacs facets) 

37 - Qz - add point-at-infinity to Delaunay triangulation 

38 - QJn - randomly joggle input in range [-n,n] 

39 - Qs - search all points for the initial simplex 

40 - Qz - add point-at-infinity to Voronoi diagram 

41 - QGn - Voronoi vertices if visible from point n, -n if not 

42 - QVn - Voronoi vertices for input point n, -n if not 

43  

44 Default is: "Qbb Qc Qz Qx" for ndim > 4 and "Qbb Qc Qz" otherwise. 

45  

46 Input points 

47 ------------ 

48 The points from which the Voronoi diagram is constructed. 

49  

50 - `ndim`: int  

51 The dimension of the input points, i.e. number of coordinates. 

52 - `npoints`: int  

53 The number of input points. 

54 - `points`: 2-d ndarray  

55 List of input point coordinates. 

56 - `center`: ndarray of floats  

57 Center of mass of the input points (i.e. mean coordinates). 

58 - `min_bound`: ndarray of floats  

59 Lower left corner of the bounding-box of the input points. 

60 - `max_bound`: ndarray of floats  

61 Upper right corner of the bounding-box of the input points. 

62  

63 Distances between input points 

64 ------------------------------ 

65 - `ridge_points`: 2-d ndarray of ints  

66 List of pairs of indices to `points` enclosing a Voronoi ridge. 

67 - `ridge_distances`: ndarray of floats  

68 For each ridge in `ridge_points` the distance between the enclosing points. 

69 - `neighbor_points`: list of ndarrays of ints  

70 For each point in `points` a list of indices of the Voronoi-cell's neighboring points. 

71 - `neighbor_distances`: list of ndarrays of floats  

72 For each point in `points` a list of distances to the Voronoi-cell's neighboring points, 

73 matching `neighbor_points`. 

74 - `nearest_points`: list of ints  

75 For each point in `points` the index of its nearest neighbor. 

76 - `nearest_distances`: ndarray of floats  

77 For each point in `points` the distance to its nearest neighbor. 

78 - `mean_nearest_distance`: float  

79 Unbiased estimate of the mean nearest-neighbor distance. This is 

80 the average nearest-neighbor distance corrected by one s.e.m, 

81 since points close to the border of the region have too large 

82 nearest-neighbor distances. 

83  

84 Voronoi diagram 

85 --------------- 

86 - `vertices`: 2-d ndarray of floats  

87 List of vertex coordinates enclosing the Voronoi regions. 

88 - `regions`: list of list of ints  

89 List of lists of indices to vertices in `vertices` making up each Voronoi region. 

90 - `ridge_vertices`: list of list of ints  

91 List of lists of indices to `vertices` making up a Voronoi ridge. 

92 The pairs of `points` to poth sides of the ridge are listed in `ridge_points`. 

93 - `ridge_lengths()`: Length of Voronoi ridges between nearest neighbors. 

94 - `areas()`: The areas of the Voronoi regions for each input point in `points`. 

95 - `point_types()`: The type of Voronoi area (infinite, finite, inside) for each input point. 

96 - `inside_vertices`: list of ints  

97 Indices of `vertices` that are inside the convex hull of the input points. 

98 - `infinite_vertices`: list of ndarrays of floats  

99 For each ridge in `ridge_vertices` the coordinates of the far-point, if it is an infinite ridge. 

100 - `infinite_regions`: list of list of ints  

101 List of Voronoi regions with infinite ridges. If positive, the indices are indices to `vertices`. 

102 If negative they are indices into `infinite_vertices` (`-index-1`). 

103  

104 Convex hull 

105 ----------- 

106 - `hull_points`: list of ints  

107 List of indices of the points in `points` making up the convex hull. 

108 - `hull_center`: ndarray of floats  

109 Center of mass of the points making up the convex hull. 

110 - `inside_vertices`: ndarray of boolean  

111 Indicates for each vertex in `vertices` whether it is inside the convex hull. 

112 - `in_hull()`: Test if points are within the convex hull of the input points. 

113 - `hull_area()`: The area contained in the convex hull. 

114  

115 Outer hull 

116 ---------- 

117 The outer hull is constructed from the convex hull by expanding each 

118 point of the convex hull away from the center of mass of the convex 

119 hull such that randomly placing the same number of points in this 

120 area results on average in the same mean nearest-neighbor 

121 distance. This enlarged hull is needed for bootstrapping. Note that 

122 in some cases the outer hull can be *smaller* than the convex hull. 

123  

124 - `outer_hull_points`: 2-d ndarray of floats  

125 Array of coordinates of the points of the outer hull. 

126 - `outer_min_bound`: ndarray of floats  

127 Lower left corner of the bounding-box of the outer hull. 

128 - `outer_max_bound`: ndarray of floats  

129 Upper right corner of the bounding-box of the outer hull. 

130 - `in_outer_hull()`: Test if points are within the outer hull. 

131 - `outer_hull_area()`: The area contained in the outer hull. 

132  

133 Bootstrap Voronoi diagrams 

134 -------------------------- 

135 - `random_points()`: Generate random points within the area defined by the input points. 

136 

137 Plotting the Voronoi diagram 

138 ---------------------------- 

139 - `plot_points()`: Plot and optionally annotate the input points of the Voronoi diagram. 

140 - `plot_center()`: Plot the center of mass of the input points. 

141 - `plot_vertices()`: Plot and optionally annotate the vertices of the Voronoi diagram. 

142 - `plot_distances()`: Plot lines connecting the neighbors in the Voronoi diagram. 

143 - `plot_ridges()`: Plot the finite ridges of the Voronoi diagram. 

144 - `plot_infinite_ridges(): Plot the infinite ridges of the Voronoi diagram. 

145 - `fill_regions()`: Fill each finite region of the Voronoi diagram with a color. 

146 - `fill_infinite_regions()`: Fill each infinite region of the Voronoi diagram with a color. 

147  

148 Plotting the convex hull 

149 ------------------------ 

150 - `plot_hull()`: Plot the convex hull line containing the input points. 

151 - `fill_hull()`: Fill the convex hull. 

152 - `plot_hull_center()`: Plot the center of mass of the convex hull. 

153 - `plot_outer_hull()`: Plot the outer hull edge. 

154 - `fill_outer_hull()`: Fill the outer hull. 

155  

156 Usage 

157 ----- 

158 Generate 20 random points in 2-D: 

159 ``` 

160 import numpy as np 

161 points = np.random.rand(20, 2) 

162 ``` 

163  

164 Calculate the Voronoi diagram: 

165 ``` 

166 from thunderlab.voronoi import Voronoi 

167 vor = Voronoi(points) 

168 ``` 

169  

170 Retrieve nearest-neighbor distances and compute Voronoi areas: 

171 ``` 

172 dists = vor.nearest_distances 

173 areas = vor.areas() 

174 ``` 

175  

176 Generate random points drawn from a Poisson process with the same 

177 intensity and mean nearest neighbor distance as the data: 

178 ``` 

179 new_points = vor.random_points(poisson=True, mode='outer') 

180 ``` 

181  

182 Plot Voronoi regions, distances, and input points: 

183 ``` 

184 import matplotlib.pyplot as plt 

185 vor.fill_regions(colors=['red', 'green', 'blue', 'orange', 'cyan'], alpha=0.3) 

186 vor.plot_distances(color='red') 

187 vor.plot_points(text='p%d', c='c', s=100) 

188 ``` 

189 """ 

190 

191 def __init__(self, points, radius=None, qhull_options=None): 

192 self.vor = sp.Voronoi(points, furthest_site=False, incremental=False, 

193 qhull_options=qhull_options) 

194 if self.vor.ndim != 2: 

195 raise ValueError("Only 2D input points are supported.") 

196 self.hull = sp.Delaunay(points, furthest_site=False, incremental=False, 

197 qhull_options=qhull_options) 

198 self._compute_distances() 

199 self._compute_infinite_vertices() 

200 self._compute_hull(qhull_options) 

201 self.ndim = self.vor.ndim 

202 self.npoints = self.vor.npoints 

203 self.points = self.vor.points 

204 self.vertices = self.vor.vertices 

205 self.regions = self.vor.regions 

206 self.ridge_points = self.vor.ridge_points 

207 self.ridge_vertices = self.vor.ridge_vertices 

208 self.min_bound = self.vor.min_bound 

209 self.max_bound = self.vor.max_bound 

210 self.center = np.mean(self.points, axis=0) 

211 

212 def _compute_distances(self): 

213 """Compute distances between points. 

214 """ 

215 # For each ridge the distance of the points enclosing the ridge: 

216 p1 = self.vor.points[self.vor.ridge_points[:,0]] 

217 p2 = self.vor.points[self.vor.ridge_points[:,1]] 

218 self.ridge_distances = sp.minkowski_distance(p1, p2) 

219 

220 # For each point all its Voronoi distances: 

221 self.neighbor_points = [[] for k in range(len(self.vor.points))] 

222 self.neighbor_distances = [[] for k in range(len(self.vor.points))] 

223 for dist, points in zip(self.ridge_distances, self.vor.ridge_points): 

224 self.neighbor_points[points[0]].append(points[1]) 

225 self.neighbor_points[points[1]].append(points[0]) 

226 self.neighbor_distances[points[0]].append(dist) 

227 self.neighbor_distances[points[1]].append(dist) 

228 for k in range(len(self.neighbor_points)): 

229 inx = np.argsort(self.neighbor_distances[k]) 

230 self.neighbor_points[k] = np.array(self.neighbor_points[k])[inx] 

231 self.neighbor_distances[k] = np.array(self.neighbor_distances[k])[inx] 

232 

233 # For each point the distance to its nearest neighbor: 

234 self.nearest_points = [] 

235 self.nearest_distances = np.zeros(len(self.neighbor_distances)) 

236 for k in range(len(self.neighbor_distances)): 

237 self.nearest_points.append(self.neighbor_points[k][0]) 

238 self.nearest_distances[k] = self.neighbor_distances[k][0] 

239 self.mean_nearest_distance = np.mean(self.nearest_distances) 

240 self.mean_nearest_distance *= 1.0-2.0*np.sqrt(1.0/np.pi-0.25)/np.sqrt(self.vor.npoints) 

241 

242 def _compute_infinite_vertices(self, radius=None): 

243 """Compute far points of infinite ridges. 

244 

245 Parameters 

246 ---------- 

247 radius: float or None 

248 Radius for computing far points of infinite ridges. 

249 If None twice the maximum extent of the input points is used. 

250 

251 Note 

252 ---- 

253 Code inspired by http://stackoverflow.com/questions/20515554/colorize-voronoi-diagram 

254 """ 

255 # For each ridge, compute far point: 

256 center = self.vor.points.mean(axis=0) 

257 if radius is None: 

258 radius = np.max(np.ptp(2.0*self.vor.points, axis=0)) 

259 self.infinite_vertices = [] 

260 for points, vertices in zip(self.vor.ridge_points, self.vor.ridge_vertices): 

261 vertices = np.asarray(vertices) 

262 if np.all(vertices >= 0): 

263 self.infinite_vertices.append(np.array([])) 

264 else: 

265 i = vertices[vertices >= 0][0] # finite end Voronoi vertex 

266 t = self.vor.points[points[1]] - self.vor.points[points[0]] # tangent 

267 t /= np.linalg.norm(t) 

268 n = np.array([-t[1], t[0]]) # normal 

269 midpoint = self.vor.points[points].mean(axis=0) 

270 direction = np.sign(np.dot(midpoint - center, n)) * n 

271 far_point = self.vor.vertices[i] + direction * radius 

272 self.infinite_vertices.append(far_point) 

273 # Assemble list of infinite regions: 

274 # Indices to self.infinite_vertices are negative minus one. 

275 self.infinite_regions = [] 

276 for rvertices in self.vor.regions: 

277 if -1 in rvertices: 

278 new_rvertices = [] 

279 prev_vertex = rvertices[-1] 

280 # find index of data point enclosed by the region: 

281 ridge_points = [] 

282 for p, v in zip(self.vor.ridge_points, self.vor.ridge_vertices): 

283 if not -1 in v and set(v) <= set(rvertices): 

284 ridge_points.extend(p) 

285 region_point = None 

286 for rp in ridge_points: 

287 if ridge_points.count(rp) > 1: 

288 region_point = rp 

289 break 

290 # fill in far points for each region: 

291 for v_inx, v in enumerate(rvertices): 

292 if v >= 0: 

293 new_rvertices.append(v) 

294 else: 

295 for v1_inx, (points, vertices) in enumerate(zip(self.vor.ridge_points, 

296 self.vor.ridge_vertices)): 

297 if prev_vertex in vertices and -1 in vertices and \ 

298 (region_point is None or region_point in points): 

299 new_rvertices.append(-v1_inx-1) 

300 break 

301 next_vertex = rvertices[0] 

302 if v_inx+1 < len(rvertices): 

303 next_vertex = rvertices[v_inx+1] 

304 for v2_inx, (points, vertices) in enumerate(zip(self.vor.ridge_points, 

305 self.vor.ridge_vertices)): 

306 if next_vertex in vertices and -1 in vertices and \ 

307 (region_point is None or region_point in points) and \ 

308 new_rvertices[-1] != -v2_inx-1: 

309 new_rvertices.append(-v2_inx-1) 

310 break 

311 prev_vertex = v 

312 self.infinite_regions.append(new_rvertices) 

313 

314 def _flatten_simplices(self, simplices): 

315 """Transforms list of simplex indices to list of vertex indices. 

316 

317 In particular, transforms the Delaunay.convex_hull to a list of points 

318 of the hull that can then be easily plotted. 

319 

320 Parameters 

321 ---------- 

322 simplices: 2-D array of ints 

323 List of pairs of indices of points forming each ridge of a polygon. 

324 

325 Returns 

326 ------- 

327 indices: list of ints 

328 Indices of vertices of the polygon. 

329 """ 

330 if len(simplices) == 0: 

331 return [] 

332 indices = list(simplices[0]) 

333 simplices = np.delete(simplices, 0, 0) 

334 while len(simplices) > 0: 

335 for i, s in enumerate(simplices): 

336 if indices[-1] in s: 

337 if s[0] == indices[-1]: 

338 indices.append(s[1]) 

339 else: 

340 indices.append(s[0]) 

341 simplices = np.delete(simplices, i, 0) 

342 break 

343 return indices 

344 

345 def _compute_hull(self, qhull_options): 

346 """Compute properties of the convex hull and set up the outer hull. 

347 """ 

348 self.inside_vertices = self.in_hull(self.vor.vertices) 

349 self.hull_points = self._flatten_simplices(self.hull.convex_hull) 

350 self.hull_center = np.mean(self.hull.points[self.hull_points], axis=0) 

351 

352 # enlarge hull: 

353 # estimate area needed for a poisson process with same mean distance: 

354 new_area = 4.0 * self.mean_nearest_distance**2 * self.vor.npoints 

355 fac = np.sqrt(new_area/self.hull_area()) 

356 self.outer_hull_points = np.zeros((len(self.hull_points), self.vor.ndim)) 

357 for k, point in enumerate(self.hull.points[self.hull_points]): 

358 point -= self.hull_center 

359 point *= fac 

360 point += self.hull_center 

361 self.outer_hull_points[k] = point 

362 # compute outer hull: 

363 self.outer_hull = sp.Delaunay(self.outer_hull_points, 

364 furthest_site=False, incremental=False, 

365 qhull_options=qhull_options) 

366 self.outer_min_bound = np.min(self.outer_hull_points, axis=0) 

367 self.outer_max_bound = np.max(self.outer_hull_points, axis=0) 

368 

369 def in_hull(self, p): 

370 """Test if points `p` are within convex hull of the input points. 

371 

372 Parameters 

373 ---------- 

374 p: 2-d ndarray 

375 Array of points to be tested. 

376 

377 Returns 

378 ------- 

379 inside: ndarray of booleans 

380 For each point in `p` whether it is inside the hull. 

381 """ 

382 inside = self.hull.find_simplex(p) >= 0 

383 return inside 

384 

385 def in_outer_hull(self, p): 

386 """Test if points `p` are within the outer hull. 

387 

388 Parameters 

389 ---------- 

390 p: 2-d ndarray 

391 Array of points to be tested. 

392 

393 Returns 

394 ------- 

395 inside: ndarray of booleans 

396 For each point in `p` whether it is inside the outer hull. 

397 """ 

398 inside = self.outer_hull.find_simplex(p) >= 0 

399 return inside 

400 

401 def point_types(self): 

402 """The type of the Voronoi regions for each input point. 

403 

404 Returns 

405 ------- 

406 points: ndarray of ints 

407 Indicates the type of Voronoi region associated 

408 with each point in `points`: 

409 

410 - 2: finite region with all vertices inside hull, 

411 - 1: finite region, 

412 - 0: infinite region. 

413 """ 

414 points = np.zeros(len(self.vor.points), dtype=int) + 2 

415 for i in range(len(self.vor.points)): 

416 for j, rp in enumerate(self.vor.ridge_points): 

417 if i in rp: 

418 if not np.all(self.inside_vertices[self.vor.ridge_vertices[j]]) and\ 

419 points[i] > 0: 

420 points[i] = 1 

421 if not np.all(np.array(self.vor.ridge_vertices[j]) >= 0) and\ 

422 points[i] > -1: 

423 points[i] = 0 

424 return points 

425 

426 def ridge_lengths(self): 

427 """Length of Voronoi ridges between nearest neighbors. 

428 

429 May be used, for example, as a weigth for `ridge_distances`. 

430 

431 Returns 

432 ------- 

433 distances: ndarray of floats 

434 The length of each ridge in `ridge_vertices`. 

435 np.inf if one vertex is at infinity. 

436 """ 

437 ridges = np.zeros(len(self.vor.ridge_vertices)) 

438 for k, p in enumerate(self.vor.ridge_vertices): 

439 if np.all(np.array(p)>=0): 

440 p1 = self.vor.vertices[p[0]] 

441 p2 = self.vor.vertices[p[1]] 

442 ridges[k] = sp.minkowski_distance(p1, p2) 

443 else: 

444 ridges[k] = np.inf 

445 return ridges 

446 

447 def ridge_areas(self): 

448 """For each ridge the triangular area of the Voronoi region 

449 spanned by the center point and the ridge. 

450 

451 Returns 

452 ------- 

453 areas: ndarray of floats 

454 For each ridge in `ridge_points` or `ridge_vertices` 

455 its corresponding triangular area. 

456 np.inf for infinite ridges. 

457 """ 

458 ridges = self.ridge_lengths() 

459 heights = 0.5*self.ridge_distances 

460 # area of a triangle: 

461 areas = 0.5*ridges*heights 

462 return areas 

463 

464 def areas(self, mode='finite'): 

465 """The areas of the Voronoi regions for each input point. 

466 

467 Parameters 

468 ---------- 

469 mode: string 

470 - 'inside': Calculate area of finite Voronoi regions 

471 whose vertices are all inside the hull, 

472 set all other to zero. 

473 - 'finite_inside': Calculate area of all Voronoi regions. 

474 Consider only areas of finite ridges 

475 whose vertices are all inside the hull. 

476 - 'full': Calculate area of finite Voronoi regions only, 

477 set all other to zero. 

478 - 'finite': Calculate area of all Voronoi regions. 

479 From infinite regions 

480 only areas contributed by finite ridges are considered. 

481 

482 Returns 

483 ------- 

484 areas: array of floats 

485 For each point in `points` its corresponding area. 

486 """ 

487 ridge_areas = self.ridge_areas() 

488 areas = np.zeros(len(self.vor.points)) 

489 if mode == 'inside': 

490 for i in range(len(self.vor.points)): 

491 a = 0.0 

492 for j, rp in enumerate(self.vor.ridge_points): 

493 if i in rp: 

494 if ridge_areas[j] != np.inf and \ 

495 np.all(self.inside_vertices[self.vor.ridge_vertices[j]]): 

496 a += ridge_areas[j] 

497 else: 

498 a = 0.0 

499 break 

500 areas[i] = a 

501 elif mode == 'full': 

502 for i in range(len(self.vor.points)): 

503 a = 0.0 

504 for j, rp in enumerate(self.vor.ridge_points): 

505 if i in rp: 

506 if ridge_areas[j] != np.inf: 

507 a += ridge_areas[j] 

508 else: 

509 a = 0.0 

510 break 

511 areas[i] = a 

512 elif mode == 'finite_inside': 

513 for i in range(len(self.vor.points)): 

514 a = 0.0 

515 for j, rp in enumerate(self.vor.ridge_points): 

516 if i in rp and ridge_areas[j] != np.inf and \ 

517 np.all(self.inside_vertices[self.vor.ridge_vertices[j]]): 

518 a += ridge_areas[j] 

519 areas[i] = a 

520 elif mode == 'finite': 

521 for i in range(len(self.vor.points)): 

522 a = 0.0 

523 for j, rp in enumerate(self.vor.ridge_points): 

524 if i in rp and ridge_areas[j] != np.inf: 

525 a += ridge_areas[j] 

526 areas[i] = a 

527 else: 

528 print('') 

529 print(f'Voronoi.areas(): unknown value "{mode}" for the mode parameter:') 

530 print('Use one of the following values:') 

531 print(' inside: Finite Voronoi regions whose vertices are all inside the hull.') 

532 print(' finite_inside: Use all areas corresponding to finite ridges whose vertices are all inside the hull.') 

533 print(' full: Finite Voronoi regions only.') 

534 print(' finite: Use all areas corresponding to finite ridges.') 

535 print('') 

536 return areas 

537 

538 def hull_area(self): 

539 """The area of the convex hull of the input points. 

540  

541 Returns 

542 ------- 

543 area: float 

544 The area of the convex hull. 

545 """ 

546 # two sides of the simplex triangles: 

547 ab = self.hull.points[self.hull.simplices[:,0],:] - self.hull.points[self.hull.simplices[:,1],:] 

548 cb = self.hull.points[self.hull.simplices[:,2],:] - self.hull.points[self.hull.simplices[:,1],:] 

549 # area of each simplex is half of the absolute value of the cross product: 

550 area = 0.5*np.sum(np.abs(cross2d(ab, cb))) 

551 return area 

552 

553 def outer_hull_area(self): 

554 """The area of the outer hull. 

555  

556 Returns 

557 ------- 

558 area: float 

559 The area of the outer hull. 

560 """ 

561 # two sides of the simplex triangles: 

562 ab = self.outer_hull.points[self.outer_hull.simplices[:,0],:] - self.outer_hull.points[self.outer_hull.simplices[:,1],:] 

563 cb = self.outer_hull.points[self.outer_hull.simplices[:,2],:] - self.outer_hull.points[self.outer_hull.simplices[:,1],:] 

564 # area of each simplex is half of the absolute value of the cross product: 

565 area = 0.5*np.sum(np.abs(cross2d(ab, cb))) 

566 return area 

567 

568 def random_points(self, n=None, poisson=True, mode='outer'): 

569 """Generate random points. 

570 

571 Parameters 

572 ---------- 

573 n: int or None 

574 Number of random points to be generated. 

575 If None `n` is set to the number of input points. 

576 poisson: boolean 

577 If `True` then draw the number of points from a Poisson distribution 

578 with mean number of points given by `n`. 

579 mode: string 

580 - 'bbox': place points randomly in rectangular bounding box 

581 of the Voronoi diagram. 

582 - 'hull': place points randomly within convex hull of input data. 

583 - 'outer' place points randomly within outer hull. 

584 The mean nearest-neighbor distance 

585 between the generated points will be close 

586 to the observed one. 

587 

588 Returns 

589 ------- 

590 points: 2-D array 

591 List of randomly generated points. 

592 """ 

593 # number of points: 

594 if n is None: 

595 n = self.npoints 

596 nn = n 

597 if poisson: 

598 nn = np.random.poisson(n) 

599 m = nn//2 

600 if m < 5: 

601 m = 5 

602 # get bounding box: 

603 if mode == 'outer': 

604 min_bound = self.outer_min_bound 

605 max_bound = self.outer_max_bound 

606 else: 

607 min_bound = self.min_bound 

608 max_bound = self.max_bound 

609 delta = np.max(max_bound - min_bound) 

610 points = np.zeros((0, self.ndim)) 

611 while len(points) < nn: 

612 # random points within bounding box: 

613 newpoints = np.random.rand(m, self.ndim) 

614 newpoints *= delta 

615 newpoints += min_bound 

616 if mode == 'outer': 

617 # only take the ones within outer hull: 

618 inside = self.in_outer_hull(newpoints) 

619 points = np.vstack((points, newpoints[inside])) 

620 elif mode == 'hull': 

621 # only take the ones within hull: 

622 inside = self.in_hull(newpoints) 

623 points = np.vstack((points, newpoints[inside])) 

624 elif mode == 'bbox': 

625 points = np.vstack((points, newpoints[np.all(newpoints<max_bound, axis=1),:])) 

626 else: 

627 print('') 

628 print(f'Voronoi.random_points(): unknown value "{mode}" for the mode parameter:') 

629 print('Use one of the following values:') 

630 print(' bbox: Place points within rectangular bounding box.') 

631 print(' hull: Place points inside the convex hull.') 

632 print(' outer: Place points inside the outer hull.') 

633 print('') 

634 return 

635 return points[:nn] 

636 

637 def plot_points(self, ax, text=None, text_offs=(0, 0.05), 

638 text_align='center', **kwargs): 

639 """Plot and optionally annotate the input points of the Voronoi diagram. 

640 

641 Parameters 

642 ---------- 

643 ax: matplotlib.Axes 

644 The axes to be used for plotting. 

645 text: string or None 

646 If not None the string that is placed at each point. 

647 A '%d' is replaced by the index of the point. 

648 text_offset: tuple of numbers 

649 The offset of the point labels. 

650 text_align: string 

651 The horizontal alignment of the point labels. 

652 **kwargs: dict 

653 Key-word arguments that are passed on to the matplotlib.scatter() function. 

654 """ 

655 ax.scatter(self.points[:,0], self.points[:,1], **kwargs) 

656 if text is not None: 

657 for i, p in enumerate(self.points): 

658 s = text 

659 if '%' in text: 

660 s = text % i 

661 ax.text(p[0]+text_offs[0], p[1]+text_offs[1], s, ha=text_align) 

662 

663 def plot_center(self, ax, **kwargs): 

664 """Plot the center of mass of the input points. 

665 

666 Parameters 

667 ---------- 

668 ax: matplotlib.Axes 

669 The axes to be used for plotting. 

670 **kwargs: dict 

671 Key-word arguments that are passed on to the `matplotlib.plot()` function. 

672 """ 

673 ax.plot(self.center[0], self.center[1], 'o', **kwargs) 

674 

675 def plot_vertices(self, ax, text=None, text_offs=(0, 0.05), 

676 text_align='center', **kwargs): 

677 """Plot and optionally annotate the vertices of the Voronoi diagram. 

678 

679 Parameters 

680 ---------- 

681 ax: matplotlib.Axes 

682 The axes to be used for plotting. 

683 text: string or None 

684 If not None the string that is placed at each vertex. 

685 A '%d' is replaced by the index of the vertex. 

686 text_offset: tuple of numbers 

687 The offset of the vertex labels. 

688 text_align: string 

689 The horizontal alignment of the vertex labels. 

690 **kwargs: dict 

691 Key-word arguments that are passed on to the matplotlib.scatter() function. 

692 """ 

693 ax.scatter(self.vertices[:,0], self.vertices[:,1], **kwargs) 

694 if text is not None: 

695 for i, p in enumerate(self.vertices): 

696 s = text 

697 if '%' in text: 

698 s = text % i 

699 ax.text(p[0]+text_offs[0], p[1]+text_offs[1], s, ha=text_align) 

700 

701 def plot_distances(self, ax, **kwargs): 

702 """Plot lines connecting the nearest neighbors in the Voronoi diagram. 

703 

704 Parameters 

705 ---------- 

706 ax: matplotlib.Axes 

707 The axes to be used for plotting. 

708 **kwargs: dict 

709 Key-word arguments that are passed on to the matplotlib.plot() function. 

710 """ 

711 for i, p in enumerate(self.ridge_points): 

712 ax.plot(self.points[p, 0], self.points[p, 1], **kwargs) 

713 

714 def plot_ridges(self, ax, **kwargs): 

715 """Plot the finite ridges of the Voronoi diagram. 

716 

717 Parameters 

718 ---------- 

719 ax: matplotlib.Axes or None 

720 The axes to be used for plotting. 

721 **kwargs: dict 

722 Key-word arguments that are passed on to the matplotlib.plot() function. 

723 """ 

724 for i, p in enumerate(self.ridge_vertices): 

725 if np.all(np.array(p)>=0): 

726 ax.plot(self.vertices[p, 0], self.vertices[p, 1], **kwargs) 

727 

728 def plot_infinite_ridges(self, ax, **kwargs): 

729 """Plot the infinite ridges of the Voronoi diagram. 

730 

731 Parameters 

732 ---------- 

733 ax: matplotlib.Axes 

734 The axes to be used for plotting. 

735 **kwargs: dict 

736 Key-word arguments that are passed on to the matplotlib.plot() function. 

737 """ 

738 for far_point, vertices in zip(self.infinite_vertices, self.vor.ridge_vertices): 

739 vertices = np.asarray(vertices) 

740 if not np.all(vertices >= 0): 

741 i = vertices[vertices >= 0][0] # finite end Voronoi vertex 

742 ax.plot([self.vor.vertices[i][0], far_point[0]], 

743 [self.vor.vertices[i][1], far_point[1]], **kwargs) 

744 

745 def fill_regions(self, ax, inside=None, colors=None, **kwargs): 

746 """Fill each finite region of the Voronoi diagram with a color. 

747 

748 Parameters 

749 ---------- 

750 ax: matplotlib.Axes 

751 The axes to be used for plotting. 

752 inside: boolean or None 

753 True: plot only finite regions with all vertices inside the hull 

754 False: plot only finite regions with at least one vertex outside the hull 

755 None: plot all finite regions 

756 colors: list of colors or None 

757 If not None then these colors are used in turn to fill the regions. 

758 **kwargs: dict 

759 Key-word arguments that are passed on to the matplotlib.fill() function. 

760 """ 

761 c = 0 

762 for region in self.regions: 

763 if not -1 in region: 

764 polygon = self.vertices[region] 

765 if len(polygon) > 0: 

766 inside_hull = self.in_hull(polygon) 

767 if inside is None or (inside and all(inside_hull)) or (not inside and any(inside_hull)): 

768 c += 1 

769 if colors is None: 

770 ax.fill(polygon[:, 0], polygon[:, 1], lw=0, **kwargs) 

771 else: 

772 ax.fill(polygon[:, 0], polygon[:, 1], 

773 color=colors[c % len(colors)], lw=0, **kwargs) 

774 

775 def fill_infinite_regions(self, ax, colors=None, **kwargs): 

776 """Fill each infinite region of the Voronoi diagram with a color. 

777 

778 Parameters 

779 ---------- 

780 ax: matplotlib.Axes 

781 The axes to be used for plotting. 

782 colors: list of colors or None 

783 If not None then these colors are used in turn to fill the regions. 

784 **kwargs: dict 

785 Key-word arguments that are passed on to the matplotlib.fill() function. 

786 """ 

787 c = 0 

788 for region in self.infinite_regions: 

789 polygon = [] 

790 for p in region: 

791 if p >= 0: 

792 polygon.append(self.vertices[p]) 

793 else: 

794 polygon.append(self.infinite_vertices[-p-1]) 

795 if len(polygon) > 0: 

796 polygon = np.asarray(polygon) 

797 c += 1 

798 if colors is None: 

799 ax.fill(polygon[:, 0], polygon[:, 1], lw=0, **kwargs) 

800 else: 

801 ax.fill(polygon[:, 0], polygon[:, 1], 

802 color=colors[c % len(colors)], lw=0, **kwargs) 

803 

804 def plot_hull(self, ax, **kwargs): 

805 """Plot the hull line containing the input points. 

806 

807 Parameters 

808 ---------- 

809 ax: matplotlib.Axes 

810 The axes to be used for plotting. 

811 **kwargs: dict 

812 Key-word arguments that are passed on to the matplotlib.plot() function. 

813 """ 

814 ax.plot(self.hull.points[self.hull_points, 0], 

815 self.hull.points[self.hull_points, 1], **kwargs) 

816 

817 def fill_hull(self, ax, **kwargs): 

818 """Fill the hull containing the input points with a color. 

819 

820 Parameters 

821 ---------- 

822 ax: matplotlib.Axes 

823 The axes to be used for plotting. 

824 **kwargs: dict 

825 Key-word arguments that are passed on to the matplotlib.fill() function. 

826 """ 

827 ax.fill(self.hull.points[self.hull_points, 0], 

828 self.hull.points[self.hull_points, 1], lw=0, **kwargs) 

829 

830 def plot_hull_center(self, ax, **kwargs): 

831 """Plot the center of mass of the convex hull of the input points. 

832 

833 Parameters 

834 ---------- 

835 ax: matplotlib.Axes 

836 The axes to be used for plotting. 

837 **kwargs: dict 

838 Key-word arguments that are passed on to the matplotlib.plot() function. 

839 """ 

840 ax.plot(self.hull_center[0], self.hull_center[1], 'o', **kwargs) 

841 

842 def plot_outer_hull(self, ax, **kwargs): 

843 """Plot the hull line containing the input points and the vertices of the Voronoi diagram. 

844 

845 Parameters 

846 ---------- 

847 ax: matplotlib.Axes or None 

848 The axes to be used for plotting. 

849 **kwargs: dict 

850 Key-word arguments that are passed on to the matplotlib.plot() function. 

851 """ 

852 ax.plot(self.outer_hull_points[:, 0], 

853 self.outer_hull_points[:, 1], **kwargs) 

854 

855 def fill_outer_hull(self, ax, **kwargs): 

856 """Fill the hull containing the input points and the vertices of the Voronoi diagram. 

857 

858 Parameters 

859 ---------- 

860 ax: matplotlib.Axes 

861 The axes to be used for plotting. 

862 **kwargs: dict 

863 Key-word arguments that are passed on to the matplotlib.fill() function. 

864 """ 

865 ax.fill(self.outer_hull_points[:, 0], 

866 self.outer_hull_points[:, 1], lw=0, **kwargs) 

867 

868 

869def main(): 

870 import scipy.stats as st 

871 import matplotlib.pyplot as plt 

872 

873 # generate random points: 

874 rs = np.random.randint(0xffffffff) 

875 #rs = 1718315706 # n=10 

876 #rs = 803645102 # n=10 

877 #rs = 2751318392 # double infinite ridges at vertex 0 (n=10) 

878 #rs = 4226093154 out-hull fac < 1.0 

879 

880 print(f'random seed: {rs}') 

881 np.random.seed(rs) 

882 n = 20 # number of points 

883 ar = 1.0 # aspect ratio of area in which the points should be placed 

884 points = np.random.rand(int(n//ar), 2) 

885 points = points[points[:, 1] < ar, :] 

886 

887 # calculate Voronoi diagram: 

888 vor = Voronoi(points) 

889 

890 # what we get is: 

891 print(f'dimension: {vor.ndim}') 

892 print(f'number of points: {vor.npoints}') 

893 print('') 

894 print('distances of nearest neighbors:') 

895 print(vor.nearest_distances) 

896 print('for each point its nearest neighbor:') 

897 print(vor.nearest_points) 

898 print('for each point all Voronoi distances:') 

899 print(vor.neighbor_distances) 

900 print('for each point all its neighbors:') 

901 print(vor.neighbor_points) 

902 print('for each ridge distances of neighbors:') 

903 print(vor.ridge_distances) 

904 print('corresponding neighbors enclosing ridges:') 

905 print(vor.ridge_points) 

906 print('') 

907 print('length of corresponding ridges:') 

908 print(vor.ridge_lengths()) 

909 print('area of corresponding triangles:') 

910 print(vor.ridge_areas()) 

911 for mode in ['inside', 'finite_inside', 'full', 'finite']: 

912 print(f'Voronoi area of each point ({mode}):') 

913 print(vor.areas(mode)) 

914 print('Comparison of sum of Voronoi areas (inside < finite_inside < full < finit):') 

915 a = [f'{np.sum(vor.areas(mode)):.2f}' for mode in ['inside', 'finite_inside', 'full', 'finite']] 

916 print(' < '.join(a)) 

917 print('Type of Voronoi area of each point:') 

918 print(vor.point_types()) 

919 

920 # plot Voronoi diagram: 

921 fig, ax = plt.subplots() 

922 ax.set_title('Voronoi') 

923 vor.fill_regions(ax, inside=True, colors=['red', 'green', 'blue', 'orange', 'cyan', 'magenta'], alpha=1.0, zorder=0) 

924 vor.fill_regions(ax, inside=False, colors=['red', 'green', 'blue', 'orange', 'cyan', 'magenta'], alpha=0.4, zorder=0) 

925 vor.fill_infinite_regions(ax, colors=['red', 'green', 'blue', 'orange', 'cyan', 'magenta'], alpha=0.1, zorder=0) 

926 vor.plot_distances(ax, color='red') 

927 #vor.plot_ridges(ax, c='k', lw=2) 

928 #vor.plot_infinite_ridges(ax, c='k', lw=2, linestyle='dashed') 

929 vor.plot_points(ax, text='p%d', c='c', s=100, zorder=10) 

930 vor.plot_vertices(ax, text='v%d', c='r', s=60) 

931 ex = 0.3 

932 ax.set_xlim(vor.min_bound[0]-ex, vor.max_bound[0]+ex) 

933 ax.set_ylim(vor.min_bound[1]-ex, vor.max_bound[1]+ex) 

934 ax.set_aspect('equal') 

935 

936 # Convex hull: 

937 print('Convex hull:') 

938 print(f'Area of convex hull: {vor.hull_area():g}') 

939 print(f'Area of outer hull: {vor.outer_hull_area():g}') 

940 new_points = vor.random_points(poisson=True, mode='outer') 

941 

942 # plot convex hull: 

943 fig, ax = plt.subplots() 

944 vor.fill_outer_hull(ax, color='black', alpha=0.1) 

945 vor.plot_outer_hull(ax, color='m', lw=2, label='outer hull') 

946 vor.fill_hull(ax, color='black', alpha=0.2) 

947 vor.plot_hull(ax, color='r', lw=2, label='convex hull') 

948 vor.plot_hull_center(ax, color='r', ms=16) 

949 vor.plot_hull_center(ax, color='k', ms=4) 

950 vor.plot_center(ax, color='c', ms=16) 

951 vor.plot_center(ax, color='k', ms=4) 

952 vor.plot_points(ax, text='p%d', c='c', s=100, zorder=10, 

953 label='input points') 

954 ax.plot(new_points[:, 0], new_points[:, 1], 'ob', ms=10, 

955 label='random points') 

956 ax.set_xlim(vor.min_bound[0]-ex, vor.max_bound[0]+ex) 

957 ax.set_ylim(vor.min_bound[1]-ex, vor.max_bound[1]+ex) 

958 ax.set_aspect('equal') 

959 ax.legend() 

960 

961 # bootstrap: 

962 def bootstrapped_nearest_distances(vor, n, poisson, mode, ax1, ax2, ax3, bins): 

963 ps = 'fixed' 

964 if poisson: 

965 ps = 'poisson' 

966 print(f'bootstrap {mode} {ps} ...') 

967 hist_distances = np.zeros((n, len(bins)-1)) 

968 mean_distances = [] 

969 cvs = [] 

970 for j in range(n): 

971 points = vor.random_points(poisson=poisson, mode=mode) 

972 if len(points) < 4: 

973 continue 

974 try: 

975 bvor = Voronoi(points) 

976 except: 

977 continue 

978 hist_distances[j,:] = np.histogram(bvor.nearest_distances, 

979 bins=bins, density=True)[0] 

980 mean_distances.append(np.mean(bvor.nearest_distances)) 

981 cvs.append(np.std(bvor.nearest_distances)/np.mean(bvor.nearest_distances)) 

982 ax1.set_title(f'bootstrap {mode} {ps} ...') 

983 ax1.hist(mean_distances, 30, density=True, label='bootstrap') 

984 de = np.mean(vor.nearest_distances) 

985 sem = 2.1*0.26136*np.sqrt(vor.outer_hull_area())/vor.npoints # note factor 2.1! 

986 h = 0.4/sem 

987 x = np.linspace(de-4.0*sem, de+4.0*sem, 200) 

988 p = st.norm.pdf(x, loc=de, scale=sem) 

989 ax1.plot(x, p, 'r', lw=2, label='Gaussian') 

990 ax1.plot([de, de], [0.0, h], 'r', lw=4, label='observed a.n.n.') 

991 bmd = np.mean(mean_distances) 

992 ax1.plot([bmd, bmd], [0.0, h], 'g', lw=4, label='bootstrapped a.n.n.') 

993 ax1.set_xlabel('mean nearest-neighbor distance') 

994 ax1.legend() 

995 ax2.set_title(f'bootstrap {mode} {ps} ...') 

996 ax2.hist(cvs, 30, density=True, label='bootstrap') 

997 # observed cv: 

998 cvo = np.std(vor.nearest_distances)/np.mean(vor.nearest_distances) 

999 # significance from bootstrap: 

1000 pb = 0.01*st.percentileofscore(cvs, cvo) 

1001 if pb > 0.5: 

1002 pb = 1.0-pb 

1003 pb *= 2.0 

1004 # significance from z-score: 

1005 secv = 2.0*np.sqrt(1.0/np.pi-0.25)/np.sqrt(vor.npoints) 

1006 zcv = (cvo - 2.0*0.26136)/secv 

1007 pz = 2.0*(1.0 - st.norm.cdf(np.abs(zcv))) 

1008 h = 0.4/secv 

1009 ax2.plot([cvo, cvo], [0.0, h], 'r', lw=4, label=rf'observed CV $\alpha=${100.0*pz:.0f}%') 

1010 cvb = np.mean(cvs) 

1011 x = np.linspace(cvb-4.0*secv, cvb+4.0*secv, 200) 

1012 p = st.norm.pdf(x, loc=cvb, scale=secv) 

1013 ax2.plot(x, p, 'g', lw=2, label='Gaussian') 

1014 ax2.plot([cvb, cvb], [0.0, h], 'g', lw=4, label=rf'bootstrapped CV $\alpha=${100.0*pb:.0f}%') 

1015 ax2.set_xlabel('CV') 

1016 ax2.legend() 

1017 # nearest-neighbor distance histogram: 

1018 ax3.set_title(f'bootstrap {mode} {ps} ...') 

1019 ax3.fill_between(bins[:-1], *np.percentile(hist_distances, [2.5, 97.5], axis=0), facecolor='blue', alpha=0.5) 

1020 ax3.plot(bins[:-1], np.median(hist_distances, axis=0), 'b', lw=2) 

1021 hd = np.histogram(vor.nearest_distances, bins=bins, density=True)[0] 

1022 ax3.plot(bins[:-1], hd, 'r', lw=2) 

1023 ax3.set_xlim(0.0, 0.5) 

1024 ax3.set_xlabel('nearest-neighbor distance') 

1025 

1026 print(f'Mean distance: {np.mean(vor.nearest_distances):g}') 

1027 print(f'CV: {np.std(vor.nearest_distances)/np.mean(vor.nearest_distances):g}') 

1028 bins = np.linspace(0.0, 1.0, 30) 

1029 nb = 300 

1030 fig1, axs1 = plt.subplots(2, 2, sharex=True) 

1031 fig2, axs2 = plt.subplots(2, 2, sharex=True) 

1032 fig3, axs3 = plt.subplots(2, 2, sharex=True) 

1033 for k, poisson in enumerate([False, True]): 

1034 for j, mode in enumerate(['hull', 'outer']): 

1035 bootstrapped_nearest_distances(vor, nb, poisson, mode, 

1036 axs1[k,j], axs2[k,j], axs3[k,j], 

1037 bins) 

1038 for f in [fig1, fig2, fig3]: 

1039 f.tight_layout() 

1040 print('... done.') 

1041 plt.tight_layout() 

1042 plt.show() 

1043 

1044 

1045if __name__ == "__main__": 

1046 main()