1use 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
23pub type ZMPoly = SparseMultivariatePolynomial<IntegerDomain, Lex>;
25
26pub 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 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 return Some(zmpoly_constant_in_y(a).gcd_univariate_x(&zmpoly_constant_in_y(b)));
54 }
55
56 let mut images: Vec<(Integer, DenseUnivariatePolynomial<IntegerDomain>)> = Vec::new();
58 let max_points = deg_y_gcd_bound + 2; 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 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 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 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 return Some(a.primitive_part());
108 }
109
110 let gcd_deg_y = deg_y_gcd_bound;
114 let result = interpolate_gcd(&images, gcd_deg_y, a.n_vars())?;
115
116 let result = result.primitive_part();
118 Some(result)
119}
120
121fn poly_degree_in(p: &ZMPoly, var: usize) -> usize {
123 p.terms_ref().keys().map(|e| e[var]).max().unwrap_or(0)
124}
125
126fn 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
152fn 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
170fn 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 let mut result = ZMPoly::new(domain, n_vars);
185 for i in 0..=gcd_deg_x {
186 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 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 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
216fn 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 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 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 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 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 for k in 0..basis.len().min(n) {
272 let new_num = &coeffs[k].0 * &denom + &yi * &basis[k] * &coeffs[k].1;
275 let new_den = &coeffs[k].1 * &denom;
276 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 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 return None;
299 }
300 if result.len() > max_deg + 1 {
301 break;
302 }
303 }
304 Some(result)
305}
306
307fn 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
319trait 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 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
338use crate::factor::multivariate::FpMPoly;
343
344pub 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
357pub 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
378pub 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 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 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
447fn fp_poly_degree_in(p: &FpMPoly, var: usize) -> usize {
449 p.terms_ref().keys().map(|e| e[var]).max().unwrap_or(0)
450}
451
452fn 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
476fn 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
491fn 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
508fn 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
546fn 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 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 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 let term = field.mul(&neg_xj, &basis[k]);
590 new_basis[k] = field.add(&new_basis[k], &term);
591 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
606pub 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 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 let prime = choose_prime(&a_prim, &b_prim)?;
634
635 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 let g_p = bivariate_gcd_fp(&a_p, &b_p)?;
642
643 let g_z = lift_from_fp(&g_p);
645
646 let g = g_z.mul_scalar(&content_gcd);
648 let g = g.primitive_part();
649
650 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
659fn choose_prime(a: &ZMPoly, b: &ZMPoly) -> Option<Integer> {
665 let candidates: Vec<i64> = vec![
667 4_294_967_291, 4_294_967_279,
669 4_294_967_231,
670 2_147_483_647, 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 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 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.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 let a = zmp2(&[(2, 0, 1), (0, 2, 1)]); let b = zmp2(&[(2, 0, 1), (0, 2, -1)]); let g = bivariate_gcd(&a, &b);
740 assert!(g.is_some(), "GCD should succeed");
741 let g = g.unwrap();
742 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 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 assert!(reconstruct_check(&a, &b, &g), "GCD degree inconsistent");
761 }
762
763 #[test]
764 fn content_and_primitive_part_bivariate() {
765 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 #[test]
778 fn reduce_mod_and_lift_roundtrip() {
779 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 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)); }
790
791 #[test]
792 fn gcd_modular_shared_linear_factor() {
793 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 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 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 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 assert!(reconstruct_check(&a, &b, &g), "GCD degree inconsistent");
830 }
831
832 proptest::proptest! {
835 #[test]
836 fn gcd_modular_consistency(
837 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 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 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 match (&g_mod, &g_heu) {
872 (Some(gm), Some(gh)) => {
873 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 }
885 }
886 }
887 }
888}