scirs2_spatial/
halfspace.rs

1//! Halfspace intersection and convex polytope construction
2//!
3//! This module provides algorithms for computing the intersection of halfspaces
4//! to construct convex polytopes. Halfspace intersection is the dual problem
5//! to convex hull computation and is fundamental in computational geometry.
6//!
7//! # Theory
8//!
9//! A halfspace in d-dimensional space is defined by a linear inequality:
10//! a₁x₁ + a₂x₂ + ... + aₐxₐ ≤ b
11//!
12//! The intersection of multiple halfspaces forms a convex polytope.
13//! This module implements algorithms to:
14//! - Compute the vertices of the polytope
15//! - Extract faces and facets
16//! - Handle degenerate cases
17//! - Check feasibility
18//!
19//! # Examples
20//!
21//! ```
22//! # use scirs2_spatial::halfspace::{HalfspaceIntersection, Halfspace};
23//! # use scirs2_core::ndarray::array;
24//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! // Define halfspaces for a unit square: x ≥ 0, y ≥ 0, x ≤ 1, y ≤ 1
26//! let halfspaces = vec![
27//!     Halfspace::new(array![-1.0, 0.0], 0.0),   // -x ≤ 0  =>  x ≥ 0
28//!     Halfspace::new(array![0.0, -1.0], 0.0),   // -y ≤ 0  =>  y ≥ 0
29//!     Halfspace::new(array![1.0, 0.0], 1.0),    //  x ≤ 1
30//!     Halfspace::new(array![0.0, 1.0], 1.0),    //  y ≤ 1
31//! ];
32//!
33//! let intersection = HalfspaceIntersection::new(&halfspaces, None)?;
34//!
35//! // Get the vertices of the resulting polytope
36//! let vertices = intersection.vertices();
37//! println!("Polytope vertices: {:?}", vertices);
38//!
39//! // Check if the polytope is bounded
40//! println!("Is bounded: {}", intersection.is_bounded());
41//! # Ok(())
42//! # }
43//! ```
44
45use crate::convex_hull::ConvexHull;
46use crate::error::{SpatialError, SpatialResult};
47use scirs2_core::ndarray::{arr1, Array1, Array2, ArrayView1};
48use statrs::statistics::Statistics;
49
50/// Representation of a halfspace: a·x ≤ b
51#[derive(Debug, Clone, PartialEq)]
52pub struct Halfspace {
53    /// Normal vector (coefficients a)
54    normal: Array1<f64>,
55    /// Offset value b
56    offset: f64,
57}
58
59impl Halfspace {
60    /// Create a new halfspace with normal vector and offset
61    ///
62    /// # Arguments
63    ///
64    /// * `normal` - Normal vector (a₁, a₂, ..., aₐ)
65    /// * `offset` - Offset value b
66    ///
67    /// # Returns
68    ///
69    /// * A new Halfspace instance
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// use scirs2_spatial::halfspace::Halfspace;
75    /// use scirs2_core::ndarray::array;
76    ///
77    /// // Halfspace x + y ≤ 1
78    /// let hs = Halfspace::new(array![1.0, 1.0], 1.0);
79    /// ```
80    pub fn new(normal: Array1<f64>, offset: f64) -> Self {
81        Self { normal, offset }
82    }
83
84    /// Get the normal vector
85    pub fn normal(&self) -> &Array1<f64> {
86        &self.normal
87    }
88
89    /// Get the offset
90    pub fn offset(&self) -> f64 {
91        self.offset
92    }
93
94    /// Get the dimension of the halfspace
95    pub fn dim(&self) -> usize {
96        self.normal.len()
97    }
98
99    /// Check if a point satisfies the halfspace constraint
100    ///
101    /// # Arguments
102    ///
103    /// * `point` - Point to test
104    ///
105    /// # Returns
106    ///
107    /// * true if point satisfies a·x ≤ b, false otherwise
108    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 // Small tolerance for numerical errors
120    }
121
122    /// Get the distance from a point to the halfspace boundary
123    ///
124    /// # Arguments
125    ///
126    /// * `point` - Point to measure distance from
127    ///
128    /// # Returns
129    ///
130    /// * Signed distance (negative if inside halfspace, positive if outside)
131    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    /// Normalize the halfspace so that the normal vector has unit length
156    ///
157    /// # Returns
158    ///
159    /// * A new normalized Halfspace
160    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/// Result of halfspace intersection computation
177#[derive(Debug, Clone)]
178pub struct HalfspaceIntersection {
179    /// Input halfspaces
180    halfspaces: Vec<Halfspace>,
181    /// Vertices of the resulting polytope
182    vertices: Array2<f64>,
183    /// Faces of the polytope (indices into vertices array)
184    faces: Vec<Vec<usize>>,
185    /// Dimension of the space
186    dim: usize,
187    /// Whether the polytope is bounded
188    is_bounded: bool,
189    /// Interior point (if provided)
190    #[allow(dead_code)]
191    interior_point: Option<Array1<f64>>,
192}
193
194impl HalfspaceIntersection {
195    /// Compute the intersection of halfspaces
196    ///
197    /// # Arguments
198    ///
199    /// * `halfspaces` - Vector of halfspaces to intersect
200    /// * `interior_point` - Optional interior point for unbounded regions
201    ///
202    /// # Returns
203    ///
204    /// * HalfspaceIntersection result or error
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// # use scirs2_spatial::halfspace::{HalfspaceIntersection, Halfspace};
210    /// # use scirs2_core::ndarray::array;
211    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
212    /// let halfspaces = vec![
213    ///     Halfspace::new(array![-1.0, 0.0], 0.0),   // x ≥ 0
214    ///     Halfspace::new(array![0.0, -1.0], 0.0),   // y ≥ 0
215    ///     Halfspace::new(array![1.0, 1.0], 2.0),    // x + y ≤ 2
216    /// ];
217    ///
218    /// let intersection = HalfspaceIntersection::new(&halfspaces, None)?;
219    /// # Ok(())
220    /// # }
221    /// ```
222    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        // Validate interior point if provided
246        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            // Check that the point is actually interior to all halfspaces
254            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        // Use dual transformation to convert to convex hull problem
264        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    /// Check if the intersection is likely to be bounded by examining halfspaces
282    fn is_likely_bounded(halfspaces: &[Halfspace]) -> bool {
283        let dim = halfspaces[0].dim();
284
285        // Check if we have enough "bounding" halfspaces in different directions
286        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        // If we have constraints in both positive and negative directions for each dimension,
300        // the polytope is likely bounded
301        positive_count
302            .iter()
303            .zip(negative_count.iter())
304            .all(|(&pos, &neg)| pos > 0 && neg > 0)
305    }
306
307    /// Compute 2D polygon intersection using direct vertex enumeration
308    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        // Find all intersection points between pairs of halfspace boundaries
315        for i in 0..n {
316            for j in (i + 1)..n {
317                let hs1 = &halfspaces[i];
318                let hs2 = &halfspaces[j];
319
320                // Solve the 2x2 system: hs1.normal · x = hs1.offset, hs2.normal · x = hs2.offset
321                let det = hs1.normal[0] * hs2.normal[1] - hs1.normal[1] * hs2.normal[0];
322
323                if det.abs() < 1e-15 {
324                    continue; // Parallel halfspaces
325                }
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                // Check if this intersection point satisfies all other halfspaces
333                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        // Remove duplicate vertices
357        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        // Order vertices counter-clockwise
365        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        // Convert to Array2
377        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        // Create simple face list for 2D polygon (single face with all vertices)
384        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    /// Compute intersection for bounded polytopes using dual transformation
394    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        // For 2D case, use direct vertex enumeration which is more reliable
401        if dim == 2 {
402            return Self::compute_2d_intersection(halfspaces);
403        }
404
405        // Find or use provided interior point
406        let interior = if let Some(point) = interior_point {
407            point.clone()
408        } else {
409            Self::find_interior_point(halfspaces)?
410        };
411
412        // Transform halfspaces to dual points using interior point as origin
413        let mut dual_points = Vec::new();
414
415        for hs in halfspaces {
416            // Transform: each halfspace a·x ≤ b becomes point
417            // p = (a₁, a₂, ..., aₐ) / (b - a·interior)
418            let denominator = hs.offset - hs.normal.dot(&interior);
419
420            if denominator.abs() < 1e-15 {
421                // Halfspace passes through or very close to interior point
422                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        // Convert to ndarray for convex hull computation
442        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        // Compute convex hull of dual points
451        let hull = ConvexHull::new(&dual_array.view())?;
452
453        // Transform hull vertices back to primal space
454        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            // Transform back: dual point (p₁, p₂, ..., pₐ) becomes primal vertex
461            // v = interior + p / ||p||²
462            let p_norm_sq: f64 = dual_vertex.iter().map(|x| x * x).sum();
463
464            if p_norm_sq < 1e-15 {
465                continue; // Skip degenerate points
466            }
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        // Convert vertices to array
484        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        // Extract faces from hull simplices
493        let faces = Self::extract_faces_from_hull(&hull)?;
494
495        Ok((vertices, faces, true))
496    }
497
498    /// Compute intersection for potentially unbounded polytopes
499    fn compute_unbounded_intersection(
500        halfspaces: &[Halfspace],
501    ) -> SpatialResult<(Array2<f64>, Vec<Vec<usize>>, bool)> {
502        let dim = halfspaces[0].dim();
503
504        // For unbounded case, we need to find intersection vertices by
505        // solving systems of linear equations
506        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        // Check if polytope is bounded by examining vertex distribution
515        let is_bounded = Self::check_boundedness(&vertices, halfspaces)?;
516
517        // For simplicity, create basic face structure
518        let faces = if vertices.nrows() > dim {
519            // Compute convex hull to get proper face structure
520            let hull = ConvexHull::new(&vertices.view())?;
521            Self::extract_faces_from_hull(&hull)?
522        } else {
523            // Create simple face structure for degenerate cases
524            vec![(0..vertices.nrows()).collect()]
525        };
526
527        Ok((vertices, faces, is_bounded))
528    }
529
530    /// Find an interior point for the given halfspaces using linear programming
531    fn find_interior_point(halfspaces: &[Halfspace]) -> SpatialResult<Array1<f64>> {
532        let dim = halfspaces[0].dim();
533
534        // Try simple candidate points first, ensuring they are truly interior
535        let candidates = vec![
536            Array1::from_elem(dim, 0.1),    // Small positive values
537            Array1::from_elem(dim, 0.01),   // Very small positive values
538            Array1::from_elem(dim, 0.5),    // Medium values
539            Array1::from_elem(dim, 0.3333), // 1/3 values
540            Array1::from_elem(dim, 0.25),   // 1/4 values
541            Array1::zeros(dim),             // Origin (try last)
542        ];
543
544        for candidate in candidates {
545            // Check that point is strictly interior (not on boundary)
546            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                    // Point is on or outside this halfspace
551                    is_strictly_interior = false;
552                    break;
553                }
554            }
555
556            if is_strictly_interior {
557                return Ok(candidate);
558            }
559        }
560
561        // Try to find a point by solving a linear programming problem
562        // Use Chebyshev center approach: find point that maximizes distance to closest constraint
563
564        // For simple cases, try analytical solutions
565        if dim == 2 && halfspaces.len() >= 3 {
566            // Try intersection of first two constraints, shifted inward
567            let hs1 = &halfspaces[0];
568            let hs2 = &halfspaces[1];
569
570            // Solve n1·x = b1 and n2·x = b2 system
571            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                // Check if this intersection point is feasible for all constraints
580                if halfspaces.iter().all(|hs| hs.contains(&candidate.view())) {
581                    return Ok(candidate);
582                }
583
584                // If boundary point is feasible, move it slightly inward
585                // Find the direction that moves away from the closest constraint
586                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                    // Point is feasible or very close, shift inward slightly
599                    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    /// Find intersection vertices by solving systems of linear equations
618    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        // Generate all combinations of d halfspaces
632        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                // Check if vertex satisfies all other halfspaces
637                if halfspaces.iter().all(|hs| hs.contains(&vertex.view())) {
638                    vertices.push(vertex.to_vec());
639                }
640            }
641        }
642
643        // Remove duplicate vertices
644        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    /// Generate combinations of k elements from n elements
668    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    /// Solve system of linear equations for intersection of d halfspaces
708    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        // Build matrix A and vector b for system Ax = b
721        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        // Use simple Gaussian elimination for small systems
731        Self::solve_linear_system(&matrix_data, &rhs, dim)
732    }
733
734    /// Simple Gaussian elimination solver for small linear systems
735    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        // Build augmented matrix
743        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        // Forward elimination
751        for i in 0..n {
752            // Find pivot
753            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            // Swap rows
761            aug_matrix.swap(i, max_row);
762
763            // Check for singular matrix
764            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            // Eliminate
771            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        // Back substitution
780        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    /// Check if a polytope is bounded by examining vertices
793    fn check_boundedness(vertices: &Array2<f64>, halfspaces: &[Halfspace]) -> SpatialResult<bool> {
794        if vertices.nrows() == 0 {
795            return Ok(false);
796        }
797
798        // Simple check: if all coordinates are finite and within reasonable bounds
799        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    /// Extract face structure from convex hull
808    fn extract_faces_from_hull(hull: &ConvexHull) -> SpatialResult<Vec<Vec<usize>>> {
809        // For now, create a simple face structure
810        // A complete implementation would extract actual facets from the _hull
811        let vertices = hull.vertex_indices();
812        if vertices.len() < 3 {
813            Ok(vec![vertices.to_vec()])
814        } else {
815            // Create triangular faces for simplicity
816            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    /// Get the vertices of the intersection polytope
825    pub fn vertices(&self) -> &Array2<f64> {
826        &self.vertices
827    }
828
829    /// Get the faces of the intersection polytope
830    pub fn faces(&self) -> &[Vec<usize>] {
831        &self.faces
832    }
833
834    /// Get the dimension of the space
835    pub fn dim(&self) -> usize {
836        self.dim
837    }
838
839    /// Check if the polytope is bounded
840    pub fn is_bounded(&self) -> bool {
841        self.is_bounded
842    }
843
844    /// Get the number of vertices
845    pub fn num_vertices(&self) -> usize {
846        self.vertices.nrows()
847    }
848
849    /// Get the number of faces
850    pub fn num_faces(&self) -> usize {
851        self.faces.len()
852    }
853
854    /// Check if the intersection is feasible (non-empty)
855    pub fn is_feasible(&self) -> bool {
856        self.vertices.nrows() > 0
857    }
858
859    /// Get the input halfspaces
860    pub fn halfspaces(&self) -> &[Halfspace] {
861        &self.halfspaces
862    }
863
864    /// Compute the volume of the polytope (2D area, 3D volume)
865    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    /// Compute area of 2D polygon using shoelace formula
884    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        // Order vertices counter-clockwise
893        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        // Apply shoelace formula
907        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    /// Compute volume of 3D polyhedron using triangulation
919    fn compute_polyhedron_volume(&self) -> SpatialResult<f64> {
920        if self.vertices.nrows() < 4 {
921            return Ok(0.0);
922        }
923
924        // Use triangulation approach: compute convex hull and sum tetrahedron volumes
925        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        // Pick a reference point (first vertex)
933        let reference = self.vertices.row(0);
934        let mut total_volume = 0.0;
935
936        // Sum volumes of tetrahedra formed by reference point and triangular faces
937        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                // Compute tetrahedron volume using scalar triple product
944                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    /// Compute volume for high-dimensional halfspace intersection
974    fn compute_high_dim_volume(&self) -> SpatialResult<f64> {
975        // For high-dimensional halfspace intersections, we can compute the volume
976        // by treating the vertices as a convex polytope and using convex hull algorithms
977
978        if self.vertices.nrows() < self.dim + 1 {
979            // Not enough vertices to form a polytope
980            return Ok(0.0);
981        }
982
983        // Create a convex hull from the vertices
984        // The vertices of the halfspace intersection already form a convex polytope
985        let hull = ConvexHull::new(&self.vertices.view())?;
986
987        // Compute and return the volume of this convex polytope
988        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); // x + y ≤ 1
1008
1009        assert!(hs.contains(&arr1(&[0.0, 0.0]).view())); // Origin
1010        assert!(hs.contains(&arr1(&[0.5, 0.5]).view())); // On boundary
1011        assert!(!hs.contains(&arr1(&[1.0, 1.0]).view())); // Outside (just barely)
1012        assert!(!hs.contains(&arr1(&[2.0, 0.0]).view())); // Clearly outside
1013    }
1014
1015    #[test]
1016    fn test_halfspace_distance() {
1017        let hs = Halfspace::new(arr1(&[1.0, 0.0]), 1.0); // x ≤ 1
1018
1019        let dist1 = hs.distance(&arr1(&[0.0, 0.0]).view()).unwrap();
1020        assert_relative_eq!(dist1, -1.0, epsilon = 1e-10); // Inside
1021
1022        let dist2 = hs.distance(&arr1(&[1.0, 0.0]).view()).unwrap();
1023        assert_relative_eq!(dist2, 0.0, epsilon = 1e-10); // On boundary
1024
1025        let dist3 = hs.distance(&arr1(&[2.0, 0.0]).view()).unwrap();
1026        assert_relative_eq!(dist3, 1.0, epsilon = 1e-10); // Outside
1027    }
1028
1029    #[test]
1030    fn test_unit_square_intersection() {
1031        let halfspaces = vec![
1032            Halfspace::new(arr1(&[-1.0, 0.0]), 0.0), // x ≥ 0
1033            Halfspace::new(arr1(&[0.0, -1.0]), 0.0), // y ≥ 0
1034            Halfspace::new(arr1(&[1.0, 0.0]), 1.0),  // x ≤ 1
1035            Halfspace::new(arr1(&[0.0, 1.0]), 1.0),  // y ≤ 1
1036        ];
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        // Should have 4 vertices for unit square
1045        assert_eq!(intersection.num_vertices(), 4);
1046
1047        // Check area
1048        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), // x ≥ 0
1056            Halfspace::new(arr1(&[0.0, -1.0]), 0.0), // y ≥ 0
1057            Halfspace::new(arr1(&[1.0, 1.0]), 1.0),  // x + y ≤ 1
1058        ];
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        // Triangle area should be 0.5
1067        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),   // x ≤ 0
1075            Halfspace::new(arr1(&[-1.0, 0.0]), -1.0), // x ≥ 1
1076        ];
1077
1078        // These halfspaces have no intersection
1079        let result = HalfspaceIntersection::new(&halfspaces, None);
1080        // This should either fail or return empty intersection
1081        if let Ok(intersection) = result {
1082            assert!(!intersection.is_feasible())
1083        }
1084        // Also acceptable if Err
1085    }
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        // The normalized offset should be 10/5 = 2
1096        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), // Different dimension
1104        ];
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), // x ≥ 0
1114            Halfspace::new(arr1(&[0.0, -1.0]), 0.0), // y ≥ 0
1115            Halfspace::new(arr1(&[1.0, 1.0]), 1.0),  // x + y ≤ 1
1116        ];
1117
1118        // Valid interior point
1119        let valid_interior = arr1(&[0.2, 0.2]);
1120        let result1 = HalfspaceIntersection::new(&halfspaces, Some(valid_interior));
1121        assert!(result1.is_ok());
1122
1123        // Invalid interior point (outside)
1124        let invalid_interior = arr1(&[2.0, 2.0]);
1125        let result2 = HalfspaceIntersection::new(&halfspaces, Some(invalid_interior));
1126        assert!(result2.is_err());
1127    }
1128}