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