Skip to main content

oxiz_math/polynomial/
gcd_multivariate.rs

1//! Multivariate Polynomial GCD.
2//!
3//! Computes greatest common divisors of genuinely multivariate polynomials
4//! over the rationals using the classical *primitive polynomial remainder
5//! sequence* (primitive PRS).
6//!
7//! ## Algorithm
8//!
9//! For `f, g` in `Q[x_1, ..., x_n]`:
10//!
11//! 1. Choose a main variable `v` and view both operands as univariate in
12//!    `v` with coefficients in `Q[x_1, ..., x_n] \ {v}`.
13//! 2. Split each operand into its content (the GCD of those coefficients,
14//!    computed recursively over the *remaining* variables) and its
15//!    primitive part (`operand / content`, an exact division).
16//! 3. `gcd(f, g) = gcd(content(f), content(g)) * gcd(pp(f), pp(g))`, where
17//!    the primitive-part GCD is the last nonzero element of the pseudo
18//!    remainder sequence `a, b, prem(a, b, v), ...`, reduced to its
19//!    primitive part at every step to keep coefficients small.
20//!
21//! Step 2's recursion is what bounds the whole computation: coefficients
22//! with respect to `v` contain no occurrence of `v`, so every recursive
23//! call works over a strictly smaller variable set. The recursion depth is
24//! therefore at most the number of distinct variables of the inputs (see
25//! [`MultivariateGcdConfig::max_recursion_depth`]). Step 3's loop is bounded
26//! by `deg_v(b)`, which strictly decreases per pseudo-division.
27//!
28//! ## References
29//!
30//! - Knuth: "TAOCP Vol 2" Section 4.6.1 (primitive PRS)
31//! - von zur Gathen & Gerhard: "Modern Computer Algebra" Chapter 6
32//! - Reference: Z3's `polynomial.cpp`
33
34use super::{Monomial, Polynomial, Term, Var};
35#[allow(unused_imports)]
36use crate::prelude::*;
37use num_traits::Zero;
38
39/// Configuration for multivariate GCD.
40#[derive(Debug, Clone)]
41pub struct MultivariateGcdConfig {
42    /// Main variable selection strategy.
43    pub var_selection: VarSelectionStrategy,
44    /// Reduce every intermediate pseudo-remainder to its primitive part.
45    ///
46    /// This is the difference between a *primitive* PRS and a plain
47    /// (Euclidean) pseudo-remainder sequence: it costs one content GCD per
48    /// step but keeps the coefficients from growing exponentially. Turning
49    /// it off does **not** change the answer -- the content/primitive split
50    /// of the *inputs*, and of the final PRS element, is required for
51    /// correctness and is always performed.
52    pub use_primitive_part: bool,
53    /// Maximum recursion depth.
54    ///
55    /// Each level of recursion eliminates one variable (see the module
56    /// documentation), so a value of `n` supports inputs in up to `n`
57    /// distinct variables. Beyond that the engine gives up and returns the
58    /// trivial common divisor `1`, recording the fact in
59    /// [`MultivariateGcdStats::incomplete`].
60    pub max_recursion_depth: usize,
61}
62
63impl Default for MultivariateGcdConfig {
64    fn default() -> Self {
65        Self {
66            var_selection: VarSelectionStrategy::MaxDegree,
67            use_primitive_part: true,
68            max_recursion_depth: 100,
69        }
70    }
71}
72
73/// Strategy for selecting main variable.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum VarSelectionStrategy {
76    /// Choose variable with maximum total degree.
77    MaxDegree,
78    /// Choose variable with maximum degree in leading term.
79    MaxLeadingDegree,
80    /// Choose first variable in order.
81    FirstVariable,
82}
83
84/// Statistics for multivariate GCD.
85#[derive(Debug, Clone, Default)]
86pub struct MultivariateGcdStats {
87    /// Recursion depth reached.
88    pub max_depth: usize,
89    /// Primitive part decompositions.
90    pub primitive_decompositions: u64,
91    /// Content GCD computations.
92    pub content_gcds: u64,
93    /// Pseudo-divisions performed.
94    pub pseudo_divisions: u64,
95    /// Set when the engine could not compute the *greatest* common divisor
96    /// and fell back to the trivial common divisor `1`.
97    ///
98    /// This happens when the recursion-depth ceiling
99    /// ([`MultivariateGcdConfig::max_recursion_depth`]) is reached, i.e. the
100    /// inputs have more distinct variables than the budget allows.
101    ///
102    /// The returned polynomial is still a *sound* common divisor -- `1`
103    /// divides everything -- but it is not necessarily the *greatest* one,
104    /// so callers lose simplification rather than getting a wrong answer.
105    /// The return type of [`MultivariateGcdEngine::gcd`] is `Polynomial`,
106    /// which has no channel to say "I gave up"; this flag is that channel,
107    /// mirroring `IsolationStats::incomplete` elsewhere in this crate.
108    pub incomplete: bool,
109}
110
111/// Multivariate GCD engine.
112pub struct MultivariateGcdEngine {
113    /// Configuration.
114    config: MultivariateGcdConfig,
115    /// Statistics.
116    stats: MultivariateGcdStats,
117}
118
119impl MultivariateGcdEngine {
120    /// Create a new multivariate GCD engine.
121    pub fn new(config: MultivariateGcdConfig) -> Self {
122        Self {
123            config,
124            stats: MultivariateGcdStats::default(),
125        }
126    }
127
128    /// Create with default configuration.
129    pub fn default_config() -> Self {
130        Self::new(MultivariateGcdConfig::default())
131    }
132
133    /// Compute GCD of two multivariate polynomials.
134    ///
135    /// The result is normalized to be monic (leading coefficient `1` with
136    /// respect to the polynomial's monomial order); GCDs over a field are
137    /// only defined up to a unit, and this is the usual convention.
138    ///
139    /// If [`MultivariateGcdStats::incomplete`] is set afterwards, the result
140    /// is a sound but possibly non-greatest common divisor.
141    pub fn gcd(&mut self, p: &Polynomial, q: &Polynomial) -> Polynomial {
142        let g = self.gcd_recursive(p, q, 0);
143        self.normalize_gcd(&g)
144    }
145
146    /// Recursive GCD computation.
147    ///
148    /// `depth` counts eliminated variables, not stack frames of an unbounded
149    /// walk: every recursive call below operates on polynomials whose
150    /// variable set is strictly smaller than this call's, so the recursion
151    /// terminates after at most `|vars(p) union vars(q)|` levels. The
152    /// configured ceiling is the hard backstop for pathological variable
153    /// counts and is reported honestly through `stats.incomplete`.
154    fn gcd_recursive(&mut self, p: &Polynomial, q: &Polynomial, depth: usize) -> Polynomial {
155        if depth > self.stats.max_depth {
156            self.stats.max_depth = depth;
157        }
158
159        if depth >= self.config.max_recursion_depth {
160            self.stats.incomplete = true;
161            return Polynomial::one();
162        }
163
164        // Base cases.
165        if p.is_zero() {
166            return q.clone();
167        }
168        if q.is_zero() {
169            return p.clone();
170        }
171        // Over a field every nonzero constant is a unit, so a constant
172        // operand forces a unit GCD.
173        if p.is_constant() || q.is_constant() {
174            return Polynomial::one();
175        }
176
177        let mut vars = p.vars();
178        vars.extend(q.vars());
179        vars.sort_unstable();
180        vars.dedup();
181
182        match vars.len() {
183            // Unreachable in practice: a variable-free nonzero polynomial is
184            // constant and was handled above. Handled explicitly rather than
185            // falling through to a main-variable choice that would have no
186            // variable to choose.
187            0 => return Polynomial::one(),
188            // Both operands live in the same single variable.
189            1 => return p.gcd_univariate(q),
190            _ => {}
191        }
192
193        let main_var = self.select_main_variable(p, q);
194
195        let (p_content, p_primitive) = self.extract_content(p, main_var, depth);
196        let (q_content, q_primitive) = self.extract_content(q, main_var, depth);
197        self.stats.primitive_decompositions += 2;
198
199        // gcd(p, q) = gcd(content(p), content(q)) * gcd(pp(p), pp(q))
200        let content_gcd = self.gcd_recursive(&p_content, &q_content, depth + 1);
201        self.stats.content_gcds += 1;
202
203        let primitive_gcd = self.primitive_prs(&p_primitive, &q_primitive, main_var, depth);
204
205        content_gcd.mul(&primitive_gcd)
206    }
207
208    /// Primitive polynomial remainder sequence in `var`.
209    ///
210    /// Both operands must already be primitive with respect to `var`. The
211    /// last nonzero element of the sequence, made primitive, is
212    /// `gcd(f, g)` up to a unit.
213    ///
214    /// Termination: `deg_var(prem(a, b, var)) < deg_var(b)` by construction,
215    /// so the degree in `var` of the trailing element strictly decreases
216    /// every iteration.
217    fn primitive_prs(
218        &mut self,
219        f: &Polynomial,
220        g: &Polynomial,
221        var: Var,
222        depth: usize,
223    ) -> Polynomial {
224        let mut a = f.clone();
225        let mut b = g.clone();
226
227        if a.degree(var) < b.degree(var) {
228            core::mem::swap(&mut a, &mut b);
229        }
230
231        while !b.is_zero() {
232            if b.degree(var) == 0 {
233                // `b` is primitive with respect to `var` and free of `var`,
234                // hence a unit: the primitive parts are coprime.
235                return Polynomial::one();
236            }
237
238            let r = pseudo_remainder(&a, &b, var);
239            self.stats.pseudo_divisions += 1;
240
241            a = b;
242            b = if r.is_zero() || !self.config.use_primitive_part {
243                r
244            } else {
245                self.extract_content(&r, var, depth).1
246            };
247        }
248
249        if a.is_zero() {
250            return Polynomial::zero();
251        }
252
253        // The trailing element may carry a spurious content picked up from
254        // the pseudo-division multipliers; strip it so the result actually
255        // divides both inputs.
256        self.extract_content(&a, var, depth).1
257    }
258
259    /// Select main variable for recursion.
260    fn select_main_variable(&self, p: &Polynomial, q: &Polynomial) -> Var {
261        match self.config.var_selection {
262            VarSelectionStrategy::MaxDegree => self.select_by_max_degree(p, q),
263            VarSelectionStrategy::MaxLeadingDegree => self.select_by_leading_degree(p, q),
264            VarSelectionStrategy::FirstVariable => {
265                // Get first variable from either polynomial
266                p.vars()
267                    .first()
268                    .copied()
269                    .or_else(|| q.vars().first().copied())
270                    .unwrap_or(0)
271            }
272        }
273    }
274
275    /// Select variable with maximum total degree.
276    ///
277    /// Ties are broken deterministically in favour of the highest variable
278    /// index, matching the "largest variable is the main variable"
279    /// convention used elsewhere in this crate.
280    fn select_by_max_degree(&self, p: &Polynomial, q: &Polynomial) -> Var {
281        let mut vars = p.vars();
282        vars.extend(q.vars());
283        vars.sort_unstable();
284        vars.dedup();
285
286        vars.iter()
287            .max_by_key(|var| p.degree(**var).max(q.degree(**var)))
288            .copied()
289            .unwrap_or(0)
290    }
291
292    /// Select variable with maximum degree in leading term.
293    fn select_by_leading_degree(&self, p: &Polynomial, q: &Polynomial) -> Var {
294        // Get leading monomials
295        let p_lead = p.leading_monomial();
296        let q_lead = q.leading_monomial();
297
298        // Find variable with max degree in either leading monomial
299        let mut max_var = 0;
300        let mut max_degree = 0;
301
302        if let Some(p_mono) = p_lead {
303            for vp in p_mono.vars() {
304                if vp.power > max_degree {
305                    max_degree = vp.power;
306                    max_var = vp.var;
307                }
308            }
309        }
310
311        if let Some(q_mono) = q_lead {
312            for vp in q_mono.vars() {
313                if vp.power > max_degree {
314                    max_degree = vp.power;
315                    max_var = vp.var;
316                }
317            }
318        }
319
320        max_var
321    }
322
323    /// Extract content and primitive part with respect to `main_var`.
324    ///
325    /// Returns `(content, primitive)` with `p = content * primitive`, where
326    /// `content` is the GCD of the coefficients of `p` viewed as a
327    /// univariate polynomial in `main_var` (and hence free of `main_var`).
328    ///
329    /// A unit content is normalized to `1`, so the primitive part is only
330    /// determined up to a rational factor -- which is all a GCD needs.
331    fn extract_content(
332        &mut self,
333        p: &Polynomial,
334        main_var: Var,
335        depth: usize,
336    ) -> (Polynomial, Polynomial) {
337        if p.is_zero() {
338            return (Polynomial::one(), p.clone());
339        }
340
341        let coefficients = self.extract_coefficients(p, main_var);
342
343        // Content = GCD of the coefficients, computed over the strictly
344        // smaller variable set `vars(p) \ {main_var}`.
345        let mut content = Polynomial::zero();
346        for coeff in &coefficients {
347            content = self.gcd_recursive(&content, coeff, depth + 1);
348            if content.is_constant() {
349                break;
350            }
351        }
352
353        if content.is_zero() || content.is_constant() {
354            return (Polynomial::one(), p.clone());
355        }
356
357        match exact_division(p, &content) {
358            Some(primitive) => (content, primitive),
359            None => {
360                // The content divides `p` by construction, so this branch is
361                // mathematically unreachable. Report it as a give-up rather
362                // than silently returning a quotient that is not one.
363                self.stats.incomplete = true;
364                (Polynomial::one(), p.clone())
365            }
366        }
367    }
368
369    /// Extract the nonzero coefficients of `p` viewed as univariate in `var`.
370    ///
371    /// Each returned polynomial is free of `var`.
372    fn extract_coefficients(&self, p: &Polynomial, var: Var) -> Vec<Polynomial> {
373        let degree = p.degree(var);
374        let mut coeffs = Vec::with_capacity(degree as usize + 1);
375        for k in 0..=degree {
376            let c = p.coeff(var, k);
377            if !c.is_zero() {
378                coeffs.push(c);
379            }
380        }
381        coeffs
382    }
383
384    /// Normalize GCD result.
385    fn normalize_gcd(&self, p: &Polynomial) -> Polynomial {
386        if p.is_zero() {
387            return Polynomial::zero();
388        }
389
390        // Make monic (leading coefficient = 1)
391        let lead = p.leading_coeff();
392
393        if lead.is_zero() {
394            return p.clone();
395        }
396
397        p.scale(&lead.recip())
398    }
399
400    /// Get statistics.
401    pub fn stats(&self) -> &MultivariateGcdStats {
402        &self.stats
403    }
404
405    /// Reset statistics.
406    pub fn reset_stats(&mut self) {
407        self.stats = MultivariateGcdStats::default();
408    }
409}
410
411/// Multivariate pseudo-remainder of `a` by `b` with respect to `var`.
412///
413/// Treats both operands as univariate in `var` with coefficients in the
414/// remaining variables and returns `r` such that
415///
416/// ```text
417/// lc_var(b)^(deg_var(a) - deg_var(b) + 1) * a = quotient * b + r
418/// ```
419///
420/// with `deg_var(r) < deg_var(b)`. The leading-coefficient multiplier is
421/// what makes the division exact without introducing denominators, which is
422/// exactly why a *pseudo* remainder is used instead of a true one.
423///
424/// Degenerate divisors are handled without panicking: dividing by the zero
425/// polynomial has no defined reduction, so `a` is returned unchanged.
426fn pseudo_remainder(a: &Polynomial, b: &Polynomial, var: Var) -> Polynomial {
427    if b.is_zero() || a.is_zero() {
428        return a.clone();
429    }
430
431    let deg_b = b.degree(var);
432    let deg_a = a.degree(var);
433    if deg_a < deg_b {
434        return a.clone();
435    }
436
437    let lc_b = b.leading_coeff_wrt(var);
438    if lc_b.is_zero() {
439        return a.clone();
440    }
441
442    // Exactly `deg_a - deg_b + 1` reduction steps suffice: each step cancels
443    // the leading `var`-coefficient of `r` exactly, so `deg_var(r)` strictly
444    // decreases. Any steps not consumed are folded back in as the remaining
445    // `lc_b` power, which keeps the pseudo-division identity exact.
446    let steps = deg_a - deg_b + 1;
447    let mut used = 0u32;
448    let mut r = a.clone();
449
450    while used < steps && !r.is_zero() && r.degree(var) >= deg_b {
451        used += 1;
452        let deg_r = r.degree(var);
453        let lc_r = r.leading_coeff_wrt(var);
454        let shift = Monomial::from_var_power(var, deg_r - deg_b);
455
456        // r <- lc_b * r - lc_r * var^(deg_r - deg_b) * b
457        let scaled = lc_b.mul(&r);
458        let subtractor = lc_r.mul(b).mul_monomial(&shift);
459        r = scaled.sub(&subtractor);
460    }
461
462    let leftover = steps - used;
463    if leftover > 0 && !r.is_zero() {
464        r = r.mul(&lc_b.pow(leftover));
465    }
466
467    r
468}
469
470/// Exact multivariate division `p / q`.
471///
472/// Returns `Some(quotient)` when `q` divides `p` exactly and `None`
473/// otherwise -- never an approximate or truncated quotient. Callers that
474/// know divisibility holds (content / primitive-part splits) can treat
475/// `None` as an internal inconsistency; callers that do not can use it as a
476/// divisibility test.
477///
478/// Termination: each step removes the leading monomial of the running
479/// dividend and only introduces strictly smaller ones under the monomial
480/// order, which is a well-order on monomials.
481fn exact_division(p: &Polynomial, q: &Polynomial) -> Option<Polynomial> {
482    if q.is_zero() {
483        return None;
484    }
485    if p.is_zero() {
486        return Some(Polynomial::zero());
487    }
488    if q.is_one() {
489        return Some(p.clone());
490    }
491    if q.is_constant() {
492        // A nonzero constant divides everything over a field.
493        return Some(p.scale(&q.constant_value().recip()));
494    }
495
496    let order = p.order;
497    // Re-express the divisor under the dividend's monomial order: the
498    // leading-term cancellation argument below is only valid when both
499    // operands are ordered the same way.
500    let divisor = Polynomial::from_terms(q.terms().to_vec(), order);
501    let lead = divisor.leading_term()?;
502    let lead_coeff = lead.coeff.clone();
503    let lead_mono = lead.monomial.clone();
504
505    let mut quotient = Polynomial::from_terms(Vec::<Term>::new(), order);
506    let mut rest = Polynomial::from_terms(p.terms().to_vec(), order);
507
508    loop {
509        let Some(term) = rest.leading_term().cloned() else {
510            return Some(quotient);
511        };
512        // If the divisor's leading monomial does not divide the running
513        // dividend's, no exact quotient exists.
514        let mono = term.monomial.div(&lead_mono)?;
515        let coeff = &term.coeff / &lead_coeff;
516        let step = Polynomial::from_terms(vec![Term::new(coeff, mono)], order);
517        quotient = quotient.add(&step);
518        rest = rest.sub(&divisor.mul(&step));
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525    use num_bigint::BigInt;
526    use num_rational::BigRational;
527    use num_traits::One;
528
529    /// `x` is variable 0, `y` variable 1, `z` variable 2 throughout.
530    const X: Var = 0;
531    const Y: Var = 1;
532    const Z: Var = 2;
533
534    fn var(v: Var) -> Polynomial {
535        Polynomial::from_var(v)
536    }
537
538    fn int(n: i64) -> Polynomial {
539        Polynomial::constant(BigRational::from_integer(BigInt::from(n)))
540    }
541
542    /// Assert that `a` and `b` are associates: each divides the other.
543    fn assert_associate(a: &Polynomial, b: &Polynomial) {
544        assert!(
545            exact_division(a, b).is_some() && exact_division(b, a).is_some(),
546            "{a:?} and {b:?} are not associates"
547        );
548    }
549
550    #[test]
551    fn test_engine_creation() {
552        let engine = MultivariateGcdEngine::default_config();
553        assert_eq!(engine.stats().max_depth, 0);
554    }
555
556    #[test]
557    fn test_gcd_constants() {
558        let mut engine = MultivariateGcdEngine::default_config();
559
560        let gcd = engine.gcd(&int(6), &int(9));
561
562        // Over the rationals every nonzero constant is a unit, so the GCD of
563        // two constants is 1.
564        assert!(gcd.is_one());
565        assert!(!engine.stats().incomplete);
566    }
567
568    /// The depth ceiling must be *observable*: when the engine falls back
569    /// to the trivial divisor `1`, `stats().incomplete` says so. A `1`
570    /// returned as a genuine GCD and a `1` returned as a give-up were
571    /// previously indistinguishable.
572    #[test]
573    fn test_depth_ceiling_is_reported_in_stats() {
574        let config = MultivariateGcdConfig {
575            max_recursion_depth: 1,
576            ..MultivariateGcdConfig::default()
577        };
578        let mut engine = MultivariateGcdEngine::new(config);
579
580        // Two variables need one level of variable elimination, which the
581        // budget of 1 cannot pay for.
582        let p = var(X).mul(&var(Y));
583        let q = var(X).mul(&var(Y)).mul(&var(X));
584
585        let gcd = engine.gcd(&p, &q);
586        assert!(engine.stats().incomplete);
587        // The fallback is still a sound common divisor.
588        assert!(!gcd.is_zero());
589        assert!(engine.stats().max_depth <= 1);
590    }
591
592    #[test]
593    fn test_gcd_univariate() {
594        let mut engine = MultivariateGcdEngine::default_config();
595
596        // x^2 - 1
597        let p = Polynomial::from_coeffs_int(&[(1, &[(X, 2)]), (-1, &[])]);
598        // x - 1
599        let q = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (-1, &[])]);
600
601        let gcd = engine.gcd(&p, &q);
602
603        assert_associate(&gcd, &q);
604        assert!(!engine.stats().incomplete);
605    }
606
607    /// The multivariate engine must agree with the crate's univariate GCD on
608    /// univariate input.
609    #[test]
610    fn test_gcd_agrees_with_univariate_engine() {
611        let mut engine = MultivariateGcdEngine::default_config();
612
613        // (x - 1)(x + 1)(x + 2) and (x - 1)(x + 1)(x - 3)
614        let x_minus_1 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (-1, &[])]);
615        let x_plus_1 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (1, &[])]);
616        let x_plus_2 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (2, &[])]);
617        let x_minus_3 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (-3, &[])]);
618
619        let p = x_minus_1.mul(&x_plus_1).mul(&x_plus_2);
620        let q = x_minus_1.mul(&x_plus_1).mul(&x_minus_3);
621
622        let gcd = engine.gcd(&p, &q);
623        let expected = x_minus_1.mul(&x_plus_1);
624
625        assert_associate(&gcd, &expected);
626        assert_associate(&gcd, &p.gcd_univariate(&q));
627        assert!(!engine.stats().incomplete);
628    }
629
630    #[test]
631    fn test_gcd_monomials_shares_single_variable() {
632        let mut engine = MultivariateGcdEngine::default_config();
633
634        // gcd(x*y, x*z) = x
635        let p = var(X).mul(&var(Y));
636        let q = var(X).mul(&var(Z));
637
638        let gcd = engine.gcd(&p, &q);
639
640        assert_eq!(gcd, var(X));
641        assert!(!engine.stats().incomplete);
642    }
643
644    #[test]
645    fn test_gcd_shared_multivariate_factor() {
646        let mut engine = MultivariateGcdEngine::default_config();
647
648        // gcd((x + y)^2 * (x - z), (x + y) * x^2) = x + y
649        let x_plus_y = var(X).add(&var(Y));
650        let x_minus_z = var(X).sub(&var(Z));
651
652        let p = x_plus_y.pow(2).mul(&x_minus_z);
653        let q = x_plus_y.mul(&var(X).pow(2));
654
655        let gcd = engine.gcd(&p, &q);
656
657        assert_associate(&gcd, &x_plus_y);
658        // Monic normalization pins the exact representative.
659        assert_eq!(gcd, x_plus_y);
660        assert!(!engine.stats().incomplete);
661    }
662
663    #[test]
664    fn test_gcd_result_divides_both_inputs() {
665        let mut engine = MultivariateGcdEngine::default_config();
666
667        // p = (x*y + z) * (x + 2*y), q = (x*y + z) * (y - z)
668        let common = var(X).mul(&var(Y)).add(&var(Z));
669        let p = common.mul(&var(X).add(&var(Y).mul(&int(2))));
670        let q = common.mul(&var(Y).sub(&var(Z)));
671
672        let gcd = engine.gcd(&p, &q);
673
674        assert!(exact_division(&p, &gcd).is_some(), "gcd must divide p");
675        assert!(exact_division(&q, &gcd).is_some(), "gcd must divide q");
676        assert_associate(&gcd, &common);
677        assert!(!engine.stats().incomplete);
678    }
679
680    #[test]
681    fn test_gcd_coprime_multivariate_is_a_unit() {
682        let mut engine = MultivariateGcdEngine::default_config();
683
684        let p = var(X).add(&var(Y));
685        let q = var(X).sub(&var(Y)).add(&int(1));
686
687        let gcd = engine.gcd(&p, &q);
688
689        assert!(gcd.is_one(), "expected a unit GCD, got {gcd:?}");
690        assert!(!engine.stats().incomplete);
691    }
692
693    #[test]
694    fn test_gcd_with_zero_operand() {
695        let mut engine = MultivariateGcdEngine::default_config();
696
697        let p = var(X).mul(&var(Y));
698        let gcd = engine.gcd(&p, &Polynomial::zero());
699
700        assert_associate(&gcd, &p);
701        assert!(
702            engine
703                .gcd(&Polynomial::zero(), &Polynomial::zero())
704                .is_zero()
705        );
706    }
707
708    /// The primitive PRS must not depend on `use_primitive_part`: the flag
709    /// only controls intermediate coefficient growth, never the answer.
710    #[test]
711    fn test_gcd_same_without_intermediate_primitive_parts() {
712        let config = MultivariateGcdConfig {
713            use_primitive_part: false,
714            ..MultivariateGcdConfig::default()
715        };
716        let mut engine = MultivariateGcdEngine::new(config);
717
718        let x_plus_y = var(X).add(&var(Y));
719        let p = x_plus_y.pow(2).mul(&var(X).sub(&var(Z)));
720        let q = x_plus_y.mul(&var(X).pow(2));
721
722        let gcd = engine.gcd(&p, &q);
723
724        assert_eq!(gcd, x_plus_y);
725        assert!(!engine.stats().incomplete);
726    }
727
728    #[test]
729    fn test_pseudo_remainder_is_not_a_stub() {
730        // prem(x^2 + y, x + y, x): lc = 1, so the pseudo-remainder is the
731        // true remainder y^2 + y.
732        let p = var(X).pow(2).add(&var(Y));
733        let q = var(X).add(&var(Y));
734
735        let r = pseudo_remainder(&p, &q, X);
736
737        assert!(!r.is_zero(), "a stub would wrongly return zero here");
738        assert_eq!(r.degree(X), 0);
739        assert_eq!(r, var(Y).pow(2).add(&var(Y)));
740    }
741
742    #[test]
743    fn test_pseudo_remainder_identity_holds() {
744        // lc(b)^(deg a - deg b + 1) * a = q*b + r, so
745        // (lc^k * a - r) must be exactly divisible by b.
746        let a = var(X).pow(3).mul(&var(Y)).add(&var(Z));
747        let b = var(Y).mul(&var(X).pow(2)).add(&var(X)).add(&int(1));
748
749        let r = pseudo_remainder(&a, &b, X);
750        assert!(r.degree(X) < b.degree(X));
751
752        let lc_b = b.leading_coeff_wrt(X);
753        let k = a.degree(X) - b.degree(X) + 1;
754        let lhs = lc_b.pow(k).mul(&a).sub(&r);
755
756        assert!(
757            exact_division(&lhs, &b).is_some(),
758            "pseudo-division identity violated"
759        );
760    }
761
762    #[test]
763    fn test_pseudo_remainder_zero_divisor_no_panic() {
764        let a = var(X).add(&int(1));
765        assert_eq!(pseudo_remainder(&a, &Polynomial::zero(), X), a);
766    }
767
768    #[test]
769    fn test_exact_division_reports_non_divisibility() {
770        // (x + y) does not divide (x + z).
771        let p = var(X).add(&var(Z));
772        let q = var(X).add(&var(Y));
773
774        assert!(exact_division(&p, &q).is_none());
775        // ... and does not divide by zero either.
776        assert!(exact_division(&p, &Polynomial::zero()).is_none());
777    }
778
779    #[test]
780    fn test_exact_division_recovers_the_factor() {
781        let a = var(X).mul(&var(Y)).add(&var(Z)).add(&int(3));
782        let b = var(X).pow(2).sub(&var(Y).mul(&var(Z)));
783        let product = a.mul(&b);
784
785        assert_eq!(exact_division(&product, &b), Some(a.clone()));
786        assert_eq!(exact_division(&product, &a), Some(b));
787    }
788
789    #[test]
790    fn test_var_selection_max_degree() {
791        let engine = MultivariateGcdEngine::default_config();
792
793        // x^3 + y^2
794        let p = Polynomial::from_coeffs_int(&[(1, &[(X, 3)]), (1, &[(Y, 2)])]);
795        // x + y^3
796        let q = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (1, &[(Y, 3)])]);
797
798        let main_var = engine.select_by_max_degree(&p, &q);
799
800        // Both variables reach degree 3; ties go to the higher index.
801        assert_eq!(main_var, Y);
802    }
803
804    #[test]
805    fn test_extract_coefficients() {
806        let engine = MultivariateGcdEngine::default_config();
807
808        // 2*x^2*y + 3*x*y^2
809        let p = Polynomial::from_coeffs_int(&[(2, &[(X, 2), (Y, 1)]), (3, &[(X, 1), (Y, 2)])]);
810
811        let coeffs = engine.extract_coefficients(&p, X);
812
813        // Nonzero coefficients for x^1 and x^2, and none of them mention x.
814        assert_eq!(coeffs.len(), 2);
815        assert!(coeffs.iter().all(|c| c.degree(X) == 0));
816    }
817
818    #[test]
819    fn test_extract_content_splits_exactly() {
820        let mut engine = MultivariateGcdEngine::default_config();
821
822        // p = y*(x^2 + x) viewed in x: content y, primitive x^2 + x.
823        let p = var(Y).mul(&var(X).pow(2).add(&var(X)));
824
825        let (content, primitive) = engine.extract_content(&p, X, 0);
826
827        assert_eq!(content, var(Y));
828        assert_eq!(content.mul(&primitive), p);
829    }
830
831    #[test]
832    fn test_normalize_gcd() {
833        let engine = MultivariateGcdEngine::default_config();
834
835        // 6*x^2 + 3*x
836        let p = Polynomial::from_coeffs_int(&[(6, &[(X, 2)]), (3, &[(X, 1)])]);
837
838        let normalized = engine.normalize_gcd(&p);
839
840        // 6x^2 + 3x divided by its leading coefficient is x^2 + x/2.
841        let half = BigRational::new(BigInt::from(1), BigInt::from(2));
842        assert!(normalized.leading_coeff().is_one());
843        assert_eq!(normalized, var(X).pow(2).add(&var(X).scale(&half)));
844    }
845
846    #[cfg(feature = "std")]
847    /// Many-variable input on a small stack: the recursion is bounded by the
848    /// variable count, so a 90-variable GCD must *return* (that is the
849    /// proof) on a 1 MiB stack without approaching the depth ceiling.
850    #[test]
851    fn test_many_variables_within_ceiling_returns_on_small_stack() {
852        let handle = std::thread::Builder::new()
853            .stack_size(1 << 20)
854            .spawn(|| {
855                let n: Var = 90;
856                let mut p = Polynomial::one();
857                for v in 0..n {
858                    p = p.mul(&Polynomial::from_var(v));
859                }
860
861                let mut engine = MultivariateGcdEngine::default_config();
862                let gcd = engine.gcd(&p, &p);
863                (
864                    gcd == p,
865                    engine.stats().incomplete,
866                    engine.stats().max_depth,
867                )
868            })
869            .expect("failed to spawn worker thread");
870
871        let (matched, incomplete, max_depth) = handle.join().expect("worker thread panicked");
872        assert!(matched, "gcd(p, p) must be p");
873        assert!(!incomplete);
874        assert!(
875            max_depth <= 90,
876            "depth {max_depth} exceeds the variable count"
877        );
878    }
879
880    #[cfg(feature = "std")]
881    /// Beyond the configured variable budget the engine gives up honestly
882    /// instead of overflowing the stack or returning a wrong divisor.
883    #[test]
884    fn test_more_variables_than_budget_gives_up_honestly() {
885        let handle = std::thread::Builder::new()
886            .stack_size(1 << 20)
887            .spawn(|| {
888                let n: Var = 150;
889                let mut p = Polynomial::one();
890                for v in 0..n {
891                    p = p.mul(&Polynomial::from_var(v));
892                }
893
894                let mut engine = MultivariateGcdEngine::default_config();
895                let gcd = engine.gcd(&p, &p);
896                (
897                    gcd.is_zero(),
898                    engine.stats().incomplete,
899                    engine.stats().max_depth,
900                )
901            })
902            .expect("failed to spawn worker thread");
903
904        let (is_zero, incomplete, max_depth) = handle.join().expect("worker thread panicked");
905        assert!(!is_zero, "the fallback divisor must still be nonzero");
906        assert!(incomplete, "the give-up must be reported");
907        assert_eq!(max_depth, 100);
908    }
909}