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