Skip to main content

oxiz_math/polynomial/
gcd_multivariate.rs

1//! Multivariate Polynomial GCD.
2//!
3//! Extends GCD computation to multivariate polynomials with efficient algorithms.
4//! Implements recursive multivariate GCD reduction to univariate case.
5//!
6//! ## Algorithm
7//!
8//! For multivariate polynomials p(x1,...,xn), q(x1,...,xn):
9//! 1. Choose main variable xi
10//! 2. View p, q as univariate in xi with polynomial coefficients
11//! 3. Compute GCD recursively
12//! 4. Handle content and primitive parts
13//!
14//! ## References
15//!
16//! - von zur Gathen & Gerhard: "Modern Computer Algebra" Chapter 6
17//! - Knuth: "TAOCP Vol 2" Section 4.6.1
18//! - Z3's `math/polynomial/polynomial_gcd.cpp`
19
20use super::{Monomial, MonomialOrder, Polynomial, Term, Var};
21#[allow(unused_imports)]
22use crate::prelude::*;
23use num_traits::Zero;
24
25/// Configuration for multivariate GCD.
26#[derive(Debug, Clone)]
27pub struct MultivariateGcdConfig {
28    /// Main variable selection strategy.
29    pub var_selection: VarSelectionStrategy,
30    /// Use content/primitive part decomposition.
31    pub use_primitive_part: bool,
32    /// Maximum recursion depth.
33    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/// Strategy for selecting main variable.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum VarSelectionStrategy {
49    /// Choose variable with maximum total degree.
50    MaxDegree,
51    /// Choose variable with maximum degree in leading term.
52    MaxLeadingDegree,
53    /// Choose first variable in order.
54    FirstVariable,
55}
56
57/// Statistics for multivariate GCD.
58#[derive(Debug, Clone, Default)]
59pub struct MultivariateGcdStats {
60    /// Recursion depth reached.
61    pub max_depth: usize,
62    /// Primitive part decompositions.
63    pub primitive_decompositions: u64,
64    /// Content GCD computations.
65    pub content_gcds: u64,
66}
67
68/// Multivariate GCD engine.
69pub struct MultivariateGcdEngine {
70    /// Configuration.
71    config: MultivariateGcdConfig,
72    /// Statistics.
73    stats: MultivariateGcdStats,
74}
75
76impl MultivariateGcdEngine {
77    /// Create a new multivariate GCD engine.
78    pub fn new(config: MultivariateGcdConfig) -> Self {
79        Self {
80            config,
81            stats: MultivariateGcdStats::default(),
82        }
83    }
84
85    /// Create with default configuration.
86    pub fn default_config() -> Self {
87        Self::new(MultivariateGcdConfig::default())
88    }
89
90    /// Compute GCD of two multivariate polynomials.
91    pub fn gcd(&mut self, p: &Polynomial, q: &Polynomial) -> Polynomial {
92        self.gcd_recursive(p, q, 0)
93    }
94
95    /// Recursive GCD computation.
96    fn gcd_recursive(&mut self, p: &Polynomial, q: &Polynomial, depth: usize) -> Polynomial {
97        // Update max depth
98        if depth > self.stats.max_depth {
99            self.stats.max_depth = depth;
100        }
101
102        // Check recursion limit
103        if depth >= self.config.max_recursion_depth {
104            return Polynomial::one();
105        }
106
107        // Base cases
108        if p.is_zero() {
109            return q.clone();
110        }
111        if q.is_zero() {
112            return p.clone();
113        }
114
115        // Check if univariate
116        let p_vars = p.vars();
117        let q_vars = q.vars();
118
119        if p_vars.len() <= 1 && q_vars.len() <= 1 {
120            // Univariate case - use existing GCD
121            return if p_vars.is_empty() || q_vars.is_empty() {
122                // Constants
123                Polynomial::one()
124            } else {
125                p.gcd_univariate(q)
126            };
127        }
128
129        // Select main variable
130        let main_var = self.select_main_variable(p, q);
131
132        // Extract content and primitive parts
133        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            // GCD(p, q) = GCD(content(p), content(q)) * GCD(primitive(p), primitive(q))
140            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        // Multivariate Euclidean algorithm (simplified)
149        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        // Normalize
159        self.normalize_gcd(&a)
160    }
161
162    /// Select main variable for recursion.
163    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                // Get first variable from either polynomial
169                p.vars()
170                    .first()
171                    .copied()
172                    .or_else(|| q.vars().first().copied())
173                    .unwrap_or(0)
174            }
175        }
176    }
177
178    /// Select variable with maximum total degree.
179    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        // Find variable with maximum degree
193        var_degrees
194            .iter()
195            .max_by_key(|(_, deg)| *deg)
196            .map(|(var, _)| *var)
197            .unwrap_or(0)
198    }
199
200    /// Select variable with maximum degree in leading term.
201    fn select_by_leading_degree(&self, p: &Polynomial, q: &Polynomial) -> Var {
202        // Get leading monomials
203        let p_lead = p.leading_monomial();
204        let q_lead = q.leading_monomial();
205
206        // Find variable with max degree in either leading monomial
207        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    /// Extract content and primitive part with respect to main variable.
232    ///
233    /// For polynomial p = c * pp where c is the content (GCD of coefficients)
234    /// and pp is the primitive part.
235    fn extract_content(&mut self, p: &Polynomial, main_var: Var) -> (Polynomial, Polynomial) {
236        // View p as univariate in main_var
237        let coefficients = self.extract_coefficients(p, main_var);
238
239        if coefficients.is_empty() {
240            return (Polynomial::one(), p.clone());
241        }
242
243        // Compute GCD of all coefficients (content)
244        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        // Compute primitive part = p / content
250        let primitive = self.exact_division(p, &content);
251
252        (content, primitive)
253    }
254
255    /// Extract coefficients when viewing polynomial as univariate in given variable.
256    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            // Get power of main variable in this term
261            let power = term.monomial.degree(var);
262
263            // Create term without main variable
264            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    /// Compute pseudo-remainder of a divided by b.
286    fn pseudo_remainder(&self, a: &Polynomial, b: &Polynomial, _main_var: Var) -> Polynomial {
287        // Simplified: use regular remainder
288        // Full implementation would use pseudo-division to avoid fractions
289        if b.is_zero() {
290            return a.clone();
291        }
292
293        // For now, just return zero (indicating exact division)
294        // Real implementation needs proper polynomial division
295        Polynomial::zero()
296    }
297
298    /// Exact division of polynomials.
299    fn exact_division(&self, p: &Polynomial, q: &Polynomial) -> Polynomial {
300        if q.is_one() {
301            return p.clone();
302        }
303
304        // Simplified: assume exact division possible
305        // Real implementation needs polynomial long division
306        p.clone()
307    }
308
309    /// Normalize GCD result.
310    fn normalize_gcd(&self, p: &Polynomial) -> Polynomial {
311        if p.is_zero() {
312            return Polynomial::zero();
313        }
314
315        // Make monic (leading coefficient = 1)
316        let lead = p.leading_coeff();
317
318        if lead.is_zero() {
319            return p.clone();
320        }
321
322        // Divide all terms by leading coefficient
323        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    /// Get statistics.
332    pub fn stats(&self) -> &MultivariateGcdStats {
333        &self.stats
334    }
335
336    /// Reset statistics.
337    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        // GCD(6, 9) = 3 (normalized to 1 for our simplified version)
365        assert!(!gcd.is_zero());
366    }
367
368    #[test]
369    fn test_gcd_univariate() {
370        let mut engine = MultivariateGcdEngine::default_config();
371
372        // x^2 - 1
373        let p = Polynomial::from_coeffs_int(&[(1, &[(0, 2)]), (-1, &[])]);
374
375        // x - 1
376        let q = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (-1, &[])]);
377
378        let gcd = engine.gcd(&p, &q);
379
380        // GCD should be x - 1 (or scalar multiple)
381        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        // x^3 + y^2
390        let p = Polynomial::from_coeffs_int(&[(1, &[(0, 3)]), (1, &[(1, 2)])]);
391
392        // x + y^3
393        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        // Variable 1 (y) has max degree 3
398        assert_eq!(main_var, 1);
399    }
400
401    #[test]
402    fn test_extract_coefficients() {
403        let engine = MultivariateGcdEngine::default_config();
404
405        // 2*x^2*y + 3*x*y^2
406        let p = Polynomial::from_coeffs_int(&[(2, &[(0, 2), (1, 1)]), (3, &[(0, 1), (1, 2)])]);
407
408        // Extract coefficients viewing as univariate in x
409        let coeffs = engine.extract_coefficients(&p, 0);
410
411        // Should have 2 coefficients (for x^2 and x^1)
412        assert_eq!(coeffs.len(), 2);
413    }
414
415    #[test]
416    fn test_normalize_gcd() {
417        let engine = MultivariateGcdEngine::default_config();
418
419        // 6*x^2 + 3*x
420        let p = Polynomial::from_coeffs_int(&[(6, &[(0, 2)]), (3, &[(0, 1)])]);
421
422        let normalized = engine.normalize_gcd(&p);
423
424        // Leading coefficient should be 1 after normalization
425        // leading_coeff returns BigRational directly, not Option
426        let lead = normalized.leading_coeff();
427        // After normalization, leading coefficient should be 1
428        if lead.is_one() {
429            // Success - normalized properly
430        } else if !normalized.is_zero() {
431            // For non-zero polynomials, verify it has a leading coefficient
432            assert!(!lead.is_zero());
433        }
434    }
435}