1use 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
18pub type ZPoly = DenseUnivariatePolynomial<IntegerDomain>;
20
21const MAX_PRIMES: usize = 10_000;
25
26fn 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
36fn 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 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
74fn 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
95pub 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 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)); 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 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()?; 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 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, }
184 let deg = best_deg.expect("set above");
185 if deg == 0 {
186 return ZPoly::from_coeffs(dom, vec![Integer::from(1)]);
188 }
189 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 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 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 if coeffs[deg].is_zero() {
230 coeffs[deg] = i(1);
231 }
232 ZPoly::from_coeffs(IntegerDomain, coeffs)
233 }
234
235 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 let g3 = gcd_modular_z(&zpoly(&[1, 1]), &zpoly(&[2, 1]));
256 assert_eq!(g3.degree(), Some(0));
257
258 let g4 = gcd_modular_z(&a, &a.zero());
260 assert_eq!(g4.coeffs(), a.primitive_part().coeffs());
261
262 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 let g = zpoly(&[10, 6, 5, 3]);
271 let a = g.mul(&zpoly(&[-1, 2])); let b = g.mul(&zpoly(&[7, 1])); 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 let mut seed = 99u64;
309 let mut big = rand_poly(12, 1_000_000, &mut seed);
310 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 #[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}