Skip to main content

ocas_poly/groebner/
mod.rs

1//! Gröbner basis computation for multivariate polynomial ideals.
2//!
3//! Provides two algorithms:
4//!
5//! - **Buchberger** ([`buchberger`]) — classic S-polynomial iteration with
6//!   Gebauer-Moeller optimization. Suitable for small ideals.
7//! - **F4** ([`f4::f4`]) — matrix-based algorithm from Faugère (1999).
8//!   Dramatically faster for larger ideals by batching S-polynomial
9//!   reductions into sparse matrix row operations.
10//!
11//! Both algorithms produce a reduced Gröbner basis. The F4 algorithm is
12//! recommended for production use and is the default in
13//! [`solve_polynomial_system`](ocas_calc::solve::solve_polynomial_system).
14
15pub mod f4;
16
17use std::collections::HashSet;
18
19use ocas_domain::Domain;
20
21use crate::sparse::{
22    MonomialOrder, SparseMultivariatePolynomial, monomial_are_coprime, monomial_divides,
23};
24
25/// A Gröbner basis for a polynomial ideal.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct GroebnerBasis<D: Domain, O: MonomialOrder> {
28    /// The polynomials forming the basis.
29    pub basis: Vec<SparseMultivariatePolynomial<D, O>>,
30}
31
32impl<D: Domain, O: MonomialOrder> GroebnerBasis<D, O> {
33    /// Compute a Gröbner basis from a set of generators using Buchberger's algorithm.
34    ///
35    /// Requires that the coefficient domain supports exact division (i.e., is
36    /// effectively a field). The algorithm will panic if division fails.
37    ///
38    /// # Example
39    ///
40    /// ```
41    /// use ocas_domain::{RationalDomain, Rational};
42    /// use ocas_poly::sparse::Lex;
43    /// use ocas_poly::GroebnerBasis;
44    /// use ocas_poly::SparseMultivariatePolynomial;
45    ///
46    /// let d = RationalDomain;
47    /// // ideal: x + y, x - y
48    /// let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
49    ///     (vec![1, 0], Rational::new(1, 1)),
50    ///     (vec![0, 1], Rational::new(1, 1)),
51    /// ]);
52    /// let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
53    ///     (vec![1, 0], Rational::new(1, 1)),
54    ///     (vec![0, 1], Rational::new(-1, 1)),
55    /// ]);
56    /// let gb = GroebnerBasis::buchberger(&[f1, f2]);
57    /// assert!(gb.basis.len() >= 2);
58    /// ```
59    pub fn buchberger(ideal: &[SparseMultivariatePolynomial<D, O>]) -> Self {
60        // Filter out zero polynomials.
61        let mut basis: Vec<SparseMultivariatePolynomial<D, O>> =
62            ideal.iter().filter(|p| !p.is_zero()).cloned().collect();
63        if basis.is_empty() {
64            return Self { basis };
65        }
66
67        // Collect critical pairs: all unordered pairs (i, j) with i < j.
68        let mut pairs: HashSet<(usize, usize)> = HashSet::new();
69        for i in 0..basis.len() {
70            for j in i + 1..basis.len() {
71                pairs.insert((i, j));
72            }
73        }
74
75        let max_iter = 10000;
76
77        for _ in 0..max_iter {
78            if pairs.is_empty() {
79                break;
80            }
81            let (i, j) = *pairs.iter().next().unwrap();
82            pairs.remove(&(i, j));
83
84            // Buchberger's first criterion: if the leading monomials are
85            // coprime, the S-polynomial reduces to zero, so skip.
86            let lm_i = basis[i].leading_monomial();
87            let lm_j = basis[j].leading_monomial();
88            if let (Some(mi), Some(mj)) = (&lm_i, &lm_j)
89                && monomial_are_coprime(mi, mj)
90            {
91                continue;
92            }
93
94            // Compute S-polynomial and reduce by current basis.
95            let s = basis[i].spoly(&basis[j]);
96            let r = s.reduce(&basis);
97
98            if !r.is_zero() {
99                let new_idx = basis.len();
100                basis.push(r);
101                for k in 0..new_idx {
102                    pairs.insert((k, new_idx));
103                }
104            }
105        }
106
107        Self { basis }
108    }
109
110    /// Minimize the basis: remove polynomials whose leading monomial is
111    /// divisible by another element's leading monomial.
112    pub fn minimize(mut self) -> Self {
113        let lms: Vec<_> = self
114            .basis
115            .iter()
116            .filter_map(|p| p.leading_monomial().cloned())
117            .collect();
118
119        let mut keep = vec![true; self.basis.len()];
120        for i in 0..self.basis.len() {
121            for j in 0..self.basis.len() {
122                // Remove i if lms[j] divides lms[i] (i.e., lms[i] is a
123                // multiple of lms[j], making i redundant).
124                // monomial_divides(big, small) returns true when small divides big.
125                if i != j && keep[i] && keep[j] && monomial_divides(&lms[i], &lms[j]) {
126                    keep[i] = false;
127                    break;
128                }
129            }
130        }
131
132        self.basis = self
133            .basis
134            .into_iter()
135            .enumerate()
136            .filter(|(i, _)| keep[*i])
137            .map(|(_, p)| p)
138            .collect();
139
140        self
141    }
142
143    /// Inter-reduce the basis: reduce each element by the others and make
144    /// each polynomial monic.
145    ///
146    /// The algorithm processes elements in ascending order of leading
147    /// monomial. Each element is reduced by all elements with strictly
148    /// smaller leading monomials (those already in the result set).
149    /// This ensures the standard reduced Gröbner basis property:
150    /// no monomial of any basis element is divisible by the leading
151    /// monomial of any other basis element.
152    pub fn auto_reduce(mut self) -> Self {
153        // Sort basis in ascending order of leading monomial (smallest first).
154        self.basis
155            .sort_by(|a, b| match (a.leading_monomial(), b.leading_monomial()) {
156                (Some(ma), Some(mb)) => O::cmp(ma, mb),
157                (Some(_), None) => std::cmp::Ordering::Greater,
158                (None, Some(_)) => std::cmp::Ordering::Less,
159                (None, None) => std::cmp::Ordering::Equal,
160            });
161
162        let mut reduced: Vec<SparseMultivariatePolynomial<D, O>> = Vec::new();
163
164        for poly in &self.basis {
165            // Reduce `poly` by all elements already in `reduced`
166            // (which have smaller leading monomials).
167            let mut r = poly.reduce(&reduced);
168            if !r.is_zero() {
169                if let Some(lc) = r.leading_coeff().cloned()
170                    && let Some(inv) = r.domain().inv(&lc)
171                {
172                    r = r.mul_scalar(&inv);
173                }
174                reduced.push(r);
175            }
176        }
177
178        self.basis = reduced;
179        self
180    }
181
182    /// Verify that this is indeed a Gröbner basis by checking that all
183    /// S-polynomials reduce to zero.
184    pub fn is_groebner_basis(&self) -> bool {
185        for i in 0..self.basis.len() {
186            for j in i + 1..self.basis.len() {
187                let s = self.basis[i].spoly(&self.basis[j]);
188                let r = s.reduce(&self.basis);
189                if !r.is_zero() {
190                    return false;
191                }
192            }
193        }
194        true
195    }
196}
197
198/// Convenience: compute a Gröbner basis and inter-reduce it.
199pub fn buchberger<D: Domain, O: MonomialOrder>(
200    ideal: &[SparseMultivariatePolynomial<D, O>],
201) -> GroebnerBasis<D, O> {
202    GroebnerBasis::buchberger(ideal).minimize().auto_reduce()
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::sparse::Lex;
209    use ocas_domain::{Rational, RationalDomain};
210
211    fn r(n: i64, d: i64) -> Rational {
212        Rational::new(n, d)
213    }
214
215    fn make_poly(
216        terms: Vec<(Vec<usize>, Rational)>,
217    ) -> SparseMultivariatePolynomial<RationalDomain, Lex> {
218        SparseMultivariatePolynomial::from_terms(RationalDomain, 2, terms)
219    }
220
221    #[test]
222    fn empty_ideal() {
223        let gb = buchberger::<RationalDomain, Lex>(&[]);
224        assert!(gb.basis.is_empty());
225    }
226
227    #[test]
228    fn single_polynomial() {
229        // f = x^2 - 1
230        let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
231            RationalDomain,
232            1,
233            vec![(vec![2], r(1, 1)), (vec![0], r(-1, 1))],
234        );
235        let gb = buchberger(&[f]);
236        assert_eq!(gb.basis.len(), 1);
237        assert!(gb.is_groebner_basis());
238    }
239
240    #[test]
241    fn linear_system() {
242        // x + y = 0, x - y = 0  →  basis = {x, y}
243        let f1 = make_poly(vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(1, 1))]);
244        let f2 = make_poly(vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(-1, 1))]);
245        let gb = buchberger(&[f1, f2]);
246        assert!(gb.is_groebner_basis());
247        // After auto-reduce, we expect {x, y} (monic leading terms)
248        assert!(gb.basis.len() >= 2);
249    }
250
251    #[test]
252    fn two_variable_ideal() {
253        // x^2 - y, x^3 - x  (elimination ideal: y = x^2, x^3 = x → x ∈ {0, ±1})
254        let f1 = make_poly(vec![(vec![2, 0], r(1, 1)), (vec![0, 1], r(-1, 1))]);
255        let f2 = make_poly(vec![(vec![3, 0], r(1, 1)), (vec![1, 0], r(-1, 1))]);
256        let gb = buchberger(&[f1, f2]);
257        assert!(gb.is_groebner_basis());
258        assert!(!gb.basis.is_empty());
259    }
260}