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    pub fn new_with_order(domain: D, n_vars: usize, order: O) -> Self {
302        Self {
303            terms: HashMap::default(),
304            domain,
305            n_vars,
306            order,
307        }
308    }
309
310    /// Create a polynomial from a list of (exponent vector, coefficient) pairs.
311    ///
312    /// Zero coefficients and empty terms are dropped automatically.
313    ///
314    /// # Example
315    ///
316    /// ```
317    /// use ocas_domain::{IntegerDomain, Integer};
318    /// use ocas_poly::sparse::Grevlex;
319    /// use ocas_poly::SparseMultivariatePolynomial;
320    ///
321    /// let domain = IntegerDomain;
322    /// let p = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
323    ///     domain,
324    ///     2,
325    ///     vec![(vec![1, 0], Integer::from(2)), (vec![0, 1], Integer::from(3))],
326    /// );
327    /// assert_eq!(p.n_terms(), 2);
328    /// assert_eq!(p.coeff(&[1, 0]), Integer::from(2));
329    /// ```
330    pub fn from_terms(domain: D, n_vars: usize, terms: Vec<(Vec<usize>, D::Element)>) -> Self {
331        let mut poly = Self::new(domain, n_vars);
332        for (exp, coeff) in terms {
333            poly.set_term(exp, coeff);
334        }
335        poly
336    }
337
338    /// Return a reference to the coefficient domain.
339    pub fn domain(&self) -> &D {
340        &self.domain
341    }
342
343    /// Return the number of variables.
344    pub fn n_vars(&self) -> usize {
345        self.n_vars
346    }
347
348    /// Return the number of non-zero terms.
349    pub fn n_terms(&self) -> usize {
350        self.terms.len()
351    }
352
353    /// Return whether this is the zero polynomial.
354    pub fn is_zero(&self) -> bool {
355        self.terms.is_empty()
356    }
357
358    /// Return a reference to the internal term map (exponent → coefficient).
359    pub fn terms_ref(&self) -> &HashMap<SmallVec<[usize; 4]>, D::Element> {
360        &self.terms
361    }
362
363    /// Set the coefficient of a monomial (public version of `set_term`).
364    /// Zero coefficients remove the term.
365    pub fn set_term_external(&mut self, exp: Vec<usize>, coeff: D::Element) {
366        self.set_term(exp, coeff);
367    }
368
369    /// Return the total degree, or `None` for the zero polynomial.
370    pub fn total_degree(&self) -> Option<usize> {
371        self.terms.keys().map(|e| e.iter().sum::<usize>()).max()
372    }
373
374    /// Return the coefficient of the given monomial, or zero if absent.
375    pub fn coeff(&self, exp: &[usize]) -> D::Element {
376        let key = Self::normalize_exp(exp, self.n_vars);
377        self.terms
378            .get(&key)
379            .cloned()
380            .unwrap_or_else(|| self.domain.zero())
381    }
382
383    /// Set the coefficient of a monomial. Zero coefficients remove the term.
384    fn set_term(&mut self, exp: Vec<usize>, coeff: D::Element) {
385        let key = Self::normalize_exp(&exp, self.n_vars);
386        if self.domain.is_zero(&coeff) {
387            self.terms.remove(&key);
388        } else {
389            self.terms.insert(key, coeff);
390        }
391    }
392
393    fn normalize_exp(exp: &[usize], n_vars: usize) -> SmallVec<[usize; 4]> {
394        let mut v = SmallVec::with_capacity(n_vars);
395        for i in 0..n_vars {
396            v.push(*exp.get(i).unwrap_or(&0));
397        }
398        v
399    }
400
401    /// Return the zero polynomial with the same shape.
402    pub fn zero(&self) -> Self {
403        Self::new(self.domain.clone(), self.n_vars)
404    }
405
406    /// Return the constant polynomial `1` over the same shape.
407    pub fn one(&self) -> Self {
408        let mut poly = Self::new(self.domain.clone(), self.n_vars);
409        let mut exp = SmallVec::with_capacity(self.n_vars);
410        exp.resize(self.n_vars, 0);
411        poly.terms.insert(exp, self.domain.one());
412        poly
413    }
414
415    /// Return the negation of this polynomial.
416    pub fn neg(&self) -> Self {
417        let mut poly = self.zero();
418        for (exp, coeff) in &self.terms {
419            poly.terms.insert(exp.clone(), self.domain.neg(coeff));
420        }
421        poly
422    }
423
424    /// Add another polynomial.
425    ///
426    /// Panics if the polynomials have different numbers of variables.
427    pub fn add(&self, other: &Self) -> Self {
428        assert_eq!(
429            self.n_vars, other.n_vars,
430            "polynomials must have the same number of variables"
431        );
432        let mut poly = self.clone();
433        for (exp, coeff) in &other.terms {
434            let existing = poly
435                .terms
436                .get(exp)
437                .cloned()
438                .unwrap_or_else(|| poly.domain.zero());
439            let sum = poly.domain.add(&existing, coeff);
440            if poly.domain.is_zero(&sum) {
441                poly.terms.remove(exp);
442            } else {
443                poly.terms.insert(exp.clone(), sum);
444            }
445        }
446        poly
447    }
448
449    /// Subtract another polynomial.
450    ///
451    /// Panics if the polynomials have different numbers of variables.
452    pub fn sub(&self, other: &Self) -> Self {
453        self.add(&other.neg())
454    }
455
456    /// Multiply by a scalar coefficient.
457    pub fn mul_scalar(&self, scalar: &D::Element) -> Self {
458        if self.domain.is_zero(scalar) {
459            return self.zero();
460        }
461        let mut poly = self.zero();
462        for (exp, coeff) in &self.terms {
463            poly.terms
464                .insert(exp.clone(), self.domain.mul(coeff, scalar));
465        }
466        poly
467    }
468
469    /// Multiply two polynomials.
470    ///
471    /// Panics if the polynomials have different numbers of variables.
472    pub fn mul(&self, other: &Self) -> Self {
473        assert_eq!(
474            self.n_vars, other.n_vars,
475            "polynomials must have the same number of variables"
476        );
477        if self.is_zero() || other.is_zero() {
478            return self.zero();
479        }
480        let mut poly = self.zero();
481        for (e1, c1) in &self.terms {
482            for (e2, c2) in &other.terms {
483                let mut exp = SmallVec::with_capacity(self.n_vars);
484                for i in 0..self.n_vars {
485                    exp.push(e1[i] + e2[i]);
486                }
487                let prod = self.domain.mul(c1, c2);
488                let existing = poly
489                    .terms
490                    .get(&exp)
491                    .cloned()
492                    .unwrap_or_else(|| poly.domain.zero());
493                let sum = poly.domain.add(&existing, &prod);
494                if poly.domain.is_zero(&sum) {
495                    poly.terms.remove(&exp);
496                } else {
497                    poly.terms.insert(exp, sum);
498                }
499            }
500        }
501        poly
502    }
503
504    /// Return the terms sorted according to the monomial ordering.
505    pub fn sorted_terms(&self) -> Vec<(&SmallVec<[usize; 4]>, &D::Element)> {
506        let mut terms: Vec<_> = self.terms.iter().collect();
507        terms.sort_by(|(a, _), (b, _)| self.order.cmp(a, b));
508        terms
509    }
510
511    // ------------------------------------------------------------------
512    //  Gröbner-basis support
513    // ------------------------------------------------------------------
514
515    /// Return the leading term `(exponent_vector, coefficient)` or `None`
516    /// for the zero polynomial.
517    ///
518    /// This scans the HashMap in O(n) without allocating — faster than
519    /// `sorted_terms()` for repeated calls during reduction.
520    pub fn leading_term(&self) -> Option<(&SmallVec<[usize; 4]>, &D::Element)> {
521        self.terms
522            .iter()
523            .max_by(|(a, _), (b, _)| self.order.cmp(a, b))
524    }
525
526    /// Return the leading monomial (exponent vector) or `None`.
527    pub fn leading_monomial(&self) -> Option<&SmallVec<[usize; 4]>> {
528        self.terms.keys().max_by(|a, b| self.order.cmp(a, b))
529    }
530
531    /// Return the leading coefficient or `None`.
532    pub fn leading_coeff(&self) -> Option<&D::Element> {
533        let lm = self.leading_monomial()?;
534        self.terms.get(lm)
535    }
536
537    /// Multiply every term's exponent vector by `exp` element-wise.
538    ///
539    /// Panics if `exp.len() != self.n_vars`.
540    pub fn mul_monomial(&self, exp: &[usize]) -> Self {
541        assert_eq!(
542            exp.len(),
543            self.n_vars,
544            "exponent vector must have length {}",
545            self.n_vars
546        );
547        let mut poly = self.zero();
548        for (e, c) in &self.terms {
549            let mut new_exp = SmallVec::with_capacity(self.n_vars);
550            for i in 0..self.n_vars {
551                new_exp.push(e[i] + exp[i]);
552            }
553            poly.terms.insert(new_exp, c.clone());
554        }
555        poly
556    }
557
558    /// Reduce `self` by the given basis (a list of polynomials).
559    ///
560    /// Implements multivariate polynomial division: repeatedly look for a
561    /// basis element whose leading monomial divides the current leading
562    /// monomial, subtract the appropriate multiple, or else move the leading
563    /// term into the remainder.  Requires that `div` on the domain succeeds
564    /// (i.e. the domain is effectively a field).
565    pub fn reduce(&self, basis: &[Self]) -> Self {
566        let mut remainder = self.clone();
567        let mut result = self.zero();
568
569        // Cache each basis element's leading term.
570        let basis_lts: Vec<_> = basis
571            .iter()
572            .filter_map(|g| g.leading_term().map(|(e, c)| (g, e.clone(), c.clone())))
573            .collect();
574
575        let max_iter = 10000;
576
577        for _ in 0..max_iter {
578            if remainder.is_zero() {
579                break;
580            }
581            let (rm, rc) = match remainder.leading_term() {
582                Some((e, c)) => (e.clone(), c.clone()),
583                None => break,
584            };
585
586            let mut reduced = false;
587            for (g, lm, lc) in &basis_lts {
588                if monomial_divides(&rm, lm) {
589                    let qm: SmallVec<[usize; 4]> =
590                        rm.iter().zip(lm.iter()).map(|(a, b)| a - b).collect();
591                    let qc = match self.domain.div(&rc, lc) {
592                        Some(q) => q,
593                        None => break,
594                    };
595                    let sub = g.mul_monomial(&qm).mul_scalar(&qc);
596                    remainder = remainder.sub(&sub);
597                    reduced = true;
598                    break;
599                }
600            }
601
602            if !reduced {
603                let key = rm;
604                let val = rc;
605                result.terms.insert(key.clone(), val);
606                remainder.terms.remove(&key);
607            }
608        }
609
610        result
611    }
612
613    /// Compute the S-polynomial of `self` and `other`:
614    ///
615    /// S(f, g) = f·lc(g)·x^(lcm-lm(f)) - g·lc(f)·x^(lcm-lm(g))
616    pub fn spoly(&self, other: &Self) -> Self {
617        let (lm_f, lc_f) = match self.leading_term() {
618            Some(t) => (t.0.clone(), t.1.clone()),
619            None => return self.zero(),
620        };
621        let (lm_g, lc_g) = match other.leading_term() {
622            Some(t) => (t.0.clone(), t.1.clone()),
623            None => return self.zero(),
624        };
625
626        let lcm = monomial_lcm(&lm_f, &lm_g);
627
628        let m_f: SmallVec<[usize; 4]> = lcm.iter().zip(lm_f.iter()).map(|(a, b)| a - b).collect();
629        let m_g: SmallVec<[usize; 4]> = lcm.iter().zip(lm_g.iter()).map(|(a, b)| a - b).collect();
630
631        let term1 = self.mul_monomial(&m_f).mul_scalar(&lc_g);
632        let term2 = other.mul_monomial(&m_g).mul_scalar(&lc_f);
633
634        term1.sub(&term2)
635    }
636
637    // ------------------------------------------------------------------
638    //  Multivariate GCD support
639    // ------------------------------------------------------------------
640
641    /// Compute the content: the GCD of all coefficients.
642    ///
643    /// For the zero polynomial the content is zero.
644    ///
645    /// # Example
646    ///
647    /// ```
648    /// use ocas_domain::{Integer, IntegerDomain};
649    /// use ocas_poly::SparseMultivariatePolynomial;
650    /// use ocas_poly::Lex;
651    ///
652    /// let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
653    ///     IntegerDomain, 1,
654    ///     vec![(vec![2], Integer::from(6)), (vec![1], Integer::from(9)), (vec![0], Integer::from(3))],
655    /// );
656    /// assert_eq!(p.content(), Integer::from(3));
657    /// ```
658    pub fn content(&self) -> D::Element
659    where
660        D: EuclideanDomain,
661    {
662        if self.is_zero() {
663            return self.domain.zero();
664        }
665        let mut g = self.domain.zero();
666        for c in self.terms.values() {
667            g = self.domain.gcd(&g, c);
668        }
669        g
670    }
671
672    /// Return the primitive part: `self / content`.
673    ///
674    /// The result has content 1 (or is zero).
675    ///
676    /// # Example
677    ///
678    /// ```
679    /// use ocas_domain::{Integer, IntegerDomain};
680    /// use ocas_poly::SparseMultivariatePolynomial;
681    /// use ocas_poly::Lex;
682    ///
683    /// let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
684    ///     IntegerDomain, 1,
685    ///     vec![(vec![2], Integer::from(6)), (vec![0], Integer::from(3))],
686    /// );
687    /// let pp = p.primitive_part();
688    /// // After dividing by content=3: 2*x^2 + 1
689    /// assert_eq!(pp.coeff(&[2]), Integer::from(2));
690    /// assert_eq!(pp.coeff(&[0]), Integer::from(1));
691    /// ```
692    pub fn primitive_part(&self) -> Self
693    where
694        D: EuclideanDomain,
695    {
696        if self.is_zero() {
697            return self.clone();
698        }
699        let content = self.content();
700        if self.domain.is_one(&content) {
701            return self.clone();
702        }
703        let mut result = self.zero();
704        for (exp, c) in &self.terms {
705            let q = self.domain.div(c, &content).unwrap_or_else(|| c.clone());
706            result.terms.insert(exp.clone(), q);
707        }
708        result
709    }
710
711    /// Divide this polynomial by another, assuming the division is exact
712    /// (no remainder).
713    ///
714    /// Each term of `self` is divided by the corresponding factor from
715    /// `divisor`. This is used in rational-function canonicalization where
716    /// the GCD is known to divide both numerator and denominator.
717    ///
718    /// # Panics
719    ///
720    /// Panics if the division is not exact.
721    pub fn div_exact(&self, divisor: &Self) -> Self {
722        if divisor.n_terms() <= 1 {
723            // Check if divisor is constant 1 (or zero).
724            let const_val = divisor.coeff(&vec![0; divisor.n_vars]);
725            if self.domain.is_one(&const_val) {
726                return self.clone();
727            }
728        }
729        let (quot, rem) = self.div_rem_sparse(divisor);
730        debug_assert!(rem.is_zero(), "div_exact: division had non-zero remainder");
731        quot
732    }
733
734    /// Sparse polynomial long division returning (quotient, remainder).
735    fn div_rem_sparse(&self, divisor: &Self) -> (Self, Self) {
736        if divisor.is_zero() {
737            panic!("division by zero polynomial");
738        }
739        let (_, div_lm) = match divisor.leading_term() {
740            Some(t) => (t.0.clone(), t.1.clone()),
741            None => return (self.zero(), self.clone()),
742        };
743        let div_lc = div_lm;
744        let div_exp = divisor.leading_monomial().unwrap().clone();
745
746        let mut remainder = self.clone();
747        let mut quotient = self.zero();
748
749        while !remainder.is_zero() {
750            let (rem_exp, rem_lc) = match remainder.leading_term() {
751                Some(t) => (t.0.clone(), t.1.clone()),
752                None => break,
753            };
754            // Check if leading monomial of divisor divides leading monomial of remainder.
755            if !monomial_divides(&rem_exp, &div_exp) {
756                break;
757            }
758            let q_coeff = match self.domain.div(&rem_lc, &div_lc) {
759                Some(q) => q,
760                None => break,
761            };
762            let q_exp: SmallVec<[usize; 4]> = rem_exp
763                .iter()
764                .zip(div_exp.iter())
765                .map(|(a, b)| a - b)
766                .collect();
767            // quotient += q_coeff * x^q_exp
768            let existing = quotient
769                .terms
770                .get(&q_exp)
771                .cloned()
772                .unwrap_or_else(|| self.domain.zero());
773            let sum = self.domain.add(&existing, &q_coeff);
774            if self.domain.is_zero(&sum) {
775                quotient.terms.remove(&q_exp);
776            } else {
777                quotient.terms.insert(q_exp, sum);
778            }
779            // remainder -= q_coeff * x^q_exp * divisor
780            let scaled = divisor.mul_monomial(
781                &remainder
782                    .leading_monomial()
783                    .unwrap()
784                    .iter()
785                    .zip(div_exp.iter())
786                    .map(|(a, b)| a - b)
787                    .collect::<SmallVec<[usize; 4]>>(),
788            );
789            let scaled = scaled.mul_scalar(&q_coeff);
790            remainder = remainder.sub(&scaled);
791        }
792        (quotient, remainder)
793    }
794
795    /// Return the degree of this polynomial in the given variable.
796    ///
797    /// Returns 0 for the zero polynomial (by convention) or if the variable
798    /// does not appear.
799    pub fn degree_in(&self, var_index: usize) -> usize {
800        self.terms
801            .keys()
802            .map(|e| e.get(var_index).copied().unwrap_or(0))
803            .max()
804            .unwrap_or(0)
805    }
806
807    // ------------------------------------------------------------------
808    //  Multivariate factorization support
809    // ------------------------------------------------------------------
810
811    /// Return the coefficient polynomial of `x_var^pow`: the sum of all terms
812    /// whose exponent in `var_index` equals `pow`, with that exponent zeroed
813    /// out. The result has the same number of variables and does not depend
814    /// on `var_index`.
815    pub fn coeff_of_var_pow(&self, var_index: usize, pow: usize) -> Self {
816        let mut result = Self::new(self.domain.clone(), self.n_vars);
817        for (exp, coeff) in &self.terms {
818            if exp.get(var_index).copied().unwrap_or(0) == pow {
819                let mut new_exp = exp.clone();
820                if var_index < new_exp.len() {
821                    new_exp[var_index] = 0;
822                }
823                result.terms.insert(new_exp, coeff.clone());
824            }
825        }
826        result
827    }
828
829    /// Return the leading coefficient when this polynomial is viewed as a
830    /// univariate polynomial in `var_index`. The result is a polynomial in
831    /// the remaining variables (same `n_vars`, exponent of `var_index` is 0).
832    pub fn leading_coeff_in(&self, var_index: usize) -> Self {
833        self.coeff_of_var_pow(var_index, self.degree_in(var_index))
834    }
835
836    /// Compute the formal partial derivative with respect to `var_index`.
837    pub fn derivative(&self, var_index: usize) -> Self {
838        let mut result = Self::new(self.domain.clone(), self.n_vars);
839        for (exp, coeff) in &self.terms {
840            let power = exp.get(var_index).copied().unwrap_or(0);
841            if power == 0 {
842                continue;
843            }
844            let mut new_exp = exp.clone();
845            new_exp[var_index] = power - 1;
846            let scalar = self.domain.cast_u64(power as u64);
847            let new_coeff = self.domain.mul(coeff, &scalar);
848            let existing = result
849                .terms
850                .get(&new_exp)
851                .cloned()
852                .unwrap_or_else(|| self.domain.zero());
853            let sum = self.domain.add(&existing, &new_coeff);
854            if self.domain.is_zero(&sum) {
855                result.terms.remove(&new_exp);
856            } else {
857                result.terms.insert(new_exp, sum);
858            }
859        }
860        result
861    }
862
863    /// Compute the Taylor coefficients in variable `var_index` around `a`:
864    /// `f = Σ_j t_j · (x_var - a)^j` where each `t_j` does not depend on
865    /// `var_index` (its exponent is zeroed).
866    ///
867    /// Returns `t_0, t_1, ..., t_d` with `d = degree_in(var_index)`.
868    pub fn taylor_coefficients(&self, var_index: usize, a: &D::Element) -> Vec<Self> {
869        let d = self.degree_in(var_index);
870        let mut coeffs = vec![Self::new(self.domain.clone(), self.n_vars); d + 1];
871        for (exp, coeff) in &self.terms {
872            let e = exp.get(var_index).copied().unwrap_or(0);
873            let mut base_exp = exp.clone();
874            if var_index < base_exp.len() {
875                base_exp[var_index] = 0;
876            }
877            // x_v^e = Σ_j binom(e, j) · a^(e-j) · (x_v - a)^j
878            for (j, t_j) in coeffs.iter_mut().enumerate().take(e + 1) {
879                let binom = self.domain.cast_u64(binomial(e, j));
880                let a_pow = self.domain.pow(a, (e - j) as u64);
881                let contrib = self.domain.mul(coeff, &self.domain.mul(&binom, &a_pow));
882                let existing = t_j
883                    .terms
884                    .get(&base_exp)
885                    .cloned()
886                    .unwrap_or_else(|| self.domain.zero());
887                let sum = self.domain.add(&existing, &contrib);
888                if self.domain.is_zero(&sum) {
889                    t_j.terms.remove(&base_exp);
890                } else {
891                    t_j.terms.insert(base_exp.clone(), sum);
892                }
893            }
894        }
895        coeffs
896    }
897
898    /// Drop variable 0, which must not occur in any term. Returns a
899    /// polynomial in `n_vars - 1` variables with indices shifted down.
900    ///
901    /// Panics in debug builds if variable 0 occurs with a non-zero exponent.
902    pub fn drop_main_var(&self) -> Self {
903        debug_assert!(
904            self.terms_ref()
905                .keys()
906                .all(|e| e.first().copied().unwrap_or(0) == 0),
907            "drop_main_var: variable 0 must not occur"
908        );
909        let new_n_vars = self.n_vars.saturating_sub(1);
910        let mut result = Self::new(self.domain.clone(), new_n_vars);
911        for (exp, coeff) in &self.terms {
912            if exp.first().copied().unwrap_or(0) != 0 {
913                continue;
914            }
915            let new_exp: SmallVec<[usize; 4]> = exp.iter().skip(1).copied().collect();
916            result.terms.insert(new_exp, coeff.clone());
917        }
918        result
919    }
920
921    /// Embed into one more variable by inserting a new variable 0 with
922    /// exponent 0 (all existing variable indices shift up by one).
923    pub fn embed_new_main(&self) -> Self {
924        let new_n_vars = self.n_vars + 1;
925        let mut result = Self::new(self.domain.clone(), new_n_vars);
926        for (exp, coeff) in &self.terms {
927            let mut new_exp = SmallVec::with_capacity(new_n_vars);
928            new_exp.push(0);
929            new_exp.extend(exp.iter().copied());
930            result.terms.insert(new_exp, coeff.clone());
931        }
932        result
933    }
934
935    /// Permute variables: the result's exponent at position `i` is the old
936    /// exponent at position `perm[i]`. `perm` must be a permutation of
937    /// `0..n_vars`.
938    pub fn permute_variables(&self, perm: &[usize]) -> Self {
939        assert_eq!(perm.len(), self.n_vars, "perm must be a permutation");
940        let mut result = Self::new(self.domain.clone(), self.n_vars);
941        for (exp, coeff) in &self.terms {
942            let mut new_exp = SmallVec::with_capacity(self.n_vars);
943            for &p in perm {
944                new_exp.push(exp.get(p).copied().unwrap_or(0));
945            }
946            result.terms.insert(new_exp, coeff.clone());
947        }
948        result
949    }
950
951    /// Divide this polynomial by `divisor`, returning the quotient only if
952    /// the division is exact (zero remainder).
953    pub fn checked_div_exact(&self, divisor: &Self) -> Option<Self> {
954        if divisor.is_zero() {
955            return None;
956        }
957        let (quot, rem) = self.div_rem_sparse(divisor);
958        if rem.is_zero() { Some(quot) } else { None }
959    }
960
961    /// Evaluate variable `var_index` at `value` while keeping the total
962    /// number of variables unchanged (the variable disappears from the
963    /// support but all indices are preserved).
964    ///
965    /// This is the substitution used by multivariate Hensel lifting, where
966    /// variable positions must stay fixed across recursion levels.
967    pub fn eval_keep(&self, var_index: usize, value: &D::Element) -> Self {
968        let mut result = Self::new(self.domain.clone(), self.n_vars);
969        for (exp, coeff) in &self.terms {
970            let power = self.domain.pow(value, exp[var_index] as u64);
971            let new_coeff = self.domain.mul(coeff, &power);
972            if self.domain.is_zero(&new_coeff) {
973                continue;
974            }
975            let mut new_exp = exp.clone();
976            new_exp[var_index] = 0;
977            let existing = result
978                .terms
979                .get(&new_exp)
980                .cloned()
981                .unwrap_or_else(|| self.domain.zero());
982            let sum = self.domain.add(&existing, &new_coeff);
983            if self.domain.is_zero(&sum) {
984                result.terms.remove(&new_exp);
985            } else {
986                result.terms.insert(new_exp, sum);
987            }
988        }
989        result
990    }
991
992    // ------------------------------------------------------------------
993    //  F4 / Gröbner support helpers
994    // ------------------------------------------------------------------
995
996    /// Return the exponent vector of the leading monomial, or `None` for zero.
997    ///
998    /// This is an alias for [`leading_monomial`](Self::leading_monomial) that
999    /// matches the Symbolica naming convention used in the F4 algorithm.
1000    #[inline]
1001    pub fn max_exp(&self) -> Option<&SmallVec<[usize; 4]>> {
1002        self.leading_monomial()
1003    }
1004
1005    /// Return the leading coefficient, or `None` for zero.
1006    ///
1007    /// This is an alias for [`leading_coeff`](Self::leading_coeff) that
1008    /// matches the Symbolica naming convention used in the F4 algorithm.
1009    #[inline]
1010    pub fn max_coeff(&self) -> Option<&D::Element> {
1011        self.leading_coeff()
1012    }
1013
1014    /// Iterate over all exponent vectors in sorted order (descending by
1015    /// the monomial ordering).
1016    ///
1017    /// The F4 algorithm uses this to enumerate every monomial in a
1018    /// polynomial for symbolic preprocessing.
1019    pub fn exponents_iter(&self) -> impl Iterator<Item = &SmallVec<[usize; 4]>> {
1020        let mut sorted: Vec<_> = self.terms.keys().collect();
1021        sorted.sort_by(|a, b| self.order.cmp(a, b));
1022        sorted.into_iter()
1023    }
1024
1025    /// Divide every term by the leading coefficient, making the polynomial
1026    /// monic. Returns `false` if the polynomial is zero or the leading
1027    /// coefficient has no inverse.
1028    pub fn make_monic_inplace(&mut self) -> bool {
1029        if self.is_zero() {
1030            return false;
1031        }
1032        let lc = self.leading_coeff().cloned().unwrap();
1033        match self.domain.inv(&lc) {
1034            Some(inv_lc) => {
1035                for coeff in self.terms.values_mut() {
1036                    *coeff = self.domain.mul(coeff, &inv_lc);
1037                }
1038                true
1039            }
1040            None => false,
1041        }
1042    }
1043
1044    /// Create a zero polynomial with the same domain and variable count.
1045    ///
1046    /// This is identical to [`zero`](Self::zero) but named to match the
1047    /// Symbolica convention used in F4 code.
1048    #[inline]
1049    pub fn zero_with_capacity(&self, _cap: usize) -> Self {
1050        self.zero()
1051    }
1052
1053    /// Append a single monomial term `coeff * x^exp`.
1054    ///
1055    /// If the monomial already exists, the coefficients are summed.
1056    /// Zero coefficients remove the term.
1057    pub fn append_monomial(&mut self, coeff: D::Element, exp: &[usize]) {
1058        let key = Self::normalize_exp(exp, self.n_vars);
1059        let existing = self
1060            .terms
1061            .get(&key)
1062            .cloned()
1063            .unwrap_or_else(|| self.domain.zero());
1064        let sum = self.domain.add(&existing, &coeff);
1065        if self.domain.is_zero(&sum) {
1066            self.terms.remove(&key);
1067        } else {
1068            self.terms.insert(key, sum);
1069        }
1070    }
1071
1072    /// Evaluate the polynomial by substituting `value` for variable `var_index`.
1073    ///
1074    /// Returns a polynomial in one fewer variable (all remaining variables
1075    /// keep their relative order). If `var_index` is the only variable, the
1076    /// result is a zero-variable (constant) polynomial.
1077    ///
1078    /// # Example
1079    ///
1080    /// ```
1081    /// use ocas_domain::{Integer, IntegerDomain};
1082    /// use ocas_poly::SparseMultivariatePolynomial;
1083    /// use ocas_poly::Lex;
1084    ///
1085    /// let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1086    ///     IntegerDomain, 2,
1087    ///     vec![
1088    ///         (vec![1, 1], Integer::from(1)), // x*y
1089    ///         (vec![0, 1], Integer::from(2)), // 2*y
1090    ///     ],
1091    /// );
1092    /// // Substitute x=3: result = 3*y + 2*y = 5*y
1093    /// let r = p.eval(0, &Integer::from(3));
1094    /// assert_eq!(r.coeff(&[1]), Integer::from(5));
1095    /// ```
1096    pub fn eval(&self, var_index: usize, value: &D::Element) -> Self {
1097        let new_n_vars = self.n_vars.saturating_sub(1);
1098        let mut result = Self::new(self.domain.clone(), new_n_vars);
1099        for (exp, coeff) in &self.terms {
1100            // Compute coefficient * value^exp[var_index].
1101            let power = self.domain.pow(value, exp[var_index] as u64);
1102            let new_coeff = self.domain.mul(coeff, &power);
1103            if self.domain.is_zero(&new_coeff) {
1104                continue;
1105            }
1106            // Build new exponent vector without var_index.
1107            let mut new_exp = SmallVec::with_capacity(new_n_vars);
1108            for i in 0..self.n_vars {
1109                if i != var_index {
1110                    new_exp.push(exp[i]);
1111                }
1112            }
1113            let existing = result
1114                .terms
1115                .get(&new_exp)
1116                .cloned()
1117                .unwrap_or_else(|| self.domain.zero());
1118            let sum = self.domain.add(&existing, &new_coeff);
1119            if self.domain.is_zero(&sum) {
1120                result.terms.remove(&new_exp);
1121            } else {
1122                result.terms.insert(new_exp, sum);
1123            }
1124        }
1125        result
1126    }
1127}
1128
1129// ------------------------------------------------------------------
1130//  Factorization entry points for sparse multivariate polynomials
1131// ------------------------------------------------------------------
1132
1133impl SparseMultivariatePolynomial<IntegerDomain, Lex> {
1134    /// Factor this bivariate integer polynomial into irreducible factors with
1135    /// multiplicities.
1136    ///
1137    /// With a constant leading coefficient in $x$ the polynomial is treated
1138    /// as univariate in $x$ with coefficients in $\mathbb{Z}[y]$ and factored
1139    /// via Wang's bivariate Hensel-lifting algorithm. With a non-constant
1140    /// leading coefficient the general EEZ path with Wang leading-coefficient
1141    /// imposition (p-adic coefficient Hensel lifting) is used instead.
1142    ///
1143    /// # Example
1144    ///
1145    /// ```
1146    /// use ocas_domain::{Integer, IntegerDomain};
1147    /// use ocas_poly::SparseMultivariatePolynomial;
1148    /// use ocas_poly::Lex;
1149    ///
1150    /// // (x^2 + y + 1)(x + y + 2)
1151    /// let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1152    ///     IntegerDomain, 2,
1153    ///     vec![
1154    ///         (vec![3, 0], Integer::from(1)),
1155    ///         (vec![2, 1], Integer::from(1)),
1156    ///         (vec![2, 0], Integer::from(2)),
1157    ///         (vec![1, 1], Integer::from(1)),
1158    ///         (vec![1, 0], Integer::from(1)),
1159    ///         (vec![0, 2], Integer::from(1)),
1160    ///         (vec![0, 1], Integer::from(3)),
1161    ///         (vec![0, 0], Integer::from(2)),
1162    ///     ],
1163    /// );
1164    /// let factors = f.factor();
1165    /// assert!(factors.len() >= 2);
1166    /// ```
1167    pub fn factor(&self) -> Vec<(Self, usize)> {
1168        if self.n_vars() >= 3 {
1169            crate::factor::eez::multivariate_factor_z(self)
1170        } else if self
1171            .leading_coeff_in(0)
1172            .terms_ref()
1173            .keys()
1174            .any(|e| e.iter().skip(1).any(|&d| d > 0))
1175        {
1176            // Non-constant leading coefficient in x: the bivariate path
1177            // requires a constant LC, so use the EEZ path with Wang
1178            // leading-coefficient imposition.
1179            crate::factor::eez::multivariate_factor_z(self)
1180        } else {
1181            bivariate_factor_z(self, 0, 1)
1182        }
1183    }
1184}
1185
1186impl SparseMultivariatePolynomial<FiniteField, Lex> {
1187    /// Factor this multivariate polynomial over a prime finite field into
1188    /// irreducible factors with multiplicities.
1189    ///
1190    /// Bivariate polynomials use the evaluation–Hensel path; polynomials in
1191    /// three or more variables use EEZ Hensel lifting. Both currently require
1192    /// the leading coefficient in the main variable to be a nonzero field
1193    /// constant.
1194    pub fn factor(&self) -> Vec<(Self, usize)> {
1195        if self.n_vars() >= 3 {
1196            crate::factor::eez::multivariate_factor_fp(self)
1197        } else {
1198            bivariate_factor_fp(self, 0, 1)
1199        }
1200    }
1201}
1202
1203// ------------------------------------------------------------------
1204//  Monomial utilities
1205// ------------------------------------------------------------------
1206
1207/// Check whether monomial `a` divides monomial `b`: `a[i] >= b[i]` for all i.
1208pub fn monomial_divides(a: &[usize], b: &[usize]) -> bool {
1209    a.iter().zip(b.iter()).all(|(x, y)| x >= y)
1210}
1211
1212/// Compute the least common multiple of two monomials: element-wise max.
1213pub fn monomial_lcm(a: &[usize], b: &[usize]) -> SmallVec<[usize; 4]> {
1214    a.iter().zip(b.iter()).map(|(x, y)| *x.max(y)).collect()
1215}
1216
1217/// Return true if the two monomials are coprime (no variable appears in both).
1218pub fn monomial_are_coprime(a: &[usize], b: &[usize]) -> bool {
1219    a.iter().zip(b.iter()).all(|(x, y)| *x == 0 || *y == 0)
1220}
1221
1222/// Binomial coefficient `n choose k`.
1223pub(crate) fn binomial(n: usize, k: usize) -> u64 {
1224    if k > n {
1225        return 0;
1226    }
1227    if k == 0 || k == n {
1228        return 1;
1229    }
1230    let k = k.min(n - k);
1231    let mut num = 1u64;
1232    let mut den = 1u64;
1233    for i in 0..k {
1234        num *= (n - i) as u64;
1235        den *= (i + 1) as u64;
1236    }
1237    num / den
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242    use super::*;
1243    use ocas_domain::{Integer, IntegerDomain, Rational, RationalDomain};
1244
1245    #[test]
1246    fn sparse_create_and_coeff() {
1247        let domain = IntegerDomain;
1248        let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1249            domain,
1250            2,
1251            vec![
1252                (vec![1, 0], Integer::from(2)),
1253                (vec![0, 1], Integer::from(3)),
1254            ],
1255        );
1256        assert_eq!(p.coeff(&[1, 0]), Integer::from(2));
1257        assert_eq!(p.coeff(&[0, 1]), Integer::from(3));
1258        assert_eq!(p.coeff(&[0, 0]), Integer::from(0));
1259    }
1260
1261    #[test]
1262    fn sparse_total_degree() {
1263        let domain = IntegerDomain;
1264        let p = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
1265            domain,
1266            2,
1267            vec![
1268                (vec![2, 1], Integer::from(1)),
1269                (vec![1, 0], Integer::from(1)),
1270            ],
1271        );
1272        assert_eq!(p.total_degree(), Some(3));
1273    }
1274
1275    #[test]
1276    fn sparse_add_and_sub() {
1277        let domain = IntegerDomain;
1278        let a = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1279            domain,
1280            2,
1281            vec![
1282                (vec![1, 0], Integer::from(1)),
1283                (vec![0, 1], Integer::from(2)),
1284            ],
1285        );
1286        let b = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1287            domain,
1288            2,
1289            vec![
1290                (vec![1, 0], Integer::from(3)),
1291                (vec![0, 0], Integer::from(4)),
1292            ],
1293        );
1294        let sum = a.add(&b);
1295        assert_eq!(sum.coeff(&[1, 0]), Integer::from(4));
1296        assert_eq!(sum.coeff(&[0, 1]), Integer::from(2));
1297        assert_eq!(sum.coeff(&[0, 0]), Integer::from(4));
1298
1299        let diff = b.sub(&a);
1300        assert_eq!(diff.coeff(&[1, 0]), Integer::from(2));
1301        assert_eq!(diff.coeff(&[0, 1]), Integer::from(-2));
1302        assert_eq!(diff.coeff(&[0, 0]), Integer::from(4));
1303    }
1304
1305    #[test]
1306    fn sparse_multiplication() {
1307        let domain = RationalDomain;
1308        // (x + 2y) * (3x + y) = 3x^2 + 7xy + 2y^2
1309        let a = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
1310            domain,
1311            2,
1312            vec![
1313                (vec![1, 0], Rational::new(1, 1)),
1314                (vec![0, 1], Rational::new(2, 1)),
1315            ],
1316        );
1317        let b = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
1318            domain,
1319            2,
1320            vec![
1321                (vec![1, 0], Rational::new(3, 1)),
1322                (vec![0, 1], Rational::new(1, 1)),
1323            ],
1324        );
1325        let prod = a.mul(&b);
1326        assert_eq!(prod.coeff(&[2, 0]), Rational::new(3, 1));
1327        assert_eq!(prod.coeff(&[1, 1]), Rational::new(7, 1));
1328        assert_eq!(prod.coeff(&[0, 2]), Rational::new(2, 1));
1329    }
1330
1331    #[test]
1332    fn sparse_sorted_terms_grevlex() {
1333        let domain = IntegerDomain;
1334        let p = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
1335            domain,
1336            2,
1337            vec![
1338                (vec![1, 0], Integer::from(1)),
1339                (vec![2, 0], Integer::from(1)),
1340                (vec![0, 1], Integer::from(1)),
1341            ],
1342        );
1343        let sorted = p.sorted_terms();
1344        let exps: Vec<_> = sorted.into_iter().map(|(e, _)| e.to_vec()).collect();
1345        // Grevlex order for these terms: x^2 (degree 2), x (degree 1), y (degree 1).
1346        // Among degree-1 terms, reverse lex compares the last non-zero exponent:
1347        // y = [0,1] comes before x = [1,0] because 1 > 0 in the last position.
1348        assert_eq!(exps, vec![vec![2, 0], vec![0, 1], vec![1, 0]]);
1349    }
1350}