1use super::{Monomial, MonomialOrder, Polynomial, Term, Var};
21#[allow(unused_imports)]
22use crate::prelude::*;
23use num_traits::Zero;
24
25#[derive(Debug, Clone)]
27pub struct MultivariateGcdConfig {
28 pub var_selection: VarSelectionStrategy,
30 pub use_primitive_part: bool,
32 pub max_recursion_depth: usize,
34}
35
36impl Default for MultivariateGcdConfig {
37 fn default() -> Self {
38 Self {
39 var_selection: VarSelectionStrategy::MaxDegree,
40 use_primitive_part: true,
41 max_recursion_depth: 100,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum VarSelectionStrategy {
49 MaxDegree,
51 MaxLeadingDegree,
53 FirstVariable,
55}
56
57#[derive(Debug, Clone, Default)]
59pub struct MultivariateGcdStats {
60 pub max_depth: usize,
62 pub primitive_decompositions: u64,
64 pub content_gcds: u64,
66}
67
68pub struct MultivariateGcdEngine {
70 config: MultivariateGcdConfig,
72 stats: MultivariateGcdStats,
74}
75
76impl MultivariateGcdEngine {
77 pub fn new(config: MultivariateGcdConfig) -> Self {
79 Self {
80 config,
81 stats: MultivariateGcdStats::default(),
82 }
83 }
84
85 pub fn default_config() -> Self {
87 Self::new(MultivariateGcdConfig::default())
88 }
89
90 pub fn gcd(&mut self, p: &Polynomial, q: &Polynomial) -> Polynomial {
92 self.gcd_recursive(p, q, 0)
93 }
94
95 fn gcd_recursive(&mut self, p: &Polynomial, q: &Polynomial, depth: usize) -> Polynomial {
97 if depth > self.stats.max_depth {
99 self.stats.max_depth = depth;
100 }
101
102 if depth >= self.config.max_recursion_depth {
104 return Polynomial::one();
105 }
106
107 if p.is_zero() {
109 return q.clone();
110 }
111 if q.is_zero() {
112 return p.clone();
113 }
114
115 let p_vars = p.vars();
117 let q_vars = q.vars();
118
119 if p_vars.len() <= 1 && q_vars.len() <= 1 {
120 return if p_vars.is_empty() || q_vars.is_empty() {
122 Polynomial::one()
124 } else {
125 p.gcd_univariate(q)
126 };
127 }
128
129 let main_var = self.select_main_variable(p, q);
131
132 if self.config.use_primitive_part {
134 let (p_content, p_primitive) = self.extract_content(p, main_var);
135 let (q_content, q_primitive) = self.extract_content(q, main_var);
136
137 self.stats.primitive_decompositions += 2;
138
139 let content_gcd = self.gcd_recursive(&p_content, &q_content, depth + 1);
141 self.stats.content_gcds += 1;
142
143 let primitive_gcd = self.gcd_recursive(&p_primitive, &q_primitive, depth + 1);
144
145 return &content_gcd * &primitive_gcd;
146 }
147
148 let mut a = p.clone();
150 let mut b = q.clone();
151
152 while !b.is_zero() {
153 let r = self.pseudo_remainder(&a, &b, main_var);
154 a = b;
155 b = r;
156 }
157
158 self.normalize_gcd(&a)
160 }
161
162 fn select_main_variable(&self, p: &Polynomial, q: &Polynomial) -> Var {
164 match self.config.var_selection {
165 VarSelectionStrategy::MaxDegree => self.select_by_max_degree(p, q),
166 VarSelectionStrategy::MaxLeadingDegree => self.select_by_leading_degree(p, q),
167 VarSelectionStrategy::FirstVariable => {
168 p.vars()
170 .first()
171 .copied()
172 .or_else(|| q.vars().first().copied())
173 .unwrap_or(0)
174 }
175 }
176 }
177
178 fn select_by_max_degree(&self, p: &Polynomial, q: &Polynomial) -> Var {
180 let mut var_degrees: FxHashMap<Var, u32> = FxHashMap::default();
181
182 for var in p.vars() {
183 let deg_p = p.degree(var);
184 let deg_q = q.degree(var);
185 var_degrees.insert(var, deg_p.max(deg_q));
186 }
187
188 for var in q.vars() {
189 var_degrees.entry(var).or_insert_with(|| q.degree(var));
190 }
191
192 var_degrees
194 .iter()
195 .max_by_key(|(_, deg)| *deg)
196 .map(|(var, _)| *var)
197 .unwrap_or(0)
198 }
199
200 fn select_by_leading_degree(&self, p: &Polynomial, q: &Polynomial) -> Var {
202 let p_lead = p.leading_monomial();
204 let q_lead = q.leading_monomial();
205
206 let mut max_var = 0;
208 let mut max_degree = 0;
209
210 if let Some(p_mono) = p_lead {
211 for vp in p_mono.vars() {
212 if vp.power > max_degree {
213 max_degree = vp.power;
214 max_var = vp.var;
215 }
216 }
217 }
218
219 if let Some(q_mono) = q_lead {
220 for vp in q_mono.vars() {
221 if vp.power > max_degree {
222 max_degree = vp.power;
223 max_var = vp.var;
224 }
225 }
226 }
227
228 max_var
229 }
230
231 fn extract_content(&mut self, p: &Polynomial, main_var: Var) -> (Polynomial, Polynomial) {
236 let coefficients = self.extract_coefficients(p, main_var);
238
239 if coefficients.is_empty() {
240 return (Polynomial::one(), p.clone());
241 }
242
243 let mut content = coefficients[0].clone();
245 for coeff in coefficients.iter().skip(1) {
246 content = self.gcd_recursive(&content, coeff, 0);
247 }
248
249 let primitive = self.exact_division(p, &content);
251
252 (content, primitive)
253 }
254
255 fn extract_coefficients(&self, p: &Polynomial, var: Var) -> Vec<Polynomial> {
257 let mut coeffs: FxHashMap<usize, Polynomial> = FxHashMap::default();
258
259 for term in p.terms() {
260 let power = term.monomial.degree(var);
262
263 let reduced_powers: Vec<(Var, u32)> = term
265 .monomial
266 .vars()
267 .iter()
268 .filter(|vp| vp.var != var)
269 .map(|vp| (vp.var, vp.power))
270 .collect();
271
272 let reduced_mono = Monomial::from_powers(reduced_powers);
273 let reduced_term = Term::new(term.coeff.clone(), reduced_mono);
274
275 let entry = coeffs
276 .entry(power as usize)
277 .or_insert_with(Polynomial::zero);
278 *entry =
279 entry.clone() + Polynomial::from_terms(vec![reduced_term], MonomialOrder::GRevLex);
280 }
281
282 coeffs.values().cloned().collect()
283 }
284
285 fn pseudo_remainder(&self, a: &Polynomial, b: &Polynomial, _main_var: Var) -> Polynomial {
287 if b.is_zero() {
290 return a.clone();
291 }
292
293 Polynomial::zero()
296 }
297
298 fn exact_division(&self, p: &Polynomial, q: &Polynomial) -> Polynomial {
300 if q.is_one() {
301 return p.clone();
302 }
303
304 p.clone()
307 }
308
309 fn normalize_gcd(&self, p: &Polynomial) -> Polynomial {
311 if p.is_zero() {
312 return Polynomial::zero();
313 }
314
315 let lead = p.leading_coeff();
317
318 if lead.is_zero() {
319 return p.clone();
320 }
321
322 let normalized_terms: Vec<Term> = p
324 .terms()
325 .iter()
326 .map(|term| Term::new(&term.coeff / &lead, term.monomial.clone()))
327 .collect();
328 Polynomial::from_terms(normalized_terms, MonomialOrder::GRevLex)
329 }
330
331 pub fn stats(&self) -> &MultivariateGcdStats {
333 &self.stats
334 }
335
336 pub fn reset_stats(&mut self) {
338 self.stats = MultivariateGcdStats::default();
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use num_bigint::BigInt;
346 use num_rational::BigRational;
347 use num_traits::One;
348
349 #[test]
350 fn test_engine_creation() {
351 let engine = MultivariateGcdEngine::default_config();
352 assert_eq!(engine.stats().max_depth, 0);
353 }
354
355 #[test]
356 fn test_gcd_constants() {
357 let mut engine = MultivariateGcdEngine::default_config();
358
359 let p = Polynomial::constant(BigRational::from_integer(BigInt::from(6)));
360 let q = Polynomial::constant(BigRational::from_integer(BigInt::from(9)));
361
362 let gcd = engine.gcd(&p, &q);
363
364 assert!(!gcd.is_zero());
366 }
367
368 #[test]
369 fn test_gcd_univariate() {
370 let mut engine = MultivariateGcdEngine::default_config();
371
372 let p = Polynomial::from_coeffs_int(&[(1, &[(0, 2)]), (-1, &[])]);
374
375 let q = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (-1, &[])]);
377
378 let gcd = engine.gcd(&p, &q);
379
380 assert!(!gcd.is_zero());
382 assert_eq!(gcd.total_degree(), 1);
383 }
384
385 #[test]
386 fn test_var_selection_max_degree() {
387 let engine = MultivariateGcdEngine::default_config();
388
389 let p = Polynomial::from_coeffs_int(&[(1, &[(0, 3)]), (1, &[(1, 2)])]);
391
392 let q = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[(1, 3)])]);
394
395 let main_var = engine.select_by_max_degree(&p, &q);
396
397 assert_eq!(main_var, 1);
399 }
400
401 #[test]
402 fn test_extract_coefficients() {
403 let engine = MultivariateGcdEngine::default_config();
404
405 let p = Polynomial::from_coeffs_int(&[(2, &[(0, 2), (1, 1)]), (3, &[(0, 1), (1, 2)])]);
407
408 let coeffs = engine.extract_coefficients(&p, 0);
410
411 assert_eq!(coeffs.len(), 2);
413 }
414
415 #[test]
416 fn test_normalize_gcd() {
417 let engine = MultivariateGcdEngine::default_config();
418
419 let p = Polynomial::from_coeffs_int(&[(6, &[(0, 2)]), (3, &[(0, 1)])]);
421
422 let normalized = engine.normalize_gcd(&p);
423
424 let lead = normalized.leading_coeff();
427 if lead.is_one() {
429 } else if !normalized.is_zero() {
431 assert!(!lead.is_zero());
433 }
434 }
435}