use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{Signed, Zero};
use rustc_hash::FxHashMap;
use crate::base::arena::Arena;
use crate::base::errors::SymplexError;
use crate::base::extended::Extended;
use crate::base::node::{ExprId, ExprNode};
use crate::base::numeric::Q;
use crate::calculus::calculus_util::is_hidden_zero;
const MAX_LHOPITAL: usize = 5;
const MAX_COMPOSE_DEPTH: usize = 48;
const LIM_DUMMY: &str = "__lim_w";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Direction {
Left,
Right,
#[default]
Both,
}
fn fail(reason: impl Into<String>) -> SymplexError {
SymplexError::ComputationFailed {
operation: "limit",
reason: reason.into(),
}
}
pub(crate) fn limit(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
) -> Result<ExprId, SymplexError> {
limit_dir_generic(arena, expr, var, point, Direction::Both)
}
pub(crate) fn limit_dir(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
dir: Direction,
) -> Result<ExprId, SymplexError> {
let value = limit_dir_generic(arena, expr, var, point, dir)?;
Ok(guard_degenerate_parameters(
arena, expr, var, point, dir, value,
))
}
pub(crate) fn limit_dir_generic(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
dir: Direction,
) -> Result<ExprId, SymplexError> {
if !matches!(arena.node(var), ExprNode::Symbol(_)) {
return Err(SymplexError::InvalidArgument {
operation: "limit",
reason: "the limit variable must be a symbol".into(),
});
}
limit_impl(arena, expr, var, point, dir, 0)
}
const MAX_DEGENERATE_VALUES: usize = 4;
fn guard_degenerate_parameters(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
dir: Direction,
value: ExprId,
) -> ExprId {
let params: Vec<ExprId> = crate::base::walk::free_symbols(arena, expr)
.into_iter()
.filter(|&s| s != var)
.filter(|&s| match arena.node(s) {
ExprNode::Symbol(sid) => !arena.symbol_name(*sid).starts_with('_'),
_ => false,
})
.collect();
let [p] = params.as_slice() else {
return value;
};
let p = *p;
let p_sym = match arena.node(p) {
ExprNode::Symbol(s) => *s,
_ => return value,
};
let mut candidates: Vec<ExprId> = Vec::new();
for id in crate::base::walk::post_order_ids(arena, expr) {
if !crate::base::walk::contains(arena, id, var) {
continue;
}
let node = arena.node(id).clone();
let mut free_part: Vec<ExprId> = Vec::new();
node.for_each_child(|c| {
if !crate::base::walk::contains(arena, c, var)
&& crate::base::walk::has_free_symbol(arena, c, p_sym)
{
candidates.push(c);
if matches!(node, ExprNode::Add(_)) {
free_part.push(c);
}
}
});
if let ExprNode::Add(children) = &node {
let constant: Vec<ExprId> = children
.iter()
.copied()
.filter(|&c| !crate::base::walk::contains(arena, c, var))
.collect();
if free_part.len() < constant.len() && !free_part.is_empty() {
let s = arena.add(&constant);
candidates.push(s);
}
}
}
candidates.sort_unstable();
candidates.dedup();
let mut values: Vec<ExprId> = Vec::new();
for c in candidates {
for s in crate::transforms::solve::solve(arena, c, p) {
let v = crate::transforms::eval::eval(arena, s.value);
if crate::base::walk::free_symbols(arena, v).is_empty()
&& is_valid_limit_value(arena, v, var)
&& v != arena.infinity()
&& v != arena.neg_infinity()
&& !values.contains(&v)
{
values.push(v);
}
}
if values.len() >= MAX_DEGENERATE_VALUES {
break;
}
}
values.truncate(MAX_DEGENERATE_VALUES);
let mut special: Vec<(ExprId, ExprId)> = Vec::new();
let mut excluded: Vec<ExprId> = Vec::new();
for v in values {
let diff = arena.sub(p, v);
let diff = crate::transforms::eval::eval(arena, diff);
let mut cache = crate::base::assumptions::AssumptionCache::new();
if cache.query(arena, diff, crate::base::assumptions::Props::NONZERO) == Some(true) {
continue;
}
let claimed = crate::transforms::subs::subs(arena, value, p, v);
let claimed = crate::transforms::eval::eval(arena, claimed);
if !is_valid_limit_value(arena, claimed, var) {
continue;
}
let at_v = crate::transforms::subs::subs(arena, expr, p, v);
let at_v = crate::transforms::eval::eval(arena, at_v);
match limit_impl(arena, at_v, var, point, dir, 1) {
Ok(actual) if same_value(arena, actual, claimed) => {}
Ok(actual) => special.push((actual, v)),
Err(_) => excluded.push(v),
}
}
if special.is_empty() && excluded.is_empty() {
return value;
}
let mut pairs: Vec<(ExprId, ExprId)> = Vec::with_capacity(special.len() + 1);
for (actual, v) in special {
let cond = arena.eq_(p, v);
pairs.push((actual, cond));
}
let generic = if excluded.is_empty() {
arena.bool_true()
} else {
let conds: Vec<ExprId> = excluded.iter().map(|&v| arena.ne_(p, v)).collect();
arena.and(&conds)
};
pairs.push((value, generic));
arena.piecewise(&pairs)
}
fn limit_impl(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
dir: Direction,
depth: usize,
) -> Result<ExprId, SymplexError> {
if !crate::base::walk::contains(arena, expr, var) {
let v = crate::transforms::eval::eval(arena, expr);
return if is_valid_limit_value(arena, v, var) {
Ok(v)
} else {
Err(fail(
"expression is a constant that is not a valid limit value",
))
};
}
if point == arena.infinity() || point == arena.neg_infinity() {
let positive = point == arena.infinity();
return limit_at_infinity_dir(arena, expr, var, positive, depth);
}
if point == arena.nan() || point == arena.complex_infinity() {
return Err(fail("limit point must be finite or ±∞"));
}
tracing::debug!(dir = ?dir, "limit: finite point");
if let Some(v) = try_direct_substitution(arena, expr, var, point) {
tracing::debug!("limit: safe direct substitution succeeded");
return Ok(v);
}
if depth < MAX_COMPOSE_DEPTH
&& let Some(r) = try_compose(arena, expr, var, point, dir, depth)
{
return r;
}
let directional = has_directional_nodes(arena, expr, var);
match dir {
Direction::Right | Direction::Left => {
let right = dir == Direction::Right;
match one_sided_gruntz(arena, expr, var, point, right) {
Ok(v) => Ok(v),
Err(e) => {
if directional {
return Err(e);
}
analytic_fallback(arena, expr, var, point).map_err(|_| e)
}
}
}
Direction::Both => {
let r = one_sided_gruntz(arena, expr, var, point, true);
let l = one_sided_gruntz(arena, expr, var, point, false);
match (r, l) {
(Ok(r), Ok(l)) => {
if same_value(arena, r, l) {
Ok(r)
} else {
let rs = arena.display(r).to_string();
let ls = arena.display(l).to_string();
Err(fail(format!(
"left and right limits differ: left = {ls}, right = {rs}"
)))
}
}
(Ok(_), Err(e)) | (Err(e), Ok(_)) | (Err(e), Err(_)) => {
if directional {
return Err(e);
}
analytic_fallback(arena, expr, var, point).map_err(|_| e)
}
}
}
}
}
pub(crate) fn is_valid_limit_value(arena: &Arena, id: ExprId, var: ExprId) -> bool {
if id == arena.nan() || id == arena.complex_infinity() {
return false;
}
if id == arena.infinity() || id == arena.neg_infinity() {
return true;
}
if crate::base::walk::contains(arena, id, var) || contains_dummy(arena, id) {
return false;
}
if crate::base::walk::has_unevaluated(arena, id) {
return false;
}
!contains_singular_atom(arena, id) && !contains_hidden_singularity(arena, id)
}
fn contains_hidden_singularity(arena: &Arena, root: ExprId) -> bool {
let mut stack = vec![root];
let mut visited = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
let node = arena.node(id);
match node {
ExprNode::Ln(a) | ExprNode::LogGamma(a) if arena.is_zero_structural(*a) => {
return true;
}
ExprNode::Gamma(a) | ExprNode::Digamma(a) if is_nonpositive_integer(arena, *a) => {
return true;
}
ExprNode::Factorial(a) => {
if let Some(r) = arena.as_num(*a)
&& r.is_integer()
&& r.is_negative()
{
return true;
}
}
ExprNode::Pow(b, e) if arena.is_zero_structural(*b) => match arena.as_num(*e) {
Some(r) if r.is_positive() => {}
_ => return true,
},
_ => {}
}
node.for_each_child(|c| stack.push(c));
}
false
}
fn contains_singular_atom(arena: &Arena, id: ExprId) -> bool {
crate::base::walk::contains(arena, id, arena.infinity())
|| crate::base::walk::contains(arena, id, arena.neg_infinity())
|| crate::base::walk::contains(arena, id, arena.complex_infinity())
|| crate::base::walk::contains(arena, id, arena.nan())
}
fn is_singular(arena: &Arena, id: ExprId) -> bool {
id == arena.infinity()
|| id == arena.neg_infinity()
|| id == arena.complex_infinity()
|| id == arena.nan()
|| contains_singular_atom(arena, id)
}
pub(crate) fn contains_dummy(arena: &Arena, root: ExprId) -> bool {
let mut stack = vec![root];
let mut visited = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
if 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 same_value(arena: &mut Arena, a: ExprId, b: ExprId) -> bool {
if a == b {
return true;
}
let a_inf = a == arena.infinity() || a == arena.neg_infinity();
let b_inf = b == arena.infinity() || b == arena.neg_infinity();
if a_inf || b_inf {
return false;
}
if let (Some(ra), Some(rb)) = (arena.as_num(a), arena.as_num(b)) {
return ra == rb;
}
let diff = arena.sub(a, b);
let diff = crate::transforms::eval::eval(arena, diff);
if arena.is_zero_structural(diff) || is_hidden_zero(arena, diff) {
return true;
}
let simplified = crate::transforms::expand::expand(arena, diff);
let simplified = crate::transforms::eval::eval(arena, simplified);
arena.is_zero_structural(simplified) || is_hidden_zero(arena, simplified)
}
pub(crate) fn has_directional_nodes(arena: &Arena, expr: ExprId, var: ExprId) -> bool {
let mut stack = vec![expr];
let mut visited = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
let node = arena.node(id);
let directional = matches!(
node,
ExprNode::Abs(_)
| ExprNode::Sign(_)
| ExprNode::Heaviside(_)
| ExprNode::DiracDelta(_)
| ExprNode::Floor(_)
| ExprNode::Ceiling(_)
| ExprNode::Piecewise(_)
| ExprNode::Min(_)
| ExprNode::Max(_)
);
if directional && crate::base::walk::contains(arena, id, var) {
return true;
}
node.for_each_child(|c| stack.push(c));
}
false
}
fn try_direct_substitution(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
) -> Option<ExprId> {
let v = safe_substitute(arena, expr, var, point)?;
if is_valid_limit_value(arena, v, var) && v != arena.infinity() && v != arena.neg_infinity() {
Some(v)
} else {
None
}
}
pub(crate) fn safe_substitute(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
) -> Option<ExprId> {
let post = crate::base::walk::post_order_ids(arena, expr);
let mut values: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for &id in &post {
if !crate::base::walk::contains(arena, id, var) {
continue;
}
let node = arena.node(id).clone();
let val_of = |arena: &mut Arena, values: &FxHashMap<ExprId, ExprId>, c: ExprId| {
if let Some(&v) = values.get(&c) {
v
} else {
crate::transforms::eval::eval(arena, c)
}
};
if !matches!(
node,
ExprNode::Ln(_)
| ExprNode::LogGamma(_)
| ExprNode::Pow(..)
| ExprNode::Asin(_)
| ExprNode::Acos(_)
| ExprNode::Acosh(_)
| ExprNode::Atanh(_)
| ExprNode::Asinh(_)
| ExprNode::Atan(_)
) && let Some((a, cut)) = branch_cut_of(arena, id)
&& crate::base::walk::contains(arena, a, var)
{
let av = val_of(arena, &values, a);
if on_branch_cut(arena, cut, av) && !inner_known_real(arena, a) {
return None;
}
}
match node {
ExprNode::Piecewise(_) => return None,
ExprNode::Ln(a) | ExprNode::LogGamma(a) => {
let av = val_of(arena, &values, a);
if is_zero_value(arena, av) || !is_definite_constant(arena, av) {
return None;
}
if on_branch_cut(arena, BranchCut::NegativeReals, av) && !inner_known_real(arena, a)
{
return None;
}
}
ExprNode::Asin(a) | ExprNode::Acos(a) | ExprNode::Acosh(a) | ExprNode::Atanh(a) => {
let av = val_of(arena, &values, a);
let cut = if matches!(node, ExprNode::Acosh(_)) {
BranchCut::BelowOne
} else {
BranchCut::BeyondOne
};
if on_branch_cut(arena, cut, av) && !inner_known_real(arena, a) {
return None;
}
if let ExprNode::Atanh(_) = node {
let one = arena.one();
let minus = arena.sub(av, one);
let plus = arena.add(&[av, one]);
let (minus, plus) = (
crate::transforms::eval::eval(arena, minus),
crate::transforms::eval::eval(arena, plus),
);
if is_zero_value(arena, minus) || is_zero_value(arena, plus) {
return None;
}
}
}
ExprNode::Asinh(a) | ExprNode::Atan(a) => {
let av = val_of(arena, &values, a);
if on_branch_cut(arena, BranchCut::ImaginaryAxis, av) {
return None;
}
}
ExprNode::Gamma(a) | ExprNode::Digamma(a) => {
let av = val_of(arena, &values, a);
if is_nonpositive_integer(arena, av) {
return None;
}
}
ExprNode::Factorial(a) => {
let av = val_of(arena, &values, a);
if let Some(r) = arena.as_num(av)
&& r.is_integer()
&& r.is_negative()
{
return None;
}
}
ExprNode::Tan(a) => {
let av = val_of(arena, &values, a);
let c = arena.cos(av);
let c = crate::transforms::eval::eval(arena, c);
if is_zero_value(arena, c) {
return None;
}
}
ExprNode::Pow(b, e) => {
let bv = val_of(arena, &values, b);
let ev = val_of(arena, &values, e);
if is_zero_value(arena, bv) {
match arena.as_num(ev) {
Some(r) if r.is_positive() => {}
_ => return None,
}
}
if arena.as_num(e).is_none_or(|r| !r.is_integer())
&& on_branch_cut(arena, BranchCut::NegativeReals, bv)
&& !inner_known_real(arena, b)
{
return None;
}
}
ExprNode::Sign(a) | ExprNode::Heaviside(a) | ExprNode::DiracDelta(a) => {
let av = val_of(arena, &values, a);
match arena.as_num(av) {
Some(r) if !r.is_zero() => {}
_ => return None,
}
}
ExprNode::Floor(a) | ExprNode::Ceiling(a) => {
let av = val_of(arena, &values, a);
match arena.as_num(av) {
Some(r) if !r.is_integer() => {}
_ => return None,
}
}
_ => {}
}
let subst = crate::transforms::subs::subs(arena, id, var, point);
let v = crate::transforms::eval::eval(arena, subst);
if is_singular(arena, v) {
return None;
}
values.insert(id, v);
}
values.get(&expr).copied()
}
const MAX_EXPAND_SIZE: usize = 400;
const MAX_EXPANSION_COST: f64 = 4_000.0;
fn expansion_cost_estimate(arena: &Arena, e: ExprId) -> f64 {
let post = crate::base::walk::post_order_ids(arena, e);
let mut terms: FxHashMap<ExprId, f64> = FxHashMap::default();
let mut cost = 0.0;
for &id in &post {
let t = |c: ExprId| terms.get(&c).copied().unwrap_or(1.0);
let v = match arena.node(id) {
ExprNode::Add(cs) => cs.iter().map(|&c| t(c)).sum(),
ExprNode::Mul(cs) => cs.iter().map(|&c| t(c)).product(),
ExprNode::Pow(b, n) => match arena.as_num(*n) {
Some(r) if r.is_integer() && r.is_positive() => {
let k = num_traits::ToPrimitive::to_f64(r).unwrap_or(f64::INFINITY);
t(*b).powf(k)
}
_ => 1.0,
},
_ => 1.0,
};
let v: f64 = v.min(1e12);
cost += v;
terms.insert(id, v);
}
cost
}
fn is_zero_value(arena: &Arena, v: ExprId) -> bool {
arena.is_zero_structural(v) || is_hidden_zero(arena, v)
}
fn is_definite_constant(arena: &mut Arena, id: ExprId) -> bool {
arena.as_num(id).is_some() || const_sign(arena, id).is_some()
}
fn is_nonpositive_integer(arena: &Arena, id: ExprId) -> bool {
match arena.as_num(id) {
Some(r) => r.is_integer() && !r.is_positive(),
None => false,
}
}
pub(crate) fn const_sign(arena: &mut Arena, e: ExprId) -> Option<i32> {
const_sign_depth(arena, e, 0)
}
fn const_sign_depth(arena: &mut Arena, e: ExprId, depth: usize) -> Option<i32> {
if depth > 16 {
return None;
}
if let Some(r) = arena.as_num(e) {
return Some(sign_of_ratio(r));
}
if e == arena.pi() || e == arena.e_const() || e == arena.infinity() {
return Some(1);
}
if e == arena.neg_infinity() {
return Some(-1);
}
if e == arena.nan() || e == arena.complex_infinity() || e == arena.i_unit() {
return None;
}
let evaled = crate::transforms::eval::eval(arena, e);
if evaled != e
&& let Some(r) = arena.as_num(evaled)
{
return Some(sign_of_ratio(r));
}
match arena.node(e).clone() {
ExprNode::Neg(inner) => const_sign_depth(arena, inner, depth + 1).map(|s| -s),
ExprNode::Mul(children) => {
let mut result = 1;
for c in children {
let s = const_sign_depth(arena, c, depth + 1)?;
if s == 0 {
return Some(0);
}
result *= s;
}
Some(result)
}
ExprNode::Add(children) => {
let mut sign = 0;
for c in children {
let s = const_sign_depth(arena, c, depth + 1)?;
if s == 0 {
continue;
}
if sign == 0 {
sign = s;
} else if sign != s {
return assumption_sign(arena, e).or_else(|| numeric_sign(arena, e));
}
}
Some(sign)
}
ExprNode::Exp(a) | ExprNode::Cosh(a) => {
if const_sign_depth(arena, a, depth + 1).is_some() {
Some(1)
} else {
assumption_sign(arena, e).or_else(|| numeric_sign(arena, e))
}
}
ExprNode::Abs(inner) => {
let s = const_sign_depth(arena, inner, depth + 1)?;
Some(if s == 0 { 0 } else { 1 })
}
ExprNode::Pow(base, exp) => {
let bs = const_sign_depth(arena, base, depth + 1)?;
if let Some(r) = arena.as_num(exp).cloned() {
if r.is_integer() {
let odd = (r.to_integer() % BigInt::from(2)) != BigInt::zero();
if bs == 0 {
return if r.is_positive() { Some(0) } else { None };
}
return Some(if odd { bs } else { 1 });
}
if bs > 0 {
return Some(1);
}
if bs == 0 && r.is_positive() {
return Some(0);
}
return None;
}
if bs > 0 && const_sign_depth(arena, exp, depth + 1).is_some() {
Some(1)
} else {
assumption_sign(arena, e).or_else(|| numeric_sign(arena, e))
}
}
ExprNode::Ln(inner) => {
let r = arena.as_num(inner).cloned()?;
if !r.is_positive() {
return None;
}
let one = Ratio::from_integer(BigInt::from(1));
Some(if r > one {
1
} else if r == one {
0
} else {
-1
})
}
_ => assumption_sign(arena, e).or_else(|| numeric_sign(arena, e)),
}
}
fn numeric_sign(arena: &mut Arena, e: ExprId) -> Option<i32> {
if !crate::base::walk::free_symbols(arena, e).is_empty()
|| crate::base::walk::has_unevaluated(arena, e)
{
return None;
}
let z = crate::transforms::evalf::evalf_complex(arena, e, NUMERIC_SIGN_DIGITS).ok()?;
if z.0.is_zero() && z.1.is_zero() {
return Some(0);
}
let mut reals = crate::base::assumptions::AssumptionCache::new();
let real =
crate::transforms::realness::constant_realness(arena, e, NUMERIC_SIGN_DIGITS, &mut reals);
if real != Some(true) {
return None;
}
if z.0.is_positive() {
Some(1)
} else if z.0.is_negative() {
Some(-1)
} else {
None
}
}
const NUMERIC_SIGN_DIGITS: u32 = 20;
fn sign_of_ratio(r: &Q) -> i32 {
if r.is_positive() {
1
} else if r.is_negative() {
-1
} else {
0
}
}
fn assumption_sign(arena: &Arena, e: ExprId) -> Option<i32> {
use crate::base::assumptions::{AssumptionCache, Props};
let mut cache = AssumptionCache::new();
if cache.query(arena, e, Props::POSITIVE) == Some(true) {
return Some(1);
}
if cache.query(arena, e, Props::NEGATIVE) == Some(true) {
return Some(-1);
}
if cache.query(arena, e, Props::ZERO) == Some(true) {
return Some(0);
}
None
}
type Ext = Extended<ExprId>;
fn classify(arena: &Arena, v: ExprId) -> Ext {
if v == arena.infinity() {
Ext::PosInf
} else if v == arena.neg_infinity() {
Ext::NegInf
} else {
Ext::Finite(v)
}
}
fn finite_candidate(arena: &mut Arena, v: ExprId, var: ExprId) -> Option<ExprId> {
let v = crate::transforms::eval::eval(arena, v);
if is_valid_limit_value(arena, v, var) && classify(arena, v).is_finite() {
Some(v)
} else {
None
}
}
fn try_compose(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
dir: Direction,
depth: usize,
) -> Option<Result<ExprId, SymplexError>> {
let node = arena.node(expr).clone();
let inner_limit = |arena: &mut Arena, inner: ExprId| -> Option<ExprId> {
limit_impl(arena, inner, var, point, dir, depth + 1).ok()
};
match node {
ExprNode::Add(ref children) => {
let (consts, deps) = split_by_var(arena, children, var);
if deps.len() != 1 {
return None;
}
let c = if consts.is_empty() {
return None;
} else {
arena.add(&consts)
};
let l = inner_limit(arena, deps[0])?;
match classify(arena, l) {
Ext::Finite(_) => {
let s = arena.add(&[c, l]);
finite_candidate(arena, s, var).map(Ok)
}
Ext::PosInf => Some(Ok(arena.infinity())),
Ext::NegInf => Some(Ok(arena.neg_infinity())),
}
}
ExprNode::Mul(ref children) => {
let (consts, deps) = split_by_var(arena, children, var);
if deps.len() != 1 || consts.is_empty() {
return None;
}
let c = arena.mul(&consts);
let l = inner_limit(arena, deps[0])?;
match classify(arena, l) {
Ext::Finite(_) => {
let p = arena.mul(&[c, l]);
finite_candidate(arena, p, var).map(Ok)
}
inf => {
let cs = const_sign(arena, c)?;
if cs == 0 {
return None;
}
let positive = (inf == Ext::PosInf) == (cs > 0);
Some(Ok(if positive {
arena.infinity()
} else {
arena.neg_infinity()
}))
}
}
}
ExprNode::Neg(inner) => {
let l = inner_limit(arena, inner)?;
match classify(arena, l) {
Ext::Finite(_) => {
let n = arena.neg(l);
finite_candidate(arena, n, var).map(Ok)
}
Ext::PosInf => Some(Ok(arena.neg_infinity())),
Ext::NegInf => Some(Ok(arena.infinity())),
}
}
ExprNode::Pow(base, exp) if !crate::base::walk::contains(arena, exp, var) => {
let k = arena.as_num(exp).cloned();
let l = inner_limit(arena, base)?;
match classify(arena, l) {
Ext::Finite(_) => {
if arena.is_zero_structural(l) {
return match &k {
Some(r) if r.is_positive() => Some(Ok(arena.zero())),
_ => None,
};
}
let fractional = k.as_ref().is_none_or(|r| !r.is_integer());
let l_sign = const_sign(arena, l);
if l_sign.is_none() && fractional {
return None;
}
if l_sign == Some(-1) && fractional && !inner_known_real(arena, base) {
return None;
}
let p = arena.pow(l, exp);
finite_candidate(arena, p, var).map(Ok)
}
Ext::PosInf => {
let r = k?;
Some(Ok(if r.is_positive() {
arena.infinity()
} else if r.is_zero() {
arena.one()
} else {
arena.zero()
}))
}
Ext::NegInf => {
let r = k?;
if r.is_zero() {
return Some(Ok(arena.one()));
}
if r.is_negative() {
return Some(Ok(arena.zero()));
}
if !r.is_integer() {
return None;
}
let odd = (r.to_integer() % BigInt::from(2)) != BigInt::zero();
Some(Ok(if odd {
arena.neg_infinity()
} else {
arena.infinity()
}))
}
}
}
ExprNode::Pow(base, exp) if !crate::base::walk::contains(arena, base, var) => {
let c = arena.as_num(base).cloned()?;
if !c.is_positive() {
return None;
}
let l = inner_limit(arena, exp)?;
let one = Ratio::from_integer(BigInt::from(1));
match classify(arena, l) {
Ext::Finite(_) => {
let p = arena.pow(base, l);
finite_candidate(arena, p, var).map(Ok)
}
inf => {
if c == one {
return Some(Ok(arena.one()));
}
let grows = (c > one) == (inf == Ext::PosInf);
Some(Ok(if grows {
arena.infinity()
} else {
arena.zero()
}))
}
}
}
ExprNode::Pow(base, exp) => {
let lb = inner_limit(arena, base)?;
let positive = match classify(arena, lb) {
Ext::PosInf => true,
Ext::Finite(_) => const_sign(arena, lb) == Some(1),
Ext::NegInf => false,
};
if !positive {
return None;
}
let ln_b = arena.ln(base);
let prod = arena.mul(&[exp, ln_b]);
let l = inner_limit(arena, prod)?;
match classify(arena, l) {
Ext::Finite(_) => {
let p = arena.exp(l);
finite_candidate(arena, p, var).map(Ok)
}
Ext::PosInf => Some(Ok(arena.infinity())),
Ext::NegInf => Some(Ok(arena.zero())),
}
}
ExprNode::Min(ref args) | ExprNode::Max(ref args) => {
let is_min = matches!(node, ExprNode::Min(_));
let args = args.clone();
let mut limits = Vec::with_capacity(args.len());
for &a in &args {
limits.push(inner_limit(arena, a)?);
}
let mut best: Option<(ExprId, Extended<Q>)> = None;
let mut all_ordered = true;
for &l in &limits {
let value: Extended<Q> = match classify(arena, l) {
Ext::Finite(_) => match arena.as_num(l) {
Some(r) => Extended::Finite(r.clone()),
None => {
all_ordered = false;
break;
}
},
Ext::PosInf => Extended::PosInf,
Ext::NegInf => Extended::NegInf,
};
let better = match &best {
None => true,
Some((_, bvalue)) => {
let ord = value.cmp(bvalue);
if is_min {
ord == std::cmp::Ordering::Less
} else {
ord == std::cmp::Ordering::Greater
}
}
};
if better {
best = Some((l, value));
}
}
if all_ordered {
return best.map(|(l, _)| Ok(l));
}
if limits.iter().any(|&l| !classify(arena, l).is_finite()) {
return None;
}
let sv: smallvec::SmallVec<[ExprId; 4]> = limits.iter().copied().collect();
let rebuilt = if is_min {
arena.intern(ExprNode::Min(sv))
} else {
arena.intern(ExprNode::Max(sv))
};
finite_candidate(arena, rebuilt, var).map(Ok)
}
ExprNode::Exp(a) => {
let l = inner_limit(arena, a)?;
match classify(arena, l) {
Ext::Finite(_) => {
let v = arena.exp(l);
finite_candidate(arena, v, var).map(Ok)
}
Ext::PosInf => Some(Ok(arena.infinity())),
Ext::NegInf => Some(Ok(arena.zero())),
}
}
ExprNode::Ln(a) => {
let l = inner_limit(arena, a)?;
match classify(arena, l) {
Ext::Finite(_) => {
if arena.is_zero_structural(l) {
return None; }
match const_sign(arena, l) {
Some(s) if s > 0 => {
let v = arena.ln(l);
finite_candidate(arena, v, var).map(Ok)
}
_ => None,
}
}
Ext::PosInf => Some(Ok(arena.infinity())),
Ext::NegInf => None,
}
}
ExprNode::Atan(a) => unary_with_asymptotes(
arena,
BranchCut::ImaginaryAxis,
a,
var,
&inner_limit,
|arena, side| {
let two = arena.int(2);
let pi = arena.pi();
let half_pi = arena.div(pi, two);
Some(if side { half_pi } else { arena.neg(half_pi) })
},
|arena, l| arena.atan(l),
),
ExprNode::Erf(a) => unary_with_asymptotes(
arena,
BranchCut::None,
a,
var,
&inner_limit,
|arena, side| Some(if side { arena.one() } else { arena.neg_one() }),
|arena, l| arena.erf(l),
),
ExprNode::Erfc(a) => unary_with_asymptotes(
arena,
BranchCut::None,
a,
var,
&inner_limit,
|arena, side| Some(if side { arena.zero() } else { arena.int(2) }),
|arena, l| arena.erfc(l),
),
ExprNode::Tanh(a) => unary_with_asymptotes(
arena,
BranchCut::None,
a,
var,
&inner_limit,
|arena, side| Some(if side { arena.one() } else { arena.neg_one() }),
|arena, l| arena.tanh(l),
),
ExprNode::Sinh(a) => unary_with_asymptotes(
arena,
BranchCut::None,
a,
var,
&inner_limit,
|arena, side| {
Some(if side {
arena.infinity()
} else {
arena.neg_infinity()
})
},
|arena, l| arena.sinh(l),
),
ExprNode::Cosh(a) => unary_with_asymptotes(
arena,
BranchCut::None,
a,
var,
&inner_limit,
|arena, _| Some(arena.infinity()),
|arena, l| arena.cosh(l),
),
ExprNode::Asinh(a) => unary_with_asymptotes(
arena,
BranchCut::ImaginaryAxis,
a,
var,
&inner_limit,
|arena, side| {
Some(if side {
arena.infinity()
} else {
arena.neg_infinity()
})
},
|arena, l| arena.asinh(l),
),
ExprNode::Acosh(a) => unary_with_asymptotes(
arena,
BranchCut::BelowOne,
a,
var,
&inner_limit,
|arena, side| if side { Some(arena.infinity()) } else { None },
|arena, l| arena.acosh(l),
),
ExprNode::Abs(a) => unary_with_asymptotes(
arena,
BranchCut::None,
a,
var,
&inner_limit,
|arena, _| Some(arena.infinity()),
|arena, l| arena.abs(l),
),
ExprNode::Sin(a) => unary_with_asymptotes(
arena,
BranchCut::None,
a,
var,
&inner_limit,
|_, _| None,
|arena, l| arena.sin(l),
),
ExprNode::Cos(a) => unary_with_asymptotes(
arena,
BranchCut::None,
a,
var,
&inner_limit,
|_, _| None,
|arena, l| arena.cos(l),
),
ExprNode::Asin(a) => unary_with_asymptotes(
arena,
BranchCut::BeyondOne,
a,
var,
&inner_limit,
|_, _| None,
|arena, l| arena.asin(l),
),
ExprNode::Acos(a) => unary_with_asymptotes(
arena,
BranchCut::BeyondOne,
a,
var,
&inner_limit,
|_, _| None,
|arena, l| arena.acos(l),
),
ExprNode::Gamma(a) => {
let l = inner_limit(arena, a)?;
match classify(arena, l) {
Ext::PosInf => Some(Ok(arena.infinity())),
Ext::NegInf => Some(Err(fail("Γ(x) has no limit as x → −∞"))),
Ext::Finite(_) => {
if is_nonpositive_integer(arena, l) {
return None;
}
let v = arena.gamma(l);
finite_candidate(arena, v, var).map(Ok)
}
}
}
ExprNode::LogGamma(a) | ExprNode::Digamma(a) | ExprNode::LambertW(a) => {
let l = inner_limit(arena, a)?;
match classify(arena, l) {
Ext::PosInf => Some(Ok(arena.infinity())),
Ext::NegInf => None,
Ext::Finite(_) => {
if matches!(node, ExprNode::LogGamma(_) | ExprNode::Digamma(_))
&& is_nonpositive_integer(arena, l)
{
return None;
}
if let Some((_, cut)) = branch_cut_of(arena, expr)
&& limit_on_branch_cut(arena, cut, l, a)
{
return None;
}
let v = match node {
ExprNode::LogGamma(_) => arena.log_gamma(l),
ExprNode::Digamma(_) => arena.digamma(l),
_ => arena.lambertw(l),
};
finite_candidate(arena, v, var).map(Ok)
}
}
}
ExprNode::Factorial(a) => {
let l = inner_limit(arena, a)?;
match classify(arena, l) {
Ext::PosInf => Some(Ok(arena.infinity())),
Ext::NegInf => None,
Ext::Finite(_) => {
if arena
.as_num(l)
.is_some_and(|r| r.is_integer() && r.is_negative())
{
return None;
}
let v = arena.factorial(l);
finite_candidate(arena, v, var).map(Ok)
}
}
}
ExprNode::Sign(a) | ExprNode::Heaviside(a) => {
let is_sign = matches!(node, ExprNode::Sign(_));
let l = inner_limit(arena, a)?;
let s = match classify(arena, l) {
Ext::PosInf => 1,
Ext::NegInf => -1,
Ext::Finite(_) => match arena.as_num(l) {
Some(r) if !r.is_zero() => sign_of_ratio(r),
_ => return None,
},
};
Some(Ok(if is_sign {
arena.int(s as i64)
} else if s > 0 {
arena.one()
} else {
arena.zero()
}))
}
ExprNode::Floor(a) | ExprNode::Ceiling(a) => {
let is_floor = matches!(node, ExprNode::Floor(_));
let l = inner_limit(arena, a)?;
match classify(arena, l) {
Ext::PosInf => Some(Ok(arena.infinity())),
Ext::NegInf => Some(Ok(arena.neg_infinity())),
Ext::Finite(_) => {
let r = arena.as_num(l).cloned()?;
if r.is_integer() {
return None;
}
let v = if is_floor { r.floor() } else { r.ceil() };
let nid = arena.intern_num(v);
Some(Ok(arena.intern(ExprNode::Num(nid))))
}
}
}
_ => None,
}
}
fn split_by_var(arena: &Arena, children: &[ExprId], var: ExprId) -> (Vec<ExprId>, Vec<ExprId>) {
let mut consts = Vec::new();
let mut deps = Vec::new();
for &c in children {
if crate::base::walk::contains(arena, c, var) {
deps.push(c);
} else {
consts.push(c);
}
}
(consts, deps)
}
fn unary_with_asymptotes(
arena: &mut Arena,
cut: BranchCut,
inner: ExprId,
var: ExprId,
inner_limit: &dyn Fn(&mut Arena, ExprId) -> Option<ExprId>,
at_inf: impl Fn(&mut Arena, bool) -> Option<ExprId>,
build: impl Fn(&mut Arena, ExprId) -> ExprId,
) -> Option<Result<ExprId, SymplexError>> {
let l = inner_limit(arena, inner)?;
match classify(arena, l) {
Ext::Finite(_) => {
let v = build(arena, l);
if limit_on_branch_cut(arena, cut, l, inner) {
return None;
}
finite_candidate(arena, v, var).map(Ok)
}
Ext::PosInf => match at_inf(arena, true) {
Some(v) => Some(Ok(v)),
None => Some(Err(fail(
"the function oscillates or is undefined as its argument diverges",
))),
},
Ext::NegInf => match at_inf(arena, false) {
Some(v) => Some(Ok(v)),
None => Some(Err(fail(
"the function oscillates or is undefined as its argument diverges",
))),
},
}
}
fn limit_on_branch_cut(arena: &mut Arena, cut: BranchCut, l: ExprId, g: ExprId) -> bool {
match cut {
BranchCut::ImaginaryAxis => on_branch_cut(arena, cut, l),
_ => on_branch_cut(arena, cut, l) && !inner_known_real(arena, g),
}
}
pub(crate) fn on_branch_cut(arena: &mut Arena, cut: BranchCut, l: ExprId) -> bool {
if cut == BranchCut::None || !crate::base::walk::free_symbols(arena, l).is_empty() {
return false;
}
let re = arena.re(l);
let re = crate::transforms::eval::eval(arena, re);
let im = arena.im(l);
let im = crate::transforms::eval::eval(arena, im);
let (Ok(re), Ok(im)) = (
crate::transforms::evalf::evalf_complex(arena, re, 20),
crate::transforms::evalf::evalf_complex(arena, im, 20),
) else {
return false;
};
let is_zero = |x: &crate::base::bigcomplex::Complex| x.0.is_zero() && x.1.is_zero();
let magnitude = |x: &crate::base::bigcomplex::Complex| {
crate::transforms::evalf::abs_to_f64(x).unwrap_or(0.0)
};
match cut {
BranchCut::None => false,
BranchCut::ImaginaryAxis => is_zero(&re) && magnitude(&im) >= 1.0,
BranchCut::NegativeReals => is_zero(&im) && re.0.is_negative(),
BranchCut::BelowOne => is_zero(&im) && (re.0.is_negative() || magnitude(&re) < 1.0),
BranchCut::BeyondOne => is_zero(&im) && magnitude(&re) > 1.0,
BranchCut::AboveOne => is_zero(&im) && re.0.is_positive() && magnitude(&re) > 1.0,
BranchCut::BelowMinusInvE => {
if !is_zero(&im) || !re.0.is_negative() {
return false;
}
let inv_e = (-1.0f64).exp();
let m = magnitude(&re);
if (m - inv_e).abs() > 1e-9 {
return m > inv_e;
}
let re_l = arena.re(l);
let m1 = arena.neg_one();
let e_inv = arena.exp(m1);
let d = arena.add(&[re_l, e_inv]);
const_sign(arena, d).is_none_or(|s| s < 0)
}
}
}
pub(crate) fn branch_cut_of(arena: &Arena, id: ExprId) -> Option<(ExprId, BranchCut)> {
use crate::base::libfn::LibFn;
let not_positive_integer = |e: ExprId| {
arena
.as_num(e)
.is_none_or(|r| !(r.is_integer() && r.is_positive()))
};
let not_integer = |e: ExprId| arena.as_num(e).is_none_or(|r| !r.is_integer());
let not_nonpositive_integer = |e: ExprId| {
arena
.as_num(e)
.is_none_or(|r| !(r.is_integer() && !r.is_positive()))
};
Some(match *arena.node(id) {
ExprNode::Ln(a) | ExprNode::LogGamma(a) | ExprNode::Ei(a) | ExprNode::Ci(a) => {
(a, BranchCut::NegativeReals)
}
ExprNode::Pow(b, e) if arena.as_num(e).is_some_and(|r| !r.is_integer()) => {
(b, BranchCut::NegativeReals)
}
ExprNode::Asin(a) | ExprNode::Acos(a) | ExprNode::Atanh(a) => (a, BranchCut::BeyondOne),
ExprNode::Acosh(a) | ExprNode::Li(a) => (a, BranchCut::BelowOne),
ExprNode::Asinh(a) | ExprNode::Atan(a) => (a, BranchCut::ImaginaryAxis),
ExprNode::LambertW(a) => (a, BranchCut::BelowMinusInvE),
ExprNode::Apply(f, ref args) => match (arena.lib_fn(f)?, args.as_slice()) {
(LibFn::Chi, &[a]) => (a, BranchCut::NegativeReals),
(LibFn::ExpInt, &[n, z]) if not_nonpositive_integer(n) => (z, BranchCut::NegativeReals),
(LibFn::BesselK | LibFn::BesselY, &[_, z]) => (z, BranchCut::NegativeReals),
(LibFn::BesselJ | LibFn::BesselI, &[nu, z]) if not_integer(nu) => {
(z, BranchCut::NegativeReals)
}
(LibFn::UpperGamma | LibFn::LowerGamma, &[s, z]) if not_positive_integer(s) => {
(z, BranchCut::NegativeReals)
}
(LibFn::PolyLog, &[s, z]) if not_nonpositive_integer(s) => (z, BranchCut::AboveOne),
(LibFn::EllipticK | LibFn::EllipticE, &[m]) => (m, BranchCut::AboveOne),
_ => return None,
},
_ => return None,
})
}
pub(crate) fn continuation_across_cut(
arena: &mut Arena,
id: ExprId,
l: ExprId,
side: i32,
along: i32,
) -> Option<ExprId> {
let (u, _) = branch_cut_of(arena, id)?;
let node = arena.node(id).clone();
let i = arena.i_unit();
let pi = arena.pi();
let half = arena.rational(1, 2);
let half_pi = arena.mul(&[half, pi]);
let i_pi = arena.mul(&[i, pi]);
let i_half_pi = arena.mul(&[i, half_pi]);
let neg_u = arena.neg(u);
let signed = |arena: &mut Arena, s: i32, v: ExprId| if s >= 0 { v } else { arena.neg(v) };
let pick = |on_cut: i32| match side {
s if s > 0 => 1,
s if s < 0 => -1,
_ => on_cut,
};
let re_l = arena.re(l);
let re_l = crate::transforms::eval::eval(arena, re_l);
let im_l = arena.im(l);
let im_l = crate::transforms::eval::eval(arena, im_l);
let g = match node {
ExprNode::Ln(_) => {
let ln = arena.ln(neg_u);
let shift = signed(arena, pick(1), i_pi);
arena.add(&[ln, shift])
}
ExprNode::Pow(_, r) => {
let p = arena.pow(neg_u, r);
let phase = arena.mul(&[i_pi, r]);
let phase = signed(arena, pick(1), phase);
let rot = arena.exp(phase);
arena.mul(&[p, rot])
}
ExprNode::Asin(_) | ExprNode::Acos(_) => {
let beyond = const_sign(arena, re_l)?;
let (c, v, t) = if beyond > 0 {
(half_pi, u, pick(-1))
} else {
(arena.neg(half_pi), neg_u, pick(1))
};
let ach = arena.acosh(v);
let term = arena.mul(&[i, ach]);
let term = signed(arena, t, term);
let asin = arena.add(&[c, term]);
if matches!(node, ExprNode::Asin(_)) {
asin
} else {
arena.sub(half_pi, asin)
}
}
ExprNode::Atanh(_) => {
let below_minus_one = const_sign(arena, re_l)? < 0;
let t = pick(if below_minus_one { 1 } else { -1 });
let inv = arena.pow(u, arena.neg_one());
let at = arena.atanh(inv);
let shift = signed(arena, t, i_half_pi);
arena.add(&[at, shift])
}
ExprNode::Acosh(_) => {
let one = arena.one();
let lp1 = arena.add(&[re_l, one]);
let inside = const_sign(arena, lp1)?;
let inside = if inside == 0 && side == 0 {
along
} else {
inside
};
match inside {
s if s > 0 => {
let ac = arena.acos(u);
let iac = arena.mul(&[i, ac]);
signed(arena, pick(1), iac)
}
0 if side == 0 => return None,
_ => {
let ach = arena.acosh(neg_u);
let shift = signed(arena, pick(1), i_pi);
arena.add(&[ach, shift])
}
}
}
ExprNode::Asinh(_) | ExprNode::Atan(_) => {
let upper = const_sign(arena, im_l)?;
let one = arena.one();
let abs_y = if upper > 0 { im_l } else { arena.neg(im_l) };
let beyond = arena.sub(abs_y, one);
let beyond = const_sign(arena, beyond)?;
if beyond < 0 || (beyond == 0 && side == 0) {
return None;
}
if matches!(node, ExprNode::Atan(_)) {
let t = pick(upper);
let inv = arena.pow(u, arena.neg_one());
let at = arena.atan(inv);
let c = signed(arena, t, half_pi);
arena.sub(c, at)
} else if upper > 0 {
let v = arena.mul(&[i, u]);
let v = arena.neg(v);
let ach = arena.acosh(v);
let term = signed(arena, pick(1), ach);
arena.add(&[i_half_pi, term])
} else {
let v = arena.mul(&[i, u]);
let ach = arena.acosh(v);
let term = signed(arena, pick(-1), ach);
let c = arena.neg(i_half_pi);
arena.add(&[c, term])
}
}
ExprNode::Ei(_) => {
let one = arena.one();
let e1 = arena.lib_apply(crate::base::libfn::LibFn::ExpInt, &[one, neg_u]);
let ne1 = arena.neg(e1);
match side.signum() {
0 => ne1,
s => {
let shift = signed(arena, s, i_pi);
arena.add(&[ne1, shift])
}
}
}
ExprNode::Ci(_) => {
let c = arena.ci(neg_u);
let shift = signed(arena, pick(1), i_pi);
arena.add(&[c, shift])
}
ExprNode::Apply(f, _) if arena.lib_fn(f) == Some(crate::base::libfn::LibFn::Chi) => {
let c = arena.lib_apply(crate::base::libfn::LibFn::Chi, &[neg_u]);
let shift = signed(arena, pick(1), i_pi);
arena.add(&[c, shift])
}
ExprNode::LogGamma(_) => {
let n = floor_of_negated(arena, l)?;
let g = arena.gamma(u);
let g = if n % 2 == 0 { arena.neg(g) } else { g };
let ln = arena.ln(g);
let k = arena.int(n + 1);
let shift = arena.mul(&[k, i_pi]);
let shift = signed(arena, -pick(1), shift);
arena.add(&[ln, shift])
}
_ => return None,
};
Some(g)
}
pub(crate) fn floor_of_negated(arena: &mut Arena, l: ExprId) -> Option<i64> {
use num_traits::ToPrimitive;
if let Some(r) = arena.as_num(l) {
if r.is_integer() {
return None;
}
return (-r).floor().to_integer().to_i64();
}
let v = crate::transforms::evalf::evalf_f64(arena, l).ok()?;
let m = -v;
if !m.is_finite() || m > 1e15 || (m - m.round()).abs() < 1e-9 {
return None;
}
Some(m.floor() as i64)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum BranchCut {
None,
ImaginaryAxis,
NegativeReals,
BelowOne,
BeyondOne,
AboveOne,
BelowMinusInvE,
}
pub(crate) fn inner_known_real(arena: &Arena, g: ExprId) -> bool {
use crate::base::assumptions::{AssumptionCache, Assumptions, Props};
let mut cache = AssumptionCache::new();
for s in crate::base::walk::free_symbols(arena, g) {
if let ExprNode::Symbol(sid) = *arena.node(s)
&& arena.symbol_assumptions(sid) == Assumptions::default()
{
let mut real = Assumptions::default();
real.known_true |= Props::REAL;
cache.set_symbol_assumptions(s, real);
}
}
cache.query(arena, g, Props::REAL) == Some(true)
}
fn one_sided_gruntz(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
right: bool,
) -> Result<ExprId, SymplexError> {
let w = arena.positive_symbol(LIM_DUMMY);
let one = arena.one();
let inv_w = arena.div(one, w);
let shift = if right { inv_w } else { arena.neg(inv_w) };
let sub_point = arena.add(&[point, shift]);
let e = crate::transforms::subs::subs(arena, expr, var, sub_point);
let e = crate::poly::polybridge::together(arena, e);
let e = crate::poly::polybridge::cancel(arena, e, w);
let e = crate::transforms::eval::eval(arena, e);
let e = if crate::transforms::pattern::tree_size_capped(arena, e, MAX_EXPAND_SIZE + 1)
<= MAX_EXPAND_SIZE
&& expansion_cost_estimate(arena, e) <= MAX_EXPANSION_COST
{
let e = crate::transforms::expand::expand(arena, e);
crate::transforms::eval::eval(arena, e)
} else {
e
};
let side = if right { "+" } else { "-" };
tracing::debug!(side, expr = %arena.display(e).to_string(), "limit: one-sided Gruntz");
let r = crate::calculus::gruntz::limit_pos_inf(arena, e, w)?;
if is_valid_limit_value(arena, r, var) && !crate::base::walk::contains(arena, r, w) {
Ok(r)
} else {
Err(fail(format!(
"could not determine the limit from the {side} side"
)))
}
}
fn analytic_fallback(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
point: ExprId,
) -> Result<ExprId, SymplexError> {
if meets_branch_cut_off_axis(arena, expr, var, point) {
return Err(fail(
"a non-real argument meets a branch cut at the point: the one-sided limits may differ",
));
}
let (numer, denom) = crate::poly::polybridge::as_numer_denom(arena, expr);
if denom != arena.one()
&& let Some(r) = try_lhopital(arena, numer, denom, var, point, 0)
&& is_valid_limit_value(arena, r, var)
{
return Ok(r);
}
Err(fail(
"could not determine the limit via substitution, the Gruntz algorithm, or L'Hôpital's rule",
))
}
fn meets_branch_cut_off_axis(arena: &mut Arena, expr: ExprId, var: ExprId, point: ExprId) -> bool {
for id in crate::base::walk::post_order_ids(arena, expr) {
let Some((arg, cut)) = branch_cut_of(arena, id) else {
continue;
};
if !crate::base::walk::contains(arena, arg, var) || inner_known_real(arena, arg) {
continue;
}
let at = crate::transforms::subs::subs(arena, arg, var, point);
let at = crate::transforms::eval::eval(arena, at);
if on_branch_cut(arena, cut, at) {
return true;
}
}
false
}
fn try_lhopital(
arena: &mut Arena,
numer: ExprId,
denom: ExprId,
var: ExprId,
point: ExprId,
depth: usize,
) -> Option<ExprId> {
if depth >= MAX_LHOPITAL {
return None;
}
let n_subst = crate::transforms::subs::subs(arena, numer, var, point);
let n_at_point = crate::transforms::eval::eval(arena, n_subst);
let d_subst = crate::transforms::subs::subs(arena, denom, var, point);
let d_at_point = crate::transforms::eval::eval(arena, d_subst);
let n_is_zero = arena.is_zero_structural(n_at_point);
let d_is_zero = arena.is_zero_structural(d_at_point);
let n_is_inf = n_at_point == arena.infinity() || n_at_point == arena.neg_infinity();
let d_is_inf = d_at_point == arena.infinity() || d_at_point == arena.neg_infinity();
if !((n_is_zero && d_is_zero) || (n_is_inf && d_is_inf)) {
return None;
}
tracing::debug!(depth, "L'Hôpital: indeterminate form detected");
let n_prime = crate::transforms::diff::diff(arena, numer, var);
let d_prime = crate::transforms::diff::diff(arena, denom, var);
let np_subst = crate::transforms::subs::subs(arena, n_prime, var, point);
let np_val = crate::transforms::eval::eval(arena, np_subst);
let dp_subst = crate::transforms::subs::subs(arena, d_prime, var, point);
let dp_val = crate::transforms::eval::eval(arena, dp_subst);
let np_zero = arena.is_zero_structural(np_val);
let dp_zero = arena.is_zero_structural(dp_val);
if np_zero && dp_zero {
return try_lhopital(arena, n_prime, d_prime, var, point, depth + 1);
}
if is_finite_constant(arena, np_val, var) && is_finite_constant(arena, dp_val, var) && !dp_zero
{
let result_ratio = arena.div(np_val, dp_val);
let result = crate::transforms::eval::eval(arena, result_ratio);
if is_finite_constant(arena, result, var) {
return Some(result);
}
}
let ratio = arena.div(n_prime, d_prime);
let subst = crate::transforms::subs::subs(arena, ratio, var, point);
let evaled = crate::transforms::eval::eval(arena, subst);
if is_finite_constant(arena, evaled, var) {
return Some(evaled);
}
try_lhopital(arena, n_prime, d_prime, var, point, depth + 1)
}
fn is_finite_constant(arena: &Arena, id: ExprId, var: ExprId) -> bool {
is_valid_limit_value(arena, id, var) && id != arena.infinity() && id != arena.neg_infinity()
}
fn limit_at_infinity_dir(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
positive: bool,
depth: usize,
) -> Result<ExprId, SymplexError> {
let point = if positive {
arena.infinity()
} else {
arena.neg_infinity()
};
if let ExprNode::Pow(base, exponent) = arena.node(expr).clone()
&& crate::base::walk::contains(arena, exponent, var)
{
let base_tends_to_one = if !crate::base::walk::contains(arena, base, var) {
base == arena.one()
} else {
match crate::calculus::gruntz::gruntz(arena, base, var, point) {
Ok(lim_base) => lim_base == arena.one(),
Err(_) => false,
}
};
if base_tends_to_one {
tracing::debug!("limit: base → 1 confirmed, applying 1^∞ heuristic");
let one = arena.one();
let base_minus_1 = arena.sub(base, one);
let product = arena.mul(&[exponent, base_minus_1]);
if let Ok(inner_lim) = crate::calculus::gruntz::gruntz(arena, product, var, point)
&& is_finite_constant(arena, inner_lim, var)
{
let result = arena.exp(inner_lim);
let result = crate::transforms::eval::eval(arena, result);
if is_valid_limit_value(arena, result, var) {
return Ok(result);
}
}
}
}
if depth < MAX_COMPOSE_DEPTH
&& let Some(r) = try_compose(arena, expr, var, point, Direction::Both, depth)
{
return r;
}
tracing::debug!("limit: at infinity, trying Gruntz algorithm");
let gruntz_err = match crate::calculus::gruntz::gruntz(arena, expr, var, point) {
Ok(result) if is_valid_limit_value(arena, result, var) => return Ok(result),
Ok(_) => fail("Gruntz algorithm produced an indeterminate result"),
Err(e) => e,
};
tracing::debug!("limit: Gruntz failed, falling back to polynomial degree analysis");
match limit_at_infinity(arena, expr, var, positive) {
Ok(r) if is_valid_limit_value(arena, r, var) => Ok(r),
_ => Err(gruntz_err),
}
}
pub(crate) fn limit_at_infinity(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
positive: bool, ) -> Result<ExprId, SymplexError> {
let (orig_numer, orig_denom) = crate::poly::polybridge::as_numer_denom(arena, expr);
if orig_denom != arena.one() {
let n_deg = crate::poly::polybridge::poly_degree(arena, orig_numer, var);
let d_deg = crate::poly::polybridge::poly_degree(arena, orig_denom, var);
if let (Some(nd), Some(dd)) = (n_deg, d_deg) {
if nd < dd {
return Ok(arena.zero());
}
if nd == dd {
let n_coeffs = crate::poly::polybridge::poly_coefficients(arena, orig_numer, var);
let d_coeffs = crate::poly::polybridge::poly_coefficients(arena, orig_denom, var);
if let (Some(nc), Some(dc)) = (n_coeffs, d_coeffs)
&& let (Some(n_lead), Some(d_lead)) = (nc.last(), dc.last())
{
let ratio = arena.div(*n_lead, *d_lead);
let result = crate::transforms::eval::eval(arena, ratio);
if is_finite_result(arena, result) {
return Ok(result);
}
}
}
if nd > dd {
let n_coeffs = crate::poly::polybridge::poly_coefficients(arena, orig_numer, var);
let d_coeffs = crate::poly::polybridge::poly_coefficients(arena, orig_denom, var);
if let (Some(nc), Some(dc)) = (n_coeffs, d_coeffs)
&& let (Some(n_lead), Some(d_lead)) = (nc.last(), dc.last())
&& let (Some(nr), Some(dr)) = (arena.as_num(*n_lead), arena.as_num(*d_lead))
{
let ratio_positive = nr.is_positive() == dr.is_positive();
let flip = !positive && ((nd - dd) % 2 == 1);
let result_positive = ratio_positive ^ flip;
if result_positive {
return Ok(arena.infinity());
} else {
return Ok(arena.neg_infinity());
}
}
}
}
} else {
let deg = crate::poly::polybridge::poly_degree(arena, expr, var);
if let Some(d) = deg
&& d == 0
{
let result = crate::transforms::eval::eval(arena, expr);
if is_finite_result(arena, result) {
return Ok(result);
}
}
}
let t = arena.positive_symbol("__limit_t");
let one = arena.one();
let t_inv = arena.div(one, t);
let sub_expr = if positive {
arena.subs_structural(expr, var, t_inv)
} else {
let neg_t_inv = arena.neg(t_inv);
arena.subs_structural(expr, var, neg_t_inv)
};
let together = crate::poly::polybridge::together(arena, sub_expr);
let cancelled = crate::poly::polybridge::cancel(arena, together, t);
let simplified = crate::transforms::eval::eval(arena, cancelled);
let expanded = crate::transforms::expand::expand(arena, simplified);
let evaled = crate::transforms::eval::eval(arena, expanded);
let zero = arena.zero();
if let Some(at_zero) = try_direct_substitution(arena, evaled, t, zero)
&& is_finite_result(arena, at_zero)
{
return Ok(at_zero);
}
let (numer, denom) = crate::poly::polybridge::as_numer_denom(arena, evaled);
if denom != arena.one() {
let n_deg = crate::poly::polybridge::poly_degree(arena, numer, t);
let d_deg = crate::poly::polybridge::poly_degree(arena, denom, t);
if let (Some(nd), Some(dd)) = (n_deg, d_deg) {
if nd < dd {
return Ok(arena.zero());
}
if nd == dd {
let cancelled = crate::poly::polybridge::cancel(arena, evaled, t);
let result = arena.subs_structural(cancelled, t, zero);
let result = crate::transforms::eval::eval(arena, result);
if is_finite_result(arena, result) {
return Ok(result);
}
}
if nd > dd {
let n_coeffs = crate::poly::polybridge::poly_coefficients(arena, numer, t);
let d_coeffs = crate::poly::polybridge::poly_coefficients(arena, denom, t);
if let (Some(nc), Some(dc)) = (n_coeffs, d_coeffs)
&& let (Some(n_lead), Some(d_lead)) = (nc.last(), dc.last())
&& let (Some(nr), Some(dr)) = (arena.as_num(*n_lead), arena.as_num(*d_lead))
{
let ratio_positive = nr.is_positive() == dr.is_positive();
if ratio_positive {
return Ok(arena.infinity());
} else {
return Ok(arena.neg_infinity());
}
}
}
}
}
if has_directional_nodes(arena, evaled, t) {
return Err(fail("could not compute limit at infinity"));
}
match analytic_fallback(arena, evaled, t, zero) {
Ok(result) if is_valid_limit_value(arena, result, var) => Ok(result),
_ => Err(fail("could not compute limit at infinity")),
}
}
fn is_finite_result(arena: &Arena, id: ExprId) -> bool {
matches!(
arena.node(id),
ExprNode::Num(_) | ExprNode::Pi | ExprNode::E
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
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 limit_polynomial_direct_sub() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x2 = a.pow(x, two);
let result = limit(&mut a, x2, x, two).unwrap();
assert_eq!(display(&a, result), "4");
}
#[test]
fn limit_sin_x_over_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let zero = a.zero;
let sin_x = a.sin(x);
let expr = a.div(sin_x, x);
let result = limit(&mut a, expr, x, zero).unwrap();
assert_eq!(display(&a, result), "1");
}
#[test]
fn limit_constant() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let zero = a.zero;
let result = limit(&mut a, five, x, zero).unwrap();
assert_eq!(display(&a, result), "5");
}
#[test]
fn limit_symbolic_constant() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let c = sym(&mut a, "c");
let zero = a.zero;
let result = limit(&mut a, c, x, zero).unwrap();
assert_eq!(result, c);
}
#[test]
fn limit_x_squared_minus_one_over_x_minus_one() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.one;
let two = a.int(2);
let x2 = a.pow(x, two);
let numer = a.sub(x2, one);
let denom = a.sub(x, one);
let expr = a.div(numer, denom);
let result = limit(&mut a, expr, x, one).unwrap();
assert_eq!(display(&a, result), "2");
}
#[test]
fn limit_direct_sub_works() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let three = a.int(3);
let expr = a.add(&[x, a.one]);
let result = limit(&mut a, expr, x, three).unwrap();
assert_eq!(display(&a, result), "4");
}
#[test]
fn one_sided_reciprocal() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let zero = a.zero;
let one = a.one;
let expr = a.div(one, x);
let r = limit_dir(&mut a, expr, x, zero, Direction::Right).unwrap();
assert_eq!(r, a.infinity());
let l = limit_dir(&mut a, expr, x, zero, Direction::Left).unwrap();
assert_eq!(l, a.neg_infinity());
assert!(limit(&mut a, expr, x, zero).is_err());
}
#[test]
fn two_sided_differ_error_message() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let zero = a.zero;
let abs_x = a.abs(x);
let expr = a.div(abs_x, x);
let err = limit(&mut a, expr, x, zero).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("differ"), "unexpected message: {msg}");
}
#[test]
fn direct_substitution_rejects_folded_indeterminate() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.one;
let two = a.int(2);
let x2 = a.pow(x, two);
let numer = a.sub(x2, one);
let denom = a.sub(x, one);
let expr = a.div(numer, denom);
assert!(try_direct_substitution(&mut a, expr, x, one).is_none());
}
#[test]
fn var_must_be_symbol() {
let mut a = Arena::new();
let two = a.int(2);
let zero = a.zero;
assert!(limit(&mut a, two, two, zero).is_err());
}
}