use ocas_domain::number_theory::{crt::crt_many, primes_from, symmetric_mod};
use ocas_domain::{Domain, EuclideanDomain, FiniteField, Integer, IntegerDomain};
use crate::dense::DenseUnivariatePolynomial;
use crate::factor::finite_field::FpPoly;
pub type ZPoly = DenseUnivariatePolynomial<IntegerDomain>;
const MAX_PRIMES: usize = 10_000;
fn reduce_mod_field(p: &ZPoly, field: &FiniteField) -> FpPoly {
let coeffs = p
.coeffs()
.iter()
.map(|c| field.element(c.to_bigint()))
.collect();
FpPoly::from_coeffs(field.clone(), coeffs)
}
fn div_exact_z(dividend: &ZPoly, divisor: &ZPoly) -> Option<ZPoly> {
if divisor.is_zero() {
return None;
}
let dom = IntegerDomain;
if dividend.is_zero() {
return Some(ZPoly::new(dom));
}
let div_deg = divisor.degree()?;
let div_lc = divisor.leading_coeff()?.clone();
let mut remainder = dividend.clone();
let mut qcoeffs: Vec<Integer> = Vec::new();
while let Some(deg) = remainder.degree() {
if deg < div_deg {
break;
}
let lc = remainder.leading_coeff()?.clone();
let q = dom.div(&lc, &div_lc)?;
let t = deg - div_deg;
if qcoeffs.len() <= t {
qcoeffs.resize(t + 1, Integer::from(0));
}
qcoeffs[t] = q.clone();
let mut sub_coeffs = vec![Integer::from(0); t];
sub_coeffs.extend(divisor.coeffs().iter().map(|c| &q * c));
remainder = remainder.sub(&ZPoly::from_coeffs(dom, sub_coeffs));
}
if remainder.is_zero() {
Some(ZPoly::from_coeffs(dom, qcoeffs))
} else {
None
}
}
fn reconstruct(images: &[(Integer, FpPoly)], deg: usize) -> Option<ZPoly> {
let mut coeffs = Vec::with_capacity(deg + 1);
for i in 0..=deg {
let cs: Vec<(Integer, Integer)> = images
.iter()
.map(|(p, g)| {
let c = g
.coeff(i)
.map(|e| Integer::from(e.value().clone()))
.unwrap_or_else(|| Integer::from(0));
(c, p.clone())
})
.collect();
let (r, m) = crt_many(&cs)?;
coeffs.push(symmetric_mod(&r, &m));
}
Some(ZPoly::from_coeffs(IntegerDomain, coeffs))
}
pub fn gcd_modular_z(a: &ZPoly, b: &ZPoly) -> ZPoly {
let dom = IntegerDomain;
if a.is_zero() {
return b.primitive_part();
}
if b.is_zero() {
return a.primitive_part();
}
let ap = a.primitive_part();
let bp = b.primitive_part();
let gamma = dom.gcd(
ap.leading_coeff().expect("nonzero polynomial"),
bp.leading_coeff().expect("nonzero polynomial"),
);
let mut best_deg: Option<usize> = None;
let mut images: Vec<(Integer, FpPoly)> = Vec::new();
let mut prime_iter = primes_from(&Integer::from(1_073_741_824)); for _ in 0..MAX_PRIMES {
let p = prime_iter.next().expect("primes are inexhaustible");
if gamma.mod_floor(&p).is_zero() {
continue;
}
let field = FiniteField::new(p.to_bigint());
let fa = reduce_mod_field(&ap, &field);
let fb = reduce_mod_field(&bp, &field);
let g = fa.gcd(&fb);
let Some(deg) = g.degree() else {
continue; };
let lc = g.leading_coeff().expect("nonzero gcd").clone();
let inv_lc = field.inv(&lc).expect("field element invertible");
let gamma_p = field.element(gamma.to_bigint());
let scale = field.mul(&inv_lc, &gamma_p);
let g_scaled = g.mul_scalar(&scale);
match best_deg {
None => {
best_deg = Some(deg);
images.push((p, g_scaled));
}
Some(bd) if deg < bd => {
best_deg = Some(deg);
images.clear();
images.push((p, g_scaled));
}
Some(bd) if deg == bd => images.push((p, g_scaled)),
_ => continue, }
let deg = best_deg.expect("set above");
if deg == 0 {
return ZPoly::from_coeffs(dom, vec![Integer::from(1)]);
}
if let Some(candidate) = reconstruct(&images, deg) {
let cand = candidate.primitive_part();
if cand.degree() == Some(deg)
&& div_exact_z(&ap, &cand).is_some()
&& div_exact_z(&bp, &cand).is_some()
{
return cand;
}
}
}
a.gcd(b)
}
#[cfg(test)]
mod tests {
use super::*;
fn i(v: i64) -> Integer {
Integer::from(v)
}
fn zpoly(coeffs: &[i64]) -> ZPoly {
ZPoly::from_coeffs(IntegerDomain, coeffs.iter().map(|&v| i(v)).collect())
}
fn rand_poly(deg: usize, bound: i64, seed: &mut u64) -> ZPoly {
let mut coeffs = Vec::with_capacity(deg + 1);
for _ in 0..=deg {
*seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let v = ((*seed >> 33) as i64) % (2 * bound) - bound;
coeffs.push(i(v));
}
if coeffs[deg].is_zero() {
coeffs[deg] = i(1);
}
ZPoly::from_coeffs(IntegerDomain, coeffs)
}
fn monic_sign(p: &ZPoly) -> ZPoly {
if p.lcoeff().is_negative() {
p.neg()
} else {
p.clone()
}
}
#[test]
fn small_cases_match_prs() {
let a = zpoly(&[-1, 0, 1]);
let b = zpoly(&[1, 2, 1]);
let g = gcd_modular_z(&a, &b);
assert_eq!(g.coeffs(), &[i(1), i(1)]);
let g2 = gcd_modular_z(&zpoly(&[-1, 0, 1]), &zpoly(&[1, 1]));
assert_eq!(g2.coeffs(), &[i(1), i(1)]);
let g3 = gcd_modular_z(&zpoly(&[1, 1]), &zpoly(&[2, 1]));
assert_eq!(g3.degree(), Some(0));
let g4 = gcd_modular_z(&a, &a.zero());
assert_eq!(g4.coeffs(), a.primitive_part().coeffs());
let g5 = gcd_modular_z(&zpoly(&[2, 2]), &zpoly(&[4, 4]));
assert_eq!(g5.coeffs(), &[i(1), i(1)]);
}
#[test]
fn constructed_common_factor() {
let g = zpoly(&[10, 6, 5, 3]);
let a = g.mul(&zpoly(&[-1, 2])); let b = g.mul(&zpoly(&[7, 1])); let got = gcd_modular_z(&a, &b);
assert_eq!(got, monic_sign(&g).primitive_part());
}
#[test]
fn gcd_is_common_divisor_and_primitive() {
let mut seed = 42u64;
for _ in 0..20 {
let g = rand_poly(4, 10, &mut seed).primitive_part();
let a = g.mul(&rand_poly(3, 10, &mut seed));
let b = g.mul(&rand_poly(5, 10, &mut seed));
let got = gcd_modular_z(&a, &b);
assert!(div_exact_z(&a, &got).is_some(), "gcd must divide a");
assert!(div_exact_z(&b, &got).is_some(), "gcd must divide b");
assert!(got.content().is_one(), "gcd must be primitive");
assert!(div_exact_z(&got, &g).is_some(), "gcd must contain g");
}
}
#[test]
fn consistency_with_prs_on_small_polys() {
let mut seed = 7u64;
for _ in 0..30 {
let a = rand_poly(6, 8, &mut seed);
let b = rand_poly(5, 8, &mut seed);
let got = monic_sign(&gcd_modular_z(&a, &b));
let want = monic_sign(&a.gcd(&b));
assert_eq!(got, want, "a={a:?} b={b:?}");
}
}
#[test]
fn big_coefficients_no_explosion() {
let mut seed = 99u64;
let mut big = rand_poly(12, 1_000_000, &mut seed);
big = big.mul(&big);
let a = big.mul(&rand_poly(6, 100, &mut seed));
let b = big.mul(&rand_poly(8, 100, &mut seed));
let got = gcd_modular_z(&a, &b);
assert!(div_exact_z(&a, &got).is_some());
assert!(div_exact_z(&b, &got).is_some());
assert_eq!(got.degree(), big.primitive_part().degree());
}
#[test]
#[ignore = "performance acceptance: run with --release --ignored"]
fn modular_gcd_degree_50_100_digit_coeffs() {
fn big_rand(digits: usize, seed: &mut u64) -> Integer {
let mut s = String::from("9");
for _ in 0..digits {
*seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
s.push((b'0' + ((*seed >> 33) % 10) as u8) as char);
}
Integer::from(s.parse::<num_bigint::BigInt>().unwrap())
}
let mut seed = 12345u64;
let mk = |deg: usize, seed: &mut u64| {
let coeffs: Vec<Integer> = (0..=deg).map(|_| big_rand(50, seed)).collect();
ZPoly::from_coeffs(IntegerDomain, coeffs)
};
let g = mk(25, &mut seed).primitive_part();
let r1 = mk(25, &mut seed);
let r2 = mk(25, &mut seed);
let a = g.mul(&r1);
let b = g.mul(&r2);
assert_eq!(a.degree(), Some(50));
let start = std::time::Instant::now();
let got = gcd_modular_z(&a, &b);
let elapsed = start.elapsed();
eprintln!("deg-50 / 100-digit modular gcd took {elapsed:?}");
assert!(div_exact_z(&a, &got).is_some(), "gcd must divide a");
assert!(div_exact_z(&b, &got).is_some(), "gcd must divide b");
assert_eq!(got.degree(), g.degree());
assert!(
elapsed.as_secs() < 120,
"modular gcd took {elapsed:?} (soft limit 120s)"
);
}
}