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 std::collections::HashMap;
9use std::marker::PhantomData;
10
11use ocas_domain::{Domain, EuclideanDomain, FiniteField, IntegerDomain};
12use smallvec::SmallVec;
13
14use crate::factor::multivariate::{bivariate_factor_fp, bivariate_factor_z};
15
16/// A monomial ordering determines how terms are sorted and compared.
17///
18/// Orderings are implemented as zero-sized types with an associated method
19/// that compares two exponent vectors.
20///
21/// # Example
22///
23/// ```
24/// use ocas_poly::sparse::{Grevlex, Lex, MonomialOrder};
25///
26/// let a = [2, 1];
27/// let b = [1, 1];
28/// assert_eq!(Lex::cmp(&a, &b), std::cmp::Ordering::Greater);
29/// assert_eq!(Grevlex::cmp(&a, &b), std::cmp::Ordering::Less);
30/// ```
31pub trait MonomialOrder: Clone + Copy + PartialEq + Eq + std::fmt::Debug {
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(lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering;
37}
38
39/// Lexicographic ordering: compare exponents left-to-right.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct Lex;
42
43impl MonomialOrder for Lex {
44    fn cmp(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.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct Grevlex;
53
54impl MonomialOrder for Grevlex {
55    fn cmp(lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering {
56        let deg_lhs: usize = lhs.iter().sum();
57        let deg_rhs: usize = rhs.iter().sum();
58        deg_rhs
59            .cmp(&deg_lhs)
60            .then_with(|| rhs.iter().rev().cmp(lhs.iter().rev()))
61    }
62}
63
64/// Graded lexicographic ordering: first by total degree descending,
65/// then lexicographic.
66///
67/// Grlex is sometimes preferred over grevlex in Gröbner basis computations
68/// because it can lead to smaller intermediate matrices in the F4 algorithm.
69///
70/// # Example
71///
72/// ```
73/// use ocas_poly::sparse::{Grlex, MonomialOrder};
74///
75/// let a = [2, 0]; // x^2, degree 2
76/// let b = [1, 1]; // x*y, degree 2
77/// let c = [0, 3]; // y^3, degree 3
78/// // c has highest degree, so it comes first
79/// assert_eq!(Grlex::cmp(&c, &a), std::cmp::Ordering::Less);
80/// // a and b have same degree; a > b lexicographically
81/// assert_eq!(Grlex::cmp(&a, &b), std::cmp::Ordering::Greater);
82/// ```
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct Grlex;
85
86impl MonomialOrder for Grlex {
87    fn cmp(lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering {
88        let deg_lhs: usize = lhs.iter().sum();
89        let deg_rhs: usize = rhs.iter().sum();
90        deg_rhs.cmp(&deg_lhs).then_with(|| lhs.cmp(rhs))
91    }
92}
93
94/// A sparse multivariate polynomial with coefficients in a domain `D` and
95/// monomial ordering `O`.
96///
97/// # Example
98///
99/// ```
100/// use ocas_domain::{IntegerDomain, Integer};
101/// use ocas_poly::sparse::Grevlex;
102/// use ocas_poly::SparseMultivariatePolynomial;
103///
104/// let domain = IntegerDomain;
105/// let p = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
106///     domain,
107///     2,
108///     vec![(vec![1, 0], Integer::from(2)), (vec![0, 1], Integer::from(3))],
109/// );
110/// let q = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
111///     domain,
112///     2,
113///     vec![(vec![1, 0], Integer::from(1)), (vec![0, 0], Integer::from(1))],
114/// );
115/// let r = p.mul(&q);
116/// assert_eq!(r.coeff(&[1, 0]), Integer::from(2));
117/// assert_eq!(r.coeff(&[0, 1]), Integer::from(3));
118/// assert_eq!(r.coeff(&[2, 0]), Integer::from(2));
119/// ```
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct SparseMultivariatePolynomial<D: Domain, O: MonomialOrder = Grevlex> {
122    /// Non-zero terms indexed by exponent vector.
123    terms: HashMap<SmallVec<[usize; 4]>, D::Element>,
124    /// The coefficient domain.
125    domain: D,
126    /// Number of variables. Exponent vectors are padded/trimmed to this length.
127    n_vars: usize,
128    _marker: PhantomData<O>,
129}
130
131impl<D: Domain, O: MonomialOrder> SparseMultivariatePolynomial<D, O> {
132    /// Create the zero polynomial in `n_vars` variables over `domain`.
133    pub fn new(domain: D, n_vars: usize) -> Self {
134        Self {
135            terms: HashMap::new(),
136            domain,
137            n_vars,
138            _marker: PhantomData,
139        }
140    }
141
142    /// Create a polynomial from a list of (exponent vector, coefficient) pairs.
143    ///
144    /// Zero coefficients and empty terms are dropped automatically.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use ocas_domain::{IntegerDomain, Integer};
150    /// use ocas_poly::sparse::Grevlex;
151    /// use ocas_poly::SparseMultivariatePolynomial;
152    ///
153    /// let domain = IntegerDomain;
154    /// let p = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
155    ///     domain,
156    ///     2,
157    ///     vec![(vec![1, 0], Integer::from(2)), (vec![0, 1], Integer::from(3))],
158    /// );
159    /// assert_eq!(p.n_terms(), 2);
160    /// assert_eq!(p.coeff(&[1, 0]), Integer::from(2));
161    /// ```
162    pub fn from_terms(domain: D, n_vars: usize, terms: Vec<(Vec<usize>, D::Element)>) -> Self {
163        let mut poly = Self::new(domain, n_vars);
164        for (exp, coeff) in terms {
165            poly.set_term(exp, coeff);
166        }
167        poly
168    }
169
170    /// Return a reference to the coefficient domain.
171    pub fn domain(&self) -> &D {
172        &self.domain
173    }
174
175    /// Return the number of variables.
176    pub fn n_vars(&self) -> usize {
177        self.n_vars
178    }
179
180    /// Return the number of non-zero terms.
181    pub fn n_terms(&self) -> usize {
182        self.terms.len()
183    }
184
185    /// Return whether this is the zero polynomial.
186    pub fn is_zero(&self) -> bool {
187        self.terms.is_empty()
188    }
189
190    /// Return a reference to the internal term map (exponent → coefficient).
191    pub fn terms_ref(&self) -> &HashMap<SmallVec<[usize; 4]>, D::Element> {
192        &self.terms
193    }
194
195    /// Set the coefficient of a monomial (public version of `set_term`).
196    /// Zero coefficients remove the term.
197    pub fn set_term_external(&mut self, exp: Vec<usize>, coeff: D::Element) {
198        self.set_term(exp, coeff);
199    }
200
201    /// Return the total degree, or `None` for the zero polynomial.
202    pub fn total_degree(&self) -> Option<usize> {
203        self.terms.keys().map(|e| e.iter().sum::<usize>()).max()
204    }
205
206    /// Return the coefficient of the given monomial, or zero if absent.
207    pub fn coeff(&self, exp: &[usize]) -> D::Element {
208        let key = Self::normalize_exp(exp, self.n_vars);
209        self.terms
210            .get(&key)
211            .cloned()
212            .unwrap_or_else(|| self.domain.zero())
213    }
214
215    /// Set the coefficient of a monomial. Zero coefficients remove the term.
216    fn set_term(&mut self, exp: Vec<usize>, coeff: D::Element) {
217        let key = Self::normalize_exp(&exp, self.n_vars);
218        if self.domain.is_zero(&coeff) {
219            self.terms.remove(&key);
220        } else {
221            self.terms.insert(key, coeff);
222        }
223    }
224
225    fn normalize_exp(exp: &[usize], n_vars: usize) -> SmallVec<[usize; 4]> {
226        let mut v = SmallVec::with_capacity(n_vars);
227        for i in 0..n_vars {
228            v.push(*exp.get(i).unwrap_or(&0));
229        }
230        v
231    }
232
233    /// Return the zero polynomial with the same shape.
234    pub fn zero(&self) -> Self {
235        Self::new(self.domain.clone(), self.n_vars)
236    }
237
238    /// Return the constant polynomial `1` over the same shape.
239    pub fn one(&self) -> Self {
240        let mut poly = Self::new(self.domain.clone(), self.n_vars);
241        let mut exp = SmallVec::with_capacity(self.n_vars);
242        exp.resize(self.n_vars, 0);
243        poly.terms.insert(exp, self.domain.one());
244        poly
245    }
246
247    /// Return the negation of this polynomial.
248    pub fn neg(&self) -> Self {
249        let mut poly = self.zero();
250        for (exp, coeff) in &self.terms {
251            poly.terms.insert(exp.clone(), self.domain.neg(coeff));
252        }
253        poly
254    }
255
256    /// Add another polynomial.
257    ///
258    /// Panics if the polynomials have different numbers of variables.
259    pub fn add(&self, other: &Self) -> Self {
260        assert_eq!(
261            self.n_vars, other.n_vars,
262            "polynomials must have the same number of variables"
263        );
264        let mut poly = self.clone();
265        for (exp, coeff) in &other.terms {
266            let existing = poly
267                .terms
268                .get(exp)
269                .cloned()
270                .unwrap_or_else(|| poly.domain.zero());
271            let sum = poly.domain.add(&existing, coeff);
272            if poly.domain.is_zero(&sum) {
273                poly.terms.remove(exp);
274            } else {
275                poly.terms.insert(exp.clone(), sum);
276            }
277        }
278        poly
279    }
280
281    /// Subtract another polynomial.
282    ///
283    /// Panics if the polynomials have different numbers of variables.
284    pub fn sub(&self, other: &Self) -> Self {
285        self.add(&other.neg())
286    }
287
288    /// Multiply by a scalar coefficient.
289    pub fn mul_scalar(&self, scalar: &D::Element) -> Self {
290        if self.domain.is_zero(scalar) {
291            return self.zero();
292        }
293        let mut poly = self.zero();
294        for (exp, coeff) in &self.terms {
295            poly.terms
296                .insert(exp.clone(), self.domain.mul(coeff, scalar));
297        }
298        poly
299    }
300
301    /// Multiply two polynomials.
302    ///
303    /// Panics if the polynomials have different numbers of variables.
304    pub fn mul(&self, other: &Self) -> Self {
305        assert_eq!(
306            self.n_vars, other.n_vars,
307            "polynomials must have the same number of variables"
308        );
309        if self.is_zero() || other.is_zero() {
310            return self.zero();
311        }
312        let mut poly = self.zero();
313        for (e1, c1) in &self.terms {
314            for (e2, c2) in &other.terms {
315                let mut exp = SmallVec::with_capacity(self.n_vars);
316                for i in 0..self.n_vars {
317                    exp.push(e1[i] + e2[i]);
318                }
319                let prod = self.domain.mul(c1, c2);
320                let existing = poly
321                    .terms
322                    .get(&exp)
323                    .cloned()
324                    .unwrap_or_else(|| poly.domain.zero());
325                let sum = poly.domain.add(&existing, &prod);
326                if poly.domain.is_zero(&sum) {
327                    poly.terms.remove(&exp);
328                } else {
329                    poly.terms.insert(exp, sum);
330                }
331            }
332        }
333        poly
334    }
335
336    /// Return the terms sorted according to the monomial ordering.
337    pub fn sorted_terms(&self) -> Vec<(&SmallVec<[usize; 4]>, &D::Element)> {
338        let mut terms: Vec<_> = self.terms.iter().collect();
339        terms.sort_by(|(a, _), (b, _)| O::cmp(a, b));
340        terms
341    }
342
343    // ------------------------------------------------------------------
344    //  Gröbner-basis support
345    // ------------------------------------------------------------------
346
347    /// Return the leading term `(exponent_vector, coefficient)` or `None`
348    /// for the zero polynomial.
349    ///
350    /// This scans the HashMap in O(n) without allocating — faster than
351    /// `sorted_terms()` for repeated calls during reduction.
352    pub fn leading_term(&self) -> Option<(&SmallVec<[usize; 4]>, &D::Element)> {
353        self.terms.iter().max_by(|(a, _), (b, _)| O::cmp(a, b))
354    }
355
356    /// Return the leading monomial (exponent vector) or `None`.
357    pub fn leading_monomial(&self) -> Option<&SmallVec<[usize; 4]>> {
358        self.terms.keys().max_by(|a, b| O::cmp(a, b))
359    }
360
361    /// Return the leading coefficient or `None`.
362    pub fn leading_coeff(&self) -> Option<&D::Element> {
363        let lm = self.leading_monomial()?;
364        self.terms.get(lm)
365    }
366
367    /// Multiply every term's exponent vector by `exp` element-wise.
368    ///
369    /// Panics if `exp.len() != self.n_vars`.
370    pub fn mul_monomial(&self, exp: &[usize]) -> Self {
371        assert_eq!(
372            exp.len(),
373            self.n_vars,
374            "exponent vector must have length {}",
375            self.n_vars
376        );
377        let mut poly = self.zero();
378        for (e, c) in &self.terms {
379            let mut new_exp = SmallVec::with_capacity(self.n_vars);
380            for i in 0..self.n_vars {
381                new_exp.push(e[i] + exp[i]);
382            }
383            poly.terms.insert(new_exp, c.clone());
384        }
385        poly
386    }
387
388    /// Reduce `self` by the given basis (a list of polynomials).
389    ///
390    /// Implements multivariate polynomial division: repeatedly look for a
391    /// basis element whose leading monomial divides the current leading
392    /// monomial, subtract the appropriate multiple, or else move the leading
393    /// term into the remainder.  Requires that `div` on the domain succeeds
394    /// (i.e. the domain is effectively a field).
395    pub fn reduce(&self, basis: &[Self]) -> Self {
396        let mut remainder = self.clone();
397        let mut result = self.zero();
398
399        // Cache each basis element's leading term.
400        let basis_lts: Vec<_> = basis
401            .iter()
402            .filter_map(|g| g.leading_term().map(|(e, c)| (g, e.clone(), c.clone())))
403            .collect();
404
405        let max_iter = 10000;
406
407        for _ in 0..max_iter {
408            if remainder.is_zero() {
409                break;
410            }
411            let (rm, rc) = match remainder.leading_term() {
412                Some((e, c)) => (e.clone(), c.clone()),
413                None => break,
414            };
415
416            let mut reduced = false;
417            for (g, lm, lc) in &basis_lts {
418                if monomial_divides(&rm, lm) {
419                    let qm: SmallVec<[usize; 4]> =
420                        rm.iter().zip(lm.iter()).map(|(a, b)| a - b).collect();
421                    let qc = match self.domain.div(&rc, lc) {
422                        Some(q) => q,
423                        None => break,
424                    };
425                    let sub = g.mul_monomial(&qm).mul_scalar(&qc);
426                    remainder = remainder.sub(&sub);
427                    reduced = true;
428                    break;
429                }
430            }
431
432            if !reduced {
433                let key = rm;
434                let val = rc;
435                result.terms.insert(key.clone(), val);
436                remainder.terms.remove(&key);
437            }
438        }
439
440        result
441    }
442
443    /// Compute the S-polynomial of `self` and `other`:
444    ///
445    /// S(f, g) = f·lc(g)·x^(lcm-lm(f)) - g·lc(f)·x^(lcm-lm(g))
446    pub fn spoly(&self, other: &Self) -> Self {
447        let (lm_f, lc_f) = match self.leading_term() {
448            Some(t) => (t.0.clone(), t.1.clone()),
449            None => return self.zero(),
450        };
451        let (lm_g, lc_g) = match other.leading_term() {
452            Some(t) => (t.0.clone(), t.1.clone()),
453            None => return self.zero(),
454        };
455
456        let lcm = monomial_lcm(&lm_f, &lm_g);
457
458        let m_f: SmallVec<[usize; 4]> = lcm.iter().zip(lm_f.iter()).map(|(a, b)| a - b).collect();
459        let m_g: SmallVec<[usize; 4]> = lcm.iter().zip(lm_g.iter()).map(|(a, b)| a - b).collect();
460
461        let term1 = self.mul_monomial(&m_f).mul_scalar(&lc_g);
462        let term2 = other.mul_monomial(&m_g).mul_scalar(&lc_f);
463
464        term1.sub(&term2)
465    }
466
467    // ------------------------------------------------------------------
468    //  Multivariate GCD support
469    // ------------------------------------------------------------------
470
471    /// Compute the content: the GCD of all coefficients.
472    ///
473    /// For the zero polynomial the content is zero.
474    ///
475    /// # Example
476    ///
477    /// ```
478    /// use ocas_domain::{Integer, IntegerDomain};
479    /// use ocas_poly::SparseMultivariatePolynomial;
480    /// use ocas_poly::Lex;
481    ///
482    /// let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
483    ///     IntegerDomain, 1,
484    ///     vec![(vec![2], Integer::from(6)), (vec![1], Integer::from(9)), (vec![0], Integer::from(3))],
485    /// );
486    /// assert_eq!(p.content(), Integer::from(3));
487    /// ```
488    pub fn content(&self) -> D::Element
489    where
490        D: EuclideanDomain,
491    {
492        if self.is_zero() {
493            return self.domain.zero();
494        }
495        let mut g = self.domain.zero();
496        for c in self.terms.values() {
497            g = self.domain.gcd(&g, c);
498        }
499        g
500    }
501
502    /// Return the primitive part: `self / content`.
503    ///
504    /// The result has content 1 (or is zero).
505    ///
506    /// # Example
507    ///
508    /// ```
509    /// use ocas_domain::{Integer, IntegerDomain};
510    /// use ocas_poly::SparseMultivariatePolynomial;
511    /// use ocas_poly::Lex;
512    ///
513    /// let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
514    ///     IntegerDomain, 1,
515    ///     vec![(vec![2], Integer::from(6)), (vec![0], Integer::from(3))],
516    /// );
517    /// let pp = p.primitive_part();
518    /// // After dividing by content=3: 2*x^2 + 1
519    /// assert_eq!(pp.coeff(&[2]), Integer::from(2));
520    /// assert_eq!(pp.coeff(&[0]), Integer::from(1));
521    /// ```
522    pub fn primitive_part(&self) -> Self
523    where
524        D: EuclideanDomain,
525    {
526        if self.is_zero() {
527            return self.clone();
528        }
529        let content = self.content();
530        if self.domain.is_one(&content) {
531            return self.clone();
532        }
533        let mut result = self.zero();
534        for (exp, c) in &self.terms {
535            let q = self.domain.div(c, &content).unwrap_or_else(|| c.clone());
536            result.terms.insert(exp.clone(), q);
537        }
538        result
539    }
540
541    /// Divide this polynomial by another, assuming the division is exact
542    /// (no remainder).
543    ///
544    /// Each term of `self` is divided by the corresponding factor from
545    /// `divisor`. This is used in rational-function canonicalization where
546    /// the GCD is known to divide both numerator and denominator.
547    ///
548    /// # Panics
549    ///
550    /// Panics if the division is not exact.
551    pub fn div_exact(&self, divisor: &Self) -> Self {
552        if divisor.n_terms() <= 1 {
553            // Check if divisor is constant 1 (or zero).
554            let const_val = divisor.coeff(&vec![0; divisor.n_vars]);
555            if self.domain.is_one(&const_val) {
556                return self.clone();
557            }
558        }
559        let (quot, rem) = self.div_rem_sparse(divisor);
560        debug_assert!(rem.is_zero(), "div_exact: division had non-zero remainder");
561        quot
562    }
563
564    /// Sparse polynomial long division returning (quotient, remainder).
565    fn div_rem_sparse(&self, divisor: &Self) -> (Self, Self) {
566        if divisor.is_zero() {
567            panic!("division by zero polynomial");
568        }
569        let (_, div_lm) = match divisor.leading_term() {
570            Some(t) => (t.0.clone(), t.1.clone()),
571            None => return (self.zero(), self.clone()),
572        };
573        let div_lc = div_lm;
574        let div_exp = divisor.leading_monomial().unwrap().clone();
575
576        let mut remainder = self.clone();
577        let mut quotient = self.zero();
578
579        while !remainder.is_zero() {
580            let (rem_exp, rem_lc) = match remainder.leading_term() {
581                Some(t) => (t.0.clone(), t.1.clone()),
582                None => break,
583            };
584            // Check if leading monomial of divisor divides leading monomial of remainder.
585            if !monomial_divides(&div_exp, &rem_exp) {
586                break;
587            }
588            let q_coeff = match self.domain.div(&rem_lc, &div_lc) {
589                Some(q) => q,
590                None => break,
591            };
592            let q_exp: SmallVec<[usize; 4]> = rem_exp
593                .iter()
594                .zip(div_exp.iter())
595                .map(|(a, b)| a - b)
596                .collect();
597            // quotient += q_coeff * x^q_exp
598            let existing = quotient
599                .terms
600                .get(&q_exp)
601                .cloned()
602                .unwrap_or_else(|| self.domain.zero());
603            let sum = self.domain.add(&existing, &q_coeff);
604            if self.domain.is_zero(&sum) {
605                quotient.terms.remove(&q_exp);
606            } else {
607                quotient.terms.insert(q_exp, sum);
608            }
609            // remainder -= q_coeff * x^q_exp * divisor
610            let scaled = divisor.mul_monomial(
611                &remainder
612                    .leading_monomial()
613                    .unwrap()
614                    .iter()
615                    .zip(div_exp.iter())
616                    .map(|(a, b)| a - b)
617                    .collect::<SmallVec<[usize; 4]>>(),
618            );
619            let scaled = scaled.mul_scalar(&q_coeff);
620            remainder = remainder.sub(&scaled);
621        }
622        (quotient, remainder)
623    }
624
625    /// Return the degree of this polynomial in the given variable.
626    ///
627    /// Returns 0 for the zero polynomial (by convention) or if the variable
628    /// does not appear.
629    pub fn degree_in(&self, var_index: usize) -> usize {
630        self.terms
631            .keys()
632            .map(|e| e.get(var_index).copied().unwrap_or(0))
633            .max()
634            .unwrap_or(0)
635    }
636
637    // ------------------------------------------------------------------
638    //  F4 / Gröbner support helpers
639    // ------------------------------------------------------------------
640
641    /// Return the exponent vector of the leading monomial, or `None` for zero.
642    ///
643    /// This is an alias for [`leading_monomial`](Self::leading_monomial) that
644    /// matches the Symbolica naming convention used in the F4 algorithm.
645    #[inline]
646    pub fn max_exp(&self) -> Option<&SmallVec<[usize; 4]>> {
647        self.leading_monomial()
648    }
649
650    /// Return the leading coefficient, or `None` for zero.
651    ///
652    /// This is an alias for [`leading_coeff`](Self::leading_coeff) that
653    /// matches the Symbolica naming convention used in the F4 algorithm.
654    #[inline]
655    pub fn max_coeff(&self) -> Option<&D::Element> {
656        self.leading_coeff()
657    }
658
659    /// Iterate over all exponent vectors in sorted order (descending by
660    /// the monomial ordering).
661    ///
662    /// The F4 algorithm uses this to enumerate every monomial in a
663    /// polynomial for symbolic preprocessing.
664    pub fn exponents_iter(&self) -> impl Iterator<Item = &SmallVec<[usize; 4]>> {
665        let mut sorted: Vec<_> = self.terms.keys().collect();
666        sorted.sort_by(|a, b| O::cmp(a, b));
667        sorted.into_iter()
668    }
669
670    /// Divide every term by the leading coefficient, making the polynomial
671    /// monic. Returns `false` if the polynomial is zero or the leading
672    /// coefficient has no inverse.
673    pub fn make_monic_inplace(&mut self) -> bool {
674        if self.is_zero() {
675            return false;
676        }
677        let lc = self.leading_coeff().cloned().unwrap();
678        match self.domain.inv(&lc) {
679            Some(inv_lc) => {
680                for coeff in self.terms.values_mut() {
681                    *coeff = self.domain.mul(coeff, &inv_lc);
682                }
683                true
684            }
685            None => false,
686        }
687    }
688
689    /// Create a zero polynomial with the same domain and variable count.
690    ///
691    /// This is identical to [`zero`](Self::zero) but named to match the
692    /// Symbolica convention used in F4 code.
693    #[inline]
694    pub fn zero_with_capacity(&self, _cap: usize) -> Self {
695        self.zero()
696    }
697
698    /// Append a single monomial term `coeff * x^exp`.
699    ///
700    /// If the monomial already exists, the coefficients are summed.
701    /// Zero coefficients remove the term.
702    pub fn append_monomial(&mut self, coeff: D::Element, exp: &[usize]) {
703        let key = Self::normalize_exp(exp, self.n_vars);
704        let existing = self
705            .terms
706            .get(&key)
707            .cloned()
708            .unwrap_or_else(|| self.domain.zero());
709        let sum = self.domain.add(&existing, &coeff);
710        if self.domain.is_zero(&sum) {
711            self.terms.remove(&key);
712        } else {
713            self.terms.insert(key, sum);
714        }
715    }
716
717    /// Evaluate the polynomial by substituting `value` for variable `var_index`.
718    ///
719    /// Returns a polynomial in one fewer variable (all remaining variables
720    /// keep their relative order). If `var_index` is the only variable, the
721    /// result is a zero-variable (constant) polynomial.
722    ///
723    /// # Example
724    ///
725    /// ```
726    /// use ocas_domain::{Integer, IntegerDomain};
727    /// use ocas_poly::SparseMultivariatePolynomial;
728    /// use ocas_poly::Lex;
729    ///
730    /// let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
731    ///     IntegerDomain, 2,
732    ///     vec![
733    ///         (vec![1, 1], Integer::from(1)), // x*y
734    ///         (vec![0, 1], Integer::from(2)), // 2*y
735    ///     ],
736    /// );
737    /// // Substitute x=3: result = 3*y + 2*y = 5*y
738    /// let r = p.eval(0, &Integer::from(3));
739    /// assert_eq!(r.coeff(&[1]), Integer::from(5));
740    /// ```
741    pub fn eval(&self, var_index: usize, value: &D::Element) -> Self {
742        let new_n_vars = self.n_vars.saturating_sub(1);
743        let mut result = Self::new(self.domain.clone(), new_n_vars);
744        for (exp, coeff) in &self.terms {
745            // Compute coefficient * value^exp[var_index].
746            let power = self.domain.pow(value, exp[var_index] as u64);
747            let new_coeff = self.domain.mul(coeff, &power);
748            if self.domain.is_zero(&new_coeff) {
749                continue;
750            }
751            // Build new exponent vector without var_index.
752            let mut new_exp = SmallVec::with_capacity(new_n_vars);
753            for i in 0..self.n_vars {
754                if i != var_index {
755                    new_exp.push(exp[i]);
756                }
757            }
758            let existing = result
759                .terms
760                .get(&new_exp)
761                .cloned()
762                .unwrap_or_else(|| self.domain.zero());
763            let sum = self.domain.add(&existing, &new_coeff);
764            if self.domain.is_zero(&sum) {
765                result.terms.remove(&new_exp);
766            } else {
767                result.terms.insert(new_exp, sum);
768            }
769        }
770        result
771    }
772}
773
774// ------------------------------------------------------------------
775//  Factorization entry points for sparse multivariate polynomials
776// ------------------------------------------------------------------
777
778impl SparseMultivariatePolynomial<IntegerDomain, Lex> {
779    /// Factor this bivariate integer polynomial into irreducible factors with
780    /// multiplicities.
781    ///
782    /// The current implementation treats the polynomial as univariate in the
783    /// first variable $x$ with coefficients in $\mathbb{Z}[y]$ and uses
784    /// Wang's Hensel-lifting algorithm. It succeeds when the leading
785    /// coefficient in $x$ is an integer constant.
786    ///
787    /// # Example
788    ///
789    /// ```
790    /// use ocas_domain::{Integer, IntegerDomain};
791    /// use ocas_poly::SparseMultivariatePolynomial;
792    /// use ocas_poly::Lex;
793    ///
794    /// // (x^2 + y + 1)(x + y + 2)
795    /// let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
796    ///     IntegerDomain, 2,
797    ///     vec![
798    ///         (vec![3, 0], Integer::from(1)),
799    ///         (vec![2, 1], Integer::from(1)),
800    ///         (vec![2, 0], Integer::from(2)),
801    ///         (vec![1, 1], Integer::from(1)),
802    ///         (vec![1, 0], Integer::from(1)),
803    ///         (vec![0, 2], Integer::from(1)),
804    ///         (vec![0, 1], Integer::from(3)),
805    ///         (vec![0, 0], Integer::from(2)),
806    ///     ],
807    /// );
808    /// let factors = f.factor();
809    /// assert!(factors.len() >= 2);
810    /// ```
811    pub fn factor(&self) -> Vec<(Self, usize)> {
812        bivariate_factor_z(self, 0, 1)
813    }
814}
815
816impl SparseMultivariatePolynomial<FiniteField, Lex> {
817    /// Factor this bivariate polynomial over a prime finite field into
818    /// irreducible factors with multiplicities.
819    ///
820    /// The current implementation treats the polynomial as univariate in the
821    /// first variable $x$ with coefficients in $\mathbb{F}_p[y]$ and uses
822    /// Hensel lifting. It succeeds when the leading coefficient in $x$ is a
823    /// field constant and the polynomial is square-free (or the derivative in
824    /// $x$ is non-zero).
825    pub fn factor(&self) -> Vec<(Self, usize)> {
826        bivariate_factor_fp(self, 0, 1)
827    }
828}
829
830// ------------------------------------------------------------------
831//  Monomial utilities
832// ------------------------------------------------------------------
833
834/// Check whether monomial `a` divides monomial `b`: `a[i] >= b[i]` for all i.
835pub fn monomial_divides(a: &[usize], b: &[usize]) -> bool {
836    a.iter().zip(b.iter()).all(|(x, y)| x >= y)
837}
838
839/// Compute the least common multiple of two monomials: element-wise max.
840pub fn monomial_lcm(a: &[usize], b: &[usize]) -> SmallVec<[usize; 4]> {
841    a.iter().zip(b.iter()).map(|(x, y)| *x.max(y)).collect()
842}
843
844/// Return true if the two monomials are coprime (no variable appears in both).
845pub fn monomial_are_coprime(a: &[usize], b: &[usize]) -> bool {
846    a.iter().zip(b.iter()).all(|(x, y)| *x == 0 || *y == 0)
847}
848
849#[cfg(test)]
850mod tests {
851    use super::*;
852    use ocas_domain::{Integer, IntegerDomain, Rational, RationalDomain};
853
854    #[test]
855    fn sparse_create_and_coeff() {
856        let domain = IntegerDomain;
857        let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
858            domain,
859            2,
860            vec![
861                (vec![1, 0], Integer::from(2)),
862                (vec![0, 1], Integer::from(3)),
863            ],
864        );
865        assert_eq!(p.coeff(&[1, 0]), Integer::from(2));
866        assert_eq!(p.coeff(&[0, 1]), Integer::from(3));
867        assert_eq!(p.coeff(&[0, 0]), Integer::from(0));
868    }
869
870    #[test]
871    fn sparse_total_degree() {
872        let domain = IntegerDomain;
873        let p = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
874            domain,
875            2,
876            vec![
877                (vec![2, 1], Integer::from(1)),
878                (vec![1, 0], Integer::from(1)),
879            ],
880        );
881        assert_eq!(p.total_degree(), Some(3));
882    }
883
884    #[test]
885    fn sparse_add_and_sub() {
886        let domain = IntegerDomain;
887        let a = SparseMultivariatePolynomial::<_, Lex>::from_terms(
888            domain,
889            2,
890            vec![
891                (vec![1, 0], Integer::from(1)),
892                (vec![0, 1], Integer::from(2)),
893            ],
894        );
895        let b = SparseMultivariatePolynomial::<_, Lex>::from_terms(
896            domain,
897            2,
898            vec![
899                (vec![1, 0], Integer::from(3)),
900                (vec![0, 0], Integer::from(4)),
901            ],
902        );
903        let sum = a.add(&b);
904        assert_eq!(sum.coeff(&[1, 0]), Integer::from(4));
905        assert_eq!(sum.coeff(&[0, 1]), Integer::from(2));
906        assert_eq!(sum.coeff(&[0, 0]), Integer::from(4));
907
908        let diff = b.sub(&a);
909        assert_eq!(diff.coeff(&[1, 0]), Integer::from(2));
910        assert_eq!(diff.coeff(&[0, 1]), Integer::from(-2));
911        assert_eq!(diff.coeff(&[0, 0]), Integer::from(4));
912    }
913
914    #[test]
915    fn sparse_multiplication() {
916        let domain = RationalDomain;
917        // (x + 2y) * (3x + y) = 3x^2 + 7xy + 2y^2
918        let a = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
919            domain,
920            2,
921            vec![
922                (vec![1, 0], Rational::new(1, 1)),
923                (vec![0, 1], Rational::new(2, 1)),
924            ],
925        );
926        let b = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
927            domain,
928            2,
929            vec![
930                (vec![1, 0], Rational::new(3, 1)),
931                (vec![0, 1], Rational::new(1, 1)),
932            ],
933        );
934        let prod = a.mul(&b);
935        assert_eq!(prod.coeff(&[2, 0]), Rational::new(3, 1));
936        assert_eq!(prod.coeff(&[1, 1]), Rational::new(7, 1));
937        assert_eq!(prod.coeff(&[0, 2]), Rational::new(2, 1));
938    }
939
940    #[test]
941    fn sparse_sorted_terms_grevlex() {
942        let domain = IntegerDomain;
943        let p = SparseMultivariatePolynomial::<_, Grevlex>::from_terms(
944            domain,
945            2,
946            vec![
947                (vec![1, 0], Integer::from(1)),
948                (vec![2, 0], Integer::from(1)),
949                (vec![0, 1], Integer::from(1)),
950            ],
951        );
952        let sorted = p.sorted_terms();
953        let exps: Vec<_> = sorted.into_iter().map(|(e, _)| e.to_vec()).collect();
954        // Grevlex order for these terms: x^2 (degree 2), x (degree 1), y (degree 1).
955        // Among degree-1 terms, reverse lex compares the last non-zero exponent:
956        // y = [0,1] comes before x = [1,0] because 1 > 0 in the last position.
957        assert_eq!(exps, vec![vec![2, 0], vec![0, 1], vec![1, 0]]);
958    }
959}