geosets_rs/sets/
hpolytope.rs

1#![allow(unused)]
2use super::*;
3use crate::VPolytope;
4use crate::cddlib_rs::compute_polytope_vertices;
5use crate::linalg_utils::{rank, vector_leq};
6use good_lp::{Expression, Solution, SolverModel, default_solver, variable, variables};
7use ndarray_linalg::Norm;
8use ndarray_rand::RandomExt;
9use ndarray_rand::rand_distr::{Normal, StandardNormal, Uniform};
10use plotly::box_plot;
11use thiserror::Error;
12
13#[derive(Clone, Debug)]
14#[allow(non_snake_case)]
15pub struct HPolytope {
16    A: Array2<f64>,
17    b: Array1<f64>,
18}
19
20#[derive(Error, Debug)]
21pub enum HPolytopeError {
22    #[error("Dimensions of A {a_dim:?} and b {b_dim:?} do not match")]
23    DimensionMismatch { a_dim: (usize, usize), b_dim: usize },
24}
25
26#[allow(non_snake_case)]
27impl HPolytope {
28    pub fn new(A: Array2<f64>, b: Array1<f64>) -> Result<HPolytope, HPolytopeError> {
29        if A.dim().0 != b.dim() {
30            Err(HPolytopeError::DimensionMismatch {
31                a_dim: A.dim(),
32                b_dim: b.dim(),
33            })
34        } else {
35            Ok(HPolytope { A, b })
36        }
37    }
38
39    pub fn from_random(dim: usize, n_constraints: usize) -> Result<HPolytope, HPolytopeError> {
40        let box_poly = HPolytope::from_unit_box(dim);
41
42        let mut random_A = Array2::random((n_constraints, dim), StandardNormal);
43        // Normalize random_A
44        for mut row in random_A.rows_mut() {
45            let norm = row.norm_l2();
46            if norm > 0.0 {
47                row /= norm;
48            }
49        }
50
51        let interior_point = Array1::random(dim, Uniform::new(-0.8, 0.8));
52        let offsets = Array1::random(n_constraints, Uniform::new(0.1, 1.0));
53
54        let random_b = random_A.dot(&interior_point) + offsets;
55
56        let A = ndarray::concatenate(Axis(0), &[box_poly.A.view(), random_A.view()]).unwrap();
57        let b = ndarray::concatenate(Axis(0), &[box_poly.b.view(), random_b.view()]).unwrap();
58
59        Ok(HPolytope { A, b })
60    }
61
62    pub fn n_constraints(&self) -> usize {
63        self.A.nrows()
64    }
65}
66
67#[allow(non_snake_case)]
68impl GeoSet for HPolytope {
69    fn from_unit_box(dim: usize) -> Self {
70        let A = ndarray::concatenate(
71            Axis(0),
72            &[Array2::eye(dim).view(), (-Array2::eye(dim)).view()],
73        )
74        .unwrap();
75        let b = Array1::ones(dim * 2);
76        HPolytope::new(A, b).unwrap()
77    }
78
79    fn dim(&self) -> usize {
80        self.A.dim().1
81    }
82
83    /// Evaluates the feasibility of the optimization problem
84    /// $\min 0$ \
85    /// $\text{subject to } A^\top x \leq b$ \
86    fn empty(&self) -> Result<bool, SetOperationError> {
87        // Define variables x_0, ..., x_{n-1} (unbounded)
88        let mut vars = variables!();
89        let x: Vec<_> = (0..self.dim()).map(|_| vars.add(variable())).collect();
90
91        // Build the problem with dummy objective
92        let mut problem = vars.minimise(0.0).using(default_solver);
93
94        // Add constraints row by row: A[i] ⋅ x <= b[i]
95        for i in 0..self.n_constraints() {
96            let row = self.A.row(i);
97            let expr: Expression = row.iter().zip(&x).map(|(coef, xi)| *coef * *xi).sum();
98            problem = problem.with(expr.leq(self.b[i]));
99        }
100
101        // Try solving
102        match problem.solve() {
103            Ok(_) => Ok(false), // feasible → not empty
104            Err(_) => Ok(true), // infeasible → empty
105        }
106    }
107
108    fn to_vertices(&self) -> Result<Array2<f64>, SetOperationError> {
109        let empty = self.empty()?;
110        if empty {
111            return Err(SetOperationError::EmptySet);
112        }
113        compute_polytope_vertices(&self.A, &self.b)
114    }
115
116    /// Solves the optimization problem: \
117    /// $\max c^\top x $ \
118    /// $\text{subject to } A^\top x \leq b$ \
119    fn center(&self) -> Result<Array1<f64>, SetOperationError> {
120        let mut vars = variables!();
121        let r = vars.add(variable().min(0.0));
122        let x: Vec<_> = (0..self.dim()).map(|_| vars.add(variable())).collect();
123
124        // maximize radius
125        let mut problem = vars.maximise(r).using(default_solver);
126
127        // constraints: a_i^T x + ||a_i|| * r <= b_i
128        for (i, row) in self.A.outer_iter().enumerate() {
129            let norm_ai = row.dot(&row).sqrt();
130            let lhs: Expression = row.iter().zip(&x).map(|(&aij, &xj)| aij * xj).sum();
131            problem = problem.with((lhs + norm_ai * r).leq(self.b[i]));
132        }
133
134        let solution = problem
135            .solve()
136            .map_err(|e| SetOperationError::InfeasibleOptimization {
137                source: Box::new(e),
138            })?;
139
140        let center =
141            Array1::from_shape_vec(self.dim(), x.iter().map(|&xi| solution.value(xi)).collect())
142                .map_err(|e| SetOperationError::DataConversionError {
143                    source: Box::new(e),
144                })?;
145
146        Ok(center)
147    }
148
149    /// Solves the optimization problem: \
150    /// $\max d^\top x $ \
151    /// $\text{subject to } A^\top x \leq b$ \
152    fn support_function(
153        &self,
154        direction: Array1<f64>,
155    ) -> Result<(Array1<f64>, f64), SetOperationError> {
156        self._check_operand_dim(direction.dim())?;
157
158        // Define variables for the support vector x_0, ..., x_{n-1} (unbounded)
159        let mut vars = variables!();
160        let x: Vec<_> = (0..self.dim()).map(|_| vars.add(variable())).collect();
161
162        // Dot product objective
163        let objective: Expression = direction.iter().zip(&x).map(|(d_i, x_i)| *d_i * *x_i).sum();
164        let mut problem = vars.maximise(objective.clone()).using(default_solver);
165
166        // Add constraints row by row: A[i] ⋅ x <= b[i]
167        for i in 0..self.n_constraints() {
168            let row = self.A.row(i);
169            let expr: Expression = row.iter().zip(&x).map(|(coef, xi)| *coef * *xi).sum();
170            problem = problem.with(expr.leq(self.b[i]));
171        }
172
173        let solution = problem
174            .solve()
175            .map_err(|e| SetOperationError::InfeasibleOptimization {
176                source: Box::new(e),
177            })?;
178
179        let support_vector =
180            Array1::from_shape_vec(self.dim(), x.iter().map(|&xi| solution.value(xi)).collect())
181                .map_err(|e| SetOperationError::DataConversionError {
182                    source: Box::new(e),
183                })?;
184
185        let support_value = solution.eval(&objective);
186
187        Ok((support_vector, support_value))
188    }
189
190    fn volume(&self) -> Result<f64, SetOperationError> {
191        let vpoly = VPolytope::new(self.to_vertices()?).map_err(|_| SetOperationError::EmptySet)?;
192        vpoly.volume()
193    }
194
195    fn minkowski_sum_(&mut self, other: &Self) -> Result<(), SetOperationError> {
196        // Implementation based on the support functions of both
197        let dim = self.dim();
198
199        // Collect candidate directions: normals from both polytopes
200        let directions = ndarray::concatenate![Axis(0), self.A.view(), other.A.view()];
201
202        // Prepare storage for new A and b
203        let n_dirs = directions.nrows();
204        let mut new_A = Array2::<f64>::zeros((n_dirs, dim));
205        let mut new_b = Array1::<f64>::zeros(n_dirs);
206
207        for (i, dir) in directions.outer_iter().enumerate() {
208            // Normalize direction to avoid scaling issues
209            let norm = dir.norm_l2();
210            if norm < 1e-9 {
211                continue;
212            }
213            let u = &dir / norm;
214
215            // Compute support values
216            let (_, h1) = self.support_function(u.clone())?;
217            let (_, h2) = other.support_function(u.clone())?;
218
219            // Fill row in A and entry in b
220            new_A.row_mut(i).assign(&u);
221            new_b[i] = h1 + h2;
222        }
223
224        // Replace self with new H-representation
225        self.A = new_A;
226        self.b = new_b;
227
228        Ok(())
229    }
230
231    fn matmul_(&mut self, mat: &Array2<f64>) -> Result<(), SetOperationError> {
232        let (m, n) = mat.dim();
233        self._check_operand_dim(m)?;
234        let mat_rank = rank(mat).unwrap();
235
236        if m == n && mat_rank == n {
237            self.A = self.A.dot(mat);
238        } else if m > n {
239            return Err(SetOperationError::NotImplemented);
240        }
241        Ok(())
242    }
243
244    fn translate_(&mut self, vector: &Array1<f64>) -> Result<(), SetOperationError> {
245        self._check_operand_dim(vector.dim())?;
246        self.b = &self.b + &self.A.dot(vector);
247        Ok(())
248    }
249
250    fn degenerate(&self) -> bool {
251        let c = match self.center() {
252            Ok(center) => center,
253            Err(SetOperationError::InfeasibleOptimization { .. }) => {
254                // Empty sets are degenerate
255                return true;
256            }
257            Err(_) => {
258                // Other errors: treat as degenerate (or propagate)
259                return true;
260            }
261        };
262
263        let residual = &self.b - &self.A.dot(&c);
264
265        // Check if any inequality is tight (≈ equality)
266        residual.iter().any(|&x| (x - 0.0).abs() <= 1e-9)
267    }
268
269    fn contains_point(&self, point: &Array1<f64>) -> Result<bool, SetOperationError> {
270        self._check_operand_dim(point.dim())?;
271        Ok(vector_leq(&self.A.dot(point), &self.b))
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_polytope_new() {
281        let _ = HPolytope::new(Array::ones((2, 2)), Array::ones(2)).unwrap();
282        let _ = HPolytope::new(Array::ones((5, 2)), Array::ones(5)).unwrap();
283    }
284}