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}