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