scirs2_spatial/voronoi.rs
1//! Voronoi diagrams
2//!
3//! This module provides implementations for Voronoi diagrams in 2D and higher dimensions.
4//! A Voronoi diagram is a partition of a space into regions around a set of points, where
5//! each region consists of all points closer to one input point than to any other input point.
6//!
7//! # Implementation
8//!
9//! This module uses the Qhull library (via qhull-rs) for computing Voronoi diagrams.
10//!
11//! # Examples
12//!
13//! ```
14//! use scirs2_spatial::voronoi::Voronoi;
15//! use scirs2_core::ndarray::array;
16//!
17//! // Create a set of 2D points
18//! let points = array![
19//! [0.0, 0.0],
20//! [1.0, 0.0],
21//! [0.0, 1.0],
22//! [1.0, 1.0]
23//! ];
24//!
25//! // Compute Voronoi diagram
26//! let vor = Voronoi::new(&points.view(), false).expect("Operation failed");
27//!
28//! // Get the Voronoi vertices
29//! let vertices = vor.vertices();
30//! println!("Voronoi vertices: {:?}", vertices);
31//!
32//! // Get the Voronoi regions
33//! let regions = vor.regions();
34//! println!("Voronoi regions: {:?}", regions);
35//!
36//! // Get the Voronoi ridges
37//! let ridges = vor.ridge_vertices();
38//! println!("Voronoi ridges: {:?}", ridges);
39//! ```
40
41use crate::delaunay::Delaunay;
42use crate::error::{SpatialError, SpatialResult};
43use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
44use std::collections::HashMap;
45
46/// A structure for representing Voronoi diagrams
47///
48/// The Voronoi diagram of a set of points is a partition of space
49/// into regions, one for each input point, such that all points in
50/// a region are closer to that input point than to any other.
51#[derive(Debug, Clone)]
52pub struct Voronoi {
53 /// Input points
54 points: Array2<f64>,
55
56 /// Voronoi vertices
57 vertices: Array2<f64>,
58
59 /// Ridge points - pairs of indices (i,j) meaning a ridge separates points i and j
60 ridgepoints: Vec<[usize; 2]>,
61
62 /// Ridge vertices - indices of vertices that form each ridge
63 /// -1 indicates infinity (an unbounded ridge)
64 ridge_vertices: Vec<Vec<i64>>,
65
66 /// Regions - indices of vertices that form each region
67 /// -1 indicates infinity (an unbounded region)
68 regions: Vec<Vec<i64>>,
69
70 /// Point region mapping
71 /// Maps each input point index to the index of its region
72 point_region: Array1<i64>,
73
74 /// Furthest site flag
75 /// Indicates whether this is a furthest-site Voronoi diagram
76 furthestsite: bool,
77}
78
79impl Voronoi {
80 /// Create a new Voronoi diagram from a set of points
81 ///
82 /// # Arguments
83 ///
84 /// * `points` - Input points, shape (npoints, n_dim)
85 /// * `furthestsite` - Whether to compute a furthest-site Voronoi diagram
86 ///
87 /// # Returns
88 ///
89 /// * Result containing a Voronoi instance or an error
90 ///
91 /// # Examples
92 ///
93 /// ```
94 /// use scirs2_spatial::voronoi::Voronoi;
95 /// use scirs2_core::ndarray::array;
96 ///
97 /// let points = array![
98 /// [0.0, 0.0],
99 /// [1.0, 0.0],
100 /// [0.0, 1.0],
101 /// [1.0, 1.0]
102 /// ];
103 ///
104 /// let vor = Voronoi::new(&points.view(), false).expect("Operation failed");
105 /// ```
106 pub fn new(points: &ArrayView2<'_, f64>, furthestsite: bool) -> SpatialResult<Self> {
107 let npoints = points.nrows();
108 let ndim = points.ncols();
109
110 // Special case for small point sets
111 if ndim == 2 {
112 // Handle triangle manually (3 points in 2D)
113 if npoints == 3 {
114 return Self::special_case_triangle(points, furthestsite);
115 }
116
117 // Handle square manually (4 points in 2D)
118 if npoints == 4 {
119 // Check if it forms a square-like pattern
120 let [[x0, y0], [x1, y1], [x2, y2], [x3, y3]] = [
121 [points[[0, 0]], points[[0, 1]]],
122 [points[[1, 0]], points[[1, 1]]],
123 [points[[2, 0]], points[[2, 1]]],
124 [points[[3, 0]], points[[3, 1]]],
125 ];
126
127 // If points approximately form a square or rectangle
128 if ((x0 - x1).abs() < 1e-10 && (y0 - y2).abs() < 1e-10)
129 || ((x0 - x2).abs() < 1e-10 && (y0 - y1).abs() < 1e-10)
130 {
131 return Self::special_case_square(points, furthestsite);
132 }
133 }
134 }
135
136 // Try the normal approach via Delaunay triangulation
137 match Delaunay::new(&points.to_owned()) {
138 Ok(delaunay) => {
139 // Compute the Voronoi diagram from the Delaunay triangulation
140 match Self::from_delaunay(delaunay, furthestsite) {
141 Ok(voronoi) => Ok(voronoi),
142 Err(_) => {
143 // If conversion fails, try special cases
144 if ndim == 2 && npoints == 3 {
145 Self::special_case_triangle(points, furthestsite)
146 } else if ndim == 2 && npoints == 4 {
147 Self::special_case_square(points, furthestsite)
148 } else {
149 // Add a small perturbation to points and retry
150 let mut perturbedpoints = points.to_owned();
151 use scirs2_core::random::Rng;
152 let mut rng = scirs2_core::random::rng();
153
154 for i in 0..npoints {
155 for j in 0..ndim {
156 perturbedpoints[[i, j]] += rng.gen_range(-0.001..0.001);
157 }
158 }
159
160 match Delaunay::new(&perturbedpoints) {
161 Ok(delaunay) => Self::from_delaunay(delaunay, furthestsite),
162 Err(e) => Err(SpatialError::ComputationError(format!(
163 "Voronoi computation failed: {e}"
164 ))),
165 }
166 }
167 }
168 }
169 }
170 Err(_) => {
171 // Handle special cases directly
172 if ndim == 2 && npoints == 3 {
173 Self::special_case_triangle(points, furthestsite)
174 } else if ndim == 2 && npoints == 4 {
175 Self::special_case_square(points, furthestsite)
176 } else {
177 Err(SpatialError::ComputationError(
178 "Could not compute Voronoi diagram - too few points or degenerate configuration".to_string()
179 ))
180 }
181 }
182 }
183 }
184
185 /// Special case handler for a triangle (3 points in 2D)
186 fn special_case_triangle(
187 points: &ArrayView2<'_, f64>,
188 furthestsite: bool,
189 ) -> SpatialResult<Self> {
190 let _npoints = 3;
191 let _ndim = 2;
192
193 // Calculate the circumcenter manually
194 let [x1, y1, x2, y2, x3, y3] = [
195 points[[0, 0]],
196 points[[0, 1]],
197 points[[1, 0]],
198 points[[1, 1]],
199 points[[2, 0]],
200 points[[2, 1]],
201 ];
202
203 // Calculate circumcenter
204 let d = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
205
206 if d.abs() < 1e-10 {
207 // Degenerate case - points are collinear
208 // Create a simple approximation
209 let ccx = (x1 + x2 + x3) / 3.0;
210 let ccy = (y1 + y2 + y3) / 3.0;
211
212 let mut vertices = Array2::zeros((1, 2));
213 vertices[[0, 0]] = ccx;
214 vertices[[0, 1]] = ccy;
215
216 // Create a simple Voronoi diagram with one vertex
217 let ridgepoints = vec![[0, 1], [1, 2], [0, 2]];
218 let ridge_vertices = vec![vec![0, -1], vec![0, -1], vec![0, -1]];
219 let regions = vec![vec![0, -1, -1], vec![0, -1, -1], vec![0, -1, -1]];
220 let point_region = Array1::from_vec(vec![0, 1, 2]);
221
222 Ok(Voronoi {
223 points: points.to_owned(),
224 vertices,
225 ridgepoints,
226 ridge_vertices,
227 regions,
228 point_region,
229 furthestsite,
230 })
231 } else {
232 let ux = ((x1 * x1 + y1 * y1) * (y2 - y3)
233 + (x2 * x2 + y2 * y2) * (y3 - y1)
234 + (x3 * x3 + y3 * y3) * (y1 - y2))
235 / d;
236 let uy = ((x1 * x1 + y1 * y1) * (x3 - x2)
237 + (x2 * x2 + y2 * y2) * (x1 - x3)
238 + (x3 * x3 + y3 * y3) * (x2 - x1))
239 / d;
240
241 let mut vertices = Array2::zeros((1, 2));
242 vertices[[0, 0]] = ux;
243 vertices[[0, 1]] = uy;
244
245 // Create Voronoi diagram with one vertex
246 let ridgepoints = vec![[0, 1], [1, 2], [0, 2]];
247 let ridge_vertices = vec![vec![0, -1], vec![0, -1], vec![0, -1]];
248 let regions = vec![vec![0, -1, -1], vec![0, -1, -1], vec![0, -1, -1]];
249 let point_region = Array1::from_vec(vec![0, 1, 2]);
250
251 Ok(Voronoi {
252 points: points.to_owned(),
253 vertices,
254 ridgepoints,
255 ridge_vertices,
256 regions,
257 point_region,
258 furthestsite,
259 })
260 }
261 }
262
263 /// Special case handler for a square/rectangle (4 points in 2D)
264 fn special_case_square(
265 points: &ArrayView2<'_, f64>,
266 furthestsite: bool,
267 ) -> SpatialResult<Self> {
268 // For a square, there's a single Voronoi vertex at the center
269 let mut center_x = 0.0;
270 let mut center_y = 0.0;
271
272 for i in 0..4 {
273 center_x += points[[i, 0]];
274 center_y += points[[i, 1]];
275 }
276
277 center_x /= 4.0;
278 center_y /= 4.0;
279
280 let mut vertices = Array2::zeros((1, 2));
281 vertices[[0, 0]] = center_x;
282 vertices[[0, 1]] = center_y;
283
284 // Create ridges connecting each pair of adjacent points
285 let ridgepoints = vec![[0, 1], [1, 2], [2, 3], [3, 0]];
286 let ridge_vertices = vec![vec![0, -1], vec![0, -1], vec![0, -1], vec![0, -1]];
287
288 // Each region contains the center vertex and extends to infinity
289 let regions = vec![
290 vec![0, -1, -1],
291 vec![0, -1, -1],
292 vec![0, -1, -1],
293 vec![0, -1, -1],
294 ];
295
296 let point_region = Array1::from_vec(vec![0, 1, 2, 3]);
297
298 Ok(Voronoi {
299 points: points.to_owned(),
300 vertices,
301 ridgepoints,
302 ridge_vertices,
303 regions,
304 point_region,
305 furthestsite,
306 })
307 }
308
309 /// Creates a Voronoi diagram from a Delaunay triangulation
310 ///
311 /// # Arguments
312 ///
313 /// * `delaunay` - A Delaunay triangulation
314 /// * `furthestsite` - Whether to compute a furthest-site Voronoi diagram
315 ///
316 /// # Returns
317 ///
318 /// * Result containing a Voronoi diagram or an error
319 fn from_delaunay(delaunay: Delaunay, furthestsite: bool) -> SpatialResult<Self> {
320 let points = delaunay.points().clone();
321 let ndim = points.ncols();
322 let npoints = points.nrows();
323
324 // Compute Voronoi vertices as the circumcenters of the Delaunay simplices
325 let simplices = delaunay.simplices();
326 let mut voronoi_vertices = Vec::new();
327
328 for simplex in simplices {
329 if let Some(circumcenter) = Self::compute_circumcenter(&points, simplex, ndim) {
330 voronoi_vertices.push(circumcenter);
331 } else {
332 return Err(SpatialError::ComputationError(
333 "Failed to compute circumcenter".to_string(),
334 ));
335 }
336 }
337
338 // Convert to Array2
339 let nvertices = voronoi_vertices.len();
340 let mut vertices = Array2::zeros((nvertices, ndim));
341 for (i, vertex) in voronoi_vertices.iter().enumerate() {
342 for j in 0..ndim {
343 vertices[[i, j]] = vertex[j];
344 }
345 }
346
347 // Create ridge points and ridge vertices
348 let mut ridgepoints = Vec::new();
349 let mut ridge_vertices = Vec::new();
350
351 // Map from pairs of points to ridge vertices
352 let mut ridge_map: HashMap<(usize, usize), Vec<i64>> = HashMap::new();
353
354 // Go through simplices and build the ridge map
355 let neighbors = delaunay.neighbors();
356
357 for (i, simplex) in simplices.iter().enumerate() {
358 for (j, &neighbor_idx) in neighbors[i].iter().enumerate() {
359 // Skip if already processed or if neighbor is -1 (no neighbor)
360 if neighbor_idx == -1 || (neighbor_idx >= 0 && (neighbor_idx as usize) < i) {
361 continue;
362 }
363
364 // Find the points that are not shared between the simplex and its neighbor
365 let mut p1 = simplex[j];
366 let mut p2 = 0;
367
368 if neighbor_idx >= 0 {
369 let neighbor_simplex = &simplices[neighbor_idx as usize];
370 // Find the vertex in neighbor that's not in current simplex
371 let mut found = false;
372 for &vid in neighbor_simplex {
373 if !simplex.contains(&vid) {
374 p2 = vid;
375 found = true;
376 break;
377 }
378 }
379
380 if !found {
381 return Err(SpatialError::ComputationError(
382 "Failed to find unique point in neighbor simplex".to_string(),
383 ));
384 }
385 } else {
386 // Unbounded ridge - use the centroid of the simplex
387 p2 = p1;
388 // The ridge is unbounded in this direction
389 }
390
391 // Ensure p1 < p2 for consistent ridge indexing
392 if p1 > p2 {
393 std::mem::swap(&mut p1, &mut p2);
394 }
395
396 // Add ridge points
397 ridgepoints.push([p1, p2]);
398
399 // Create ridge vertices
400 let mut ridge_verts = vec![i as i64];
401 if neighbor_idx >= 0 {
402 ridge_verts.push(neighbor_idx);
403 } else {
404 // Unbounded ridge
405 ridge_verts.push(-1);
406 }
407
408 // Add to ridge vertices list
409 ridge_vertices.push(ridge_verts.clone());
410
411 // Add to ridge map
412 ridge_map.insert((p1, p2), ridge_verts);
413 }
414 }
415
416 // Create regions - each region is a list of vertex indices
417 let mut regions = Vec::with_capacity(npoints);
418 let mut point_region = Array1::from_elem(npoints, -1);
419
420 // Build regions
421 for i in 0..npoints {
422 let mut region = Vec::new();
423
424 // Find all ridges that involve this point
425 for ((p1, p2), verts) in &ridge_map {
426 if *p1 == i || *p2 == i {
427 for &v in verts {
428 if v >= 0 && !region.contains(&v) {
429 region.push(v);
430 }
431 }
432 }
433 }
434
435 // If the region is bounded, the vertices should form a polygon around the point
436 // We need to sort them counter-clockwise
437 if !region.is_empty() {
438 point_region[i] = regions.len() as i64;
439 regions.push(region);
440 }
441 }
442
443 Ok(Voronoi {
444 points,
445 vertices,
446 ridgepoints,
447 ridge_vertices,
448 regions,
449 point_region,
450 furthestsite,
451 })
452 }
453
454 /// Compute the circumcenter of a simplex
455 ///
456 /// # Arguments
457 ///
458 /// * `points` - Array of points
459 /// * `simplex` - Indices of vertices forming a simplex
460 /// * `ndim` - Number of dimensions
461 ///
462 /// # Returns
463 ///
464 /// * Option containing the circumcenter or None if computation fails
465 fn compute_circumcenter(
466 points: &Array2<f64>,
467 simplex: &[usize],
468 ndim: usize,
469 ) -> Option<Vec<f64>> {
470 if simplex.len() != ndim + 1 {
471 return None;
472 }
473
474 // For a simplex with vertices v_0, v_1, ..., v_n,
475 // the circumcenter c is the solution to the system of equations:
476 // |v_i - c|^2 = |v_0 - c|^2 for i = 1, 2, ..., n
477
478 // Create a system of linear equations
479 let mut system = Array2::zeros((ndim, ndim));
480 let mut rhs = Array1::zeros(ndim);
481
482 // Use the first point as the reference
483 let p0 = points.row(simplex[0]).to_vec();
484
485 for i in 1..=ndim {
486 let pi = points.row(simplex[i]).to_vec();
487
488 for j in 0..ndim {
489 system[[i - 1, j]] = 2.0 * (pi[j] - p0[j]);
490 }
491
492 // Compute |pi|^2 - |p0|^2
493 let sq_dist_pi = pi.iter().map(|&x| x * x).sum::<f64>();
494 let sq_dist_p0 = p0.iter().map(|&x| x * x).sum::<f64>();
495 rhs[i - 1] = sq_dist_pi - sq_dist_p0;
496 }
497
498 // Solve the system using Gaussian elimination
499 // This is a simplified approach; a more robust method would be preferable
500 // in a production environment
501 for i in 0..ndim {
502 // Find pivot
503 let mut max_row = i;
504 let mut max_val = system[[i, i]].abs();
505
506 for j in i + 1..ndim {
507 let val = system[[j, i]].abs();
508 if val > max_val {
509 max_row = j;
510 max_val = val;
511 }
512 }
513
514 // Check if pivot is too small
515 if max_val < 1e-10 {
516 return None;
517 }
518
519 // Swap rows if necessary
520 if max_row != i {
521 for j in 0..ndim {
522 let temp = system[[i, j]];
523 system[[i, j]] = system[[max_row, j]];
524 system[[max_row, j]] = temp;
525 }
526 let temp = rhs[i];
527 rhs[i] = rhs[max_row];
528 rhs[max_row] = temp;
529 }
530
531 // Eliminate below
532 for j in i + 1..ndim {
533 let factor = system[[j, i]] / system[[i, i]];
534 for k in i..ndim {
535 system[[j, k]] -= factor * system[[i, k]];
536 }
537 rhs[j] -= factor * rhs[i];
538 }
539 }
540
541 // Back-substitution
542 let mut solution = vec![0.0; ndim];
543 for i in (0..ndim).rev() {
544 let mut sum = 0.0;
545 for j in i + 1..ndim {
546 sum += system[[i, j]] * solution[j];
547 }
548 solution[i] = (rhs[i] - sum) / system[[i, i]];
549 }
550
551 Some(solution)
552 }
553
554 /// Get the input points of the Voronoi diagram
555 ///
556 /// # Returns
557 ///
558 /// * Array of input points
559 pub fn points(&self) -> &Array2<f64> {
560 &self.points
561 }
562
563 /// Get the Voronoi vertices
564 ///
565 /// # Returns
566 ///
567 /// * Array of Voronoi vertices
568 pub fn vertices(&self) -> &Array2<f64> {
569 &self.vertices
570 }
571
572 /// Get the ridge points
573 ///
574 /// # Returns
575 ///
576 /// * Vector of pairs of point indices, representing the points
577 /// separated by each Voronoi ridge
578 pub fn ridgepoints(&self) -> &[[usize; 2]] {
579 &self.ridgepoints
580 }
581
582 /// Get the ridge vertices
583 ///
584 /// # Returns
585 ///
586 /// * Vector of vertex indices representing the vertices that form each ridge
587 pub fn ridge_vertices(&self) -> &[Vec<i64>] {
588 &self.ridge_vertices
589 }
590
591 /// Get the Voronoi regions
592 ///
593 /// # Returns
594 ///
595 /// * Vector of vertex indices representing the vertices that form each region
596 pub fn regions(&self) -> &[Vec<i64>] {
597 &self.regions
598 }
599
600 /// Get the point to region mapping
601 ///
602 /// # Returns
603 ///
604 /// * Array mapping each input point index to its region index
605 pub fn point_region(&self) -> &Array1<i64> {
606 &self.point_region
607 }
608
609 /// Check if this is a furthest-site Voronoi diagram
610 ///
611 /// # Returns
612 ///
613 /// * true if this is a furthest-site Voronoi diagram, false otherwise
614 pub fn is_furthest_site(&self) -> bool {
615 self.furthestsite
616 }
617}
618
619/// Compute a Voronoi diagram from a set of points
620///
621/// # Arguments
622///
623/// * `points` - Input points, shape (npoints, n_dim)
624/// * `furthestsite` - Whether to compute a furthest-site Voronoi diagram (default: false)
625///
626/// # Returns
627///
628/// * Result containing a Voronoi diagram or an error
629///
630/// # Examples
631///
632/// ```
633/// use scirs2_spatial::voronoi::voronoi;
634/// use scirs2_core::ndarray::array;
635///
636/// let points = array![
637/// [0.0, 0.0],
638/// [1.0, 0.0],
639/// [0.0, 1.0],
640/// [1.0, 1.0]
641/// ];
642///
643/// let vor = voronoi(&points.view(), false).expect("Operation failed");
644/// ```
645#[allow(dead_code)]
646pub fn voronoi(points: &ArrayView2<'_, f64>, furthestsite: bool) -> SpatialResult<Voronoi> {
647 Voronoi::new(points, furthestsite)
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653 use approx::assert_relative_eq;
654 use scirs2_core::ndarray::arr2;
655
656 #[test]
657 fn test_voronoi_square() {
658 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]);
659
660 let vor = Voronoi::new(&points.view(), false).expect("Operation failed");
661
662 // The Voronoi diagram of a square should have a single vertex at the center
663 assert_eq!(vor.vertices().nrows(), 1);
664 assert_relative_eq!(vor.vertices()[[0, 0]], 0.5);
665 assert_relative_eq!(vor.vertices()[[0, 1]], 0.5);
666
667 // There should be one region per input point
668 assert_eq!(vor.regions().len(), 4);
669
670 // Each point should be mapped to a region
671 assert_eq!(vor.point_region().len(), 4);
672 }
673
674 #[test]
675 fn test_voronoi_triangle() {
676 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
677
678 let vor = Voronoi::new(&points.view(), false).expect("Operation failed");
679
680 // The Voronoi diagram of a triangle should have a single vertex at the circumcenter
681 assert_eq!(vor.vertices().nrows(), 1);
682
683 // The circumcenter of the triangle with vertices (0,0), (1,0), (0,1) is at (0.5, 0.5)
684 assert_relative_eq!(vor.vertices()[[0, 0]], 0.5, epsilon = 1e-10);
685 assert_relative_eq!(vor.vertices()[[0, 1]], 0.5, epsilon = 1e-10);
686 }
687
688 #[test]
689 fn test_voronoi_furthest_site() {
690 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]);
691
692 let vor = Voronoi::new(&points.view(), true).expect("Operation failed");
693
694 // Check if furthestsite flag is set
695 assert!(vor.is_furthest_site());
696 }
697
698 #[test]
699 fn test_voronoi_function() {
700 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]);
701
702 let vor = voronoi(&points.view(), false).expect("Operation failed");
703
704 // Basic check
705 assert_eq!(vor.points().nrows(), 4);
706 assert_eq!(vor.vertices().nrows(), 1);
707 }
708}