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}