scirs2_spatial/convex_hull/algorithms/
special_cases.rs1use crate::convex_hull::core::ConvexHull;
8use crate::error::{SpatialError, SpatialResult};
9use qhull::Qh;
10use scirs2_core::ndarray::ArrayView2;
11
12pub fn handle_degenerate_case(points: &ArrayView2<'_, f64>) -> Option<SpatialResult<ConvexHull>> {
37 let npoints = points.nrows();
38 let ndim = points.ncols();
39
40 if npoints == 1 {
42 return Some(handle_single_point(points));
43 }
44
45 if npoints == 2 {
47 return Some(handle_two_points(points));
48 }
49
50 if is_all_collinear(points) {
52 return Some(handle_collinear_points(points));
53 }
54
55 if has_all_identical_points(points) {
57 return Some(handle_identical_points(points));
58 }
59
60 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 }
72
73fn 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![]; 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
112fn 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]]; 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
151fn 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 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] };
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 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
200fn handle_identical_points(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
210 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
229pub 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; }
262
263 if ndim == 2 {
264 return is_all_collinear_2d(points);
265 }
266
267 is_all_collinear_nd(points)
269}
270
271fn 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 for i in 2..npoints {
284 let p3 = [points[[i, 0]], points[[i, 1]]];
285
286 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
297fn 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 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; }
328
329 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 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 let projection: f64 = point_to_first
347 .iter()
348 .zip(direction.iter())
349 .map(|(a, b)| a * b)
350 .sum();
351
352 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
368pub 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
411fn 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 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 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 let collinear = arr2(&[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]]);
503 assert!(is_all_collinear(&collinear.view()));
504
505 let not_collinear = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
507 assert!(!is_all_collinear(¬_collinear.view()));
508
509 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 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 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 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 assert_eq!(min_idx, 0);
536 assert_eq!(max_idx, 2);
537 }
538
539 #[test]
540 fn test_handle_degenerate_case() {
541 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 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 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 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 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 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(¬_collinear_3d.view()));
574 }
575}