1use crate::convex_hull::ConvexHull;
46use crate::error::{SpatialError, SpatialResult};
47use scirs2_core::ndarray::{arr1, Array1, Array2, ArrayView1};
48use statrs::statistics::Statistics;
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct Halfspace {
53 normal: Array1<f64>,
55 offset: f64,
57}
58
59impl Halfspace {
60 pub fn new(normal: Array1<f64>, offset: f64) -> Self {
81 Self { normal, offset }
82 }
83
84 pub fn normal(&self) -> &Array1<f64> {
86 &self.normal
87 }
88
89 pub fn offset(&self) -> f64 {
91 self.offset
92 }
93
94 pub fn dim(&self) -> usize {
96 self.normal.len()
97 }
98
99 pub fn contains(&self, point: &ArrayView1<f64>) -> bool {
109 if point.len() != self.normal.len() {
110 return false;
111 }
112
113 let dot_product: f64 = self
114 .normal
115 .iter()
116 .zip(point.iter())
117 .map(|(a, x)| a * x)
118 .sum();
119 dot_product <= self.offset + 1e-10 }
121
122 pub fn distance(&self, point: &ArrayView1<f64>) -> SpatialResult<f64> {
132 if point.len() != self.normal.len() {
133 return Err(SpatialError::ValueError(
134 "Point dimension must match halfspace dimension".to_string(),
135 ));
136 }
137
138 let dot_product: f64 = self
139 .normal
140 .iter()
141 .zip(point.iter())
142 .map(|(a, x)| a * x)
143 .sum();
144 let normal_norm = (self.normal.iter().map(|x| x * x).sum::<f64>()).sqrt();
145
146 if normal_norm < 1e-15 {
147 return Err(SpatialError::ValueError(
148 "Halfspace normal vector cannot be zero".to_string(),
149 ));
150 }
151
152 Ok((dot_product - self.offset) / normal_norm)
153 }
154
155 pub fn normalize(&self) -> SpatialResult<Self> {
161 let normal_norm = (self.normal.iter().map(|x| x * x).sum::<f64>()).sqrt();
162
163 if normal_norm < 1e-15 {
164 return Err(SpatialError::ValueError(
165 "Cannot normalize halfspace with zero normal vector".to_string(),
166 ));
167 }
168
169 Ok(Self {
170 normal: &self.normal / normal_norm,
171 offset: self.offset / normal_norm,
172 })
173 }
174}
175
176#[derive(Debug, Clone)]
178pub struct HalfspaceIntersection {
179 halfspaces: Vec<Halfspace>,
181 vertices: Array2<f64>,
183 faces: Vec<Vec<usize>>,
185 dim: usize,
187 is_bounded: bool,
189 #[allow(dead_code)]
191 interior_point: Option<Array1<f64>>,
192}
193
194impl HalfspaceIntersection {
195 pub fn new(
223 halfspaces: &[Halfspace],
224 interior_point: Option<Array1<f64>>,
225 ) -> SpatialResult<Self> {
226 if halfspaces.is_empty() {
227 return Err(SpatialError::ValueError(
228 "At least one halfspace is required".to_string(),
229 ));
230 }
231
232 let dim = halfspaces[0].dim();
233 if halfspaces.iter().any(|hs| hs.dim() != dim) {
234 return Err(SpatialError::ValueError(
235 "All halfspaces must have the same dimension".to_string(),
236 ));
237 }
238
239 if dim < 2 {
240 return Err(SpatialError::ValueError(
241 "Halfspace intersection requires at least 2D".to_string(),
242 ));
243 }
244
245 if let Some(ref point) = interior_point {
247 if point.len() != dim {
248 return Err(SpatialError::ValueError(
249 "Interior point dimension must match halfspace dimension".to_string(),
250 ));
251 }
252
253 for hs in halfspaces {
255 if !hs.contains(&point.view()) {
256 return Err(SpatialError::ValueError(
257 "Provided point is not in the interior of all halfspaces".to_string(),
258 ));
259 }
260 }
261 }
262
263 let (vertices, faces, is_bounded) =
265 if interior_point.is_some() || Self::is_likely_bounded(halfspaces) {
266 Self::compute_bounded_intersection(halfspaces, interior_point.as_ref())?
267 } else {
268 Self::compute_unbounded_intersection(halfspaces)?
269 };
270
271 Ok(HalfspaceIntersection {
272 halfspaces: halfspaces.to_vec(),
273 vertices,
274 faces,
275 dim,
276 is_bounded,
277 interior_point,
278 })
279 }
280
281 fn is_likely_bounded(halfspaces: &[Halfspace]) -> bool {
283 let dim = halfspaces[0].dim();
284
285 let mut positive_count = vec![0; dim];
287 let mut negative_count = vec![0; dim];
288
289 for hs in halfspaces {
290 for (i, &val) in hs.normal.iter().enumerate() {
291 if val > 1e-10 {
292 positive_count[i] += 1;
293 } else if val < -1e-10 {
294 negative_count[i] += 1;
295 }
296 }
297 }
298
299 positive_count
302 .iter()
303 .zip(negative_count.iter())
304 .all(|(&pos, &neg)| pos > 0 && neg > 0)
305 }
306
307 fn compute_2d_intersection(
309 halfspaces: &[Halfspace],
310 ) -> SpatialResult<(Array2<f64>, Vec<Vec<usize>>, bool)> {
311 let mut vertices = Vec::new();
312 let n = halfspaces.len();
313
314 for i in 0..n {
316 for j in (i + 1)..n {
317 let hs1 = &halfspaces[i];
318 let hs2 = &halfspaces[j];
319
320 let det = hs1.normal[0] * hs2.normal[1] - hs1.normal[1] * hs2.normal[0];
322
323 if det.abs() < 1e-15 {
324 continue; }
326
327 let x = (hs2.normal[1] * hs1.offset - hs1.normal[1] * hs2.offset) / det;
328 let y = (hs1.normal[0] * hs2.offset - hs2.normal[0] * hs1.offset) / det;
329
330 let candidate = arr1(&[x, y]);
331
332 let mut is_vertex = true;
334 for (k, hs_k) in halfspaces.iter().enumerate() {
335 if k == i || k == j {
336 continue;
337 }
338 if !hs_k.contains(&candidate.view()) {
339 is_vertex = false;
340 break;
341 }
342 }
343
344 if is_vertex {
345 vertices.push((x, y));
346 }
347 }
348 }
349
350 if vertices.is_empty() {
351 return Err(SpatialError::ComputationError(
352 "No vertices found in intersection".to_string(),
353 ));
354 }
355
356 vertices.sort_by(|a, b| {
358 a.0.partial_cmp(&b.0)
359 .unwrap_or(std::cmp::Ordering::Equal)
360 .then_with(|| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
361 });
362 vertices.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-10 && (a.1 - b.1).abs() < 1e-10);
363
364 let center_x = vertices.iter().map(|v| v.0).sum::<f64>() / vertices.len() as f64;
366 let center_y = vertices.iter().map(|v| v.1).sum::<f64>() / vertices.len() as f64;
367
368 vertices.sort_by(|a, b| {
369 let angle_a = (a.1 - center_y).atan2(a.0 - center_x);
370 let angle_b = (b.1 - center_y).atan2(b.0 - center_x);
371 angle_a
372 .partial_cmp(&angle_b)
373 .unwrap_or(std::cmp::Ordering::Equal)
374 });
375
376 let vertex_array = Array2::from_shape_vec(
378 (vertices.len(), 2),
379 vertices.iter().flat_map(|&(x, y)| vec![x, y]).collect(),
380 )
381 .map_err(|_| SpatialError::ComputationError("Failed to create vertex array".to_string()))?;
382
383 let faces = if vertices.len() >= 3 {
385 vec![(0..vertices.len()).collect()]
386 } else {
387 vec![]
388 };
389
390 Ok((vertex_array, faces, true))
391 }
392
393 fn compute_bounded_intersection(
395 halfspaces: &[Halfspace],
396 interior_point: Option<&Array1<f64>>,
397 ) -> SpatialResult<(Array2<f64>, Vec<Vec<usize>>, bool)> {
398 let dim = halfspaces[0].dim();
399
400 if dim == 2 {
402 return Self::compute_2d_intersection(halfspaces);
403 }
404
405 let interior = if let Some(point) = interior_point {
407 point.clone()
408 } else {
409 Self::find_interior_point(halfspaces)?
410 };
411
412 let mut dual_points = Vec::new();
414
415 for hs in halfspaces {
416 let denominator = hs.offset - hs.normal.dot(&interior);
419
420 if denominator.abs() < 1e-15 {
421 continue;
423 }
424
425 if denominator < 0.0 {
426 return Err(SpatialError::ComputationError(
427 "Interior point violates halfspace constraint".to_string(),
428 ));
429 }
430
431 let dual_point: Vec<f64> = hs.normal.iter().map(|&a| a / denominator).collect();
432 dual_points.push(dual_point);
433 }
434
435 if dual_points.len() < dim + 1 {
436 return Err(SpatialError::ComputationError(
437 "Insufficient halfspaces to form bounded polytope".to_string(),
438 ));
439 }
440
441 let dual_array = Array2::from_shape_vec(
443 (dual_points.len(), dim),
444 dual_points.into_iter().flatten().collect(),
445 )
446 .map_err(|_| {
447 SpatialError::ComputationError("Failed to create dual points array".to_string())
448 })?;
449
450 let hull = ConvexHull::new(&dual_array.view())?;
452
453 let hull_vertices = hull.vertex_indices();
455 let mut primal_vertices = Vec::new();
456
457 for &vertex_idx in hull_vertices {
458 let dual_vertex = dual_array.row(vertex_idx);
459
460 let p_norm_sq: f64 = dual_vertex.iter().map(|x| x * x).sum();
463
464 if p_norm_sq < 1e-15 {
465 continue; }
467
468 let primal_vertex: Vec<f64> = interior
469 .iter()
470 .zip(dual_vertex.iter())
471 .map(|(&interior_i, &p_i)| interior_i + p_i / p_norm_sq)
472 .collect();
473
474 primal_vertices.push(primal_vertex);
475 }
476
477 if primal_vertices.is_empty() {
478 return Err(SpatialError::ComputationError(
479 "No valid vertices found in intersection".to_string(),
480 ));
481 }
482
483 let vertices = Array2::from_shape_vec(
485 (primal_vertices.len(), dim),
486 primal_vertices.into_iter().flatten().collect(),
487 )
488 .map_err(|_| {
489 SpatialError::ComputationError("Failed to create vertices array".to_string())
490 })?;
491
492 let faces = Self::extract_faces_from_hull(&hull)?;
494
495 Ok((vertices, faces, true))
496 }
497
498 fn compute_unbounded_intersection(
500 halfspaces: &[Halfspace],
501 ) -> SpatialResult<(Array2<f64>, Vec<Vec<usize>>, bool)> {
502 let dim = halfspaces[0].dim();
503
504 let vertices = Self::find_intersection_vertices(halfspaces)?;
507
508 if vertices.nrows() == 0 {
509 return Err(SpatialError::ComputationError(
510 "No intersection vertices found".to_string(),
511 ));
512 }
513
514 let is_bounded = Self::check_boundedness(&vertices, halfspaces)?;
516
517 let faces = if vertices.nrows() > dim {
519 let hull = ConvexHull::new(&vertices.view())?;
521 Self::extract_faces_from_hull(&hull)?
522 } else {
523 vec![(0..vertices.nrows()).collect()]
525 };
526
527 Ok((vertices, faces, is_bounded))
528 }
529
530 fn find_interior_point(halfspaces: &[Halfspace]) -> SpatialResult<Array1<f64>> {
532 let dim = halfspaces[0].dim();
533
534 let candidates = vec![
536 Array1::from_elem(dim, 0.1), Array1::from_elem(dim, 0.01), Array1::from_elem(dim, 0.5), Array1::from_elem(dim, 0.3333), Array1::from_elem(dim, 0.25), Array1::zeros(dim), ];
543
544 for candidate in candidates {
545 let mut is_strictly_interior = true;
547 for hs in halfspaces {
548 let dot_product = hs.normal.dot(&candidate);
549 if dot_product >= hs.offset - 1e-10 {
550 is_strictly_interior = false;
552 break;
553 }
554 }
555
556 if is_strictly_interior {
557 return Ok(candidate);
558 }
559 }
560
561 if dim == 2 && halfspaces.len() >= 3 {
566 let hs1 = &halfspaces[0];
568 let hs2 = &halfspaces[1];
569
570 let det = hs1.normal[0] * hs2.normal[1] - hs1.normal[1] * hs2.normal[0];
572
573 if det.abs() > 1e-10 {
574 let x = (hs2.normal[1] * hs1.offset - hs1.normal[1] * hs2.offset) / det;
575 let y = (hs1.normal[0] * hs2.offset - hs2.normal[0] * hs1.offset) / det;
576
577 let candidate = arr1(&[x, y]);
578
579 if halfspaces.iter().all(|hs| hs.contains(&candidate.view())) {
581 return Ok(candidate);
582 }
583
584 let mut min_slack = f64::INFINITY;
587 let mut worst_constraint_idx = 0;
588
589 for (i, hs) in halfspaces.iter().enumerate() {
590 let slack = hs.offset - hs.normal.dot(&candidate);
591 if slack < min_slack {
592 min_slack = slack;
593 worst_constraint_idx = i;
594 }
595 }
596
597 if min_slack >= -1e-10 {
598 let shift_direction = &halfspaces[worst_constraint_idx].normal * (-0.1);
600 let shifted_candidate = &candidate + &shift_direction;
601
602 if halfspaces
603 .iter()
604 .all(|hs| hs.contains(&shifted_candidate.view()))
605 {
606 return Ok(shifted_candidate);
607 }
608 }
609 }
610 }
611
612 Err(SpatialError::ComputationError(
613 "Cannot find feasible interior point".to_string(),
614 ))
615 }
616
617 fn find_intersection_vertices(halfspaces: &[Halfspace]) -> SpatialResult<Array2<f64>> {
619 let dim = halfspaces[0].dim();
620 let n = halfspaces.len();
621
622 if n < dim {
623 return Err(SpatialError::ComputationError(
624 "Need at least d halfspaces to find intersection vertices in d dimensions"
625 .to_string(),
626 ));
627 }
628
629 let mut vertices = Vec::new();
630
631 let combinations = Self::generate_combinations(n, dim);
633
634 for combo in combinations {
635 if let Ok(vertex) = Self::solve_intersection_system(halfspaces, &combo) {
636 if halfspaces.iter().all(|hs| hs.contains(&vertex.view())) {
638 vertices.push(vertex.to_vec());
639 }
640 }
641 }
642
643 vertices.sort_by(|a, b| {
645 for (x, y) in a.iter().zip(b.iter()) {
646 match x.partial_cmp(y) {
647 Some(std::cmp::Ordering::Equal) => continue,
648 Some(order) => return order,
649 None => return std::cmp::Ordering::Equal,
650 }
651 }
652 std::cmp::Ordering::Equal
653 });
654 vertices.dedup_by(|a, b| a.iter().zip(b.iter()).all(|(x, y)| (x - y).abs() < 1e-10));
655
656 if vertices.is_empty() {
657 return Ok(Array2::zeros((0, dim)));
658 }
659
660 Array2::from_shape_vec(
661 (vertices.len(), dim),
662 vertices.into_iter().flatten().collect(),
663 )
664 .map_err(|_| SpatialError::ComputationError("Failed to create vertices array".to_string()))
665 }
666
667 fn generate_combinations(n: usize, k: usize) -> Vec<Vec<usize>> {
669 if k > n {
670 return vec![];
671 }
672
673 if k == 0 {
674 return vec![vec![]];
675 }
676
677 if k == 1 {
678 return (0..n).map(|i| vec![i]).collect();
679 }
680
681 let mut result = Vec::new();
682
683 fn backtrack(
684 start: usize,
685 n: usize,
686 k: usize,
687 current: &mut Vec<usize>,
688 result: &mut Vec<Vec<usize>>,
689 ) {
690 if current.len() == k {
691 result.push(current.clone());
692 return;
693 }
694
695 for i in start..n {
696 current.push(i);
697 backtrack(i + 1, n, k, current, result);
698 current.pop();
699 }
700 }
701
702 let mut current = Vec::new();
703 backtrack(0, n, k, &mut current, &mut result);
704 result
705 }
706
707 fn solve_intersection_system(
709 halfspaces: &[Halfspace],
710 indices: &[usize],
711 ) -> SpatialResult<Array1<f64>> {
712 let dim = halfspaces[0].dim();
713
714 if indices.len() != dim {
715 return Err(SpatialError::ValueError(
716 "Need exactly d halfspaces to solve for d-dimensional intersection".to_string(),
717 ));
718 }
719
720 let mut matrix_data = Vec::with_capacity(dim * dim);
722 let mut rhs = Vec::with_capacity(dim);
723
724 for &idx in indices {
725 let hs = &halfspaces[idx];
726 matrix_data.extend(hs.normal.iter());
727 rhs.push(hs.offset);
728 }
729
730 Self::solve_linear_system(&matrix_data, &rhs, dim)
732 }
733
734 fn solve_linear_system(
736 matrix_data: &[f64],
737 rhs: &[f64],
738 n: usize,
739 ) -> SpatialResult<Array1<f64>> {
740 let mut aug_matrix = vec![vec![0.0; n + 1]; n];
741
742 for i in 0..n {
744 for j in 0..n {
745 aug_matrix[i][j] = matrix_data[i * n + j];
746 }
747 aug_matrix[i][n] = rhs[i];
748 }
749
750 for i in 0..n {
752 let mut max_row = i;
754 for k in (i + 1)..n {
755 if aug_matrix[k][i].abs() > aug_matrix[max_row][i].abs() {
756 max_row = k;
757 }
758 }
759
760 aug_matrix.swap(i, max_row);
762
763 if aug_matrix[i][i].abs() < 1e-15 {
765 return Err(SpatialError::ComputationError(
766 "Singular matrix in intersection computation".to_string(),
767 ));
768 }
769
770 for k in (i + 1)..n {
772 let factor = aug_matrix[k][i] / aug_matrix[i][i];
773 for j in i..(n + 1) {
774 aug_matrix[k][j] -= factor * aug_matrix[i][j];
775 }
776 }
777 }
778
779 let mut solution = vec![0.0; n];
781 for i in (0..n).rev() {
782 solution[i] = aug_matrix[i][n];
783 for j in (i + 1)..n {
784 solution[i] -= aug_matrix[i][j] * solution[j];
785 }
786 solution[i] /= aug_matrix[i][i];
787 }
788
789 Ok(Array1::from(solution))
790 }
791
792 fn check_boundedness(vertices: &Array2<f64>, halfspaces: &[Halfspace]) -> SpatialResult<bool> {
794 if vertices.nrows() == 0 {
795 return Ok(false);
796 }
797
798 let max_coord = vertices
800 .iter()
801 .map(|&x| x.abs())
802 .fold(0.0f64, |acc, x| acc.max(x));
803
804 Ok(max_coord.is_finite() && max_coord < 1e10)
805 }
806
807 fn extract_faces_from_hull(hull: &ConvexHull) -> SpatialResult<Vec<Vec<usize>>> {
809 let vertices = hull.vertex_indices();
812 if vertices.len() < 3 {
813 Ok(vec![vertices.to_vec()])
814 } else {
815 let mut faces = Vec::new();
817 for i in 1..(vertices.len() - 1) {
818 faces.push(vec![vertices[0], vertices[i], vertices[i + 1]]);
819 }
820 Ok(faces)
821 }
822 }
823
824 pub fn vertices(&self) -> &Array2<f64> {
826 &self.vertices
827 }
828
829 pub fn faces(&self) -> &[Vec<usize>] {
831 &self.faces
832 }
833
834 pub fn dim(&self) -> usize {
836 self.dim
837 }
838
839 pub fn is_bounded(&self) -> bool {
841 self.is_bounded
842 }
843
844 pub fn num_vertices(&self) -> usize {
846 self.vertices.nrows()
847 }
848
849 pub fn num_faces(&self) -> usize {
851 self.faces.len()
852 }
853
854 pub fn is_feasible(&self) -> bool {
856 self.vertices.nrows() > 0
857 }
858
859 pub fn halfspaces(&self) -> &[Halfspace] {
861 &self.halfspaces
862 }
863
864 pub fn volume(&self) -> SpatialResult<f64> {
866 if !self.is_bounded {
867 return Err(SpatialError::ComputationError(
868 "Cannot compute volume of unbounded polytope".to_string(),
869 ));
870 }
871
872 if self.vertices.nrows() == 0 {
873 return Ok(0.0);
874 }
875
876 match self.dim {
877 2 => self.compute_polygon_area(),
878 3 => self.compute_polyhedron_volume(),
879 _ => self.compute_high_dim_volume(),
880 }
881 }
882
883 fn compute_polygon_area(&self) -> SpatialResult<f64> {
885 let vertices = &self.vertices;
886 let n = vertices.nrows();
887
888 if n < 3 {
889 return Ok(0.0);
890 }
891
892 let center_x = vertices.column(0).to_owned().mean();
894 let center_y = vertices.column(1).to_owned().mean();
895
896 let mut vertex_angles: Vec<(usize, f64)> = (0..n)
897 .map(|i| {
898 let dx = vertices[[i, 0]] - center_x;
899 let dy = vertices[[i, 1]] - center_y;
900 (i, dy.atan2(dx))
901 })
902 .collect();
903
904 vertex_angles.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
905
906 let mut area = 0.0;
908 for i in 0..n {
909 let curr = vertex_angles[i].0;
910 let next = vertex_angles[(i + 1) % n].0;
911 area += vertices[[curr, 0]] * vertices[[next, 1]];
912 area -= vertices[[next, 0]] * vertices[[curr, 1]];
913 }
914
915 Ok(area.abs() / 2.0)
916 }
917
918 fn compute_polyhedron_volume(&self) -> SpatialResult<f64> {
920 if self.vertices.nrows() < 4 {
921 return Ok(0.0);
922 }
923
924 let hull = ConvexHull::new(&self.vertices.view())?;
926 let hull_vertices = hull.vertices();
927
928 if hull_vertices.len() < 4 {
929 return Ok(0.0);
930 }
931
932 let reference = self.vertices.row(0);
934 let mut total_volume = 0.0;
935
936 for face in &self.faces {
938 if face.len() >= 3 {
939 let v1 = self.vertices.row(face[0]);
940 let v2 = self.vertices.row(face[1]);
941 let v3 = self.vertices.row(face[2]);
942
943 let a = [
945 v1[0] - reference[0],
946 v1[1] - reference[1],
947 v1[2] - reference[2],
948 ];
949 let b = [
950 v2[0] - reference[0],
951 v2[1] - reference[1],
952 v2[2] - reference[2],
953 ];
954 let c = [
955 v3[0] - reference[0],
956 v3[1] - reference[1],
957 v3[2] - reference[2],
958 ];
959
960 let volume = (a[0] * (b[1] * c[2] - b[2] * c[1])
961 - a[1] * (b[0] * c[2] - b[2] * c[0])
962 + a[2] * (b[0] * c[1] - b[1] * c[0]))
963 .abs()
964 / 6.0;
965
966 total_volume += volume;
967 }
968 }
969
970 Ok(total_volume)
971 }
972
973 fn compute_high_dim_volume(&self) -> SpatialResult<f64> {
975 if self.vertices.nrows() < self.dim + 1 {
979 return Ok(0.0);
981 }
982
983 let hull = ConvexHull::new(&self.vertices.view())?;
986
987 hull.volume()
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995 use approx::assert_relative_eq;
996
997 #[test]
998 fn test_halfspace_creation() {
999 let hs = Halfspace::new(arr1(&[1.0, 2.0]), 3.0);
1000 assert_eq!(hs.normal(), &arr1(&[1.0, 2.0]));
1001 assert_eq!(hs.offset(), 3.0);
1002 assert_eq!(hs.dim(), 2);
1003 }
1004
1005 #[test]
1006 fn test_halfspace_contains() {
1007 let hs = Halfspace::new(arr1(&[1.0, 1.0]), 1.0); assert!(hs.contains(&arr1(&[0.0, 0.0]).view())); assert!(hs.contains(&arr1(&[0.5, 0.5]).view())); assert!(!hs.contains(&arr1(&[1.0, 1.0]).view())); assert!(!hs.contains(&arr1(&[2.0, 0.0]).view())); }
1014
1015 #[test]
1016 fn test_halfspace_distance() {
1017 let hs = Halfspace::new(arr1(&[1.0, 0.0]), 1.0); let dist1 = hs.distance(&arr1(&[0.0, 0.0]).view()).unwrap();
1020 assert_relative_eq!(dist1, -1.0, epsilon = 1e-10); let dist2 = hs.distance(&arr1(&[1.0, 0.0]).view()).unwrap();
1023 assert_relative_eq!(dist2, 0.0, epsilon = 1e-10); let dist3 = hs.distance(&arr1(&[2.0, 0.0]).view()).unwrap();
1026 assert_relative_eq!(dist3, 1.0, epsilon = 1e-10); }
1028
1029 #[test]
1030 fn test_unit_square_intersection() {
1031 let halfspaces = vec![
1032 Halfspace::new(arr1(&[-1.0, 0.0]), 0.0), Halfspace::new(arr1(&[0.0, -1.0]), 0.0), Halfspace::new(arr1(&[1.0, 0.0]), 1.0), Halfspace::new(arr1(&[0.0, 1.0]), 1.0), ];
1037
1038 let intersection = HalfspaceIntersection::new(&halfspaces, None).unwrap();
1039
1040 assert!(intersection.is_feasible());
1041 assert!(intersection.is_bounded());
1042 assert_eq!(intersection.dim(), 2);
1043
1044 assert_eq!(intersection.num_vertices(), 4);
1046
1047 let area = intersection.volume().unwrap();
1049 assert_relative_eq!(area, 1.0, epsilon = 1e-6);
1050 }
1051
1052 #[test]
1053 fn test_triangle_intersection() {
1054 let halfspaces = vec![
1055 Halfspace::new(arr1(&[-1.0, 0.0]), 0.0), Halfspace::new(arr1(&[0.0, -1.0]), 0.0), Halfspace::new(arr1(&[1.0, 1.0]), 1.0), ];
1059
1060 let intersection = HalfspaceIntersection::new(&halfspaces, None).unwrap();
1061
1062 assert!(intersection.is_feasible());
1063 assert!(intersection.is_bounded());
1064 assert_eq!(intersection.num_vertices(), 3);
1065
1066 let area = intersection.volume().unwrap();
1068 assert_relative_eq!(area, 0.5, epsilon = 1e-6);
1069 }
1070
1071 #[test]
1072 fn test_empty_intersection() {
1073 let halfspaces = vec![
1074 Halfspace::new(arr1(&[1.0, 0.0]), 0.0), Halfspace::new(arr1(&[-1.0, 0.0]), -1.0), ];
1077
1078 let result = HalfspaceIntersection::new(&halfspaces, None);
1080 if let Ok(intersection) = result {
1082 assert!(!intersection.is_feasible())
1083 }
1084 }
1086
1087 #[test]
1088 fn test_halfspace_normalize() {
1089 let hs = Halfspace::new(arr1(&[3.0, 4.0]), 10.0);
1090 let normalized = hs.normalize().unwrap();
1091
1092 let normal_norm = (normalized.normal()[0].powi(2) + normalized.normal()[1].powi(2)).sqrt();
1093 assert_relative_eq!(normal_norm, 1.0, epsilon = 1e-10);
1094
1095 assert_relative_eq!(normalized.offset(), 2.0, epsilon = 1e-10);
1097 }
1098
1099 #[test]
1100 fn test_invalid_dimensions() {
1101 let halfspaces = vec![
1102 Halfspace::new(arr1(&[1.0, 0.0]), 1.0),
1103 Halfspace::new(arr1(&[1.0, 0.0, 0.0]), 1.0), ];
1105
1106 let result = HalfspaceIntersection::new(&halfspaces, None);
1107 assert!(result.is_err());
1108 }
1109
1110 #[test]
1111 fn test_interior_point_validation() {
1112 let halfspaces = vec![
1113 Halfspace::new(arr1(&[-1.0, 0.0]), 0.0), Halfspace::new(arr1(&[0.0, -1.0]), 0.0), Halfspace::new(arr1(&[1.0, 1.0]), 1.0), ];
1117
1118 let valid_interior = arr1(&[0.2, 0.2]);
1120 let result1 = HalfspaceIntersection::new(&halfspaces, Some(valid_interior));
1121 assert!(result1.is_ok());
1122
1123 let invalid_interior = arr1(&[2.0, 2.0]);
1125 let result2 = HalfspaceIntersection::new(&halfspaces, Some(invalid_interior));
1126 assert!(result2.is_err());
1127 }
1128}