Skip to main content

ocas_poly/gcd/
modular.rs

1//! Modular (Brown) GCD for dense univariate polynomials over ℤ.
2//!
3//! The naive pseudo-remainder GCD in [`crate::gcd`] explodes coefficients
4//! for degrees ≳ 16. Brown's algorithm instead computes monic GCDs modulo
5//! several primes, reconstructs an integer multiple of the true primitive
6//! GCD by CRT with symmetric representatives, and confirms it by exact
7//! trial division. Primes where the modular GCD has a larger degree than
8//! the true GCD ("unlucky" primes) are detected by degree comparison and
9//! discarded.
10
11use ocas_domain::number_theory::{crt::crt_many, primes_from, symmetric_mod};
12use ocas_domain::{Domain, EuclideanDomain, FiniteField, Integer, IntegerDomain};
13
14use crate::dense::DenseUnivariatePolynomial;
15use crate::factor::finite_field::FpPoly;
16
17/// Dense univariate polynomial over ℤ.
18pub type ZPoly = DenseUnivariatePolynomial<IntegerDomain>;
19
20/// Safety cap on the number of primes tried before falling back to the
21/// pseudo-remainder GCD. In practice CRT succeeds within a few dozen
22/// primes (coefficient bit-length divided by ~30).
23const MAX_PRIMES: usize = 10_000;
24
25/// Reduce a ℤ[x] polynomial modulo the prime of `field`.
26fn reduce_mod_field(p: &ZPoly, field: &FiniteField) -> FpPoly {
27    let coeffs = p
28        .coeffs()
29        .iter()
30        .map(|c| field.element(c.to_bigint()))
31        .collect();
32    FpPoly::from_coeffs(field.clone(), coeffs)
33}
34
35/// Exact quotient `dividend / divisor` in ℤ[x], or `None` when the division
36/// is not exact (some leading coefficient fails to divide, or a nonzero
37/// remainder survives).
38fn div_exact_z(dividend: &ZPoly, divisor: &ZPoly) -> Option<ZPoly> {
39    if divisor.is_zero() {
40        return None;
41    }
42    let dom = IntegerDomain;
43    if dividend.is_zero() {
44        return Some(ZPoly::new(dom));
45    }
46    let div_deg = divisor.degree()?;
47    let div_lc = divisor.leading_coeff()?.clone();
48    let mut remainder = dividend.clone();
49    let mut qcoeffs: Vec<Integer> = Vec::new();
50    while let Some(deg) = remainder.degree() {
51        if deg < div_deg {
52            break;
53        }
54        let lc = remainder.leading_coeff()?.clone();
55        // Exact divisibility check on the leading coefficient.
56        let q = dom.div(&lc, &div_lc)?;
57        let t = deg - div_deg;
58        if qcoeffs.len() <= t {
59            qcoeffs.resize(t + 1, Integer::from(0));
60        }
61        qcoeffs[t] = q.clone();
62        let mut sub_coeffs = vec![Integer::from(0); t];
63        sub_coeffs.extend(divisor.coeffs().iter().map(|c| &q * c));
64        remainder = remainder.sub(&ZPoly::from_coeffs(dom, sub_coeffs));
65    }
66    if remainder.is_zero() {
67        Some(ZPoly::from_coeffs(dom, qcoeffs))
68    } else {
69        None
70    }
71}
72
73/// CRT-reconstruct the coefficient vector of the scaled GCD from the
74/// per-prime images, using symmetric representatives.
75fn reconstruct(images: &[(Integer, FpPoly)], deg: usize) -> Option<ZPoly> {
76    let mut coeffs = Vec::with_capacity(deg + 1);
77    for i in 0..=deg {
78        let cs: Vec<(Integer, Integer)> = images
79            .iter()
80            .map(|(p, g)| {
81                let c = g
82                    .coeff(i)
83                    .map(|e| Integer::from(e.value().clone()))
84                    .unwrap_or_else(|| Integer::from(0));
85                (c, p.clone())
86            })
87            .collect();
88        let (r, m) = crt_many(&cs)?;
89        coeffs.push(symmetric_mod(&r, &m));
90    }
91    Some(ZPoly::from_coeffs(IntegerDomain, coeffs))
92}
93
94/// Compute the primitive GCD of `a` and `b` in ℤ[x] by the modular Brown
95/// algorithm: monic GCDs over `𝔽_p` are scaled by `γ = gcd(lc a, lc b)`,
96/// combined across primes with CRT, and confirmed by exact trial division.
97///
98/// The result is primitive (like [`DenseUnivariatePolynomial::gcd`]); the
99/// contents of the inputs are ignored. Falls back to the pseudo-remainder
100/// GCD only if an implausible number of primes was exhausted.
101///
102/// # Example
103///
104/// ```
105/// use ocas_domain::{IntegerDomain, Integer};
106/// use ocas_poly::DenseUnivariatePolynomial;
107/// use ocas_poly::gcd::modular::gcd_modular_z;
108///
109/// let d = IntegerDomain;
110/// let i = |v: i64| Integer::from(v);
111/// let a = DenseUnivariatePolynomial::from_coeffs(d, vec![i(-1), i(0), i(1)]);
112/// let b = DenseUnivariatePolynomial::from_coeffs(d, vec![i(1), i(2), i(1)]);
113/// let g = gcd_modular_z(&a, &b);
114/// assert_eq!(g.coeffs(), &[i(1), i(1)]); // x + 1
115/// ```
116pub fn gcd_modular_z(a: &ZPoly, b: &ZPoly) -> ZPoly {
117    let dom = IntegerDomain;
118    if a.is_zero() {
119        return b.primitive_part();
120    }
121    if b.is_zero() {
122        return a.primitive_part();
123    }
124    let ap = a.primitive_part();
125    let bp = b.primitive_part();
126    // γ is a multiple of the true GCD's leading coefficient; scaling the
127    // monic modular images by γ keeps the CRT targets integral.
128    let gamma = dom.gcd(
129        ap.leading_coeff().expect("nonzero polynomial"),
130        bp.leading_coeff().expect("nonzero polynomial"),
131    );
132
133    let mut best_deg: Option<usize> = None;
134    let mut images: Vec<(Integer, FpPoly)> = Vec::new();
135    let mut prime_iter = primes_from(&Integer::from(1_073_741_824)); // > 2^30
136    for _ in 0..MAX_PRIMES {
137        let p = prime_iter.next().expect("primes are inexhaustible");
138        if gamma.mod_floor(&p).is_zero() {
139            continue;
140        }
141        let field = FiniteField::new(p.to_bigint());
142        let fa = reduce_mod_field(&ap, &field);
143        let fb = reduce_mod_field(&bp, &field);
144        let g = fa.gcd(&fb);
145        let Some(deg) = g.degree() else {
146            continue; // one input vanished mod p: unlucky
147        };
148        // Normalize: monic, then scaled by γ.
149        let lc = g.leading_coeff().expect("nonzero gcd").clone();
150        let inv_lc = field.inv(&lc).expect("field element invertible");
151        let gamma_p = field.element(gamma.to_bigint());
152        let scale = field.mul(&inv_lc, &gamma_p);
153        let g_scaled = g.mul_scalar(&scale);
154
155        match best_deg {
156            None => {
157                best_deg = Some(deg);
158                images.push((p, g_scaled));
159            }
160            Some(bd) if deg < bd => {
161                // Earlier primes were unlucky; restart with the smaller GCD.
162                best_deg = Some(deg);
163                images.clear();
164                images.push((p, g_scaled));
165            }
166            Some(bd) if deg == bd => images.push((p, g_scaled)),
167            _ => continue, // unlucky prime: modular GCD degree too large
168        }
169        let deg = best_deg.expect("set above");
170        if deg == 0 {
171            // GCD of the primitive parts is a constant.
172            return ZPoly::from_coeffs(dom, vec![Integer::from(1)]);
173        }
174        // Trial reconstruction: accept only a common divisor of full degree.
175        if let Some(candidate) = reconstruct(&images, deg) {
176            let cand = candidate.primitive_part();
177            if cand.degree() == Some(deg)
178                && div_exact_z(&ap, &cand).is_some()
179                && div_exact_z(&bp, &cand).is_some()
180            {
181                return cand;
182            }
183        }
184    }
185    // Unreachable in practice; keeps the function total.
186    a.gcd(b)
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    fn i(v: i64) -> Integer {
194        Integer::from(v)
195    }
196
197    fn zpoly(coeffs: &[i64]) -> ZPoly {
198        ZPoly::from_coeffs(IntegerDomain, coeffs.iter().map(|&v| i(v)).collect())
199    }
200
201    /// Deterministic pseudo-random polynomial with `deg` coefficients in
202    /// `(-bound, bound)`.
203    fn rand_poly(deg: usize, bound: i64, seed: &mut u64) -> ZPoly {
204        let mut coeffs = Vec::with_capacity(deg + 1);
205        for _ in 0..=deg {
206            *seed = seed
207                .wrapping_mul(6364136223846793005)
208                .wrapping_add(1442695040888963407);
209            let v = ((*seed >> 33) as i64) % (2 * bound) - bound;
210            coeffs.push(i(v));
211        }
212        // Ensure exact degree.
213        if coeffs[deg].is_zero() {
214            coeffs[deg] = i(1);
215        }
216        ZPoly::from_coeffs(IntegerDomain, coeffs)
217    }
218
219    /// Normalize sign: make the leading coefficient positive.
220    fn monic_sign(p: &ZPoly) -> ZPoly {
221        if p.lcoeff().is_negative() {
222            p.neg()
223        } else {
224            p.clone()
225        }
226    }
227
228    #[test]
229    fn small_cases_match_prs() {
230        let a = zpoly(&[-1, 0, 1]);
231        let b = zpoly(&[1, 2, 1]);
232        let g = gcd_modular_z(&a, &b);
233        assert_eq!(g.coeffs(), &[i(1), i(1)]);
234
235        let g2 = gcd_modular_z(&zpoly(&[-1, 0, 1]), &zpoly(&[1, 1]));
236        assert_eq!(g2.coeffs(), &[i(1), i(1)]);
237
238        // Coprime.
239        let g3 = gcd_modular_z(&zpoly(&[1, 1]), &zpoly(&[2, 1]));
240        assert_eq!(g3.degree(), Some(0));
241
242        // Zero handling.
243        let g4 = gcd_modular_z(&a, &a.zero());
244        assert_eq!(g4.coeffs(), a.primitive_part().coeffs());
245
246        // Contents are ignored (primitive result).
247        let g5 = gcd_modular_z(&zpoly(&[2, 2]), &zpoly(&[4, 4]));
248        assert_eq!(g5.coeffs(), &[i(1), i(1)]);
249    }
250
251    #[test]
252    fn constructed_common_factor() {
253        // g = (3x + 5)(x² + 2) = 3x³ + 5x² + 6x + 10.
254        let g = zpoly(&[10, 6, 5, 3]);
255        let a = g.mul(&zpoly(&[-1, 2])); // (2x − 1)
256        let b = g.mul(&zpoly(&[7, 1])); // (x + 7)
257        let got = gcd_modular_z(&a, &b);
258        assert_eq!(got, monic_sign(&g).primitive_part());
259    }
260
261    #[test]
262    fn gcd_is_common_divisor_and_primitive() {
263        let mut seed = 42u64;
264        for _ in 0..20 {
265            let g = rand_poly(4, 10, &mut seed).primitive_part();
266            let a = g.mul(&rand_poly(3, 10, &mut seed));
267            let b = g.mul(&rand_poly(5, 10, &mut seed));
268            let got = gcd_modular_z(&a, &b);
269            assert!(div_exact_z(&a, &got).is_some(), "gcd must divide a");
270            assert!(div_exact_z(&b, &got).is_some(), "gcd must divide b");
271            assert!(got.content().is_one(), "gcd must be primitive");
272            assert!(div_exact_z(&got, &g).is_some(), "gcd must contain g");
273        }
274    }
275
276    #[test]
277    fn consistency_with_prs_on_small_polys() {
278        let mut seed = 7u64;
279        for _ in 0..30 {
280            let a = rand_poly(6, 8, &mut seed);
281            let b = rand_poly(5, 8, &mut seed);
282            let got = monic_sign(&gcd_modular_z(&a, &b));
283            let want = monic_sign(&a.gcd(&b));
284            assert_eq!(got, want, "a={a:?} b={b:?}");
285        }
286    }
287
288    #[test]
289    fn big_coefficients_no_explosion() {
290        // Degree 24 with ~50-digit coefficients: hopeless for naive PRS,
291        // routine for the modular path.
292        let mut seed = 99u64;
293        let mut big = rand_poly(12, 1_000_000, &mut seed);
294        // Square it twice to get large coefficients.
295        big = big.mul(&big);
296        let a = big.mul(&rand_poly(6, 100, &mut seed));
297        let b = big.mul(&rand_poly(8, 100, &mut seed));
298        let got = gcd_modular_z(&a, &b);
299        assert!(div_exact_z(&a, &got).is_some());
300        assert!(div_exact_z(&b, &got).is_some());
301        assert_eq!(got.degree(), big.primitive_part().degree());
302    }
303
304    /// 0.21.0 acceptance: degree-50 polynomials with ~100-digit integer
305    /// coefficients — the naive pseudo-remainder GCD explodes on these.
306    #[test]
307    #[ignore = "performance acceptance: run with --release --ignored"]
308    fn modular_gcd_degree_50_100_digit_coeffs() {
309        fn big_rand(digits: usize, seed: &mut u64) -> Integer {
310            let mut s = String::from("9");
311            for _ in 0..digits {
312                *seed = seed
313                    .wrapping_mul(6364136223846793005)
314                    .wrapping_add(1442695040888963407);
315                s.push((b'0' + ((*seed >> 33) % 10) as u8) as char);
316            }
317            Integer::from(s.parse::<num_bigint::BigInt>().unwrap())
318        }
319        let mut seed = 12345u64;
320        let mk = |deg: usize, seed: &mut u64| {
321            let coeffs: Vec<Integer> = (0..=deg).map(|_| big_rand(50, seed)).collect();
322            ZPoly::from_coeffs(IntegerDomain, coeffs)
323        };
324        let g = mk(25, &mut seed).primitive_part();
325        let r1 = mk(25, &mut seed);
326        let r2 = mk(25, &mut seed);
327        let a = g.mul(&r1);
328        let b = g.mul(&r2);
329        assert_eq!(a.degree(), Some(50));
330        let start = std::time::Instant::now();
331        let got = gcd_modular_z(&a, &b);
332        let elapsed = start.elapsed();
333        eprintln!("deg-50 / 100-digit modular gcd took {elapsed:?}");
334        assert!(div_exact_z(&a, &got).is_some(), "gcd must divide a");
335        assert!(div_exact_z(&b, &got).is_some(), "gcd must divide b");
336        assert_eq!(got.degree(), g.degree());
337        assert!(
338            elapsed.as_secs() < 120,
339            "modular gcd took {elapsed:?} (soft limit 120s)"
340        );
341    }
342}