use super::{Monomial, Polynomial, Term, Var};
#[allow(unused_imports)]
use crate::prelude::*;
use num_traits::Zero;
#[derive(Debug, Clone)]
pub struct MultivariateGcdConfig {
pub var_selection: VarSelectionStrategy,
pub use_primitive_part: bool,
pub max_recursion_depth: usize,
}
impl Default for MultivariateGcdConfig {
fn default() -> Self {
Self {
var_selection: VarSelectionStrategy::MaxDegree,
use_primitive_part: true,
max_recursion_depth: 100,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VarSelectionStrategy {
MaxDegree,
MaxLeadingDegree,
FirstVariable,
}
#[derive(Debug, Clone, Default)]
pub struct MultivariateGcdStats {
pub max_depth: usize,
pub primitive_decompositions: u64,
pub content_gcds: u64,
pub pseudo_divisions: u64,
pub incomplete: bool,
}
pub struct MultivariateGcdEngine {
config: MultivariateGcdConfig,
stats: MultivariateGcdStats,
}
impl MultivariateGcdEngine {
pub fn new(config: MultivariateGcdConfig) -> Self {
Self {
config,
stats: MultivariateGcdStats::default(),
}
}
pub fn default_config() -> Self {
Self::new(MultivariateGcdConfig::default())
}
pub fn gcd(&mut self, p: &Polynomial, q: &Polynomial) -> Polynomial {
let g = self.gcd_recursive(p, q, 0);
self.normalize_gcd(&g)
}
fn gcd_recursive(&mut self, p: &Polynomial, q: &Polynomial, depth: usize) -> Polynomial {
if depth > self.stats.max_depth {
self.stats.max_depth = depth;
}
if depth >= self.config.max_recursion_depth {
self.stats.incomplete = true;
return Polynomial::one();
}
if p.is_zero() {
return q.clone();
}
if q.is_zero() {
return p.clone();
}
if p.is_constant() || q.is_constant() {
return Polynomial::one();
}
let mut vars = p.vars();
vars.extend(q.vars());
vars.sort_unstable();
vars.dedup();
match vars.len() {
0 => return Polynomial::one(),
1 => return p.gcd_univariate(q),
_ => {}
}
let main_var = self.select_main_variable(p, q);
let (p_content, p_primitive) = self.extract_content(p, main_var, depth);
let (q_content, q_primitive) = self.extract_content(q, main_var, depth);
self.stats.primitive_decompositions += 2;
let content_gcd = self.gcd_recursive(&p_content, &q_content, depth + 1);
self.stats.content_gcds += 1;
let primitive_gcd = self.primitive_prs(&p_primitive, &q_primitive, main_var, depth);
content_gcd.mul(&primitive_gcd)
}
fn primitive_prs(
&mut self,
f: &Polynomial,
g: &Polynomial,
var: Var,
depth: usize,
) -> Polynomial {
let mut a = f.clone();
let mut b = g.clone();
if a.degree(var) < b.degree(var) {
core::mem::swap(&mut a, &mut b);
}
while !b.is_zero() {
if b.degree(var) == 0 {
return Polynomial::one();
}
let r = pseudo_remainder(&a, &b, var);
self.stats.pseudo_divisions += 1;
a = b;
b = if r.is_zero() || !self.config.use_primitive_part {
r
} else {
self.extract_content(&r, var, depth).1
};
}
if a.is_zero() {
return Polynomial::zero();
}
self.extract_content(&a, var, depth).1
}
fn select_main_variable(&self, p: &Polynomial, q: &Polynomial) -> Var {
match self.config.var_selection {
VarSelectionStrategy::MaxDegree => self.select_by_max_degree(p, q),
VarSelectionStrategy::MaxLeadingDegree => self.select_by_leading_degree(p, q),
VarSelectionStrategy::FirstVariable => {
p.vars()
.first()
.copied()
.or_else(|| q.vars().first().copied())
.unwrap_or(0)
}
}
}
fn select_by_max_degree(&self, p: &Polynomial, q: &Polynomial) -> Var {
let mut vars = p.vars();
vars.extend(q.vars());
vars.sort_unstable();
vars.dedup();
vars.iter()
.max_by_key(|var| p.degree(**var).max(q.degree(**var)))
.copied()
.unwrap_or(0)
}
fn select_by_leading_degree(&self, p: &Polynomial, q: &Polynomial) -> Var {
let p_lead = p.leading_monomial();
let q_lead = q.leading_monomial();
let mut max_var = 0;
let mut max_degree = 0;
if let Some(p_mono) = p_lead {
for vp in p_mono.vars() {
if vp.power > max_degree {
max_degree = vp.power;
max_var = vp.var;
}
}
}
if let Some(q_mono) = q_lead {
for vp in q_mono.vars() {
if vp.power > max_degree {
max_degree = vp.power;
max_var = vp.var;
}
}
}
max_var
}
fn extract_content(
&mut self,
p: &Polynomial,
main_var: Var,
depth: usize,
) -> (Polynomial, Polynomial) {
if p.is_zero() {
return (Polynomial::one(), p.clone());
}
let coefficients = self.extract_coefficients(p, main_var);
let mut content = Polynomial::zero();
for coeff in &coefficients {
content = self.gcd_recursive(&content, coeff, depth + 1);
if content.is_constant() {
break;
}
}
if content.is_zero() || content.is_constant() {
return (Polynomial::one(), p.clone());
}
match exact_division(p, &content) {
Some(primitive) => (content, primitive),
None => {
self.stats.incomplete = true;
(Polynomial::one(), p.clone())
}
}
}
fn extract_coefficients(&self, p: &Polynomial, var: Var) -> Vec<Polynomial> {
let degree = p.degree(var);
let mut coeffs = Vec::with_capacity(degree as usize + 1);
for k in 0..=degree {
let c = p.coeff(var, k);
if !c.is_zero() {
coeffs.push(c);
}
}
coeffs
}
fn normalize_gcd(&self, p: &Polynomial) -> Polynomial {
if p.is_zero() {
return Polynomial::zero();
}
let lead = p.leading_coeff();
if lead.is_zero() {
return p.clone();
}
p.scale(&lead.recip())
}
pub fn stats(&self) -> &MultivariateGcdStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = MultivariateGcdStats::default();
}
}
fn pseudo_remainder(a: &Polynomial, b: &Polynomial, var: Var) -> Polynomial {
if b.is_zero() || a.is_zero() {
return a.clone();
}
let deg_b = b.degree(var);
let deg_a = a.degree(var);
if deg_a < deg_b {
return a.clone();
}
let lc_b = b.leading_coeff_wrt(var);
if lc_b.is_zero() {
return a.clone();
}
let steps = deg_a - deg_b + 1;
let mut used = 0u32;
let mut r = a.clone();
while used < steps && !r.is_zero() && r.degree(var) >= deg_b {
used += 1;
let deg_r = r.degree(var);
let lc_r = r.leading_coeff_wrt(var);
let shift = Monomial::from_var_power(var, deg_r - deg_b);
let scaled = lc_b.mul(&r);
let subtractor = lc_r.mul(b).mul_monomial(&shift);
r = scaled.sub(&subtractor);
}
let leftover = steps - used;
if leftover > 0 && !r.is_zero() {
r = r.mul(&lc_b.pow(leftover));
}
r
}
fn exact_division(p: &Polynomial, q: &Polynomial) -> Option<Polynomial> {
if q.is_zero() {
return None;
}
if p.is_zero() {
return Some(Polynomial::zero());
}
if q.is_one() {
return Some(p.clone());
}
if q.is_constant() {
return Some(p.scale(&q.constant_value().recip()));
}
let order = p.order;
let divisor = Polynomial::from_terms(q.terms().to_vec(), order);
let lead = divisor.leading_term()?;
let lead_coeff = lead.coeff.clone();
let lead_mono = lead.monomial.clone();
let mut quotient = Polynomial::from_terms(Vec::<Term>::new(), order);
let mut rest = Polynomial::from_terms(p.terms().to_vec(), order);
loop {
let Some(term) = rest.leading_term().cloned() else {
return Some(quotient);
};
let mono = term.monomial.div(&lead_mono)?;
let coeff = &term.coeff / &lead_coeff;
let step = Polynomial::from_terms(vec![Term::new(coeff, mono)], order);
quotient = quotient.add(&step);
rest = rest.sub(&divisor.mul(&step));
}
}
#[cfg(test)]
mod tests {
use super::*;
use num_bigint::BigInt;
use num_rational::BigRational;
use num_traits::One;
const X: Var = 0;
const Y: Var = 1;
const Z: Var = 2;
fn var(v: Var) -> Polynomial {
Polynomial::from_var(v)
}
fn int(n: i64) -> Polynomial {
Polynomial::constant(BigRational::from_integer(BigInt::from(n)))
}
fn assert_associate(a: &Polynomial, b: &Polynomial) {
assert!(
exact_division(a, b).is_some() && exact_division(b, a).is_some(),
"{a:?} and {b:?} are not associates"
);
}
#[test]
fn test_engine_creation() {
let engine = MultivariateGcdEngine::default_config();
assert_eq!(engine.stats().max_depth, 0);
}
#[test]
fn test_gcd_constants() {
let mut engine = MultivariateGcdEngine::default_config();
let gcd = engine.gcd(&int(6), &int(9));
assert!(gcd.is_one());
assert!(!engine.stats().incomplete);
}
#[test]
fn test_depth_ceiling_is_reported_in_stats() {
let config = MultivariateGcdConfig {
max_recursion_depth: 1,
..MultivariateGcdConfig::default()
};
let mut engine = MultivariateGcdEngine::new(config);
let p = var(X).mul(&var(Y));
let q = var(X).mul(&var(Y)).mul(&var(X));
let gcd = engine.gcd(&p, &q);
assert!(engine.stats().incomplete);
assert!(!gcd.is_zero());
assert!(engine.stats().max_depth <= 1);
}
#[test]
fn test_gcd_univariate() {
let mut engine = MultivariateGcdEngine::default_config();
let p = Polynomial::from_coeffs_int(&[(1, &[(X, 2)]), (-1, &[])]);
let q = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (-1, &[])]);
let gcd = engine.gcd(&p, &q);
assert_associate(&gcd, &q);
assert!(!engine.stats().incomplete);
}
#[test]
fn test_gcd_agrees_with_univariate_engine() {
let mut engine = MultivariateGcdEngine::default_config();
let x_minus_1 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (-1, &[])]);
let x_plus_1 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (1, &[])]);
let x_plus_2 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (2, &[])]);
let x_minus_3 = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (-3, &[])]);
let p = x_minus_1.mul(&x_plus_1).mul(&x_plus_2);
let q = x_minus_1.mul(&x_plus_1).mul(&x_minus_3);
let gcd = engine.gcd(&p, &q);
let expected = x_minus_1.mul(&x_plus_1);
assert_associate(&gcd, &expected);
assert_associate(&gcd, &p.gcd_univariate(&q));
assert!(!engine.stats().incomplete);
}
#[test]
fn test_gcd_monomials_shares_single_variable() {
let mut engine = MultivariateGcdEngine::default_config();
let p = var(X).mul(&var(Y));
let q = var(X).mul(&var(Z));
let gcd = engine.gcd(&p, &q);
assert_eq!(gcd, var(X));
assert!(!engine.stats().incomplete);
}
#[test]
fn test_gcd_shared_multivariate_factor() {
let mut engine = MultivariateGcdEngine::default_config();
let x_plus_y = var(X).add(&var(Y));
let x_minus_z = var(X).sub(&var(Z));
let p = x_plus_y.pow(2).mul(&x_minus_z);
let q = x_plus_y.mul(&var(X).pow(2));
let gcd = engine.gcd(&p, &q);
assert_associate(&gcd, &x_plus_y);
assert_eq!(gcd, x_plus_y);
assert!(!engine.stats().incomplete);
}
#[test]
fn test_gcd_result_divides_both_inputs() {
let mut engine = MultivariateGcdEngine::default_config();
let common = var(X).mul(&var(Y)).add(&var(Z));
let p = common.mul(&var(X).add(&var(Y).mul(&int(2))));
let q = common.mul(&var(Y).sub(&var(Z)));
let gcd = engine.gcd(&p, &q);
assert!(exact_division(&p, &gcd).is_some(), "gcd must divide p");
assert!(exact_division(&q, &gcd).is_some(), "gcd must divide q");
assert_associate(&gcd, &common);
assert!(!engine.stats().incomplete);
}
#[test]
fn test_gcd_coprime_multivariate_is_a_unit() {
let mut engine = MultivariateGcdEngine::default_config();
let p = var(X).add(&var(Y));
let q = var(X).sub(&var(Y)).add(&int(1));
let gcd = engine.gcd(&p, &q);
assert!(gcd.is_one(), "expected a unit GCD, got {gcd:?}");
assert!(!engine.stats().incomplete);
}
#[test]
fn test_gcd_with_zero_operand() {
let mut engine = MultivariateGcdEngine::default_config();
let p = var(X).mul(&var(Y));
let gcd = engine.gcd(&p, &Polynomial::zero());
assert_associate(&gcd, &p);
assert!(
engine
.gcd(&Polynomial::zero(), &Polynomial::zero())
.is_zero()
);
}
#[test]
fn test_gcd_same_without_intermediate_primitive_parts() {
let config = MultivariateGcdConfig {
use_primitive_part: false,
..MultivariateGcdConfig::default()
};
let mut engine = MultivariateGcdEngine::new(config);
let x_plus_y = var(X).add(&var(Y));
let p = x_plus_y.pow(2).mul(&var(X).sub(&var(Z)));
let q = x_plus_y.mul(&var(X).pow(2));
let gcd = engine.gcd(&p, &q);
assert_eq!(gcd, x_plus_y);
assert!(!engine.stats().incomplete);
}
#[test]
fn test_pseudo_remainder_is_not_a_stub() {
let p = var(X).pow(2).add(&var(Y));
let q = var(X).add(&var(Y));
let r = pseudo_remainder(&p, &q, X);
assert!(!r.is_zero(), "a stub would wrongly return zero here");
assert_eq!(r.degree(X), 0);
assert_eq!(r, var(Y).pow(2).add(&var(Y)));
}
#[test]
fn test_pseudo_remainder_identity_holds() {
let a = var(X).pow(3).mul(&var(Y)).add(&var(Z));
let b = var(Y).mul(&var(X).pow(2)).add(&var(X)).add(&int(1));
let r = pseudo_remainder(&a, &b, X);
assert!(r.degree(X) < b.degree(X));
let lc_b = b.leading_coeff_wrt(X);
let k = a.degree(X) - b.degree(X) + 1;
let lhs = lc_b.pow(k).mul(&a).sub(&r);
assert!(
exact_division(&lhs, &b).is_some(),
"pseudo-division identity violated"
);
}
#[test]
fn test_pseudo_remainder_zero_divisor_no_panic() {
let a = var(X).add(&int(1));
assert_eq!(pseudo_remainder(&a, &Polynomial::zero(), X), a);
}
#[test]
fn test_exact_division_reports_non_divisibility() {
let p = var(X).add(&var(Z));
let q = var(X).add(&var(Y));
assert!(exact_division(&p, &q).is_none());
assert!(exact_division(&p, &Polynomial::zero()).is_none());
}
#[test]
fn test_exact_division_recovers_the_factor() {
let a = var(X).mul(&var(Y)).add(&var(Z)).add(&int(3));
let b = var(X).pow(2).sub(&var(Y).mul(&var(Z)));
let product = a.mul(&b);
assert_eq!(exact_division(&product, &b), Some(a.clone()));
assert_eq!(exact_division(&product, &a), Some(b));
}
#[test]
fn test_var_selection_max_degree() {
let engine = MultivariateGcdEngine::default_config();
let p = Polynomial::from_coeffs_int(&[(1, &[(X, 3)]), (1, &[(Y, 2)])]);
let q = Polynomial::from_coeffs_int(&[(1, &[(X, 1)]), (1, &[(Y, 3)])]);
let main_var = engine.select_by_max_degree(&p, &q);
assert_eq!(main_var, Y);
}
#[test]
fn test_extract_coefficients() {
let engine = MultivariateGcdEngine::default_config();
let p = Polynomial::from_coeffs_int(&[(2, &[(X, 2), (Y, 1)]), (3, &[(X, 1), (Y, 2)])]);
let coeffs = engine.extract_coefficients(&p, X);
assert_eq!(coeffs.len(), 2);
assert!(coeffs.iter().all(|c| c.degree(X) == 0));
}
#[test]
fn test_extract_content_splits_exactly() {
let mut engine = MultivariateGcdEngine::default_config();
let p = var(Y).mul(&var(X).pow(2).add(&var(X)));
let (content, primitive) = engine.extract_content(&p, X, 0);
assert_eq!(content, var(Y));
assert_eq!(content.mul(&primitive), p);
}
#[test]
fn test_normalize_gcd() {
let engine = MultivariateGcdEngine::default_config();
let p = Polynomial::from_coeffs_int(&[(6, &[(X, 2)]), (3, &[(X, 1)])]);
let normalized = engine.normalize_gcd(&p);
let half = BigRational::new(BigInt::from(1), BigInt::from(2));
assert!(normalized.leading_coeff().is_one());
assert_eq!(normalized, var(X).pow(2).add(&var(X).scale(&half)));
}
#[cfg(feature = "std")]
#[test]
fn test_many_variables_within_ceiling_returns_on_small_stack() {
let handle = std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(|| {
let n: Var = 90;
let mut p = Polynomial::one();
for v in 0..n {
p = p.mul(&Polynomial::from_var(v));
}
let mut engine = MultivariateGcdEngine::default_config();
let gcd = engine.gcd(&p, &p);
(
gcd == p,
engine.stats().incomplete,
engine.stats().max_depth,
)
})
.expect("failed to spawn worker thread");
let (matched, incomplete, max_depth) = handle.join().expect("worker thread panicked");
assert!(matched, "gcd(p, p) must be p");
assert!(!incomplete);
assert!(
max_depth <= 90,
"depth {max_depth} exceeds the variable count"
);
}
#[cfg(feature = "std")]
#[test]
fn test_more_variables_than_budget_gives_up_honestly() {
let handle = std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(|| {
let n: Var = 150;
let mut p = Polynomial::one();
for v in 0..n {
p = p.mul(&Polynomial::from_var(v));
}
let mut engine = MultivariateGcdEngine::default_config();
let gcd = engine.gcd(&p, &p);
(
gcd.is_zero(),
engine.stats().incomplete,
engine.stats().max_depth,
)
})
.expect("failed to spawn worker thread");
let (is_zero, incomplete, max_depth) = handle.join().expect("worker thread panicked");
assert!(!is_zero, "the fallback divisor must still be nonzero");
assert!(incomplete, "the give-up must be reported");
assert_eq!(max_depth, 100);
}
}