mathhook_core/algebra/groebner/
efficient_buchberger.rs

1//! Efficient Buchberger Algorithm using SparsePolynomial
2//!
3//! This implementation uses the sparse polynomial representation for O(n²) arithmetic
4//! instead of the exponential growth from Expression AST.
5//!
6//! Performance target: Match SymPy (0.00s for circle-line system)
7
8use super::monomial_order::MonomialOrder;
9use crate::core::polynomial::sparse_polynomial::{
10    expression_to_sparse_polynomial, sparse_polynomial_to_expression, Monomial, SparsePolynomial,
11};
12use crate::core::{Expression, Symbol};
13use crate::error::{MathError, MathResult};
14use std::collections::VecDeque;
15
16/// Compute Gröbner basis using efficient sparse polynomial representation
17///
18/// This implementation uses SparsePolynomial for O(n) addition and O(n²) multiplication,
19/// achieving SymPy-level performance.
20///
21/// # Arguments
22///
23/// * `generators` - Initial polynomial generators as Expressions
24/// * `variables` - Variables in the polynomial ring
25/// * `order` - Monomial ordering
26///
27/// # Returns
28///
29/// Returns the Gröbner basis as Expressions
30///
31/// # Examples
32///
33/// ```rust,ignore
34/// use mathhook_core::{symbol, Expression};
35/// use mathhook_core::algebra::groebner::efficient_buchberger::efficient_buchberger_algorithm;
36/// use mathhook_core::algebra::groebner::MonomialOrder;
37///
38/// let x = symbol!(x);
39/// let y = symbol!(y);
40/// // Circle-line system: x² + y² = 1, x - y = 0
41/// let f1 = Expression::add(vec![
42///     Expression::pow(x.clone().into(), Expression::integer(2)),
43///     Expression::pow(y.clone().into(), Expression::integer(2)),
44///     Expression::integer(-1),
45/// ]);
46/// let f2 = Expression::add(vec![
47///     x.clone().into(),
48///     Expression::mul(vec![Expression::integer(-1), y.clone().into()]),
49/// ]);
50///
51/// let result = efficient_buchberger_algorithm(
52///     &[f1, f2],
53///     &[x, y],
54///     &MonomialOrder::Lex
55/// );
56/// assert!(result.is_ok());
57/// ```
58pub fn efficient_buchberger_algorithm(
59    generators: &[Expression],
60    variables: &[Symbol],
61    order: &MonomialOrder,
62) -> MathResult<Vec<Expression>> {
63    // Convert expressions to sparse polynomials
64    let mut basis: Vec<SparsePolynomial> = generators
65        .iter()
66        .filter_map(|expr| expression_to_sparse_polynomial(expr, variables))
67        .filter(|poly| !poly.is_zero())
68        .collect();
69
70    if basis.is_empty() {
71        return Ok(vec![Expression::integer(0)]);
72    }
73
74    // Initialize pairs queue
75    let mut pairs = VecDeque::new();
76    for i in 0..basis.len() {
77        for j in (i + 1)..basis.len() {
78            pairs.push_back((i, j));
79        }
80    }
81
82    // Iteration limit for safety
83    let max_iterations = 10000;
84    let mut iterations = 0;
85
86    while !pairs.is_empty() && iterations < max_iterations {
87        iterations += 1;
88
89        let (i, j) = pairs.pop_front().unwrap();
90
91        // Check Buchberger criterion
92        if can_skip_pair_sparse(i, j, &basis, order) {
93            continue;
94        }
95
96        // Compute S-polynomial
97        let s_poly = s_polynomial_sparse(&basis[i], &basis[j], order);
98
99        if s_poly.is_zero() {
100            continue;
101        }
102
103        // Reduce S-polynomial modulo current basis
104        let basis_refs: Vec<&SparsePolynomial> = basis.iter().collect();
105        let remainder = poly_reduce_completely_sparse(&s_poly, &basis_refs, order);
106
107        if !remainder.is_zero() {
108            let new_idx = basis.len();
109            basis.push(remainder);
110
111            // Add new pairs
112            for k in 0..new_idx {
113                pairs.push_back((k, new_idx));
114            }
115        }
116    }
117
118    if iterations >= max_iterations {
119        return Err(MathError::MaxIterationsReached { max_iterations });
120    }
121
122    // Remove zero polynomials
123    basis.retain(|p| !p.is_zero());
124
125    // Auto-reduce: each polynomial reduced by all others
126    let n = basis.len();
127    for i in 0..n {
128        let others: Vec<&SparsePolynomial> = basis
129            .iter()
130            .enumerate()
131            .filter(|(j, _)| *j != i)
132            .map(|(_, p)| p)
133            .collect();
134
135        if !others.is_empty() {
136            let reduced = poly_reduce_completely_sparse(&basis[i], &others, order);
137            basis[i] = reduced;
138        }
139    }
140
141    basis.retain(|p| !p.is_zero());
142
143    // Convert back to expressions
144    let result_exprs: Vec<Expression> = basis
145        .iter()
146        .map(|poly| sparse_polynomial_to_expression(poly, variables))
147        .collect();
148
149    Ok(result_exprs)
150}
151
152/// Compute S-polynomial of two sparse polynomials
153fn s_polynomial_sparse(
154    f: &SparsePolynomial,
155    g: &SparsePolynomial,
156    order: &MonomialOrder,
157) -> SparsePolynomial {
158    // Get leading monomials (unwrap is safe since polynomials are non-zero in basis)
159    let lt_f = f.leading_monomial(order).expect("f is non-zero");
160    let lt_g = g.leading_monomial(order).expect("g is non-zero");
161
162    let lcm_mono = lt_f.lcm(lt_g);
163
164    // S(f, g) = (lcm / LT(f)) * f - (lcm / LT(g)) * g
165    let f_factor = lcm_mono.divide(lt_f);
166    let g_factor = lcm_mono.divide(lt_g);
167
168    let scaled_f = f.mul_monomial(&f_factor);
169    let scaled_g = g.mul_monomial(&g_factor);
170
171    scaled_f.sub(&scaled_g)
172}
173
174/// Reduce polynomial modulo a set of polynomials
175fn poly_reduce_completely_sparse(
176    poly: &SparsePolynomial,
177    basis: &[&SparsePolynomial],
178    order: &MonomialOrder,
179) -> SparsePolynomial {
180    let mut remainder = poly.clone();
181
182    loop {
183        let mut reduced = false;
184
185        for divisor in basis {
186            if divisor.is_zero() {
187                continue;
188            }
189
190            let divisor_lt = divisor.leading_monomial(order);
191
192            // Try to reduce remainder by divisor
193            while !remainder.is_zero() {
194                let remainder_lt = remainder.leading_monomial(order);
195
196                if let (Some(r_lt), Some(d_lt)) = (remainder_lt, divisor_lt) {
197                    if let Some(quotient_mono) = r_lt.try_divide(d_lt) {
198                        // remainder -= (quotient_mono * divisor)
199                        let to_subtract = divisor.mul_monomial(&quotient_mono);
200                        remainder = remainder.sub(&to_subtract);
201                        reduced = true;
202                    } else {
203                        break;
204                    }
205                } else {
206                    break;
207                }
208            }
209        }
210
211        if !reduced {
212            break;
213        }
214    }
215
216    remainder
217}
218
219/// Check if pair can be skipped (Buchberger criterion)
220fn can_skip_pair_sparse(
221    i: usize,
222    j: usize,
223    basis: &[SparsePolynomial],
224    order: &MonomialOrder,
225) -> bool {
226    let lt_i = basis[i].leading_monomial(order);
227    let lt_j = basis[j].leading_monomial(order);
228
229    if let (Some(mono_i), Some(mono_j)) = (lt_i, lt_j) {
230        are_relatively_prime_sparse(mono_i, mono_j)
231    } else {
232        false
233    }
234}
235
236/// Check if two monomials are relatively prime
237fn are_relatively_prime_sparse(mono1: &Monomial, mono2: &Monomial) -> bool {
238    for (e1, e2) in mono1.exponents.iter().zip(mono2.exponents.iter()) {
239        if *e1 > 0 && *e2 > 0 {
240            return false;
241        }
242    }
243    true
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::symbol;
250
251    #[test]
252    fn test_efficient_buchberger_simple() {
253        let x = symbol!(x);
254        let y = symbol!(y);
255        let vars = vec![x.clone(), y.clone()];
256
257        let f1 = Expression::add(vec![
258            x.clone().into(),
259            Expression::Mul(Box::new(vec![
260                Expression::integer(-1),
261                Expression::symbol(y.clone()),
262            ])),
263        ]);
264        let f2 = Expression::add(vec![
265            Expression::pow(y.clone().into(), Expression::integer(2)),
266            Expression::integer(-1),
267        ]);
268
269        let gb = efficient_buchberger_algorithm(&[f1, f2], &vars, &MonomialOrder::Lex)
270            .expect("Should converge for simple system");
271
272        assert!(!gb.is_empty());
273        assert!(gb.len() >= 2);
274    }
275
276    #[test]
277    fn test_efficient_buchberger_circle_line() {
278        let x = symbol!(x);
279        let y = symbol!(y);
280        let vars = vec![x.clone(), y.clone()];
281
282        // Circle: x² + y² - 1 = 0
283        let f1 = Expression::add(vec![
284            Expression::pow(x.clone().into(), Expression::integer(2)),
285            Expression::pow(y.clone().into(), Expression::integer(2)),
286            Expression::integer(-1),
287        ]);
288
289        // Line: x - y = 0
290        let f2 = Expression::add(vec![
291            x.clone().into(),
292            Expression::Mul(Box::new(vec![
293                Expression::integer(-1),
294                Expression::symbol(y.clone()),
295            ])),
296        ]);
297
298        let start = std::time::Instant::now();
299        let gb = efficient_buchberger_algorithm(&[f1, f2], &vars, &MonomialOrder::Lex)
300            .expect("Should converge for circle-line system");
301        let elapsed = start.elapsed();
302
303        assert!(!gb.is_empty());
304        // Should complete in < 1 second (SymPy does it in 0.00s)
305        assert!(elapsed.as_secs() < 1, "Took {:?}, should be < 1s", elapsed);
306    }
307}