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