1use super::{Monomial, Polynomial, Term, Var};
35#[allow(unused_imports)]
36use crate::prelude::*;
37use num_traits::Zero;
38
39#[derive(Debug, Clone)]
41pub struct MultivariateGcdConfig {
42 pub var_selection: VarSelectionStrategy,
44 pub use_primitive_part: bool,
53 pub max_recursion_depth: usize,
61}
62
63impl Default for MultivariateGcdConfig {
64 fn default() -> Self {
65 Self {
66 var_selection: VarSelectionStrategy::MaxDegree,
67 use_primitive_part: true,
68 max_recursion_depth: 100,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum VarSelectionStrategy {
76 MaxDegree,
78 MaxLeadingDegree,
80 FirstVariable,
82}
83
84#[derive(Debug, Clone, Default)]
86pub struct MultivariateGcdStats {
87 pub max_depth: usize,
89 pub primitive_decompositions: u64,
91 pub content_gcds: u64,
93 pub pseudo_divisions: u64,
95 pub incomplete: bool,
109}
110
111pub struct MultivariateGcdEngine {
113 config: MultivariateGcdConfig,
115 stats: MultivariateGcdStats,
117}
118
119impl MultivariateGcdEngine {
120 pub fn new(config: MultivariateGcdConfig) -> Self {
122 Self {
123 config,
124 stats: MultivariateGcdStats::default(),
125 }
126 }
127
128 pub fn default_config() -> Self {
130 Self::new(MultivariateGcdConfig::default())
131 }
132
133 pub fn gcd(&mut self, p: &Polynomial, q: &Polynomial) -> Polynomial {
142 let g = self.gcd_recursive(p, q, 0);
143 self.normalize_gcd(&g)
144 }
145
146 fn gcd_recursive(&mut self, p: &Polynomial, q: &Polynomial, depth: usize) -> Polynomial {
155 if depth > self.stats.max_depth {
156 self.stats.max_depth = depth;
157 }
158
159 if depth >= self.config.max_recursion_depth {
160 self.stats.incomplete = true;
161 return Polynomial::one();
162 }
163
164 if p.is_zero() {
166 return q.clone();
167 }
168 if q.is_zero() {
169 return p.clone();
170 }
171 if p.is_constant() || q.is_constant() {
174 return Polynomial::one();
175 }
176
177 let mut vars = p.vars();
178 vars.extend(q.vars());
179 vars.sort_unstable();
180 vars.dedup();
181
182 match vars.len() {
183 0 => return Polynomial::one(),
188 1 => return p.gcd_univariate(q),
190 _ => {}
191 }
192
193 let main_var = self.select_main_variable(p, q);
194
195 let (p_content, p_primitive) = self.extract_content(p, main_var, depth);
196 let (q_content, q_primitive) = self.extract_content(q, main_var, depth);
197 self.stats.primitive_decompositions += 2;
198
199 let content_gcd = self.gcd_recursive(&p_content, &q_content, depth + 1);
201 self.stats.content_gcds += 1;
202
203 let primitive_gcd = self.primitive_prs(&p_primitive, &q_primitive, main_var, depth);
204
205 content_gcd.mul(&primitive_gcd)
206 }
207
208 fn primitive_prs(
218 &mut self,
219 f: &Polynomial,
220 g: &Polynomial,
221 var: Var,
222 depth: usize,
223 ) -> Polynomial {
224 let mut a = f.clone();
225 let mut b = g.clone();
226
227 if a.degree(var) < b.degree(var) {
228 core::mem::swap(&mut a, &mut b);
229 }
230
231 while !b.is_zero() {
232 if b.degree(var) == 0 {
233 return Polynomial::one();
236 }
237
238 let r = pseudo_remainder(&a, &b, var);
239 self.stats.pseudo_divisions += 1;
240
241 a = b;
242 b = if r.is_zero() || !self.config.use_primitive_part {
243 r
244 } else {
245 self.extract_content(&r, var, depth).1
246 };
247 }
248
249 if a.is_zero() {
250 return Polynomial::zero();
251 }
252
253 self.extract_content(&a, var, depth).1
257 }
258
259 fn select_main_variable(&self, p: &Polynomial, q: &Polynomial) -> Var {
261 match self.config.var_selection {
262 VarSelectionStrategy::MaxDegree => self.select_by_max_degree(p, q),
263 VarSelectionStrategy::MaxLeadingDegree => self.select_by_leading_degree(p, q),
264 VarSelectionStrategy::FirstVariable => {
265 p.vars()
267 .first()
268 .copied()
269 .or_else(|| q.vars().first().copied())
270 .unwrap_or(0)
271 }
272 }
273 }
274
275 fn select_by_max_degree(&self, p: &Polynomial, q: &Polynomial) -> Var {
281 let mut vars = p.vars();
282 vars.extend(q.vars());
283 vars.sort_unstable();
284 vars.dedup();
285
286 vars.iter()
287 .max_by_key(|var| p.degree(**var).max(q.degree(**var)))
288 .copied()
289 .unwrap_or(0)
290 }
291
292 fn select_by_leading_degree(&self, p: &Polynomial, q: &Polynomial) -> Var {
294 let p_lead = p.leading_monomial();
296 let q_lead = q.leading_monomial();
297
298 let mut max_var = 0;
300 let mut max_degree = 0;
301
302 if let Some(p_mono) = p_lead {
303 for vp in p_mono.vars() {
304 if vp.power > max_degree {
305 max_degree = vp.power;
306 max_var = vp.var;
307 }
308 }
309 }
310
311 if let Some(q_mono) = q_lead {
312 for vp in q_mono.vars() {
313 if vp.power > max_degree {
314 max_degree = vp.power;
315 max_var = vp.var;
316 }
317 }
318 }
319
320 max_var
321 }
322
323 fn extract_content(
332 &mut self,
333 p: &Polynomial,
334 main_var: Var,
335 depth: usize,
336 ) -> (Polynomial, Polynomial) {
337 if p.is_zero() {
338 return (Polynomial::one(), p.clone());
339 }
340
341 let coefficients = self.extract_coefficients(p, main_var);
342
343 let mut content = Polynomial::zero();
346 for coeff in &coefficients {
347 content = self.gcd_recursive(&content, coeff, depth + 1);
348 if content.is_constant() {
349 break;
350 }
351 }
352
353 if content.is_zero() || content.is_constant() {
354 return (Polynomial::one(), p.clone());
355 }
356
357 match exact_division(p, &content) {
358 Some(primitive) => (content, primitive),
359 None => {
360 self.stats.incomplete = true;
364 (Polynomial::one(), p.clone())
365 }
366 }
367 }
368
369 fn extract_coefficients(&self, p: &Polynomial, var: Var) -> Vec<Polynomial> {
373 let degree = p.degree(var);
374 let mut coeffs = Vec::with_capacity(degree as usize + 1);
375 for k in 0..=degree {
376 let c = p.coeff(var, k);
377 if !c.is_zero() {
378 coeffs.push(c);
379 }
380 }
381 coeffs
382 }
383
384 fn normalize_gcd(&self, p: &Polynomial) -> Polynomial {
386 if p.is_zero() {
387 return Polynomial::zero();
388 }
389
390 let lead = p.leading_coeff();
392
393 if lead.is_zero() {
394 return p.clone();
395 }
396
397 p.scale(&lead.recip())
398 }
399
400 pub fn stats(&self) -> &MultivariateGcdStats {
402 &self.stats
403 }
404
405 pub fn reset_stats(&mut self) {
407 self.stats = MultivariateGcdStats::default();
408 }
409}
410
411fn pseudo_remainder(a: &Polynomial, b: &Polynomial, var: Var) -> Polynomial {
427 if b.is_zero() || a.is_zero() {
428 return a.clone();
429 }
430
431 let deg_b = b.degree(var);
432 let deg_a = a.degree(var);
433 if deg_a < deg_b {
434 return a.clone();
435 }
436
437 let lc_b = b.leading_coeff_wrt(var);
438 if lc_b.is_zero() {
439 return a.clone();
440 }
441
442 let steps = deg_a - deg_b + 1;
447 let mut used = 0u32;
448 let mut r = a.clone();
449
450 while used < steps && !r.is_zero() && r.degree(var) >= deg_b {
451 used += 1;
452 let deg_r = r.degree(var);
453 let lc_r = r.leading_coeff_wrt(var);
454 let shift = Monomial::from_var_power(var, deg_r - deg_b);
455
456 let scaled = lc_b.mul(&r);
458 let subtractor = lc_r.mul(b).mul_monomial(&shift);
459 r = scaled.sub(&subtractor);
460 }
461
462 let leftover = steps - used;
463 if leftover > 0 && !r.is_zero() {
464 r = r.mul(&lc_b.pow(leftover));
465 }
466
467 r
468}
469
470fn exact_division(p: &Polynomial, q: &Polynomial) -> Option<Polynomial> {
482 if q.is_zero() {
483 return None;
484 }
485 if p.is_zero() {
486 return Some(Polynomial::zero());
487 }
488 if q.is_one() {
489 return Some(p.clone());
490 }
491 if q.is_constant() {
492 return Some(p.scale(&q.constant_value().recip()));
494 }
495
496 let order = p.order;
497 let divisor = Polynomial::from_terms(q.terms().to_vec(), order);
501 let lead = divisor.leading_term()?;
502 let lead_coeff = lead.coeff.clone();
503 let lead_mono = lead.monomial.clone();
504
505 let mut quotient = Polynomial::from_terms(Vec::<Term>::new(), order);
506 let mut rest = Polynomial::from_terms(p.terms().to_vec(), order);
507
508 loop {
509 let Some(term) = rest.leading_term().cloned() else {
510 return Some(quotient);
511 };
512 let mono = term.monomial.div(&lead_mono)?;
515 let coeff = &term.coeff / &lead_coeff;
516 let step = Polynomial::from_terms(vec![Term::new(coeff, mono)], order);
517 quotient = quotient.add(&step);
518 rest = rest.sub(&divisor.mul(&step));
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use num_bigint::BigInt;
526 use num_rational::BigRational;
527 use num_traits::One;
528
529 const X: Var = 0;
531 const Y: Var = 1;
532 const Z: Var = 2;
533
534 fn var(v: Var) -> Polynomial {
535 Polynomial::from_var(v)
536 }
537
538 fn int(n: i64) -> Polynomial {
539 Polynomial::constant(BigRational::from_integer(BigInt::from(n)))
540 }
541
542 fn assert_associate(a: &Polynomial, b: &Polynomial) {
544 assert!(
545 exact_division(a, b).is_some() && exact_division(b, a).is_some(),
546 "{a:?} and {b:?} are not associates"
547 );
548 }
549
550 #[test]
551 fn test_engine_creation() {
552 let engine = MultivariateGcdEngine::default_config();
553 assert_eq!(engine.stats().max_depth, 0);
554 }
555
556 #[test]
557 fn test_gcd_constants() {
558 let mut engine = MultivariateGcdEngine::default_config();
559
560 let gcd = engine.gcd(&int(6), &int(9));
561
562 assert!(gcd.is_one());
565 assert!(!engine.stats().incomplete);
566 }
567
568 #[test]
573 fn test_depth_ceiling_is_reported_in_stats() {
574 let config = MultivariateGcdConfig {
575 max_recursion_depth: 1,
576 ..MultivariateGcdConfig::default()
577 };
578 let mut engine = MultivariateGcdEngine::new(config);
579
580 let p = var(X).mul(&var(Y));
583 let q = var(X).mul(&var(Y)).mul(&var(X));
584
585 let gcd = engine.gcd(&p, &q);
586 assert!(engine.stats().incomplete);
587 assert!(!gcd.is_zero());
589 assert!(engine.stats().max_depth <= 1);
590 }
591
592 #[test]
593 fn test_gcd_univariate() {
594 let mut engine = MultivariateGcdEngine::default_config();
595
596 let p = Polynomial::from_coeffs_int(&[(1, &[(X, 2)]), (-1, &[])]);
598 let q = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (-1, &[])]);
600
601 let gcd = engine.gcd(&p, &q);
602
603 assert_associate(&gcd, &q);
604 assert!(!engine.stats().incomplete);
605 }
606
607 #[test]
610 fn test_gcd_agrees_with_univariate_engine() {
611 let mut engine = MultivariateGcdEngine::default_config();
612
613 let x_minus_1 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (-1, &[])]);
615 let x_plus_1 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (1, &[])]);
616 let x_plus_2 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (2, &[])]);
617 let x_minus_3 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (-3, &[])]);
618
619 let p = x_minus_1.mul(&x_plus_1).mul(&x_plus_2);
620 let q = x_minus_1.mul(&x_plus_1).mul(&x_minus_3);
621
622 let gcd = engine.gcd(&p, &q);
623 let expected = x_minus_1.mul(&x_plus_1);
624
625 assert_associate(&gcd, &expected);
626 assert_associate(&gcd, &p.gcd_univariate(&q));
627 assert!(!engine.stats().incomplete);
628 }
629
630 #[test]
631 fn test_gcd_monomials_shares_single_variable() {
632 let mut engine = MultivariateGcdEngine::default_config();
633
634 let p = var(X).mul(&var(Y));
636 let q = var(X).mul(&var(Z));
637
638 let gcd = engine.gcd(&p, &q);
639
640 assert_eq!(gcd, var(X));
641 assert!(!engine.stats().incomplete);
642 }
643
644 #[test]
645 fn test_gcd_shared_multivariate_factor() {
646 let mut engine = MultivariateGcdEngine::default_config();
647
648 let x_plus_y = var(X).add(&var(Y));
650 let x_minus_z = var(X).sub(&var(Z));
651
652 let p = x_plus_y.pow(2).mul(&x_minus_z);
653 let q = x_plus_y.mul(&var(X).pow(2));
654
655 let gcd = engine.gcd(&p, &q);
656
657 assert_associate(&gcd, &x_plus_y);
658 assert_eq!(gcd, x_plus_y);
660 assert!(!engine.stats().incomplete);
661 }
662
663 #[test]
664 fn test_gcd_result_divides_both_inputs() {
665 let mut engine = MultivariateGcdEngine::default_config();
666
667 let common = var(X).mul(&var(Y)).add(&var(Z));
669 let p = common.mul(&var(X).add(&var(Y).mul(&int(2))));
670 let q = common.mul(&var(Y).sub(&var(Z)));
671
672 let gcd = engine.gcd(&p, &q);
673
674 assert!(exact_division(&p, &gcd).is_some(), "gcd must divide p");
675 assert!(exact_division(&q, &gcd).is_some(), "gcd must divide q");
676 assert_associate(&gcd, &common);
677 assert!(!engine.stats().incomplete);
678 }
679
680 #[test]
681 fn test_gcd_coprime_multivariate_is_a_unit() {
682 let mut engine = MultivariateGcdEngine::default_config();
683
684 let p = var(X).add(&var(Y));
685 let q = var(X).sub(&var(Y)).add(&int(1));
686
687 let gcd = engine.gcd(&p, &q);
688
689 assert!(gcd.is_one(), "expected a unit GCD, got {gcd:?}");
690 assert!(!engine.stats().incomplete);
691 }
692
693 #[test]
694 fn test_gcd_with_zero_operand() {
695 let mut engine = MultivariateGcdEngine::default_config();
696
697 let p = var(X).mul(&var(Y));
698 let gcd = engine.gcd(&p, &Polynomial::zero());
699
700 assert_associate(&gcd, &p);
701 assert!(
702 engine
703 .gcd(&Polynomial::zero(), &Polynomial::zero())
704 .is_zero()
705 );
706 }
707
708 #[test]
711 fn test_gcd_same_without_intermediate_primitive_parts() {
712 let config = MultivariateGcdConfig {
713 use_primitive_part: false,
714 ..MultivariateGcdConfig::default()
715 };
716 let mut engine = MultivariateGcdEngine::new(config);
717
718 let x_plus_y = var(X).add(&var(Y));
719 let p = x_plus_y.pow(2).mul(&var(X).sub(&var(Z)));
720 let q = x_plus_y.mul(&var(X).pow(2));
721
722 let gcd = engine.gcd(&p, &q);
723
724 assert_eq!(gcd, x_plus_y);
725 assert!(!engine.stats().incomplete);
726 }
727
728 #[test]
729 fn test_pseudo_remainder_is_not_a_stub() {
730 let p = var(X).pow(2).add(&var(Y));
733 let q = var(X).add(&var(Y));
734
735 let r = pseudo_remainder(&p, &q, X);
736
737 assert!(!r.is_zero(), "a stub would wrongly return zero here");
738 assert_eq!(r.degree(X), 0);
739 assert_eq!(r, var(Y).pow(2).add(&var(Y)));
740 }
741
742 #[test]
743 fn test_pseudo_remainder_identity_holds() {
744 let a = var(X).pow(3).mul(&var(Y)).add(&var(Z));
747 let b = var(Y).mul(&var(X).pow(2)).add(&var(X)).add(&int(1));
748
749 let r = pseudo_remainder(&a, &b, X);
750 assert!(r.degree(X) < b.degree(X));
751
752 let lc_b = b.leading_coeff_wrt(X);
753 let k = a.degree(X) - b.degree(X) + 1;
754 let lhs = lc_b.pow(k).mul(&a).sub(&r);
755
756 assert!(
757 exact_division(&lhs, &b).is_some(),
758 "pseudo-division identity violated"
759 );
760 }
761
762 #[test]
763 fn test_pseudo_remainder_zero_divisor_no_panic() {
764 let a = var(X).add(&int(1));
765 assert_eq!(pseudo_remainder(&a, &Polynomial::zero(), X), a);
766 }
767
768 #[test]
769 fn test_exact_division_reports_non_divisibility() {
770 let p = var(X).add(&var(Z));
772 let q = var(X).add(&var(Y));
773
774 assert!(exact_division(&p, &q).is_none());
775 assert!(exact_division(&p, &Polynomial::zero()).is_none());
777 }
778
779 #[test]
780 fn test_exact_division_recovers_the_factor() {
781 let a = var(X).mul(&var(Y)).add(&var(Z)).add(&int(3));
782 let b = var(X).pow(2).sub(&var(Y).mul(&var(Z)));
783 let product = a.mul(&b);
784
785 assert_eq!(exact_division(&product, &b), Some(a.clone()));
786 assert_eq!(exact_division(&product, &a), Some(b));
787 }
788
789 #[test]
790 fn test_var_selection_max_degree() {
791 let engine = MultivariateGcdEngine::default_config();
792
793 let p = Polynomial::from_coeffs_int(&[(1, &[(X, 3)]), (1, &[(Y, 2)])]);
795 let q = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (1, &[(Y, 3)])]);
797
798 let main_var = engine.select_by_max_degree(&p, &q);
799
800 assert_eq!(main_var, Y);
802 }
803
804 #[test]
805 fn test_extract_coefficients() {
806 let engine = MultivariateGcdEngine::default_config();
807
808 let p = Polynomial::from_coeffs_int(&[(2, &[(X, 2), (Y, 1)]), (3, &[(X, 1), (Y, 2)])]);
810
811 let coeffs = engine.extract_coefficients(&p, X);
812
813 assert_eq!(coeffs.len(), 2);
815 assert!(coeffs.iter().all(|c| c.degree(X) == 0));
816 }
817
818 #[test]
819 fn test_extract_content_splits_exactly() {
820 let mut engine = MultivariateGcdEngine::default_config();
821
822 let p = var(Y).mul(&var(X).pow(2).add(&var(X)));
824
825 let (content, primitive) = engine.extract_content(&p, X, 0);
826
827 assert_eq!(content, var(Y));
828 assert_eq!(content.mul(&primitive), p);
829 }
830
831 #[test]
832 fn test_normalize_gcd() {
833 let engine = MultivariateGcdEngine::default_config();
834
835 let p = Polynomial::from_coeffs_int(&[(6, &[(X, 2)]), (3, &[(X, 1)])]);
837
838 let normalized = engine.normalize_gcd(&p);
839
840 let half = BigRational::new(BigInt::from(1), BigInt::from(2));
842 assert!(normalized.leading_coeff().is_one());
843 assert_eq!(normalized, var(X).pow(2).add(&var(X).scale(&half)));
844 }
845
846 #[cfg(feature = "std")]
847 #[test]
851 fn test_many_variables_within_ceiling_returns_on_small_stack() {
852 let handle = std::thread::Builder::new()
853 .stack_size(1 << 20)
854 .spawn(|| {
855 let n: Var = 90;
856 let mut p = Polynomial::one();
857 for v in 0..n {
858 p = p.mul(&Polynomial::from_var(v));
859 }
860
861 let mut engine = MultivariateGcdEngine::default_config();
862 let gcd = engine.gcd(&p, &p);
863 (
864 gcd == p,
865 engine.stats().incomplete,
866 engine.stats().max_depth,
867 )
868 })
869 .expect("failed to spawn worker thread");
870
871 let (matched, incomplete, max_depth) = handle.join().expect("worker thread panicked");
872 assert!(matched, "gcd(p, p) must be p");
873 assert!(!incomplete);
874 assert!(
875 max_depth <= 90,
876 "depth {max_depth} exceeds the variable count"
877 );
878 }
879
880 #[cfg(feature = "std")]
881 #[test]
884 fn test_more_variables_than_budget_gives_up_honestly() {
885 let handle = std::thread::Builder::new()
886 .stack_size(1 << 20)
887 .spawn(|| {
888 let n: Var = 150;
889 let mut p = Polynomial::one();
890 for v in 0..n {
891 p = p.mul(&Polynomial::from_var(v));
892 }
893
894 let mut engine = MultivariateGcdEngine::default_config();
895 let gcd = engine.gcd(&p, &p);
896 (
897 gcd.is_zero(),
898 engine.stats().incomplete,
899 engine.stats().max_depth,
900 )
901 })
902 .expect("failed to spawn worker thread");
903
904 let (is_zero, incomplete, max_depth) = handle.join().expect("worker thread panicked");
905 assert!(!is_zero, "the fallback divisor must still be nonzero");
906 assert!(incomplete, "the give-up must be reported");
907 assert_eq!(max_depth, 100);
908 }
909}