use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{Signed, ToPrimitive, Zero};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode};
const MAX_DEPTH: usize = 15;
const MAX_WORK: u32 = 20_000;
const SERIES_COST: u32 = 25;
const MAX_TREE_SIZE: usize = 4_000;
#[derive(Debug)]
pub(crate) struct Budget {
dummies: u32,
work: u32,
}
impl Budget {
pub(crate) fn new() -> Self {
Self {
dummies: 0,
work: 0,
}
}
fn tick(&mut self, weight: u32) -> Result<(), crate::base::errors::SymplexError> {
self.work = self.work.saturating_add(weight);
if self.work > MAX_WORK {
tracing::warn!(work = self.work, "gruntz: work budget exhausted");
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz",
reason: "work budget exhausted (expression too complex for the limit engine)"
.into(),
});
}
Ok(())
}
fn charge_size(
&mut self,
arena: &Arena,
e: ExprId,
) -> Result<(), crate::base::errors::SymplexError> {
let size = crate::transforms::pattern::tree_size_capped(arena, e, MAX_TREE_SIZE + 1);
if size > MAX_TREE_SIZE {
tracing::warn!(size, "gruntz: expression too large for series fallback");
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz",
reason: "intermediate expression too large for the limit engine".into(),
});
}
self.tick((size / 8) as u32)
}
#[cfg(test)]
fn work(&self) -> u32 {
self.work
}
}
fn fresh_dummy(arena: &mut Arena, budget: &mut Budget) -> ExprId {
let n = budget.dummies;
budget.dummies += 1;
arena.symbol(&format!("__gw{n}"))
}
fn budgeted_series(
arena: &mut Arena,
budget: &mut Budget,
expr: ExprId,
var: ExprId,
order: u32,
) -> Result<ExprId, crate::base::errors::SymplexError> {
budget.tick(SERIES_COST)?;
let zero = arena.zero();
let pole = || crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::series",
reason: "pole detected at expansion point".into(),
};
let mut terms: Vec<ExprId> = Vec::with_capacity(order as usize);
let mut deriv = expr;
let mut factorial = Ratio::from_integer(BigInt::from(1));
for k in 0..order {
budget.charge_size(arena, deriv)?;
let value = if crate::base::walk::contains(arena, deriv, var) {
crate::calculus::limit::safe_substitute(arena, deriv, var, zero).ok_or_else(pole)?
} else {
crate::transforms::eval::eval(arena, deriv)
};
if is_infinite(arena, value) || value == arena.nan() || contains_singular_atom(arena, value)
{
return Err(pole());
}
if !arena.is_zero_structural(value) {
let term = if k == 0 {
value
} else {
let k_id = arena.int(i64::from(k));
let power = arena.pow(var, k_id);
let coeff_nid =
arena.intern_num(Ratio::from_integer(BigInt::from(1)) / factorial.clone());
let coeff = arena.intern(ExprNode::Num(coeff_nid));
arena.mul(&[value, coeff, power])
};
terms.push(term);
}
if k + 1 < order {
if !crate::base::walk::contains(arena, deriv, var) {
break;
}
deriv = crate::transforms::diff::diff(arena, deriv, var);
deriv = crate::transforms::eval::eval(arena, deriv);
factorial *= Ratio::from_integer(BigInt::from(i64::from(k) + 1));
}
}
Ok(match terms.len() {
0 => zero,
1 => terms[0],
_ => arena.add(&terms),
})
}
#[derive(Clone, Debug)]
struct SubsSet {
exprs: FxHashMap<ExprId, ExprId>,
rewrites: FxHashMap<ExprId, ExprId>,
}
impl SubsSet {
fn new() -> Self {
Self {
exprs: FxHashMap::default(),
rewrites: FxHashMap::default(),
}
}
fn is_empty(&self) -> bool {
self.exprs.is_empty()
}
fn representative(&self) -> Result<ExprId, crate::base::errors::SymplexError> {
self.exprs.keys().next().copied().ok_or_else(|| {
crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz",
reason: "internal: MRV set unexpectedly empty".into(),
}
})
}
fn len(&self) -> usize {
self.exprs.len()
}
fn contains_key(&self, e: &ExprId) -> bool {
self.exprs.contains_key(e)
}
fn get_or_create_dummy(
&mut self,
expr: ExprId,
arena: &mut Arena,
budget: &mut Budget,
) -> ExprId {
if let Some(&d) = self.exprs.get(&expr) {
return d;
}
let d = fresh_dummy(arena, budget);
self.exprs.insert(expr, d);
d
}
fn do_subs(&self, arena: &mut Arena, e: ExprId) -> ExprId {
let mut result = e;
for (&orig, &dummy) in &self.exprs {
result = crate::transforms::subs::subs(arena, result, orig, dummy);
}
result
}
fn undo_subs(&self, arena: &mut Arena, e: ExprId) -> ExprId {
let mut result = e;
for (&orig, &dummy) in &self.exprs {
result = crate::transforms::subs::subs(arena, result, dummy, orig);
}
result
}
fn union_with(&mut self, other: &SubsSet) {
for (&expr, &dummy) in &other.exprs {
self.exprs.entry(expr).or_insert(dummy);
}
for (&dummy, &rewrite) in &other.rewrites {
self.rewrites.entry(dummy).or_insert(rewrite);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GrowthOrder {
Less,
Equal,
Greater,
}
fn compare(
arena: &mut Arena,
a: ExprId,
b: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<GrowthOrder, crate::base::errors::SymplexError> {
tracing::debug!(depth, "gruntz::compare: comparing growth rates");
budget.tick(1)?;
let la = log_of(arena, a);
let lb = log_of(arena, b);
let ratio = arena.div(la, lb);
tracing::trace!("gruntz::compare: computing limitinf of log ratio");
let c = limitinf(arena, ratio, x, depth + 1, budget)?;
if arena.is_zero_structural(c) {
tracing::debug!("gruntz::compare → Less (a grows slower)");
Ok(GrowthOrder::Less)
} else if is_infinite(arena, c) {
tracing::debug!("gruntz::compare → Greater (a grows faster)");
Ok(GrowthOrder::Greater)
} else {
tracing::debug!("gruntz::compare → Equal (same comparability class)");
Ok(GrowthOrder::Equal)
}
}
fn log_of(arena: &mut Arena, e: ExprId) -> ExprId {
match arena.node(e).clone() {
ExprNode::Exp(inner) => {
tracing::trace!("gruntz::log_of: exp(f) → f");
inner
}
_ => {
tracing::trace!("gruntz::log_of: general → ln(|e|)");
let abs_e = arena.abs(e);
arena.ln(abs_e)
}
}
}
fn sign_at_inf(
arena: &mut Arena,
e: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<i32, crate::base::errors::SymplexError> {
if depth > MAX_DEPTH {
tracing::warn!("gruntz::sign_at_inf: max depth exceeded");
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::sign_at_inf",
reason: "maximum recursion depth exceeded".into(),
});
}
budget.tick(1)?;
let e = crate::transforms::eval::eval(arena, e);
if !crate::base::walk::contains(arena, e, x) {
tracing::trace!("gruntz::sign_at_inf: constant expression");
return sign_of_constant(arena, e);
}
if e == x {
tracing::trace!("gruntz::sign_at_inf: e == x → +1 (x → +∞)");
return Ok(1);
}
match arena.node(e).clone() {
ExprNode::Pow(base, exp) if base == x && arena.as_num(exp).is_some() => {
tracing::trace!("gruntz::sign_at_inf: x^c → +1");
return Ok(1);
}
ExprNode::Exp(_) => {
tracing::trace!("gruntz::sign_at_inf: exp() is always positive → +1");
return Ok(1);
}
ExprNode::Neg(inner) => {
return sign_at_inf(arena, inner, x, depth + 1, budget).map(|s| -s);
}
ExprNode::Mul(children) => {
let mut result = 1i32;
for c in children {
let s = sign_at_inf(arena, c, x, depth + 1, budget)?;
if s == 0 {
return Ok(0);
}
result *= s;
}
return Ok(result);
}
_ => {}
}
tracing::trace!("gruntz::sign_at_inf: computing leading term to determine sign");
let (c0, _e0) = mrv_leadterm(arena, e, x, depth + 1, budget)?;
if contains_foreign_dummy(arena, c0, x) {
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::sign_at_inf",
reason: "leading coefficient is not a genuine constant".into(),
});
}
if arena.is_zero_structural(c0) {
return Ok(0);
}
let result = sign_at_inf(arena, c0, x, depth + 1, budget);
tracing::debug!(sign = ?result, "gruntz::sign_at_inf result");
result
}
fn sign_of_constant(
arena: &mut Arena,
e: ExprId,
) -> Result<i32, crate::base::errors::SymplexError> {
match crate::calculus::limit::const_sign(arena, e) {
Some(s) => Ok(s),
None => Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::sign_of_constant",
reason: format!("cannot determine the sign of {}", arena.display(e)),
}),
}
}
fn contains_foreign_dummy(arena: &Arena, e: ExprId, x: ExprId) -> bool {
let mut stack = vec![e];
let mut visited = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
if id != x
&& let ExprNode::Symbol(sid) = arena.node(id)
{
let name = arena.symbol_name(*sid);
if name.starts_with("__gw") || name.starts_with("__lim") {
return true;
}
}
arena.node(id).for_each_child(|c| stack.push(c));
}
false
}
fn mrv(
arena: &mut Arena,
e: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<(SubsSet, ExprId), crate::base::errors::SymplexError> {
if depth > MAX_DEPTH {
tracing::warn!("gruntz::mrv: max depth exceeded");
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::mrv",
reason: "maximum recursion depth exceeded".into(),
});
}
budget.tick(1)?;
let e = crate::transforms::eval::eval(arena, e);
if !crate::base::walk::contains(arena, e, x) {
tracing::trace!("gruntz::mrv: constant → empty MRV set");
return Ok((SubsSet::new(), e));
}
if e == x {
tracing::trace!("gruntz::mrv: e == x → MRV = {{x}}");
let mut s = SubsSet::new();
let d = s.get_or_create_dummy(x, arena, budget);
return Ok((s, d));
}
let node = arena.node(e).clone();
match node {
ExprNode::Add(ref children) | ExprNode::Mul(ref children) => {
let is_add = matches!(node, ExprNode::Add(_));
let children_vec: SmallVec<[ExprId; 6]> = children.clone();
tracing::trace!(
n = children_vec.len(),
kind = if is_add { "Add" } else { "Mul" },
"gruntz::mrv: processing n-ary node"
);
mrv_nary(arena, e, &children_vec, x, depth, is_add, budget)
}
ExprNode::Pow(base, exp) => {
tracing::trace!("gruntz::mrv: Pow node");
if crate::base::walk::contains(arena, exp, x) {
tracing::debug!("gruntz::mrv: Pow with x-dependent exponent → exp(exp*ln(base))");
let ln_base = arena.ln(base);
let product = arena.mul(&[exp, ln_base]);
let as_exp = arena.exp(product);
mrv(arena, as_exp, x, depth + 1, budget)
} else {
let (s, rw_base) = mrv(arena, base, x, depth + 1, budget)?;
let rebuilt = arena.pow(rw_base, exp);
Ok((s, rebuilt))
}
}
ExprNode::Exp(arg) => {
tracing::debug!("gruntz::mrv: Exp node — checking if exponent → ±∞");
if let ExprNode::Ln(inner) = arena.node(arg).clone() {
tracing::trace!("gruntz::mrv: exp(ln(f)) → mrv(f)");
return mrv(arena, inner, x, depth + 1, budget);
}
let li = limitinf(arena, arg, x, depth + 1, budget)?;
let li_is_inf = is_infinite(arena, li);
if li_is_inf {
tracing::debug!("gruntz::mrv: exp(arg) with arg → ∞ — new comparability class");
let mut s1 = SubsSet::new();
let e1 = s1.get_or_create_dummy(e, arena, budget);
let (s2, e2) = mrv(arena, arg, x, depth + 1, budget)?;
let exp_e2 = arena.exp(e2);
mrv_max3(arena, s1, e1, s2, exp_e2, x, depth, budget)
} else {
tracing::debug!("gruntz::mrv: exp(arg) with arg → finite — same class as arg");
let (s, rw_arg) = mrv(arena, arg, x, depth + 1, budget)?;
let rebuilt = arena.exp(rw_arg);
Ok((s, rebuilt))
}
}
ExprNode::Ln(inner) => {
tracing::trace!("gruntz::mrv: Ln node — recurse into argument");
let (s, rw) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.ln(rw)))
}
ExprNode::Neg(inner) => {
let (s, rw) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.neg(rw)))
}
ExprNode::Sin(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.sin(r)))
}
ExprNode::Cos(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.cos(r)))
}
ExprNode::Tan(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.tan(r)))
}
ExprNode::Asin(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.asin(r)))
}
ExprNode::Acos(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.acos(r)))
}
ExprNode::Atan(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.atan(r)))
}
ExprNode::Sinh(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.sinh(r)))
}
ExprNode::Cosh(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.cosh(r)))
}
ExprNode::Tanh(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.tanh(r)))
}
ExprNode::Abs(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.abs(r)))
}
ExprNode::Sign(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.sign(r)))
}
ExprNode::Asinh(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.asinh(r)))
}
ExprNode::Acosh(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.acosh(r)))
}
ExprNode::Atanh(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, arena.atanh(r)))
}
ExprNode::Erf(inner)
| ExprNode::Erfc(inner)
| ExprNode::Gamma(inner)
| ExprNode::LogGamma(inner)
| ExprNode::Digamma(inner)
| ExprNode::LambertW(inner)
| ExprNode::Factorial(inner)
| ExprNode::Heaviside(inner) => {
let (s, r) = mrv(arena, inner, x, depth + 1, budget)?;
Ok((s, apply_unary(arena, &node, r)))
}
_ => {
tracing::trace!("gruntz::mrv: fallback — treating expression as atomic with x");
let mut s = SubsSet::new();
let d = s.get_or_create_dummy(x, arena, budget);
let rw = crate::transforms::subs::subs(arena, e, x, d);
Ok((s, rw))
}
}
}
fn mrv_nary(
arena: &mut Arena,
_original: ExprId,
children: &[ExprId],
x: ExprId,
depth: usize,
is_add: bool,
budget: &mut Budget,
) -> Result<(SubsSet, ExprId), crate::base::errors::SymplexError> {
if children.is_empty() {
return Ok((
SubsSet::new(),
if is_add { arena.zero() } else { arena.one() },
));
}
let mut indep: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut dep: SmallVec<[ExprId; 6]> = SmallVec::new();
for &child in children {
if crate::base::walk::contains(arena, child, x) {
dep.push(child);
} else {
indep.push(child);
}
}
if dep.is_empty() {
return Ok((
SubsSet::new(),
if is_add {
arena.add(children)
} else {
arena.mul(children)
},
));
}
let (mut combined_set, mut rw_first) = mrv(arena, dep[0], x, depth + 1, budget)?;
for &child in &dep[1..] {
let (child_set, rw_child) = mrv(arena, child, x, depth + 1, budget)?;
let (merged, rw_a, rw_b) = mrv_max1(
arena,
&combined_set,
rw_first,
&child_set,
rw_child,
x,
depth,
budget,
)?;
combined_set = merged;
rw_first = if is_add {
arena.add(&[rw_a, rw_b])
} else {
arena.mul(&[rw_a, rw_b])
};
}
if !indep.is_empty() {
if is_add {
indep.push(rw_first);
rw_first = arena.add(&indep);
} else {
indep.push(rw_first);
rw_first = arena.mul(&indep);
}
}
Ok((combined_set, rw_first))
}
#[allow(clippy::too_many_arguments)]
fn mrv_max1(
arena: &mut Arena,
s1: &SubsSet,
e1: ExprId,
s2: &SubsSet,
e2: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<(SubsSet, ExprId, ExprId), crate::base::errors::SymplexError> {
if s1.is_empty() {
return Ok((s2.clone(), e1, e2));
}
if s2.is_empty() {
return Ok((s1.clone(), e1, e2));
}
let a_rep = s1.representative()?;
let b_rep = s2.representative()?;
if a_rep == b_rep {
let mut rw_e2 = e2;
for (&expr, &d1_dummy) in &s1.exprs {
if let Some(&d2_dummy) = s2.exprs.get(&expr)
&& d1_dummy != d2_dummy
{
tracing::trace!("gruntz::mrv_max1: unifying dummy for shared key");
rw_e2 = crate::transforms::subs::subs(arena, rw_e2, d2_dummy, d1_dummy);
}
}
let mut merged = s1.clone();
merged.union_with(s2);
return Ok((merged, e1, rw_e2));
}
tracing::debug!("gruntz::mrv_max1: comparing MRV representatives");
match compare(arena, a_rep, b_rep, x, depth + 1, budget)? {
GrowthOrder::Greater => {
tracing::debug!(
"gruntz::mrv_max1: s1 grows faster — keeping s1, rewriting e2 with s1 dummies"
);
let restored = s2.undo_subs(arena, e2);
let rw_e2 = s1.do_subs(arena, restored);
Ok((s1.clone(), e1, rw_e2))
}
GrowthOrder::Less => {
tracing::debug!(
"gruntz::mrv_max1: s2 grows faster — keeping s2, rewriting e1 with s2 dummies"
);
let restored = s1.undo_subs(arena, e1);
let rw_e1 = s2.do_subs(arena, restored);
Ok((s2.clone(), rw_e1, e2))
}
GrowthOrder::Equal => {
tracing::debug!("gruntz::mrv_max1: same class — merging both sets");
let mut rw_e2 = e2;
for (&expr, &d1_dummy) in &s1.exprs {
if let Some(&d2_dummy) = s2.exprs.get(&expr)
&& d1_dummy != d2_dummy
{
tracing::trace!("gruntz::mrv_max1: unifying dummy in Equal merge");
rw_e2 = crate::transforms::subs::subs(arena, rw_e2, d2_dummy, d1_dummy);
}
}
let mut merged = s1.clone();
merged.union_with(s2);
Ok((merged, e1, rw_e2))
}
}
}
#[allow(clippy::too_many_arguments)]
fn mrv_max3(
arena: &mut Arena,
mut s1: SubsSet, e1: ExprId, s2: SubsSet, exp_e2: ExprId, x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<(SubsSet, ExprId), crate::base::errors::SymplexError> {
if s2.is_empty() {
return Ok((s1, e1));
}
let a_rep = s1.representative()?;
let b_rep = s2.representative()?;
tracing::debug!("gruntz::mrv_max3: comparing exp vs arg MRV");
match compare(arena, a_rep, b_rep, x, depth + 1, budget)? {
GrowthOrder::Greater => {
tracing::debug!("gruntz::mrv_max3: exp dominates — keeping exp as MRV");
Ok((s1, e1))
}
GrowthOrder::Less => {
tracing::debug!("gruntz::mrv_max3: arg's MRV dominates — using that");
Ok((s2, exp_e2))
}
GrowthOrder::Equal => {
tracing::debug!("gruntz::mrv_max3: same class — merging, recording rewrite");
s1.rewrites.insert(e1, exp_e2);
s1.union_with(&s2);
Ok((s1, e1))
}
}
}
fn moveup_expr(arena: &mut Arena, e: ExprId, x: ExprId) -> ExprId {
let exp_x = arena.exp(x);
crate::transforms::subs::subs(arena, e, x, exp_x)
}
fn moveup_subsset(arena: &mut Arena, s: &SubsSet, x: ExprId) -> SubsSet {
let mut result = SubsSet::new();
for (&expr, &dummy) in &s.exprs {
let moved = moveup_expr(arena, expr, x);
result.exprs.insert(moved, dummy);
}
for (&dummy, &rw) in &s.rewrites {
result.rewrites.insert(dummy, moveup_expr(arena, rw, x));
}
result
}
fn rewrite(
arena: &mut Arena,
exps: ExprId,
omega: &SubsSet,
x: ExprId,
wsym: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<(ExprId, ExprId), crate::base::errors::SymplexError> {
if omega.is_empty() {
return Ok((exps, arena.zero()));
}
budget.tick(1)?;
let exps_display = arena.display(exps).to_string();
tracing::debug!(mrv_size = omega.len(), expr = %exps_display, "gruntz::rewrite: starting rewrite");
let entries: Vec<(ExprId, ExprId)> = omega.exprs.iter().map(|(&e, &d)| (e, d)).collect();
let (g_expr, _g_dummy) = entries[entries.len() - 1];
let g_exp = match arena.node(g_expr).clone() {
ExprNode::Exp(inner) => inner,
_ => {
tracing::warn!("gruntz::rewrite: MRV element is not Exp after moveup!");
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::rewrite",
reason: "MRV element is not Exp (moveup may have failed)".into(),
});
}
};
let sig = sign_at_inf(arena, g_exp, x, depth + 1, budget)?;
tracing::debug!(
sign = sig,
"gruntz::rewrite: sign of representative's exponent"
);
let w_actual = if sig >= 0 {
tracing::debug!("gruntz::rewrite: g → ∞, using ω = 1/g (so ω → 0)");
arena.div(arena.one(), wsym) } else {
tracing::debug!("gruntz::rewrite: g → 0, using ω = g (already → 0)");
wsym
};
let _ = w_actual;
let mut subs_table: Vec<(ExprId, ExprId)> = Vec::new();
for &(f_expr, f_dummy) in &entries {
let f_exp = if let Some(&rewrite_form) = omega.rewrites.get(&f_dummy) {
match arena.node(rewrite_form).clone() {
ExprNode::Exp(inner) => inner,
_ => rewrite_form, }
} else {
match arena.node(f_expr).clone() {
ExprNode::Exp(inner) => inner,
_ => {
tracing::warn!("gruntz::rewrite: non-Exp MRV element without rewrite");
continue;
}
}
};
let ratio = arena.div(f_exp, g_exp);
let c = limitinf(arena, ratio, x, depth + 1, budget)?;
let c_display = arena.display(c).to_string();
let f_exp_display = arena.display(f_exp).to_string();
let g_exp_display = arena.display(g_exp).to_string();
tracing::debug!(f_exp = %f_exp_display, g_exp = %g_exp_display, c = %c_display, "gruntz::rewrite: c = limitinf(f_exp/g_exp)");
let c_times_gexp = arena.mul(&[c, g_exp]);
let remainder = arena.sub(f_exp, c_times_gexp);
let remainder = crate::transforms::eval::eval(arena, remainder);
let exp_remainder = if arena.is_zero_structural(remainder) {
arena.one()
} else {
arena.exp(remainder)
};
let w_power = if sig >= 0 { arena.neg(c) } else { c };
let w_to_c = arena.pow(wsym, w_power);
let replacement = arena.mul(&[exp_remainder, w_to_c]);
let replacement = crate::transforms::eval::eval(arena, replacement);
subs_table.push((f_dummy, replacement));
}
let mut f = exps;
for &(dummy, replacement) in &subs_table {
f = crate::transforms::subs::subs(arena, f, dummy, replacement);
}
let pre_simplify = arena.display(f).to_string();
f = simplify_positive_powers(arena, f, wsym);
f = crate::transforms::eval::eval(arena, f);
f = crate::transforms::expand::expand(arena, f);
f = crate::transforms::eval::eval(arena, f);
let post_simplify = arena.display(f).to_string();
tracing::debug!(before = %pre_simplify, after = %post_simplify, "gruntz::rewrite: simplification of rewritten expression");
let logw = if sig >= 0 {
arena.neg(g_exp) } else {
g_exp };
tracing::debug!("gruntz::rewrite: rewrite complete");
Ok((f, logw))
}
#[allow(clippy::too_many_arguments)]
fn leadterm(
arena: &mut Arena,
f: ExprId,
w: ExprId,
logw: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<(ExprId, ExprId), crate::base::errors::SymplexError> {
if depth > MAX_DEPTH {
tracing::warn!("gruntz::leadterm: max depth exceeded");
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "maximum recursion depth exceeded".into(),
});
}
budget.tick(1)?;
if !crate::base::walk::contains(arena, f, w) {
tracing::trace!("gruntz::leadterm: constant wrt ω → (f, 0)");
return Ok((f, arena.zero()));
}
if f == w {
tracing::trace!("gruntz::leadterm: f == ω → (1, 1)");
return Ok((arena.one(), arena.one()));
}
let node = arena.node(f).clone();
match node {
ExprNode::Mul(ref children) => {
tracing::trace!(n = children.len(), "gruntz::leadterm: Mul");
let mut total_coeff = arena.one();
let mut total_exp = arena.zero();
for &child in children {
let (c, e) = leadterm(arena, child, w, logw, x, depth + 1, budget)?;
total_coeff = arena.mul(&[total_coeff, c]);
total_exp = arena.add(&[total_exp, e]);
}
total_coeff = crate::transforms::eval::eval(arena, total_coeff);
total_exp = crate::transforms::eval::eval(arena, total_exp);
Ok((total_coeff, total_exp))
}
ExprNode::Pow(base, exp) if !crate::base::walk::contains(arena, exp, w) => {
tracing::trace!("gruntz::leadterm: Pow with constant exponent");
let (c_b, e_b) = leadterm(arena, base, w, logw, x, depth + 1, budget)?;
if crate::base::walk::contains(arena, c_b, w) {
match arena.as_num(exp) {
Some(r) if r.is_integer() && r.is_positive() => {}
_ => return Err(oscillation_err()),
}
}
let new_coeff = arena.pow(c_b, exp);
let new_exp = arena.mul(&[e_b, exp]);
Ok((
crate::transforms::eval::eval(arena, new_coeff),
crate::transforms::eval::eval(arena, new_exp),
))
}
ExprNode::Pow(base, exp) => {
tracing::trace!("gruntz::leadterm: Pow with ω-dependent exponent → exp(g·ln b)");
let ln_b = arena.ln(base);
let prod = arena.mul(&[exp, ln_b]);
let as_exp = arena.exp(prod);
leadterm(arena, as_exp, w, logw, x, depth + 1, budget)
}
ExprNode::Add(ref children) => {
tracing::trace!(
n = children.len(),
"gruntz::leadterm: Add — finding dominant term"
);
if children.is_empty() {
return Ok((arena.zero(), arena.zero()));
}
let mut terms: Vec<(ExprId, ExprId, Ratio<BigInt>)> = Vec::new();
for &child in children {
let (c, e) = leadterm(arena, child, w, logw, x, depth + 1, budget)?;
let e_eval = crate::transforms::eval::eval(arena, e);
let e_num = arena.as_num(e_eval).cloned();
if let Some(r) = e_num {
terms.push((c, e_eval, r));
} else {
terms.push((c, e_eval, Ratio::from_integer(BigInt::from(0))));
}
}
terms.sort_by(|a, b| a.2.cmp(&b.2));
let min_exp = terms[0].2.clone();
let mut coeff_sum = arena.zero();
let leading_exp_id = terms[0].1;
for (c, _, e_val) in &terms {
if *e_val == min_exp {
coeff_sum = arena.add(&[coeff_sum, *c]);
}
}
coeff_sum = crate::transforms::eval::eval(arena, coeff_sum);
let needs_series = arena.is_zero_structural(coeff_sum)
|| crate::base::walk::contains(arena, coeff_sum, w);
if needs_series {
let coeff_display = arena.display(coeff_sum).to_string();
let f_display = arena.display(f).to_string();
tracing::debug!(
coeff = %coeff_display,
full_expr = %f_display,
"gruntz::leadterm: Add coefficients cancel or depend on ω, using series expansion"
);
return leadterm_add_by_series(arena, f, w, logw, &min_exp, x, depth, budget);
}
Ok((coeff_sum, leading_exp_id))
}
ExprNode::Neg(inner) => {
tracing::trace!("gruntz::leadterm: Neg");
let (c, e) = leadterm(arena, inner, w, logw, x, depth + 1, budget)?;
Ok((arena.neg(c), e))
}
ExprNode::Exp(arg) if crate::base::walk::contains(arena, arg, w) => {
tracing::trace!("gruntz::leadterm: Exp(arg) where arg depends on ω");
let (c_arg, e_arg) = leadterm(arena, arg, w, logw, x, depth + 1, budget)?;
let e_eval = crate::transforms::eval::eval(arena, e_arg);
if let Some(r) = arena.as_num(e_eval) {
let r = r.clone();
if r.is_positive() {
return Ok((arena.one(), arena.zero()));
}
if r.is_zero() {
let coeff = arena.exp(c_arg);
return Ok((crate::transforms::eval::eval(arena, coeff), arena.zero()));
}
}
Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "exp of a divergent argument was not captured by the MRV set".into(),
})
}
ExprNode::Ln(arg) if crate::base::walk::contains(arena, arg, w) => {
tracing::trace!("gruntz::leadterm: Ln(arg) where arg depends on ω");
let (c_arg, e_arg) = leadterm(arena, arg, w, logw, x, depth + 1, budget)?;
if crate::base::walk::contains(arena, c_arg, w) {
return Err(oscillation_err());
}
let e_eval = crate::transforms::eval::eval(arena, e_arg);
if arena.is_zero_structural(e_eval) {
let coeff = arena.ln(c_arg);
let coeff = crate::transforms::eval::eval(arena, coeff);
if !arena.is_zero_structural(coeff) {
return Ok((coeff, arena.zero()));
}
let delta = arena.sub(arg, c_arg);
let delta = crate::transforms::eval::eval(arena, delta);
if !arena.is_zero_structural(delta) && crate::base::walk::contains(arena, delta, w)
{
tracing::debug!("gruntz::leadterm: ln(arg) with arg→1, using ln(1+δ) ≈ δ");
return leadterm(arena, delta, w, logw, x, depth + 1, budget);
}
return Ok((arena.zero(), arena.zero()));
}
let ln_c = arena.ln(c_arg);
let e_logw = arena.mul(&[e_arg, logw]);
let coeff = arena.add(&[ln_c, e_logw]);
Ok((crate::transforms::eval::eval(arena, coeff), arena.zero()))
}
ExprNode::Sin(inner)
| ExprNode::Cos(inner)
| ExprNode::Tan(inner)
| ExprNode::Asin(inner)
| ExprNode::Acos(inner)
| ExprNode::Atan(inner)
| ExprNode::Sinh(inner)
| ExprNode::Cosh(inner)
| ExprNode::Tanh(inner)
| ExprNode::Asinh(inner)
| ExprNode::Acosh(inner)
| ExprNode::Atanh(inner)
| ExprNode::Erf(inner)
| ExprNode::Erfc(inner)
| ExprNode::Gamma(inner)
| ExprNode::LogGamma(inner)
| ExprNode::Digamma(inner)
| ExprNode::LambertW(inner)
| ExprNode::Factorial(inner)
| ExprNode::Abs(inner)
| ExprNode::Sign(inner)
| ExprNode::Heaviside(inner)
if crate::base::walk::contains(arena, inner, w) =>
{
unary_leadterm(arena, f, &node, inner, w, logw, x, depth, budget)
}
_ => {
tracing::debug!("gruntz::leadterm: fallback — trying series expansion");
if let Ok(series) = budgeted_series(arena, budget, f, w, 4) {
let evaled = crate::transforms::eval::eval(arena, series);
if evaled != f && !crate::base::walk::contains(arena, evaled, w) {
return Ok((evaled, arena.zero()));
}
if evaled != f {
return leadterm(arena, evaled, w, logw, x, depth + 1, budget);
}
}
Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: format!(
"cannot determine the leading behaviour of {}",
arena.display(f)
),
})
}
}
}
fn oscillation_err() -> crate::base::errors::SymplexError {
crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "unbounded function of a bounded oscillation has no limit".into(),
}
}
fn unary_is_bounded(template: &ExprNode) -> bool {
matches!(
template,
ExprNode::Sin(_)
| ExprNode::Cos(_)
| ExprNode::Atan(_)
| ExprNode::Tanh(_)
| ExprNode::Erf(_)
| ExprNode::Erfc(_)
| ExprNode::Abs(_)
| ExprNode::Sign(_)
| ExprNode::Heaviside(_)
)
}
fn apply_unary(arena: &mut Arena, template: &ExprNode, arg: ExprId) -> ExprId {
match template {
ExprNode::Sin(_) => arena.sin(arg),
ExprNode::Cos(_) => arena.cos(arg),
ExprNode::Tan(_) => arena.tan(arg),
ExprNode::Asin(_) => arena.asin(arg),
ExprNode::Acos(_) => arena.acos(arg),
ExprNode::Atan(_) => arena.atan(arg),
ExprNode::Sinh(_) => arena.sinh(arg),
ExprNode::Cosh(_) => arena.cosh(arg),
ExprNode::Tanh(_) => arena.tanh(arg),
ExprNode::Asinh(_) => arena.asinh(arg),
ExprNode::Acosh(_) => arena.acosh(arg),
ExprNode::Atanh(_) => arena.atanh(arg),
ExprNode::Erf(_) => arena.erf(arg),
ExprNode::Erfc(_) => arena.erfc(arg),
ExprNode::Gamma(_) => arena.gamma(arg),
ExprNode::LogGamma(_) => arena.log_gamma(arg),
ExprNode::Digamma(_) => arena.digamma(arg),
ExprNode::LambertW(_) => arena.lambertw(arg),
ExprNode::Factorial(_) => arena.factorial(arg),
ExprNode::Abs(_) => arena.abs(arg),
ExprNode::Sign(_) => arena.sign(arg),
ExprNode::Heaviside(_) => arena.heaviside(arg),
ExprNode::Exp(_) => arena.exp(arg),
ExprNode::Ln(_) => arena.ln(arg),
_ => arg,
}
}
fn unary_is_singular_at(arena: &Arena, template: &ExprNode, c: ExprId) -> bool {
let r = arena.as_num(c);
match template {
ExprNode::Gamma(_) | ExprNode::LogGamma(_) | ExprNode::Digamma(_) => match r {
Some(r) => r.is_integer() && !r.is_positive(),
None => false,
},
ExprNode::Factorial(_) => match r {
Some(r) => r.is_integer() && r.is_negative(),
None => false,
},
ExprNode::Atanh(_) => match r {
Some(r) => r.abs() == Ratio::from_integer(BigInt::from(1)),
None => false,
},
ExprNode::Sign(_) | ExprNode::Heaviside(_) => r.is_some_and(|r| r.is_zero()),
_ => false,
}
}
#[allow(clippy::too_many_arguments)]
fn unary_leadterm(
arena: &mut Arena,
f: ExprId,
template: &ExprNode,
inner: ExprId,
w: ExprId,
logw: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<(ExprId, ExprId), crate::base::errors::SymplexError> {
let (c_in, e_in) = leadterm(arena, inner, w, logw, x, depth + 1, budget)?;
let e_eval = crate::transforms::eval::eval(arena, e_in);
let Some(r) = arena.as_num(e_eval).cloned() else {
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "non-numeric exponent in function argument".into(),
});
};
let zero = arena.zero();
if r.is_positive() {
if unary_is_singular_at(arena, template, zero) {
if crate::base::walk::contains(arena, c_in, w) {
return Err(oscillation_err());
}
if matches!(template, ExprNode::Gamma(_) | ExprNode::Digamma(_)) {
let neg_e = arena.neg(e_in);
let inv_c = arena.pow(c_in, arena.neg_one());
let inv_c = crate::transforms::eval::eval(arena, inv_c);
let coeff = if matches!(template, ExprNode::Gamma(_)) {
inv_c
} else {
arena.neg(inv_c)
};
return Ok((coeff, crate::transforms::eval::eval(arena, neg_e)));
}
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "function has a pole at the limit of its argument".into(),
});
}
tracing::trace!("gruntz::leadterm: F(inner) with inner → 0, expanding F at 0");
let t = fresh_dummy(arena, budget);
let ft = apply_unary(arena, template, t);
let p = budgeted_series(arena, budget, ft, t, 6)?;
let p0 = crate::transforms::subs::subs(arena, p, t, zero);
let p0 = crate::transforms::eval::eval(arena, p0);
if !arena.is_zero_structural(p0) {
return Ok((p0, zero));
}
let p_inner = crate::transforms::subs::subs(arena, p, t, inner);
let p_inner = crate::transforms::eval::eval(arena, p_inner);
if arena.is_zero_structural(p_inner) || p_inner == f {
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "series expansion of function did not resolve the leading term".into(),
});
}
return leadterm(arena, p_inner, w, logw, x, depth + 1, budget);
}
if r.is_zero() {
if crate::base::walk::contains(arena, c_in, w) && !unary_is_bounded(template) {
return Err(oscillation_err());
}
if unary_is_singular_at(arena, template, c_in) {
if let ExprNode::Gamma(_) = template
&& let Some(cn) = arena.as_num(c_in).cloned()
&& cn.is_integer()
&& !cn.is_positive()
{
let n = (-cn.to_integer()).to_u64().unwrap_or(0);
let mut fact = BigInt::from(1u64);
for i in 2..=n {
fact *= BigInt::from(i);
}
let sign = if n % 2 == 0 { 1 } else { -1 };
let coeff_rat = Ratio::from_integer(BigInt::from(sign)) / Ratio::from_integer(fact);
let coeff = {
let nid = arena.intern_num(coeff_rat);
arena.intern(ExprNode::Num(nid))
};
let delta = arena.sub(inner, c_in);
let delta = crate::transforms::eval::eval(arena, delta);
if arena.is_zero_structural(delta) {
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "Γ evaluated exactly at a pole".into(),
});
}
let inv_delta = arena.pow(delta, arena.neg_one());
let approx = arena.mul(&[coeff, inv_delta]);
return leadterm(arena, approx, w, logw, x, depth + 1, budget);
}
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "function has a pole at the limit of its argument".into(),
});
}
let v = apply_unary(arena, template, c_in);
let v = crate::transforms::eval::eval(arena, v);
if is_infinite(arena, v) || v == arena.nan() {
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "function value is infinite at the limit of its argument".into(),
});
}
if !arena.is_zero_structural(v) {
return Ok((v, zero));
}
let delta = arena.sub(inner, c_in);
let delta = crate::transforms::eval::eval(arena, delta);
if arena.is_zero_structural(delta) {
return Ok((zero, zero));
}
tracing::trace!("gruntz::leadterm: F(c) = 0, expanding F(c + δ)");
let t = fresh_dummy(arena, budget);
let arg = arena.add(&[c_in, t]);
let ft = apply_unary(arena, template, arg);
let p = budgeted_series(arena, budget, ft, t, 6)?;
let p_delta = crate::transforms::subs::subs(arena, p, t, delta);
let p_delta = crate::transforms::eval::eval(arena, p_delta);
if arena.is_zero_structural(p_delta) || p_delta == f {
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "series expansion of function did not resolve the leading term".into(),
});
}
return leadterm(arena, p_delta, w, logw, x, depth + 1, budget);
}
let s = if crate::base::walk::contains(arena, c_in, x) {
sign_at_inf(arena, c_in, x, depth + 1, budget)?
} else {
sign_of_constant(arena, c_in)?
};
let pos = s > 0;
let unbounded = || {
Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "unbounded function of a divergent argument".into(),
})
};
match template {
ExprNode::Atan(_) => {
let two = arena.int(2);
let pi = arena.pi();
let half_pi = arena.div(pi, two);
let v = if pos { half_pi } else { arena.neg(half_pi) };
Ok((v, zero))
}
ExprNode::Erf(_) | ExprNode::Tanh(_) | ExprNode::Sign(_) => {
let v = if pos { arena.one() } else { arena.neg_one() };
Ok((v, zero))
}
ExprNode::Erfc(_) => {
if pos {
unbounded()
} else {
Ok((arena.int(2), zero))
}
}
ExprNode::Heaviside(_) => Ok((if pos { arena.one() } else { zero }, zero)),
ExprNode::Abs(_) => {
if s == 0 {
return unbounded();
}
let c = if pos { c_in } else { arena.neg(c_in) };
Ok((crate::transforms::eval::eval(arena, c), e_in))
}
ExprNode::Sin(_) | ExprNode::Cos(_) => Ok((f, zero)),
ExprNode::LambertW(_) if pos => {
let ln_inner = arena.ln(inner);
leadterm(arena, ln_inner, w, logw, x, depth + 1, budget)
}
_ => unbounded(),
}
}
#[allow(clippy::too_many_arguments)]
fn leadterm_add_by_series(
arena: &mut Arena,
f: ExprId,
w: ExprId,
logw: ExprId,
min_exp: &Ratio<BigInt>,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<(ExprId, ExprId), crate::base::errors::SymplexError> {
let min_exp_id = {
let nid = arena.intern_num(min_exp.clone());
arena.intern(ExprNode::Num(nid))
};
budget.charge_size(arena, f)?;
if let Ok(normalized) = normalize_poles(arena, f, w, logw, x, depth, budget) {
let neg_min = {
let nid = arena.intern_num(-min_exp.clone());
arena.intern(ExprNode::Num(nid))
};
let w_shift = arena.pow(w, neg_min);
let g = arena.mul(&[normalized, w_shift]);
let g = simplify_positive_powers(arena, g, w);
let g = crate::transforms::eval::eval(arena, g);
budget.charge_size(arena, g)?;
let g = crate::transforms::expand::expand(arena, g);
let g = crate::transforms::eval::eval(arena, g);
let g_display = arena.display(g).to_string();
tracing::debug!(regular = %g_display, "gruntz::leadterm_add_by_series: normalised expression");
for order in [3u32, 5, 8, 12] {
match budgeted_series(arena, budget, g, w, order) {
Ok(s) => {
let h = crate::transforms::expand::expand(arena, s);
let h = crate::transforms::eval::eval(arena, h);
if arena.is_zero_structural(h) {
continue;
}
let h_display = arena.display(h).to_string();
tracing::debug!(order, series = %h_display, "gruntz::leadterm_add_by_series: series");
let (c, e) = leadterm(arena, h, w, logw, x, depth + 1, budget)?;
if arena.is_zero_structural(c) {
continue;
}
let e_total = arena.add(&[e, min_exp_id]);
let e_total = crate::transforms::eval::eval(arena, e_total);
return Ok((c, e_total));
}
Err(_) => break,
}
}
}
let mut agreed: Option<(ExprId, ExprId)> = None;
for order in [6u32, 10, 14] {
let func_expanded = expand_functions_as_series(arena, budget, f, w, order)?;
if func_expanded == f {
break;
}
let simplified = crate::transforms::eval::eval(arena, func_expanded);
budget.charge_size(arena, simplified)?;
let simplified = crate::transforms::expand::expand(arena, simplified);
let simplified = crate::transforms::eval::eval(arena, simplified);
budget.charge_size(arena, simplified)?;
if simplified == f {
break;
}
if arena.is_zero_structural(simplified) {
agreed = None;
continue;
}
let Ok((c, e)) = leadterm(arena, simplified, w, logw, x, depth + 1, budget) else {
break;
};
let c = crate::transforms::eval::eval(arena, c);
let e = crate::transforms::eval::eval(arena, e);
match agreed {
Some((c0, e0)) if c0 == c && e0 == e => {
tracing::debug!(
"gruntz::leadterm_add_by_series: function-series strategy agreed at two consecutive orders"
);
return Ok((c, e));
}
_ => agreed = Some((c, e)),
}
}
for order in 2..10 {
if let Ok(series) = budgeted_series(arena, budget, f, w, order) {
let expanded = crate::transforms::expand::expand(arena, series);
let evaled = crate::transforms::eval::eval(arena, expanded);
if evaled != f && !arena.is_zero_structural(evaled) {
return leadterm(arena, evaled, w, logw, x, depth + 1, budget);
}
} else {
break;
}
}
Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::leadterm",
reason: "leading coefficients cancel and series expansion failed".into(),
})
}
fn simplify_positive_powers(arena: &mut Arena, f: ExprId, w: ExprId) -> ExprId {
let post = crate::base::walk::post_order_ids(arena, f);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for &id in &post {
if !crate::base::walk::contains(arena, id, w) {
cache.insert(id, id);
continue;
}
let rebuilt = crate::base::walk::rebuild_with_cache(arena, id, &cache);
let new = match arena.node(rebuilt).clone() {
ExprNode::Pow(base, b) if arena.as_num(b).is_some() => match arena.node(base).clone() {
ExprNode::Pow(inner, a) if inner == w && arena.as_num(a).is_some() => {
let ab = arena.mul(&[a, b]);
let ab = crate::transforms::eval::eval(arena, ab);
arena.pow(w, ab)
}
ExprNode::Mul(children) => {
let mut w_exp = Ratio::from_integer(BigInt::from(0));
let mut rest: SmallVec<[ExprId; 4]> = SmallVec::new();
for &c in &children {
if c == w {
w_exp += Ratio::from_integer(BigInt::from(1));
} else if let ExprNode::Pow(inner, a) = arena.node(c).clone()
&& inner == w
&& let Some(r) = arena.as_num(a)
{
w_exp += r.clone();
} else {
rest.push(c);
}
}
let b_int = arena.as_num(b).is_some_and(|r| r.is_integer());
let rest_positive = rest.iter().all(|&c| {
arena.as_num(c).is_some_and(|r| r.is_positive())
|| matches!(arena.node(c), ExprNode::Exp(_))
});
if w_exp.is_zero() || !(b_int || rest_positive) {
rebuilt
} else {
let w_exp_id = {
let nid = arena.intern_num(w_exp);
arena.intern(ExprNode::Num(nid))
};
let ab = arena.mul(&[w_exp_id, b]);
let ab = crate::transforms::eval::eval(arena, ab);
let w_part = arena.pow(w, ab);
let rest_mul = arena.mul(&rest);
let rest_part = arena.pow(rest_mul, b);
arena.mul(&[w_part, rest_part])
}
}
_ => rebuilt,
},
_ => rebuilt,
};
cache.insert(id, new);
}
cache.get(&f).copied().unwrap_or(f)
}
#[allow(clippy::too_many_arguments)]
fn normalize_poles(
arena: &mut Arena,
f: ExprId,
w: ExprId,
logw: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<ExprId, crate::base::errors::SymplexError> {
let post = crate::base::walk::post_order_ids(arena, f);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for &id in &post {
if !crate::base::walk::contains(arena, id, w) {
cache.insert(id, id);
continue;
}
let rebuilt = crate::base::walk::rebuild_with_cache(arena, id, &cache);
let node = arena.node(rebuilt).clone();
let new = match node {
ExprNode::Pow(base, k)
if base != w
&& !crate::base::walk::contains(arena, k, w)
&& crate::base::walk::contains(arena, base, w)
&& matches!(arena.node(base), ExprNode::Add(_)) =>
{
let (_, e) = leadterm(arena, base, w, logw, x, depth + 1, budget)?;
let e = crate::transforms::eval::eval(arena, e);
if arena.as_num(e).is_some() && !arena.is_zero_structural(e) {
let neg_e = arena.neg(e);
let w_neg_e = arena.pow(w, neg_e);
let scaled = arena.mul(&[base, w_neg_e]);
let scaled = crate::transforms::expand::expand(arena, scaled);
let scaled = crate::transforms::eval::eval(arena, scaled);
let ek = arena.mul(&[e, k]);
let w_ek = arena.pow(w, ek);
let regular_pow = arena.pow(scaled, k);
arena.mul(&[w_ek, regular_pow])
} else {
rebuilt
}
}
ExprNode::Pow(base, k) if crate::base::walk::contains(arena, k, w) => {
let ln_b = arena.ln(base);
let prod = arena.mul(&[k, ln_b]);
arena.exp(prod)
}
ExprNode::Ln(arg) => {
let (_, e) = leadterm(arena, arg, w, logw, x, depth + 1, budget)?;
let e = crate::transforms::eval::eval(arena, e);
if arena.as_num(e).is_some() && !arena.is_zero_structural(e) {
let neg_e = arena.neg(e);
let w_neg_e = arena.pow(w, neg_e);
let scaled = arena.mul(&[arg, w_neg_e]);
let scaled = crate::transforms::expand::expand(arena, scaled);
let scaled = crate::transforms::eval::eval(arena, scaled);
let e_logw = arena.mul(&[e, logw]);
let ln_scaled = arena.ln(scaled);
arena.add(&[e_logw, ln_scaled])
} else {
rebuilt
}
}
_ => rebuilt,
};
cache.insert(id, new);
}
Ok(cache.get(&f).copied().unwrap_or(f))
}
fn mrv_leadterm(
arena: &mut Arena,
e: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<(ExprId, ExprId), crate::base::errors::SymplexError> {
if depth > MAX_DEPTH {
tracing::warn!("gruntz::mrv_leadterm: max depth exceeded");
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::mrv_leadterm",
reason: "maximum recursion depth exceeded".into(),
});
}
budget.tick(1)?;
if !crate::base::walk::contains(arena, e, x) {
return Ok((e, arena.zero()));
}
let e_display = arena.display(e).to_string();
tracing::debug!(depth, expr = %e_display, "gruntz::mrv_leadterm: Step 1 — computing MRV set");
let (omega, exps) = mrv(arena, e, x, depth + 1, budget)?;
if omega.is_empty() {
return Ok((exps, arena.zero()));
}
let exps_display = arena.display(exps).to_string();
let mrv_keys: Vec<String> = omega
.exprs
.keys()
.map(|k| arena.display(*k).to_string())
.collect();
tracing::debug!(
mrv_size = omega.len(),
mrv_keys = ?mrv_keys,
rewritten = %exps_display,
"gruntz::mrv_leadterm: MRV set computed"
);
let (omega, exps) = if omega.contains_key(&x) {
tracing::debug!("gruntz::mrv_leadterm: Step 2 — x in MRV, moving up (x → exp(x))");
let omega_up = moveup_subsset(arena, &omega, x);
let exps_up = moveup_expr(arena, exps, x);
let up_display = arena.display(exps_up).to_string();
let up_keys: Vec<String> = omega_up
.exprs
.keys()
.map(|k| arena.display(*k).to_string())
.collect();
tracing::debug!(moved_expr = %up_display, moved_mrv = ?up_keys, "gruntz::mrv_leadterm: after moveup");
(omega_up, exps_up)
} else {
tracing::trace!("gruntz::mrv_leadterm: x not in MRV, no moveup needed");
(omega, exps)
};
if omega.is_empty() {
return Ok((exps, arena.zero()));
}
tracing::debug!("gruntz::mrv_leadterm: Step 3 — rewriting in terms of ω");
let w = fresh_dummy(arena, budget);
let (f, logw) = rewrite(arena, exps, &omega, x, w, depth, budget)?;
let f_display = arena.display(f).to_string();
let w_display = arena.display(w).to_string();
let logw_display = arena.display(logw).to_string();
tracing::debug!(rewritten_f = %f_display, w = %w_display, logw = %logw_display, "gruntz::mrv_leadterm: Step 4 — extracting leading term from rewritten expression");
let (c0, e0) = leadterm(arena, f, w, logw, x, depth + 1, budget)?;
let c0_display = arena.display(c0).to_string();
let e0_display = arena.display(e0).to_string();
tracing::debug!(c0 = %c0_display, e0 = %e0_display, "gruntz::mrv_leadterm: leading term extracted");
let ln_w = arena.ln(w);
let c0 = crate::transforms::subs::subs(arena, c0, ln_w, logw);
tracing::debug!("gruntz::mrv_leadterm: done");
Ok((c0, e0))
}
pub(crate) fn limitinf(
arena: &mut Arena,
e: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<ExprId, crate::base::errors::SymplexError> {
if depth > MAX_DEPTH {
tracing::warn!(depth, "gruntz::limitinf: max recursion depth exceeded");
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::limitinf",
reason: "maximum recursion depth exceeded".into(),
});
}
budget.tick(1)?;
let e = crate::transforms::eval::eval(arena, e);
if !crate::base::walk::contains(arena, e, x) {
tracing::trace!("gruntz::limitinf: constant → returning as-is");
return Ok(e);
}
let e = rewrite_tractable(arena, e, x, depth, budget)?;
if !crate::base::walk::contains(arena, e, x) {
let v = crate::transforms::eval::eval(arena, e);
return Ok(v);
}
let e_display = arena.display(e).to_string();
let x_display = arena.display(x).to_string();
tracing::debug!(depth, expr = %e_display, var = %x_display, "gruntz::limitinf: computing limit at infinity");
let (c0, e0) = mrv_leadterm(arena, e, x, depth, budget)?;
let e0_eval = crate::transforms::eval::eval(arena, e0);
let c0_display = arena.display(c0).to_string();
let e0_display = arena.display(e0_eval).to_string();
tracing::debug!(c0 = %c0_display, e0 = %e0_display, "gruntz::limitinf: leading term extracted, checking exponent sign");
if arena.is_zero_structural(c0) {
return Ok(arena.zero());
}
let e0_sign = if let Some(r) = arena.as_num(e0_eval) {
let r = r.clone();
if r.is_positive() {
1
} else if r.is_negative() {
-1
} else {
0
}
} else {
sign_at_inf(arena, e0_eval, x, depth + 1, budget)?
};
if contains_foreign_dummy(arena, c0, x) {
if e0_sign > 0 {
tracing::info!("gruntz::limitinf: bounded × vanishing → limit is 0");
return Ok(arena.zero());
}
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::limitinf",
reason: "expression has no limit (bounded oscillation)".into(),
});
}
match e0_sign {
s if s > 0 => {
tracing::info!("gruntz::limitinf: e0 > 0 → limit is 0");
Ok(arena.zero())
}
s if s < 0 => {
let c0_sign = sign_at_inf(arena, c0, x, depth + 1, budget)?;
tracing::info!(c0_sign, "gruntz::limitinf: e0 < 0 → limit is ±∞");
if c0_sign > 0 {
Ok(arena.infinity())
} else if c0_sign < 0 {
Ok(arena.neg_infinity())
} else {
Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz::limitinf",
reason: "indeterminate 0·∞ form in leading term".into(),
})
}
}
_ => {
tracing::debug!("gruntz::limitinf: e0 = 0 → recursing on coefficient c0");
limitinf(arena, c0, x, depth + 1, budget)
}
}
}
fn needs_tractable_rewrite(arena: &Arena, e: ExprId) -> bool {
let mut stack = vec![e];
let mut visited = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
let node = arena.node(id);
if matches!(
node,
ExprNode::Tan(_)
| ExprNode::Sinh(_)
| ExprNode::Cosh(_)
| ExprNode::Tanh(_)
| ExprNode::Asinh(_)
| ExprNode::Acosh(_)
| ExprNode::Atanh(_)
| ExprNode::Abs(_)
| ExprNode::Sign(_)
| ExprNode::Heaviside(_)
| ExprNode::DiracDelta(_)
| ExprNode::Floor(_)
| ExprNode::Ceiling(_)
| ExprNode::Piecewise(_)
| ExprNode::Min(_)
| ExprNode::Max(_)
) {
return true;
}
node.for_each_child(|c| stack.push(c));
}
false
}
fn rewrite_tractable(
arena: &mut Arena,
e: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Result<ExprId, crate::base::errors::SymplexError> {
if !needs_tractable_rewrite(arena, e) {
return Ok(e);
}
let post = crate::base::walk::post_order_ids(arena, e);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for &id in &post {
if !crate::base::walk::contains(arena, id, x) {
cache.insert(id, id);
continue;
}
let rebuilt = crate::base::walk::rebuild_with_cache(arena, id, &cache);
let node = arena.node(rebuilt).clone();
let new = match node {
ExprNode::Tan(u) => {
let s = arena.sin(u);
let c = arena.cos(u);
arena.div(s, c)
}
ExprNode::Sinh(u) => {
let neg_u = arena.neg(u);
let ep = arena.exp(u);
let em = arena.exp(neg_u);
let diff = arena.sub(ep, em);
let two = arena.int(2);
arena.div(diff, two)
}
ExprNode::Cosh(u) => {
let neg_u = arena.neg(u);
let ep = arena.exp(u);
let em = arena.exp(neg_u);
let sum = arena.add(&[ep, em]);
let two = arena.int(2);
arena.div(sum, two)
}
ExprNode::Tanh(u) => {
let two = arena.int(2);
let two_u = arena.mul(&[two, u]);
let e2u = arena.exp(two_u);
let one = arena.one();
let num = arena.sub(e2u, one);
let den = arena.add(&[e2u, one]);
arena.div(num, den)
}
ExprNode::Asinh(u) => {
let two = arena.int(2);
let u2 = arena.pow(u, two);
let one = arena.one();
let inner = arena.add(&[u2, one]);
let root = arena.sqrt(inner);
let sum = arena.add(&[u, root]);
arena.ln(sum)
}
ExprNode::Acosh(u) => {
let two = arena.int(2);
let u2 = arena.pow(u, two);
let one = arena.one();
let inner = arena.sub(u2, one);
let root = arena.sqrt(inner);
let sum = arena.add(&[u, root]);
arena.ln(sum)
}
ExprNode::Atanh(u) => {
let one = arena.one();
let p = arena.add(&[one, u]);
let m = arena.sub(one, u);
let lp = arena.ln(p);
let lm = arena.ln(m);
let diff = arena.sub(lp, lm);
let two = arena.int(2);
arena.div(diff, two)
}
ExprNode::Abs(u) => match sign_at_inf(arena, u, x, depth + 1, budget) {
Ok(s) if s > 0 => u,
Ok(s) if s < 0 => arena.neg(u),
_ => rebuilt,
},
ExprNode::Sign(u) => match sign_at_inf(arena, u, x, depth + 1, budget) {
Ok(s) => arena.int(s as i64),
Err(_) => rebuilt,
},
ExprNode::Heaviside(u) => match sign_at_inf(arena, u, x, depth + 1, budget) {
Ok(s) if s > 0 => arena.one(),
Ok(s) if s < 0 => arena.zero(),
_ => rebuilt,
},
ExprNode::DiracDelta(u) => match sign_at_inf(arena, u, x, depth + 1, budget) {
Ok(s) if s != 0 => arena.zero(),
_ => rebuilt,
},
ExprNode::Floor(u) | ExprNode::Ceiling(u) => {
let is_floor = matches!(node, ExprNode::Floor(_));
match limitinf(arena, u, x, depth + 1, budget) {
Ok(l) if !is_infinite(arena, l) => match arena.as_num(l).cloned() {
Some(r) if r.is_integer() => {
let n_id = l;
let diff = arena.sub(u, n_id);
match sign_at_inf(arena, diff, x, depth + 1, budget) {
Ok(s) => {
let one = arena.one();
if s > 0 {
if is_floor {
n_id
} else {
arena.add(&[n_id, one])
}
} else if s < 0 {
if is_floor { arena.sub(n_id, one) } else { n_id }
} else {
n_id
}
}
Err(_) => rebuilt,
}
}
Some(r) => {
let v = if is_floor { r.floor() } else { r.ceil() };
let nid = arena.intern_num(v);
arena.intern(ExprNode::Num(nid))
}
None => rebuilt,
},
_ => rebuilt,
}
}
ExprNode::Min(ref args) | ExprNode::Max(ref args) => {
let is_min = matches!(node, ExprNode::Min(_));
let args = args.clone();
let mut best = args[0];
let mut ok = true;
for &a in &args[1..] {
let diff = arena.sub(a, best);
match sign_at_inf(arena, diff, x, depth + 1, budget) {
Ok(s) => {
if (is_min && s < 0) || (!is_min && s > 0) {
best = a;
}
}
Err(_) => {
ok = false;
break;
}
}
}
if ok { best } else { rebuilt }
}
ExprNode::Piecewise(ref pairs) => {
let pairs = pairs.clone();
let mut chosen = None;
for &(val, cond) in &pairs {
match eventually_true(arena, cond, x, depth, budget) {
Some(true) => {
chosen = Some(val);
break;
}
Some(false) => continue,
None => break,
}
}
chosen.unwrap_or(rebuilt)
}
_ => rebuilt,
};
cache.insert(id, new);
}
let result = cache.get(&e).copied().unwrap_or(e);
if result != e {
let r_display = arena.display(result).to_string();
tracing::debug!(rewritten = %r_display, "gruntz::rewrite_tractable");
}
Ok(crate::transforms::eval::eval(arena, result))
}
fn eventually_true(
arena: &mut Arena,
cond: ExprId,
x: ExprId,
depth: usize,
budget: &mut Budget,
) -> Option<bool> {
if depth > MAX_DEPTH {
return None;
}
let node = arena.node(cond).clone();
match node {
ExprNode::BoolTrue => Some(true),
ExprNode::BoolFalse => Some(false),
ExprNode::Gt(a, b) | ExprNode::Ge(a, b) | ExprNode::Eq_(a, b) | ExprNode::Ne(a, b) => {
let diff = arena.sub(a, b);
let s = sign_at_inf(arena, diff, x, depth + 1, budget).ok()?;
Some(match node {
ExprNode::Gt(..) => s > 0,
ExprNode::Ge(..) => s >= 0,
ExprNode::Eq_(..) => s == 0,
_ => s != 0,
})
}
ExprNode::And(children) => {
let mut all_true = true;
for c in children {
match eventually_true(arena, c, x, depth + 1, budget) {
Some(true) => {}
Some(false) => return Some(false),
None => all_true = false,
}
}
if all_true { Some(true) } else { None }
}
ExprNode::Or(children) => {
let mut all_false = true;
for c in children {
match eventually_true(arena, c, x, depth + 1, budget) {
Some(true) => return Some(true),
Some(false) => {}
None => all_false = false,
}
}
if all_false { Some(false) } else { None }
}
ExprNode::Not(inner) => eventually_true(arena, inner, x, depth + 1, budget).map(|b| !b),
_ => None,
}
}
fn validate_result(
arena: &Arena,
r: ExprId,
x: ExprId,
) -> Result<ExprId, crate::base::errors::SymplexError> {
if crate::base::walk::contains(arena, r, x) || contains_foreign_dummy(arena, r, x) {
return Err(crate::base::errors::SymplexError::ComputationFailed {
operation: "gruntz",
reason: "expression has no limit or the limit could not be determined".into(),
});
}
Ok(r)
}
pub(crate) fn limit_pos_inf(
arena: &mut Arena,
e: ExprId,
x: ExprId,
) -> Result<ExprId, crate::base::errors::SymplexError> {
let mut budget = Budget::new();
let r = limitinf(arena, e, x, 0, &mut budget)?;
validate_result(arena, r, x)
}
pub(crate) fn gruntz(
arena: &mut Arena,
e: ExprId,
z: ExprId,
z0: ExprId,
) -> Result<ExprId, crate::base::errors::SymplexError> {
let mut budget = Budget::new();
gruntz_with_budget(arena, e, z, z0, &mut budget)
}
fn gruntz_with_budget(
arena: &mut Arena,
e: ExprId,
z: ExprId,
z0: ExprId,
budget: &mut Budget,
) -> Result<ExprId, crate::base::errors::SymplexError> {
tracing::info!("gruntz: entry point");
if z0 == arena.infinity() {
tracing::debug!("gruntz: limit at +∞");
let r = limitinf(arena, e, z, 0, budget)?;
return validate_result(arena, r, z);
}
if z0 == arena.neg_infinity() {
tracing::debug!("gruntz: limit at -∞, substituting z = -x");
let x = fresh_dummy(arena, budget);
let neg_x = arena.neg(x);
let e_sub = crate::transforms::subs::subs(arena, e, z, neg_x);
let r = limitinf(arena, e_sub, x, 0, budget)?;
let r = validate_result(arena, r, x)?;
return validate_result(arena, r, z);
}
let e_display = arena.display(e).to_string();
let z0_display = arena.display(z0).to_string();
tracing::debug!(expr = %e_display, z0 = %z0_display, "gruntz: finite-point limit, substituting z = z0 + 1/x");
let x = fresh_dummy(arena, budget);
let one = arena.one();
let inv_x = arena.div(one, x);
let z0_plus_inv_x = arena.add(&[z0, inv_x]);
let e_sub = crate::transforms::subs::subs(arena, e, z, z0_plus_inv_x);
let sub_display = arena.display(e_sub).to_string();
tracing::debug!(after_sub = %sub_display, "gruntz: after z = z0 + 1/x substitution");
let e_together = crate::poly::polybridge::together(arena, e_sub);
let e_cancelled = crate::poly::polybridge::cancel(arena, e_together, x);
let e_simplified = crate::transforms::eval::eval(arena, e_cancelled);
let e_simplified = crate::transforms::expand::expand(arena, e_simplified);
let e_simplified = crate::transforms::eval::eval(arena, e_simplified);
let simplified_display = arena.display(e_simplified).to_string();
tracing::debug!(simplified = %simplified_display, "gruntz: finite-point expression simplified, calling limitinf");
let r = limitinf(arena, e_simplified, x, 0, budget)?;
let r = validate_result(arena, r, x)?;
validate_result(arena, r, z)
}
fn expand_functions_as_series(
arena: &mut Arena,
budget: &mut Budget,
expr: ExprId,
w: ExprId,
order: u32,
) -> Result<ExprId, crate::base::errors::SymplexError> {
let post_order = crate::base::walk::post_order_ids(arena, expr);
let mut result = expr;
for &id in &post_order {
budget.tick(1)?;
let should_expand = match arena.node(id).clone() {
ExprNode::Exp(arg)
| ExprNode::Ln(arg)
| ExprNode::Sin(arg)
| ExprNode::Cos(arg)
| ExprNode::Tan(arg)
| ExprNode::Asin(arg)
| ExprNode::Atan(arg)
| ExprNode::Sinh(arg)
| ExprNode::Cosh(arg)
| ExprNode::Tanh(arg)
| ExprNode::Asinh(arg)
| ExprNode::Atanh(arg)
| ExprNode::Erf(arg) => crate::base::walk::contains(arena, arg, w),
ExprNode::Pow(base, e) => {
crate::base::walk::contains(arena, base, w)
&& arena.as_num(e).is_some_and(|r| !r.is_integer())
}
_ => false,
};
if should_expand && let Ok(series) = budgeted_series(arena, budget, id, w, order) {
let expanded = crate::transforms::expand::expand(arena, series);
let evaled = crate::transforms::eval::eval(arena, expanded);
if contains_singular_atom(arena, evaled) {
continue;
}
let old_display = arena.display(id).to_string();
let new_display = arena.display(evaled).to_string();
tracing::trace!(
original = %old_display,
series = %new_display,
"gruntz::expand_functions_as_series: expanded function"
);
result = crate::transforms::subs::subs(arena, result, id, evaled);
budget.charge_size(arena, result)?;
}
}
Ok(result)
}
fn is_infinite(arena: &Arena, e: ExprId) -> bool {
e == arena.infinity() || e == arena.neg_infinity() || e == arena.complex_infinity()
}
fn contains_singular_atom(arena: &Arena, e: ExprId) -> bool {
crate::base::walk::contains(arena, e, arena.infinity())
|| crate::base::walk::contains(arena, e, arena.neg_infinity())
|| crate::base::walk::contains(arena, e, arena.complex_infinity())
|| crate::base::walk::contains(arena, e, arena.nan())
}
#[cfg(test)]
mod tests {
use super::*;
fn sym(a: &mut Arena, name: &str) -> ExprId {
a.symbol(name)
}
fn display(a: &Arena, id: ExprId) -> String {
a.display(id).to_string()
}
#[test]
fn gruntz_constant() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let inf = a.infinity();
let result = gruntz(&mut a, five, x, inf).unwrap();
assert_eq!(display(&a, result), "5");
}
#[test]
fn gruntz_1_over_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let expr = a.div(one, x);
let inf = a.infinity();
let result = gruntz(&mut a, expr, x, inf).unwrap();
assert_eq!(display(&a, result), "0", "lim(1/x, x→∞) = 0");
}
#[test]
fn gruntz_x_over_x_plus_1() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let denom = a.add(&[x, one]);
let expr = a.div(x, denom);
let inf = a.infinity();
let result = gruntz(&mut a, expr, x, inf).unwrap();
assert_eq!(display(&a, result), "1", "lim(x/(x+1), x→∞) = 1");
}
#[test]
fn gruntz_3x2_plus_1_over_x2_plus_1() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let three = a.int(3);
let two = a.int(2);
let x_sq = a.pow(x, two);
let three_x_sq = a.mul(&[three, x_sq]);
let numer = a.add(&[three_x_sq, one]);
let denom = a.add(&[x_sq, one]);
let expr = a.div(numer, denom);
let inf = a.infinity();
let result = gruntz(&mut a, expr, x, inf).unwrap();
assert_eq!(display(&a, result), "3", "lim((3x²+1)/(x²+1), x→∞) = 3");
}
#[test]
fn gruntz_x_over_x2_plus_1() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let two = a.int(2);
let x_sq = a.pow(x, two);
let denom = a.add(&[x_sq, one]);
let expr = a.div(x, denom);
let inf = a.infinity();
let result = gruntz(&mut a, expr, x, inf).unwrap();
assert_eq!(display(&a, result), "0", "lim(x/(x²+1), x→∞) = 0");
}
#[test]
fn gruntz_2x_plus_1_over_x_plus_1() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let two = a.int(2);
let two_x = a.mul(&[two, x]);
let numer = a.add(&[two_x, one]);
let denom = a.add(&[x, one]);
let expr = a.div(numer, denom);
let inf = a.infinity();
let result = gruntz(&mut a, expr, x, inf).unwrap();
assert_eq!(display(&a, result), "2", "lim((2x+1)/(x+1), x→∞) = 2");
}
#[test]
fn gruntz_exp_neg_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let neg_x = a.neg(x);
let expr = a.exp(neg_x);
let inf = a.infinity();
let result = gruntz(&mut a, expr, x, inf).unwrap();
assert_eq!(display(&a, result), "0", "lim(exp(-x), x→∞) = 0");
}
#[test]
fn gruntz_x_times_exp_neg_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let neg_x = a.neg(x);
let exp_neg_x = a.exp(neg_x);
let expr = a.mul(&[x, exp_neg_x]);
let inf = a.infinity();
let result = gruntz(&mut a, expr, x, inf).unwrap();
assert_eq!(display(&a, result), "0", "lim(x·exp(-x), x→∞) = 0");
}
#[test]
fn gruntz_exp_x_over_x2() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let numer = a.exp(x);
let denom = a.pow(x, two);
let expr = a.div(numer, denom);
let inf = a.infinity();
let result = gruntz(&mut a, expr, x, inf).unwrap();
assert!(
is_infinite(&a, result),
"lim(exp(x)/x², x→∞) = ∞, got: {}",
display(&a, result)
);
}
#[test]
fn gruntz_ln_x_over_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let ln_x = a.ln(x);
let expr = a.div(ln_x, x);
let inf = a.infinity();
let result = gruntz(&mut a, expr, x, inf).unwrap();
assert_eq!(display(&a, result), "0", "lim(ln(x)/x, x→∞) = 0");
}
#[test]
fn gruntz_sin_x_over_x_at_0() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sin_x = a.sin(x);
let expr = a.div(sin_x, x);
let zero = a.zero();
let result = gruntz(&mut a, expr, x, zero).unwrap();
assert_eq!(display(&a, result), "1", "lim(sin(x)/x, x→0) = 1");
}
#[test]
fn gruntz_1_minus_cos_over_x2_at_0() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let cos_x = a.cos(x);
let numer = a.sub(one, cos_x);
let two = a.int(2);
let x_sq = a.pow(x, two);
let expr = a.div(numer, x_sq);
let zero = a.zero();
let result = gruntz(&mut a, expr, x, zero).unwrap();
assert_eq!(display(&a, result), "1/2", "lim((1-cos(x))/x², x→0) = 1/2");
}
#[test]
fn gruntz_exp_minus_1_over_x_at_0() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let exp_x = a.exp(x);
let numer = a.sub(exp_x, one);
let expr = a.div(numer, x);
let zero = a.zero();
let result = gruntz(&mut a, expr, x, zero).unwrap();
assert_eq!(display(&a, result), "1", "lim((exp(x)-1)/x, x→0) = 1");
}
#[test]
fn gruntz_exp_minus_1_minus_x_over_x2_at_0() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let exp_x = a.exp(x);
let exp_m1 = a.sub(exp_x, one);
let numer = a.sub(exp_m1, x);
let two = a.int(2);
let x_sq = a.pow(x, two);
let expr = a.div(numer, x_sq);
let zero = a.zero();
let result = gruntz(&mut a, expr, x, zero).unwrap();
assert_eq!(
display(&a, result),
"1/2",
"lim((exp(x)-1-x)/x², x→0) = 1/2"
);
}
#[test]
fn gruntz_x2_minus_1_over_x_minus_1_at_1() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let two = a.int(2);
let x_sq = a.pow(x, two);
let numer = a.sub(x_sq, one);
let denom = a.sub(x, one);
let expr = a.div(numer, denom);
let result = gruntz(&mut a, expr, x, one).unwrap();
assert_eq!(display(&a, result), "2", "lim((x²-1)/(x-1), x→1) = 2");
}
#[test]
fn gruntz_exp_x_at_neg_inf() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.exp(x);
let neg_inf = a.neg_infinity();
let result = gruntz(&mut a, expr, x, neg_inf).unwrap();
assert_eq!(display(&a, result), "0", "lim(exp(x), x→-∞) = 0");
}
#[test]
fn gruntz_polynomial_at_2() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let two_c = a.int(2);
let x_sq = a.pow(x, two_c);
let expr = a.add(&[x_sq, one]);
let two = a.int(2);
let result = gruntz(&mut a, expr, x, two).unwrap();
assert_eq!(display(&a, result), "5", "lim(x²+1, x→2) = 5");
}
#[test]
fn budget_headroom_on_hard_limits() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let inf = a.infinity();
let zero = a.zero();
let one = a.one();
let two = a.int(2);
let three = a.int(3);
let tan_x = a.tan(x);
let sin_x = a.sin(x);
let num = a.sub(tan_x, sin_x);
let x3 = a.pow(x, three);
let e1 = a.div(num, x3);
let neg_x = a.neg(x);
let exp_neg_x = a.exp(neg_x);
let arg = a.sub(x, exp_neg_x);
let e_arg = a.exp(arg);
let exp_x = a.exp(x);
let e2 = a.sub(e_arg, exp_x);
let x2 = a.pow(x, two);
let inv_x2 = a.div(one, x2);
let sin2 = a.pow(sin_x, two);
let inv_sin2 = a.div(one, sin2);
let e3 = a.sub(inv_x2, inv_sin2);
for (e, p, want) in [(e1, zero, "1/2"), (e2, inf, "-1"), (e3, zero, "-1/3")] {
let mut budget = Budget::new();
let r = gruntz_with_budget(&mut a, e, x, p, &mut budget).unwrap();
assert_eq!(display(&a, r), want);
assert!(
budget.work() < MAX_WORK / 4,
"work {} too close to the cap {MAX_WORK} for {}",
budget.work(),
display(&a, e)
);
}
}
#[test]
fn exhausted_budget_fails_cleanly() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let inf = a.infinity();
let neg_x = a.neg(x);
let exp_neg_x = a.exp(neg_x);
let e = a.mul(&[x, exp_neg_x]);
let mut budget = Budget {
dummies: 0,
work: MAX_WORK,
};
let err = gruntz_with_budget(&mut a, e, x, inf, &mut budget).unwrap_err();
assert!(err.to_string().contains("budget"), "{err}");
}
}