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}