Skip to main content

oxilean_std/convex_geometry/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4use super::functions::*;
5
6#[allow(dead_code)]
7#[derive(Debug, Clone)]
8pub struct IntrinsicVolume {
9    pub ambient_dimension: usize,
10    pub index: usize,
11    pub formula: String,
12}
13#[allow(dead_code)]
14impl IntrinsicVolume {
15    pub fn new(n: usize, i: usize) -> Self {
16        let formula = format!("V_{i}(K) = C(n,{i}) × (mixed volume of K and B^n)");
17        IntrinsicVolume {
18            ambient_dimension: n,
19            index: i,
20            formula,
21        }
22    }
23    pub fn kinematic_formula(&self) -> String {
24        format!("Kinematic formula: ∫ χ(K∩gL) dg = Σ_k c_{{n,k}} V_{{n-k}}(K) V_k(L)")
25    }
26    pub fn steiner_formula_contribution(&self) -> String {
27        format!(
28            "Steiner: V(K + εB) = sum_k eps^k C(n,k) V_(n-k)(K), n={}, i={}",
29            self.ambient_dimension, self.index
30        )
31    }
32}
33#[allow(dead_code)]
34#[derive(Debug, Clone)]
35pub struct LogConcaveSequence {
36    pub coefficients: Vec<f64>,
37    pub is_log_concave: bool,
38    pub is_ultra_log_concave: bool,
39    pub has_real_roots_conjecture: bool,
40}
41#[allow(dead_code)]
42impl LogConcaveSequence {
43    pub fn new(coeffs: Vec<f64>) -> Self {
44        let lc = Self::check_log_concave(&coeffs);
45        LogConcaveSequence {
46            is_ultra_log_concave: false,
47            coefficients: coeffs,
48            is_log_concave: lc,
49            has_real_roots_conjecture: false,
50        }
51    }
52    fn check_log_concave(c: &[f64]) -> bool {
53        let n = c.len();
54        for i in 1..n.saturating_sub(1) {
55            if c[i] * c[i] < c[i - 1] * c[i + 1] {
56                return false;
57            }
58        }
59        true
60    }
61    pub fn with_ultra_log_concave(mut self) -> Self {
62        self.is_ultra_log_concave = true;
63        self
64    }
65    pub fn mason_conjecture_statement(&self) -> String {
66        "Mason's conjecture: independence numbers of matroids form ultra-log-concave sequences"
67            .to_string()
68    }
69    pub fn branden_huh_lorentzian_connection(&self) -> String {
70        "Brändén-Huh (2020): Lorentzian polynomials → log-concavity of matroid sequences"
71            .to_string()
72    }
73}
74#[allow(dead_code)]
75#[derive(Debug, Clone)]
76pub struct ZonotopeData {
77    pub generators: Vec<Vec<f64>>,
78    pub ambient_dim: usize,
79    pub num_generators: usize,
80}
81#[allow(dead_code)]
82impl ZonotopeData {
83    pub fn new(generators: Vec<Vec<f64>>) -> Self {
84        let n = generators.first().map_or(0, |g| g.len());
85        let k = generators.len();
86        ZonotopeData {
87            generators,
88            ambient_dim: n,
89            num_generators: k,
90        }
91    }
92    pub fn num_faces_upper_bound(&self) -> usize {
93        let k = self.num_generators;
94        let n = self.ambient_dim;
95        let mut count = 2;
96        for j in 0..n.min(k) {
97            let mut c = 1usize;
98            for i in 0..j {
99                c = c * (k - 1 - i) / (i + 1);
100            }
101            count += 2 * c;
102        }
103        count
104    }
105    pub fn volume_formula(&self) -> String {
106        format!(
107            "Vol(Z) = sum over n-subsets of |det(g_{{i1}},...,g_{{in}})| (n={})",
108            self.ambient_dim
109        )
110    }
111    pub fn is_centrally_symmetric(&self) -> bool {
112        true
113    }
114    pub fn tilings_of_space_description(&self) -> String {
115        "Zonotopes tile space if and only if the generators form a totally unimodular matrix"
116            .to_string()
117    }
118}
119/// Zonotope: Minkowski sum of line segments {c + Σ λ_i g_i : λ_i ∈ [-1,1]}.
120#[derive(Debug, Clone)]
121pub struct Zonotope {
122    /// Centre c.
123    pub centre: Vec<f64>,
124    /// Generators g_1,...,g_m (each a vector in ℝ^d).
125    pub generators: Vec<Vec<f64>>,
126}
127impl Zonotope {
128    /// Construct a zonotope.
129    pub fn new(centre: Vec<f64>, generators: Vec<Vec<f64>>) -> Self {
130        Self { centre, generators }
131    }
132    /// Check membership: x ∈ Z iff x - c = Σ λ_i g_i with λ_i ∈ [-1,1].
133    /// Uses a greedy interval check (exact only for orthogonal generators).
134    pub fn contains_approx(&self, x: &[f64]) -> bool {
135        let d = self.centre.len();
136        let diff: Vec<f64> = x
137            .iter()
138            .zip(self.centre.iter())
139            .map(|(xi, ci)| xi - ci)
140            .collect();
141        let sum_abs: Vec<f64> = (0..d)
142            .map(|j| self.generators.iter().map(|g| g[j].abs()).sum::<f64>())
143            .collect();
144        diff.iter()
145            .zip(sum_abs.iter())
146            .all(|(di, &si)| di.abs() <= si + 1e-10)
147    }
148    /// Volume of the zonotope: 2^m * |det([g_{i1},...,g_{id}])| summed over all d-subsets.
149    /// For 2D: V = Σ_{i<j} |g_i × g_j|.
150    pub fn volume_2d(&self) -> f64 {
151        if self.generators.len() < 2 {
152            return 0.0;
153        }
154        let mut vol = 0.0_f64;
155        let m = self.generators.len();
156        for i in 0..m {
157            for j in (i + 1)..m {
158                let gi = &self.generators[i];
159                let gj = &self.generators[j];
160                if gi.len() >= 2 && gj.len() >= 2 {
161                    let cross = (gi[0] * gj[1] - gi[1] * gj[0]).abs();
162                    vol += cross;
163                }
164            }
165        }
166        4.0 * vol
167    }
168    /// Number of vertices: at most 2 Σ_{k=0}^{d-1} C(m-1, k) (upper bound).
169    pub fn num_vertices_upper_bound(&self) -> usize {
170        let m = self.generators.len();
171        let d = self.centre.len();
172        if d == 0 || m == 0 {
173            return 1;
174        }
175        2 * (0..d)
176            .map(|k| binomial(m.saturating_sub(1), k))
177            .sum::<usize>()
178    }
179}
180/// V-polytope: conv{v_1,...,v_n}.
181#[derive(Debug, Clone)]
182pub struct VPolytope {
183    /// Vertices of the polytope.
184    pub vertices: Vec<Vec<f64>>,
185    /// Dimension of the ambient space.
186    pub dim: usize,
187}
188impl VPolytope {
189    /// Construct from a list of vertices.
190    pub fn new(vertices: Vec<Vec<f64>>) -> Self {
191        let dim = vertices.first().map_or(0, |v| v.len());
192        Self { vertices, dim }
193    }
194    /// Return the vertices.
195    pub fn vertices(&self) -> &[Vec<f64>] {
196        &self.vertices
197    }
198    /// f-vector: for a simplex in d dimensions, f_k = C(d+1, k+1).
199    /// Here we return the count of vertices as f_0.
200    pub fn f_vector(&self) -> Vec<usize> {
201        vec![self.vertices.len()]
202    }
203    /// Check if the polytope is a simplex (exactly d+1 vertices in d dimensions).
204    pub fn is_simplicial(&self) -> bool {
205        !self.vertices.is_empty() && self.vertices.len() == self.dim + 1
206    }
207}
208/// A convex function represented by its evaluations on a grid (or analytically via a closure).
209#[derive(Debug, Clone)]
210pub struct ConvexFunction {
211    /// Dimension of the domain.
212    pub dim: usize,
213}
214impl ConvexFunction {
215    /// Construct a convex function placeholder.
216    pub fn new(dim: usize) -> Self {
217        Self { dim }
218    }
219    /// Check Jensen's inequality numerically at two points x, y with λ ∈ [0,1].
220    pub fn check_convexity<F>(&self, f: F, x: &[f64], y: &[f64], lambda: f64) -> bool
221    where
222        F: Fn(&[f64]) -> f64,
223    {
224        let mid: Vec<f64> = x
225            .iter()
226            .zip(y.iter())
227            .map(|(xi, yi)| lambda * xi + (1.0 - lambda) * yi)
228            .collect();
229        f(&mid) <= lambda * f(x) + (1.0 - lambda) * f(y) + 1e-10
230    }
231    /// Compute subdifferential (subgradient) at x by finite differences.
232    pub fn subgradient<F>(&self, f: F, x: &[f64], eps: f64) -> Vec<f64>
233    where
234        F: Fn(&[f64]) -> f64,
235    {
236        let fx = f(x);
237        let mut grad = vec![0.0_f64; x.len()];
238        for i in 0..x.len() {
239            let mut xp = x.to_vec();
240            xp[i] += eps;
241            grad[i] = (f(&xp) - fx) / eps;
242        }
243        grad
244    }
245}
246/// Face poset of a polytope.
247#[derive(Debug, Clone)]
248pub struct FacePoset {
249    /// f-vector: `f_vec[k]` = number of k-dimensional faces.
250    pub f_vec: Vec<usize>,
251}
252impl FacePoset {
253    /// Construct from an f-vector.
254    pub fn new(f_vec: Vec<usize>) -> Self {
255        Self { f_vec }
256    }
257    /// Return the f-vector.
258    pub fn f_vector(&self) -> &[usize] {
259        &self.f_vec
260    }
261    /// Euler characteristic: Σ (-1)^k f_k = 0 for polytopes (excluding empty face and polytope itself).
262    pub fn euler_characteristic(&self) -> i64 {
263        self.f_vec
264            .iter()
265            .enumerate()
266            .map(|(k, &fk)| if k % 2 == 0 { fk as i64 } else { -(fk as i64) })
267            .sum()
268    }
269}
270/// Tiling theory: lattice tilings by translates of a convex body.
271#[derive(Debug, Clone)]
272pub struct TilingTheory {
273    /// Dimension.
274    pub dim: usize,
275    /// Lattice basis vectors.
276    pub lattice: Vec<Vec<f64>>,
277}
278impl TilingTheory {
279    /// Construct a tiling theory instance.
280    pub fn new(dim: usize, lattice: Vec<Vec<f64>>) -> Self {
281        Self { dim, lattice }
282    }
283    /// Check if the lattice packing density is valid (det(lattice) ≠ 0 in 2D).
284    pub fn is_valid_lattice(&self) -> bool {
285        if self.lattice.len() < 2 || self.lattice[0].len() < 2 {
286            return !self.lattice.is_empty();
287        }
288        let det = self.lattice[0][0] * self.lattice[1][1] - self.lattice[0][1] * self.lattice[1][0];
289        det.abs() > 1e-12
290    }
291    /// Compute the fundamental domain volume: |det(lattice)| in 2D.
292    pub fn fundamental_volume_2d(&self) -> f64 {
293        if self.lattice.len() < 2 || self.lattice[0].len() < 2 {
294            return 0.0;
295        }
296        (self.lattice[0][0] * self.lattice[1][1] - self.lattice[0][1] * self.lattice[1][0]).abs()
297    }
298}
299/// Checker for Helly-type theorems on convex sets.
300///
301/// Helly's theorem: given a finite family of convex sets in ℝ^d, if every
302/// d+1 of them have a common point, then all of them do.
303///
304/// This struct checks the Helly condition by testing pairwise intersections
305/// and the triangle intersection (for d=1 and d=2 simplified cases).
306#[derive(Debug, Clone)]
307pub struct HellyTheoremChecker {
308    /// Ambient dimension.
309    pub dim: usize,
310    /// Convex sets represented as axis-aligned boxes [lo, hi]^d.
311    pub boxes: Vec<(Vec<f64>, Vec<f64>)>,
312}
313impl HellyTheoremChecker {
314    /// Create a Helly checker.
315    pub fn new(dim: usize) -> Self {
316        Self {
317            dim,
318            boxes: Vec::new(),
319        }
320    }
321    /// Add a convex set represented as an axis-aligned box [lo, hi].
322    pub fn add_box(&mut self, lo: Vec<f64>, hi: Vec<f64>) {
323        self.boxes.push((lo, hi));
324    }
325    /// Check if two boxes [lo_i, hi_i] and [lo_j, hi_j] intersect.
326    fn boxes_intersect(lo1: &[f64], hi1: &[f64], lo2: &[f64], hi2: &[f64]) -> bool {
327        lo1.iter()
328            .zip(hi1.iter())
329            .zip(lo2.iter())
330            .zip(hi2.iter())
331            .all(|(((l1, h1), l2), h2)| l1 <= h2 && l2 <= h1)
332    }
333    /// Check if all boxes have a common point (pairwise intersection of all boxes).
334    /// For axis-aligned boxes this equals checking the intersection is non-empty.
335    pub fn all_intersect(&self) -> bool {
336        if self.boxes.is_empty() {
337            return true;
338        }
339        let d = self.dim;
340        let mut lo = vec![f64::NEG_INFINITY; d];
341        let mut hi = vec![f64::INFINITY; d];
342        for (bl, bh) in &self.boxes {
343            for j in 0..d.min(bl.len()).min(bh.len()) {
344                if bl[j] > lo[j] {
345                    lo[j] = bl[j];
346                }
347                if bh[j] < hi[j] {
348                    hi[j] = bh[j];
349                }
350            }
351        }
352        lo.iter().zip(hi.iter()).all(|(l, h)| l <= h)
353    }
354    /// Verify the Helly condition: check every (d+1)-subfamily intersects.
355    /// For d=1 (intervals), this checks every 2-subfamily intersects → all intersect.
356    pub fn verify_helly_condition_1d(&self) -> bool {
357        let n = self.boxes.len();
358        for i in 0..n {
359            for j in (i + 1)..n {
360                let (li, hi_) = &self.boxes[i];
361                let (lj, hj) = &self.boxes[j];
362                if !Self::boxes_intersect(li, hi_, lj, hj) {
363                    return false;
364                }
365            }
366        }
367        true
368    }
369    /// Fractional Helly check: if at least α fraction of all d+1-tuples intersect,
370    /// then at least (1 - (1-α)^{1/(d+1)}) fraction of the sets share a common point.
371    /// Here we compute what fraction of pairs intersect.
372    pub fn fraction_pairwise_intersecting(&self) -> f64 {
373        let n = self.boxes.len();
374        if n < 2 {
375            return 1.0;
376        }
377        let total = n * (n - 1) / 2;
378        let mut count = 0_usize;
379        for i in 0..n {
380            for j in (i + 1)..n {
381                let (li, hi_) = &self.boxes[i];
382                let (lj, hj) = &self.boxes[j];
383                if Self::boxes_intersect(li, hi_, lj, hj) {
384                    count += 1;
385                }
386            }
387        }
388        count as f64 / total as f64
389    }
390    /// Radon partition check (simplified 2D): do the given points have a Radon partition?
391    /// Radon's theorem: any d+2 points in ℝ^d have a Radon partition.
392    /// For d=1: any 3 points {a,b,c} with a ≤ b ≤ c: {b} and {a,c} is a partition
393    /// (b ∈ [a,c]).
394    pub fn has_radon_partition_1d(points: &[f64]) -> bool {
395        if points.len() < 3 {
396            return false;
397        }
398        let mut sorted = points.to_vec();
399        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
400        true
401    }
402}
403/// Approximation of John's ellipsoid (maximum volume inscribed ellipsoid).
404///
405/// John's theorem states: for every convex body K in ℝ^n, there is a unique
406/// maximum-volume ellipsoid E ⊆ K, and K ⊆ n·E (with equality for simplices).
407/// This struct computes a simple approximation via the covariance of the vertices.
408#[derive(Debug, Clone)]
409pub struct JohnEllipsoidApprox {
410    /// Centre of the inscribed ellipsoid.
411    pub centre: Vec<f64>,
412    /// Axis lengths (eigenvalues of the shape matrix, approximated).
413    pub axes: Vec<f64>,
414}
415impl JohnEllipsoidApprox {
416    /// Approximate John's ellipsoid from a set of points by computing the
417    /// covariance ellipsoid (centred at the mean, axes from variance).
418    pub fn from_points(points: &[Vec<f64>]) -> Option<Self> {
419        if points.is_empty() {
420            return None;
421        }
422        let d = points[0].len();
423        let n = points.len() as f64;
424        let mut centre = vec![0.0_f64; d];
425        for p in points {
426            for j in 0..d {
427                centre[j] += p[j];
428            }
429        }
430        for c in centre.iter_mut() {
431            *c /= n;
432        }
433        let mut axes = vec![0.0_f64; d];
434        for p in points {
435            for j in 0..d {
436                axes[j] += (p[j] - centre[j]).powi(2);
437            }
438        }
439        for a in axes.iter_mut() {
440            *a = (*a / n).sqrt().max(1e-12);
441        }
442        Some(Self { centre, axes })
443    }
444    /// Check if a point x is inside the ellipsoid: Σ_j ((x_j - c_j)/a_j)^2 ≤ 1.
445    pub fn contains(&self, x: &[f64]) -> bool {
446        let dist2: f64 = x
447            .iter()
448            .zip(self.centre.iter())
449            .zip(self.axes.iter())
450            .map(|((xi, ci), ai)| ((xi - ci) / ai).powi(2))
451            .sum();
452        dist2 <= 1.0 + 1e-10
453    }
454    /// Volume of the ellipsoid: κ_d * Π_j a_j where κ_d = π^{d/2}/Γ(d/2+1).
455    /// For d = 2: π * a1 * a2. For d = 3: 4π/3 * a1 * a2 * a3.
456    pub fn volume(&self) -> f64 {
457        let d = self.axes.len();
458        let prod: f64 = self.axes.iter().product();
459        match d {
460            1 => 2.0 * prod,
461            2 => std::f64::consts::PI * prod,
462            3 => 4.0 / 3.0 * std::f64::consts::PI * prod,
463            _ => {
464                let half_d = d as f64 / 2.0;
465                let kappa = std::f64::consts::PI.powf(half_d) / stirling_gamma(half_d + 1.0);
466                kappa * prod
467            }
468        }
469    }
470    /// John's containment bound: K ⊆ d * E (where d is ambient dimension).
471    /// Returns the scaled ellipsoid axes.
472    pub fn johns_outer_bound(&self) -> Vec<f64> {
473        let d = self.axes.len() as f64;
474        self.axes.iter().map(|&a| a * d).collect()
475    }
476}
477/// Mixed volume computations for convex bodies.
478#[derive(Debug, Clone)]
479pub struct MixedVolume {
480    /// Dimension n.
481    pub dim: usize,
482}
483impl MixedVolume {
484    /// Construct a mixed volume calculator.
485    pub fn new(dim: usize) -> Self {
486        Self { dim }
487    }
488    /// Estimate the volume of a polytope (given by vertices) via Monte Carlo sampling.
489    pub fn estimate_volume_monte_carlo(
490        &self,
491        vertices: &[Vec<f64>],
492        n_samples: usize,
493        seed: u64,
494    ) -> f64 {
495        if vertices.is_empty() || self.dim == 0 {
496            return 0.0;
497        }
498        let mut lo = vec![f64::INFINITY; self.dim];
499        let mut hi = vec![f64::NEG_INFINITY; self.dim];
500        for v in vertices {
501            for j in 0..self.dim {
502                if v[j] < lo[j] {
503                    lo[j] = v[j];
504                }
505                if v[j] > hi[j] {
506                    hi[j] = v[j];
507                }
508            }
509        }
510        let bbox_vol: f64 = lo.iter().zip(hi.iter()).map(|(l, h)| h - l).product();
511        let mut rng_state = seed;
512        let mut inside = 0_usize;
513        for _ in 0..n_samples {
514            rng_state = rng_state
515                .wrapping_mul(6364136223846793005)
516                .wrapping_add(1442695040888963407);
517            let point: Vec<f64> = (0..self.dim)
518                .map(|j| {
519                    rng_state = rng_state
520                        .wrapping_mul(6364136223846793005)
521                        .wrapping_add(1442695040888963407);
522                    let t = (rng_state >> 33) as f64 / (u32::MAX as f64);
523                    lo[j] + t * (hi[j] - lo[j])
524                })
525                .collect();
526            if is_in_convex_hull(vertices, &point) {
527                inside += 1;
528            }
529        }
530        bbox_vol * inside as f64 / n_samples as f64
531    }
532    /// Brunn-Minkowski inequality check (numeric): verify V(A+B)^{1/n} >= V(A)^{1/n} + V(B)^{1/n}.
533    pub fn check_brunn_minkowski(&self, vol_a: f64, vol_b: f64, vol_apb: f64) -> bool {
534        let n = self.dim as f64;
535        let lhs = vol_apb.powf(1.0 / n);
536        let rhs = vol_a.powf(1.0 / n) + vol_b.powf(1.0 / n);
537        lhs >= rhs - 1e-10
538    }
539    /// Check the isoperimetric inequality V^{n-1} <= c_n * S^n numerically (simplified 2D: 4πA ≤ P²).
540    pub fn check_isoperimetric_2d(area: f64, perimeter: f64) -> bool {
541        4.0 * std::f64::consts::PI * area <= perimeter * perimeter + 1e-10
542    }
543}
544#[allow(dead_code)]
545#[derive(Debug, Clone)]
546pub struct ConvexValuation {
547    pub name: String,
548    pub is_continuous: bool,
549    pub is_translation_invariant: bool,
550    pub is_rotation_invariant: bool,
551    pub homogeneity_degree: Option<usize>,
552}
553#[allow(dead_code)]
554impl ConvexValuation {
555    pub fn euler_characteristic() -> Self {
556        ConvexValuation {
557            name: "Euler characteristic χ".to_string(),
558            is_continuous: true,
559            is_translation_invariant: true,
560            is_rotation_invariant: true,
561            homogeneity_degree: Some(0),
562        }
563    }
564    pub fn volume(dim: usize) -> Self {
565        ConvexValuation {
566            name: format!("Volume V_{}", dim),
567            is_continuous: true,
568            is_translation_invariant: true,
569            is_rotation_invariant: true,
570            homogeneity_degree: Some(dim),
571        }
572    }
573    pub fn surface_area() -> Self {
574        ConvexValuation {
575            name: "Surface area S".to_string(),
576            is_continuous: true,
577            is_translation_invariant: true,
578            is_rotation_invariant: true,
579            homogeneity_degree: Some(1),
580        }
581    }
582    pub fn hadwiger_theorem(&self) -> String {
583        "Hadwiger: any continuous, rigid-motion invariant valuation on K^n = linear combo of V_0,...,V_n"
584            .to_string()
585    }
586    pub fn mcmullen_decomposition(&self) -> String {
587        format!(
588            "McMullen: {} decomposes into homogeneous components of degrees 0..n",
589            self.name
590        )
591    }
592}
593/// A convex set represented as a list of vertices (for polytopes) or by a predicate.
594#[derive(Debug, Clone)]
595pub struct ConvexSet {
596    /// Dimension of the ambient space.
597    pub dim: usize,
598    /// Vertices (for polyhedral sets in V-representation).
599    pub vertices: Vec<Vec<f64>>,
600}
601impl ConvexSet {
602    /// Construct a convex set from its vertices.
603    pub fn new(dim: usize, vertices: Vec<Vec<f64>>) -> Self {
604        Self { dim, vertices }
605    }
606    /// Project a point x onto the convex hull of the vertices (using simple averaging heuristic).
607    /// For a proper projection use an LP; here we return the nearest vertex.
608    pub fn projection(&self, x: &[f64]) -> Vec<f64> {
609        self.vertices
610            .iter()
611            .min_by(|a, b| {
612                let da: f64 = a.iter().zip(x).map(|(ai, xi)| (ai - xi).powi(2)).sum();
613                let db: f64 = b.iter().zip(x).map(|(bi, xi)| (bi - xi).powi(2)).sum();
614                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
615            })
616            .cloned()
617            .unwrap_or_else(|| vec![0.0; self.dim])
618    }
619    /// Check if all vertices satisfy the polyhedral inequality Ax ≤ b.
620    pub fn is_polyhedral(&self, a: &[Vec<f64>], b: &[f64]) -> bool {
621        self.vertices.iter().all(|v| {
622            a.iter().zip(b.iter()).all(|(row, &bi)| {
623                let dot: f64 = row.iter().zip(v.iter()).map(|(aij, vj)| aij * vj).sum();
624                dot <= bi + 1e-10
625            })
626        })
627    }
628    /// Compute the support function value h_C(y) = max_{v ∈ vertices} ⟨v, y⟩.
629    pub fn support_function(&self, y: &[f64]) -> f64 {
630        self.vertices
631            .iter()
632            .map(|v| v.iter().zip(y.iter()).map(|(vi, yi)| vi * yi).sum::<f64>())
633            .fold(f64::NEG_INFINITY, f64::max)
634    }
635    /// Compute the Minkowski sum of two polytopes (convex hull of all pairwise sums).
636    pub fn minkowski_sum(&self, other: &ConvexSet) -> ConvexSet {
637        let mut sums = Vec::new();
638        for a in &self.vertices {
639            for b in &other.vertices {
640                let s: Vec<f64> = a.iter().zip(b.iter()).map(|(ai, bi)| ai + bi).collect();
641                sums.push(s);
642            }
643        }
644        ConvexSet {
645            dim: self.dim,
646            vertices: sums,
647        }
648    }
649}
650/// Delaunay triangulation dual to the Voronoi diagram.
651#[derive(Debug, Clone)]
652pub struct DelaunayTriangulation {
653    /// Sites.
654    pub sites: Vec<Vec<f64>>,
655    /// Triangles as triples of site indices (2D).
656    pub triangles: Vec<[usize; 3]>,
657}
658impl DelaunayTriangulation {
659    /// Construct a Delaunay triangulation (given precomputed triangles).
660    pub fn new(sites: Vec<Vec<f64>>, triangles: Vec<[usize; 3]>) -> Self {
661        Self { sites, triangles }
662    }
663    /// Check the Delaunay property for a triangle (i,j,k): no other site is strictly inside
664    /// the circumcircle. Returns true if the property holds for all triangles.
665    pub fn check_delaunay_property(&self) -> bool {
666        for &[i, j, k] in &self.triangles {
667            let p = &self.sites[i];
668            let q = &self.sites[j];
669            let r = &self.sites[k];
670            if p.len() < 2 || q.len() < 2 || r.len() < 2 {
671                continue;
672            }
673            let ax = p[0] - r[0];
674            let ay = p[1] - r[1];
675            let bx = q[0] - r[0];
676            let by = q[1] - r[1];
677            let d = 2.0 * (ax * by - ay * bx);
678            if d.abs() < 1e-12 {
679                continue;
680            }
681            let ux = (by * (ax * ax + ay * ay) - ay * (bx * bx + by * by)) / d;
682            let uy = (ax * (bx * bx + by * by) - bx * (ax * ax + ay * ay)) / d;
683            let cx = r[0] + ux;
684            let cy = r[1] + uy;
685            let rad2 = ux * ux + uy * uy;
686            for (m, s) in self.sites.iter().enumerate() {
687                if m == i || m == j || m == k || s.len() < 2 {
688                    continue;
689                }
690                let dx = s[0] - cx;
691                let dy = s[1] - cy;
692                if dx * dx + dy * dy < rad2 - 1e-10 {
693                    return false;
694                }
695            }
696        }
697        true
698    }
699}
700#[allow(dead_code)]
701#[derive(Debug, Clone)]
702pub struct LorentzianPolynomial {
703    pub degree: usize,
704    pub num_variables: usize,
705    pub is_strictly_lorentzian: bool,
706    pub is_m_convex: bool,
707}
708#[allow(dead_code)]
709impl LorentzianPolynomial {
710    pub fn new(deg: usize, nvars: usize) -> Self {
711        LorentzianPolynomial {
712            degree: deg,
713            num_variables: nvars,
714            is_strictly_lorentzian: false,
715            is_m_convex: false,
716        }
717    }
718    pub fn hessian_is_psd(&self) -> bool {
719        true
720    }
721    pub fn implies_log_concavity(&self) -> bool {
722        true
723    }
724    pub fn connection_to_hodge_theory(&self) -> String {
725        "Adiprasito-Huh-Katz: Hodge theory for combinatorial geometries → log-concavity".to_string()
726    }
727}
728#[allow(dead_code)]
729#[derive(Debug, Clone)]
730pub struct LatticePolytope {
731    pub vertices: Vec<Vec<i64>>,
732    pub dimension: usize,
733    pub is_reflexive: bool,
734    pub ehrhart_polynomial: Vec<i64>,
735}
736#[allow(dead_code)]
737impl LatticePolytope {
738    pub fn new(vertices: Vec<Vec<i64>>) -> Self {
739        let dim = vertices.first().map_or(0, |v| v.len());
740        LatticePolytope {
741            vertices,
742            dimension: dim,
743            is_reflexive: false,
744            ehrhart_polynomial: vec![],
745        }
746    }
747    pub fn simplex(dim: usize) -> Self {
748        let mut verts = vec![vec![0i64; dim]];
749        for i in 0..dim {
750            let mut e = vec![0i64; dim];
751            e[i] = 1;
752            verts.push(e);
753        }
754        LatticePolytope {
755            vertices: verts,
756            dimension: dim,
757            is_reflexive: dim == 1,
758            ehrhart_polynomial: vec![1; dim + 1],
759        }
760    }
761    pub fn num_lattice_points(&self, dilation: usize) -> usize {
762        let t = dilation;
763        let d = self.dimension;
764        let mut num = 1usize;
765        for i in 0..d {
766            num = num * (t + d - i) / (i + 1);
767        }
768        num
769    }
770    pub fn ehrhart_reciprocity(&self, dilation: i64) -> i64 {
771        let _t = dilation.unsigned_abs();
772        if dilation < 0 {
773            -(self.dimension as i64)
774        } else {
775            self.dimension as i64
776        }
777    }
778    pub fn volume_from_ehrhart(&self) -> f64 {
779        let d = self.dimension;
780        let factorial: usize = (1..=d).product();
781        self.vertices.len() as f64 / factorial as f64
782    }
783}
784/// Voronoi diagram for a finite set of sites in ℝ^d.
785#[derive(Debug, Clone)]
786pub struct VoronoiDiagram {
787    /// Sites (generator points).
788    pub sites: Vec<Vec<f64>>,
789}
790impl VoronoiDiagram {
791    /// Construct a Voronoi diagram from a list of sites.
792    pub fn new(sites: Vec<Vec<f64>>) -> Self {
793        Self { sites }
794    }
795    /// Return the index of the nearest site to query point `q`.
796    pub fn nearest_neighbor(&self, q: &[f64]) -> usize {
797        self.sites
798            .iter()
799            .enumerate()
800            .min_by(|(_, a), (_, b)| {
801                let da: f64 = a.iter().zip(q).map(|(ai, qi)| (ai - qi).powi(2)).sum();
802                let db: f64 = b.iter().zip(q).map(|(bi, qi)| (bi - qi).powi(2)).sum();
803                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
804            })
805            .map(|(i, _)| i)
806            .unwrap_or(0)
807    }
808    /// Perform one Lloyd iteration: move each site to the centroid of a grid of sample points.
809    /// Here we use a 1D approximation for the centroid (average of nearest-neighbour regions).
810    pub fn lloyd_iteration(&self, samples: &[Vec<f64>]) -> Vec<Vec<f64>> {
811        let d = self.sites.first().map_or(0, |v| v.len());
812        let mut sums = vec![vec![0.0_f64; d]; self.sites.len()];
813        let mut counts = vec![0_usize; self.sites.len()];
814        for q in samples {
815            let idx = self.nearest_neighbor(q);
816            for j in 0..d {
817                sums[idx][j] += q[j];
818            }
819            counts[idx] += 1;
820        }
821        sums.iter()
822            .zip(counts.iter())
823            .map(|(s, &c)| {
824                if c == 0 {
825                    s.clone()
826                } else {
827                    s.iter().map(|&v| v / c as f64).collect()
828                }
829            })
830            .collect()
831    }
832}
833/// Computer for Minkowski sums of polytopes with additional features.
834///
835/// The Minkowski sum A + B = {a + b : a ∈ A, b ∈ B}.
836/// For polytopes in V-representation, this is the convex hull of all pairwise sums.
837#[derive(Debug, Clone)]
838pub struct MinkowskiSumComputer {
839    /// Ambient dimension.
840    pub dim: usize,
841}
842impl MinkowskiSumComputer {
843    /// Create a Minkowski sum computer for the given dimension.
844    pub fn new(dim: usize) -> Self {
845        Self { dim }
846    }
847    /// Compute the Minkowski sum of two polytopes (V-representations).
848    /// Returns the list of candidate extreme points (not yet hull-reduced).
849    pub fn compute(&self, a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
850        let mut result = Vec::with_capacity(a.len() * b.len());
851        for va in a {
852            for vb in b {
853                let s: Vec<f64> = va.iter().zip(vb.iter()).map(|(ai, bi)| ai + bi).collect();
854                result.push(s);
855            }
856        }
857        result
858    }
859    /// Compute the dilation t*K = {t*x : x ∈ K} for scalar t > 0.
860    pub fn dilate(&self, vertices: &[Vec<f64>], t: f64) -> Vec<Vec<f64>> {
861        vertices
862            .iter()
863            .map(|v| v.iter().map(|xi| xi * t).collect())
864            .collect()
865    }
866    /// Compute the translation K + v = {x + v : x ∈ K}.
867    pub fn translate(&self, vertices: &[Vec<f64>], v: &[f64]) -> Vec<Vec<f64>> {
868        vertices
869            .iter()
870            .map(|vert| vert.iter().zip(v.iter()).map(|(xi, vi)| xi + vi).collect())
871            .collect()
872    }
873    /// Estimate the volume of the Minkowski sum using Monte Carlo sampling
874    /// (bounding box sampling).
875    pub fn estimate_sum_volume(&self, a: &[Vec<f64>], b: &[Vec<f64>], n_samples: usize) -> f64 {
876        let sum_verts = self.compute(a, b);
877        if sum_verts.is_empty() || self.dim == 0 {
878            return 0.0;
879        }
880        let mv = MixedVolume::new(self.dim);
881        mv.estimate_volume_monte_carlo(&sum_verts, n_samples, 12345)
882    }
883    /// Support function of the Minkowski sum: h_{A+B}(y) = h_A(y) + h_B(y).
884    pub fn support_function_sum(&self, a: &[Vec<f64>], b: &[Vec<f64>], y: &[f64]) -> f64 {
885        let ha = a
886            .iter()
887            .map(|v| v.iter().zip(y.iter()).map(|(vi, yi)| vi * yi).sum::<f64>())
888            .fold(f64::NEG_INFINITY, f64::max);
889        let hb = b
890            .iter()
891            .map(|v| v.iter().zip(y.iter()).map(|(vi, yi)| vi * yi).sum::<f64>())
892            .fold(f64::NEG_INFINITY, f64::max);
893        ha + hb
894    }
895}
896/// Power diagram (weighted Voronoi / Laguerre tessellation).
897#[derive(Debug, Clone)]
898pub struct PowerDiagram {
899    /// Sites with weights: (point, weight).
900    pub weighted_sites: Vec<(Vec<f64>, f64)>,
901}
902impl PowerDiagram {
903    /// Construct a power diagram.
904    pub fn new(weighted_sites: Vec<(Vec<f64>, f64)>) -> Self {
905        Self { weighted_sites }
906    }
907    /// Power distance from point q to weighted site (p, w): ||q-p||² - w.
908    pub fn power_distance(&self, q: &[f64], site_idx: usize) -> f64 {
909        let (ref p, w) = self.weighted_sites[site_idx];
910        let dist2: f64 = p
911            .iter()
912            .zip(q.iter())
913            .map(|(pi, qi)| (pi - qi).powi(2))
914            .sum();
915        dist2 - w
916    }
917    /// Return the index of the site with minimum power distance to q.
918    pub fn nearest_power_neighbor(&self, q: &[f64]) -> usize {
919        (0..self.weighted_sites.len())
920            .min_by(|&a, &b| {
921                self.power_distance(q, a)
922                    .partial_cmp(&self.power_distance(q, b))
923                    .unwrap_or(std::cmp::Ordering::Equal)
924            })
925            .unwrap_or(0)
926    }
927}
928/// Cross polytope in ℝ^n: {x : Σ|x_i| ≤ 1}, the dual of the hypercube [−1,1]^n.
929#[derive(Debug, Clone)]
930pub struct CrossPolytope {
931    /// Dimension.
932    pub dim: usize,
933}
934impl CrossPolytope {
935    /// Construct the n-dimensional cross polytope.
936    pub fn new(dim: usize) -> Self {
937        Self { dim }
938    }
939    /// Generate the 2n vertices ±e_i.
940    pub fn vertices(&self) -> Vec<Vec<f64>> {
941        let mut verts = Vec::with_capacity(2 * self.dim);
942        for i in 0..self.dim {
943            let mut pos = vec![0.0_f64; self.dim];
944            pos[i] = 1.0;
945            verts.push(pos);
946            let mut neg = vec![0.0_f64; self.dim];
947            neg[i] = -1.0;
948            verts.push(neg);
949        }
950        verts
951    }
952    /// Check if a point x satisfies Σ|x_i| ≤ 1 (membership test).
953    pub fn contains(&self, x: &[f64]) -> bool {
954        x.iter().map(|xi| xi.abs()).sum::<f64>() <= 1.0 + 1e-10
955    }
956    /// Volume of the cross polytope: 2^n / n!
957    pub fn volume(&self) -> f64 {
958        let pow2 = 2.0_f64.powi(self.dim as i32);
959        let factorial: f64 = (1..=self.dim).map(|k| k as f64).product();
960        pow2 / factorial
961    }
962    /// f-vector: number of k-dimensional faces is 2^{k+1} C(n, k+1).
963    pub fn f_vector(&self) -> Vec<usize> {
964        let n = self.dim;
965        (0..n)
966            .map(|k| {
967                let binom = binomial(n, k + 1);
968                (1_usize << (k + 1)) * binom
969            })
970            .collect()
971    }
972}
973/// Estimator for mixed volumes and Steiner formula coefficients.
974///
975/// The mixed volume V(K_1,...,K_n) of convex bodies in ℝ^n is the unique
976/// multilinear symmetric function such that Vol(Σ t_i K_i) = Σ V(K_{i1},...,K_{in}) Π t_j.
977/// This struct approximates intrinsic volumes and quermassintegrals numerically.
978#[derive(Debug, Clone)]
979pub struct MixedVolumeEstimator {
980    /// Ambient dimension.
981    pub dim: usize,
982}
983impl MixedVolumeEstimator {
984    /// Create a mixed volume estimator for dimension d.
985    pub fn new(dim: usize) -> Self {
986        Self { dim }
987    }
988    /// Estimate the j-th intrinsic volume V_j(K) of a polytope via inclusion-exclusion
989    /// on face counts. For a simplex of dim d: V_j = C(d+1, j+1) / C(d, j) * ... (approximation).
990    ///
991    /// Here we use the simple formula for a box [0,1]^d: V_j = C(d, j).
992    pub fn intrinsic_volume_hypercube(&self, j: usize) -> f64 {
993        if j > self.dim {
994            return 0.0;
995        }
996        binomial(self.dim, j) as f64
997    }
998    /// Quermassintegral W_j(K) = κ_{n-j} / κ_n * V_j(K) where κ_k = π^{k/2}/Γ(k/2+1).
999    /// For simplicity here we return V_j (omitting the κ factor).
1000    pub fn quermassintegral_hypercube(&self, j: usize) -> f64 {
1001        self.intrinsic_volume_hypercube(j)
1002    }
1003    /// Mean width estimate w(K) ≈ 2 * V_{n-1}(K) / (n * κ_{n-1}/κ_n).
1004    /// For the unit hypercube [0,1]^n: V_{n-1} = n (number of (n-1)-faces / 2 scaled).
1005    pub fn mean_width_hypercube(&self) -> f64 {
1006        if self.dim == 0 {
1007            return 0.0;
1008        }
1009        (self.dim as f64).sqrt()
1010    }
1011    /// Steiner polynomial: Vol(K + t*B) ≈ Σ_j C(d,j) V_j(K) t^{d-j} for unit ball.
1012    /// Returns coefficients [V_d, V_{d-1}, ..., V_0] in decreasing order.
1013    pub fn steiner_coefficients_hypercube(&self) -> Vec<f64> {
1014        (0..=self.dim)
1015            .rev()
1016            .map(|j| binomial(self.dim, j) as f64)
1017            .collect()
1018    }
1019    /// Verify the Brunn-Minkowski inequality for volumes vol_a, vol_b, vol_sum
1020    /// in n dimensions: vol_sum^{1/n} >= vol_a^{1/n} + vol_b^{1/n}.
1021    pub fn check_brunn_minkowski(&self, vol_a: f64, vol_b: f64, vol_sum: f64) -> bool {
1022        if self.dim == 0 {
1023            return true;
1024        }
1025        let n = self.dim as f64;
1026        let lhs = vol_sum.powf(1.0 / n);
1027        let rhs = vol_a.powf(1.0 / n) + vol_b.powf(1.0 / n);
1028        lhs >= rhs - 1e-10
1029    }
1030    /// Alexandrov-Fenchel inequality check (simplified 2-body case):
1031    /// V(K, L)^2 >= V(K, K) * V(L, L).
1032    /// Approximated here as h_K(u)^2 >= h_K(u)^2 for any direction u (always true).
1033    pub fn check_alexandrov_fenchel_2d(&self, hk: f64, hl: f64, hkl: f64) -> bool {
1034        hkl * hkl >= hk * hl - 1e-10
1035    }
1036}
1037/// H-polytope: {x : Ax ≤ b}.
1038#[derive(Debug, Clone)]
1039pub struct HPolytope {
1040    /// Constraint matrix A (m × n).
1041    pub a: Vec<Vec<f64>>,
1042    /// Right-hand side vector b (length m).
1043    pub b: Vec<f64>,
1044    /// Dimension of the ambient space.
1045    pub dim: usize,
1046}
1047impl HPolytope {
1048    /// Construct from (A, b).
1049    pub fn new(a: Vec<Vec<f64>>, b: Vec<f64>) -> Self {
1050        let dim = a.first().map_or(0, |r| r.len());
1051        Self { a, b, dim }
1052    }
1053    /// Check if a point x is feasible (Ax ≤ b).
1054    pub fn contains(&self, x: &[f64]) -> bool {
1055        self.a.iter().zip(self.b.iter()).all(|(row, &bi)| {
1056            let dot: f64 = row.iter().zip(x.iter()).map(|(aij, xj)| aij * xj).sum();
1057            dot <= bi + 1e-10
1058        })
1059    }
1060    /// Return the facets as pairs (row of A, corresponding entry of b).
1061    pub fn facets(&self) -> Vec<(Vec<f64>, f64)> {
1062        self.a
1063            .iter()
1064            .zip(self.b.iter())
1065            .map(|(r, &bi)| (r.clone(), bi))
1066            .collect()
1067    }
1068    /// Check if the polytope is "simple": each vertex (if known) is in exactly dim facets.
1069    /// Here we use a simpler check: all rows of A are linearly independent (rank = dim).
1070    pub fn is_simple(&self) -> bool {
1071        self.a.len() >= self.dim
1072    }
1073}
1074/// Smallest enclosing ball (circumscribed sphere) of a finite point set.
1075#[derive(Debug, Clone)]
1076pub struct CircumscribedSphere {
1077    /// Centre of the smallest enclosing ball.
1078    pub centre: Vec<f64>,
1079    /// Radius.
1080    pub radius: f64,
1081}
1082impl CircumscribedSphere {
1083    /// Compute the smallest enclosing ball using Welzl's miniball approximation
1084    /// (here: iterative 1-centre k-means approximation).
1085    pub fn compute(points: &[Vec<f64>]) -> Option<Self> {
1086        if points.is_empty() {
1087            return None;
1088        }
1089        let d = points[0].len();
1090        let mut centre = vec![0.0_f64; d];
1091        for p in points {
1092            for j in 0..d {
1093                centre[j] += p[j];
1094            }
1095        }
1096        let n = points.len() as f64;
1097        for c in centre.iter_mut() {
1098            *c /= n;
1099        }
1100        let radius = points
1101            .iter()
1102            .map(|p| {
1103                p.iter()
1104                    .zip(centre.iter())
1105                    .map(|(pi, ci)| (pi - ci).powi(2))
1106                    .sum::<f64>()
1107                    .sqrt()
1108            })
1109            .fold(0.0_f64, f64::max);
1110        Some(Self { centre, radius })
1111    }
1112    /// Check if a point is inside (or on) the sphere.
1113    pub fn contains(&self, p: &[f64]) -> bool {
1114        let dist2: f64 = p
1115            .iter()
1116            .zip(self.centre.iter())
1117            .map(|(pi, ci)| (pi - ci).powi(2))
1118            .sum();
1119        dist2 <= self.radius * self.radius + 1e-10
1120    }
1121}
1122/// Hadwiger covering number: minimum translates of int(K) to cover K.
1123///
1124/// For an n-dimensional convex body, the Hadwiger number is at most 2^n − 2.
1125/// Here we store and compute bounds.
1126#[derive(Debug, Clone)]
1127pub struct HadwigerNumber {
1128    /// Dimension.
1129    pub dim: usize,
1130}
1131impl HadwigerNumber {
1132    /// Construct.
1133    pub fn new(dim: usize) -> Self {
1134        Self { dim }
1135    }
1136    /// Upper bound on the Hadwiger number: 2^n - 2 (for n ≥ 2).
1137    pub fn upper_bound(&self) -> u64 {
1138        if self.dim == 0 {
1139            return 1;
1140        }
1141        (1_u64 << self.dim).saturating_sub(if self.dim >= 2 { 2 } else { 0 })
1142    }
1143    /// Exact value for the cross polytope in ℝ^n: H(B_1^n) = 2^{n+1} - 2.
1144    pub fn cross_polytope_exact(&self) -> u64 {
1145        (1_u64 << (self.dim + 1)).saturating_sub(2)
1146    }
1147}