Skip to main content

oxiz_math/polynomial/
gcd.rs

1//! Polynomial GCD Algorithms.
2//!
3//! This module implements efficient algorithms for computing the greatest
4//! common divisor (GCD) of multivariate polynomials.
5//!
6//! ## Algorithms
7//!
8//! 1. **Euclidean Algorithm**: Classic algorithm for univariate polynomials
9//! 2. **Subresultant PRS**: Polynomial remainder sequences avoiding coefficient growth
10//! 3. **Modular GCD**: Compute GCD mod p, then lift to full precision
11//! 4. **Sparse GCD**: Optimized for sparse polynomials
12//!
13//! ## Applications
14//!
15//! - Simplification of rational functions
16//! - Factorization preprocessing
17//! - Solving polynomial systems
18//! - Gröbner basis computation
19//!
20//! ## References
21//!
22//! - Knuth: "The Art of Computer Programming Vol. 2" (GCD algorithms)
23//! - Geddes et al.: "Algorithms for Computer Algebra" (1992)
24//! - Z3's `math/polynomial/polynomial_gcd.cpp`
25
26use super::{NULL_VAR, Polynomial, Var};
27#[allow(unused_imports)]
28use crate::prelude::*;
29use num_rational::BigRational;
30use num_traits::{One, Zero};
31
32/// Combine two "maximum variable" values (as returned by
33/// [`Polynomial::max_var`], where [`NULL_VAR`] means "no variables /
34/// constant") the way a remainder/division computation needs: the shared
35/// reduction variable is the higher-indexed *actual* variable appearing in
36/// either operand, and is `NULL_VAR` only when *both* operands are
37/// constant.
38///
39/// A naive `a.max(b)` is wrong here: `NULL_VAR == u32::MAX`, so it would
40/// always "win" over any real variable index, e.g. `combined_var(0,
41/// NULL_VAR)` would incorrectly report "no shared variable" even though one
42/// operand has variable 0. (`Polynomial::gcd_univariate`'s use of a plain
43/// `.max()` is safe only because *its* early-return for `NULL_VAR` --
44/// "either operand is a nonzero constant, so GCD is 1" -- happens to be
45/// correct regardless of which operand was constant; a remainder/quotient
46/// computation is not symmetric like that, so it needs this precise
47/// version.)
48fn combined_var(a: Var, b: Var) -> Var {
49    match (a, b) {
50        (NULL_VAR, NULL_VAR) => NULL_VAR,
51        (NULL_VAR, v) | (v, NULL_VAR) => v,
52        (va, vb) => va.max(vb),
53    }
54}
55
56/// Configuration for GCD computation.
57#[derive(Debug, Clone)]
58pub struct GcdConfig {
59    /// Use modular GCD algorithm.
60    pub use_modular: bool,
61    /// Use subresultant PRS.
62    pub use_subresultant: bool,
63    /// Modulus for modular algorithm.
64    pub modulus: u64,
65    /// Maximum degree to attempt.
66    pub max_degree: u32,
67}
68
69impl Default for GcdConfig {
70    fn default() -> Self {
71        Self {
72            use_modular: true,
73            use_subresultant: true,
74            modulus: 2147483647, // Large prime
75            max_degree: 1000,
76        }
77    }
78}
79
80/// Statistics for GCD computation.
81#[derive(Debug, Clone, Default)]
82pub struct GcdStats {
83    /// GCDs computed.
84    pub gcds_computed: u64,
85    /// Euclidean steps performed.
86    pub euclidean_steps: u64,
87    /// Modular reductions performed.
88    pub modular_reductions: u64,
89    /// Time (microseconds).
90    pub time_us: u64,
91}
92
93/// Polynomial GCD engine.
94pub struct PolynomialGcd {
95    config: GcdConfig,
96    stats: GcdStats,
97}
98
99impl PolynomialGcd {
100    /// Create new GCD engine.
101    pub fn new() -> Self {
102        Self::with_config(GcdConfig::default())
103    }
104
105    /// Create with configuration.
106    pub fn with_config(config: GcdConfig) -> Self {
107        Self {
108            config,
109            stats: GcdStats::default(),
110        }
111    }
112
113    /// Get statistics.
114    pub fn stats(&self) -> &GcdStats {
115        &self.stats
116    }
117
118    /// Compute GCD of two polynomials.
119    pub fn gcd(&mut self, a: &Polynomial, b: &Polynomial) -> Polynomial {
120        #[cfg(feature = "std")]
121        let start = std::time::Instant::now();
122
123        // Handle special cases
124        if a.is_zero() {
125            self.stats.gcds_computed += 1;
126            return b.clone();
127        }
128        if b.is_zero() {
129            self.stats.gcds_computed += 1;
130            return a.clone();
131        }
132
133        // Check degrees
134        if a.total_degree() > self.config.max_degree || b.total_degree() > self.config.max_degree {
135            // Too large - return trivial GCD
136            self.stats.gcds_computed += 1;
137            return Polynomial::constant(BigRational::one());
138        }
139
140        // Use Euclidean algorithm for simple cases
141        let result = if a.total_degree() < 10 && b.total_degree() < 10 {
142            self.euclidean_gcd(a, b)
143        } else if self.config.use_modular {
144            self.modular_gcd(a, b)
145        } else {
146            self.euclidean_gcd(a, b)
147        };
148
149        self.stats.gcds_computed += 1;
150        #[cfg(feature = "std")]
151        {
152            self.stats.time_us += start.elapsed().as_micros() as u64;
153        }
154
155        result
156    }
157
158    /// Euclidean algorithm for polynomial GCD.
159    ///
160    /// Based on repeated polynomial division until remainder is zero.
161    fn euclidean_gcd(&mut self, a: &Polynomial, b: &Polynomial) -> Polynomial {
162        let mut r0 = a.clone();
163        let mut r1 = b.clone();
164
165        // Bound iterations for safety. For genuinely univariate inputs each
166        // step strictly reduces the degree in the shared variable, so this
167        // bound is never the limiting factor there. For inputs that mix
168        // multiple distinct variables -- where `polynomial_remainder`'s
169        // "univariate in one designated variable" approach (see its docs)
170        // cannot always make progress -- the sequence can settle into a
171        // fixed 2-cycle instead of shrinking toward zero; this bound
172        // guarantees the loop still terminates rather than hanging.
173        let max_iters = a.total_degree() as usize + b.total_degree() as usize + 16;
174        let mut iters = 0;
175
176        while !r1.is_zero() && iters < max_iters {
177            iters += 1;
178            self.stats.euclidean_steps += 1;
179
180            // Compute r0 mod r1 (polynomial remainder)
181            let remainder = self.polynomial_remainder(&r0, &r1);
182
183            r0 = r1;
184            r1 = remainder;
185        }
186
187        // Normalize by making leading coefficient positive
188        self.normalize_polynomial(r0)
189    }
190
191    /// Modular GCD algorithm.
192    ///
193    /// 1. Compute GCD mod p for a prime p
194    /// 2. Lift to full precision
195    fn modular_gcd(&mut self, a: &Polynomial, b: &Polynomial) -> Polynomial {
196        self.stats.modular_reductions += 1;
197
198        // Simplified implementation - full version would:
199        // 1. Reduce polynomials modulo p
200        // 2. Compute GCD in Z_p[x]
201        // 3. Lift using Hensel lifting or Chinese remainder theorem
202
203        // For now, fall back to Euclidean
204        self.euclidean_gcd(a, b)
205    }
206
207    /// Compute polynomial remainder: a mod b (exact rational polynomial long
208    /// division, not the placeholder that used to always return zero).
209    ///
210    /// Division by the zero polynomial is mathematically undefined; rather
211    /// than panicking on a crafted degenerate `b` (see finding R3), this
212    /// treats it as "no reduction possible" and returns `a` unchanged.
213    ///
214    /// # Multivariate limitation
215    ///
216    /// This performs *univariate* long division in the highest-indexed
217    /// variable appearing in either operand, reusing
218    /// [`Polynomial::exact_div_rem_univariate`]. It is exact when `a` and
219    /// `b` are both univariate in that variable. For genuinely multivariate
220    /// polynomials (terms mixing two or more distinct variables), the
221    /// result is not guaranteed to be the true multivariate remainder --
222    /// see [`Polynomial::exact_div_rem_univariate`]'s docs for why. Callers
223    /// that need an exact n-variate remainder should use
224    /// [`super::gcd_multivariate`] / [`super::gcd_multivariate_advanced`]
225    /// instead.
226    fn polynomial_remainder(&self, a: &Polynomial, b: &Polynomial) -> Polynomial {
227        if b.is_zero() {
228            return a.clone();
229        }
230        if a.is_zero() {
231            return Polynomial::zero();
232        }
233
234        let var = combined_var(a.max_var(), b.max_var());
235        if var == NULL_VAR {
236            // Both operands are non-zero constants: over a field, a mod b
237            // is always exactly 0.
238            return Polynomial::zero();
239        }
240
241        a.exact_div_rem_univariate(b, var).1
242    }
243
244    /// Normalize polynomial (make leading coefficient = 1, i.e. monic).
245    fn normalize_polynomial(&self, poly: Polynomial) -> Polynomial {
246        if poly.is_zero() {
247            return poly;
248        }
249
250        let leading_coeff = poly.leading_coeff();
251        if leading_coeff.is_one() || leading_coeff.is_zero() {
252            return poly;
253        }
254
255        // Divide every coefficient by the leading coefficient (exact over
256        // BigRational) so the result is monic, the standard GCD
257        // normalization convention.
258        poly.scale(&leading_coeff.recip())
259    }
260
261    /// Compute GCD of multiple polynomials.
262    pub fn gcd_multiple(&mut self, polys: &[Polynomial]) -> Polynomial {
263        if polys.is_empty() {
264            return Polynomial::zero();
265        }
266
267        let mut result = polys[0].clone();
268
269        for poly in &polys[1..] {
270            result = self.gcd(&result, poly);
271
272            // Early termination if GCD becomes 1
273            if result.total_degree() == 0 && !result.is_zero() {
274                break;
275            }
276        }
277
278        result
279    }
280
281    /// Compute LCM (least common multiple) of two polynomials.
282    ///
283    /// LCM(a,b) = (a * b) / GCD(a,b), computed via exact polynomial
284    /// division (previously this omitted the division entirely and
285    /// returned the plain product `a * b`).
286    pub fn lcm(&mut self, a: &Polynomial, b: &Polynomial) -> Polynomial {
287        if a.is_zero() || b.is_zero() {
288            return Polynomial::zero();
289        }
290
291        let gcd = self.gcd(a, b);
292
293        if gcd.is_zero() {
294            return Polynomial::zero();
295        }
296
297        let product = a.mul(b);
298        let var = combined_var(product.max_var(), gcd.max_var());
299        if var == NULL_VAR {
300            // Both product and gcd are constants: LCM of nonzero constants
301            // is conventionally 1 (any nonzero constant divides any other).
302            return Polynomial::one();
303        }
304
305        let (quotient, _remainder) = product.exact_div_rem_univariate(&gcd, var);
306        quotient
307    }
308}
309
310impl Default for PolynomialGcd {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use num_bigint::BigInt;
320    use num_rational::BigRational;
321
322    #[test]
323    fn test_gcd_creation() {
324        let gcd_engine = PolynomialGcd::new();
325        assert_eq!(gcd_engine.stats().gcds_computed, 0);
326    }
327
328    #[test]
329    fn test_gcd_zero() {
330        let mut gcd_engine = PolynomialGcd::new();
331
332        let a = Polynomial::constant(BigRational::from(BigInt::from(42)));
333        let b = Polynomial::zero();
334
335        let result = gcd_engine.gcd(&a, &b);
336
337        // GCD of a polynomial with zero is the non-zero polynomial
338        assert!(!result.is_zero());
339        assert_eq!(gcd_engine.stats().gcds_computed, 1);
340    }
341
342    #[test]
343    fn test_gcd_constants() {
344        let mut gcd_engine = PolynomialGcd::new();
345
346        let a = Polynomial::constant(BigRational::from(BigInt::from(12)));
347        let b = Polynomial::constant(BigRational::from(BigInt::from(18)));
348
349        let result = gcd_engine.gcd(&a, &b);
350
351        assert!(!result.is_zero());
352    }
353
354    #[test]
355    fn test_gcd_config() {
356        let config = GcdConfig {
357            use_modular: false,
358            use_subresultant: false,
359            ..Default::default()
360        };
361
362        let gcd_engine = PolynomialGcd::with_config(config);
363        assert!(!gcd_engine.config.use_modular);
364    }
365
366    #[test]
367    fn test_gcd_multiple() {
368        let mut gcd_engine = PolynomialGcd::new();
369
370        let polys = vec![
371            Polynomial::constant(BigRational::from(BigInt::from(12))),
372            Polynomial::constant(BigRational::from(BigInt::from(18))),
373            Polynomial::constant(BigRational::from(BigInt::from(24))),
374        ];
375
376        let result = gcd_engine.gcd_multiple(&polys);
377
378        assert!(!result.is_zero());
379    }
380
381    #[test]
382    fn test_lcm() {
383        let mut gcd_engine = PolynomialGcd::new();
384
385        let a = Polynomial::constant(BigRational::from(BigInt::from(4)));
386        let b = Polynomial::constant(BigRational::from(BigInt::from(6)));
387
388        let result = gcd_engine.lcm(&a, &b);
389
390        assert!(!result.is_zero());
391    }
392
393    // -- Regression tests for MATH-2 (PolynomialGcd was a broken stub) --
394
395    /// Build the univariate polynomial `x^2` (variable 0).
396    fn poly_x_squared() -> Polynomial {
397        Polynomial::from_coeffs_int(&[(1, &[(0, 2)])])
398    }
399
400    /// Build the univariate polynomial `x + 1` (variable 0).
401    fn poly_x_plus_1() -> Polynomial {
402        Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[])])
403    }
404
405    /// Build the univariate polynomial `x - 1` (variable 0).
406    fn poly_x_minus_1() -> Polynomial {
407        Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (-1, &[])])
408    }
409
410    #[test]
411    fn test_polynomial_remainder_is_real_division_not_stub() {
412        // Regression test for MATH-2: `polynomial_remainder` used to
413        // unconditionally return `Polynomial::zero()` whenever
414        // `deg(a) >= deg(b)`. x^2 mod (x+1) is the constant 1 (long
415        // division: x^2 = (x-1)(x+1) + 1), never 0.
416        let gcd_engine = PolynomialGcd::new();
417        let a = poly_x_squared();
418        let b = poly_x_plus_1();
419
420        let remainder = gcd_engine.polynomial_remainder(&a, &b);
421
422        assert!(!remainder.is_zero(), "stub would wrongly return zero here");
423        assert!(remainder.is_constant());
424        assert_eq!(remainder.constant_value(), BigRational::one());
425    }
426
427    #[test]
428    fn test_gcd_coprime_polynomials_is_a_unit() {
429        // Regression test for MATH-2: gcd(x^2, x+1) must be a nonzero
430        // constant (they are coprime). The old stub's broken
431        // `polynomial_remainder` made `euclidean_gcd` terminate after one
432        // step and return `x+1` -- a non-unit -- as if it were the GCD.
433        let mut gcd_engine = PolynomialGcd::new();
434        let a = poly_x_squared();
435        let b = poly_x_plus_1();
436
437        let result = gcd_engine.gcd(&a, &b);
438
439        assert!(!result.is_zero());
440        assert_eq!(
441            result.total_degree(),
442            0,
443            "gcd of coprime polynomials must be a nonzero constant, got {result:?}"
444        );
445    }
446
447    #[test]
448    fn test_gcd_shared_linear_factor() {
449        // gcd((x-1)(x+1), (x-1)(x+2)) == (a unit multiple of) x-1.
450        let mut gcd_engine = PolynomialGcd::new();
451        let a = poly_x_minus_1().mul(&poly_x_plus_1()); // x^2 - 1
452        let b = poly_x_minus_1().mul(&Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (2, &[])])); // x^2 + x - 2
453
454        let result = gcd_engine.gcd(&a, &b);
455
456        assert_eq!(result.total_degree(), 1, "expected a linear common factor");
457        // The result must exactly divide both inputs (remainder zero).
458        assert!(gcd_engine.polynomial_remainder(&a, &result).is_zero());
459        assert!(gcd_engine.polynomial_remainder(&b, &result).is_zero());
460        // Normalization makes it monic: leading coefficient exactly 1.
461        assert_eq!(result.leading_coeff(), BigRational::one());
462    }
463
464    #[test]
465    fn test_normalize_polynomial_makes_monic() {
466        // Regression test for MATH-2: `normalize_polynomial`'s coefficient
467        // loop used to be an empty no-op, so `2x + 4` stayed `2x + 4`
468        // instead of becoming monic `x + 2`.
469        let gcd_engine = PolynomialGcd::new();
470        let poly = Polynomial::from_coeffs_int(&[(2, &[(0, 1)]), (4, &[])]); // 2x + 4
471
472        let normalized = gcd_engine.normalize_polynomial(poly);
473
474        assert_eq!(normalized.leading_coeff(), BigRational::one());
475        assert_eq!(
476            normalized,
477            Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (2, &[])]) // x + 2
478        );
479    }
480
481    #[test]
482    fn test_normalize_polynomial_zero_and_already_monic_unchanged() {
483        let gcd_engine = PolynomialGcd::new();
484
485        let zero = Polynomial::zero();
486        assert!(gcd_engine.normalize_polynomial(zero).is_zero());
487
488        let monic = poly_x_plus_1();
489        assert_eq!(gcd_engine.normalize_polynomial(monic.clone()), monic);
490    }
491
492    #[test]
493    fn test_lcm_of_coprime_polynomials_equals_product() {
494        // Regression test for MATH-2: `lcm` used to just return `a * b`
495        // unconditionally (never dividing by the GCD). For coprime a, b
496        // this happens to coincide with the correct answer, so exercise it
497        // to confirm the *new* division-based implementation still gets
498        // the right answer here too.
499        let mut gcd_engine = PolynomialGcd::new();
500        let a = poly_x_minus_1(); // x - 1
501        let b = poly_x_plus_1(); // x + 1
502
503        let result = gcd_engine.lcm(&a, &b);
504
505        assert_eq!(
506            result,
507            Polynomial::from_coeffs_int(&[(1, &[(0, 2)]), (-1, &[])])
508        ); // x^2 - 1
509    }
510
511    #[test]
512    fn test_lcm_divides_product_by_true_gcd() {
513        // lcm(a, b) * gcd(a, b) must equal a * b up to a unit factor; check
514        // this via exact divisibility instead of a hard-coded polynomial,
515        // to catch a regression to the old "just multiply" stub for an
516        // input with a nontrivial common factor.
517        let mut gcd_engine = PolynomialGcd::new();
518        let a = poly_x_minus_1().mul(&poly_x_plus_1()); // x^2 - 1
519        let b = poly_x_minus_1().mul(&Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (2, &[])])); // x^2 + x - 2
520
521        let lcm = gcd_engine.lcm(&a, &b);
522        let product = a.mul(&b);
523
524        // lcm must be strictly smaller-degree than the naive product
525        // whenever a, b share a nontrivial factor (product has degree 4,
526        // lcm should have degree 3: (x-1)(x+1)(x+2)).
527        assert_eq!(lcm.total_degree(), 3);
528        assert!(gcd_engine.polynomial_remainder(&product, &lcm).is_zero());
529    }
530
531    #[test]
532    fn test_polynomial_remainder_zero_divisor_no_panic() {
533        // Regression test for R3: division by the zero polynomial must not
534        // panic; `polynomial_remainder` treats it as "no reduction
535        // possible" and returns the dividend unchanged.
536        let gcd_engine = PolynomialGcd::new();
537        let a = poly_x_plus_1();
538        let zero = Polynomial::zero();
539
540        let remainder = gcd_engine.polynomial_remainder(&a, &zero);
541        assert_eq!(remainder, a);
542    }
543
544    #[test]
545    fn test_gcd_disjoint_variables_terminates() {
546        // Regression test: `euclidean_gcd` must terminate (not hang) even
547        // for polynomials in entirely different variables, where the
548        // univariate-in-one-variable remainder computation cannot always
549        // reduce the degree (see `polynomial_remainder`'s documented
550        // multivariate limitation). This is guarded by an iteration bound;
551        // this test simply confirms the call returns instead of looping
552        // forever.
553        let mut gcd_engine = PolynomialGcd::new();
554        let a = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[])]); // x0 + 1
555        let b = Polynomial::from_coeffs_int(&[(1, &[(1, 1)]), (1, &[])]); // x1 + 1
556
557        let _ = gcd_engine.gcd(&a, &b); // must return, not hang
558    }
559
560    #[test]
561    fn test_gcd_constant_dividend_nonconstant_divisor_remainder() {
562        // Regression test for the `combined_var` fix: when the *dividend*
563        // is a nonzero constant and the divisor is non-constant, the
564        // remainder must be the dividend itself (deg(dividend) <
565        // deg(divisor)), not incorrectly forced to zero by treating a
566        // one-sided constant as "both operands are constant".
567        let gcd_engine = PolynomialGcd::new();
568        let five = Polynomial::constant(BigRational::from(BigInt::from(5)));
569        let b = poly_x_plus_1();
570
571        let remainder = gcd_engine.polynomial_remainder(&five, &b);
572        assert_eq!(remainder, five);
573    }
574}