Skip to main content

symplex/poly/
multipoly.rs

1//! Sparse multivariate polynomials over ℚ.
2//!
3//! `MultiPoly` represents polynomials in multiple variables with
4//! exact rational coefficients. Monomials are stored sparsely
5//! as `(exponent_vector, coefficient)` pairs.
6//!
7//! This module provides:
8//! - Polynomial arithmetic (+, -, ×)
9//! - Degree computation
10//! - Evaluation
11//! - Partial derivatives
12//! - Variable substitution
13//! - Multivariate polynomial division
14//! - S-polynomials for Gröbner basis computation
15//!
16//! The monomial ordering is parameterized via the `MonomialOrd` trait,
17//! enabling graded reverse lex (default), lex, and graded lex orderings.
18
19use num_bigint::BigInt;
20use num_rational::Ratio;
21use num_traits::{One, Zero};
22use std::collections::BTreeMap;
23use std::fmt;
24use std::ops;
25
26// ═══════════════════════════════════════════════════════════════════════════
27// Monomial orderings
28// ═══════════════════════════════════════════════════════════════════════════
29
30/// Trait for monomial orderings. Implemented by zero-sized types.
31pub trait MonomialOrd: 'static + Clone + Send + Sync + std::fmt::Debug {
32    /// Compare two exponent vectors according to this ordering.
33    fn cmp_exponents(a: &[u32], b: &[u32]) -> std::cmp::Ordering;
34}
35
36/// Graded reverse lexicographic ordering (default for Gröbner computation).
37#[derive(Clone, Debug, PartialEq, Eq, Hash)]
38pub struct GrevLex;
39
40/// Pure lexicographic ordering (for elimination/back-substitution).
41#[derive(Clone, Debug, PartialEq, Eq, Hash)]
42pub struct Lex;
43
44/// Graded lexicographic ordering.
45#[derive(Clone, Debug, PartialEq, Eq, Hash)]
46pub struct GrLex;
47
48impl MonomialOrd for GrevLex {
49    fn cmp_exponents(a: &[u32], b: &[u32]) -> std::cmp::Ordering {
50        let deg_a: u32 = a.iter().sum();
51        let deg_b: u32 = b.iter().sum();
52        deg_a.cmp(&deg_b).then_with(|| {
53            // Reverse lex: compare from the LAST variable, REVERSED
54            for (ai, bi) in a.iter().rev().zip(b.iter().rev()) {
55                match bi.cmp(ai) {
56                    // Note: reversed! Higher last component = SMALLER in grevlex
57                    std::cmp::Ordering::Equal => continue,
58                    other => return other,
59                }
60            }
61            std::cmp::Ordering::Equal
62        })
63    }
64}
65
66impl MonomialOrd for Lex {
67    fn cmp_exponents(a: &[u32], b: &[u32]) -> std::cmp::Ordering {
68        // Compare from first variable (highest priority)
69        for (ai, bi) in a.iter().zip(b.iter()) {
70            match ai.cmp(bi) {
71                std::cmp::Ordering::Equal => continue,
72                other => return other,
73            }
74        }
75        std::cmp::Ordering::Equal
76    }
77}
78
79impl MonomialOrd for GrLex {
80    fn cmp_exponents(a: &[u32], b: &[u32]) -> std::cmp::Ordering {
81        let deg_a: u32 = a.iter().sum();
82        let deg_b: u32 = b.iter().sum();
83        deg_a.cmp(&deg_b).then_with(|| Lex::cmp_exponents(a, b))
84    }
85}
86
87/// A monomial order chosen at run time — the value-level counterpart of
88/// the zero-sized order types [`Lex`] and [`GrevLex`], for APIs that take
89/// the order as an argument (`Ex::groebner`, `Ex::reduce_modulo`).
90///
91/// `Lex` orders by the first variable first (elimination / triangular
92/// bases); `GrevLex` orders by total degree, then reverse lexicographically
93/// (the default for Gröbner computation, usually much faster).
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
95#[non_exhaustive]
96pub enum MonomialOrder {
97    /// Pure lexicographic order ([`Lex`]).
98    Lex,
99    /// Graded reverse lexicographic order ([`GrevLex`]).
100    GrevLex,
101}
102
103// ═══════════════════════════════════════════════════════════════════════════
104// MonoKey — exponent vector with ordering
105// ═══════════════════════════════════════════════════════════════════════════
106
107/// A monomial exponent vector with ordering determined by type parameter O.
108#[derive(Clone, Debug)]
109pub struct MonoKey<O: MonomialOrd> {
110    /// The exponent vector.
111    pub exponents: Vec<u32>,
112    _phantom: std::marker::PhantomData<O>,
113}
114
115impl<O: MonomialOrd> MonoKey<O> {
116    /// Create a new `MonoKey` from an exponent vector.
117    pub fn new(exponents: Vec<u32>) -> Self {
118        Self {
119            exponents,
120            _phantom: std::marker::PhantomData,
121        }
122    }
123}
124
125impl<O: MonomialOrd> PartialEq for MonoKey<O> {
126    fn eq(&self, other: &Self) -> bool {
127        self.exponents == other.exponents
128    }
129}
130impl<O: MonomialOrd> Eq for MonoKey<O> {}
131
132impl<O: MonomialOrd> PartialOrd for MonoKey<O> {
133    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
134        Some(self.cmp(other))
135    }
136}
137impl<O: MonomialOrd> Ord for MonoKey<O> {
138    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
139        O::cmp_exponents(&self.exponents, &other.exponents)
140    }
141}
142
143impl<O: MonomialOrd> std::hash::Hash for MonoKey<O> {
144    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
145        self.exponents.hash(state);
146    }
147}
148
149// ═══════════════════════════════════════════════════════════════════════════
150// Exponent type alias (convenience)
151// ═══════════════════════════════════════════════════════════════════════════
152
153/// An exponent vector representing a monomial x₀^a · x₁^b · x₂^c · … as [a, b, c, …].
154/// The length equals the number of variables.
155pub type Exponent = Vec<u32>;
156
157// ═══════════════════════════════════════════════════════════════════════════
158// MultiPoly
159// ═══════════════════════════════════════════════════════════════════════════
160
161/// A sparse multivariate polynomial over ℚ.
162///
163/// Internally stored as a map from exponent vectors to coefficients,
164/// ordered by the monomial ordering `O`.
165/// Zero coefficients are never stored.
166///
167/// # Invariants
168///
169/// - Every key in `terms` has exponent vector of length `num_vars`.
170/// - No value in `terms` is zero.
171/// - The zero polynomial has an empty `terms` map.
172#[derive(Clone, Debug)]
173pub struct MultiPoly<O: MonomialOrd = GrevLex> {
174    /// Number of variables.
175    num_vars: usize,
176    /// Map from exponent vector to coefficient.
177    /// Uses `BTreeMap` with `MonoKey<O>` for ordering-aware storage.
178    terms: BTreeMap<MonoKey<O>, Ratio<BigInt>>,
179}
180
181impl<O: MonomialOrd> PartialEq for MultiPoly<O> {
182    fn eq(&self, other: &Self) -> bool {
183        self.num_vars == other.num_vars && self.terms == other.terms
184    }
185}
186impl<O: MonomialOrd> Eq for MultiPoly<O> {}
187
188// ═══════════════════════════════════════════════════════════════════════════
189// Helper: rational from i64
190// ═══════════════════════════════════════════════════════════════════════════
191
192fn rat(n: i64) -> Ratio<BigInt> {
193    Ratio::from_integer(BigInt::from(n))
194}
195
196// ═══════════════════════════════════════════════════════════════════════════
197// Monomial helper functions
198// ═══════════════════════════════════════════════════════════════════════════
199
200/// Component-wise maximum of two exponent vectors (LCM of monomials).
201pub fn monomial_lcm(a: &[u32], b: &[u32]) -> Vec<u32> {
202    a.iter()
203        .zip(b.iter())
204        .map(|(&ai, &bi)| ai.max(bi))
205        .collect()
206}
207
208/// Check if monomial a divides monomial b (component-wise ≤).
209pub fn monomial_divides(a: &[u32], b: &[u32]) -> bool {
210    a.iter().zip(b.iter()).all(|(&ai, &bi)| ai <= bi)
211}
212
213/// Divide monomial b by a (component-wise subtraction). Returns None if a doesn't divide b.
214pub fn monomial_div(a: &[u32], b: &[u32]) -> Option<Vec<u32>> {
215    if !monomial_divides(a, b) {
216        return None;
217    }
218    Some(a.iter().zip(b.iter()).map(|(&ai, &bi)| bi - ai).collect())
219}
220
221/// Multiply two monomials (component-wise addition).
222pub fn monomial_mul(a: &[u32], b: &[u32]) -> Vec<u32> {
223    a.iter().zip(b.iter()).map(|(&ai, &bi)| ai + bi).collect()
224}
225
226/// Check if two monomials are coprime (no shared variable).
227pub fn monomial_coprime(a: &[u32], b: &[u32]) -> bool {
228    a.iter().zip(b.iter()).all(|(&ai, &bi)| ai == 0 || bi == 0)
229}
230
231// ═══════════════════════════════════════════════════════════════════════════
232// Construction
233// ═══════════════════════════════════════════════════════════════════════════
234
235impl<O: MonomialOrd> MultiPoly<O> {
236    /// Create the zero polynomial in `num_vars` variables.
237    pub fn zero(num_vars: usize) -> Self {
238        MultiPoly {
239            num_vars,
240            terms: BTreeMap::new(),
241        }
242    }
243
244    /// Create a constant polynomial.
245    pub fn constant(num_vars: usize, c: Ratio<BigInt>) -> Self {
246        let mut p = Self::zero(num_vars);
247        if !c.is_zero() {
248            p.terms.insert(MonoKey::new(vec![0; num_vars]), c);
249        }
250        p
251    }
252
253    /// Create a polynomial from an integer constant.
254    pub fn from_int(num_vars: usize, n: i64) -> Self {
255        Self::constant(num_vars, rat(n))
256    }
257
258    /// Create a single-variable polynomial: the `var_index`-th variable
259    /// (0-indexed).
260    ///
261    /// # Panics
262    ///
263    /// Panics if `var_index >= num_vars`.
264    pub fn var(num_vars: usize, var_index: usize) -> Self {
265        assert!(
266            var_index < num_vars,
267            "var_index {var_index} out of range for {num_vars} variables"
268        );
269        let mut exp = vec![0u32; num_vars];
270        exp[var_index] = 1;
271        let mut terms = BTreeMap::new();
272        terms.insert(MonoKey::new(exp), Ratio::one());
273        MultiPoly { num_vars, terms }
274    }
275
276    /// Create a monomial: `c · x₀^e₀ · x₁^e₁ · …`
277    ///
278    /// The number of variables is inferred from the length of `exponents`.
279    pub fn monomial(c: Ratio<BigInt>, exponents: Exponent) -> Self {
280        let num_vars = exponents.len();
281        let mut p = Self::zero(num_vars);
282        if !c.is_zero() {
283            p.terms.insert(MonoKey::new(exponents), c);
284        }
285        p
286    }
287
288    // ─── internal helper: insert a term, combining with any existing ───
289
290    fn insert_term(&mut self, exp: Vec<u32>, coeff: Ratio<BigInt>) {
291        if coeff.is_zero() {
292            return;
293        }
294        let key = MonoKey::new(exp);
295        let entry = self
296            .terms
297            .entry(key)
298            .or_insert_with(|| Ratio::from_integer(BigInt::from(0)));
299        *entry += coeff;
300    }
301
302    /// Look up the coefficient of a given exponent vector.
303    ///
304    /// Returns `None` when the monomial is absent (its coefficient is zero)
305    /// or when `exp` has the wrong length.
306    ///
307    /// # Examples
308    ///
309    /// ```
310    /// use symplex::multipoly::MultiPoly;
311    /// use num_bigint::BigInt;
312    /// use num_rational::Ratio;
313    ///
314    /// let [x, y]: [MultiPoly; 2] = [MultiPoly::var(2, 0), MultiPoly::var(2, 1)];
315    /// let f = x.mul(&y).scale(&Ratio::from_integer(BigInt::from(3))).add(&x);
316    /// assert_eq!(f.coeff(&[1, 1]), Some(&Ratio::from_integer(BigInt::from(3))));
317    /// assert_eq!(f.coeff(&[0, 1]), None);
318    /// ```
319    pub fn coeff(&self, exp: &[u32]) -> Option<&Ratio<BigInt>> {
320        if exp.len() != self.num_vars {
321            return None;
322        }
323        self.terms.get(&MonoKey::<O>::new(exp.to_vec()))
324    }
325
326    /// Build a polynomial from `(exponent_vector, coefficient)` pairs.
327    ///
328    /// Repeated monomials are summed and zero coefficients are dropped.
329    /// Returns `None` if any exponent vector does not have length
330    /// `num_vars`.
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// use symplex::multipoly::MultiPoly;
336    /// use num_bigint::BigInt;
337    /// use num_rational::Ratio;
338    ///
339    /// let r = |n: i64| Ratio::from_integer(BigInt::from(n));
340    /// let f: MultiPoly = MultiPoly::from_terms(2, vec![(vec![1, 0], r(2)), (vec![1, 0], r(-2)), (vec![0, 1], r(5))]).unwrap();
341    /// assert_eq!(f.num_terms(), 1);
342    /// assert_eq!(f.coeff(&[0, 1]), Some(&r(5)));
343    /// assert!(MultiPoly::<symplex::multipoly::GrevLex>::from_terms(2, vec![(vec![1], r(1))]).is_none());
344    /// ```
345    pub fn from_terms(num_vars: usize, terms: Vec<(Vec<u32>, Ratio<BigInt>)>) -> Option<Self> {
346        let mut p = Self::zero(num_vars);
347        for (exp, c) in terms {
348            if exp.len() != num_vars {
349                return None;
350            }
351            p.insert_term(exp, c);
352        }
353        p.prune();
354        Some(p)
355    }
356
357    /// Apply `f` to every coefficient, dropping terms that become zero.
358    ///
359    /// # Examples
360    ///
361    /// ```
362    /// use symplex::multipoly::MultiPoly;
363    /// use num_bigint::BigInt;
364    /// use num_rational::Ratio;
365    ///
366    /// let x: MultiPoly = MultiPoly::var(1, 0);
367    /// let f = x.scale(&Ratio::from_integer(BigInt::from(6))) + 4;
368    /// let halved = f.map_coeffs(|c| c / Ratio::from_integer(BigInt::from(2)));
369    /// assert_eq!(halved, x.scale(&Ratio::from_integer(BigInt::from(3))) + 2);
370    /// ```
371    pub fn map_coeffs(&self, mut f: impl FnMut(&Ratio<BigInt>) -> Ratio<BigInt>) -> Self {
372        let mut terms = BTreeMap::new();
373        for (k, c) in &self.terms {
374            let nc = f(c);
375            if !nc.is_zero() {
376                terms.insert(k.clone(), nc);
377            }
378        }
379        MultiPoly {
380            num_vars: self.num_vars,
381            terms,
382        }
383    }
384
385    /// Remove any terms whose coefficient has become zero.
386    fn prune(&mut self) {
387        self.terms.retain(|_, c| !c.is_zero());
388    }
389
390    /// Assert that both polynomials live in the same variable ring.
391    fn assert_compatible(&self, other: &MultiPoly<O>) {
392        assert_eq!(
393            self.num_vars, other.num_vars,
394            "MultiPoly: incompatible variable counts ({} vs {})",
395            self.num_vars, other.num_vars
396        );
397    }
398}
399
400// ═══════════════════════════════════════════════════════════════════════════
401// Queries
402// ═══════════════════════════════════════════════════════════════════════════
403
404impl<O: MonomialOrd> MultiPoly<O> {
405    /// Check if this is the zero polynomial.
406    pub fn is_zero(&self) -> bool {
407        self.terms.is_empty()
408    }
409
410    /// Number of variables.
411    pub fn num_vars(&self) -> usize {
412        self.num_vars
413    }
414
415    /// Number of non-zero terms.
416    pub fn num_terms(&self) -> usize {
417        self.terms.len()
418    }
419
420    /// Total degree: maximum sum of exponents over all terms.
421    ///
422    /// Returns `None` for the zero polynomial (which has no defined degree).
423    pub fn total_degree(&self) -> Option<u32> {
424        self.terms.keys().map(|k| k.exponents.iter().sum()).max()
425    }
426
427    /// Degree in a specific variable.
428    ///
429    /// Returns 0 for the zero polynomial.
430    ///
431    /// # Panics
432    ///
433    /// Panics if `var_index >= num_vars`.
434    pub fn degree_in(&self, var_index: usize) -> u32 {
435        assert!(
436            var_index < self.num_vars,
437            "var_index {var_index} out of range for {} variables",
438            self.num_vars
439        );
440        self.terms
441            .keys()
442            .map(|k| k.exponents[var_index])
443            .max()
444            .unwrap_or(0)
445    }
446
447    /// Leading term (highest monomial under the ordering O).
448    /// O(log n) via BTreeMap::last().
449    pub fn leading_term(&self) -> Option<(&[u32], &Ratio<BigInt>)> {
450        self.terms
451            .last_key_value()
452            .map(|(k, v)| (k.exponents.as_slice(), v))
453    }
454
455    /// Leading monomial exponent vector.
456    pub fn leading_monomial(&self) -> Option<&[u32]> {
457        self.terms
458            .last_key_value()
459            .map(|(k, _)| k.exponents.as_slice())
460    }
461
462    /// Leading coefficient.
463    pub fn leading_coeff(&self) -> Option<&Ratio<BigInt>> {
464        self.terms.last_key_value().map(|(_, v)| v)
465    }
466
467    /// Iterate over all terms as `(exponent_slice, coefficient)` pairs.
468    pub fn terms(&self) -> impl Iterator<Item = (&[u32], &Ratio<BigInt>)> {
469        self.terms.iter().map(|(k, v)| (k.exponents.as_slice(), v))
470    }
471
472    /// The value of a constant polynomial (`Some(0)` for the zero
473    /// polynomial), or `None` when some variable occurs.
474    ///
475    /// ```
476    /// use symplex::multipoly::MultiPoly;
477    /// use symplex::linprog::qi;
478    ///
479    /// let x: MultiPoly = MultiPoly::var(2, 0);
480    /// assert_eq!(MultiPoly::<symplex::multipoly::GrevLex>::zero(2).as_constant(), Some(qi(0)));
481    /// assert_eq!(MultiPoly::<symplex::multipoly::GrevLex>::from_int(2, 7).as_constant(), Some(qi(7)));
482    /// assert_eq!(x.as_constant(), None);
483    /// ```
484    pub fn as_constant(&self) -> Option<Ratio<BigInt>> {
485        match self.terms.len() {
486            0 => Some(Ratio::from_integer(BigInt::from(0))),
487            1 => self
488                .terms
489                .iter()
490                .next()
491                .filter(|(k, _)| k.exponents.iter().all(|&e| e == 0))
492                .map(|(_, c)| c.clone()),
493            _ => None,
494        }
495    }
496
497    /// The polynomial as an affine form `a₀x₀ + … + a_{n−1}x_{n−1} + c`,
498    /// or `None` if its total degree exceeds one.  The zero polynomial is
499    /// `(0, …, 0; 0)`.
500    ///
501    /// This is the bridge from a polynomial that is affine in its variables
502    /// (a half-space `p ≥ 0`, say) to explicit coefficients.
503    ///
504    /// ```
505    /// use symplex::multipoly::MultiPoly;
506    /// use symplex::linprog::qi;
507    ///
508    /// let [x, y]: [MultiPoly; 2] = [MultiPoly::var(2, 0), MultiPoly::var(2, 1)];
509    /// let p = x.scale(&qi(3)).sub(&y).add(&MultiPoly::from_int(2, 5));
510    /// assert_eq!(p.affine_form(), Some((vec![qi(3), qi(-1)], qi(5))));
511    /// assert_eq!(x.mul(&y).affine_form(), None);
512    /// ```
513    pub fn affine_form(&self) -> Option<(Vec<Ratio<BigInt>>, Ratio<BigInt>)> {
514        let zero = Ratio::from_integer(BigInt::from(0));
515        let mut coeffs = vec![zero.clone(); self.num_vars];
516        let mut constant = zero;
517        for (key, c) in &self.terms {
518            let degree: u32 = key.exponents.iter().sum();
519            match degree {
520                0 => constant = c.clone(),
521                1 => {
522                    let i = key.exponents.iter().position(|&e| e == 1)?;
523                    coeffs[i] = c.clone();
524                }
525                _ => return None,
526            }
527        }
528        Some((coeffs, constant))
529    }
530
531    /// Convert this polynomial to a different monomial ordering.
532    pub fn convert_order<B: MonomialOrd>(&self) -> MultiPoly<B> {
533        let mut new_terms = BTreeMap::new();
534        for (key, coeff) in &self.terms {
535            new_terms.insert(MonoKey::<B>::new(key.exponents.clone()), coeff.clone());
536        }
537        MultiPoly {
538            num_vars: self.num_vars,
539            terms: new_terms,
540        }
541    }
542}
543
544// ═══════════════════════════════════════════════════════════════════════════
545// Evaluation
546// ═══════════════════════════════════════════════════════════════════════════
547
548impl<O: MonomialOrd> MultiPoly<O> {
549    /// Evaluate the polynomial at a point: substitute each variable with
550    /// a rational value.
551    ///
552    /// # Panics
553    ///
554    /// Panics if `values.len() != self.num_vars`.
555    pub fn eval(&self, values: &[Ratio<BigInt>]) -> Ratio<BigInt> {
556        assert_eq!(
557            values.len(),
558            self.num_vars,
559            "eval: expected {} values, got {}",
560            self.num_vars,
561            values.len()
562        );
563        let mut result = Ratio::from_integer(BigInt::from(0));
564        for (key, coeff) in &self.terms {
565            let mut term_val = coeff.clone();
566            for (i, &e) in key.exponents.iter().enumerate() {
567                if e > 0 {
568                    term_val *= pow_ratio(&values[i], e);
569                }
570            }
571            result += term_val;
572        }
573        result
574    }
575}
576
577/// Raise a rational to a non-negative integer power.
578fn pow_ratio(base: &Ratio<BigInt>, exp: u32) -> Ratio<BigInt> {
579    let mut result = Ratio::one();
580    for _ in 0..exp {
581        result *= base;
582    }
583    result
584}
585
586// ═══════════════════════════════════════════════════════════════════════════
587// Calculus
588// ═══════════════════════════════════════════════════════════════════════════
589
590impl<O: MonomialOrd> MultiPoly<O> {
591    /// Partial derivative with respect to variable `var_index`.
592    ///
593    /// # Panics
594    ///
595    /// Panics if `var_index >= num_vars`.
596    pub fn partial_derivative(&self, var_index: usize) -> MultiPoly<O> {
597        assert!(
598            var_index < self.num_vars,
599            "partial_derivative: var_index {var_index} out of range for {} variables",
600            self.num_vars
601        );
602        let mut result = Self::zero(self.num_vars);
603        for (key, coeff) in &self.terms {
604            let e_i = key.exponents[var_index];
605            if e_i == 0 {
606                continue; // derivative kills this term
607            }
608            let new_coeff = coeff * Ratio::from_integer(BigInt::from(e_i));
609            let mut new_exp = key.exponents.clone();
610            new_exp[var_index] -= 1;
611            result.terms.insert(MonoKey::new(new_exp), new_coeff);
612        }
613        result
614    }
615
616    /// Substitute a value for one variable, **keeping** the number of
617    /// variables (the variable simply no longer occurs), so that the
618    /// remaining variables keep their indices.  This is the operation for
619    /// instantiating a parameter: `p(j, x, y)` at `j = 3` is `p(3, x, y)`
620    /// as a polynomial in the same three slots.  Use
621    /// [`substitute`](Self::substitute) to also drop the variable.
622    ///
623    /// ```
624    /// use symplex::multipoly::MultiPoly;
625    /// use symplex::linprog::qi;
626    ///
627    /// let [j, x]: [MultiPoly; 2] = [MultiPoly::var(2, 0), MultiPoly::var(2, 1)];
628    /// let p = j.mul(&x).add(&j.mul(&j));           // j·x + j²
629    /// let at3 = p.eval_var(0, &qi(3));
630    /// assert_eq!(at3.num_vars(), 2);
631    /// assert_eq!(at3.affine_form(), Some((vec![qi(0), qi(3)], qi(9))));
632    /// ```
633    ///
634    /// # Panics
635    ///
636    /// Panics if `var_index >= num_vars`.
637    pub fn eval_var(&self, var_index: usize, value: &Ratio<BigInt>) -> MultiPoly<O> {
638        assert!(
639            var_index < self.num_vars,
640            "eval_var: var_index {var_index} out of range for {} variables",
641            self.num_vars
642        );
643        let mut result = Self::zero(self.num_vars);
644        for (key, coeff) in &self.terms {
645            let e_i = key.exponents[var_index];
646            let new_coeff = coeff * pow_ratio(value, e_i);
647            if new_coeff.is_zero() {
648                continue;
649            }
650            let mut new_exp = key.exponents.clone();
651            new_exp[var_index] = 0;
652            result.insert_term(new_exp, new_coeff);
653        }
654        result.prune();
655        result
656    }
657
658    /// Substitute a value for one variable, reducing the number of
659    /// variables by one.
660    ///
661    /// The resulting polynomial lives in a ring with `num_vars - 1`
662    /// variables. Variable indices above `var_index` are shifted down
663    /// by one.  See [`eval_var`](Self::eval_var) to keep the indices.
664    ///
665    /// # Panics
666    ///
667    /// Panics if `var_index >= num_vars` or `num_vars == 0`.
668    pub fn substitute(&self, var_index: usize, value: &Ratio<BigInt>) -> MultiPoly<O> {
669        assert!(
670            var_index < self.num_vars,
671            "substitute: var_index {var_index} out of range for {} variables",
672            self.num_vars
673        );
674        assert!(
675            self.num_vars > 0,
676            "substitute: cannot reduce below 0 variables"
677        );
678        let new_num_vars = self.num_vars - 1;
679        let mut result = Self::zero(new_num_vars);
680        for (key, coeff) in &self.terms {
681            let e_i = key.exponents[var_index];
682            let val_pow = pow_ratio(value, e_i);
683            let new_coeff = coeff * val_pow;
684            if new_coeff.is_zero() {
685                continue;
686            }
687            // Build new exponent vector without var_index
688            let mut new_exp = Vec::with_capacity(new_num_vars);
689            for (j, &ej) in key.exponents.iter().enumerate() {
690                if j != var_index {
691                    new_exp.push(ej);
692                }
693            }
694            result.insert_term(new_exp, new_coeff);
695        }
696        result.prune();
697        result
698    }
699}
700
701// ═══════════════════════════════════════════════════════════════════════════
702// Arithmetic
703// ═══════════════════════════════════════════════════════════════════════════
704
705impl<O: MonomialOrd> MultiPoly<O> {
706    /// Add two polynomials.
707    ///
708    /// # Panics
709    ///
710    /// Panics if the polynomials have different numbers of variables.
711    pub fn add(&self, other: &MultiPoly<O>) -> MultiPoly<O> {
712        self.assert_compatible(other);
713        let mut result = self.clone();
714        for (key, coeff) in &other.terms {
715            result.insert_term(key.exponents.clone(), coeff.clone());
716        }
717        result.prune();
718        result
719    }
720
721    /// Subtract two polynomials.
722    ///
723    /// # Panics
724    ///
725    /// Panics if the polynomials have different numbers of variables.
726    pub fn sub(&self, other: &MultiPoly<O>) -> MultiPoly<O> {
727        self.assert_compatible(other);
728        let mut result = self.clone();
729        for (key, coeff) in &other.terms {
730            result.insert_term(key.exponents.clone(), -coeff.clone());
731        }
732        result.prune();
733        result
734    }
735
736    /// Negate the polynomial.
737    pub fn neg(&self) -> MultiPoly<O> {
738        let terms = self
739            .terms
740            .iter()
741            .map(|(k, c)| (k.clone(), -c.clone()))
742            .collect();
743        MultiPoly {
744            num_vars: self.num_vars,
745            terms,
746        }
747    }
748
749    /// Multiply two polynomials.
750    ///
751    /// # Panics
752    ///
753    /// Panics if the polynomials have different numbers of variables.
754    pub fn mul(&self, other: &MultiPoly<O>) -> MultiPoly<O> {
755        self.assert_compatible(other);
756        let mut result = Self::zero(self.num_vars);
757        for (key_a, coeff_a) in &self.terms {
758            for (key_b, coeff_b) in &other.terms {
759                let new_coeff = coeff_a * coeff_b;
760                let new_exp: Vec<u32> = key_a
761                    .exponents
762                    .iter()
763                    .zip(key_b.exponents.iter())
764                    .map(|(&a, &b)| a + b)
765                    .collect();
766                result.insert_term(new_exp, new_coeff);
767            }
768        }
769        result.prune();
770        result
771    }
772
773    /// Scale by a rational constant.
774    pub fn scale(&self, c: &Ratio<BigInt>) -> MultiPoly<O> {
775        if c.is_zero() {
776            return Self::zero(self.num_vars);
777        }
778        let terms = self
779            .terms
780            .iter()
781            .map(|(k, coeff)| (k.clone(), coeff * c))
782            .collect();
783        MultiPoly {
784            num_vars: self.num_vars,
785            terms,
786        }
787    }
788
789    /// Multiply by a single monomial: coeff * x^exp
790    pub fn mul_monomial(&self, coeff: &Ratio<BigInt>, exp: &[u32]) -> Self {
791        if coeff.is_zero() {
792            return Self::zero(self.num_vars);
793        }
794        let mut result = BTreeMap::new();
795        for (key, c) in &self.terms {
796            let new_exp = monomial_mul(&key.exponents, exp);
797            let new_coeff = c * coeff;
798            if !new_coeff.is_zero() {
799                result.insert(MonoKey::new(new_exp), new_coeff);
800            }
801        }
802        MultiPoly {
803            num_vars: self.num_vars,
804            terms: result,
805        }
806    }
807
808    // Removed: div_rem_univariate was a todo!() stub. Use reduce() for multivariate division.
809}
810
811// ═══════════════════════════════════════════════════════════════════════════
812// Monic and primitive part
813// ═══════════════════════════════════════════════════════════════════════════
814
815impl<O: MonomialOrd> MultiPoly<O> {
816    /// Make monic: divide all coefficients by the leading coefficient.
817    pub fn monic(&self) -> Self {
818        let Some(lc) = self.leading_coeff() else {
819            return self.clone();
820        };
821        self.scale(&(Ratio::one() / lc.clone()))
822    }
823
824    /// Primitive part over ℚ: clear denominators, then divide by GCD of integer coefficients.
825    pub fn primitive_part_q(&self) -> Self {
826        if self.is_zero() {
827            return self.clone();
828        }
829        // Find LCM of all denominators
830        let mut denom_lcm = BigInt::one();
831        for (_, coeff) in self.terms() {
832            denom_lcm = num_integer::lcm(denom_lcm, coeff.denom().clone());
833        }
834        // Multiply through to clear denominators
835        let scale_factor = Ratio::from_integer(denom_lcm);
836        let integer_poly = self.scale(&scale_factor);
837        // Find GCD of all numerators
838        let mut content = BigInt::zero();
839        for (_, coeff) in integer_poly.terms() {
840            content = num_integer::gcd(content, coeff.numer().clone());
841        }
842        if content.is_zero() || content.is_one() {
843            return integer_poly;
844        }
845        integer_poly.scale(&Ratio::new(BigInt::one(), content))
846    }
847}
848
849// ═══════════════════════════════════════════════════════════════════════════
850// Integer content, denominators, heuristic GCD
851// ═══════════════════════════════════════════════════════════════════════════
852
853/// Maximum number of evaluation points tried by the heuristic GCD before
854/// giving up.
855const HEUGCD_MAX_TRIES: usize = 6;
856
857/// Symmetric remainder of `c` modulo `m`: the representative of `c mod m`
858/// in `(-m/2, m/2]`.
859fn symmetric_mod(c: &BigInt, m: &BigInt) -> BigInt {
860    use num_integer::Integer;
861    let r = c.mod_floor(m);
862    if &r + &r > *m { r - m } else { r }
863}
864
865impl<O: MonomialOrd> MultiPoly<O> {
866    /// GCD of the numerators of all coefficients (non-negative).
867    ///
868    /// For a polynomial with integer coefficients this is the usual
869    /// integer content; denominators are ignored, so call
870    /// [`clear_denominators`](Self::clear_denominators) first for a
871    /// general rational polynomial.  The zero polynomial has content `0`.
872    ///
873    /// # Examples
874    ///
875    /// ```
876    /// use symplex::multipoly::MultiPoly;
877    /// use num_bigint::BigInt;
878    ///
879    /// let x: MultiPoly = MultiPoly::var(1, 0);
880    /// let f = x.clone() * 6 + 9;
881    /// assert_eq!(f.integer_content(), BigInt::from(3));
882    /// assert_eq!(MultiPoly::<symplex::multipoly::GrevLex>::zero(1).integer_content(), BigInt::from(0));
883    /// ```
884    pub fn integer_content(&self) -> BigInt {
885        let mut g = BigInt::zero();
886        for (_, c) in self.terms() {
887            g = num_integer::gcd(g, c.numer().clone());
888            if g.is_one() {
889                break;
890            }
891        }
892        g
893    }
894
895    /// Multiply through by the least common multiple `d` of all coefficient
896    /// denominators, returning `(d, d · self)`; the second component has
897    /// integer coefficients.
898    ///
899    /// # Examples
900    ///
901    /// ```
902    /// use symplex::multipoly::MultiPoly;
903    /// use num_bigint::BigInt;
904    /// use num_rational::Ratio;
905    ///
906    /// let x: MultiPoly = MultiPoly::var(1, 0);
907    /// let f = x.scale(&Ratio::new(BigInt::from(1), BigInt::from(2))) + 1;   // x/2 + 1
908    /// let (d, g) = f.clear_denominators();
909    /// assert_eq!(d, BigInt::from(2));
910    /// assert_eq!(g, x + 2);
911    /// ```
912    pub fn clear_denominators(&self) -> (BigInt, Self) {
913        let mut d = BigInt::one();
914        for (_, c) in self.terms() {
915            d = num_integer::lcm(d, c.denom().clone());
916        }
917        if d.is_one() {
918            return (d, self.clone());
919        }
920        let scaled = self.scale(&Ratio::from_integer(d.clone()));
921        (d, scaled)
922    }
923
924    /// Largest absolute value of a coefficient numerator (the max-norm for
925    /// integer polynomials).  Zero for the zero polynomial.
926    fn max_norm(&self) -> BigInt {
927        let mut m = BigInt::zero();
928        for (_, c) in self.terms() {
929            let a = num_traits::Signed::abs(c.numer());
930            if a > m {
931                m = a;
932            }
933        }
934        m
935    }
936
937    /// `true` if the leading coefficient (under `O`) is negative.
938    fn leading_is_negative(&self) -> bool {
939        self.leading_coeff()
940            .is_some_and(num_traits::Signed::is_negative)
941    }
942
943    /// Integer-normalised form used by [`gcd`](Self::gcd): denominators
944    /// cleared and leading coefficient made positive.
945    fn normalized_over_z(&self) -> Self {
946        let (_, z) = self.clear_denominators();
947        if z.leading_is_negative() { z.neg() } else { z }
948    }
949
950    /// Greatest common divisor in ℤ[x₁, …, xₙ] of the inputs after clearing
951    /// denominators, computed with the heuristic GCD algorithm (GCDHEU).
952    ///
953    /// The result has integer coefficients, positive leading coefficient
954    /// (under `O`), and integer content equal to the GCD of the inputs'
955    /// integer contents.  For inputs with integer coefficients this is the
956    /// ordinary GCD over ℤ; over ℚ the GCD is only defined up to a nonzero
957    /// rational factor, and this normalisation picks one representative.
958    /// `gcd(0, 0) = 0`, `gcd(f, 0)` is the normalised `f`.
959    ///
960    /// The heuristic evaluates one variable at a large integer, recurses on
961    /// the remaining variables (integer GCD in the univariate case), and
962    /// reconstructs the candidate by symmetric ξ-adic expansion; the
963    /// candidate is verified by exact division of both inputs, so a wrong
964    /// answer is never returned.  If every evaluation point fails (which
965    /// does not happen for inputs of realistic size), the constant `1` is
966    /// returned, meaning "no common factor found".
967    ///
968    /// # Examples
969    ///
970    /// ```
971    /// use symplex::multipoly::MultiPoly;
972    ///
973    /// let [x, y]: [MultiPoly; 2] = [MultiPoly::var(2, 0), MultiPoly::var(2, 1)];
974    /// let s = x.add(&y);                 // x + y
975    /// let d = x.sub(&y);                 // x − y
976    /// let f = s.mul(&d);                 // x² − y²
977    /// let g = s.mul(&s);                 // (x + y)²
978    /// assert_eq!(MultiPoly::gcd(&f, &g), s);
979    /// assert_eq!(MultiPoly::gcd(&x, &y), MultiPoly::from_int(2, 1));
980    /// ```
981    pub fn gcd(a: &Self, b: &Self) -> Self {
982        if a.num_vars != b.num_vars {
983            return Self::from_int(a.num_vars, 1);
984        }
985        match (a.is_zero(), b.is_zero()) {
986            (true, true) => return Self::zero(a.num_vars),
987            (true, false) => return b.normalized_over_z(),
988            (false, true) => return a.normalized_over_z(),
989            (false, false) => {}
990        }
991        let (_, az) = a.clear_denominators();
992        let (_, bz) = b.clear_denominators();
993        match Self::heugcd_z(&az, &bz, 0) {
994            Some(h) => {
995                if h.leading_is_negative() {
996                    h.neg()
997                } else {
998                    h
999                }
1000            }
1001            None => Self::from_int(a.num_vars, 1),
1002        }
1003    }
1004
1005    /// Least common multiple `a · b / gcd(a, b)` (integer-normalised like
1006    /// [`gcd`](Self::gcd)); zero if either input is zero.
1007    ///
1008    /// # Examples
1009    ///
1010    /// ```
1011    /// use symplex::multipoly::MultiPoly;
1012    ///
1013    /// let [x, y]: [MultiPoly; 2] = [MultiPoly::var(2, 0), MultiPoly::var(2, 1)];
1014    /// let f = x.mul(&y);                 // xy
1015    /// let g = y.mul(&y);                 // y²
1016    /// assert_eq!(MultiPoly::lcm(&f, &g), x.mul(&y).mul(&y));
1017    /// ```
1018    pub fn lcm(a: &Self, b: &Self) -> Self {
1019        if a.is_zero() || b.is_zero() {
1020            return Self::zero(a.num_vars);
1021        }
1022        let g = Self::gcd(a, b);
1023        let az = a.normalized_over_z();
1024        let bz = b.normalized_over_z();
1025        let prod = az.mul(&bz);
1026        match prod.div_exact(&g) {
1027            Some(l) => l,
1028            None => prod,
1029        }
1030    }
1031
1032    /// Heuristic GCD over ℤ for nonzero integer-coefficient inputs with the
1033    /// same number of variables.  Returns the full GCD (including integer
1034    /// content), or `None` if every evaluation point failed.
1035    fn heugcd_z(f: &Self, g: &Self, depth: usize) -> Option<Self> {
1036        let nv = f.num_vars;
1037        // Integer content.
1038        let cf = f.integer_content();
1039        let cg = g.integer_content();
1040        if cf.is_zero() || cg.is_zero() {
1041            return None;
1042        }
1043        let c = num_integer::gcd(cf.clone(), cg.clone());
1044        let inv_cf = Ratio::new(BigInt::one(), cf);
1045        let inv_cg = Ratio::new(BigInt::one(), cg);
1046        let f = f.scale(&inv_cf);
1047        let g = g.scale(&inv_cg);
1048        let c_rat = Ratio::from_integer(c);
1049
1050        if nv == 0 {
1051            // Both are ±1 after content removal.
1052            return Some(Self::constant(0, c_rat));
1053        }
1054        // A primitive constant is ±1: the GCD is the content GCD.
1055        if f.total_degree() == Some(0) || g.total_degree() == Some(0) {
1056            return Some(Self::constant(nv, c_rat));
1057        }
1058        if f == g || f == g.neg() {
1059            return Some(f.scale(&c_rat));
1060        }
1061        // Cheap exact-division shortcuts.
1062        if g.div_exact(&f).is_some() {
1063            return Some(f.scale(&c_rat));
1064        }
1065        if f.div_exact(&g).is_some() {
1066            return Some(g.scale(&c_rat));
1067        }
1068        // Guard against pathological recursion depth (one level per variable).
1069        if depth > nv + 1 {
1070            return None;
1071        }
1072
1073        let var = nv - 1;
1074        let f_norm = f.max_norm();
1075        let g_norm = g.max_norm();
1076        let two = BigInt::from(2);
1077        let mut xi: BigInt = &two * f_norm.min(g_norm) + BigInt::from(29);
1078
1079        for _ in 0..HEUGCD_MAX_TRIES {
1080            if let Some(h) = Self::heugcd_attempt(&f, &g, var, &xi, depth) {
1081                return Some(h.scale(&c_rat));
1082            }
1083            xi = Self::next_xi(&xi);
1084        }
1085        None
1086    }
1087
1088    /// Next evaluation point: `73794 · ξ · ξ^(1/4) / 27011` (grows like
1089    /// `ξ^1.25`, the schedule used by the classical implementations).
1090    fn next_xi(xi: &BigInt) -> BigInt {
1091        let root4 = xi.sqrt().sqrt().max(BigInt::from(2));
1092        (BigInt::from(73794) * xi * root4) / BigInt::from(27011)
1093    }
1094
1095    /// One evaluation/interpolation round of the heuristic GCD for primitive
1096    /// inputs `f`, `g` in variable `var` at the point `xi`.  Returns the
1097    /// verified GCD candidate or `None`.
1098    fn heugcd_attempt(f: &Self, g: &Self, var: usize, xi: &BigInt, depth: usize) -> Option<Self> {
1099        let xi_rat = Ratio::from_integer(xi.clone());
1100        let ff = f.substitute(var, &xi_rat);
1101        let gg = g.substitute(var, &xi_rat);
1102        if ff.is_zero() || gg.is_zero() {
1103            return None;
1104        }
1105        let h = Self::heugcd_z(&ff, &gg, depth + 1)?;
1106        let h = Self::interpolate_xi(&h, xi, var);
1107        if h.is_zero() {
1108            return None;
1109        }
1110        // Make primitive (the content of the true GCD is 1 here).
1111        let content = h.integer_content();
1112        if content.is_zero() {
1113            return None;
1114        }
1115        let h = h.scale(&Ratio::new(BigInt::one(), content));
1116        if f.div_exact(&h).is_some() && g.div_exact(&h).is_some() {
1117            Some(h)
1118        } else {
1119            None
1120        }
1121    }
1122
1123    /// Reconstruct a polynomial in variable `var` from its value `h` at
1124    /// `xi` by symmetric ξ-adic expansion: `h = Σᵢ gᵢ · ξⁱ` with the
1125    /// coefficients of each `gᵢ` in `(-ξ/2, ξ/2]`.
1126    fn interpolate_xi(h: &Self, xi: &BigInt, var: usize) -> Self {
1127        let nv = h.num_vars + 1;
1128        let mut result = Self::zero(nv);
1129        let mut rest = h.clone();
1130        let mut i: u32 = 0;
1131        let xi_rat = Ratio::from_integer(xi.clone());
1132        while !rest.is_zero() {
1133            // With exact integer arithmetic every digit step divides the
1134            // remaining magnitude by ξ, so this terminates; the cap only
1135            // guards against a non-integer `h` slipping through.
1136            if i > 4096 || rest.terms().any(|(_, c)| !c.is_integer()) {
1137                return Self::zero(nv);
1138            }
1139            let digit = rest.map_coeffs(|c| Ratio::from_integer(symmetric_mod(c.numer(), xi)));
1140            for (exp, c) in digit.terms() {
1141                let mut e = Vec::with_capacity(nv);
1142                e.extend_from_slice(&exp[..var]);
1143                e.push(i);
1144                e.extend_from_slice(&exp[var..]);
1145                result.insert_term(e, c.clone());
1146            }
1147            rest = rest.sub(&digit).map_coeffs(|c| c / &xi_rat);
1148            i += 1;
1149        }
1150        result.prune();
1151        result
1152    }
1153}
1154
1155// ═══════════════════════════════════════════════════════════════════════════
1156// Multivariate division
1157// ═══════════════════════════════════════════════════════════════════════════
1158
1159impl<O: MonomialOrd> MultiPoly<O> {
1160    /// Reduce this polynomial modulo a set of divisors.
1161    /// Returns the remainder after multivariate division.
1162    pub fn reduce(&self, divisors: &[&MultiPoly<O>]) -> MultiPoly<O> {
1163        if self.is_zero() || divisors.is_empty() {
1164            return self.clone();
1165        }
1166
1167        let mut remainder = MultiPoly::zero(self.num_vars);
1168        let mut p = self.clone();
1169
1170        while let Some((lt_exp, lt_coeff)) = p.leading_term() {
1171            let mut divided = false;
1172            let lt_exp = lt_exp.to_vec();
1173            let lt_coeff = lt_coeff.clone();
1174
1175            for divisor in divisors {
1176                // A zero divisor has no leading term and cannot divide anything.
1177                let Some((div_lt_exp, div_lt_coeff)) = divisor.leading_term() else {
1178                    continue;
1179                };
1180
1181                if let Some(quot_exp) = monomial_div(div_lt_exp, &lt_exp) {
1182                    // Can divide: subtract (lt/div_lt) * divisor from p
1183                    let quot_coeff = &lt_coeff / div_lt_coeff;
1184
1185                    // p -= quot_monomial * divisor
1186                    let subtrahend = divisor.mul_monomial(&quot_coeff, &quot_exp);
1187                    p = p.sub(&subtrahend);
1188                    divided = true;
1189                    break;
1190                }
1191            }
1192
1193            if !divided {
1194                // Leading term not divisible by any divisor — move to remainder
1195                remainder.insert_term(lt_exp.clone(), lt_coeff);
1196                // Remove leading term from p
1197                p.terms.remove(&MonoKey::<O>::new(lt_exp));
1198            }
1199        }
1200
1201        remainder
1202    }
1203
1204    /// Exact division by a single polynomial.
1205    ///
1206    /// Returns `Some(q)` with `self == q · divisor` when `divisor` divides
1207    /// `self` in ℚ[x₁, …, xₙ], and `None` otherwise (including for a zero
1208    /// divisor).  Uses the multivariate division algorithm with one divisor,
1209    /// for which the remainder vanishes iff the division is exact.
1210    ///
1211    /// # Examples
1212    ///
1213    /// ```
1214    /// use symplex::multipoly::MultiPoly;
1215    ///
1216    /// let [x, y]: [MultiPoly; 2] = [MultiPoly::var(2, 0), MultiPoly::var(2, 1)];
1217    /// let f = x.mul(&x).sub(&y.mul(&y));            // x² − y²
1218    /// let g = x.sub(&y);                            // x − y
1219    /// assert_eq!(f.div_exact(&g), Some(x.add(&y))); // x + y
1220    /// assert_eq!(f.div_exact(&x), None);
1221    /// ```
1222    pub fn div_exact(&self, divisor: &MultiPoly<O>) -> Option<MultiPoly<O>> {
1223        self.assert_compatible(divisor);
1224        let (div_lt_exp, div_lt_coeff) = divisor.leading_term()?;
1225        let div_lt_exp = div_lt_exp.to_vec();
1226        let div_lt_coeff = div_lt_coeff.clone();
1227
1228        let mut quotient = MultiPoly::zero(self.num_vars);
1229        let mut p = self.clone();
1230        while let Some((lt_exp, lt_coeff)) = p.leading_term() {
1231            let quot_exp = monomial_div(&div_lt_exp, lt_exp)?;
1232            let quot_coeff = lt_coeff / &div_lt_coeff;
1233            let subtrahend = divisor.mul_monomial(&quot_coeff, &quot_exp);
1234            quotient.insert_term(quot_exp, quot_coeff);
1235            p = p.sub(&subtrahend);
1236        }
1237        Some(quotient)
1238    }
1239
1240    /// Component-wise minimum of all exponent vectors: the largest monomial
1241    /// dividing every term.  Returns the all-zero vector for the zero
1242    /// polynomial.
1243    pub fn monomial_content(&self) -> Vec<u32> {
1244        let mut min: Option<Vec<u32>> = None;
1245        for (exp, _) in self.terms() {
1246            match &mut min {
1247                None => min = Some(exp.to_vec()),
1248                Some(m) => {
1249                    for (mi, &e) in m.iter_mut().zip(exp) {
1250                        *mi = (*mi).min(e);
1251                    }
1252                }
1253            }
1254        }
1255        min.unwrap_or_else(|| vec![0; self.num_vars])
1256    }
1257
1258    /// Indices of the variables that actually occur (with positive
1259    /// exponent) in some term.
1260    pub fn variables_present(&self) -> Vec<usize> {
1261        (0..self.num_vars)
1262            .filter(|&i| self.terms.keys().any(|k| k.exponents[i] > 0))
1263            .collect()
1264    }
1265}
1266
1267// ═══════════════════════════════════════════════════════════════════════════
1268// S-polynomial
1269// ═══════════════════════════════════════════════════════════════════════════
1270
1271/// Compute the S-polynomial of f and g.
1272pub fn s_polynomial<O: MonomialOrd>(f: &MultiPoly<O>, g: &MultiPoly<O>) -> MultiPoly<O> {
1273    assert_eq!(f.num_vars(), g.num_vars());
1274    // S(f, 0) = S(0, g) = 0: a zero operand has no leading term.
1275    let (Some((lm_f, lc_f)), Some((lm_g, lc_g))) = (f.leading_term(), g.leading_term()) else {
1276        return MultiPoly::zero(f.num_vars());
1277    };
1278
1279    let lcm = monomial_lcm(lm_f, lm_g);
1280
1281    // LCM/LT(f) * f - LCM/LT(g) * g.  Each leading monomial divides the lcm
1282    // component-wise (lcm = max), so the quotients are plain differences.
1283    let quot_f: Vec<u32> = lcm.iter().zip(lm_f).map(|(&l, &e)| l - e).collect();
1284    let quot_g: Vec<u32> = lcm.iter().zip(lm_g).map(|(&l, &e)| l - e).collect();
1285
1286    let coeff_f = Ratio::one() / lc_f;
1287    let coeff_g = Ratio::one() / lc_g;
1288
1289    let scaled_f = f.mul_monomial(&coeff_f, &quot_f);
1290    let scaled_g = g.mul_monomial(&coeff_g, &quot_g);
1291
1292    scaled_f.sub(&scaled_g)
1293}
1294
1295// ═══════════════════════════════════════════════════════════════════════════
1296// Display
1297// ═══════════════════════════════════════════════════════════════════════════
1298
1299impl<O: MonomialOrd> fmt::Display for MultiPoly<O> {
1300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1301        if self.is_zero() {
1302            return write!(f, "0");
1303        }
1304
1305        // Iterate in REVERSE (highest term first) for conventional display
1306        let mut first = true;
1307        for (key, coeff) in self.terms.iter().rev() {
1308            let exp = &key.exponents;
1309            let is_constant = exp.iter().all(|&e| e == 0);
1310            let coeff_is_one = *coeff == Ratio::one();
1311            let coeff_is_neg_one = *coeff == -Ratio::<BigInt>::one();
1312            let is_negative = coeff < &Ratio::from_integer(BigInt::from(0));
1313
1314            if first {
1315                if is_constant {
1316                    write!(f, "{coeff}")?;
1317                } else if coeff_is_one {
1318                    write!(f, "{}", format_monomial_vars(exp))?;
1319                } else if coeff_is_neg_one {
1320                    write!(f, "-{}", format_monomial_vars(exp))?;
1321                } else {
1322                    write!(f, "{}*{}", coeff, format_monomial_vars(exp))?;
1323                }
1324            } else if is_constant {
1325                if is_negative {
1326                    write!(f, " - {}", -coeff.clone())?;
1327                } else {
1328                    write!(f, " + {coeff}")?;
1329                }
1330            } else if coeff_is_one {
1331                write!(f, " + {}", format_monomial_vars(exp))?;
1332            } else if coeff_is_neg_one {
1333                write!(f, " - {}", format_monomial_vars(exp))?;
1334            } else if is_negative {
1335                let pos = -coeff.clone();
1336                write!(f, " - {}*{}", pos, format_monomial_vars(exp))?;
1337            } else {
1338                write!(f, " + {}*{}", coeff, format_monomial_vars(exp))?;
1339            }
1340            first = false;
1341        }
1342        Ok(())
1343    }
1344}
1345
1346/// Format the variable part of a monomial, e.g. `x0^2*x1`.
1347fn format_monomial_vars(exp: &[u32]) -> String {
1348    let mut parts = Vec::new();
1349    for (i, &e) in exp.iter().enumerate() {
1350        if e == 0 {
1351            continue;
1352        } else if e == 1 {
1353            parts.push(format!("x{i}"));
1354        } else {
1355            parts.push(format!("x{i}^{e}"));
1356        }
1357    }
1358    if parts.is_empty() {
1359        "1".to_string()
1360    } else {
1361        parts.join("*")
1362    }
1363}
1364
1365// ═══════════════════════════════════════════════════════════════════════════
1366// Operator overloads for &MultiPoly<O>
1367// ═══════════════════════════════════════════════════════════════════════════
1368
1369impl<O: MonomialOrd> ops::Add for &MultiPoly<O> {
1370    type Output = MultiPoly<O>;
1371    fn add(self, rhs: &MultiPoly<O>) -> MultiPoly<O> {
1372        MultiPoly::add(self, rhs)
1373    }
1374}
1375
1376impl<O: MonomialOrd> ops::Sub for &MultiPoly<O> {
1377    type Output = MultiPoly<O>;
1378    fn sub(self, rhs: &MultiPoly<O>) -> MultiPoly<O> {
1379        MultiPoly::sub(self, rhs)
1380    }
1381}
1382
1383impl<O: MonomialOrd> ops::Mul for &MultiPoly<O> {
1384    type Output = MultiPoly<O>;
1385    fn mul(self, rhs: &MultiPoly<O>) -> MultiPoly<O> {
1386        MultiPoly::mul(self, rhs)
1387    }
1388}
1389
1390impl<O: MonomialOrd> ops::Neg for &MultiPoly<O> {
1391    type Output = MultiPoly<O>;
1392    fn neg(self) -> MultiPoly<O> {
1393        MultiPoly::neg(self)
1394    }
1395}
1396
1397// Also implement for owned values for convenience.
1398
1399impl<O: MonomialOrd> ops::Add for MultiPoly<O> {
1400    type Output = MultiPoly<O>;
1401    fn add(self, rhs: MultiPoly<O>) -> MultiPoly<O> {
1402        MultiPoly::add(&self, &rhs)
1403    }
1404}
1405
1406impl<O: MonomialOrd> ops::Sub for MultiPoly<O> {
1407    type Output = MultiPoly<O>;
1408    fn sub(self, rhs: MultiPoly<O>) -> MultiPoly<O> {
1409        MultiPoly::sub(&self, &rhs)
1410    }
1411}
1412
1413impl<O: MonomialOrd> ops::Mul for MultiPoly<O> {
1414    type Output = MultiPoly<O>;
1415    fn mul(self, rhs: MultiPoly<O>) -> MultiPoly<O> {
1416        MultiPoly::mul(&self, &rhs)
1417    }
1418}
1419
1420impl<O: MonomialOrd> ops::Neg for MultiPoly<O> {
1421    type Output = MultiPoly<O>;
1422    fn neg(self) -> MultiPoly<O> {
1423        MultiPoly::neg(&self)
1424    }
1425}
1426
1427// ═══════════════════════════════════════════════════════════════════════════
1428// Operator overloads: MultiPoly<O> with i64
1429// ═══════════════════════════════════════════════════════════════════════════
1430
1431impl<O: MonomialOrd> ops::Add<i64> for &MultiPoly<O> {
1432    type Output = MultiPoly<O>;
1433    fn add(self, rhs: i64) -> MultiPoly<O> {
1434        let c = MultiPoly::from_int(self.num_vars(), rhs);
1435        MultiPoly::add(self, &c)
1436    }
1437}
1438impl<O: MonomialOrd> ops::Add<i64> for MultiPoly<O> {
1439    type Output = MultiPoly<O>;
1440    fn add(self, rhs: i64) -> MultiPoly<O> {
1441        (&self) + rhs
1442    }
1443}
1444
1445impl<O: MonomialOrd> ops::Sub<i64> for &MultiPoly<O> {
1446    type Output = MultiPoly<O>;
1447    fn sub(self, rhs: i64) -> MultiPoly<O> {
1448        let c = MultiPoly::from_int(self.num_vars(), rhs);
1449        MultiPoly::sub(self, &c)
1450    }
1451}
1452impl<O: MonomialOrd> ops::Sub<i64> for MultiPoly<O> {
1453    type Output = MultiPoly<O>;
1454    fn sub(self, rhs: i64) -> MultiPoly<O> {
1455        (&self) - rhs
1456    }
1457}
1458
1459impl<O: MonomialOrd> ops::Mul<i64> for &MultiPoly<O> {
1460    type Output = MultiPoly<O>;
1461    fn mul(self, rhs: i64) -> MultiPoly<O> {
1462        let c = Ratio::from_integer(BigInt::from(rhs));
1463        self.scale(&c)
1464    }
1465}
1466impl<O: MonomialOrd> ops::Mul<i64> for MultiPoly<O> {
1467    type Output = MultiPoly<O>;
1468    fn mul(self, rhs: i64) -> MultiPoly<O> {
1469        (&self) * rhs
1470    }
1471}
1472
1473// ═══════════════════════════════════════════════════════════════════════════
1474// Ring builder
1475// ═══════════════════════════════════════════════════════════════════════════
1476
1477/// Create variable polynomials for a ring with the given number of variables.
1478///
1479/// Returns a vector where element i is the polynomial xᵢ.
1480///
1481/// # Example
1482/// ```
1483/// use symplex::multipoly::*;
1484/// let vars = multipoly_vars::<GrevLex>(2);
1485/// let x = &vars[0];
1486/// let y = &vars[1];
1487/// let circle = x * x + y * y - 1;  // x² + y² - 1
1488/// ```
1489pub fn multipoly_vars<O: MonomialOrd>(num_vars: usize) -> Vec<MultiPoly<O>> {
1490    (0..num_vars).map(|i| MultiPoly::var(num_vars, i)).collect()
1491}
1492
1493// ═══════════════════════════════════════════════════════════════════════════
1494// Tests (unit, kept close to the code)
1495// ═══════════════════════════════════════════════════════════════════════════
1496
1497#[cfg(test)]
1498mod tests {
1499    use super::*;
1500
1501    #[test]
1502    fn grevlex_ordering() {
1503        // x^2 > xy in grevlex? Both total degree 2.
1504        // x^2 = [2,0], xy = [1,1].
1505        // Rightmost differing position: index 1 → 0 < 1, so [2,0] > [1,1].
1506        assert_eq!(
1507            GrevLex::cmp_exponents(&[2, 0], &[1, 1]),
1508            std::cmp::Ordering::Greater
1509        );
1510
1511        // xy > y^2 in grevlex? Both total degree 2.
1512        // xy = [1,1], y^2 = [0,2].
1513        // Rightmost differing: index 1 → 1 < 2, so [1,1] > [0,2].
1514        assert_eq!(
1515            GrevLex::cmp_exponents(&[1, 1], &[0, 2]),
1516            std::cmp::Ordering::Greater
1517        );
1518
1519        // Higher total degree always wins.
1520        assert_eq!(
1521            GrevLex::cmp_exponents(&[3, 0], &[1, 1]),
1522            std::cmp::Ordering::Greater
1523        );
1524    }
1525
1526    #[test]
1527    fn zero_is_zero() {
1528        let z: MultiPoly<GrevLex> = MultiPoly::zero(3);
1529        assert!(z.is_zero());
1530        assert_eq!(z.num_terms(), 0);
1531        assert_eq!(z.total_degree(), None);
1532    }
1533
1534    #[test]
1535    fn constant_round_trip() {
1536        let c: MultiPoly<GrevLex> = MultiPoly::from_int(2, 42);
1537        assert!(!c.is_zero());
1538        assert_eq!(c.num_terms(), 1);
1539        assert_eq!(c.total_degree(), Some(0));
1540        assert_eq!(c.eval(&[rat(0), rat(0)]), rat(42));
1541    }
1542
1543    // ── heuristic GCD ──────────────────────────────────────────────────────────────
1544
1545    fn vars2() -> (MultiPoly<GrevLex>, MultiPoly<GrevLex>) {
1546        (MultiPoly::var(2, 0), MultiPoly::var(2, 1))
1547    }
1548
1549    fn pow(p: &MultiPoly<GrevLex>, n: u32) -> MultiPoly<GrevLex> {
1550        let mut acc = MultiPoly::from_int(p.num_vars(), 1);
1551        for _ in 0..n {
1552            acc = acc.mul(p);
1553        }
1554        acc
1555    }
1556
1557    #[test]
1558    fn gcd_coprime_is_one() {
1559        let (x, y) = vars2();
1560        let f = x.mul(&x).add(&y); // x² + y
1561        let g = x.add(&y).add(&MultiPoly::from_int(2, 1)); // x + y + 1
1562        assert_eq!(MultiPoly::gcd(&f, &g), MultiPoly::from_int(2, 1));
1563        assert_eq!(MultiPoly::gcd(&x, &y), MultiPoly::from_int(2, 1));
1564    }
1565
1566    #[test]
1567    fn gcd_shared_linear_factor() {
1568        let (x, y) = vars2();
1569        let s = x.add(&y);
1570        let d = x.sub(&y);
1571        let f = s.mul(&d); // x² − y²
1572        let g = s.mul(&s); // (x + y)²
1573        assert_eq!(MultiPoly::gcd(&f, &g), s);
1574        // Negated input: sign is normalised away.
1575        assert_eq!(MultiPoly::gcd(&f.neg(), &g), s);
1576    }
1577
1578    #[test]
1579    fn gcd_three_variables() {
1580        let x: MultiPoly<GrevLex> = MultiPoly::var(3, 0);
1581        let y: MultiPoly<GrevLex> = MultiPoly::var(3, 1);
1582        let z: MultiPoly<GrevLex> = MultiPoly::var(3, 2);
1583        // h = xy + z + 1, f = h·(x − z), g = h·(y² + x)
1584        let h = x.mul(&y).add(&z).add(&MultiPoly::from_int(3, 1));
1585        let f = h.mul(&x.sub(&z));
1586        let g = h.mul(&y.mul(&y).add(&x));
1587        assert_eq!(MultiPoly::gcd(&f, &g), h);
1588        assert_eq!(MultiPoly::gcd(&g, &f), h);
1589    }
1590
1591    #[test]
1592    fn gcd_zero_handling() {
1593        let (x, y) = vars2();
1594        let f = x.mul(&y).scale(&rat(-4)); // −4xy
1595        let z: MultiPoly<GrevLex> = MultiPoly::zero(2);
1596        assert!(MultiPoly::gcd(&z, &z).is_zero());
1597        // gcd(f, 0) is f with a positive leading coefficient.
1598        assert_eq!(MultiPoly::gcd(&f, &z), x.mul(&y).scale(&rat(4)));
1599        assert_eq!(MultiPoly::gcd(&z, &f), x.mul(&y).scale(&rat(4)));
1600    }
1601
1602    #[test]
1603    fn gcd_includes_integer_content() {
1604        let (x, _y) = vars2();
1605        let f = x.scale(&rat(6)); // 6x
1606        let g = x.mul(&x).scale(&rat(4)); // 4x²
1607        assert_eq!(MultiPoly::gcd(&f, &g), x.scale(&rat(2)));
1608        // Constants.
1609        let twelve: MultiPoly<GrevLex> = MultiPoly::from_int(2, 12);
1610        assert_eq!(
1611            MultiPoly::gcd(&twelve, &MultiPoly::from_int(2, 18)),
1612            MultiPoly::from_int(2, 6)
1613        );
1614    }
1615
1616    #[test]
1617    fn gcd_clears_rational_denominators() {
1618        let (x, y) = vars2();
1619        let s = x.add(&y);
1620        let half = Ratio::new(BigInt::from(1), BigInt::from(2));
1621        let third = Ratio::new(BigInt::from(1), BigInt::from(3));
1622        let f = s.mul(&x).scale(&half); // (x + y)x / 2
1623        let g = s.mul(&y).scale(&third); // (x + y)y / 3
1624        let h = MultiPoly::gcd(&f, &g);
1625        assert!(f.div_exact(&h).is_some() && g.div_exact(&h).is_some());
1626        assert_eq!(h, s);
1627    }
1628
1629    #[test]
1630    fn gcd_large_coefficients() {
1631        let (x, y) = vars2();
1632        let big = |s: &str| Ratio::from_integer(s.parse::<BigInt>().unwrap());
1633        // h = 123456789012345678901234567890·x + 987654321098765432109876543210·y + 1
1634        let h = x
1635            .scale(&big("123456789012345678901234567890"))
1636            .add(&y.scale(&big("987654321098765432109876543210")))
1637            .add(&MultiPoly::from_int(2, 1));
1638        let f = h.mul(&x.add(&MultiPoly::from_int(2, 7)));
1639        let g = h.mul(&y.sub(&x.scale(&big("5555555555555555555"))));
1640        assert_eq!(MultiPoly::gcd(&f, &g), h);
1641    }
1642
1643    #[test]
1644    fn gcd_first_evaluation_point_fails_then_retry_succeeds() {
1645        // gcd = (x + 1)⁸ has a coefficient 70, but the inputs have max-norms
1646        // 28 and 112, so the first evaluation point ξ = 2·28 + 29 = 85 cannot
1647        // represent 70 as a symmetric digit: the first attempt must fail and
1648        // the next ξ must recover the answer.
1649        let x: MultiPoly<GrevLex> = MultiPoly::var(1, 0);
1650        let one = MultiPoly::from_int(1, 1);
1651        let h = pow(&x.add(&one), 8);
1652        let f = h.mul(&x.sub(&one)); // norm 28
1653        let g = h.mul(&x.mul(&x).add(&one)); // norm 112
1654        assert_eq!(f.max_norm(), BigInt::from(28));
1655        assert_eq!(g.max_norm(), BigInt::from(112));
1656        let xi0 = BigInt::from(85);
1657        assert!(MultiPoly::heugcd_attempt(&f, &g, 0, &xi0, 0).is_none());
1658        let xi1 = MultiPoly::<GrevLex>::next_xi(&xi0);
1659        assert!(xi1 > BigInt::from(140), "next ξ = {xi1}");
1660        assert_eq!(
1661            MultiPoly::heugcd_attempt(&f, &g, 0, &xi1, 0),
1662            Some(h.clone())
1663        );
1664        assert_eq!(MultiPoly::gcd(&f, &g), h);
1665    }
1666
1667    #[test]
1668    fn gcd_never_wrong_on_random_products() {
1669        // Deterministic pseudo-random small polynomials: gcd(h·a, h·b) must
1670        // be divisible by h and divide both products.
1671        let (x, y) = vars2();
1672        let mut seed: u64 = 0x2545_F491_4F6C_DD1D;
1673        let mut next = || {
1674            seed ^= seed << 13;
1675            seed ^= seed >> 7;
1676            seed ^= seed << 17;
1677            (seed % 7) as i64 - 3
1678        };
1679        let mut rand_poly = || {
1680            let mut p = MultiPoly::zero(2);
1681            for ex in 0..3u32 {
1682                for ey in 0..3u32 {
1683                    let c = next();
1684                    if c != 0 {
1685                        p = p.add(&MultiPoly::monomial(rat(c), vec![ex, ey]));
1686                    }
1687                }
1688            }
1689            if p.is_zero() { x.add(&y) } else { p }
1690        };
1691        for _ in 0..12 {
1692            let h = rand_poly();
1693            let a = rand_poly();
1694            let b = rand_poly();
1695            let f = h.mul(&a);
1696            let g = h.mul(&b);
1697            let d = MultiPoly::gcd(&f, &g);
1698            assert!(f.div_exact(&d).is_some(), "gcd does not divide f");
1699            assert!(g.div_exact(&d).is_some(), "gcd does not divide g");
1700            assert!(d.div_exact(&h).is_some(), "gcd {d} misses factor {h}");
1701        }
1702    }
1703
1704    #[test]
1705    fn lcm_of_monomials() {
1706        let (x, y) = vars2();
1707        let f = x.mul(&y);
1708        let g = y.mul(&y);
1709        assert_eq!(MultiPoly::lcm(&f, &g), x.mul(&y).mul(&y));
1710        assert!(MultiPoly::lcm(&f, &MultiPoly::zero(2)).is_zero());
1711    }
1712
1713    #[test]
1714    fn from_terms_and_map_coeffs() {
1715        let p: MultiPoly<GrevLex> =
1716            MultiPoly::from_terms(2, vec![(vec![1, 0], rat(2)), (vec![1, 0], rat(-2))]).unwrap();
1717        assert!(p.is_zero());
1718        let q: MultiPoly<GrevLex> = MultiPoly::from_terms(2, vec![(vec![2, 1], rat(3))]).unwrap();
1719        assert_eq!(q.coeff(&[2, 1]), Some(&rat(3)));
1720        assert_eq!(q.coeff(&[2]), None);
1721        let doubled = q.map_coeffs(|c| c * rat(2));
1722        assert_eq!(doubled.coeff(&[2, 1]), Some(&rat(6)));
1723        let killed = q.map_coeffs(|_| rat(0));
1724        assert!(killed.is_zero());
1725    }
1726
1727    #[test]
1728    fn integer_content_and_clear_denominators() {
1729        let (x, y) = vars2();
1730        let f = x.scale(&rat(6)).add(&y.scale(&rat(9)));
1731        assert_eq!(f.integer_content(), BigInt::from(3));
1732        let g = x
1733            .scale(&Ratio::new(BigInt::from(1), BigInt::from(2)))
1734            .add(&y.scale(&Ratio::new(BigInt::from(2), BigInt::from(3))));
1735        let (d, gz) = g.clear_denominators();
1736        assert_eq!(d, BigInt::from(6));
1737        assert_eq!(gz, x.scale(&rat(3)).add(&y.scale(&rat(4))));
1738    }
1739}