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