Skip to main content

oxiz_math/
hilbert.rs

1//! Hilbert basis computation for integer cones.
2//!
3//! This module implements algorithms for computing Hilbert bases of rational
4//! polyhedral cones, which is fundamental for integer programming and
5//! polyhedral computations in SMT solving.
6//!
7//! A Hilbert basis is a minimal generating set for the set of integer points
8//! in a pointed rational polyhedral cone.
9//!
10//! Reference: Z3's arithmetic theories and polyhedral computation literature.
11
12#[allow(unused_imports)]
13use crate::prelude::*;
14use num_bigint::BigInt;
15use num_rational::BigRational;
16use num_traits::{One, Signed, Zero};
17
18/// A vector in integer space.
19pub type IntVector = Vec<BigInt>;
20
21/// A vector in rational space.
22pub type RatVector = Vec<BigRational>;
23
24/// A rational polyhedral cone defined by a system of inequalities: Ax >= 0.
25#[derive(Debug, Clone)]
26pub struct Cone {
27    /// Constraint matrix A.
28    constraints: Vec<RatVector>,
29    /// Dimension of the ambient space.
30    dimension: usize,
31}
32
33impl Cone {
34    /// Create a new cone from constraint inequalities.
35    ///
36    /// Each constraint is a vector a such that a^T x >= 0.
37    pub fn new(constraints: Vec<RatVector>) -> Self {
38        let dimension = if constraints.is_empty() {
39            0
40        } else {
41            constraints[0].len()
42        };
43
44        Self {
45            constraints,
46            dimension,
47        }
48    }
49
50    /// Get the dimension of the ambient space.
51    pub fn dimension(&self) -> usize {
52        self.dimension
53    }
54
55    /// Check if a point is in the cone.
56    pub fn contains(&self, point: &RatVector) -> bool {
57        if point.len() != self.dimension {
58            return false;
59        }
60
61        for constraint in &self.constraints {
62            let mut dot = BigRational::zero();
63            for i in 0..self.dimension {
64                dot += &constraint[i] * &point[i];
65            }
66            if dot < BigRational::zero() {
67                return false;
68            }
69        }
70
71        true
72    }
73}
74
75/// Compute the Hilbert basis of a rational polyhedral cone.
76///
77/// This uses a simplified version of the algorithm based on Normaliz.
78/// The full implementation would use more sophisticated techniques like
79/// triangulation and unimodular cones.
80pub fn hilbert_basis(cone: &Cone) -> Vec<IntVector> {
81    if cone.dimension == 0 {
82        return vec![];
83    }
84
85    // Start with extreme rays (simplified: use unit vectors in positive orthant)
86    let mut basis = Vec::new();
87    let mut candidates = Vec::new();
88
89    // Generate initial candidates from unit vectors
90    for i in 0..cone.dimension {
91        let mut v = vec![BigInt::zero(); cone.dimension];
92        v[i] = BigInt::one();
93
94        let rat_v: RatVector = v
95            .iter()
96            .map(|x| BigRational::from_integer(x.clone()))
97            .collect();
98        if cone.contains(&rat_v) {
99            candidates.push(v);
100        }
101    }
102
103    // Compute Hilbert basis using a simplified algorithm
104    // In a full implementation, this would use cone decomposition
105    let max_iterations = 100;
106    let mut iteration = 0;
107
108    while !candidates.is_empty() && iteration < max_iterations {
109        iteration += 1;
110
111        let candidate = candidates
112            .pop()
113            .expect("collection validated to be non-empty");
114
115        // Check if this is a primitive vector (not a multiple of another)
116        if is_primitive(&candidate) {
117            // Check if this vector is already in the basis or can be generated
118            if !is_generated_by(&candidate, &basis) {
119                basis.push(candidate.clone());
120
121                // Generate new candidates by adding to existing basis elements
122                for base_vec in &basis {
123                    if base_vec != &candidate {
124                        let sum = add_vectors(&candidate, base_vec);
125                        let rat_sum: RatVector = sum
126                            .iter()
127                            .map(|x| BigRational::from_integer(x.clone()))
128                            .collect();
129
130                        if cone.contains(&rat_sum) && !candidates.contains(&sum) {
131                            // Limit the size of candidates to prevent explosion
132                            if candidates.len() < 1000 && vector_norm(&sum) < BigInt::from(100) {
133                                candidates.push(sum);
134                            }
135                        }
136                    }
137                }
138            }
139        }
140    }
141
142    // Filter to ensure minimality
143    minimize_basis(basis)
144}
145
146/// Check if a vector is primitive (GCD of components is 1).
147fn is_primitive(v: &IntVector) -> bool {
148    if v.is_empty() {
149        return false;
150    }
151
152    let mut g = v[0].clone();
153    for component in v.iter().skip(1) {
154        g = gcd(&g, component);
155        if g == BigInt::one() {
156            return true;
157        }
158    }
159
160    g == BigInt::one()
161}
162
163/// Compute GCD of two BigInts.
164fn gcd(a: &BigInt, b: &BigInt) -> BigInt {
165    let mut a = a.abs();
166    let mut b = b.abs();
167
168    while !b.is_zero() {
169        let temp = b.clone();
170        b = &a % &b;
171        a = temp;
172    }
173
174    a
175}
176
177/// Check if a vector is generated by (is a non-negative integer combination of) a set of vectors.
178fn is_generated_by(v: &IntVector, generators: &[IntVector]) -> bool {
179    if generators.is_empty() {
180        return v.iter().all(|x| x.is_zero());
181    }
182
183    // Simple check: if v is a scalar multiple of any generator
184    for generator in generators {
185        if is_multiple(v, generator) {
186            return true;
187        }
188    }
189
190    // For a complete implementation, we would solve an integer programming problem
191    // For now, use a simplified heuristic
192    false
193}
194
195/// Check if v1 is a non-negative integer multiple of v2.
196fn is_multiple(v1: &IntVector, v2: &IntVector) -> bool {
197    if v1.len() != v2.len() {
198        return false;
199    }
200
201    // Special case: zero vector is a multiple of any vector (0 * v2 = 0)
202    if v1.iter().all(|x| x.is_zero()) {
203        return true;
204    }
205
206    // Find the first non-zero component in v2
207    let mut ratio: Option<BigRational> = None;
208
209    for i in 0..v1.len() {
210        if !v2[i].is_zero() {
211            let r = BigRational::new(v1[i].clone(), v2[i].clone());
212            if let Some(ref existing_ratio) = ratio {
213                if &r != existing_ratio {
214                    return false;
215                }
216            } else {
217                // Check if ratio is a positive integer
218                if r < BigRational::zero() || !r.is_integer() {
219                    return false;
220                }
221                ratio = Some(r);
222            }
223        } else if !v1[i].is_zero() {
224            return false;
225        }
226    }
227
228    ratio.is_some()
229}
230
231/// Add two integer vectors component-wise.
232fn add_vectors(v1: &IntVector, v2: &IntVector) -> IntVector {
233    assert_eq!(v1.len(), v2.len());
234    v1.iter().zip(v2.iter()).map(|(a, b)| a + b).collect()
235}
236
237/// Compute the L1 norm of a vector.
238fn vector_norm(v: &IntVector) -> BigInt {
239    v.iter().map(|x| x.abs()).sum()
240}
241
242/// Remove redundant vectors from a Hilbert basis to ensure minimality.
243fn minimize_basis(mut basis: Vec<IntVector>) -> Vec<IntVector> {
244    let mut minimal = Vec::new();
245
246    // Sort by norm to process smaller vectors first
247    basis.sort_by(|a, b| {
248        let norm_a = vector_norm(a);
249        let norm_b = vector_norm(b);
250        norm_a.cmp(&norm_b)
251    });
252
253    for vec in basis {
254        // Check if this vector is generated by vectors already in minimal basis
255        if !is_generated_by(&vec, &minimal) {
256            minimal.push(vec);
257        }
258    }
259
260    minimal
261}
262
263/// Integer cone operations.
264pub struct IntCone {
265    /// Hilbert basis generators.
266    generators: Vec<IntVector>,
267    /// Dimension of the cone.
268    dimension: usize,
269}
270
271impl IntCone {
272    /// Create a new integer cone from its Hilbert basis.
273    pub fn from_hilbert_basis(generators: Vec<IntVector>) -> Self {
274        let dimension = if generators.is_empty() {
275            0
276        } else {
277            generators[0].len()
278        };
279
280        Self {
281            generators,
282            dimension,
283        }
284    }
285
286    /// Get the generators.
287    pub fn generators(&self) -> &[IntVector] {
288        &self.generators
289    }
290
291    /// Get the dimension.
292    pub fn dimension(&self) -> usize {
293        self.dimension
294    }
295
296    /// Check if a point is in the integer cone.
297    pub fn contains_int(&self, point: &IntVector) -> bool {
298        // Check if point can be expressed as a non-negative integer combination
299        // of generators. This is a simplified check.
300        if point.len() != self.dimension {
301            return false;
302        }
303
304        // Trivial case: zero vector
305        if point.iter().all(|x| x.is_zero()) {
306            return true;
307        }
308
309        // Check if point is one of the generators or a multiple
310        for generator in &self.generators {
311            if is_multiple(point, generator) {
312                return true;
313            }
314        }
315
316        // For a complete implementation, solve an integer programming problem
317        false
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    fn int_vec(values: &[i64]) -> IntVector {
326        values.iter().map(|&x| BigInt::from(x)).collect()
327    }
328
329    fn rat_vec(values: &[i64]) -> RatVector {
330        values
331            .iter()
332            .map(|&x| BigRational::from_integer(BigInt::from(x)))
333            .collect()
334    }
335
336    #[test]
337    fn test_cone_contains() {
338        // Positive orthant in 2D: x >= 0, y >= 0
339        let constraints = vec![rat_vec(&[1, 0]), rat_vec(&[0, 1])];
340        let cone = Cone::new(constraints);
341
342        assert!(cone.contains(&rat_vec(&[1, 1])));
343        assert!(cone.contains(&rat_vec(&[0, 0])));
344        assert!(!cone.contains(&rat_vec(&[-1, 1])));
345        assert!(!cone.contains(&rat_vec(&[1, -1])));
346    }
347
348    #[test]
349    fn test_is_primitive() {
350        assert!(is_primitive(&int_vec(&[1, 2, 3])));
351        assert!(is_primitive(&int_vec(&[1, 0, 0])));
352        assert!(!is_primitive(&int_vec(&[2, 4, 6])));
353        assert!(is_primitive(&int_vec(&[3, 5, 7])));
354    }
355
356    #[test]
357    fn test_gcd() {
358        assert_eq!(gcd(&BigInt::from(12), &BigInt::from(8)), BigInt::from(4));
359        assert_eq!(gcd(&BigInt::from(7), &BigInt::from(3)), BigInt::from(1));
360        assert_eq!(gcd(&BigInt::from(0), &BigInt::from(5)), BigInt::from(5));
361    }
362
363    #[test]
364    fn test_is_multiple() {
365        assert!(is_multiple(&int_vec(&[2, 4]), &int_vec(&[1, 2])));
366        assert!(!is_multiple(&int_vec(&[2, 5]), &int_vec(&[1, 2])));
367        assert!(is_multiple(&int_vec(&[0, 0]), &int_vec(&[1, 2])));
368        assert!(!is_multiple(&int_vec(&[1, 2]), &int_vec(&[0, 0])));
369    }
370
371    #[test]
372    fn test_add_vectors() {
373        let v1 = int_vec(&[1, 2, 3]);
374        let v2 = int_vec(&[4, 5, 6]);
375        let sum = add_vectors(&v1, &v2);
376        assert_eq!(sum, int_vec(&[5, 7, 9]));
377    }
378
379    #[test]
380    fn test_vector_norm() {
381        assert_eq!(vector_norm(&int_vec(&[1, -2, 3])), BigInt::from(6));
382        assert_eq!(vector_norm(&int_vec(&[0, 0, 0])), BigInt::from(0));
383    }
384
385    #[test]
386    fn test_hilbert_basis_simple() {
387        // Positive orthant in 2D
388        let constraints = vec![rat_vec(&[1, 0]), rat_vec(&[0, 1])];
389        let cone = Cone::new(constraints);
390
391        let basis = hilbert_basis(&cone);
392
393        // Should contain at least the unit vectors
394        assert!(!basis.is_empty());
395        assert!(basis.len() >= 2);
396    }
397
398    #[test]
399    fn test_int_cone() {
400        let generators = vec![int_vec(&[1, 0]), int_vec(&[0, 1])];
401        let cone = IntCone::from_hilbert_basis(generators);
402
403        assert_eq!(cone.dimension(), 2);
404        assert!(cone.contains_int(&int_vec(&[0, 0])));
405        assert!(cone.contains_int(&int_vec(&[1, 0])));
406        assert!(cone.contains_int(&int_vec(&[2, 0])));
407    }
408}