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