ocas_poly/groebner/mod.rs
1//! Gröbner basis computation for multivariate polynomial ideals.
2//!
3//! Provides three algorithms, all reachable through the unified
4//! [`groebner_basis`] entry point with an [`Algorithm`] selector:
5//!
6//! - **Buchberger** ([`buchberger`]) — classic S-polynomial iteration with
7//! Gebauer-Moeller optimization. Suitable for small ideals.
8//! - **F4** ([`f4::f4`]) — matrix-based algorithm from Faugère (1999).
9//! Dramatically faster for larger ideals by batching S-polynomial
10//! reductions into sparse matrix row operations.
11//! - **F5** ([`f5::f5`]) — signature-based algorithm from Faugère (2002).
12//! Rejects zero-reducers *before* matrix construction via syzygy
13//! criteria, targeting order-of-magnitude speedups on difficult ideals
14//! (e.g. cyclic-n). Production-grade since 0.19.0.
15//!
16//! All algorithms produce a reduced Gröbner basis. [`Algorithm::Auto`]
17//! selects a backend by heuristic (currently F4).
18
19pub mod f4;
20pub mod f5;
21pub mod fglm;
22pub mod hilbert;
23
24use ocas_core::FastHashSet as HashSet;
25use ocas_domain::Domain;
26
27use crate::sparse::{
28 MonomialOrder, SparseMultivariatePolynomial, monomial_are_coprime, monomial_divides,
29};
30
31/// A Gröbner basis for a polynomial ideal.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct GroebnerBasis<D: Domain, O: MonomialOrder> {
34 /// The polynomials forming the basis.
35 pub basis: Vec<SparseMultivariatePolynomial<D, O>>,
36}
37
38impl<D: Domain, O: MonomialOrder> GroebnerBasis<D, O> {
39 /// Compute a Gröbner basis from a set of generators using Buchberger's algorithm.
40 ///
41 /// Requires that the coefficient domain supports exact division (i.e., is
42 /// effectively a field). The algorithm will panic if division fails.
43 ///
44 /// # Example
45 ///
46 /// ```
47 /// use ocas_domain::{RationalDomain, Rational};
48 /// use ocas_poly::sparse::Lex;
49 /// use ocas_poly::GroebnerBasis;
50 /// use ocas_poly::SparseMultivariatePolynomial;
51 ///
52 /// let d = RationalDomain;
53 /// // ideal: x + y, x - y
54 /// let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
55 /// (vec![1, 0], Rational::new(1, 1)),
56 /// (vec![0, 1], Rational::new(1, 1)),
57 /// ]);
58 /// let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
59 /// (vec![1, 0], Rational::new(1, 1)),
60 /// (vec![0, 1], Rational::new(-1, 1)),
61 /// ]);
62 /// let gb = GroebnerBasis::buchberger(&[f1, f2]);
63 /// assert!(gb.basis.len() >= 2);
64 /// ```
65 pub fn buchberger(ideal: &[SparseMultivariatePolynomial<D, O>]) -> Self {
66 // Filter out zero polynomials.
67 let mut basis: Vec<SparseMultivariatePolynomial<D, O>> =
68 ideal.iter().filter(|p| !p.is_zero()).cloned().collect();
69 if basis.is_empty() {
70 return Self { basis };
71 }
72
73 // Collect critical pairs: all unordered pairs (i, j) with i < j.
74 let mut pairs: HashSet<(usize, usize)> = HashSet::default();
75 for i in 0..basis.len() {
76 for j in i + 1..basis.len() {
77 pairs.insert((i, j));
78 }
79 }
80
81 let max_iter = 10000;
82
83 for _ in 0..max_iter {
84 if pairs.is_empty() {
85 break;
86 }
87 let (i, j) = *pairs.iter().next().unwrap();
88 pairs.remove(&(i, j));
89
90 // Buchberger's first criterion: if the leading monomials are
91 // coprime, the S-polynomial reduces to zero, so skip.
92 let lm_i = basis[i].leading_monomial();
93 let lm_j = basis[j].leading_monomial();
94 if let (Some(mi), Some(mj)) = (&lm_i, &lm_j)
95 && monomial_are_coprime(mi, mj)
96 {
97 continue;
98 }
99
100 // Compute S-polynomial and reduce by current basis.
101 let s = basis[i].spoly(&basis[j]);
102 let r = s.reduce(&basis);
103
104 if !r.is_zero() {
105 let new_idx = basis.len();
106 basis.push(r);
107 for k in 0..new_idx {
108 pairs.insert((k, new_idx));
109 }
110 }
111 }
112
113 Self { basis }
114 }
115
116 /// Minimize the basis: remove polynomials whose leading monomial is
117 /// divisible by another element's leading monomial.
118 pub fn minimize(mut self) -> Self {
119 let lms: Vec<_> = self
120 .basis
121 .iter()
122 .filter_map(|p| p.leading_monomial().cloned())
123 .collect();
124
125 let mut keep = vec![true; self.basis.len()];
126 for i in 0..self.basis.len() {
127 for j in 0..self.basis.len() {
128 // Remove i if lms[j] divides lms[i] (i.e., lms[i] is a
129 // multiple of lms[j], making i redundant).
130 // monomial_divides(big, small) returns true when small divides big.
131 if i != j && keep[i] && keep[j] && monomial_divides(&lms[i], &lms[j]) {
132 keep[i] = false;
133 break;
134 }
135 }
136 }
137
138 self.basis = self
139 .basis
140 .into_iter()
141 .enumerate()
142 .filter(|(i, _)| keep[*i])
143 .map(|(_, p)| p)
144 .collect();
145
146 self
147 }
148
149 /// Inter-reduce the basis: reduce each element by the others and make
150 /// each polynomial monic.
151 ///
152 /// The algorithm processes elements in ascending order of leading
153 /// monomial. Each element is reduced by all elements with strictly
154 /// smaller leading monomials (those already in the result set).
155 /// This ensures the standard reduced Gröbner basis property:
156 /// no monomial of any basis element is divisible by the leading
157 /// monomial of any other basis element.
158 pub fn auto_reduce(mut self) -> Self {
159 let order = self
160 .basis
161 .first()
162 .map(|p| p.order.clone())
163 .unwrap_or_default();
164 // Sort basis in ascending order of leading monomial (smallest first).
165 self.basis
166 .sort_by(|a, b| match (a.leading_monomial(), b.leading_monomial()) {
167 (Some(ma), Some(mb)) => order.cmp(ma, mb),
168 (Some(_), None) => std::cmp::Ordering::Greater,
169 (None, Some(_)) => std::cmp::Ordering::Less,
170 (None, None) => std::cmp::Ordering::Equal,
171 });
172
173 let mut reduced: Vec<SparseMultivariatePolynomial<D, O>> = Vec::new();
174
175 for poly in &self.basis {
176 // Reduce `poly` by all elements already in `reduced`
177 // (which have smaller leading monomials).
178 let mut r = poly.reduce(&reduced);
179 if !r.is_zero() {
180 if let Some(lc) = r.leading_coeff().cloned()
181 && let Some(inv) = r.domain().inv(&lc)
182 {
183 r = r.mul_scalar(&inv);
184 }
185 reduced.push(r);
186 }
187 }
188
189 self.basis = reduced;
190 self
191 }
192
193 /// Verify that this is indeed a Gröbner basis by checking that all
194 /// S-polynomials reduce to zero.
195 pub fn is_groebner_basis(&self) -> bool {
196 for i in 0..self.basis.len() {
197 for j in i + 1..self.basis.len() {
198 let s = self.basis[i].spoly(&self.basis[j]);
199 let r = s.reduce(&self.basis);
200 if !r.is_zero() {
201 return false;
202 }
203 }
204 }
205 true
206 }
207
208 /// Change the monomial order of this Gröbner basis.
209 ///
210 /// The polynomials are re-interpreted under the target order `O2`
211 /// and the F4 algorithm is re-run. This is the simple reorder path
212 /// (Symbolica's `reorder::<Order>()`). For zero-dimensional ideals,
213 /// use [`crate::groebner::fglm::fglm`] for a much faster conversion.
214 ///
215 /// # Example
216 ///
217 /// ```
218 /// use ocas_domain::{RationalDomain, Rational};
219 /// use ocas_poly::sparse::{Grevlex, Lex};
220 /// use ocas_poly::{GroebnerBasis, SparseMultivariatePolynomial, f4};
221 ///
222 /// let d = RationalDomain;
223 /// // ideal: x + y, x - y → basis {y, x} under Lex
224 /// let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
225 /// (vec![1, 0], Rational::new(1, 1)),
226 /// (vec![0, 1], Rational::new(1, 1)),
227 /// ]);
228 /// let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
229 /// (vec![1, 0], Rational::new(1, 1)),
230 /// (vec![0, 1], Rational::new(-1, 1)),
231 /// ]);
232 /// let gb_lex = f4::f4(&[f1, f2]);
233 /// let gb_grevlex = gb_lex.reorder::<Grevlex>();
234 /// assert!(gb_grevlex.is_groebner_basis());
235 /// ```
236 pub fn reorder<O2: MonomialOrder>(&self) -> GroebnerBasis<D, O2>
237 where
238 D: 'static,
239 {
240 let converted: Vec<SparseMultivariatePolynomial<D, O2>> = self
241 .basis
242 .iter()
243 .map(|p| {
244 SparseMultivariatePolynomial::from_terms(
245 p.domain().clone(),
246 p.n_vars(),
247 p.terms_ref()
248 .iter()
249 .map(|(e, c)| (e.to_vec(), c.clone()))
250 .collect(),
251 )
252 })
253 .collect();
254 crate::groebner::f4::f4(&converted)
255 }
256}
257
258/// Convenience: compute a Gröbner basis and inter-reduce it.
259pub fn buchberger<D: Domain, O: MonomialOrder>(
260 ideal: &[SparseMultivariatePolynomial<D, O>],
261) -> GroebnerBasis<D, O> {
262 GroebnerBasis::buchberger(ideal).minimize().auto_reduce()
263}
264
265/// Algorithm selector for [`groebner_basis`].
266///
267/// `Auto` picks a backend based on the ideal's size and structure; the
268/// other variants force a specific algorithm. See [`groebner_basis`] for
269/// the unified entry point.
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
271pub enum Algorithm {
272 /// Automatically select the most suitable algorithm based on ideal
273 /// size and structure (heuristic, calibrated from benchmarks).
274 /// Currently routes to F4; the crossover to F5 will be tuned from
275 /// cyclic-n benchmarks once the F5 core is complete.
276 #[default]
277 Auto,
278 /// Force the F4 matrix algorithm (Faugère 1999).
279 F4,
280 /// Force the F5 signature-based algorithm (Faugère 2002).
281 F5,
282 /// Force Buchberger's classic S-polynomial iteration.
283 Buchberger,
284}
285
286/// Compute a Gröbner basis using the requested [`Algorithm`].
287///
288/// This is the unified entry point for Gröbner basis computation. Zero
289/// polynomials in `ideal` are filtered internally by each backend.
290///
291/// [`Algorithm::Auto`] currently routes to F4; the crossover to F5 will
292/// be calibrated from cyclic-n benchmarks once the F5 core is complete.
293///
294/// # Example
295///
296/// ```
297/// use ocas_domain::{RationalDomain, Rational};
298/// use ocas_poly::sparse::Lex;
299/// use ocas_poly::{Algorithm, groebner_basis, SparseMultivariatePolynomial};
300///
301/// let d = RationalDomain;
302/// // ideal: x + y, x - y
303/// let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
304/// (vec![1, 0], Rational::new(1, 1)),
305/// (vec![0, 1], Rational::new(1, 1)),
306/// ]);
307/// let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
308/// (vec![1, 0], Rational::new(1, 1)),
309/// (vec![0, 1], Rational::new(-1, 1)),
310/// ]);
311/// let gb = groebner_basis(&[f1, f2], Algorithm::Auto);
312/// assert!(gb.is_groebner_basis());
313/// ```
314pub fn groebner_basis<D: Domain + 'static, O: MonomialOrder>(
315 ideal: &[SparseMultivariatePolynomial<D, O>],
316 algo: Algorithm,
317) -> GroebnerBasis<D, O> {
318 match algo {
319 Algorithm::Auto | Algorithm::F4 => f4::f4(ideal),
320 Algorithm::F5 => f5::f5(ideal),
321 Algorithm::Buchberger => buchberger(ideal),
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::sparse::Lex;
329 use ocas_domain::{Rational, RationalDomain};
330
331 fn r(n: i64, d: i64) -> Rational {
332 Rational::new(n, d)
333 }
334
335 fn make_poly(
336 terms: Vec<(Vec<usize>, Rational)>,
337 ) -> SparseMultivariatePolynomial<RationalDomain, Lex> {
338 SparseMultivariatePolynomial::from_terms(RationalDomain, 2, terms)
339 }
340
341 #[test]
342 fn empty_ideal() {
343 let gb = buchberger::<RationalDomain, Lex>(&[]);
344 assert!(gb.basis.is_empty());
345 }
346
347 #[test]
348 fn single_polynomial() {
349 // f = x^2 - 1
350 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
351 RationalDomain,
352 1,
353 vec![(vec![2], r(1, 1)), (vec![0], r(-1, 1))],
354 );
355 let gb = buchberger(&[f]);
356 assert_eq!(gb.basis.len(), 1);
357 assert!(gb.is_groebner_basis());
358 }
359
360 #[test]
361 fn linear_system() {
362 // x + y = 0, x - y = 0 → basis = {x, y}
363 let f1 = make_poly(vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(1, 1))]);
364 let f2 = make_poly(vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(-1, 1))]);
365 let gb = buchberger(&[f1, f2]);
366 assert!(gb.is_groebner_basis());
367 // After auto-reduce, we expect {x, y} (monic leading terms)
368 assert!(gb.basis.len() >= 2);
369 }
370
371 #[test]
372 fn two_variable_ideal() {
373 // x^2 - y, x^3 - x (elimination ideal: y = x^2, x^3 = x → x ∈ {0, ±1})
374 let f1 = make_poly(vec![(vec![2, 0], r(1, 1)), (vec![0, 1], r(-1, 1))]);
375 let f2 = make_poly(vec![(vec![3, 0], r(1, 1)), (vec![1, 0], r(-1, 1))]);
376 let gb = buchberger(&[f1, f2]);
377 assert!(gb.is_groebner_basis());
378 assert!(!gb.basis.is_empty());
379 }
380}