Skip to main content

ocas_poly/
sparse.rs

1//! Sparse multivariate polynomial implementation.
2//!
3//! A [`SparseMultivariatePolynomial`] stores only non-zero terms as a map from
4//! exponent vectors to coefficients. The exponent vector `vec![e1, e2, ...]`
5//! represents the monomial `x1^e1 * x2^e2 * ...`. Monomial ordering is
6//! controlled by the [`MonomialOrder`] type parameter.
7
8use ocas_core::FastHashMap as HashMap;
9use ocas_domain::{Domain, EuclideanDomain, FiniteField, IntegerDomain};
10use smallvec::SmallVec;
11
12use crate::factor::multivariate::{bivariate_factor_fp, bivariate_factor_z};
13
14/// A monomial ordering determines how terms are sorted and compared.
15///
16/// Simple orderings (Lex, Grevlex, Grlex) are zero-sized types with no
17/// runtime data. Parameterized orderings (WeightOrder, BlockOrder) carry
18/// configuration at runtime.
19///
20/// # Example
21///
22/// ```
23/// use ocas_poly::sparse::{Grevlex, Lex, MonomialOrder};
24///
25/// let a = [2, 1];
26/// let b = [1, 1];
27/// assert_eq!(Lex.cmp(&a, &b), std::cmp::Ordering::Greater);
28/// assert_eq!(Grevlex.cmp(&a, &b), std::cmp::Ordering::Less);
29/// ```
30pub trait MonomialOrder: Clone + PartialEq + Eq + std::fmt::Debug + Default {
31    /// Compare two exponent vectors.
32    ///
33    /// Returns `std::cmp::Ordering::Less` if `lhs` should appear before `rhs`
34    /// in the ordering.
35    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering;
36}
37
38/// Lexicographic ordering: compare exponents left-to-right.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub struct Lex;
41
42impl MonomialOrder for Lex {
43    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering {
44        lhs.cmp(rhs)
45    }
46}
47
48/// Graded reverse lexicographic ordering: first by total degree descending,
49/// then reverse lexicographic.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub struct Grevlex;
52
53impl MonomialOrder for Grevlex {
54    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering {
55        let deg_lhs: usize = lhs.iter().sum();
56        let deg_rhs: usize = rhs.iter().sum();
57        deg_rhs
58            .cmp(&deg_lhs)
59            .then_with(|| rhs.iter().rev().cmp(lhs.iter().rev()))
60    }
61}
62
63/// Graded lexicographic ordering: first by total degree descending,
64/// then lexicographic.
65///
66/// Grlex is sometimes preferred over grevlex in Gröbner basis computations
67/// because it can lead to smaller intermediate matrices in the F4 algorithm.
68///
69/// # Example
70///
71/// ```
72/// use ocas_poly::sparse::{Grlex, MonomialOrder};
73///
74/// let a = [2, 0]; // x^2, degree 2
75/// let b = [1, 1]; // x*y, degree 2
76/// let c = [0, 3]; // y^3, degree 3
77/// // c has highest degree, so it comes first
78/// assert_eq!(Grlex.cmp(&c, &a), std::cmp::Ordering::Less);
79/// // a and b have same degree; a > b lexicographically
80/// assert_eq!(Grlex.cmp(&a, &b), std::cmp::Ordering::Greater);
81/// ```
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83pub struct Grlex;
84
85impl MonomialOrder for Grlex {
86    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering {
87        let deg_lhs: usize = lhs.iter().sum();
88        let deg_rhs: usize = rhs.iter().sum();
89        deg_rhs.cmp(&deg_lhs).then_with(|| lhs.cmp(rhs))
90    }
91}
92
93/// Weighted ordering: compare by $\sum_i w_i \cdot e_i$ descending.
94///
95/// The weight vector is stored at construction time, enabling arbitrary
96/// elimination orderings that cannot be expressed as zero-sized types.
97///
98/// # Example
99///
100/// ```
101/// use ocas_poly::sparse::{MonomialOrder, WeightOrder};
102/// use smallvec::smallvec;
103///
104/// let ord = WeightOrder::new(smallvec![2, 1]);
105/// // [1,0] → weight 2, [0,1] → weight 1 → [1,0] is "larger"
106/// assert_eq!(ord.cmp(&[1, 0], &[0, 1]), std::cmp::Ordering::Less);
107/// ```
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct WeightOrder {
110    weights: SmallVec<[i64; 4]>,
111}
112
113impl WeightOrder {
114    /// Create a new weighted ordering with the given weight vector.
115    pub fn new(weights: SmallVec<[i64; 4]>) -> Self {
116        Self { weights }
117    }
118
119    /// Create a weighted ordering from a slice.
120    pub fn from_slice(weights: &[i64]) -> Self {
121        Self {
122            weights: SmallVec::from_slice(weights),
123        }
124    }
125}
126
127impl Default for WeightOrder {
128    /// Default: all-ones weights (total degree ordering).
129    fn default() -> Self {
130        Self {
131            weights: smallvec::smallvec![1; 4],
132        }
133    }
134}
135
136impl MonomialOrder for WeightOrder {
137    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering {
138        let w_lhs: i64 = lhs
139            .iter()
140            .zip(self.weights.iter())
141            .map(|(&e, &w)| w * e as i64)
142            .sum();
143        let w_rhs: i64 = rhs
144            .iter()
145            .zip(self.weights.iter())
146            .map(|(&e, &w)| w * e as i64)
147            .sum();
148        // Higher weight first (descending).
149        w_rhs.cmp(&w_lhs)
150    }
151}
152
153/// A sub-ordering used inside [`BlockOrder`] for each variable block.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum SubOrder {
156    /// Lexicographic within the block.
157    Lex,
158    /// Graded reverse lexicographic within the block.
159    Grevlex,
160    /// Graded lexicographic within the block.
161    Grlex,
162}
163
164impl SubOrder {
165    fn cmp_block(&self, lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering {
166        match self {
167            SubOrder::Lex => lhs.cmp(rhs),
168            SubOrder::Grevlex => {
169                let deg_l: usize = lhs.iter().sum();
170                let deg_r: usize = rhs.iter().sum();
171                deg_r
172                    .cmp(&deg_l)
173                    .then_with(|| rhs.iter().rev().cmp(lhs.iter().rev()))
174            }
175            SubOrder::Grlex => {
176                let deg_l: usize = lhs.iter().sum();
177                let deg_r: usize = rhs.iter().sum();
178                deg_r.cmp(&deg_l).then_with(|| lhs.cmp(rhs))
179            }
180        }
181    }
182}
183
184/// Block ordering: partition variables into contiguous blocks, each compared
185/// under its own sub-ordering.
186///
187/// Blocks are defined by `boundaries`: a sorted list of split points
188/// (exclusive upper bounds, *not* including `n_vars`). For example,
189/// `boundaries = [2]` with `orders = [Lex, Grevlex]` on a 4-variable
190/// polynomial means: compare variables 0–1 under Lex first; if equal,
191/// compare variables 2–3 under Grevlex.
192///
193/// # Example
194///
195/// ```
196/// use ocas_poly::sparse::{BlockOrder, MonomialOrder, SubOrder};
197/// use smallvec::smallvec;
198///
199/// let ord = BlockOrder::new(smallvec![2], smallvec![SubOrder::Lex, SubOrder::Grevlex]);
200/// // First compare variables 0–1 lex, then variables 2–3 grevlex.
201/// let a = [1, 0, 0, 0]; // x₀
202/// let b = [0, 1, 0, 0]; // x₁
203/// // Lex: [1,0] > [0,1], so a is "greater" (comes first in ordering)
204/// assert_eq!(ord.cmp(&a, &b), std::cmp::Ordering::Greater);
205/// ```
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct BlockOrder {
208    /// Sorted split points (exclusive upper bounds, excluding n_vars).
209    boundaries: SmallVec<[usize; 4]>,
210    /// One sub-ordering per block (len = boundaries.len() + 1).
211    orders: SmallVec<[SubOrder; 4]>,
212}
213
214impl BlockOrder {
215    /// Create a new block ordering.
216    ///
217    /// `boundaries` must be sorted in ascending order and not include `n_vars`.
218    /// `orders.len()` must equal `boundaries.len() + 1`.
219    pub fn new(boundaries: SmallVec<[usize; 4]>, orders: SmallVec<[SubOrder; 4]>) -> Self {
220        debug_assert_eq!(orders.len(), boundaries.len() + 1);
221        Self { boundaries, orders }
222    }
223}
224
225impl Default for BlockOrder {
226    /// Default: single block with Grevlex.
227    fn default() -> Self {
228        Self {
229            boundaries: SmallVec::new(),
230            orders: smallvec::smallvec![SubOrder::Grevlex],
231        }
232    }
233}
234
235impl MonomialOrder for BlockOrder {
236    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering {
237        let mut start = 0;
238        for (i, &end) in self.boundaries.iter().enumerate() {
239            match self.orders[i].cmp_block(&lhs[start..end], &rhs[start..end]) {
240                std::cmp::Ordering::Equal => {}
241                ord => return ord,
242            }
243            start = end;
244        }
245        // Last block: from `start` to end of slice.
246        self.orders[self.boundaries.len()].cmp_block(&lhs[start..], &rhs[start..])
247    }
248}
249
250/// A sparse multivariate polynomial with coefficients in a domain `D` and
251/// monomial ordering `O`.
252///
253/// # Example
254///
255/// ```
256/// use ocas_domain::{IntegerDomain, Integer};
257/// use ocas_poly::sparse::Grevlex;
258/// use ocas_poly::SparseMultivariatePolynomial;
259///
260/// let domain = IntegerDomain;
261/// let p = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
262///     domain,
263///     2,
264///     vec![(vec![1, 0], Integer::from(2)), (vec![0, 1], Integer::from(3))],
265/// );
266/// let q = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
267///     domain,
268///     2,
269///     vec![(vec![1, 0], Integer::from(1)), (vec![0, 0], Integer::from(1))],
270/// );
271/// let r = p.mul(&q);
272/// assert_eq!(r.coeff(&[1, 0]), Integer::from(2));
273/// assert_eq!(r.coeff(&[0, 1]), Integer::from(3));
274/// assert_eq!(r.coeff(&[2, 0]), Integer::from(2));
275/// ```
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct SparseMultivariatePolynomial<D: Domain, O: MonomialOrder = Grevlex> {
278    /// Non-zero terms indexed by exponent vector.
279    terms: HashMap<SmallVec<[usize; 4]>, D::Element>,
280    /// The coefficient domain.
281    domain: D,
282    /// Number of variables. Exponent vectors are padded/trimmed to this length.
283    n_vars: usize,
284    /// The monomial ordering used for leading-term and sorting operations.
285    pub order: O,
286}
287
288impl<D: Domain, O: MonomialOrder> SparseMultivariatePolynomial<D, O> {
289    /// Create the zero polynomial in `n_vars` variables over `domain`
290    /// with the default monomial ordering.
291    pub fn new(domain: D, n_vars: usize) -> Self {
292        Self {
293            terms: HashMap::default(),
294            domain,
295            n_vars,
296            order: O::default(),
297        }
298    }
299
300    /// Create the zero polynomial with an explicit monomial ordering.
301    ///
302    /// # Example
303    ///
304    /// ```
305    /// use ocas_domain::IntegerDomain;
306    /// use ocas_poly::sparse::{SparseMultivariatePolynomial, WeightOrder};
307    ///
308    /// let order = WeightOrder::from_slice(&[2, 1]);
309    /// let p = SparseMultivariatePolynomial::<_, WeightOrder>::new_with_order(
310    ///     IntegerDomain, 2, order,
311    /// );
312    /// assert_eq!(p.n_vars(), 2);
313    /// ```
314    pub fn new_with_order(domain: D, n_vars: usize, order: O) -> Self {
315        Self {
316            terms: HashMap::default(),
317            domain,
318            n_vars,
319            order,
320        }
321    }
322
323    /// Create a polynomial from a list of (exponent vector, coefficient) pairs.
324    ///
325    /// Zero coefficients and empty terms are dropped automatically.
326    ///
327    /// # Example
328    ///
329    /// ```
330    /// use ocas_domain::{IntegerDomain, Integer};
331    /// use ocas_poly::sparse::Grevlex;
332    /// use ocas_poly::SparseMultivariatePolynomial;
333    ///
334    /// let domain = IntegerDomain;
335    /// let p = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
336    ///     domain,
337    ///     2,
338    ///     vec![(vec![1, 0], Integer::from(2)), (vec![0, 1], Integer::from(3))],
339    /// );
340    /// assert_eq!(p.n_terms(), 2);
341    /// assert_eq!(p.coeff(&[1, 0]), Integer::from(2));
342    /// ```
343    pub fn from_terms(domain: D, n_vars: usize, terms: Vec<(Vec<usize>, D::Element)>) -> Self {
344        let mut poly = Self::new(domain, n_vars);
345        for (exp, coeff) in terms {
346            poly.set_term(exp, coeff);
347        }
348        poly
349    }
350
351    /// Return a reference to the coefficient domain.
352    pub fn domain(&self) -> &D {
353        &self.domain
354    }
355
356    /// Return the number of variables.
357    pub fn n_vars(&self) -> usize {
358        self.n_vars
359    }
360
361    /// Return the number of non-zero terms.
362    pub fn n_terms(&self) -> usize {
363        self.terms.len()
364    }
365
366    /// Return whether this is the zero polynomial.
367    pub fn is_zero(&self) -> bool {
368        self.terms.is_empty()
369    }
370
371    /// Return a reference to the internal term map (exponent → coefficient).
372    pub fn terms_ref(&self) -> &HashMap<SmallVec<[usize; 4]>, D::Element> {
373        &self.terms
374    }
375
376    /// Set the coefficient of a monomial (public version of `set_term`).
377    /// Zero coefficients remove the term.
378    pub fn set_term_external(&mut self, exp: Vec<usize>, coeff: D::Element) {
379        self.set_term(exp, coeff);
380    }
381
382    /// Return the total degree, or `None` for the zero polynomial.
383    pub fn total_degree(&self) -> Option<usize> {
384        self.terms.keys().map(|e| e.iter().sum::<usize>()).max()
385    }
386
387    /// Return the coefficient of the given monomial, or zero if absent.
388    pub fn coeff(&self, exp: &[usize]) -> D::Element {
389        let key = Self::normalize_exp(exp, self.n_vars);
390        self.terms
391            .get(&key)
392            .cloned()
393            .unwrap_or_else(|| self.domain.zero())
394    }
395
396    /// Set the coefficient of a monomial. Zero coefficients remove the term.
397    fn set_term(&mut self, exp: Vec<usize>, coeff: D::Element) {
398        let key = Self::normalize_exp(&exp, self.n_vars);
399        if self.domain.is_zero(&coeff) {
400            self.terms.remove(&key);
401        } else {
402            self.terms.insert(key, coeff);
403        }
404    }
405
406    fn normalize_exp(exp: &[usize], n_vars: usize) -> SmallVec<[usize; 4]> {
407        let mut v = SmallVec::with_capacity(n_vars);
408        for i in 0..n_vars {
409            v.push(*exp.get(i).unwrap_or(&0));
410        }
411        v
412    }
413
414    /// Return the zero polynomial with the same shape.
415    pub fn zero(&self) -> Self {
416        Self::new(self.domain.clone(), self.n_vars)
417    }
418
419    /// Return the constant polynomial `1` over the same shape.
420    pub fn one(&self) -> Self {
421        let mut poly = Self::new(self.domain.clone(), self.n_vars);
422        let mut exp = SmallVec::with_capacity(self.n_vars);
423        exp.resize(self.n_vars, 0);
424        poly.terms.insert(exp, self.domain.one());
425        poly
426    }
427
428    /// Return the negation of this polynomial.
429    pub fn neg(&self) -> Self {
430        let mut poly = self.zero();
431        for (exp, coeff) in &self.terms {
432            poly.terms.insert(exp.clone(), self.domain.neg(coeff));
433        }
434        poly
435    }
436
437    /// Add another polynomial.
438    ///
439    /// Panics if the polynomials have different numbers of variables.
440    pub fn add(&self, other: &Self) -> Self {
441        assert_eq!(
442            self.n_vars, other.n_vars,
443            "polynomials must have the same number of variables"
444        );
445        let mut poly = self.clone();
446        for (exp, coeff) in &other.terms {
447            let existing = poly
448                .terms
449                .get(exp)
450                .cloned()
451                .unwrap_or_else(|| poly.domain.zero());
452            let sum = poly.domain.add(&existing, coeff);
453            if poly.domain.is_zero(&sum) {
454                poly.terms.remove(exp);
455            } else {
456                poly.terms.insert(exp.clone(), sum);
457            }
458        }
459        poly
460    }
461
462    /// Subtract another polynomial.
463    ///
464    /// Panics if the polynomials have different numbers of variables.
465    pub fn sub(&self, other: &Self) -> Self {
466        self.add(&other.neg())
467    }
468
469    /// Multiply by a scalar coefficient.
470    pub fn mul_scalar(&self, scalar: &D::Element) -> Self {
471        if self.domain.is_zero(scalar) {
472            return self.zero();
473        }
474        let mut poly = self.zero();
475        for (exp, coeff) in &self.terms {
476            poly.terms
477                .insert(exp.clone(), self.domain.mul(coeff, scalar));
478        }
479        poly
480    }
481
482    /// Multiply two polynomials.
483    ///
484    /// Panics if the polynomials have different numbers of variables.
485    pub fn mul(&self, other: &Self) -> Self {
486        assert_eq!(
487            self.n_vars, other.n_vars,
488            "polynomials must have the same number of variables"
489        );
490        if self.is_zero() || other.is_zero() {
491            return self.zero();
492        }
493        let mut poly = self.zero();
494        for (e1, c1) in &self.terms {
495            for (e2, c2) in &other.terms {
496                let mut exp = SmallVec::with_capacity(self.n_vars);
497                for i in 0..self.n_vars {
498                    exp.push(e1[i] + e2[i]);
499                }
500                let prod = self.domain.mul(c1, c2);
501                let existing = poly
502                    .terms
503                    .get(&exp)
504                    .cloned()
505                    .unwrap_or_else(|| poly.domain.zero());
506                let sum = poly.domain.add(&existing, &prod);
507                if poly.domain.is_zero(&sum) {
508                    poly.terms.remove(&exp);
509                } else {
510                    poly.terms.insert(exp, sum);
511                }
512            }
513        }
514        poly
515    }
516
517    /// Return the terms sorted according to the monomial ordering.
518    pub fn sorted_terms(&self) -> Vec<(&SmallVec<[usize; 4]>, &D::Element)> {
519        let mut terms: Vec<_> = self.terms.iter().collect();
520        terms.sort_by(|(a, _), (b, _)| self.order.cmp(a, b));
521        terms
522    }
523
524    // ------------------------------------------------------------------
525    //  Gröbner-basis support
526    // ------------------------------------------------------------------
527
528    /// Return the leading term `(exponent_vector, coefficient)` or `None`
529    /// for the zero polynomial.
530    ///
531    /// This scans the HashMap in O(n) without allocating — faster than
532    /// `sorted_terms()` for repeated calls during reduction.
533    pub fn leading_term(&self) -> Option<(&SmallVec<[usize; 4]>, &D::Element)> {
534        self.terms
535            .iter()
536            .max_by(|(a, _), (b, _)| self.order.cmp(a, b))
537    }
538
539    /// Return the leading monomial (exponent vector) or `None`.
540    pub fn leading_monomial(&self) -> Option<&SmallVec<[usize; 4]>> {
541        self.terms.keys().max_by(|a, b| self.order.cmp(a, b))
542    }
543
544    /// Return the leading coefficient or `None`.
545    pub fn leading_coeff(&self) -> Option<&D::Element> {
546        let lm = self.leading_monomial()?;
547        self.terms.get(lm)
548    }
549
550    /// Multiply every term's exponent vector by `exp` element-wise.
551    ///
552    /// Panics if `exp.len() != self.n_vars`.
553    pub fn mul_monomial(&self, exp: &[usize]) -> Self {
554        assert_eq!(
555            exp.len(),
556            self.n_vars,
557            "exponent vector must have length {}",
558            self.n_vars
559        );
560        let mut poly = self.zero();
561        for (e, c) in &self.terms {
562            let mut new_exp = SmallVec::with_capacity(self.n_vars);
563            for i in 0..self.n_vars {
564                new_exp.push(e[i] + exp[i]);
565            }
566            poly.terms.insert(new_exp, c.clone());
567        }
568        poly
569    }
570
571    /// Reduce `self` by the given basis (a list of polynomials).
572    ///
573    /// Implements multivariate polynomial division: repeatedly look for a
574    /// basis element whose leading monomial divides the current leading
575    /// monomial, subtract the appropriate multiple, or else move the leading
576    /// term into the remainder.  Requires that `div` on the domain succeeds
577    /// (i.e. the domain is effectively a field).
578    pub fn reduce(&self, basis: &[Self]) -> Self {
579        let mut remainder = self.clone();
580        let mut result = self.zero();
581
582        // Cache each basis element's leading term.
583        let basis_lts: Vec<_> = basis
584            .iter()
585            .filter_map(|g| g.leading_term().map(|(e, c)| (g, e.clone(), c.clone())))
586            .collect();
587
588        let max_iter = 10000;
589
590        for _ in 0..max_iter {
591            if remainder.is_zero() {
592                break;
593            }
594            let (rm, rc) = match remainder.leading_term() {
595                Some((e, c)) => (e.clone(), c.clone()),
596                None => break,
597            };
598
599            let mut reduced = false;
600            for (g, lm, lc) in &basis_lts {
601                if monomial_divides(&rm, lm) {
602                    let qm: SmallVec<[usize; 4]> =
603                        rm.iter().zip(lm.iter()).map(|(a, b)| a - b).collect();
604                    let qc = match self.domain.div(&rc, lc) {
605                        Some(q) => q,
606                        None => break,
607                    };
608                    let sub = g.mul_monomial(&qm).mul_scalar(&qc);
609                    remainder = remainder.sub(&sub);
610                    reduced = true;
611                    break;
612                }
613            }
614
615            if !reduced {
616                let key = rm;
617                let val = rc;
618                result.terms.insert(key.clone(), val);
619                remainder.terms.remove(&key);
620            }
621        }
622
623        result
624    }
625
626    /// Compute the S-polynomial of `self` and `other`:
627    ///
628    /// S(f, g) = f·lc(g)·x^(lcm-lm(f)) - g·lc(f)·x^(lcm-lm(g))
629    pub fn spoly(&self, other: &Self) -> Self {
630        let (lm_f, lc_f) = match self.leading_term() {
631            Some(t) => (t.0.clone(), t.1.clone()),
632            None => return self.zero(),
633        };
634        let (lm_g, lc_g) = match other.leading_term() {
635            Some(t) => (t.0.clone(), t.1.clone()),
636            None => return self.zero(),
637        };
638
639        let lcm = monomial_lcm(&lm_f, &lm_g);
640
641        let m_f: SmallVec<[usize; 4]> = lcm.iter().zip(lm_f.iter()).map(|(a, b)| a - b).collect();
642        let m_g: SmallVec<[usize; 4]> = lcm.iter().zip(lm_g.iter()).map(|(a, b)| a - b).collect();
643
644        let term1 = self.mul_monomial(&m_f).mul_scalar(&lc_g);
645        let term2 = other.mul_monomial(&m_g).mul_scalar(&lc_f);
646
647        term1.sub(&term2)
648    }
649
650    // ------------------------------------------------------------------
651    //  Multivariate GCD support
652    // ------------------------------------------------------------------
653
654    /// Compute the content: the GCD of all coefficients.
655    ///
656    /// For the zero polynomial the content is zero.
657    ///
658    /// # Example
659    ///
660    /// ```
661    /// use ocas_domain::{Integer, IntegerDomain};
662    /// use ocas_poly::SparseMultivariatePolynomial;
663    /// use ocas_poly::Lex;
664    ///
665    /// let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
666    ///     IntegerDomain, 1,
667    ///     vec![(vec![2], Integer::from(6)), (vec![1], Integer::from(9)), (vec![0], Integer::from(3))],
668    /// );
669    /// assert_eq!(p.content(), Integer::from(3));
670    /// ```
671    pub fn content(&self) -> D::Element
672    where
673        D: EuclideanDomain,
674    {
675        if self.is_zero() {
676            return self.domain.zero();
677        }
678        let mut g = self.domain.zero();
679        for c in self.terms.values() {
680            g = self.domain.gcd(&g, c);
681        }
682        g
683    }
684
685    /// Return the primitive part: `self / content`.
686    ///
687    /// The result has content 1 (or is zero).
688    ///
689    /// # Example
690    ///
691    /// ```
692    /// use ocas_domain::{Integer, IntegerDomain};
693    /// use ocas_poly::SparseMultivariatePolynomial;
694    /// use ocas_poly::Lex;
695    ///
696    /// let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
697    ///     IntegerDomain, 1,
698    ///     vec![(vec![2], Integer::from(6)), (vec![0], Integer::from(3))],
699    /// );
700    /// let pp = p.primitive_part();
701    /// // After dividing by content=3: 2*x^2 + 1
702    /// assert_eq!(pp.coeff(&[2]), Integer::from(2));
703    /// assert_eq!(pp.coeff(&[0]), Integer::from(1));
704    /// ```
705    pub fn primitive_part(&self) -> Self
706    where
707        D: EuclideanDomain,
708    {
709        if self.is_zero() {
710            return self.clone();
711        }
712        let content = self.content();
713        if self.domain.is_one(&content) {
714            return self.clone();
715        }
716        let mut result = self.zero();
717        for (exp, c) in &self.terms {
718            let q = self.domain.div(c, &content).unwrap_or_else(|| c.clone());
719            result.terms.insert(exp.clone(), q);
720        }
721        result
722    }
723
724    /// Divide this polynomial by another, assuming the division is exact
725    /// (no remainder).
726    ///
727    /// Each term of `self` is divided by the corresponding factor from
728    /// `divisor`. This is used in rational-function canonicalization where
729    /// the GCD is known to divide both numerator and denominator.
730    ///
731    /// # Panics
732    ///
733    /// Panics if the division is not exact.
734    pub fn div_exact(&self, divisor: &Self) -> Self {
735        if divisor.n_terms() <= 1 {
736            // Check if divisor is constant 1 (or zero).
737            let const_val = divisor.coeff(&vec![0; divisor.n_vars]);
738            if self.domain.is_one(&const_val) {
739                return self.clone();
740            }
741        }
742        let (quot, rem) = self.div_rem_sparse(divisor);
743        debug_assert!(rem.is_zero(), "div_exact: division had non-zero remainder");
744        quot
745    }
746
747    /// Sparse polynomial long division returning (quotient, remainder).
748    fn div_rem_sparse(&self, divisor: &Self) -> (Self, Self) {
749        if divisor.is_zero() {
750            panic!("division by zero polynomial");
751        }
752        let (_, div_lm) = match divisor.leading_term() {
753            Some(t) => (t.0.clone(), t.1.clone()),
754            None => return (self.zero(), self.clone()),
755        };
756        let div_lc = div_lm;
757        let div_exp = divisor.leading_monomial().unwrap().clone();
758
759        let mut remainder = self.clone();
760        let mut quotient = self.zero();
761
762        while !remainder.is_zero() {
763            let (rem_exp, rem_lc) = match remainder.leading_term() {
764                Some(t) => (t.0.clone(), t.1.clone()),
765                None => break,
766            };
767            // Check if leading monomial of divisor divides leading monomial of remainder.
768            if !monomial_divides(&rem_exp, &div_exp) {
769                break;
770            }
771            let q_coeff = match self.domain.div(&rem_lc, &div_lc) {
772                Some(q) => q,
773                None => break,
774            };
775            let q_exp: SmallVec<[usize; 4]> = rem_exp
776                .iter()
777                .zip(div_exp.iter())
778                .map(|(a, b)| a - b)
779                .collect();
780            // quotient += q_coeff * x^q_exp
781            let existing = quotient
782                .terms
783                .get(&q_exp)
784                .cloned()
785                .unwrap_or_else(|| self.domain.zero());
786            let sum = self.domain.add(&existing, &q_coeff);
787            if self.domain.is_zero(&sum) {
788                quotient.terms.remove(&q_exp);
789            } else {
790                quotient.terms.insert(q_exp, sum);
791            }
792            // remainder -= q_coeff * x^q_exp * divisor
793            let scaled = divisor.mul_monomial(
794                &remainder
795                    .leading_monomial()
796                    .unwrap()
797                    .iter()
798                    .zip(div_exp.iter())
799                    .map(|(a, b)| a - b)
800                    .collect::<SmallVec<[usize; 4]>>(),
801            );
802            let scaled = scaled.mul_scalar(&q_coeff);
803            remainder = remainder.sub(&scaled);
804        }
805        (quotient, remainder)
806    }
807
808    /// Return the degree of this polynomial in the given variable.
809    ///
810    /// Returns 0 for the zero polynomial (by convention) or if the variable
811    /// does not appear.
812    pub fn degree_in(&self, var_index: usize) -> usize {
813        self.terms
814            .keys()
815            .map(|e| e.get(var_index).copied().unwrap_or(0))
816            .max()
817            .unwrap_or(0)
818    }
819
820    // ------------------------------------------------------------------
821    //  Multivariate factorization support
822    // ------------------------------------------------------------------
823
824    /// Return the coefficient polynomial of `x_var^pow`: the sum of all terms
825    /// whose exponent in `var_index` equals `pow`, with that exponent zeroed
826    /// out. The result has the same number of variables and does not depend
827    /// on `var_index`.
828    pub fn coeff_of_var_pow(&self, var_index: usize, pow: usize) -> Self {
829        let mut result = Self::new(self.domain.clone(), self.n_vars);
830        for (exp, coeff) in &self.terms {
831            if exp.get(var_index).copied().unwrap_or(0) == pow {
832                let mut new_exp = exp.clone();
833                if var_index < new_exp.len() {
834                    new_exp[var_index] = 0;
835                }
836                result.terms.insert(new_exp, coeff.clone());
837            }
838        }
839        result
840    }
841
842    /// Return the leading coefficient when this polynomial is viewed as a
843    /// univariate polynomial in `var_index`. The result is a polynomial in
844    /// the remaining variables (same `n_vars`, exponent of `var_index` is 0).
845    pub fn leading_coeff_in(&self, var_index: usize) -> Self {
846        self.coeff_of_var_pow(var_index, self.degree_in(var_index))
847    }
848
849    /// Compute the formal partial derivative with respect to `var_index`.
850    pub fn derivative(&self, var_index: usize) -> Self {
851        let mut result = Self::new(self.domain.clone(), self.n_vars);
852        for (exp, coeff) in &self.terms {
853            let power = exp.get(var_index).copied().unwrap_or(0);
854            if power == 0 {
855                continue;
856            }
857            let mut new_exp = exp.clone();
858            new_exp[var_index] = power - 1;
859            let scalar = self.domain.cast_u64(power as u64);
860            let new_coeff = self.domain.mul(coeff, &scalar);
861            let existing = result
862                .terms
863                .get(&new_exp)
864                .cloned()
865                .unwrap_or_else(|| self.domain.zero());
866            let sum = self.domain.add(&existing, &new_coeff);
867            if self.domain.is_zero(&sum) {
868                result.terms.remove(&new_exp);
869            } else {
870                result.terms.insert(new_exp, sum);
871            }
872        }
873        result
874    }
875
876    /// Compute the Taylor coefficients in variable `var_index` around `a`:
877    /// `f = Σ_j t_j · (x_var - a)^j` where each `t_j` does not depend on
878    /// `var_index` (its exponent is zeroed).
879    ///
880    /// Returns `t_0, t_1, ..., t_d` with `d = degree_in(var_index)`.
881    pub fn taylor_coefficients(&self, var_index: usize, a: &D::Element) -> Vec<Self> {
882        let d = self.degree_in(var_index);
883        let mut coeffs = vec![Self::new(self.domain.clone(), self.n_vars); d + 1];
884        for (exp, coeff) in &self.terms {
885            let e = exp.get(var_index).copied().unwrap_or(0);
886            let mut base_exp = exp.clone();
887            if var_index < base_exp.len() {
888                base_exp[var_index] = 0;
889            }
890            // x_v^e = Σ_j binom(e, j) · a^(e-j) · (x_v - a)^j
891            for (j, t_j) in coeffs.iter_mut().enumerate().take(e + 1) {
892                let binom = self.domain.cast_u64(binomial(e, j));
893                let a_pow = self.domain.pow(a, (e - j) as u64);
894                let contrib = self.domain.mul(coeff, &self.domain.mul(&binom, &a_pow));
895                let existing = t_j
896                    .terms
897                    .get(&base_exp)
898                    .cloned()
899                    .unwrap_or_else(|| self.domain.zero());
900                let sum = self.domain.add(&existing, &contrib);
901                if self.domain.is_zero(&sum) {
902                    t_j.terms.remove(&base_exp);
903                } else {
904                    t_j.terms.insert(base_exp.clone(), sum);
905                }
906            }
907        }
908        coeffs
909    }
910
911    /// Drop variable 0, which must not occur in any term. Returns a
912    /// polynomial in `n_vars - 1` variables with indices shifted down.
913    ///
914    /// Panics in debug builds if variable 0 occurs with a non-zero exponent.
915    pub fn drop_main_var(&self) -> Self {
916        debug_assert!(
917            self.terms_ref()
918                .keys()
919                .all(|e| e.first().copied().unwrap_or(0) == 0),
920            "drop_main_var: variable 0 must not occur"
921        );
922        let new_n_vars = self.n_vars.saturating_sub(1);
923        let mut result = Self::new(self.domain.clone(), new_n_vars);
924        for (exp, coeff) in &self.terms {
925            if exp.first().copied().unwrap_or(0) != 0 {
926                continue;
927            }
928            let new_exp: SmallVec<[usize; 4]> = exp.iter().skip(1).copied().collect();
929            result.terms.insert(new_exp, coeff.clone());
930        }
931        result
932    }
933
934    /// Embed into one more variable by inserting a new variable 0 with
935    /// exponent 0 (all existing variable indices shift up by one).
936    pub fn embed_new_main(&self) -> Self {
937        let new_n_vars = self.n_vars + 1;
938        let mut result = Self::new(self.domain.clone(), new_n_vars);
939        for (exp, coeff) in &self.terms {
940            let mut new_exp = SmallVec::with_capacity(new_n_vars);
941            new_exp.push(0);
942            new_exp.extend(exp.iter().copied());
943            result.terms.insert(new_exp, coeff.clone());
944        }
945        result
946    }
947
948    /// Permute variables: the result's exponent at position `i` is the old
949    /// exponent at position `perm[i]`. `perm` must be a permutation of
950    /// `0..n_vars`.
951    pub fn permute_variables(&self, perm: &[usize]) -> Self {
952        assert_eq!(perm.len(), self.n_vars, "perm must be a permutation");
953        let mut result = Self::new(self.domain.clone(), self.n_vars);
954        for (exp, coeff) in &self.terms {
955            let mut new_exp = SmallVec::with_capacity(self.n_vars);
956            for &p in perm {
957                new_exp.push(exp.get(p).copied().unwrap_or(0));
958            }
959            result.terms.insert(new_exp, coeff.clone());
960        }
961        result
962    }
963
964    /// Divide this polynomial by `divisor`, returning the quotient only if
965    /// the division is exact (zero remainder).
966    pub fn checked_div_exact(&self, divisor: &Self) -> Option<Self> {
967        if divisor.is_zero() {
968            return None;
969        }
970        let (quot, rem) = self.div_rem_sparse(divisor);
971        if rem.is_zero() { Some(quot) } else { None }
972    }
973
974    /// Evaluate variable `var_index` at `value` while keeping the total
975    /// number of variables unchanged (the variable disappears from the
976    /// support but all indices are preserved).
977    ///
978    /// This is the substitution used by multivariate Hensel lifting, where
979    /// variable positions must stay fixed across recursion levels.
980    pub fn eval_keep(&self, var_index: usize, value: &D::Element) -> Self {
981        let mut result = Self::new(self.domain.clone(), self.n_vars);
982        for (exp, coeff) in &self.terms {
983            let power = self.domain.pow(value, exp[var_index] as u64);
984            let new_coeff = self.domain.mul(coeff, &power);
985            if self.domain.is_zero(&new_coeff) {
986                continue;
987            }
988            let mut new_exp = exp.clone();
989            new_exp[var_index] = 0;
990            let existing = result
991                .terms
992                .get(&new_exp)
993                .cloned()
994                .unwrap_or_else(|| self.domain.zero());
995            let sum = self.domain.add(&existing, &new_coeff);
996            if self.domain.is_zero(&sum) {
997                result.terms.remove(&new_exp);
998            } else {
999                result.terms.insert(new_exp, sum);
1000            }
1001        }
1002        result
1003    }
1004
1005    // ------------------------------------------------------------------
1006    //  F4 / Gröbner support helpers
1007    // ------------------------------------------------------------------
1008
1009    /// Return the exponent vector of the leading monomial, or `None` for zero.
1010    ///
1011    /// This is an alias for [`leading_monomial`](Self::leading_monomial) that
1012    /// matches the Symbolica naming convention used in the F4 algorithm.
1013    #[inline]
1014    pub fn max_exp(&self) -> Option<&SmallVec<[usize; 4]>> {
1015        self.leading_monomial()
1016    }
1017
1018    /// Return the leading coefficient, or `None` for zero.
1019    ///
1020    /// This is an alias for [`leading_coeff`](Self::leading_coeff) that
1021    /// matches the Symbolica naming convention used in the F4 algorithm.
1022    #[inline]
1023    pub fn max_coeff(&self) -> Option<&D::Element> {
1024        self.leading_coeff()
1025    }
1026
1027    /// Iterate over all exponent vectors in sorted order (descending by
1028    /// the monomial ordering).
1029    ///
1030    /// The F4 algorithm uses this to enumerate every monomial in a
1031    /// polynomial for symbolic preprocessing.
1032    pub fn exponents_iter(&self) -> impl Iterator<Item = &SmallVec<[usize; 4]>> {
1033        let mut sorted: Vec<_> = self.terms.keys().collect();
1034        sorted.sort_by(|a, b| self.order.cmp(a, b));
1035        sorted.into_iter()
1036    }
1037
1038    /// Divide every term by the leading coefficient, making the polynomial
1039    /// monic. Returns `false` if the polynomial is zero or the leading
1040    /// coefficient has no inverse.
1041    pub fn make_monic_inplace(&mut self) -> bool {
1042        if self.is_zero() {
1043            return false;
1044        }
1045        let lc = self.leading_coeff().cloned().unwrap();
1046        match self.domain.inv(&lc) {
1047            Some(inv_lc) => {
1048                for coeff in self.terms.values_mut() {
1049                    *coeff = self.domain.mul(coeff, &inv_lc);
1050                }
1051                true
1052            }
1053            None => false,
1054        }
1055    }
1056
1057    /// Create a zero polynomial with the same domain and variable count.
1058    ///
1059    /// This is identical to [`zero`](Self::zero) but named to match the
1060    /// Symbolica convention used in F4 code.
1061    #[inline]
1062    pub fn zero_with_capacity(&self, _cap: usize) -> Self {
1063        self.zero()
1064    }
1065
1066    /// Append a single monomial term `coeff * x^exp`.
1067    ///
1068    /// If the monomial already exists, the coefficients are summed.
1069    /// Zero coefficients remove the term.
1070    pub fn append_monomial(&mut self, coeff: D::Element, exp: &[usize]) {
1071        let key = Self::normalize_exp(exp, self.n_vars);
1072        let existing = self
1073            .terms
1074            .get(&key)
1075            .cloned()
1076            .unwrap_or_else(|| self.domain.zero());
1077        let sum = self.domain.add(&existing, &coeff);
1078        if self.domain.is_zero(&sum) {
1079            self.terms.remove(&key);
1080        } else {
1081            self.terms.insert(key, sum);
1082        }
1083    }
1084
1085    /// Evaluate the polynomial by substituting `value` for variable `var_index`.
1086    ///
1087    /// Returns a polynomial in one fewer variable (all remaining variables
1088    /// keep their relative order). If `var_index` is the only variable, the
1089    /// result is a zero-variable (constant) polynomial.
1090    ///
1091    /// # Example
1092    ///
1093    /// ```
1094    /// use ocas_domain::{Integer, IntegerDomain};
1095    /// use ocas_poly::SparseMultivariatePolynomial;
1096    /// use ocas_poly::Lex;
1097    ///
1098    /// let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1099    ///     IntegerDomain, 2,
1100    ///     vec![
1101    ///         (vec![1, 1], Integer::from(1)), // x*y
1102    ///         (vec![0, 1], Integer::from(2)), // 2*y
1103    ///     ],
1104    /// );
1105    /// // Substitute x=3: result = 3*y + 2*y = 5*y
1106    /// let r = p.eval(0, &Integer::from(3));
1107    /// assert_eq!(r.coeff(&[1]), Integer::from(5));
1108    /// ```
1109    pub fn eval(&self, var_index: usize, value: &D::Element) -> Self {
1110        let new_n_vars = self.n_vars.saturating_sub(1);
1111        let mut result = Self::new(self.domain.clone(), new_n_vars);
1112        for (exp, coeff) in &self.terms {
1113            // Compute coefficient * value^exp[var_index].
1114            let power = self.domain.pow(value, exp[var_index] as u64);
1115            let new_coeff = self.domain.mul(coeff, &power);
1116            if self.domain.is_zero(&new_coeff) {
1117                continue;
1118            }
1119            // Build new exponent vector without var_index.
1120            let mut new_exp = SmallVec::with_capacity(new_n_vars);
1121            for i in 0..self.n_vars {
1122                if i != var_index {
1123                    new_exp.push(exp[i]);
1124                }
1125            }
1126            let existing = result
1127                .terms
1128                .get(&new_exp)
1129                .cloned()
1130                .unwrap_or_else(|| self.domain.zero());
1131            let sum = self.domain.add(&existing, &new_coeff);
1132            if self.domain.is_zero(&sum) {
1133                result.terms.remove(&new_exp);
1134            } else {
1135                result.terms.insert(new_exp, sum);
1136            }
1137        }
1138        result
1139    }
1140}
1141
1142// ------------------------------------------------------------------
1143//  Factorization entry points for sparse multivariate polynomials
1144// ------------------------------------------------------------------
1145
1146impl SparseMultivariatePolynomial<IntegerDomain, Lex> {
1147    /// Factor this bivariate integer polynomial into irreducible factors with
1148    /// multiplicities.
1149    ///
1150    /// With a constant leading coefficient in $x$ the polynomial is treated
1151    /// as univariate in $x$ with coefficients in $\mathbb{Z}[y]$ and factored
1152    /// via Wang's bivariate Hensel-lifting algorithm. With a non-constant
1153    /// leading coefficient the general EEZ path with Wang leading-coefficient
1154    /// imposition (p-adic coefficient Hensel lifting) is used instead.
1155    ///
1156    /// # Example
1157    ///
1158    /// ```
1159    /// use ocas_domain::{Integer, IntegerDomain};
1160    /// use ocas_poly::SparseMultivariatePolynomial;
1161    /// use ocas_poly::Lex;
1162    ///
1163    /// // (x^2 + y + 1)(x + y + 2)
1164    /// let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1165    ///     IntegerDomain, 2,
1166    ///     vec![
1167    ///         (vec![3, 0], Integer::from(1)),
1168    ///         (vec![2, 1], Integer::from(1)),
1169    ///         (vec![2, 0], Integer::from(2)),
1170    ///         (vec![1, 1], Integer::from(1)),
1171    ///         (vec![1, 0], Integer::from(1)),
1172    ///         (vec![0, 2], Integer::from(1)),
1173    ///         (vec![0, 1], Integer::from(3)),
1174    ///         (vec![0, 0], Integer::from(2)),
1175    ///     ],
1176    /// );
1177    /// let factors = f.factor();
1178    /// assert!(factors.len() >= 2);
1179    /// ```
1180    pub fn factor(&self) -> Vec<(Self, usize)> {
1181        if self.n_vars() >= 3 {
1182            crate::factor::eez::multivariate_factor_z(self)
1183        } else if self
1184            .leading_coeff_in(0)
1185            .terms_ref()
1186            .keys()
1187            .any(|e| e.iter().skip(1).any(|&d| d > 0))
1188        {
1189            // Non-constant leading coefficient in x: the bivariate path
1190            // requires a constant LC, so use the EEZ path with Wang
1191            // leading-coefficient imposition.
1192            crate::factor::eez::multivariate_factor_z(self)
1193        } else {
1194            bivariate_factor_z(self, 0, 1)
1195        }
1196    }
1197}
1198
1199impl SparseMultivariatePolynomial<FiniteField, Lex> {
1200    /// Factor this multivariate polynomial over a prime finite field into
1201    /// irreducible factors with multiplicities.
1202    ///
1203    /// Bivariate polynomials use the evaluation–Hensel path; polynomials in
1204    /// three or more variables use EEZ Hensel lifting. Both currently require
1205    /// the leading coefficient in the main variable to be a nonzero field
1206    /// constant.
1207    pub fn factor(&self) -> Vec<(Self, usize)> {
1208        if self.n_vars() >= 3 {
1209            crate::factor::eez::multivariate_factor_fp(self)
1210        } else {
1211            bivariate_factor_fp(self, 0, 1)
1212        }
1213    }
1214}
1215
1216// ------------------------------------------------------------------
1217//  Monomial utilities
1218// ------------------------------------------------------------------
1219
1220/// Check whether monomial `a` divides monomial `b`: `a[i] >= b[i]` for all i.
1221pub fn monomial_divides(a: &[usize], b: &[usize]) -> bool {
1222    a.iter().zip(b.iter()).all(|(x, y)| x >= y)
1223}
1224
1225/// Compute the least common multiple of two monomials: element-wise max.
1226pub fn monomial_lcm(a: &[usize], b: &[usize]) -> SmallVec<[usize; 4]> {
1227    a.iter().zip(b.iter()).map(|(x, y)| *x.max(y)).collect()
1228}
1229
1230/// Return true if the two monomials are coprime (no variable appears in both).
1231pub fn monomial_are_coprime(a: &[usize], b: &[usize]) -> bool {
1232    a.iter().zip(b.iter()).all(|(x, y)| *x == 0 || *y == 0)
1233}
1234
1235/// Binomial coefficient `n choose k`.
1236pub(crate) fn binomial(n: usize, k: usize) -> u64 {
1237    if k > n {
1238        return 0;
1239    }
1240    if k == 0 || k == n {
1241        return 1;
1242    }
1243    let k = k.min(n - k);
1244    let mut num = 1u64;
1245    let mut den = 1u64;
1246    for i in 0..k {
1247        num *= (n - i) as u64;
1248        den *= (i + 1) as u64;
1249    }
1250    num / den
1251}
1252
1253#[cfg(test)]
1254mod tests {
1255    use super::*;
1256    use ocas_domain::{Integer, IntegerDomain, Rational, RationalDomain};
1257
1258    #[test]
1259    fn sparse_create_and_coeff() {
1260        let domain = IntegerDomain;
1261        let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1262            domain,
1263            2,
1264            vec![
1265                (vec![1, 0], Integer::from(2)),
1266                (vec![0, 1], Integer::from(3)),
1267            ],
1268        );
1269        assert_eq!(p.coeff(&[1, 0]), Integer::from(2));
1270        assert_eq!(p.coeff(&[0, 1]), Integer::from(3));
1271        assert_eq!(p.coeff(&[0, 0]), Integer::from(0));
1272    }
1273
1274    #[test]
1275    fn sparse_total_degree() {
1276        let domain = IntegerDomain;
1277        let p = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
1278            domain,
1279            2,
1280            vec![
1281                (vec![2, 1], Integer::from(1)),
1282                (vec![1, 0], Integer::from(1)),
1283            ],
1284        );
1285        assert_eq!(p.total_degree(), Some(3));
1286    }
1287
1288    #[test]
1289    fn sparse_add_and_sub() {
1290        let domain = IntegerDomain;
1291        let a = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1292            domain,
1293            2,
1294            vec![
1295                (vec![1, 0], Integer::from(1)),
1296                (vec![0, 1], Integer::from(2)),
1297            ],
1298        );
1299        let b = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1300            domain,
1301            2,
1302            vec![
1303                (vec![1, 0], Integer::from(3)),
1304                (vec![0, 0], Integer::from(4)),
1305            ],
1306        );
1307        let sum = a.add(&b);
1308        assert_eq!(sum.coeff(&[1, 0]), Integer::from(4));
1309        assert_eq!(sum.coeff(&[0, 1]), Integer::from(2));
1310        assert_eq!(sum.coeff(&[0, 0]), Integer::from(4));
1311
1312        let diff = b.sub(&a);
1313        assert_eq!(diff.coeff(&[1, 0]), Integer::from(2));
1314        assert_eq!(diff.coeff(&[0, 1]), Integer::from(-2));
1315        assert_eq!(diff.coeff(&[0, 0]), Integer::from(4));
1316    }
1317
1318    #[test]
1319    fn sparse_multiplication() {
1320        let domain = RationalDomain;
1321        // (x + 2y) * (3x + y) = 3x^2 + 7xy + 2y^2
1322        let a = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
1323            domain,
1324            2,
1325            vec![
1326                (vec![1, 0], Rational::new(1, 1)),
1327                (vec![0, 1], Rational::new(2, 1)),
1328            ],
1329        );
1330        let b = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
1331            domain,
1332            2,
1333            vec![
1334                (vec![1, 0], Rational::new(3, 1)),
1335                (vec![0, 1], Rational::new(1, 1)),
1336            ],
1337        );
1338        let prod = a.mul(&b);
1339        assert_eq!(prod.coeff(&[2, 0]), Rational::new(3, 1));
1340        assert_eq!(prod.coeff(&[1, 1]), Rational::new(7, 1));
1341        assert_eq!(prod.coeff(&[0, 2]), Rational::new(2, 1));
1342    }
1343
1344    #[test]
1345    fn sparse_sorted_terms_grevlex() {
1346        let domain = IntegerDomain;
1347        let p = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
1348            domain,
1349            2,
1350            vec![
1351                (vec![1, 0], Integer::from(1)),
1352                (vec![2, 0], Integer::from(1)),
1353                (vec![0, 1], Integer::from(1)),
1354            ],
1355        );
1356        let sorted = p.sorted_terms();
1357        let exps: Vec<_> = sorted.into_iter().map(|(e, _)| e.to_vec()).collect();
1358        // Grevlex order for these terms: x^2 (degree 2), x (degree 1), y (degree 1).
1359        // Among degree-1 terms, reverse lex compares the last non-zero exponent:
1360        // y = [0,1] comes before x = [1,0] because 1 > 0 in the last position.
1361        assert_eq!(exps, vec![vec![2, 0], vec![0, 1], vec![1, 0]]);
1362    }
1363}