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