use num_bigint::BigInt;
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::base::arena::Arena;
use crate::base::canon::Everywhere;
use crate::base::node::{ExprId, ExprNode};
use crate::base::numeric::Q;
use crate::base::walk;
use crate::poly::multipoly::{GrevLex, Lex, MonomialOrd, MultiPoly};
use crate::poly::polybridge::multipoly_to_expr;
type RatPoly = MultiPoly<GrevLex>;
const MAX_TERMS: usize = 20_000;
const MAX_DEGREE: u32 = 1 << 20;
pub(crate) fn ratsimp(arena: &mut Arena, expr: ExprId) -> ExprId {
if !is_admissible(arena, expr) {
return expr;
}
let vanishing = vanishing_bases(arena, expr, &FxHashMap::default());
let gens = collect_generators(arena, expr);
let Some((p, q)) = to_rational_function_tracked(arena, expr, &gens, &mut false, &vanishing)
else {
return expr;
};
let out = rebuild(arena, &p, &q, &gens).unwrap_or(expr);
if out == arena.complex_infinity && numerator_expands_to_zero(arena, expr) {
return arena.nan;
}
out
}
fn vanishing_bases(
arena: &mut Arena,
expr: ExprId,
known: &FxHashMap<ExprId, Vanishing>,
) -> FxHashMap<ExprId, Vanishing> {
let mut powers: Vec<(ExprId, ExprId, bool)> = Vec::new();
let mut products: Vec<ExprId> = Vec::new();
let mut sums: Vec<ExprId> = Vec::new();
let mut generators: Vec<ExprId> = Vec::new();
let mut seen: FxHashSet<ExprId> = FxHashSet::default();
let mut stack = vec![expr];
while let Some(id) = stack.pop() {
if !seen.insert(id) {
continue;
}
match arena.node(id) {
ExprNode::Mul(children) => {
products.push(id);
stack.extend(children.iter());
}
ExprNode::Add(children) => {
sums.push(id);
stack.extend(children.iter());
}
ExprNode::Neg(child) => stack.push(*child),
ExprNode::Pow(base, e) => {
if let Some(n) = integer_exponent(arena, *e) {
stack.push(*base);
if n != 0 && !arena.node(*base).is_atom() {
powers.push((id, *base, n < 0));
}
} else if let Some(q) = arena.as_num(*e)
&& !arena.node(*base).is_atom()
{
powers.push((id, *base, q.is_negative()));
generators.push(id);
} else {
generators.push(id);
}
}
node if !node.is_atom() => generators.push(id),
_ => {}
}
}
let mut vanishing: FxHashMap<ExprId, Vanishing> = FxHashMap::default();
let mut decided: FxHashMap<ExprId, bool> = FxHashMap::default();
for &(_, base, _) in &powers {
if known.get(&base) == Some(&Vanishing::Zero) {
decided.insert(base, true);
}
}
for &(_, base, negative) in &powers {
if negative && !decided.contains_key(&base) {
let zero = crate::base::canon::factor_vanishes(arena, base);
decided.insert(base, zero);
}
}
let candidates = crate::base::canon::identity_denominator_candidates(arena, &products);
let mut by_identity: FxHashSet<ExprId> = FxHashSet::default();
for &d in &candidates {
if decided.get(&d) != Some(&true)
&& by_identity.insert(d)
&& crate::simplify::identically_zero::vanishes_by_identity(arena, d)
{
decided.insert(d, true);
}
}
let something_vanishes = !candidates.is_empty() || decided.values().any(|&zero| zero);
for d in identity_bases(arena, &powers, &products, something_vanishes) {
if decided.get(&d) != Some(&true)
&& by_identity.insert(d)
&& crate::simplify::identically_zero::vanishes_by_identity(arena, d)
{
decided.insert(d, true);
}
}
if decided.values().any(|&zero| zero) {
let partners = crate::base::canon::sum_partner_denominators(arena, &sums, |d| {
decided.get(&d) == Some(&true)
});
for d in partners {
if decided.get(&d) != Some(&true)
&& crate::simplify::identically_zero::vanishes_by_identity(arena, d)
{
decided.insert(d, true);
}
}
}
let infinite = if decided.values().any(|&zero| zero) {
let order = walk::post_order_ids(arena, expr);
crate::base::canon::nodes_over_vanishing_bases(arena, &order, |b| {
decided.get(&b) == Some(&true)
})
} else {
FxHashSet::default()
};
for &prod in &products {
let ExprNode::Mul(children) = arena.node(prod).clone() else {
continue;
};
if !children.iter().any(|c| infinite.contains(c)) {
continue;
}
for c in children {
let positive = match arena.node(c) {
ExprNode::Pow(_, e) => integer_exponent(arena, *e).is_none_or(|n| n > 0),
_ => true,
};
if positive
&& !infinite.contains(&c)
&& !arena.node(c).is_atom()
&& !vanishing.contains_key(&c)
&& (crate::base::canon::factor_vanishes(arena, c)
|| crate::simplify::identically_zero::vanishes_by_identity(arena, c)
|| crate::base::canon::zero_candidate_sums(arena, c)
.into_iter()
.any(|s| {
s != c
&& crate::simplify::identically_zero::vanishes_by_identity(arena, s)
}))
{
vanishing.insert(c, Vanishing::Zero);
}
}
}
for &(power, base, negative) in &powers {
if decided.get(&base) == Some(&true) {
vanishing.insert(base, Vanishing::Zero);
let v = if negative {
Vanishing::Pole
} else {
Vanishing::Zero
};
vanishing.insert(power, v);
}
}
let values = crate::base::canon::everywhere_values(arena, &generators);
for (&g, value) in generators.iter().zip(values) {
if value == Everywhere::Undefined {
vanishing.insert(g, Vanishing::Undefined);
}
}
vanishing
}
fn identity_bases(
arena: &Arena,
powers: &[(ExprId, ExprId, bool)],
products: &[ExprId],
something_vanishes: bool,
) -> Vec<ExprId> {
let mut bases: Vec<ExprId> = Vec::new();
for &(_, base, negative) in powers {
if negative && !bases.contains(&base) && may_vanish_by_identity(arena, base) {
bases.push(base);
}
}
if bases.is_empty() || something_vanishes {
return bases;
}
let zero_factor = products.iter().any(|&p| {
let ExprNode::Mul(children) = arena.node(p) else {
return false;
};
children.iter().any(|&c| {
let positive = match arena.node(c) {
ExprNode::Pow(_, e) => arena.as_num(*e).is_none_or(|q| q.is_positive()),
_ => true,
};
positive
&& !arena.node(c).is_atom()
&& crate::base::canon::may_vanish_identically_as_factor(arena, c)
})
});
if zero_factor { bases } else { Vec::new() }
}
fn may_vanish_by_identity(arena: &Arena, d: ExprId) -> bool {
if arena.node(d).is_atom() || walk::has_unevaluated(arena, d) {
return false;
}
let mut transcendental = false;
for id in walk::post_order_ids(arena, d) {
match arena.node(id) {
ExprNode::Infinity
| ExprNode::NegInfinity
| ExprNode::ComplexInfinity
| ExprNode::NaN => return false,
ExprNode::Num(_)
| ExprNode::Symbol(_)
| ExprNode::Add(_)
| ExprNode::Mul(_)
| ExprNode::Neg(_) => {}
ExprNode::Pow(_, e) if arena.as_num(*e).is_some_and(|q| q.is_integer()) => {}
_ => transcendental = true,
}
}
transcendental
}
pub(crate) fn numerator_expands_to_zero(arena: &mut Arena, expr: ExprId) -> bool {
let (n, _) = crate::poly::polybridge::fraction_parts(arena, expr);
let expanded = crate::transforms::expand::expand(arena, n);
let expanded = crate::transforms::eval::eval(arena, expanded);
arena.is_zero_structural(expanded)
}
pub(crate) fn collect_reduced_terms(
arena: &mut Arena,
expr: ExprId,
keys: impl Fn(&Arena, ExprId) -> bool,
) -> Option<ExprId> {
if !is_admissible(arena, expr) {
return None;
}
let gens = collect_generators(arena, expr);
let (p, q) = to_rational_function(arena, expr, &gens)?;
if p.is_zero() || q.is_zero() {
return None;
}
let key: Vec<bool> = gens.iter().map(|&g| keys(arena, g)).collect();
let has_key = |exp: &[u32]| exp.iter().zip(&key).any(|(&e, &k)| k && e > 0);
if q.terms().any(|(exp, _)| has_key(exp)) {
if RatPoly::gcd(&p, &q).total_degree().unwrap_or(0) == 0 {
return None;
}
let out = rebuild(arena, &p, &q, &gens)?;
return (out != expr).then_some(out);
}
let mut groups: Vec<(Vec<u32>, RatPoly)> = Vec::new();
for (exp, c) in p.terms() {
let key_exp: Vec<u32> = exp
.iter()
.zip(&key)
.map(|(&e, &k)| if k { e } else { 0 })
.collect();
let rest_exp: Vec<u32> = exp
.iter()
.zip(&key)
.map(|(&e, &k)| if k { 0 } else { e })
.collect();
let term = RatPoly::monomial(c.clone(), rest_exp);
match groups.iter_mut().find(|(m, _)| *m == key_exp) {
Some((_, acc)) => *acc = acc.add(&term),
None => groups.push((key_exp, term)),
}
}
if q.total_degree().unwrap_or(0) == 0
|| (groups.len() == 1 && RatPoly::gcd(&groups[0].1, &q).total_degree().unwrap_or(0) == 0)
{
return None;
}
groups.sort_by(|a, b| a.0.cmp(&b.0));
let mut terms: Vec<ExprId> = Vec::with_capacity(groups.len());
for (key_exp, coeff) in groups {
let reduced = rebuild(arena, &coeff, &q, &gens)?;
let monomial = multipoly_to_expr(arena, &RatPoly::monomial(Q::one(), key_exp), &gens);
terms.push(arena.mul(&[monomial, reduced]));
}
let out = match terms.len() {
1 => terms[0],
_ => arena.add(&terms),
};
(out != expr).then_some(out)
}
fn is_admissible(arena: &Arena, expr: ExprId) -> bool {
if walk::has_unevaluated(arena, expr) {
return false;
}
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
let mut stack = vec![expr];
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
match arena.node(id) {
ExprNode::Infinity
| ExprNode::NegInfinity
| ExprNode::ComplexInfinity
| ExprNode::NaN => return false,
node => stack.extend(node.children()),
}
}
true
}
fn integer_exponent(arena: &Arena, exp: ExprId) -> Option<i64> {
let r = arena.as_num(exp)?;
if !r.is_integer() {
return None;
}
let n: i64 = r.to_integer().try_into().ok()?;
if n.unsigned_abs() > u64::from(MAX_DEGREE) {
return None;
}
Some(n)
}
fn is_structural(arena: &Arena, id: ExprId) -> bool {
match arena.node(id) {
ExprNode::Num(_) | ExprNode::Add(_) | ExprNode::Mul(_) | ExprNode::Neg(_) => true,
ExprNode::Pow(_, exp) => integer_exponent(arena, *exp).is_some(),
_ => false,
}
}
fn collect_generators(arena: &Arena, expr: ExprId) -> Vec<ExprId> {
let mut gens: Vec<ExprId> = Vec::new();
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
let mut stack = vec![expr];
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
if is_structural(arena, id) {
match arena.node(id) {
ExprNode::Pow(base, _) => stack.push(*base),
node => stack.extend(node.children()),
}
} else {
gens.push(id);
}
}
gens.sort_by(|a, b| {
arena
.sort_key(*a)
.cmp(arena.sort_key(*b))
.then_with(|| a.0.cmp(&b.0))
});
gens
}
fn is_one(p: &RatPoly) -> bool {
p.num_terms() == 1 && p.total_degree() == Some(0) && p.leading_coeff().is_some_and(One::is_one)
}
fn within_budget(p: &RatPoly) -> bool {
p.num_terms() <= MAX_TERMS && p.total_degree().unwrap_or(0) <= MAX_DEGREE
}
fn pow(base: &RatPoly, mut n: u64) -> Option<RatPoly> {
let nv = base.num_vars();
let mut result = RatPoly::from_int(nv, 1);
let mut sq = base.clone();
while n > 0 {
if n & 1 == 1 {
result = result.mul(&sq);
if !within_budget(&result) {
return None;
}
}
n >>= 1;
if n > 0 {
sq = sq.mul(&sq);
if !within_budget(&sq) {
return None;
}
}
}
Some(result)
}
fn add_fractions(p1: &RatPoly, q1: &RatPoly, p2: &RatPoly, q2: &RatPoly) -> (RatPoly, RatPoly) {
if q1.is_zero() && q2.is_zero() {
let nv = q1.num_vars();
return (RatPoly::zero(nv), RatPoly::zero(nv));
}
if q1 == q2 {
return (p1.add(p2), q1.clone());
}
if q1.is_zero() || q2.is_zero() {
return (p1.mul(q2).add(&p2.mul(q1)), q1.mul(q2));
}
if is_one(q1) {
return (p1.mul(q2).add(p2), q2.clone());
}
if is_one(q2) {
return (p1.add(&p2.mul(q1)), q1.clone());
}
let l = RatPoly::lcm(q1, q2);
match (l.div_exact(q1), l.div_exact(q2)) {
(Some(m1), Some(m2)) => (p1.mul(&m1).add(&p2.mul(&m2)), l),
_ => (p1.mul(q2).add(&p2.mul(q1)), q1.mul(q2)),
}
}
fn to_rational_function(
arena: &Arena,
expr: ExprId,
gens: &[ExprId],
) -> Option<(RatPoly, RatPoly)> {
to_rational_function_tracked(arena, expr, gens, &mut false, &FxHashMap::default())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Vanishing {
Zero,
Pole,
Undefined,
}
pub(crate) fn vanishes_as_rational_function(arena: &mut Arena, expr: ExprId) -> bool {
if !is_admissible(arena, expr) {
return false;
}
let gens = collect_generators(arena, expr);
let Some((p, q)) = to_rational_function(arena, expr, &gens) else {
return false;
};
let i = arena.i_unit();
let Some(t) = gens.iter().position(|&g| g == i) else {
return p.is_zero() && !q.is_zero();
};
let modulo_i = |f: &RatPoly| -> Option<RatPoly> {
let terms: Vec<(Vec<u32>, Q)> = f
.terms()
.map(|(exp, c)| {
let mut e = exp.to_vec();
let k = e.get(t).copied().unwrap_or(0);
if let Some(slot) = e.get_mut(t) {
*slot = k % 2;
}
let c = if (k / 2) % 2 == 1 {
-c.clone()
} else {
c.clone()
};
(e, c)
})
.collect();
RatPoly::from_terms(f.num_vars(), terms)
};
match (modulo_i(&p), modulo_i(&q)) {
(Some(p), Some(q)) => p.is_zero() && !q.is_zero(),
_ => false,
}
}
pub(crate) fn ratsimp_vanishing(
arena: &mut Arena,
expr: ExprId,
vanishing: &FxHashMap<ExprId, Vanishing>,
) -> Option<ExprId> {
if !is_admissible(arena, expr) {
return None;
}
let mut vanishing = vanishing.clone();
for (id, v) in vanishing_bases(arena, expr, &vanishing) {
vanishing.entry(id).or_insert(v);
}
let gens = collect_generators(arena, expr);
let (p, q) = to_rational_function_tracked(arena, expr, &gens, &mut false, &vanishing)?;
let out = rebuild(arena, &p, &q, &gens)?;
if out == arena.complex_infinity && numerator_expands_to_zero(arena, expr) {
return Some(arena.nan);
}
Some(out)
}
pub(crate) fn ratsimp_zero_denominator(arena: &mut Arena, expr: ExprId) -> Option<ExprId> {
if !is_admissible(arena, expr) {
return None;
}
let vanishing = vanishing_bases(arena, expr, &FxHashMap::default());
let gens = collect_generators(arena, expr);
let mut zero_denominator = false;
let (p, q) =
to_rational_function_tracked(arena, expr, &gens, &mut zero_denominator, &vanishing)?;
if !zero_denominator {
return None;
}
let out = rebuild(arena, &p, &q, &gens)?;
if out == arena.complex_infinity && numerator_expands_to_zero(arena, expr) {
return Some(arena.nan);
}
Some(out)
}
fn to_rational_function_tracked(
arena: &Arena,
expr: ExprId,
gens: &[ExprId],
zero_denominator: &mut bool,
vanishing: &FxHashMap<ExprId, Vanishing>,
) -> Option<(RatPoly, RatPoly)> {
let nv = gens.len();
let gen_index: FxHashMap<ExprId, usize> =
gens.iter().enumerate().map(|(i, &g)| (g, i)).collect();
let mut cache: FxHashMap<ExprId, (RatPoly, RatPoly)> = FxHashMap::default();
let one = RatPoly::from_int(nv, 1);
let mut stack: Vec<(ExprId, bool)> = vec![(expr, false)];
while let Some(&(id, expanded)) = stack.last() {
if cache.contains_key(&id) {
stack.pop();
continue;
}
if let Some(v) = vanishing.get(&id) {
let value = match v {
Vanishing::Zero => (RatPoly::zero(nv), one.clone()),
Vanishing::Pole => {
*zero_denominator = true;
(one.clone(), RatPoly::zero(nv))
}
Vanishing::Undefined => {
*zero_denominator = true;
(RatPoly::zero(nv), RatPoly::zero(nv))
}
};
cache.insert(id, value);
stack.pop();
continue;
}
if let Some(&i) = gen_index.get(&id) {
cache.insert(id, (RatPoly::var(nv, i), one.clone()));
stack.pop();
continue;
}
let node = arena.node(id).clone();
if !expanded {
if let Some(top) = stack.last_mut() {
top.1 = true;
}
match &node {
ExprNode::Pow(base, _) => stack.push((*base, false)),
n => {
for child in n.children() {
stack.push((child, false));
}
}
}
continue;
}
let value: (RatPoly, RatPoly) = match &node {
ExprNode::Num(nid) => (RatPoly::constant(nv, arena.num(*nid).clone()), one.clone()),
ExprNode::Add(children) => {
let mut acc_p = RatPoly::zero(nv);
let mut acc_q = one.clone();
for c in children.iter() {
let (p, q) = cache.get(c)?;
let (np, nq) = add_fractions(&acc_p, &acc_q, p, q);
if !within_budget(&np) || !within_budget(&nq) {
return None;
}
acc_p = np;
acc_q = nq;
}
(acc_p, acc_q)
}
ExprNode::Mul(children) => {
let mut acc_p = one.clone();
let mut acc_q = one.clone();
for c in children.iter() {
let (p, q) = cache.get(c)?;
acc_p = acc_p.mul(p);
acc_q = acc_q.mul(q);
if !within_budget(&acc_p) || !within_budget(&acc_q) {
return None;
}
}
(acc_p, acc_q)
}
ExprNode::Neg(inner) => {
let (p, q) = cache.get(inner)?;
(p.neg(), q.clone())
}
ExprNode::Pow(base, exp) => {
let n = integer_exponent(arena, *exp)?;
let (p, q) = cache.get(base)?;
if n >= 0 {
(pow(p, n.unsigned_abs())?, pow(q, n.unsigned_abs())?)
} else {
*zero_denominator |= p.is_zero();
(pow(q, n.unsigned_abs())?, pow(p, n.unsigned_abs())?)
}
}
_ => return None,
};
cache.insert(id, value);
stack.pop();
}
cache.remove(&expr)
}
fn lex_leading_coeff(p: &RatPoly) -> Option<Q> {
let mut best: Option<(&[u32], &Q)> = None;
for (exp, c) in p.terms() {
match best {
Some((be, _)) if Lex::cmp_exponents(exp, be) != std::cmp::Ordering::Greater => {}
_ => best = Some((exp, c)),
}
}
best.map(|(_, c)| c.clone())
}
fn rebuild(arena: &mut Arena, p: &RatPoly, q: &RatPoly, gens: &[ExprId]) -> Option<ExprId> {
if q.is_zero() {
return Some(if p.is_zero() {
arena.nan
} else {
arena.complex_infinity
});
}
if p.is_zero() {
return Some(arena.zero);
}
let mut p = p.clone();
let mut q = q.clone();
let g = RatPoly::gcd(&p, &q);
if g.total_degree().unwrap_or(0) > 0
&& let (Some(pq), Some(qq)) = (p.div_exact(&g), q.div_exact(&g))
{
p = pq;
q = qq;
}
let (dp, pz) = p.clear_denominators();
let (dq, qz) = q.clear_denominators();
p = pz.scale(&Ratio::from_integer(dq));
q = qz.scale(&Ratio::from_integer(dp));
let c = p.integer_content().gcd(&q.integer_content());
if !c.is_zero() && !c.is_one() {
let inv = Ratio::new(BigInt::one(), c);
p = p.scale(&inv);
q = q.scale(&inv);
}
if lex_leading_coeff(&q).is_some_and(|lc| lc.is_negative()) {
p = p.neg();
q = q.neg();
}
let p_expr = multipoly_to_expr(arena, &p, gens);
if is_one(&q) {
return Some(p_expr);
}
let q_expr = multipoly_to_expr(arena, &q, gens);
Some(arena.div(p_expr, q_expr))
}
#[cfg(test)]
mod tests {
use super::*;
fn show(arena: &Arena, id: ExprId) -> String {
arena.display(id).to_string()
}
#[test]
fn linear_over_linear_cancels_and_normalises() {
let mut a = Arena::new();
let j = a.symbol("j");
let one = a.one;
let two = a.int(2);
let j2 = a.pow(j, two);
let num1 = a.sub(j2, one);
let den1 = a.mul(&[two, j]);
let t1 = a.div(num1, den1);
let num2 = a.sub(j, one);
let t2 = a.div(num2, two);
let e = a.sub(t1, t2);
let r = ratsimp(&mut a, e);
let expected = a.div(num2, den1);
assert_eq!(r, expected, "got {}", show(&a, r));
}
#[test]
fn opaque_subexpressions_are_generators() {
let mut a = Arena::new();
let x = a.symbol("x");
let s = a.sin(x);
let two = a.int(2);
let s2 = a.pow(s, two);
let e = a.div(s2, s);
assert_eq!(ratsimp(&mut a, e), s);
}
#[test]
fn infinity_is_left_alone() {
let mut a = Arena::new();
let x = a.symbol("x");
let inf = a.infinity;
let e = a.add(&[x, inf]);
assert_eq!(ratsimp(&mut a, e), e);
}
#[test]
fn already_normal_input_is_unchanged() {
let mut a = Arena::new();
let x = a.symbol("x");
let one = a.one;
let e = a.add(&[x, one]);
assert_eq!(ratsimp(&mut a, e), e);
let inv = a.div(one, x);
assert_eq!(ratsimp(&mut a, inv), inv);
}
}