scirs2_spatial/convex_hull/algorithms/
qhull.rs1use crate::convex_hull::core::ConvexHull;
7use crate::error::{SpatialError, SpatialResult};
8use qhull::Qh;
9use scirs2_core::ndarray::{Array2, ArrayView2};
10
11pub fn compute_qhull(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
41 let npoints = points.nrows();
42 let ndim = points.ncols();
43
44 if ndim == 1 {
46 return handle_special_case_1d(points);
47 }
48
49 if ndim == 2 && npoints <= 4 {
51 return handle_special_case_2d(points);
52 } else if ndim == 3 && npoints <= 4 {
53 return handle_special_case_3d(points);
54 }
55
56 let points_vec: Vec<Vec<f64>> = (0..npoints).map(|i| points.row(i).to_vec()).collect();
58
59 let qh_result = Qh::builder()
61 .compute(true)
62 .triangulate(true)
63 .build_from_iter(points_vec.clone());
64
65 let qh = match qh_result {
67 Ok(qh) => qh,
68 Err(_) => {
69 let mut perturbed_points = vec![];
71 use scirs2_core::random::Rng;
72 let mut rng = scirs2_core::random::rng();
73
74 for i in 0..npoints {
75 let mut pt = points.row(i).to_vec();
76 for val in pt.iter_mut().take(ndim) {
77 *val += rng.gen_range(-0.0001..0.0001);
78 }
79 perturbed_points.push(pt);
80 }
81
82 match Qh::builder()
84 .compute(true)
85 .triangulate(true)
86 .build_from_iter(perturbed_points)
87 {
88 Ok(qh2) => qh2,
89 Err(e) => {
90 if ndim == 2 {
92 return handle_special_case_2d(points);
93 } else if ndim == 3 {
94 return handle_special_case_3d(points);
95 } else {
96 return Err(SpatialError::ComputationError(format!("Qhull error: {e}")));
97 }
98 }
99 }
100 }
101 };
102
103 let (vertex_indices, simplices, equations) = extract_qhull_results(&qh, ndim);
105
106 Ok(ConvexHull {
107 points: points.to_owned(),
108 qh,
109 vertex_indices,
110 simplices,
111 equations,
112 })
113}
114
115fn extract_qhull_results(
126 qh: &Qh,
127 ndim: usize,
128) -> (Vec<usize>, Vec<Vec<usize>>, Option<Array2<f64>>) {
129 let mut vertex_indices: Vec<usize> = qh.vertices().filter_map(|v| v.index(qh)).collect();
131
132 vertex_indices.sort();
134 vertex_indices.dedup();
135
136 let mut simplices: Vec<Vec<usize>> = qh
138 .simplices()
139 .filter_map(|f| {
140 let vertices = f.vertices()?;
141 let mut indices: Vec<usize> = vertices.iter().filter_map(|v| v.index(qh)).collect();
142
143 if !indices.is_empty() && indices.len() == ndim {
145 indices.sort();
146 indices.dedup();
147 Some(indices)
148 } else {
149 None
150 }
151 })
152 .collect();
153
154 if simplices.is_empty() {
156 simplices = generate_fallback_simplices(&vertex_indices, ndim);
157 }
158
159 let equations = ConvexHull::extract_equations(qh, ndim);
161
162 (vertex_indices, simplices, equations)
163}
164
165fn generate_fallback_simplices(vertex_indices: &[usize], ndim: usize) -> Vec<Vec<usize>> {
176 let mut simplices = Vec::new();
177
178 if ndim == 2 && vertex_indices.len() >= 3 {
179 let n = vertex_indices.len();
181 for i in 0..n {
182 let j = (i + 1) % n;
183 simplices.push(vec![vertex_indices[i], vertex_indices[j]]);
184 }
185 } else if ndim == 3 && vertex_indices.len() >= 4 {
186 let n = vertex_indices.len();
188 if n >= 4 {
189 simplices.push(vec![
190 vertex_indices[0],
191 vertex_indices[1],
192 vertex_indices[2],
193 ]);
194 simplices.push(vec![
195 vertex_indices[0],
196 vertex_indices[1],
197 vertex_indices[3],
198 ]);
199 simplices.push(vec![
200 vertex_indices[0],
201 vertex_indices[2],
202 vertex_indices[3],
203 ]);
204 simplices.push(vec![
205 vertex_indices[1],
206 vertex_indices[2],
207 vertex_indices[3],
208 ]);
209 }
210 }
211
212 simplices
213}
214
215fn handle_special_case_1d(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
225 let npoints = points.nrows();
226
227 let mut min_idx = 0;
229 let mut max_idx = 0;
230 let mut min_val = points[[0, 0]];
231 let mut max_val = points[[0, 0]];
232
233 for i in 1..npoints {
234 let val = points[[i, 0]];
235 if val < min_val {
236 min_val = val;
237 min_idx = i;
238 }
239 if val > max_val {
240 max_val = val;
241 max_idx = i;
242 }
243 }
244
245 let vertex_indices = if min_idx != max_idx {
247 vec![min_idx, max_idx]
248 } else {
249 vec![min_idx]
251 };
252
253 let simplices = if vertex_indices.len() == 2 {
255 vec![vec![0, 1]] } else {
257 vec![] };
259
260 let dummy_points = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![0.0, 1.0]];
263
264 let qh = Qh::builder()
265 .compute(false) .build_from_iter(dummy_points)
267 .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
268
269 let equations = None;
271
272 Ok(ConvexHull {
273 points: points.to_owned(),
274 qh,
275 vertex_indices,
276 simplices,
277 equations,
278 })
279}
280
281fn handle_special_case_2d(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
291 let npoints = points.nrows();
292
293 if npoints == 1 {
295 return crate::convex_hull::algorithms::special_cases::handle_degenerate_case(points)
296 .unwrap_or_else(|| {
297 Err(SpatialError::ValueError(
298 "Failed to handle single point".to_string(),
299 ))
300 });
301 }
302
303 if npoints == 2 {
304 return crate::convex_hull::algorithms::special_cases::handle_degenerate_case(points)
305 .unwrap_or_else(|| {
306 Err(SpatialError::ValueError(
307 "Failed to handle two points".to_string(),
308 ))
309 });
310 }
311
312 if npoints == 3 {
314 let vertex_indices = vec![0, 1, 2];
316 let simplices = vec![vec![0, 1], vec![1, 2], vec![2, 0]];
318
319 let points_vec: Vec<Vec<f64>> = (0..npoints).map(|i| points.row(i).to_vec()).collect();
321
322 let qh = Qh::builder()
323 .compute(false) .build_from_iter(points_vec)
325 .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
326
327 let equations = None;
329
330 return Ok(ConvexHull {
331 points: points.to_owned(),
332 qh,
333 vertex_indices,
334 simplices,
335 equations,
336 });
337 }
338
339 if npoints == 4 {
341 let vertex_indices = vec![0, 1, 2, 3];
349
350 let n = vertex_indices.len();
352 let mut simplices = Vec::new();
353 for i in 0..n {
354 let j = (i + 1) % n;
355 simplices.push(vec![vertex_indices[i], vertex_indices[j]]);
356 }
357
358 let points_vec: Vec<Vec<f64>> = (0..npoints).map(|i| points.row(i).to_vec()).collect();
360
361 let qh = Qh::builder()
362 .compute(false) .build_from_iter(points_vec)
364 .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
365
366 let equations = None;
368
369 return Ok(ConvexHull {
370 points: points.to_owned(),
371 qh,
372 vertex_indices,
373 simplices,
374 equations,
375 });
376 }
377
378 Err(SpatialError::ValueError(
380 "Invalid number of points for special case".to_string(),
381 ))
382}
383
384fn handle_special_case_3d(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
394 let npoints = points.nrows();
395
396 if npoints <= 2 {
398 return crate::convex_hull::algorithms::special_cases::handle_degenerate_case(points)
399 .unwrap_or_else(|| {
400 Err(SpatialError::ValueError(
401 "Failed to handle degenerate 3D case".to_string(),
402 ))
403 });
404 }
405
406 if npoints == 4 {
408 let vertex_indices = vec![0, 1, 2, 3];
410 let simplices = vec![vec![0, 1, 2], vec![0, 1, 3], vec![0, 2, 3], vec![1, 2, 3]];
412
413 let points_vec: Vec<Vec<f64>> = (0..npoints).map(|i| points.row(i).to_vec()).collect();
415
416 let qh = Qh::builder()
417 .compute(false) .build_from_iter(points_vec)
419 .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
420
421 let equations = None;
423
424 return Ok(ConvexHull {
425 points: points.to_owned(),
426 qh,
427 vertex_indices,
428 simplices,
429 equations,
430 });
431 }
432
433 Err(SpatialError::ValueError(
435 "Invalid number of points for special case".to_string(),
436 ))
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use scirs2_core::ndarray::arr2;
443
444 #[test]
445 fn test_qhull_2d() {
446 let points = arr2(&[
447 [0.0, 0.0],
448 [1.0, 0.0],
449 [0.0, 1.0],
450 [0.5, 0.5], ]);
452
453 let hull = compute_qhull(&points.view()).unwrap();
454
455 assert_eq!(hull.ndim(), 2);
457
458 let vertex_count = hull.vertex_indices().len();
460 assert!(vertex_count == 3 || vertex_count == 4);
461
462 assert!(!hull.contains([2.0, 2.0]).unwrap());
464 }
465
466 #[test]
467 fn test_qhull_3d() {
468 let points = arr2(&[
469 [0.0, 0.0, 0.0],
470 [1.0, 0.0, 0.0],
471 [0.0, 1.0, 0.0],
472 [0.0, 0.0, 1.0],
473 [0.5, 0.5, 0.5], ]);
475
476 let hull = compute_qhull(&points.view()).unwrap();
477
478 assert_eq!(hull.ndim(), 3);
480
481 assert!(hull.vertex_indices().len() >= 4);
483
484 assert!(hull.contains([0.25, 0.25, 0.25]).unwrap());
486 assert!(!hull.contains([2.0, 2.0, 2.0]).unwrap());
487 }
488
489 #[test]
490 fn test_special_case_2d() {
491 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
493 let hull = handle_special_case_2d(&points.view()).unwrap();
494
495 assert_eq!(hull.ndim(), 2);
496 assert_eq!(hull.vertex_indices().len(), 3);
497 assert_eq!(hull.simplices().len(), 3); }
499
500 #[test]
501 fn test_special_case_3d() {
502 let points = arr2(&[
504 [0.0, 0.0, 0.0],
505 [1.0, 0.0, 0.0],
506 [0.0, 1.0, 0.0],
507 [0.0, 0.0, 1.0],
508 ]);
509 let hull = handle_special_case_3d(&points.view()).unwrap();
510
511 assert_eq!(hull.ndim(), 3);
512 assert_eq!(hull.vertex_indices().len(), 4);
513 assert_eq!(hull.simplices().len(), 4); }
515
516 #[test]
517 fn test_degenerate_hull() {
518 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]]);
520
521 let hull = compute_qhull(&points.view());
522
523 assert!(hull.is_ok());
525
526 let hull = hull.unwrap();
527 assert!(hull.vertex_indices().len() >= 2);
529
530 assert!(!hull.contains([1.5, 0.1]).unwrap());
532 }
533}