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