scirs2_spatial/convex_hull/
core.rs

1//! Core ConvexHull structure and basic methods
2//!
3//! This module contains the main ConvexHull struct and its fundamental
4//! operations, providing a foundation for convex hull computations.
5
6use crate::error::{SpatialError, SpatialResult};
7use qhull::Qh;
8use scirs2_core::ndarray::{Array2, ArrayView2};
9
10/// Algorithms available for computing convex hulls
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum ConvexHullAlgorithm {
13    /// Use QHull (default) - works for any dimension
14    #[default]
15    QHull,
16    /// Graham scan algorithm - only for 2D
17    GrahamScan,
18    /// Jarvis march (gift wrapping) algorithm - only for 2D
19    JarvisMarch,
20}
21
22/// A ConvexHull represents the convex hull of a set of points.
23///
24/// It provides methods to access hull properties, check if points are
25/// inside the hull, and access hull facets and vertices.
26pub struct ConvexHull {
27    /// Input points
28    pub(crate) points: Array2<f64>,
29    /// QHull instance
30    #[allow(dead_code)]
31    pub(crate) qh: Qh<'static>,
32    /// Vertex indices of the convex hull (indices into the original points array)
33    pub(crate) vertex_indices: Vec<usize>,
34    /// Simplex indices (facets) of the convex hull
35    pub(crate) simplices: Vec<Vec<usize>>,
36    /// Equations of the hull facets (shape: n_facets x (n_dim+1))
37    pub(crate) equations: Option<Array2<f64>>,
38}
39
40impl ConvexHull {
41    /// Create a new ConvexHull from a set of points.
42    ///
43    /// # Arguments
44    ///
45    /// * `points` - Input points (shape: npoints x n_dim)
46    ///
47    /// # Returns
48    ///
49    /// * Result containing a ConvexHull instance or an error
50    ///
51    /// # Errors
52    ///
53    /// * Returns error if hull computation fails or input is invalid
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use scirs2_spatial::convex_hull::ConvexHull;
59    /// use scirs2_core::ndarray::array;
60    ///
61    /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
62    /// let hull = ConvexHull::new(&points.view()).unwrap();
63    /// ```
64    pub fn new(points: &ArrayView2<'_, f64>) -> SpatialResult<Self> {
65        Self::new_with_algorithm(points, ConvexHullAlgorithm::default())
66    }
67
68    /// Create a new ConvexHull from a set of points using a specific algorithm.
69    ///
70    /// # Arguments
71    ///
72    /// * `points` - Input points (shape: npoints x n_dim)
73    /// * `algorithm` - Algorithm to use for convex hull computation
74    ///
75    /// # Returns
76    ///
77    /// * Result containing a ConvexHull instance or an error
78    ///
79    /// # Errors
80    ///
81    /// * Returns error if hull computation fails or input is invalid
82    /// * Returns error if algorithm is not supported for the given dimensionality
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use scirs2_spatial::convex_hull::{ConvexHull, ConvexHullAlgorithm};
88    /// use scirs2_core::ndarray::array;
89    ///
90    /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
91    /// let hull = ConvexHull::new_with_algorithm(&points.view(), ConvexHullAlgorithm::GrahamScan).unwrap();
92    /// ```
93    pub fn new_with_algorithm(
94        points: &ArrayView2<'_, f64>,
95        algorithm: ConvexHullAlgorithm,
96    ) -> SpatialResult<Self> {
97        let npoints = points.nrows();
98        let ndim = points.ncols();
99
100        // For 1D, allow at least 1 point (degenerate case)
101        // For 2D, need at least 3 points for a proper hull, but allow 2 for degenerate line case
102        // For 3D and higher, require at least ndim + 1 points
103        let min_points = match ndim {
104            1 => 1, // Allow single points in 1D
105            2 => 3, // Need at least 3 points for a 2D convex hull
106            _ => ndim + 1,
107        };
108
109        if npoints < min_points {
110            return Err(SpatialError::ValueError(format!(
111                "Need at least {} points to construct a {}D convex hull",
112                min_points, ndim
113            )));
114        }
115
116        // Check if algorithm is compatible with dimensionality
117        match algorithm {
118            ConvexHullAlgorithm::GrahamScan | ConvexHullAlgorithm::JarvisMarch => {
119                if ndim != 2 {
120                    return Err(SpatialError::ValueError(format!(
121                        "{algorithm:?} algorithm only supports 2D points, got {ndim}D"
122                    )));
123                }
124            }
125            ConvexHullAlgorithm::QHull => {
126                // QHull supports any dimension
127            }
128        }
129
130        match algorithm {
131            ConvexHullAlgorithm::GrahamScan => {
132                crate::convex_hull::algorithms::graham_scan::compute_graham_scan(points)
133            }
134            ConvexHullAlgorithm::JarvisMarch => {
135                crate::convex_hull::algorithms::jarvis_march::compute_jarvis_march(points)
136            }
137            ConvexHullAlgorithm::QHull => {
138                crate::convex_hull::algorithms::qhull::compute_qhull(points)
139            }
140        }
141    }
142
143    /// Get the vertices of the convex hull
144    ///
145    /// # Returns
146    ///
147    /// * Array of vertices of the convex hull
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use scirs2_spatial::convex_hull::ConvexHull;
153    /// use scirs2_core::ndarray::array;
154    ///
155    /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
156    /// let hull = ConvexHull::new(&points.view()).unwrap();
157    ///
158    /// // The hull vertices should be the corners, not the interior point
159    /// // The number of vertices can vary depending on QHull implementation
160    /// assert!(hull.vertices().len() >= 3);
161    /// ```
162    pub fn vertices(&self) -> Vec<Vec<f64>> {
163        self.vertex_indices
164            .iter()
165            .map(|&idx| self.points.row(idx).to_vec())
166            .collect()
167    }
168
169    /// Get the vertices of the convex hull as an Array2
170    ///
171    /// # Returns
172    ///
173    /// * Array2 of shape (n_vertices, n_dim) containing the vertices of the convex hull
174    pub fn vertices_array(&self) -> Array2<f64> {
175        let ndim = self.points.ncols();
176        let n_vertices = self.vertex_indices.len();
177        let mut vertices = Array2::zeros((n_vertices, ndim));
178
179        for (i, &idx) in self.vertex_indices.iter().enumerate() {
180            // Make sure the index is valid
181            if idx < self.points.nrows() {
182                for j in 0..ndim {
183                    vertices[[i, j]] = self.points[[idx, j]];
184                }
185            } else {
186                // If we have an invalid index (shouldn't happen, but for safety)
187                for j in 0..ndim {
188                    vertices[[i, j]] = 0.0;
189                }
190            }
191        }
192
193        vertices
194    }
195
196    /// Get the indices of the vertices of the convex hull
197    ///
198    /// # Returns
199    ///
200    /// * Vector of indices of the hull vertices (into the original points array)
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use scirs2_spatial::convex_hull::ConvexHull;
206    /// use scirs2_core::ndarray::array;
207    ///
208    /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
209    /// let hull = ConvexHull::new(&points.view()).unwrap();
210    ///
211    /// // Get the vertex indices of the hull
212    /// let vertices = hull.vertex_indices();
213    /// println!("Convex hull vertices: {:?}", vertices);
214    /// ```
215    pub fn vertex_indices(&self) -> &[usize] {
216        &self.vertex_indices
217    }
218
219    /// Get the simplices (facets) of the convex hull
220    ///
221    /// # Returns
222    ///
223    /// * Vector of simplices, where each simplex is a vector of vertex indices
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// use scirs2_spatial::convex_hull::ConvexHull;
229    /// use scirs2_core::ndarray::array;
230    ///
231    /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
232    /// let hull = ConvexHull::new(&points.view()).unwrap();
233    ///
234    /// // For a 2D hull, examine the simplices
235    /// for simplex in hull.simplices() {
236    ///     // Print each simplex
237    ///     println!("Simplex: {:?}", simplex);
238    /// }
239    /// ```
240    pub fn simplices(&self) -> &[Vec<usize>] {
241        &self.simplices
242    }
243
244    /// Get the dimensionality of the convex hull
245    ///
246    /// # Returns
247    ///
248    /// * Number of dimensions of the convex hull
249    ///
250    /// # Examples
251    ///
252    /// ```
253    /// use scirs2_spatial::convex_hull::ConvexHull;
254    /// use scirs2_core::ndarray::array;
255    ///
256    /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
257    /// let hull = ConvexHull::new(&points.view()).unwrap();
258    ///
259    /// assert_eq!(hull.ndim(), 2);
260    /// ```
261    pub fn ndim(&self) -> usize {
262        self.points.ncols()
263    }
264
265    /// Check if a point is inside the convex hull
266    ///
267    /// # Arguments
268    ///
269    /// * `point` - The point to check
270    ///
271    /// # Returns
272    ///
273    /// * Result containing a boolean indicating if the point is inside the hull
274    ///
275    /// # Errors
276    ///
277    /// * Returns error if point dimension doesn't match hull dimension
278    ///
279    /// # Examples
280    ///
281    /// ```
282    /// use scirs2_spatial::convex_hull::ConvexHull;
283    /// use scirs2_core::ndarray::array;
284    ///
285    /// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
286    /// let hull = ConvexHull::new(&points.view()).unwrap();
287    ///
288    /// // Check a point inside the hull
289    /// // Result may vary depending on QHull implementation and special case handling
290    /// let inside = hull.contains(&[0.25, 0.25]).unwrap();
291    /// println!("Point [0.25, 0.25] inside: {}", inside);
292    ///
293    /// // Check a point outside the hull
294    /// assert!(!hull.contains(&[2.0, 2.0]).unwrap());
295    /// ```
296    pub fn contains<T: AsRef<[f64]>>(&self, point: T) -> SpatialResult<bool> {
297        crate::convex_hull::properties::containment::check_point_containment(self, point)
298    }
299
300    /// Get the volume of the convex hull
301    ///
302    /// For 2D hulls, this returns the area. For 3D hulls, this returns the volume.
303    /// For higher dimensions, this returns the hypervolume.
304    ///
305    /// # Returns
306    ///
307    /// * Result containing the volume/area of the convex hull
308    ///
309    /// # Errors
310    ///
311    /// * Returns error if volume computation fails
312    ///
313    /// # Examples
314    ///
315    /// ```
316    /// use scirs2_spatial::convex_hull::ConvexHull;
317    /// use scirs2_core::ndarray::array;
318    ///
319    /// // Create a square with area 1
320    /// let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
321    /// let hull = ConvexHull::new(&points.view()).unwrap();
322    ///
323    /// let area = hull.volume().unwrap();
324    /// assert!((area - 1.0).abs() < 1e-10);
325    /// ```
326    pub fn volume(&self) -> SpatialResult<f64> {
327        crate::convex_hull::properties::volume::compute_volume(self)
328    }
329
330    /// Get the surface area of the convex hull
331    ///
332    /// For 2D hulls, this returns the perimeter. For 3D hulls, this returns the surface area.
333    /// For higher dimensions, this returns the surface hypervolume.
334    ///
335    /// # Returns
336    ///
337    /// * Result containing the surface area/perimeter of the convex hull
338    ///
339    /// # Errors
340    ///
341    /// * Returns error if area computation fails
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use scirs2_spatial::convex_hull::ConvexHull;
347    /// use scirs2_core::ndarray::array;
348    ///
349    /// // Create a 3D cube
350    /// let points = array![
351    ///     [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0],
352    ///     [0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0]
353    /// ];
354    /// let hull = ConvexHull::new(&points.view()).unwrap();
355    ///
356    /// // Surface area of the cube should be 6 square units
357    /// let area = hull.area().unwrap();
358    /// assert!((area - 6.0).abs() < 1e-10);
359    /// ```
360    pub fn area(&self) -> SpatialResult<f64> {
361        crate::convex_hull::properties::surface_area::compute_surface_area(self)
362    }
363
364    /// Extract facet equations from the Qhull instance
365    ///
366    /// # Arguments
367    ///
368    /// * `qh` - Qhull instance
369    /// * `ndim` - Dimensionality of the hull
370    ///
371    /// # Returns
372    ///
373    /// * Option containing an Array2 of shape (n_facets, ndim+1) with the equations of the hull facets
374    ///   or None if equations cannot be extracted
375    pub fn extract_equations(qh: &Qh, ndim: usize) -> Option<Array2<f64>> {
376        // Get facets from qhull
377        let facets: Vec<_> = qh.facets().collect();
378        let n_facets = facets.len();
379
380        // Allocate array for equations
381        let mut equations = Array2::zeros((n_facets, ndim + 1));
382
383        // Extract facet equations
384        for (i, facet) in facets.iter().enumerate() {
385            // Qhull facet equation format: normal coefficients followed by offset
386            // Ax + By + Cz + offset <= 0 for points inside the hull
387            if let Some(normal) = facet.normal() {
388                // Fill in normal coefficients
389                for j in 0..ndim {
390                    equations[[i, j]] = normal[j];
391                }
392
393                // Fill in offset (last column)
394                equations[[i, ndim]] = facet.offset();
395            } else {
396                // If we can't get a facet's equation, we can't provide all equations
397                return None;
398            }
399        }
400
401        Some(equations)
402    }
403}