Skip to main content

ocas_poly/
multivariate_gcd.rs

1//! Multivariate polynomial GCD.
2//!
3//! Implements the heuristic/evaluation-interpolation approach: evaluate
4//! secondary variables at random points, compute the univariate GCD in the
5//! main variable, then recover the multivariate GCD by interpolation.
6//!
7//! For the bivariate case this reduces to: fix variable `y` at several points,
8//! compute univariate GCDs in `x`, and interpolate the coefficients (which are
9//! polynomials in `y`).
10//!
11//! References: Brown (1971); Geddes, Czapor, Labahn, *Algorithms for Computer
12//! Algebra*, §7.
13
14use num_bigint::BigInt;
15use num_traits::{One, Signed, Zero};
16use ocas_domain::{
17    Domain, EuclideanDomain, FiniteField, FiniteFieldElement, Integer, IntegerDomain,
18};
19
20use crate::dense::DenseUnivariatePolynomial;
21use crate::sparse::{Lex, SparseMultivariatePolynomial};
22
23/// Alias for sparse polynomials over the integers with lexicographic order.
24pub type ZMPoly = SparseMultivariatePolynomial<IntegerDomain, Lex>;
25
26/// Compute the GCD of two bivariate polynomials over ℤ[x, y] via evaluation
27/// and interpolation.
28///
29/// Treats `var0` as the main variable `x` and `var1` as `y`. Evaluates `y` at
30/// several integer points, computes univariate GCDs in `x`, and interpolates
31/// the GCD's coefficients (which are univariate in `y`).
32///
33/// Returns a primitive polynomial that divides both `a` and `b`, or `None` if
34/// the heuristic fails (e.g. unlucky evaluation points).
35pub fn bivariate_gcd(a: &ZMPoly, b: &ZMPoly) -> Option<ZMPoly> {
36    if a.is_zero() {
37        return Some(b.primitive_part());
38    }
39    if b.is_zero() {
40        return Some(a.primitive_part());
41    }
42    if a.n_vars() < 2 || b.n_vars() < 2 {
43        return None;
44    }
45
46    // Determine degree bounds in y for both polynomials and the GCD.
47    let deg_y_a = poly_degree_in(a, 1);
48    let deg_y_b = poly_degree_in(b, 1);
49    let deg_y_gcd_bound = deg_y_a.min(deg_y_b);
50
51    if deg_y_gcd_bound == 0 {
52        // GCD is purely in x — univariate GCD of the contents.
53        return Some(zmpoly_constant_in_y(a).gcd_univariate_x(&zmpoly_constant_in_y(b)));
54    }
55
56    // Collect evaluation points and compute univariate GCD images.
57    let mut images: Vec<(Integer, DenseUnivariatePolynomial<IntegerDomain>)> = Vec::new();
58    let max_points = deg_y_gcd_bound + 2; // a few extra for safety
59    let mut eval_point = Integer::from(2i64);
60
61    for _ in 0..max_points + 10 {
62        if images.len() >= max_points {
63            break;
64        }
65        // Skip points where either polynomial evaluates to zero.
66        let a_eval = eval_univariate_x(a, &eval_point);
67        let b_eval = eval_univariate_x(b, &eval_point);
68        if a_eval.is_zero() || b_eval.is_zero() {
69            eval_point = Integer::from(eval_point.to_bigint() + 1);
70            continue;
71        }
72        let g_eval = a_eval.gcd(&b_eval);
73        if g_eval.is_zero() {
74            eval_point = Integer::from(eval_point.to_bigint() + 1);
75            continue;
76        }
77        images.push((eval_point.clone(), g_eval));
78        eval_point = Integer::from(eval_point.to_bigint() + 1);
79    }
80
81    if images.is_empty() {
82        return None;
83    }
84
85    // Check that all images have the same degree in x (the GCD degree).
86    let gcd_deg_x = images[0].1.degree().unwrap_or(0);
87    if !images
88        .iter()
89        .all(|(_, g)| g.degree().unwrap_or(0) == gcd_deg_x)
90    {
91        // Unlucky evaluation points — some gave a GCD of higher degree.
92        // Filter to the minimum degree.
93        let min_deg = images
94            .iter()
95            .map(|(_, g)| g.degree().unwrap_or(0))
96            .min()
97            .unwrap_or(0);
98        images.retain(|(_, g)| g.degree().unwrap_or(0) == min_deg);
99        if images.is_empty() {
100            return None;
101        }
102    }
103
104    if images.len() < 2 {
105        // Only one image — can't interpolate. Return primitive part of one
106        // polynomial (fallback).
107        return Some(a.primitive_part());
108    }
109
110    // Interpolate each coefficient of the univariate GCD in x as a polynomial
111    // in y. For x^i, the i-th coefficient is interpolated from the evaluation
112    // points.
113    let gcd_deg_y = deg_y_gcd_bound;
114    let result = interpolate_gcd(&images, gcd_deg_y, a.n_vars())?;
115
116    // Normalize: make primitive and adjust sign.
117    let result = result.primitive_part();
118    Some(result)
119}
120
121/// Compute the degree of a multivariate polynomial in a given variable.
122fn poly_degree_in(p: &ZMPoly, var: usize) -> usize {
123    p.terms_ref().keys().map(|e| e[var]).max().unwrap_or(0)
124}
125
126/// Evaluate a bivariate ℤ[x,y] polynomial at y=value, yielding a univariate
127/// ℤ[x] polynomial.
128fn eval_univariate_x(p: &ZMPoly, value: &Integer) -> DenseUnivariatePolynomial<IntegerDomain> {
129    let domain = IntegerDomain;
130    let mut coeffs_map: std::collections::BTreeMap<usize, Integer> = Default::default();
131    for (exp, coeff) in p.terms_ref() {
132        let x_deg = exp[0];
133        let power = domain.pow(value, exp[1] as u64);
134        let new_coeff = domain.mul(coeff, &power);
135        let existing = coeffs_map
136            .get(&x_deg)
137            .cloned()
138            .unwrap_or_else(|| Integer::from(0));
139        coeffs_map.insert(
140            x_deg,
141            Integer::from(existing.to_bigint() + new_coeff.to_bigint()),
142        );
143    }
144    let max_deg = *coeffs_map.keys().max().unwrap_or(&0);
145    let mut coeffs = vec![Integer::from(0); max_deg + 1];
146    for (deg, c) in coeffs_map {
147        coeffs[deg] = c;
148    }
149    DenseUnivariatePolynomial::from_coeffs(domain, coeffs)
150}
151
152/// Extract the terms of `p` that have zero degree in y (i.e. purely in x),
153/// as a univariate polynomial. This is a fallback for degenerate GCD cases.
154fn zmpoly_constant_in_y(p: &ZMPoly) -> DenseUnivariatePolynomial<IntegerDomain> {
155    let domain = IntegerDomain;
156    let mut coeffs_map: std::collections::BTreeMap<usize, Integer> = Default::default();
157    for (exp, coeff) in p.terms_ref() {
158        if exp[1] == 0 {
159            coeffs_map.insert(exp[0], coeff.clone());
160        }
161    }
162    let max_deg = *coeffs_map.keys().max().unwrap_or(&0);
163    let mut coeffs = vec![Integer::from(0); max_deg + 1];
164    for (deg, c) in coeffs_map {
165        coeffs[deg] = c;
166    }
167    DenseUnivariatePolynomial::from_coeffs(domain, coeffs)
168}
169
170/// Given a set of `(y_value, g_x_image)` pairs, interpolate the bivariate GCD.
171///
172/// For each power of x, the coefficient is a polynomial in y interpolated
173/// from the corresponding coefficient of each univariate image.
174fn interpolate_gcd(
175    images: &[(Integer, DenseUnivariatePolynomial<IntegerDomain>)],
176    deg_y_bound: usize,
177    n_vars: usize,
178) -> Option<ZMPoly> {
179    let domain = IntegerDomain;
180    let gcd_deg_x = images[0].1.degree().unwrap_or(0);
181    let _n_points = images.len();
182
183    // For each power of x, collect (y_value, coeff_of_x^i) and interpolate.
184    let mut result = ZMPoly::new(domain, n_vars);
185    for i in 0..=gcd_deg_x {
186        // Collect data points for the i-th coefficient.
187        let data: Vec<(Integer, Integer)> = images
188            .iter()
189            .map(|(y_val, g)| {
190                let c = g.coeff(i).cloned().unwrap_or_else(|| Integer::from(0));
191                (y_val.clone(), c)
192            })
193            .collect();
194
195        // Interpolate via Lagrange interpolation to get the y-polynomial.
196        if let Some(y_poly) = lagrange_interpolate(&data, deg_y_bound) {
197            for (y_deg, y_coeff) in y_poly.iter().enumerate() {
198                if y_coeff.is_zero() {
199                    continue;
200                }
201                let mut exp = vec![0usize; n_vars];
202                exp[0] = i;
203                if n_vars > 1 {
204                    exp[1] = y_deg;
205                }
206                // Accumulate the term.
207                let existing = result.coeff(&exp);
208                let sum = domain.add(&existing, y_coeff);
209                result.set_term_external(exp, sum);
210            }
211        }
212    }
213    Some(result)
214}
215
216/// Lagrange interpolation: given points (x_0, y_0), ..., (x_n, y_n), find the
217/// polynomial p of degree ≤ n with p(x_i) = y_i.
218///
219/// Returns the coefficient vector [a_0, a_1, ..., a_n] (ascending powers) using
220/// exact arithmetic with numerator/denominator pairs over `BigInt`.
221fn lagrange_interpolate(points: &[(Integer, Integer)], max_deg: usize) -> Option<Vec<Integer>> {
222    let n = points.len();
223    if n == 0 {
224        return Some(Vec::new());
225    }
226    if n == 1 {
227        return Some(vec![points[0].1.clone()]);
228    }
229
230    // Represent coefficients as (numerator, denominator) fractions.
231    // Start with all-zero coefficients.
232    let mut coeffs: Vec<(BigInt, BigInt)> = vec![(BigInt::zero(), BigInt::one()); n];
233
234    for i in 0..n {
235        let xi = points[i].0.to_bigint();
236        let yi = points[i].1.to_bigint();
237
238        // denominator = product of (x_i - x_j) for j != i
239        let mut denom = BigInt::one();
240        for (j, (xj_val, _)) in points.iter().enumerate() {
241            if j == i {
242                continue;
243            }
244            let xj = xj_val.to_bigint();
245            denom *= xi.clone() - xj;
246        }
247        if denom.is_zero() {
248            return None;
249        }
250
251        // scale = y_i / denom — we'll multiply basis polynomials by yi and
252        // accumulate with common denominator `denom`.
253        // Build the Lagrange basis L_i(x) = product of (x - x_j) for j != i.
254        let mut basis: Vec<BigInt> = vec![BigInt::one()];
255        for (j, (xj_val, _)) in points.iter().enumerate() {
256            if j == i {
257                continue;
258            }
259            let neg_xj = -(xj_val.to_bigint());
260            // Multiply basis by (x - x_j):
261            let mut new_basis = vec![BigInt::zero(); basis.len() + 1];
262            for k in 0..basis.len() {
263                new_basis[k] += &neg_xj * &basis[k];
264                new_basis[k + 1] += &basis[k];
265            }
266            basis = new_basis;
267        }
268
269        // Scale basis by yi/denom and accumulate into coeffs.
270        // coeffs[k] += yi * basis[k] / denom
271        for k in 0..basis.len().min(n) {
272            // coeffs[k] += yi * basis[k] / denom
273            // = (coeffs_num[k] * denom + yi * basis[k] * coeffs_den[k]) / (coeffs_den[k] * denom)
274            let new_num = &coeffs[k].0 * &denom + &yi * &basis[k] * &coeffs[k].1;
275            let new_den = &coeffs[k].1 * &denom;
276            // Reduce by GCD to prevent blowup.
277            let g = bigint_gcd(&new_num, &new_den);
278            if !g.is_zero() && !g.is_one() {
279                coeffs[k] = (new_num / &g, new_den / &g);
280            } else {
281                coeffs[k] = (new_num, new_den);
282            }
283        }
284    }
285
286    // Convert to integers: all coefficients should be integers (denominator divides numerator).
287    let mut result = Vec::with_capacity(n.min(max_deg + 1));
288    for (num_, den) in &coeffs {
289        if den.is_zero() {
290            return None;
291        }
292        let q = num_ / den;
293        let r = num_ % den;
294        if r.is_zero() {
295            result.push(Integer::from(q));
296        } else {
297            // Non-integer coefficient — interpolation failed.
298            return None;
299        }
300        if result.len() > max_deg + 1 {
301            break;
302        }
303    }
304    Some(result)
305}
306
307/// Compute gcd(a, b) for two BigInt values via the Euclidean algorithm.
308fn bigint_gcd(a: &BigInt, b: &BigInt) -> BigInt {
309    let mut a = a.abs();
310    let mut b = b.abs();
311    while !b.is_zero() {
312        let r = &a % &b;
313        a = b;
314        b = r;
315    }
316    a
317}
318
319/// Trait extension for univariate polynomials used by the bivariate GCD.
320trait UnivariateGcdExt {
321    fn gcd_univariate_x(&self, other: &Self) -> ZMPoly;
322}
323
324impl UnivariateGcdExt for DenseUnivariatePolynomial<IntegerDomain> {
325    fn gcd_univariate_x(&self, other: &Self) -> ZMPoly {
326        let g = self.gcd(other);
327        // Wrap into a bivariate polynomial (y degree 0).
328        let mut result = ZMPoly::new(IntegerDomain, 2);
329        for (i, c) in g.coeffs().iter().enumerate() {
330            if !c.is_zero() {
331                result.set_term_external(vec![i, 0], c.clone());
332            }
333        }
334        result.primitive_part()
335    }
336}
337
338// =========================================================================
339// Modular multivariate GCD (§F1–F3)
340// =========================================================================
341
342use crate::factor::multivariate::FpMPoly;
343
344/// Reduce a ℤ polynomial mod a prime, yielding a ℤ_p polynomial.
345pub fn reduce_mod(p: &ZMPoly, prime: &BigInt) -> FpMPoly {
346    let field = FiniteField::new(prime.clone());
347    let mut result = FpMPoly::new(field.clone(), p.n_vars());
348    for (exp, coeff) in p.terms_ref() {
349        let c_fp = field.element(coeff.to_bigint());
350        if !c_fp.value().is_zero() {
351            result.set_term_external(exp.to_vec(), c_fp);
352        }
353    }
354    result
355}
356
357/// Lift a ℤ_p polynomial back to ℤ using symmetric representatives in
358/// `[-(p-1)/2, (p-1)/2]`.
359pub fn lift_from_fp(p: &FpMPoly) -> ZMPoly {
360    let field = p.domain().clone();
361    let prime = field.prime();
362    let half_p = prime / 2u32;
363    let mut result = ZMPoly::new(IntegerDomain, p.n_vars());
364    for (exp, coeff) in p.terms_ref() {
365        let v = coeff.value();
366        let lifted = if *v > half_p {
367            Integer::from(v - prime)
368        } else {
369            Integer::from(v.clone())
370        };
371        if !lifted.is_zero() {
372            result.set_term_external(exp.to_vec(), lifted);
373        }
374    }
375    result
376}
377
378/// Evaluation-interpolation bivariate GCD over ℤ_p.
379///
380/// Same algorithm as [`bivariate_gcd`] but operating in a finite field,
381/// avoiding coefficient growth.
382pub fn bivariate_gcd_fp(a: &FpMPoly, b: &FpMPoly) -> Option<FpMPoly> {
383    if a.is_zero() {
384        return Some(b.clone());
385    }
386    if b.is_zero() {
387        return Some(a.clone());
388    }
389    if a.n_vars() < 2 || b.n_vars() < 2 {
390        return None;
391    }
392
393    let field = a.domain().clone();
394    let deg_y_a = fp_poly_degree_in(a, 1);
395    let deg_y_b = fp_poly_degree_in(b, 1);
396    let deg_y_gcd_bound = deg_y_a.min(deg_y_b);
397
398    if deg_y_gcd_bound == 0 {
399        // GCD is purely in x — univariate GCD of the contents.
400        return Some(fp_univariate_gcd_x(a, b));
401    }
402
403    let mut images: Vec<(usize, DenseUnivariatePolynomial<FiniteField>)> = Vec::new();
404    let max_points = deg_y_gcd_bound + 2;
405    let mut eval_val = 1usize;
406
407    for _ in 0..max_points + 20 {
408        if images.len() >= max_points {
409            break;
410        }
411        let a_eval = fp_eval_univariate_x(a, eval_val);
412        let b_eval = fp_eval_univariate_x(b, eval_val);
413        if a_eval.is_zero() || b_eval.is_zero() {
414            eval_val += 1;
415            continue;
416        }
417        let g_eval = a_eval.gcd(&b_eval);
418        if g_eval.is_zero() {
419            eval_val += 1;
420            continue;
421        }
422        images.push((eval_val, g_eval));
423        eval_val += 1;
424    }
425
426    if images.is_empty() {
427        return None;
428    }
429
430    // Filter to consistent degree.
431    let min_deg = images
432        .iter()
433        .map(|(_, g)| g.degree().unwrap_or(0))
434        .min()
435        .unwrap_or(0);
436    images.retain(|(_, g)| g.degree().unwrap_or(0) == min_deg);
437    if images.is_empty() {
438        return None;
439    }
440    if images.len() < 2 {
441        return Some(a.clone());
442    }
443
444    fp_interpolate_gcd(&images, deg_y_gcd_bound, a.n_vars(), &field)
445}
446
447/// Degree of a multivariate polynomial in a given variable (FiniteField version).
448fn fp_poly_degree_in(p: &FpMPoly, var: usize) -> usize {
449    p.terms_ref().keys().map(|e| e[var]).max().unwrap_or(0)
450}
451
452/// Evaluate a bivariate ℤ_p[x,y] polynomial at y=value, yielding a univariate
453/// ℤ_p[x] polynomial.
454fn fp_eval_univariate_x(p: &FpMPoly, value: usize) -> DenseUnivariatePolynomial<FiniteField> {
455    let field = p.domain().clone();
456    let val_el = field.element(BigInt::from(value));
457    let mut coeffs_map: std::collections::BTreeMap<usize, FiniteFieldElement> = Default::default();
458    for (exp, coeff) in p.terms_ref() {
459        let x_deg = exp[0];
460        let power = field.pow(&val_el, exp[1] as u64);
461        let new_coeff = field.mul(coeff, &power);
462        let existing = coeffs_map
463            .get(&x_deg)
464            .cloned()
465            .unwrap_or_else(|| field.zero());
466        coeffs_map.insert(x_deg, field.add(&existing, &new_coeff));
467    }
468    let max_deg = *coeffs_map.keys().max().unwrap_or(&0);
469    let mut coeffs = vec![field.zero(); max_deg + 1];
470    for (deg, c) in coeffs_map {
471        coeffs[deg] = c;
472    }
473    DenseUnivariatePolynomial::from_coeffs(field, coeffs)
474}
475
476/// Univariate GCD of the y=0 content (both polynomials viewed as univariate in x).
477fn fp_univariate_gcd_x(a: &FpMPoly, b: &FpMPoly) -> FpMPoly {
478    let field = a.domain().clone();
479    let a_x = fp_extract_constant_in_y(a);
480    let b_x = fp_extract_constant_in_y(b);
481    let g = a_x.gcd(&b_x);
482    let mut result = FpMPoly::new(field, a.n_vars());
483    for (i, c) in g.coeffs().iter().enumerate() {
484        if !c.value().is_zero() {
485            result.set_term_external(vec![i, 0], c.clone());
486        }
487    }
488    result
489}
490
491/// Extract terms with y-degree 0 as a univariate polynomial.
492fn fp_extract_constant_in_y(p: &FpMPoly) -> DenseUnivariatePolynomial<FiniteField> {
493    let field = p.domain().clone();
494    let mut coeffs_map: std::collections::BTreeMap<usize, FiniteFieldElement> = Default::default();
495    for (exp, coeff) in p.terms_ref() {
496        if exp[1] == 0 {
497            coeffs_map.insert(exp[0], coeff.clone());
498        }
499    }
500    let max_deg = *coeffs_map.keys().max().unwrap_or(&0);
501    let mut coeffs = vec![field.zero(); max_deg + 1];
502    for (deg, c) in coeffs_map {
503        coeffs[deg] = c;
504    }
505    DenseUnivariatePolynomial::from_coeffs(field, coeffs)
506}
507
508/// Interpolate the bivariate GCD from univariate images in ℤ_p.
509fn fp_interpolate_gcd(
510    images: &[(usize, DenseUnivariatePolynomial<FiniteField>)],
511    deg_y_bound: usize,
512    n_vars: usize,
513    field: &FiniteField,
514) -> Option<FpMPoly> {
515    let gcd_deg_x = images[0].1.degree().unwrap_or(0);
516    let mut result = FpMPoly::new(field.clone(), n_vars);
517
518    for i in 0..=gcd_deg_x {
519        let data: Vec<(usize, FiniteFieldElement)> = images
520            .iter()
521            .map(|(y_val, g)| {
522                let c = g.coeff(i).cloned().unwrap_or_else(|| field.zero());
523                (*y_val, c)
524            })
525            .collect();
526
527        if let Some(y_poly) = fp_lagrange_interpolate(&data, deg_y_bound, field) {
528            for (y_deg, y_coeff) in y_poly.iter().enumerate() {
529                if y_coeff.value().is_zero() {
530                    continue;
531                }
532                let mut exp = vec![0usize; n_vars];
533                exp[0] = i;
534                if n_vars > 1 {
535                    exp[1] = y_deg;
536                }
537                let existing = result.coeff(&exp);
538                let sum = field.add(&existing, y_coeff);
539                result.set_term_external(exp, sum);
540            }
541        }
542    }
543    Some(result)
544}
545
546/// Lagrange interpolation in ℤ_p.
547fn fp_lagrange_interpolate(
548    points: &[(usize, FiniteFieldElement)],
549    _max_deg: usize,
550    field: &FiniteField,
551) -> Option<Vec<FiniteFieldElement>> {
552    let n = points.len();
553    if n == 0 {
554        return Some(Vec::new());
555    }
556    if n == 1 {
557        return Some(vec![points[0].1.clone()]);
558    }
559
560    let mut coeffs = vec![field.zero(); n];
561
562    for i in 0..n {
563        let xi = field.element(BigInt::from(points[i].0));
564        let yi = &points[i].1;
565
566        // denominator = product of (x_i - x_j) for j != i
567        let mut denom = field.one();
568        for (j, (xj_val, _)) in points.iter().enumerate() {
569            if j == i {
570                continue;
571            }
572            let xj = field.element(BigInt::from(*xj_val));
573            let diff = field.sub(&xi, &xj);
574            denom = field.mul(&denom, &diff);
575        }
576        let denom_inv = field.inv(&denom)?;
577        let scale = field.mul(yi, &denom_inv);
578
579        // Build Lagrange basis L_i(x) = product of (x - x_j) for j != i.
580        let mut basis: Vec<FiniteFieldElement> = vec![field.one()];
581        for (j, (xj_val, _)) in points.iter().enumerate() {
582            if j == i {
583                continue;
584            }
585            let neg_xj = field.neg(&field.element(BigInt::from(*xj_val)));
586            let mut new_basis = vec![field.zero(); basis.len() + 1];
587            for k in 0..basis.len() {
588                // new_basis[k] += neg_xj * basis[k]
589                let term = field.mul(&neg_xj, &basis[k]);
590                new_basis[k] = field.add(&new_basis[k], &term);
591                // new_basis[k+1] += basis[k]
592                new_basis[k + 1] = field.add(&new_basis[k + 1], &basis[k]);
593            }
594            basis = new_basis;
595        }
596
597        for k in 0..basis.len().min(n) {
598            let term = field.mul(&scale, &basis[k]);
599            coeffs[k] = field.add(&coeffs[k], &term);
600        }
601    }
602
603    Some(coeffs)
604}
605
606/// Modular bivariate GCD over ℤ.
607///
608/// Reduces the input polynomials mod a suitable prime, computes the GCD in ℤ_p
609/// via evaluation-interpolation, then lifts the result back to ℤ.
610///
611/// This avoids coefficient growth that plagues direct integer GCD computation.
612/// For robustness with large coefficients, multiple primes + CRT should be used;
613/// this implementation uses a single prime as a first step.
614pub fn gcd_modular(a: &ZMPoly, b: &ZMPoly) -> Option<ZMPoly> {
615    if a.is_zero() {
616        return Some(b.primitive_part());
617    }
618    if b.is_zero() {
619        return Some(a.primitive_part());
620    }
621    if a.n_vars() < 2 || b.n_vars() < 2 {
622        return None;
623    }
624
625    // Compute content GCD and work with primitive parts.
626    let content_a = a.content();
627    let content_b = b.content();
628    let content_gcd = IntegerDomain.gcd(&content_a, &content_b);
629    let a_prim = a.primitive_part();
630    let b_prim = b.primitive_part();
631
632    // Choose a suitable prime: must not divide any leading coefficient.
633    let prime = choose_prime(&a_prim, &b_prim)?;
634
635    // Reduce mod p.
636    let prime_bi = prime.to_bigint();
637    let a_p = reduce_mod(&a_prim, &prime_bi);
638    let b_p = reduce_mod(&b_prim, &prime_bi);
639
640    // Compute GCD in ℤ_p.
641    let g_p = bivariate_gcd_fp(&a_p, &b_p)?;
642
643    // Lift back to ℤ.
644    let g_z = lift_from_fp(&g_p);
645
646    // Multiply back by the content GCD and make primitive.
647    let g = g_z.mul_scalar(&content_gcd);
648    let g = g.primitive_part();
649
650    // Verify: g should divide both a and b (degree check).
651    let deg_g = g.total_degree().unwrap_or(0);
652    if deg_g > a.total_degree().unwrap_or(0) || deg_g > b.total_degree().unwrap_or(0) {
653        return None;
654    }
655
656    Some(g)
657}
658
659/// Choose a prime that does not divide any coefficient of `a` or `b`.
660///
661/// This is conservative: the modular GCD only requires the prime to not
662/// divide the leading coefficient, but checking all coefficients avoids
663/// accidental term loss during reduction.
664fn choose_prime(a: &ZMPoly, b: &ZMPoly) -> Option<Integer> {
665    // Start with a small prime and try candidates.
666    let candidates: Vec<i64> = vec![
667        4_294_967_291, // large 32-bit prime
668        4_294_967_279,
669        4_294_967_231,
670        2_147_483_647, // Mersenne prime
671        1_000_000_007,
672        998_244_353,
673        1_000_003,
674        999_983,
675    ];
676
677    for &p in &candidates {
678        let prime = Integer::from(p);
679        let prime_bi = prime.to_bigint();
680        // Check that no leading coefficient is divisible by p.
681        let ok_a = a.terms_ref().values().all(|c| {
682            let rem = c.to_bigint() % &prime_bi;
683            !rem.is_zero()
684        });
685        let ok_b = b.terms_ref().values().all(|c| {
686            let rem = c.to_bigint() % &prime_bi;
687            !rem.is_zero()
688        });
689        if ok_a && ok_b {
690            return Some(prime);
691        }
692    }
693    // Fallback: try to find any prime that works.
694    for p in [1_000_003i64, 999_983, 999_979, 999_961] {
695        let prime = Integer::from(p);
696        let prime_bi = prime.to_bigint();
697        let ok = a
698            .terms_ref()
699            .values()
700            .chain(b.terms_ref().values())
701            .all(|c| {
702                let rem = c.to_bigint() % &prime_bi;
703                !rem.is_zero()
704            });
705        if ok {
706            return Some(prime);
707        }
708    }
709    None
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715    use ocas_domain::Integer;
716
717    fn zmp2(terms: &[(usize, usize, i64)]) -> ZMPoly {
718        let domain = IntegerDomain;
719        let terms_vec: Vec<(Vec<usize>, Integer)> = terms
720            .iter()
721            .map(|(xd, yd, c)| (vec![*xd, *yd], Integer::from(*c)))
722            .collect();
723        ZMPoly::from_terms(domain, 2, terms_vec)
724    }
725
726    fn reconstruct_check(a: &ZMPoly, b: &ZMPoly, g: &ZMPoly) -> bool {
727        // g should divide both a and b (approximately: check that
728        // evaluating g's univariate images divides correctly).
729        // Simple check: g's total degree ≤ both a and b's.
730        g.total_degree().unwrap_or(0) <= a.total_degree().unwrap_or(0)
731            && g.total_degree().unwrap_or(0) <= b.total_degree().unwrap_or(0)
732    }
733
734    #[test]
735    fn gcd_coprime_bivariate() {
736        // gcd(x^2+y^2, x^2-y^2) = 1 (or x^2, but they're coprime over Z).
737        let a = zmp2(&[(2, 0, 1), (0, 2, 1)]); // x^2 + y^2
738        let b = zmp2(&[(2, 0, 1), (0, 2, -1)]); // x^2 - y^2
739        let g = bivariate_gcd(&a, &b);
740        assert!(g.is_some(), "GCD should succeed");
741        let g = g.unwrap();
742        // gcd should be 1 (constant).
743        assert!(
744            g.total_degree().unwrap_or(0) == 0 || g.n_terms() <= 1,
745            "coprime GCD should be constant, got {:?}",
746            g.total_degree()
747        );
748    }
749
750    #[test]
751    fn gcd_shared_linear_factor() {
752        // a = (x+y)(x+1) = x^2 + xy + x + y
753        // b = (x+y)(x+2) = x^2 + 2x + xy + 2y
754        let a = zmp2(&[(2, 0, 1), (1, 1, 1), (1, 0, 1), (0, 1, 1)]);
755        let b = zmp2(&[(2, 0, 1), (1, 1, 1), (1, 0, 2), (0, 1, 2)]);
756        let g = bivariate_gcd(&a, &b);
757        assert!(g.is_some());
758        let g = g.unwrap();
759        // GCD should be x+y (degree 1 in x, degree 1 in y, total degree 1 in x).
760        assert!(reconstruct_check(&a, &b, &g), "GCD degree inconsistent");
761    }
762
763    #[test]
764    fn content_and_primitive_part_bivariate() {
765        // 2x^2 + 4xy + 6y has content 2, primitive part x^2 + 2xy + 3y.
766        let p = zmp2(&[(2, 0, 2), (1, 1, 4), (0, 1, 6)]);
767        let content = p.content();
768        assert_eq!(content, Integer::from(2));
769        let pp = p.primitive_part();
770        assert_eq!(pp.coeff(&[2, 0]), Integer::from(1));
771        assert_eq!(pp.coeff(&[1, 1]), Integer::from(2));
772        assert_eq!(pp.coeff(&[0, 1]), Integer::from(3));
773    }
774
775    // --- Modular GCD tests (F1–F3) ---
776
777    #[test]
778    fn reduce_mod_and_lift_roundtrip() {
779        // p = 3x^2 + 5xy - 7y, mod 11
780        let p = zmp2(&[(2, 0, 3), (1, 1, 5), (0, 1, -7)]);
781        let prime = BigInt::from(11);
782        let p_fp = reduce_mod(&p, &prime);
783        let p_lifted = lift_from_fp(&p_fp);
784        // After reduce+lift, coefficients should match mod 11 (symmetric rep).
785        // 3 mod 11 = 3, 5 mod 11 = 5, -7 mod 11 = 4 (symmetric: 4).
786        assert_eq!(p_lifted.coeff(&[2, 0]), Integer::from(3));
787        assert_eq!(p_lifted.coeff(&[1, 1]), Integer::from(5));
788        assert_eq!(p_lifted.coeff(&[0, 1]), Integer::from(4)); // -7 mod 11 = 4
789    }
790
791    #[test]
792    fn gcd_modular_shared_linear_factor() {
793        // a = (x+y)(x+1) = x^2 + xy + x + y
794        // b = (x+y)(x+2) = x^2 + 2x + xy + 2y
795        let a = zmp2(&[(2, 0, 1), (1, 1, 1), (1, 0, 1), (0, 1, 1)]);
796        let b = zmp2(&[(2, 0, 1), (1, 1, 1), (1, 0, 2), (0, 1, 2)]);
797        let g = gcd_modular(&a, &b);
798        assert!(g.is_some(), "modular GCD should succeed");
799        let g = g.unwrap();
800        assert!(reconstruct_check(&a, &b, &g), "GCD degree inconsistent");
801    }
802
803    #[test]
804    fn gcd_modular_coprime() {
805        // gcd(x+y, x-y) = 1
806        let a = zmp2(&[(1, 0, 1), (0, 1, 1)]);
807        let b = zmp2(&[(1, 0, 1), (0, 1, -1)]);
808        let g = gcd_modular(&a, &b);
809        assert!(g.is_some(), "modular GCD should succeed for coprime");
810        let g = g.unwrap();
811        // Should be constant (degree 0).
812        assert!(
813            g.total_degree().unwrap_or(0) == 0 || g.n_terms() <= 1,
814            "coprime GCD should be constant, got degree {:?}",
815            g.total_degree()
816        );
817    }
818
819    #[test]
820    fn gcd_modular_shared_quadratic() {
821        // a = (x^2 + y)(x + 1) = x^3 + x^2 + xy + y
822        // b = (x^2 + y)(x + 2) = x^3 + 2x^2 + xy + 2y
823        let a = zmp2(&[(3, 0, 1), (2, 0, 1), (1, 1, 1), (0, 1, 1)]);
824        let b = zmp2(&[(3, 0, 1), (2, 0, 2), (1, 1, 1), (0, 1, 2)]);
825        let g = gcd_modular(&a, &b);
826        assert!(g.is_some(), "modular GCD should succeed");
827        let g = g.unwrap();
828        // GCD should be x^2 + y (degree 2).
829        assert!(reconstruct_check(&a, &b, &g), "GCD degree inconsistent");
830    }
831
832    // --- Property tests ---
833
834    proptest::proptest! {
835        #[test]
836        fn gcd_modular_consistency(
837            // Generate two bivariate polynomials with a shared linear factor (x + ay + b)
838            a_coeff in -5i64..5,
839            b_coeff in -5i64..5,
840            c1 in -3i64..3,
841            d1 in -3i64..3,
842            c2 in -3i64..3,
843            d2 in -3i64..3,
844        ) {
845            // shared factor: x + a_coeff*y + b_coeff
846            // p1 = c1*x + d1*y + (c1*b_coeff + d1*a_coeff*y_coeff... no, use direct construction
847            // Build: a = (x + ay + b)(c1*x + d1) = c1*x^2 + a*c1*xy + b*c1*x + d1*x + a*d1*y + b*d1
848            // Build: b = (x + ay + b)(c2*x + d2) = c2*x^2 + a*c2*xy + b*c2*x + d2*x + a*d2*y + b*d2
849            let a = zmp2(&[
850                (2, 0, c1),
851                (1, 1, a_coeff * c1),
852                (1, 0, b_coeff * c1 + d1),
853                (0, 1, a_coeff * d1),
854                (0, 0, b_coeff * d1),
855            ]);
856            let b = zmp2(&[
857                (2, 0, c2),
858                (1, 1, a_coeff * c2),
859                (1, 0, b_coeff * c2 + d2),
860                (0, 1, a_coeff * d2),
861                (0, 0, b_coeff * d2),
862            ]);
863
864            // Skip trivial cases where either polynomial is zero.
865            if a.is_zero() || b.is_zero() { return Ok(()); }
866
867            let g_mod = gcd_modular(&a, &b);
868            let g_heu = bivariate_gcd(&a, &b);
869
870            // Both should succeed or both should fail.
871            match (&g_mod, &g_heu) {
872                (Some(gm), Some(gh)) => {
873                    // Modular GCD degree should be ≤ heuristic GCD degree
874                    // (heuristic may return inflated results in edge cases).
875                    let deg_m = gm.total_degree().unwrap_or(0);
876                    let deg_h = gh.total_degree().unwrap_or(0);
877                    assert!(deg_m <= deg_h,
878                        "modular GCD degree {} > heuristic GCD degree {}", deg_m, deg_h);
879                }
880                (None, None) => {}
881                _ => {
882                    // One succeeded and the other didn't — acceptable for edge cases
883                    // where the modular approach needs different prime selection.
884                }
885            }
886        }
887    }
888}