use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Zero};
use std::collections::BTreeMap;
use std::fmt;
use std::ops;
use super::zpoly::{self, ZPoly, pow_ratio};
use crate::base::errors::SymplexError;
pub trait MonomialOrd: 'static + Clone + Send + Sync + std::fmt::Debug {
fn cmp_exponents(a: &[u32], b: &[u32]) -> std::cmp::Ordering;
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct GrevLex;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Lex;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct GrLex;
impl MonomialOrd for GrevLex {
fn cmp_exponents(a: &[u32], b: &[u32]) -> std::cmp::Ordering {
let deg_a: u32 = a.iter().sum();
let deg_b: u32 = b.iter().sum();
deg_a.cmp(°_b).then_with(|| {
for (ai, bi) in a.iter().rev().zip(b.iter().rev()) {
match bi.cmp(ai) {
std::cmp::Ordering::Equal => continue,
other => return other,
}
}
std::cmp::Ordering::Equal
})
}
}
impl MonomialOrd for Lex {
fn cmp_exponents(a: &[u32], b: &[u32]) -> std::cmp::Ordering {
for (ai, bi) in a.iter().zip(b.iter()) {
match ai.cmp(bi) {
std::cmp::Ordering::Equal => continue,
other => return other,
}
}
std::cmp::Ordering::Equal
}
}
impl MonomialOrd for GrLex {
fn cmp_exponents(a: &[u32], b: &[u32]) -> std::cmp::Ordering {
let deg_a: u32 = a.iter().sum();
let deg_b: u32 = b.iter().sum();
deg_a.cmp(°_b).then_with(|| Lex::cmp_exponents(a, b))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MonomialOrder {
Lex,
GrevLex,
}
#[derive(Clone, Debug)]
pub struct MonoKey<O: MonomialOrd> {
pub exponents: Vec<u32>,
_phantom: std::marker::PhantomData<O>,
}
impl<O: MonomialOrd> MonoKey<O> {
pub fn new(exponents: Vec<u32>) -> Self {
Self {
exponents,
_phantom: std::marker::PhantomData,
}
}
}
impl<O: MonomialOrd> PartialEq for MonoKey<O> {
fn eq(&self, other: &Self) -> bool {
self.exponents == other.exponents
}
}
impl<O: MonomialOrd> Eq for MonoKey<O> {}
impl<O: MonomialOrd> PartialOrd for MonoKey<O> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<O: MonomialOrd> Ord for MonoKey<O> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
O::cmp_exponents(&self.exponents, &other.exponents)
}
}
impl<O: MonomialOrd> std::hash::Hash for MonoKey<O> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.exponents.hash(state);
}
}
pub type Exponent = Vec<u32>;
#[derive(Clone, Debug)]
pub struct MultiPoly<O: MonomialOrd = GrevLex> {
num_vars: usize,
terms: BTreeMap<MonoKey<O>, Ratio<BigInt>>,
}
impl<O: MonomialOrd> PartialEq for MultiPoly<O> {
fn eq(&self, other: &Self) -> bool {
self.num_vars == other.num_vars && self.terms == other.terms
}
}
impl<O: MonomialOrd> Eq for MultiPoly<O> {}
fn rat(n: i64) -> Ratio<BigInt> {
Ratio::from_integer(BigInt::from(n))
}
fn q_add_assign(a: &mut Ratio<BigInt>, b: Ratio<BigInt>) {
if a.is_integer() && b.is_integer() {
let sum = a.numer() + b.numer();
*a = Ratio::from_integer(sum);
} else {
*a += b;
}
}
fn q_mul(a: &Ratio<BigInt>, b: &Ratio<BigInt>) -> Ratio<BigInt> {
if a.is_integer() && b.is_integer() {
Ratio::from_integer(a.numer() * b.numer())
} else {
a * b
}
}
fn q_div(a: &Ratio<BigInt>, b: &Ratio<BigInt>) -> Ratio<BigInt> {
if a.is_integer() && b.is_integer() {
let (q, r) = num_integer::Integer::div_rem(a.numer(), b.numer());
if r.is_zero() {
return Ratio::from_integer(q);
}
}
a / b
}
pub fn monomial_lcm(a: &[u32], b: &[u32]) -> Vec<u32> {
a.iter()
.zip(b.iter())
.map(|(&ai, &bi)| ai.max(bi))
.collect()
}
pub fn monomial_divides(a: &[u32], b: &[u32]) -> bool {
a.iter().zip(b.iter()).all(|(&ai, &bi)| ai <= bi)
}
pub fn monomial_div(a: &[u32], b: &[u32]) -> Option<Vec<u32>> {
if !monomial_divides(a, b) {
return None;
}
Some(a.iter().zip(b.iter()).map(|(&ai, &bi)| bi - ai).collect())
}
pub fn monomial_mul(a: &[u32], b: &[u32]) -> Option<Vec<u32>> {
if a.len() != b.len() {
return None;
}
a.iter()
.zip(b.iter())
.map(|(&ai, &bi)| ai.checked_add(bi))
.collect()
}
pub fn monomial_coprime(a: &[u32], b: &[u32]) -> bool {
a.iter().zip(b.iter()).all(|(&ai, &bi)| ai == 0 || bi == 0)
}
impl<O: MonomialOrd> MultiPoly<O> {
pub fn zero(num_vars: usize) -> Self {
MultiPoly {
num_vars,
terms: BTreeMap::new(),
}
}
pub fn constant(num_vars: usize, c: Ratio<BigInt>) -> Self {
let mut p = Self::zero(num_vars);
if !c.is_zero() {
p.terms.insert(MonoKey::new(vec![0; num_vars]), c);
}
p
}
pub fn from_int(num_vars: usize, n: i64) -> Self {
Self::constant(num_vars, rat(n))
}
pub fn var(num_vars: usize, var_index: usize) -> Self {
assert!(
var_index < num_vars,
"var_index {var_index} out of range for {num_vars} variables"
);
let mut exp = vec![0u32; num_vars];
exp[var_index] = 1;
let mut terms = BTreeMap::new();
terms.insert(MonoKey::new(exp), Ratio::one());
MultiPoly { num_vars, terms }
}
pub fn try_var(num_vars: usize, var_index: usize) -> Option<Self> {
(var_index < num_vars).then(|| Self::var(num_vars, var_index))
}
pub fn monomial(c: Ratio<BigInt>, exponents: Exponent) -> Self {
let num_vars = exponents.len();
let mut p = Self::zero(num_vars);
if !c.is_zero() {
p.terms.insert(MonoKey::new(exponents), c);
}
p
}
fn insert_term(&mut self, exp: Vec<u32>, coeff: Ratio<BigInt>) {
if coeff.is_zero() {
return;
}
let key = MonoKey::new(exp);
let entry = self
.terms
.entry(key)
.or_insert_with(|| Ratio::from_integer(BigInt::from(0)));
q_add_assign(entry, coeff);
}
pub fn coeff(&self, exp: &[u32]) -> Option<&Ratio<BigInt>> {
if exp.len() != self.num_vars {
return None;
}
self.terms.get(&MonoKey::<O>::new(exp.to_vec()))
}
pub fn from_terms(num_vars: usize, terms: Vec<(Vec<u32>, Ratio<BigInt>)>) -> Option<Self> {
let mut p = Self::zero(num_vars);
for (exp, c) in terms {
if exp.len() != num_vars {
return None;
}
p.insert_term(exp, c);
}
p.prune();
Some(p)
}
pub(crate) fn from_distinct_terms(
num_vars: usize,
terms: Vec<(Vec<u32>, Ratio<BigInt>)>,
) -> Option<Self> {
let mut p = Self::zero(num_vars);
for (exp, c) in terms {
if exp.len() != num_vars {
return None;
}
if c.is_zero() {
continue;
}
match p.terms.entry(MonoKey::new(exp)) {
std::collections::btree_map::Entry::Vacant(slot) => {
slot.insert(c);
}
std::collections::btree_map::Entry::Occupied(mut slot) => {
*slot.get_mut() += c;
}
}
}
p.prune();
Some(p)
}
pub(crate) fn from_term_map(
num_vars: usize,
terms: BTreeMap<MonoKey<O>, Ratio<BigInt>>,
) -> Self {
let mut p = MultiPoly { num_vars, terms };
p.prune();
p
}
pub fn map_coeffs(&self, mut f: impl FnMut(&Ratio<BigInt>) -> Ratio<BigInt>) -> Self {
let mut terms = BTreeMap::new();
for (k, c) in &self.terms {
let nc = f(c);
if !nc.is_zero() {
terms.insert(k.clone(), nc);
}
}
MultiPoly {
num_vars: self.num_vars,
terms,
}
}
fn prune(&mut self) {
self.terms.retain(|_, c| !c.is_zero());
}
fn assert_compatible(&self, other: &MultiPoly<O>) {
assert_eq!(
self.num_vars, other.num_vars,
"MultiPoly: incompatible variable counts ({} vs {})",
self.num_vars, other.num_vars
);
}
}
impl<O: MonomialOrd> MultiPoly<O> {
pub fn is_zero(&self) -> bool {
self.terms.is_empty()
}
pub fn num_vars(&self) -> usize {
self.num_vars
}
pub fn num_terms(&self) -> usize {
self.terms.len()
}
pub fn total_degree(&self) -> Option<u32> {
self.terms.keys().map(|k| k.exponents.iter().sum()).max()
}
pub fn degree_in(&self, var_index: usize) -> u32 {
self.terms
.keys()
.filter_map(|k| k.exponents.get(var_index).copied())
.max()
.unwrap_or(0)
}
pub fn leading_term(&self) -> Option<(&[u32], &Ratio<BigInt>)> {
self.terms
.last_key_value()
.map(|(k, v)| (k.exponents.as_slice(), v))
}
pub fn leading_monomial(&self) -> Option<&[u32]> {
self.terms
.last_key_value()
.map(|(k, _)| k.exponents.as_slice())
}
pub fn leading_coeff(&self) -> Option<&Ratio<BigInt>> {
self.terms.last_key_value().map(|(_, v)| v)
}
pub fn terms(
&self,
) -> impl DoubleEndedIterator<Item = (&[u32], &Ratio<BigInt>)> + ExactSizeIterator {
self.terms.iter().map(|(k, v)| (k.exponents.as_slice(), v))
}
pub fn as_constant(&self) -> Option<Ratio<BigInt>> {
match self.terms.len() {
0 => Some(Ratio::from_integer(BigInt::from(0))),
1 => self
.terms
.iter()
.next()
.filter(|(k, _)| k.exponents.iter().all(|&e| e == 0))
.map(|(_, c)| c.clone()),
_ => None,
}
}
pub fn affine_form(&self) -> Option<(Vec<Ratio<BigInt>>, Ratio<BigInt>)> {
let zero = Ratio::from_integer(BigInt::from(0));
let mut coeffs = vec![zero.clone(); self.num_vars];
let mut constant = zero;
for (key, c) in &self.terms {
let degree: u32 = key.exponents.iter().sum();
match degree {
0 => constant = c.clone(),
1 => {
let i = key.exponents.iter().position(|&e| e == 1)?;
coeffs[i] = c.clone();
}
_ => return None,
}
}
Some((coeffs, constant))
}
pub fn convert_order<B: MonomialOrd>(&self) -> MultiPoly<B> {
let mut new_terms = BTreeMap::new();
for (key, coeff) in &self.terms {
new_terms.insert(MonoKey::<B>::new(key.exponents.clone()), coeff.clone());
}
MultiPoly {
num_vars: self.num_vars,
terms: new_terms,
}
}
}
impl<O: MonomialOrd> MultiPoly<O> {
pub fn eval(&self, values: &[Ratio<BigInt>]) -> Result<Ratio<BigInt>, SymplexError> {
if values.len() != self.num_vars {
return Err(SymplexError::invalid_argument(
"MultiPoly::eval",
format!("expected {} values, got {}", self.num_vars, values.len()),
));
}
Ok(ZPoly::from_multipoly(self).eval(values))
}
}
impl<O: MonomialOrd> MultiPoly<O> {
pub fn partial_derivative(&self, var_index: usize) -> MultiPoly<O> {
if var_index >= self.num_vars {
return Self::zero(self.num_vars);
}
let mut result = Self::zero(self.num_vars);
for (key, coeff) in &self.terms {
let e_i = key.exponents[var_index];
if e_i == 0 {
continue; }
let new_coeff = coeff * Ratio::from_integer(BigInt::from(e_i));
let mut new_exp = key.exponents.clone();
new_exp[var_index] -= 1;
result.terms.insert(MonoKey::new(new_exp), new_coeff);
}
result
}
pub fn eval_var(&self, var_index: usize, value: &Ratio<BigInt>) -> MultiPoly<O> {
if var_index >= self.num_vars {
return self.clone();
}
let mut result = Self::zero(self.num_vars);
for (key, coeff) in &self.terms {
let e_i = key.exponents[var_index];
let new_coeff = coeff * pow_ratio(value, e_i);
if new_coeff.is_zero() {
continue;
}
let mut new_exp = key.exponents.clone();
new_exp[var_index] = 0;
result.insert_term(new_exp, new_coeff);
}
result.prune();
result
}
pub fn substitute(&self, var_index: usize, value: &Ratio<BigInt>) -> MultiPoly<O> {
assert!(
var_index < self.num_vars,
"substitute: var_index {var_index} out of range for {} variables",
self.num_vars
);
let new_num_vars = self.num_vars - 1;
let mut result = Self::zero(new_num_vars);
for (key, coeff) in &self.terms {
let e_i = key.exponents[var_index];
let val_pow = pow_ratio(value, e_i);
let new_coeff = coeff * val_pow;
if new_coeff.is_zero() {
continue;
}
let mut new_exp = Vec::with_capacity(new_num_vars);
for (j, &ej) in key.exponents.iter().enumerate() {
if j != var_index {
new_exp.push(ej);
}
}
result.insert_term(new_exp, new_coeff);
}
result.prune();
result
}
pub fn try_substitute(&self, var_index: usize, value: &Ratio<BigInt>) -> Option<MultiPoly<O>> {
(var_index < self.num_vars).then(|| self.substitute(var_index, value))
}
}
impl<O: MonomialOrd> MultiPoly<O> {
pub fn add(&self, other: &MultiPoly<O>) -> MultiPoly<O> {
self.assert_compatible(other);
let mut result = self.clone();
for (key, coeff) in &other.terms {
result.insert_term(key.exponents.clone(), coeff.clone());
}
result.prune();
result
}
pub fn try_add(&self, other: &MultiPoly<O>) -> Option<MultiPoly<O>> {
(self.num_vars == other.num_vars).then(|| self.add(other))
}
pub fn sub(&self, other: &MultiPoly<O>) -> MultiPoly<O> {
self.assert_compatible(other);
let mut result = self.clone();
for (key, coeff) in &other.terms {
result.insert_term(key.exponents.clone(), -coeff.clone());
}
result.prune();
result
}
pub fn try_sub(&self, other: &MultiPoly<O>) -> Option<MultiPoly<O>> {
(self.num_vars == other.num_vars).then(|| self.sub(other))
}
pub fn neg(&self) -> MultiPoly<O> {
let terms = self
.terms
.iter()
.map(|(k, c)| (k.clone(), -c.clone()))
.collect();
MultiPoly {
num_vars: self.num_vars,
terms,
}
}
pub fn mul(&self, other: &MultiPoly<O>) -> MultiPoly<O> {
self.assert_compatible(other);
let prod = self.try_mul(other);
assert!(prod.is_some(), "MultiPoly::mul: exponent overflow");
prod.unwrap_or_else(|| Self::zero(self.num_vars))
}
pub fn try_mul(&self, other: &MultiPoly<O>) -> Option<MultiPoly<O>> {
if self.num_vars != other.num_vars {
return None;
}
ZPoly::from_multipoly(self)
.mul(&ZPoly::from_multipoly(other))
.map(|z| z.into_multipoly(self.num_vars))
}
pub fn pow(&self, n: u32) -> MultiPoly<O> {
let p = self.try_pow(n);
assert!(p.is_some(), "MultiPoly::pow: exponent overflow");
p.unwrap_or_else(|| Self::zero(self.num_vars))
}
pub fn try_pow(&self, n: u32) -> Option<MultiPoly<O>> {
match n {
0 => return Some(Self::from_int(self.num_vars, 1)),
1 => return Some(self.clone()),
_ => {}
}
ZPoly::from_multipoly(self)
.pow(self.num_vars, n)
.map(|z| z.into_multipoly(self.num_vars))
}
pub fn scale(&self, c: &Ratio<BigInt>) -> MultiPoly<O> {
if c.is_zero() {
return Self::zero(self.num_vars);
}
let terms = self
.terms
.iter()
.map(|(k, coeff)| (k.clone(), q_mul(coeff, c)))
.collect();
MultiPoly {
num_vars: self.num_vars,
terms,
}
}
pub fn mul_monomial(&self, coeff: &Ratio<BigInt>, exp: &[u32]) -> Self {
match self.try_mul_monomial(coeff, exp) {
Some(p) => p,
None => self.mul(&Self::monomial(coeff.clone(), exp.to_vec())),
}
}
pub fn try_mul_monomial(&self, coeff: &Ratio<BigInt>, exp: &[u32]) -> Option<Self> {
if exp.len() != self.num_vars {
return None;
}
if coeff.is_zero() {
return Some(Self::zero(self.num_vars));
}
let mut result = BTreeMap::new();
for (key, c) in &self.terms {
let new_exp = monomial_mul(&key.exponents, exp)?;
let new_coeff = q_mul(c, coeff);
if !new_coeff.is_zero() {
result.insert(MonoKey::new(new_exp), new_coeff);
}
}
Some(MultiPoly {
num_vars: self.num_vars,
terms: result,
})
}
}
impl<O: MonomialOrd> MultiPoly<O> {
pub fn monic(&self) -> Self {
let Some(lc) = self.leading_coeff() else {
return self.clone();
};
self.scale(&(Ratio::one() / lc.clone()))
}
pub fn primitive_part_q(&self) -> Self {
if self.is_zero() {
return self.clone();
}
let (_, integer_poly) = self.clear_denominators();
let content = integer_poly.integer_content();
if content.is_zero() || content.is_one() {
return integer_poly;
}
integer_poly.scale(&Ratio::new(BigInt::one(), content))
}
}
const HEUGCD_MAX_TRIES: usize = 6;
enum Attempt<P> {
Found(P, P, P),
Retry,
Failed,
}
fn symmetric_mod(c: &BigInt, m: &BigInt) -> BigInt {
use num_integer::Integer;
let r = c.mod_floor(m);
if &r + &r > *m { r - m } else { r }
}
impl<O: MonomialOrd> MultiPoly<O> {
pub fn integer_content(&self) -> BigInt {
zpoly::integer_content(self.terms().map(|(_, c)| c.numer()))
}
pub fn clear_denominators(&self) -> (BigInt, Self) {
let d = zpoly::denominator_lcm(self.terms().map(|(_, c)| c));
if d.is_one() {
return (d, self.clone());
}
let scaled = self.scale(&Ratio::from_integer(d.clone()));
(d, scaled)
}
fn max_norm(&self) -> BigInt {
let mut m = BigInt::zero();
for (_, c) in self.terms() {
let a = num_traits::Signed::abs(c.numer());
if a > m {
m = a;
}
}
m
}
fn leading_is_negative(&self) -> bool {
self.leading_coeff()
.is_some_and(num_traits::Signed::is_negative)
}
fn normalized_over_z(&self) -> Self {
let (_, z) = self.clear_denominators();
if z.leading_is_negative() { z.neg() } else { z }
}
pub fn gcd(a: &Self, b: &Self) -> Self {
if a.num_vars != b.num_vars {
return Self::from_int(a.num_vars, 1);
}
match (a.is_zero(), b.is_zero()) {
(true, true) => return Self::zero(a.num_vars),
(true, false) => return b.normalized_over_z(),
(false, true) => return a.normalized_over_z(),
(false, false) => {}
}
let (_, az) = a.clear_denominators();
let (_, bz) = b.clear_denominators();
let h = match Self::heugcd_z(&az, &bz) {
Some((h, _, _)) => h,
None => Self::gcd_prs(&az, &bz),
};
if h.leading_is_negative() { h.neg() } else { h }
}
fn gcd_prs(f: &Self, g: &Self) -> Self {
let nv = f.num_vars;
if f.is_zero() {
return g.normalized_over_z();
}
if g.is_zero() {
return f.normalized_over_z();
}
let mut present = f.variables_present();
present.extend(g.variables_present());
let Some(v) = present.into_iter().max() else {
let c = num_integer::gcd(f.integer_content(), g.integer_content());
return Self::constant(nv, Ratio::from_integer(c));
};
let (cf, pf) = f.content_in(v);
let (cg, pg) = g.content_in(v);
let c = Self::gcd(&cf, &cg);
let (mut a, mut b) = if pf.degree_in(v) >= pg.degree_in(v) {
(pf, pg)
} else {
(pg, pf)
};
while !b.is_zero() {
if b.degree_in(v) == 0 {
a = Self::from_int(nv, 1);
break;
}
let r = a.prem_in(&b, v);
a = b;
b = if r.is_zero() { r } else { r.content_in(v).1 };
}
let h = c.mul(&a);
if h.leading_is_negative() { h.neg() } else { h }
}
fn coeff_in(&self, v: usize, k: u32) -> Self {
let mut out = Self::zero(self.num_vars);
for (e, c) in self.terms() {
if e[v] == k {
let mut e = e.to_vec();
e[v] = 0;
out.insert_term(e, c.clone());
}
}
out.prune();
out
}
fn content_in(&self, v: usize) -> (Self, Self) {
let mut content = Self::zero(self.num_vars);
for k in 0..=self.degree_in(v) {
let c = self.coeff_in(v, k);
if !c.is_zero() {
content = Self::gcd(&content, &c);
}
}
if content.leading_is_negative() {
content = content.neg();
}
match self.div_exact(&content) {
Some(p) => (content, p),
None => (Self::from_int(self.num_vars, 1), self.clone()),
}
}
fn prem_in(&self, b: &Self, v: usize) -> Self {
let db = b.degree_in(v);
let lcb = b.coeff_in(v, db);
let one = Ratio::from_integer(BigInt::one());
let mut r = self.clone();
while !r.is_zero() && r.degree_in(v) >= db {
let dr = r.degree_in(v);
let lcr = r.coeff_in(v, dr);
let mut e = vec![0u32; self.num_vars];
e[v] = dr - db;
r = r.mul(&lcb).sub(&b.mul(&lcr).mul_monomial(&one, &e));
}
r
}
pub fn lcm(a: &Self, b: &Self) -> Self {
if a.is_zero() || b.is_zero() {
return Self::zero(a.num_vars);
}
let g = Self::gcd(a, b);
let az = a.normalized_over_z();
let bz = b.normalized_over_z();
let prod = az.mul(&bz);
match prod.div_exact(&g) {
Some(l) => l,
None => prod,
}
}
fn heugcd_z(f: &Self, g: &Self) -> Option<(Self, Self, Self)> {
let nv = f.num_vars;
let cf = f.integer_content();
let cg = g.integer_content();
if cf.is_zero() || cg.is_zero() {
return None;
}
let c = num_integer::gcd(cf, cg);
let c_rat = Ratio::from_integer(c.clone());
let inv_c = Ratio::new(BigInt::one(), c);
let f = f.scale(&inv_c);
let g = g.scale(&inv_c);
let one = Self::from_int(nv, 1);
if nv == 0 {
let a = f.as_constant()?.to_integer();
let b = g.as_constant()?.to_integer();
let h = num_integer::gcd(a.clone(), b.clone());
if h.is_zero() {
return None;
}
return Some((
Self::constant(0, Ratio::from_integer(&h * c_rat.to_integer())),
Self::constant(0, Ratio::from_integer(a / &h)),
Self::constant(0, Ratio::from_integer(b / &h)),
));
}
if f.total_degree() == Some(0) || g.total_degree() == Some(0) {
let k = num_integer::gcd(f.integer_content(), g.integer_content());
let inv_k = Ratio::new(BigInt::one(), k.clone());
return Some((
Self::constant(nv, Ratio::from_integer(k) * &c_rat),
f.scale(&inv_k),
g.scale(&inv_k),
));
}
if let Some(q) = g.div_exact_z(&f) {
return Some((f.scale(&c_rat), one, q));
}
if let Some(q) = f.div_exact_z(&g) {
return Some((g.scale(&c_rat), q, one));
}
let var = nv - 1;
let f_norm = f.max_norm();
let g_norm = g.max_norm();
let two = BigInt::from(2);
let b = &two * f_norm.clone().min(g_norm.clone()) + BigInt::from(29);
let lc_ratio = |norm: &BigInt, p: &Self| -> BigInt {
let lc = p
.leading_coeff()
.map(|c| num_traits::Signed::abs(c.numer()))
.filter(|c| !c.is_zero())
.unwrap_or_else(BigInt::one);
norm / lc
};
let mut xi = (b.clone().min(BigInt::from(99) * b.sqrt()))
.max(&two * lc_ratio(&f_norm, &f).min(lc_ratio(&g_norm, &g)) + BigInt::from(4));
for _ in 0..HEUGCD_MAX_TRIES {
match Self::heugcd_attempt(&f, &g, var, &xi) {
Attempt::Found(h, cff, cfg) => return Some((h.scale(&c_rat), cff, cfg)),
Attempt::Retry => {}
Attempt::Failed => return None,
}
xi = Self::next_xi(&xi);
}
None
}
fn next_xi(xi: &BigInt) -> BigInt {
let root4 = xi.sqrt().sqrt().max(BigInt::from(2));
(BigInt::from(73794) * xi * root4) / BigInt::from(27011)
}
fn heugcd_attempt(f: &Self, g: &Self, var: usize, xi: &BigInt) -> Attempt<Self> {
let xi_rat = Ratio::from_integer(xi.clone());
let ff = f.substitute(var, &xi_rat);
let gg = g.substitute(var, &xi_rat);
if ff.is_zero() || gg.is_zero() {
return Attempt::Retry;
}
let Some((h, cff, cfg)) = Self::heugcd_z(&ff, &gg) else {
return Attempt::Failed;
};
let h = Self::interpolate_xi(&h, xi, var);
let content = h.integer_content();
if !h.is_zero() && !content.is_zero() {
let h = h.scale(&Ratio::new(BigInt::one(), content));
if let (Some(cf), Some(cg)) = (f.div_exact_z(&h), g.div_exact_z(&h)) {
return Attempt::Found(h, cf, cg);
}
}
let cff = Self::interpolate_xi(&cff, xi, var);
if !cff.is_zero()
&& let Some(h) = f.div_exact_z(&cff)
&& let Some(cg) = g.div_exact_z(&h)
{
return Attempt::Found(h, cff, cg);
}
let cfg = Self::interpolate_xi(&cfg, xi, var);
if !cfg.is_zero()
&& let Some(h) = g.div_exact_z(&cfg)
&& let Some(cf) = f.div_exact_z(&h)
{
return Attempt::Found(h, cf, cfg);
}
Attempt::Retry
}
fn div_exact_z(&self, divisor: &Self) -> Option<Self> {
self.div_exact(divisor)
.filter(|q| q.terms().all(|(_, c)| c.is_integer()))
}
fn interpolate_xi(h: &Self, xi: &BigInt, var: usize) -> Self {
let nv = h.num_vars + 1;
let mut result = Self::zero(nv);
let mut rest = h.clone();
let mut i: u32 = 0;
let xi_rat = Ratio::from_integer(xi.clone());
while !rest.is_zero() {
if i > 4096 || rest.terms().any(|(_, c)| !c.is_integer()) {
return Self::zero(nv);
}
let digit = rest.map_coeffs(|c| Ratio::from_integer(symmetric_mod(c.numer(), xi)));
for (exp, c) in digit.terms() {
let mut e = Vec::with_capacity(nv);
e.extend_from_slice(&exp[..var]);
e.push(i);
e.extend_from_slice(&exp[var..]);
result.insert_term(e, c.clone());
}
rest = rest.sub(&digit).map_coeffs(|c| c / &xi_rat);
i += 1;
}
result.prune();
result
}
}
impl<O: MonomialOrd> MultiPoly<O> {
pub fn reduce(&self, divisors: &[&MultiPoly<O>]) -> MultiPoly<O> {
if self.is_zero() || divisors.is_empty() {
return self.clone();
}
let mut remainder = MultiPoly::zero(self.num_vars);
let mut p = self.clone();
while let Some((lt_exp, lt_coeff)) = p.leading_term() {
let mut divided = false;
let lt_exp = lt_exp.to_vec();
let lt_coeff = lt_coeff.clone();
for divisor in divisors {
let Some((div_lt_exp, div_lt_coeff)) = divisor.leading_term() else {
continue;
};
if let Some(quot_exp) = monomial_div(div_lt_exp, <_exp) {
let quot_coeff = <_coeff / div_lt_coeff;
let subtrahend = divisor.mul_monomial("_coeff, "_exp);
p = p.sub(&subtrahend);
divided = true;
break;
}
}
if !divided {
remainder.insert_term(lt_exp.clone(), lt_coeff);
p.terms.remove(&MonoKey::<O>::new(lt_exp));
}
}
remainder
}
pub fn div_exact(&self, divisor: &MultiPoly<O>) -> Option<MultiPoly<O>> {
if self.num_vars != divisor.num_vars {
return None;
}
let (div_lt_exp, div_lt_coeff) = divisor.leading_term()?;
let div_lt_exp = div_lt_exp.to_vec();
let div_lt_coeff = div_lt_coeff.clone();
let mut quotient = MultiPoly::zero(self.num_vars);
let mut p = self.clone();
while let Some((lt_exp, lt_coeff)) = p.leading_term() {
let quot_exp = monomial_div(&div_lt_exp, lt_exp)?;
let quot_coeff = q_div(lt_coeff, &div_lt_coeff);
let subtrahend = divisor.try_mul_monomial("_coeff, "_exp)?;
quotient.insert_term(quot_exp, quot_coeff);
for (key, c) in subtrahend.terms {
match p.terms.entry(key) {
std::collections::btree_map::Entry::Occupied(mut e) => {
q_add_assign(e.get_mut(), -c);
if e.get().is_zero() {
e.remove();
}
}
std::collections::btree_map::Entry::Vacant(v) => {
v.insert(-c);
}
}
}
}
Some(quotient)
}
pub fn monomial_content(&self) -> Vec<u32> {
let mut min: Option<Vec<u32>> = None;
for (exp, _) in self.terms() {
match &mut min {
None => min = Some(exp.to_vec()),
Some(m) => {
for (mi, &e) in m.iter_mut().zip(exp) {
*mi = (*mi).min(e);
}
}
}
}
min.unwrap_or_else(|| vec![0; self.num_vars])
}
pub fn variables_present(&self) -> Vec<usize> {
(0..self.num_vars)
.filter(|&i| self.terms.keys().any(|k| k.exponents[i] > 0))
.collect()
}
}
pub fn s_polynomial<O: MonomialOrd>(f: &MultiPoly<O>, g: &MultiPoly<O>) -> MultiPoly<O> {
assert_eq!(
f.num_vars(),
g.num_vars(),
"s_polynomial: incompatible variable counts"
);
let Some([(quot_f, coeff_f), (quot_g, coeff_g)]) = s_polynomial_cofactors(f, g) else {
return MultiPoly::zero(f.num_vars());
};
let scaled_f = f.mul_monomial(&coeff_f, "_f);
let scaled_g = g.mul_monomial(&coeff_g, "_g);
scaled_f.sub(&scaled_g)
}
pub fn try_s_polynomial<O: MonomialOrd>(
f: &MultiPoly<O>,
g: &MultiPoly<O>,
) -> Option<MultiPoly<O>> {
if f.num_vars() != g.num_vars() {
return None;
}
let Some([(quot_f, coeff_f), (quot_g, coeff_g)]) = s_polynomial_cofactors(f, g) else {
return Some(MultiPoly::zero(f.num_vars()));
};
let scaled_f = f.try_mul_monomial(&coeff_f, "_f)?;
let scaled_g = g.try_mul_monomial(&coeff_g, "_g)?;
scaled_f.try_sub(&scaled_g)
}
fn s_polynomial_cofactors<O: MonomialOrd>(
f: &MultiPoly<O>,
g: &MultiPoly<O>,
) -> Option<[(Vec<u32>, Ratio<BigInt>); 2]> {
let (Some((lm_f, lc_f)), Some((lm_g, lc_g))) = (f.leading_term(), g.leading_term()) else {
return None;
};
let lcm = monomial_lcm(lm_f, lm_g);
let quot_f: Vec<u32> = lcm.iter().zip(lm_f).map(|(&l, &e)| l - e).collect();
let quot_g: Vec<u32> = lcm.iter().zip(lm_g).map(|(&l, &e)| l - e).collect();
let coeff_f = Ratio::one() / lc_f;
let coeff_g = Ratio::one() / lc_g;
Some([(quot_f, coeff_f), (quot_g, coeff_g)])
}
impl<O: MonomialOrd> fmt::Display for MultiPoly<O> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_zero() {
return write!(f, "0");
}
let mut first = true;
for (key, coeff) in self.terms.iter().rev() {
let exp = &key.exponents;
let is_constant = exp.iter().all(|&e| e == 0);
let coeff_is_one = *coeff == Ratio::one();
let coeff_is_neg_one = *coeff == -Ratio::<BigInt>::one();
let is_negative = coeff < &Ratio::from_integer(BigInt::from(0));
if first {
if is_constant {
write!(f, "{coeff}")?;
} else if coeff_is_one {
write!(f, "{}", format_monomial_vars(exp))?;
} else if coeff_is_neg_one {
write!(f, "-{}", format_monomial_vars(exp))?;
} else {
write!(f, "{}*{}", coeff, format_monomial_vars(exp))?;
}
} else if is_constant {
if is_negative {
write!(f, " - {}", -coeff.clone())?;
} else {
write!(f, " + {coeff}")?;
}
} else if coeff_is_one {
write!(f, " + {}", format_monomial_vars(exp))?;
} else if coeff_is_neg_one {
write!(f, " - {}", format_monomial_vars(exp))?;
} else if is_negative {
let pos = -coeff.clone();
write!(f, " - {}*{}", pos, format_monomial_vars(exp))?;
} else {
write!(f, " + {}*{}", coeff, format_monomial_vars(exp))?;
}
first = false;
}
Ok(())
}
}
fn format_monomial_vars(exp: &[u32]) -> String {
let mut parts = Vec::new();
for (i, &e) in exp.iter().enumerate() {
if e == 0 {
continue;
} else if e == 1 {
parts.push(format!("x{i}"));
} else {
parts.push(format!("x{i}^{e}"));
}
}
if parts.is_empty() {
"1".to_string()
} else {
parts.join("*")
}
}
impl<O: MonomialOrd> ops::Add for &MultiPoly<O> {
type Output = MultiPoly<O>;
fn add(self, rhs: &MultiPoly<O>) -> MultiPoly<O> {
MultiPoly::add(self, rhs)
}
}
impl<O: MonomialOrd> ops::Sub for &MultiPoly<O> {
type Output = MultiPoly<O>;
fn sub(self, rhs: &MultiPoly<O>) -> MultiPoly<O> {
MultiPoly::sub(self, rhs)
}
}
impl<O: MonomialOrd> ops::Mul for &MultiPoly<O> {
type Output = MultiPoly<O>;
fn mul(self, rhs: &MultiPoly<O>) -> MultiPoly<O> {
MultiPoly::mul(self, rhs)
}
}
impl<O: MonomialOrd> ops::Neg for &MultiPoly<O> {
type Output = MultiPoly<O>;
fn neg(self) -> MultiPoly<O> {
MultiPoly::neg(self)
}
}
impl<O: MonomialOrd> ops::Add for MultiPoly<O> {
type Output = MultiPoly<O>;
fn add(self, rhs: MultiPoly<O>) -> MultiPoly<O> {
MultiPoly::add(&self, &rhs)
}
}
impl<O: MonomialOrd> ops::Sub for MultiPoly<O> {
type Output = MultiPoly<O>;
fn sub(self, rhs: MultiPoly<O>) -> MultiPoly<O> {
MultiPoly::sub(&self, &rhs)
}
}
impl<O: MonomialOrd> ops::Mul for MultiPoly<O> {
type Output = MultiPoly<O>;
fn mul(self, rhs: MultiPoly<O>) -> MultiPoly<O> {
MultiPoly::mul(&self, &rhs)
}
}
impl<O: MonomialOrd> ops::Neg for MultiPoly<O> {
type Output = MultiPoly<O>;
fn neg(self) -> MultiPoly<O> {
MultiPoly::neg(&self)
}
}
impl<O: MonomialOrd> ops::Add<i64> for &MultiPoly<O> {
type Output = MultiPoly<O>;
fn add(self, rhs: i64) -> MultiPoly<O> {
let c = MultiPoly::from_int(self.num_vars(), rhs);
MultiPoly::add(self, &c)
}
}
impl<O: MonomialOrd> ops::Add<i64> for MultiPoly<O> {
type Output = MultiPoly<O>;
fn add(self, rhs: i64) -> MultiPoly<O> {
(&self) + rhs
}
}
impl<O: MonomialOrd> ops::Sub<i64> for &MultiPoly<O> {
type Output = MultiPoly<O>;
fn sub(self, rhs: i64) -> MultiPoly<O> {
let c = MultiPoly::from_int(self.num_vars(), rhs);
MultiPoly::sub(self, &c)
}
}
impl<O: MonomialOrd> ops::Sub<i64> for MultiPoly<O> {
type Output = MultiPoly<O>;
fn sub(self, rhs: i64) -> MultiPoly<O> {
(&self) - rhs
}
}
impl<O: MonomialOrd> ops::Mul<i64> for &MultiPoly<O> {
type Output = MultiPoly<O>;
fn mul(self, rhs: i64) -> MultiPoly<O> {
let c = Ratio::from_integer(BigInt::from(rhs));
self.scale(&c)
}
}
impl<O: MonomialOrd> ops::Mul<i64> for MultiPoly<O> {
type Output = MultiPoly<O>;
fn mul(self, rhs: i64) -> MultiPoly<O> {
(&self) * rhs
}
}
pub fn multipoly_vars<O: MonomialOrd>(num_vars: usize) -> Vec<MultiPoly<O>> {
(0..num_vars).map(|i| MultiPoly::var(num_vars, i)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grevlex_ordering() {
assert_eq!(
GrevLex::cmp_exponents(&[2, 0], &[1, 1]),
std::cmp::Ordering::Greater
);
assert_eq!(
GrevLex::cmp_exponents(&[1, 1], &[0, 2]),
std::cmp::Ordering::Greater
);
assert_eq!(
GrevLex::cmp_exponents(&[3, 0], &[1, 1]),
std::cmp::Ordering::Greater
);
}
#[test]
fn zero_is_zero() {
let z: MultiPoly<GrevLex> = MultiPoly::zero(3);
assert!(z.is_zero());
assert_eq!(z.num_terms(), 0);
assert_eq!(z.total_degree(), None);
}
#[test]
fn constant_round_trip() {
let c: MultiPoly<GrevLex> = MultiPoly::from_int(2, 42);
assert!(!c.is_zero());
assert_eq!(c.num_terms(), 1);
assert_eq!(c.total_degree(), Some(0));
assert_eq!(c.eval(&[rat(0), rat(0)]).unwrap(), rat(42));
}
fn vars2() -> (MultiPoly<GrevLex>, MultiPoly<GrevLex>) {
(MultiPoly::var(2, 0), MultiPoly::var(2, 1))
}
fn pow(p: &MultiPoly<GrevLex>, n: u32) -> MultiPoly<GrevLex> {
let mut acc = MultiPoly::from_int(p.num_vars(), 1);
for _ in 0..n {
acc = acc.mul(p);
}
acc
}
#[test]
fn gcd_coprime_is_one() {
let (x, y) = vars2();
let f = x.mul(&x).add(&y); let g = x.add(&y).add(&MultiPoly::from_int(2, 1)); assert_eq!(MultiPoly::gcd(&f, &g), MultiPoly::from_int(2, 1));
assert_eq!(MultiPoly::gcd(&x, &y), MultiPoly::from_int(2, 1));
}
#[test]
fn gcd_shared_linear_factor() {
let (x, y) = vars2();
let s = x.add(&y);
let d = x.sub(&y);
let f = s.mul(&d); let g = s.mul(&s); assert_eq!(MultiPoly::gcd(&f, &g), s);
assert_eq!(MultiPoly::gcd(&f.neg(), &g), s);
}
#[test]
fn gcd_three_variables() {
let x: MultiPoly<GrevLex> = MultiPoly::var(3, 0);
let y: MultiPoly<GrevLex> = MultiPoly::var(3, 1);
let z: MultiPoly<GrevLex> = MultiPoly::var(3, 2);
let h = x.mul(&y).add(&z).add(&MultiPoly::from_int(3, 1));
let f = h.mul(&x.sub(&z));
let g = h.mul(&y.mul(&y).add(&x));
assert_eq!(MultiPoly::gcd(&f, &g), h);
assert_eq!(MultiPoly::gcd(&g, &f), h);
}
#[test]
fn gcd_zero_handling() {
let (x, y) = vars2();
let f = x.mul(&y).scale(&rat(-4)); let z: MultiPoly<GrevLex> = MultiPoly::zero(2);
assert!(MultiPoly::gcd(&z, &z).is_zero());
assert_eq!(MultiPoly::gcd(&f, &z), x.mul(&y).scale(&rat(4)));
assert_eq!(MultiPoly::gcd(&z, &f), x.mul(&y).scale(&rat(4)));
}
#[test]
fn gcd_includes_integer_content() {
let (x, _y) = vars2();
let f = x.scale(&rat(6)); let g = x.mul(&x).scale(&rat(4)); assert_eq!(MultiPoly::gcd(&f, &g), x.scale(&rat(2)));
let twelve: MultiPoly<GrevLex> = MultiPoly::from_int(2, 12);
assert_eq!(
MultiPoly::gcd(&twelve, &MultiPoly::from_int(2, 18)),
MultiPoly::from_int(2, 6)
);
}
#[test]
fn gcd_clears_rational_denominators() {
let (x, y) = vars2();
let s = x.add(&y);
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
let third = Ratio::new(BigInt::from(1), BigInt::from(3));
let f = s.mul(&x).scale(&half); let g = s.mul(&y).scale(&third); let h = MultiPoly::gcd(&f, &g);
assert!(f.div_exact(&h).is_some() && g.div_exact(&h).is_some());
assert_eq!(h, s);
}
#[test]
fn gcd_large_coefficients() {
let (x, y) = vars2();
let big = |s: &str| Ratio::from_integer(s.parse::<BigInt>().unwrap());
let h = x
.scale(&big("123456789012345678901234567890"))
.add(&y.scale(&big("987654321098765432109876543210")))
.add(&MultiPoly::from_int(2, 1));
let f = h.mul(&x.add(&MultiPoly::from_int(2, 7)));
let g = h.mul(&y.sub(&x.scale(&big("5555555555555555555"))));
assert_eq!(MultiPoly::gcd(&f, &g), h);
}
#[test]
fn gcd_first_evaluation_point_fails_then_retry_succeeds() {
let x: MultiPoly<GrevLex> = MultiPoly::var(1, 0);
let one = MultiPoly::from_int(1, 1);
let h = pow(&x.add(&one), 8);
let f = h.mul(&x.sub(&one)); let g = h.mul(&x.mul(&x).add(&one)); assert_eq!(f.max_norm(), BigInt::from(28));
assert_eq!(g.max_norm(), BigInt::from(112));
let xi0 = BigInt::from(85);
assert!(matches!(
MultiPoly::heugcd_attempt(&f, &g, 0, &xi0),
Attempt::Retry
));
let xi1 = MultiPoly::<GrevLex>::next_xi(&xi0);
assert!(xi1 > BigInt::from(140), "next ξ = {xi1}");
match MultiPoly::heugcd_attempt(&f, &g, 0, &xi1) {
Attempt::Found(hh, cff, cfg) => {
assert_eq!(hh, h);
assert_eq!(cff, x.sub(&one));
assert_eq!(cfg, x.mul(&x).add(&one));
}
_ => panic!("ξ = {xi1} should recover the GCD"),
}
assert_eq!(MultiPoly::gcd(&f, &g), h);
}
#[test]
fn gcd_of_four_variable_products_is_not_one() {
let v: Vec<MultiPoly<GrevLex>> = (0..4).map(|i| MultiPoly::var(4, i)).collect();
let (x, y, z, w) = (&v[0], &v[1], &v[2], &v[3]);
let c = |n: i64| MultiPoly::<GrevLex>::from_int(4, n);
let h = z.scale(&rat(7)).add(&c(34));
let fa = w
.mul(z)
.scale(&rat(7_723_980))
.add(&w.scale(&rat(13_640_220)))
.add(&x.mul(z).scale(&rat(608_556)))
.add(&x.scale(&rat(1_074_684)))
.add(&z.mul(z).scale(&rat(46_812)))
.add(&z.scale(&rat(2_376_456)))
.add(&c(4_050_732));
let ga = w
.mul(w)
.scale(&rat(-2928))
.add(&x.mul(x).mul(x).scale(&rat(-1464)))
.add(&x.scale(&rat(146_400)))
.add(&y.mul(z).scale(&rat(-1464)))
.add(&c(16_104));
let f = h.mul(&fa);
let g = h.mul(&ga);
let want = z.scale(&rat(84)).add(&c(408));
assert_eq!(MultiPoly::gcd(&f, &g), want);
assert_eq!(MultiPoly::gcd_prs(&f, &g), want);
}
#[test]
fn gcd_never_wrong_on_random_products() {
let (x, y) = vars2();
let mut seed: u64 = 0x2545_F491_4F6C_DD1D;
let mut next = || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed % 7) as i64 - 3
};
let mut rand_poly = || {
let mut p = MultiPoly::zero(2);
for ex in 0..3u32 {
for ey in 0..3u32 {
let c = next();
if c != 0 {
p = p.add(&MultiPoly::monomial(rat(c), vec![ex, ey]));
}
}
}
if p.is_zero() { x.add(&y) } else { p }
};
for _ in 0..12 {
let h = rand_poly();
let a = rand_poly();
let b = rand_poly();
let f = h.mul(&a);
let g = h.mul(&b);
let d = MultiPoly::gcd(&f, &g);
assert!(f.div_exact(&d).is_some(), "gcd does not divide f");
assert!(g.div_exact(&d).is_some(), "gcd does not divide g");
assert!(d.div_exact(&h).is_some(), "gcd {d} misses factor {h}");
}
}
#[test]
fn lcm_of_monomials() {
let (x, y) = vars2();
let f = x.mul(&y);
let g = y.mul(&y);
assert_eq!(MultiPoly::lcm(&f, &g), x.mul(&y).mul(&y));
assert!(MultiPoly::lcm(&f, &MultiPoly::zero(2)).is_zero());
}
#[test]
fn from_terms_and_map_coeffs() {
let p: MultiPoly<GrevLex> =
MultiPoly::from_terms(2, vec![(vec![1, 0], rat(2)), (vec![1, 0], rat(-2))]).unwrap();
assert!(p.is_zero());
let q: MultiPoly<GrevLex> = MultiPoly::from_terms(2, vec![(vec![2, 1], rat(3))]).unwrap();
assert_eq!(q.coeff(&[2, 1]), Some(&rat(3)));
assert_eq!(q.coeff(&[2]), None);
let doubled = q.map_coeffs(|c| c * rat(2));
assert_eq!(doubled.coeff(&[2, 1]), Some(&rat(6)));
let killed = q.map_coeffs(|_| rat(0));
assert!(killed.is_zero());
}
#[test]
fn integer_content_and_clear_denominators() {
let (x, y) = vars2();
let f = x.scale(&rat(6)).add(&y.scale(&rat(9)));
assert_eq!(f.integer_content(), BigInt::from(3));
let g = x
.scale(&Ratio::new(BigInt::from(1), BigInt::from(2)))
.add(&y.scale(&Ratio::new(BigInt::from(2), BigInt::from(3))));
let (d, gz) = g.clear_denominators();
assert_eq!(d, BigInt::from(6));
assert_eq!(gz, x.scale(&rat(3)).add(&y.scale(&rat(4))));
}
}