scirs2_spatial/convex_hull/algorithms/
special_cases.rs

1//! Special case handlers for convex hull computation
2//!
3//! This module provides specialized handling for edge cases that may not be
4//! well-handled by the general algorithms, such as very small point sets,
5//! degenerate cases, or highly regular geometries.
6
7use crate::convex_hull::core::ConvexHull;
8use crate::error::{SpatialError, SpatialResult};
9use qhull::Qh;
10use scirs2_core::ndarray::ArrayView2;
11
12/// Handle degenerate cases for convex hull computation
13///
14/// This function detects and handles various degenerate cases that might
15/// cause issues with the standard algorithms.
16///
17/// # Arguments
18///
19/// * `points` - Input points array
20///
21/// # Returns
22///
23/// * Option containing a ConvexHull if this is a special case, None otherwise
24///
25/// # Examples
26///
27/// ```rust
28/// use scirs2_spatial::convex_hull::algorithms::special_cases::handle_degenerate_case;
29/// use scirs2_core::ndarray::array;
30///
31/// // Single point
32/// let points = array![[0.0, 0.0]];
33/// let result = handle_degenerate_case(&points.view());
34/// assert!(result.is_some());
35/// ```
36pub fn handle_degenerate_case(points: &ArrayView2<'_, f64>) -> Option<SpatialResult<ConvexHull>> {
37    let npoints = points.nrows();
38    let ndim = points.ncols();
39
40    // Handle single point
41    if npoints == 1 {
42        return Some(handle_single_point(points));
43    }
44
45    // Handle two points
46    if npoints == 2 {
47        return Some(handle_two_points(points));
48    }
49
50    // Handle collinear points
51    if is_all_collinear(points) {
52        return Some(handle_collinear_points(points));
53    }
54
55    // Handle duplicate points
56    if has_all_identical_points(points) {
57        return Some(handle_identical_points(points));
58    }
59
60    // Check for insufficient points for the dimension
61    if npoints < ndim + 1 {
62        return Some(Err(SpatialError::ValueError(format!(
63            "Need at least {} points to construct a {}D convex hull, got {}",
64            ndim + 1,
65            ndim,
66            npoints
67        ))));
68    }
69
70    None // Not a special case
71}
72
73/// Handle the case of a single point
74///
75/// # Arguments
76///
77/// * `points` - Input points array (should contain exactly 1 point)
78///
79/// # Returns
80///
81/// * Result containing a degenerate ConvexHull
82fn handle_single_point(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
83    let npoints = points.nrows();
84
85    if npoints != 1 {
86        return Err(SpatialError::ValueError(
87            "handle_single_point called with wrong number of points".to_string(),
88        ));
89    }
90
91    let vertex_indices = vec![0];
92    let simplices = vec![]; // No simplices for a single point
93
94    // Create dummy QHull instance
95    // Qhull requires at least 3 points in 2D, so create a dummy triangle
96    let dummy_points = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![0.0, 1.0]];
97
98    let qh = Qh::builder()
99        .compute(false)
100        .build_from_iter(dummy_points)
101        .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
102
103    Ok(ConvexHull {
104        points: points.to_owned(),
105        qh,
106        vertex_indices,
107        simplices,
108        equations: None,
109    })
110}
111
112/// Handle the case of exactly two points
113///
114/// # Arguments
115///
116/// * `points` - Input points array (should contain exactly 2 points)
117///
118/// # Returns
119///
120/// * Result containing a degenerate ConvexHull (line segment)
121fn handle_two_points(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
122    let npoints = points.nrows();
123
124    if npoints != 2 {
125        return Err(SpatialError::ValueError(
126            "handle_two_points called with wrong number of points".to_string(),
127        ));
128    }
129
130    let vertex_indices = vec![0, 1];
131    let simplices = vec![vec![0, 1]]; // Single line segment
132
133    // Create dummy QHull instance
134    // Qhull requires at least 3 points in 2D, so create a dummy triangle
135    let dummy_points = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![0.0, 1.0]];
136
137    let qh = Qh::builder()
138        .compute(false)
139        .build_from_iter(dummy_points)
140        .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
141
142    Ok(ConvexHull {
143        points: points.to_owned(),
144        qh,
145        vertex_indices,
146        simplices,
147        equations: None,
148    })
149}
150
151/// Handle the case of all points being collinear
152///
153/// # Arguments
154///
155/// * `points` - Input points array (all points should be collinear)
156///
157/// # Returns
158///
159/// * Result containing a degenerate ConvexHull (line segment)
160fn handle_collinear_points(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
161    let npoints = points.nrows();
162
163    if npoints < 2 {
164        return Err(SpatialError::ValueError(
165            "Need at least 2 points for collinear case".to_string(),
166        ));
167    }
168
169    // Find the two extreme points along the line
170    let (min_idx, max_idx) = find_extreme_points_on_line(points)?;
171
172    let vertex_indices = if min_idx != max_idx {
173        vec![min_idx, max_idx]
174    } else {
175        vec![min_idx] // All points are identical
176    };
177
178    let simplices = if vertex_indices.len() == 2 {
179        vec![vec![vertex_indices[0], vertex_indices[1]]]
180    } else {
181        vec![]
182    };
183
184    // Create dummy QHull instance
185    let points_vec: Vec<Vec<f64>> = (0..npoints).map(|i| points.row(i).to_vec()).collect();
186    let qh = Qh::builder()
187        .compute(false)
188        .build_from_iter(points_vec)
189        .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
190
191    Ok(ConvexHull {
192        points: points.to_owned(),
193        qh,
194        vertex_indices,
195        simplices,
196        equations: None,
197    })
198}
199
200/// Handle the case where all points are identical
201///
202/// # Arguments
203///
204/// * `points` - Input points array (all points should be identical)
205///
206/// # Returns
207///
208/// * Result containing a degenerate ConvexHull (single point)
209fn handle_identical_points(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
210    // All points are the same, so the hull is just one point
211    let vertex_indices = vec![0];
212    let simplices = vec![];
213
214    let points_vec: Vec<Vec<f64>> = vec![points.row(0).to_vec()];
215    let qh = Qh::builder()
216        .compute(false)
217        .build_from_iter(points_vec)
218        .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
219
220    Ok(ConvexHull {
221        points: points.to_owned(),
222        qh,
223        vertex_indices,
224        simplices,
225        equations: None,
226    })
227}
228
229/// Check if all points are collinear
230///
231/// # Arguments
232///
233/// * `points` - Input points array
234///
235/// # Returns
236///
237/// * true if all points lie on the same line, false otherwise
238///
239/// # Examples
240///
241/// ```rust
242/// use scirs2_spatial::convex_hull::algorithms::special_cases::is_all_collinear;
243/// use scirs2_core::ndarray::array;
244///
245/// let collinear = array![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
246/// assert!(is_all_collinear(&collinear.view()));
247///
248/// let not_collinear = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
249/// assert!(!is_all_collinear(&not_collinear.view()));
250/// ```
251pub fn is_all_collinear(points: &ArrayView2<'_, f64>) -> bool {
252    let npoints = points.nrows();
253    let ndim = points.ncols();
254
255    if npoints <= 2 {
256        return true;
257    }
258
259    if ndim == 1 {
260        return true; // 1D points are always "collinear"
261    }
262
263    if ndim == 2 {
264        return is_all_collinear_2d(points);
265    }
266
267    // For higher dimensions, use more general approach
268    is_all_collinear_nd(points)
269}
270
271/// Check if all 2D points are collinear
272fn is_all_collinear_2d(points: &ArrayView2<'_, f64>) -> bool {
273    let npoints = points.nrows();
274
275    if npoints <= 2 {
276        return true;
277    }
278
279    let p1 = [points[[0, 0]], points[[0, 1]]];
280    let p2 = [points[[1, 0]], points[[1, 1]]];
281
282    // Check if all subsequent points are collinear with the first two
283    for i in 2..npoints {
284        let p3 = [points[[i, 0]], points[[i, 1]]];
285
286        // Use cross product to check collinearity
287        let cross = (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0]);
288
289        if cross.abs() > 1e-10 {
290            return false;
291        }
292    }
293
294    true
295}
296
297/// Check if all n-dimensional points are collinear
298fn is_all_collinear_nd(points: &ArrayView2<'_, f64>) -> bool {
299    let npoints = points.nrows();
300    let ndim = points.ncols();
301
302    if npoints <= 2 {
303        return true;
304    }
305
306    // Find direction vector from first two distinct points
307    let mut direction_found = false;
308    let mut direction = vec![0.0; ndim];
309
310    for i in 1..npoints {
311        let mut is_different = false;
312        for d in 0..ndim {
313            direction[d] = points[[i, d]] - points[[0, d]];
314            if direction[d].abs() > 1e-10 {
315                is_different = true;
316            }
317        }
318
319        if is_different {
320            direction_found = true;
321            break;
322        }
323    }
324
325    if !direction_found {
326        return true; // All points are identical
327    }
328
329    // Normalize direction vector
330    let length = (direction.iter().map(|x| x * x).sum::<f64>()).sqrt();
331    if length < 1e-10 {
332        return true;
333    }
334    for d in 0..ndim {
335        direction[d] /= length;
336    }
337
338    // Check if all points lie on the line defined by the first point and direction
339    for i in 2..npoints {
340        let mut point_to_first = vec![0.0; ndim];
341        for d in 0..ndim {
342            point_to_first[d] = points[[i, d]] - points[[0, d]];
343        }
344
345        // Project point_to_first onto direction
346        let projection: f64 = point_to_first
347            .iter()
348            .zip(direction.iter())
349            .map(|(a, b)| a * b)
350            .sum();
351
352        // Check if the projection fully explains the vector
353        let mut residual = 0.0;
354        for d in 0..ndim {
355            let projected_component = projection * direction[d];
356            let residual_component = point_to_first[d] - projected_component;
357            residual += residual_component * residual_component;
358        }
359
360        if residual.sqrt() > 1e-10 {
361            return false;
362        }
363    }
364
365    true
366}
367
368/// Check if all points are identical
369///
370/// # Arguments
371///
372/// * `points` - Input points array
373///
374/// # Returns
375///
376/// * true if all points are identical, false otherwise
377///
378/// # Examples
379///
380/// ```rust
381/// use scirs2_spatial::convex_hull::algorithms::special_cases::has_all_identical_points;
382/// use scirs2_core::ndarray::array;
383///
384/// let identical = array![[1.0, 2.0], [1.0, 2.0], [1.0, 2.0]];
385/// assert!(has_all_identical_points(&identical.view()));
386///
387/// let different = array![[1.0, 2.0], [1.0, 2.0], [1.0, 2.1]];
388/// assert!(!has_all_identical_points(&different.view()));
389/// ```
390pub fn has_all_identical_points(points: &ArrayView2<'_, f64>) -> bool {
391    let npoints = points.nrows();
392    let ndim = points.ncols();
393
394    if npoints <= 1 {
395        return true;
396    }
397
398    let first_point = points.row(0);
399
400    for i in 1..npoints {
401        for d in 0..ndim {
402            if (points[[i, d]] - first_point[d]).abs() > 1e-10 {
403                return false;
404            }
405        }
406    }
407
408    true
409}
410
411/// Find the two extreme points along a line for collinear points
412///
413/// # Arguments
414///
415/// * `points` - Input points array (should be collinear)
416///
417/// # Returns
418///
419/// * Tuple of (min_index, max_index) representing the endpoints of the line segment
420fn find_extreme_points_on_line(points: &ArrayView2<'_, f64>) -> SpatialResult<(usize, usize)> {
421    let npoints = points.nrows();
422    let ndim = points.ncols();
423
424    if npoints == 0 {
425        return Err(SpatialError::ValueError("Empty point set".to_string()));
426    }
427
428    if npoints == 1 {
429        return Ok((0, 0));
430    }
431
432    // Find the dimension with maximum spread
433    let mut max_spread = 0.0;
434    let mut spread_dim = 0;
435
436    for d in 0..ndim {
437        let mut min_val = points[[0, d]];
438        let mut max_val = points[[0, d]];
439
440        for i in 1..npoints {
441            let val = points[[i, d]];
442            min_val = min_val.min(val);
443            max_val = max_val.max(val);
444        }
445
446        let spread = max_val - min_val;
447        if spread > max_spread {
448            max_spread = spread;
449            spread_dim = d;
450        }
451    }
452
453    // Find points with minimum and maximum values in the spread dimension
454    let mut min_idx = 0;
455    let mut max_idx = 0;
456    let mut min_val = points[[0, spread_dim]];
457    let mut max_val = points[[0, spread_dim]];
458
459    for i in 1..npoints {
460        let val = points[[i, spread_dim]];
461        if val < min_val {
462            min_val = val;
463            min_idx = i;
464        }
465        if val > max_val {
466            max_val = val;
467            max_idx = i;
468        }
469    }
470
471    Ok((min_idx, max_idx))
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use scirs2_core::ndarray::arr2;
478
479    #[test]
480    fn test_single_point() {
481        let points = arr2(&[[1.0, 2.0]]);
482        let hull = handle_single_point(&points.view()).unwrap();
483
484        assert_eq!(hull.vertex_indices().len(), 1);
485        assert_eq!(hull.simplices().len(), 0);
486        assert_eq!(hull.vertex_indices()[0], 0);
487    }
488
489    #[test]
490    fn test_two_points() {
491        let points = arr2(&[[0.0, 0.0], [1.0, 1.0]]);
492        let hull = handle_two_points(&points.view()).unwrap();
493
494        assert_eq!(hull.vertex_indices().len(), 2);
495        assert_eq!(hull.simplices().len(), 1);
496        assert_eq!(hull.simplices()[0], vec![0, 1]);
497    }
498
499    #[test]
500    fn test_is_all_collinear_2d() {
501        // Collinear points
502        let collinear = arr2(&[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]]);
503        assert!(is_all_collinear(&collinear.view()));
504
505        // Non-collinear points
506        let not_collinear = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
507        assert!(!is_all_collinear(&not_collinear.view()));
508
509        // Two points (always collinear)
510        let two_points = arr2(&[[0.0, 0.0], [1.0, 1.0]]);
511        assert!(is_all_collinear(&two_points.view()));
512    }
513
514    #[test]
515    fn test_has_all_identical_points() {
516        // Identical points
517        let identical = arr2(&[[1.0, 2.0], [1.0, 2.0], [1.0, 2.0]]);
518        assert!(has_all_identical_points(&identical.view()));
519
520        // Different points
521        let different = arr2(&[[1.0, 2.0], [1.0, 2.0], [1.0, 2.1]]);
522        assert!(!has_all_identical_points(&different.view()));
523
524        // Single point
525        let single = arr2(&[[1.0, 2.0]]);
526        assert!(has_all_identical_points(&single.view()));
527    }
528
529    #[test]
530    fn test_find_extreme_points_on_line() {
531        let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [1.5, 0.0]]);
532        let (min_idx, max_idx) = find_extreme_points_on_line(&points.view()).unwrap();
533
534        // Should find points [0.0, 0.0] and [2.0, 0.0] as extremes
535        assert_eq!(min_idx, 0);
536        assert_eq!(max_idx, 2);
537    }
538
539    #[test]
540    fn test_handle_degenerate_case() {
541        // Single point case
542        let single = arr2(&[[1.0, 2.0]]);
543        let result = handle_degenerate_case(&single.view());
544        assert!(result.is_some());
545        assert!(result.unwrap().is_ok());
546
547        // Two points case
548        let two = arr2(&[[0.0, 0.0], [1.0, 1.0]]);
549        let result = handle_degenerate_case(&two.view());
550        assert!(result.is_some());
551        assert!(result.unwrap().is_ok());
552
553        // Collinear case
554        let collinear = arr2(&[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]]);
555        let result = handle_degenerate_case(&collinear.view());
556        assert!(result.is_some());
557        assert!(result.unwrap().is_ok());
558
559        // Normal case (not degenerate)
560        let normal = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
561        let result = handle_degenerate_case(&normal.view());
562        assert!(result.is_none());
563    }
564
565    #[test]
566    fn test_is_all_collinear_3d() {
567        // 3D collinear points
568        let collinear_3d = arr2(&[[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]);
569        assert!(is_all_collinear(&collinear_3d.view()));
570
571        // 3D non-collinear points
572        let not_collinear_3d = arr2(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
573        assert!(!is_all_collinear(&not_collinear_3d.view()));
574    }
575}