use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, ToPrimitive, Zero};
use rustc_hash::FxHashMap;
use crate::base::arena::Arena;
use crate::base::libfn::LibFn;
use crate::base::node::{ExprId, ExprNode};
use crate::base::numeric::Q;
use crate::base::walk;
pub(crate) fn eval(arena: &mut Arena, expr: ExprId) -> ExprId {
let post_order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for &id in &post_order {
let node = arena.node(id).clone();
let evaluated = match node {
ExprNode::Sin(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_sin(arena, inner).unwrap_or_else(|| arena.sin(inner))
}
ExprNode::Cos(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_cos(arena, inner).unwrap_or_else(|| arena.cos(inner))
}
ExprNode::Tan(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_tan(arena, inner).unwrap_or_else(|| arena.tan(inner))
}
ExprNode::Exp(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_exp(arena, inner).unwrap_or_else(|| arena.exp(inner))
}
ExprNode::Ln(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_ln(arena, inner).unwrap_or_else(|| arena.ln(inner))
}
ExprNode::Abs(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_abs(arena, inner).unwrap_or_else(|| arena.abs(inner))
}
ExprNode::Asin(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_asin(arena, inner).unwrap_or_else(|| arena.intern(ExprNode::Asin(inner)))
}
ExprNode::Acos(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_acos(arena, inner).unwrap_or_else(|| arena.intern(ExprNode::Acos(inner)))
}
ExprNode::Atan(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_atan(arena, inner).unwrap_or_else(|| arena.intern(ExprNode::Atan(inner)))
}
ExprNode::Atan2(y, x) => {
let ny = cache.get(&y).copied().unwrap_or(y);
let nx = cache.get(&x).copied().unwrap_or(x);
if let Some(result) = eval_atan2(arena, ny, nx) {
result
} else if ny == y && nx == x {
id
} else {
arena.atan2(ny, nx)
}
}
ExprNode::Sinh(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_sinh(arena, inner).unwrap_or_else(|| arena.intern(ExprNode::Sinh(inner)))
}
ExprNode::Cosh(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_cosh(arena, inner).unwrap_or_else(|| arena.intern(ExprNode::Cosh(inner)))
}
ExprNode::Tanh(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_tanh(arena, inner).unwrap_or_else(|| arena.intern(ExprNode::Tanh(inner)))
}
ExprNode::Asinh(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_asinh(arena, inner).unwrap_or_else(|| arena.intern(ExprNode::Asinh(inner)))
}
ExprNode::Acosh(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_acosh(arena, inner).unwrap_or_else(|| arena.intern(ExprNode::Acosh(inner)))
}
ExprNode::Atanh(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
eval_atanh(arena, inner).unwrap_or_else(|| arena.intern(ExprNode::Atanh(inner)))
}
ExprNode::Sign(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(result) = eval_sign(arena, ni) {
result
} else if ni == inner {
id
} else {
arena.sign(ni)
}
}
ExprNode::Heaviside(inner) => {
let evaled = cache.get(&inner).copied().unwrap_or(inner);
if let Some(r) = arena.as_num(evaled).cloned() {
if r.is_positive() {
arena.one
} else if r.is_negative() {
arena.zero
} else {
arena.rational(1, 2)
}
} else if evaled == inner {
id
} else {
arena.intern(ExprNode::Heaviside(evaled))
}
}
ExprNode::DiracDelta(inner) => {
let evaled = cache.get(&inner).copied().unwrap_or(inner);
if let Some(r) = arena.as_num(evaled).cloned() {
if !r.is_zero() {
arena.zero
} else {
if evaled == inner {
id
} else {
arena.intern(ExprNode::DiracDelta(evaled))
}
}
} else if evaled == inner {
id
} else {
arena.intern(ExprNode::DiracDelta(evaled))
}
}
ExprNode::Gamma(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(result) = eval_gamma(arena, ni) {
result
} else if ni == inner {
id
} else {
arena.gamma(ni)
}
}
ExprNode::LogGamma(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(result) = eval_log_gamma(arena, ni) {
result
} else if ni == inner {
id
} else {
arena.log_gamma(ni)
}
}
ExprNode::Digamma(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(result) = eval_digamma(arena, ni) {
result
} else if ni == inner {
id
} else {
arena.digamma(ni)
}
}
ExprNode::Re(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
arena.re(ni)
}
ExprNode::Im(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
arena.im(ni)
}
ExprNode::Conjugate(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
arena.conjugate(ni)
}
ExprNode::Arg(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
arena.arg(ni)
}
ExprNode::Si(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
arena.si(ni)
}
ExprNode::Ci(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
arena.ci(ni)
}
ExprNode::Ei(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
arena.ei(ni)
}
ExprNode::Li(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
arena.li(ni)
}
ExprNode::Zeta(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
arena.zeta(ni)
}
ExprNode::Polygamma(n, x) => {
let nn = cache.get(&n).copied().unwrap_or(n);
let nx = cache.get(&x).copied().unwrap_or(x);
arena.polygamma(nn, nx)
}
ExprNode::KroneckerDelta(i, j) => {
let ni = cache.get(&i).copied().unwrap_or(i);
let nj = cache.get(&j).copied().unwrap_or(j);
arena.kronecker_delta(ni, nj)
}
ExprNode::Erf(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(result) = eval_erf(arena, ni) {
result
} else if ni == inner {
id
} else {
arena.erf(ni)
}
}
ExprNode::Erfc(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(result) = eval_erfc(arena, ni) {
result
} else if ni == inner {
id
} else {
arena.erfc(ni)
}
}
ExprNode::LambertW(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(result) = eval_lambertw(arena, ni) {
result
} else if ni == inner {
id
} else {
arena.lambertw(ni)
}
}
ExprNode::Beta(a, b) => {
let na = cache.get(&a).copied().unwrap_or(a);
let nb = cache.get(&b).copied().unwrap_or(b);
if let Some(result) = eval_beta(arena, na, nb) {
result
} else if na == a && nb == b {
id
} else {
arena.beta(na, nb)
}
}
ExprNode::Add(ref children) => {
let new: smallvec::SmallVec<[ExprId; 6]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
if new == *children {
id
} else {
arena.add(&new)
}
}
ExprNode::Mul(ref children) => {
let new: smallvec::SmallVec<[ExprId; 6]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
if new == *children {
id
} else {
arena.mul(&new)
}
}
ExprNode::Pow(base, exp) => {
let nb = cache.get(&base).copied().unwrap_or(base);
let ne = cache.get(&exp).copied().unwrap_or(exp);
if let Some(result) = eval_pow_root(arena, nb, ne) {
result
}
else if let ExprNode::Exp(inner) = arena.node(nb).clone()
&& exp_pow_merges(arena, inner, ne)
{
let product = arena.mul(&[inner, ne]);
let product = eval(arena, product);
eval_exp(arena, product).unwrap_or_else(|| arena.exp(product))
} else if nb == base && ne == exp {
id
} else {
arena.pow(nb, ne)
}
}
ExprNode::Neg(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if ni == inner { id } else { arena.neg(ni) }
}
ExprNode::Factorial(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(result) = eval_factorial(arena, ni) {
result
} else if ni == inner {
id
} else {
arena.factorial(ni)
}
}
ExprNode::Binomial(n, k) => {
let nn = cache.get(&n).copied().unwrap_or(n);
let nk = cache.get(&k).copied().unwrap_or(k);
if let Some(result) = eval_binomial(arena, nn, nk) {
result
} else if nn == n && nk == k {
id
} else {
arena.binomial(nn, nk)
}
}
ExprNode::BoolTrue | ExprNode::BoolFalse => id,
ExprNode::Gt(a, b) => {
let na = cache.get(&a).copied().unwrap_or(a);
let nb = cache.get(&b).copied().unwrap_or(b);
if let (Some(ra), Some(rb)) = (arena.as_num(na), arena.as_num(nb)) {
if ra > rb {
arena.bool_true
} else {
arena.bool_false
}
} else if let Some(holds) = order_against_zero(arena, na, nb, true)
.or_else(|| order_by_difference(arena, na, nb, true))
{
if holds {
arena.bool_true
} else {
arena.bool_false
}
} else if na == a && nb == b {
id
} else {
arena.gt(na, nb)
}
}
ExprNode::Ge(a, b) => {
let na = cache.get(&a).copied().unwrap_or(a);
let nb = cache.get(&b).copied().unwrap_or(b);
if let (Some(ra), Some(rb)) = (arena.as_num(na), arena.as_num(nb)) {
if ra >= rb {
arena.bool_true
} else {
arena.bool_false
}
} else if let Some(holds) = order_against_zero(arena, na, nb, false)
.or_else(|| order_by_difference(arena, na, nb, false))
{
if holds {
arena.bool_true
} else {
arena.bool_false
}
} else if na == a && nb == b {
id
} else {
arena.ge(na, nb)
}
}
ExprNode::Eq_(a, b) => {
let na = cache.get(&a).copied().unwrap_or(a);
let nb = cache.get(&b).copied().unwrap_or(b);
if let Some(equal) =
gaussian_equal(arena, na, nb).or_else(|| equal_by_difference(arena, na, nb))
{
if equal {
arena.bool_true
} else {
arena.bool_false
}
} else if na == a && nb == b {
id
} else {
arena.eq_(na, nb)
}
}
ExprNode::Ne(a, b) => {
let na = cache.get(&a).copied().unwrap_or(a);
let nb = cache.get(&b).copied().unwrap_or(b);
if let Some(equal) =
gaussian_equal(arena, na, nb).or_else(|| equal_by_difference(arena, na, nb))
{
if equal {
arena.bool_false
} else {
arena.bool_true
}
} else if na == a && nb == b {
id
} else {
arena.ne_(na, nb)
}
}
ExprNode::And(ref children) => {
let new: smallvec::SmallVec<[ExprId; 6]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
let mut filtered: smallvec::SmallVec<[ExprId; 6]> = smallvec::SmallVec::new();
for &c in &new {
if c == arena.bool_false {
cache.insert(id, arena.bool_false);
continue;
}
if c == arena.bool_true {
continue; }
filtered.push(c);
}
if new.contains(&arena.bool_false) {
arena.bool_false
} else if filtered.is_empty() {
arena.bool_true
} else if filtered.len() == 1 {
filtered[0]
} else if filtered == *children {
id
} else {
arena.and(&filtered)
}
}
ExprNode::Or(ref children) => {
let new: smallvec::SmallVec<[ExprId; 6]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
if new.contains(&arena.bool_true) {
arena.bool_true
} else {
let filtered: smallvec::SmallVec<[ExprId; 6]> = new
.iter()
.copied()
.filter(|&c| c != arena.bool_false)
.collect();
if filtered.is_empty() {
arena.bool_false
} else if filtered.len() == 1 {
filtered[0]
} else if filtered == *children {
id
} else {
arena.or(&filtered)
}
}
}
ExprNode::Not(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if ni == arena.bool_true {
arena.bool_false
} else if ni == arena.bool_false {
arena.bool_true
} else if ni == inner {
id
} else {
arena.not(ni)
}
}
ExprNode::Piecewise(ref pairs) => {
let t = arena.bool_true;
let f = arena.bool_false;
let mut kept: smallvec::SmallVec<[(ExprId, ExprId); 3]> = smallvec::SmallVec::new();
for &(val, cond) in pairs.iter() {
let nv = cache.get(&val).copied().unwrap_or(val);
let nc = cache.get(&cond).copied().unwrap_or(cond);
if nc == f {
continue;
}
kept.push((nv, nc));
if nc == t {
break;
}
}
if kept.is_empty() {
arena.nan
} else if kept.len() == 1 && kept[0].1 == t {
kept[0].0
} else if kept == *pairs {
id
} else {
arena.intern(ExprNode::Piecewise(kept))
}
}
ExprNode::Apply(name_sid, ref args) => {
let new_args: smallvec::SmallVec<[ExprId; 2]> = args
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
let folded = match arena.lib_fn(name_sid) {
Some(f) if f.arity().accepts(new_args.len()) => {
eval_lib_fn(arena, f, &new_args)
}
_ => None,
};
match folded {
Some(result) => result,
None if new_args[..] == args[..] => id,
None => arena.intern(ExprNode::Apply(name_sid, new_args)),
}
}
ExprNode::Floor(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(r) = arena.as_num(ni).cloned() {
if r.is_integer() {
ni
} else {
let p = r.numer().clone();
let q = r.denom().clone();
let floor_val = if p.is_negative() && !(&p % &q).is_zero() {
&p / &q - BigInt::from(1)
} else {
&p / &q
};
arena.big_int(floor_val)
}
} else if ni == inner {
id
} else {
arena.floor(ni)
}
}
ExprNode::Ceiling(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if let Some(r) = arena.as_num(ni).cloned() {
if r.is_integer() {
ni
} else {
let p = r.numer().clone();
let q = r.denom().clone();
let ceil_val = if p.is_positive() && !(&p % &q).is_zero() {
&p / &q + BigInt::from(1)
} else {
&p / &q
};
arena.big_int(ceil_val)
}
} else if ni == inner {
id
} else {
arena.ceiling(ni)
}
}
ExprNode::Min(ref children) => {
let new: smallvec::SmallVec<[ExprId; 4]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
let all_numeric: Option<Vec<(Q, ExprId)>> = new
.iter()
.map(|&c| arena.as_num(c).cloned().map(|r| (r, c)))
.collect();
if let Some(nums) = all_numeric {
if let Some((_, min_id)) = nums.iter().min_by_key(|(r, _)| r.clone()) {
*min_id
} else {
id
}
} else if let Some(m) = extremum_of_real_constants(arena, &new, true) {
m
} else if new[..] == children[..] {
id
} else {
arena.intern(ExprNode::Min(new))
}
}
ExprNode::Max(ref children) => {
let new: smallvec::SmallVec<[ExprId; 4]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
let all_numeric: Option<Vec<(Q, ExprId)>> = new
.iter()
.map(|&c| arena.as_num(c).cloned().map(|r| (r, c)))
.collect();
if let Some(nums) = all_numeric {
if let Some((_, max_id)) = nums.iter().max_by_key(|(r, _)| r.clone()) {
*max_id
} else {
id
}
} else if let Some(m) = extremum_of_real_constants(arena, &new, false) {
m
} else if new[..] == children[..] {
id
} else {
arena.intern(ExprNode::Max(new))
}
}
ExprNode::Sum(body, sum_var, lo, hi) => {
let nbody = cache.get(&body).copied().unwrap_or(body);
let nvar = cache.get(&sum_var).copied().unwrap_or(sum_var);
let nlo = cache.get(&lo).copied().unwrap_or(lo);
let nhi = cache.get(&hi).copied().unwrap_or(hi);
let lo_int = arena.as_num(nlo).and_then(|r| {
if r.is_integer() {
r.numer().to_i64()
} else {
None
}
});
let hi_int = arena.as_num(nhi).and_then(|r| {
if r.is_integer() {
r.numer().to_i64()
} else {
None
}
});
if let (Some(lo_val), Some(hi_val)) = (lo_int, hi_int)
&& hi_val < lo_val
{
cache.insert(id, arena.zero);
continue;
}
if let Some(closed) =
crate::calculus::sum_eval::eval_sum_symbolic(arena, nbody, nvar, nlo, nhi)
{
let result = eval(arena, closed);
cache.insert(id, result);
continue;
}
if let (Some(lo_val), Some(hi_val)) = (lo_int, hi_int) {
if (hi_val - lo_val) <= 1000 {
let mut terms = smallvec::SmallVec::<[ExprId; 8]>::new();
for k in lo_val..=hi_val {
let k_expr = arena.int(k);
let substituted = arena.subs_structural(nbody, nvar, k_expr);
let evaluated_term = eval(arena, substituted);
terms.push(evaluated_term);
}
if terms.is_empty() {
arena.zero
} else {
arena.add(&terms)
}
} else if nbody == body && nvar == sum_var && nlo == lo && nhi == hi {
id
} else {
arena.intern(ExprNode::Sum(nbody, nvar, nlo, nhi))
}
} else if nbody == body && nvar == sum_var && nlo == lo && nhi == hi {
id
} else {
arena.intern(ExprNode::Sum(nbody, nvar, nlo, nhi))
}
}
ExprNode::Product_(body, prod_var, lo, hi) => {
let nbody = cache.get(&body).copied().unwrap_or(body);
let nvar = cache.get(&prod_var).copied().unwrap_or(prod_var);
let nlo = cache.get(&lo).copied().unwrap_or(lo);
let nhi = cache.get(&hi).copied().unwrap_or(hi);
let lo_int = arena.as_num(nlo).and_then(|r| {
if r.is_integer() {
r.numer().to_i64()
} else {
None
}
});
let hi_int = arena.as_num(nhi).and_then(|r| {
if r.is_integer() {
r.numer().to_i64()
} else {
None
}
});
if let Some(closed) =
crate::calculus::sum_eval::eval_product_symbolic(arena, nbody, nvar, nlo, nhi)
{
let result = eval(arena, closed);
cache.insert(id, result);
continue;
}
if let (Some(lo_val), Some(hi_val)) = (lo_int, hi_int) {
if hi_val < lo_val {
arena.one
} else if (hi_val - lo_val) <= 1000 {
let mut factors = smallvec::SmallVec::<[ExprId; 8]>::new();
for k in lo_val..=hi_val {
let k_expr = arena.int(k);
let substituted = arena.subs_structural(nbody, nvar, k_expr);
let evaluated_term = eval(arena, substituted);
factors.push(evaluated_term);
}
if factors.is_empty() {
arena.one
} else {
arena.mul(&factors)
}
} else if nbody == body && nvar == prod_var && nlo == lo && nhi == hi {
id
} else {
arena.intern(ExprNode::Product_(nbody, nvar, nlo, nhi))
}
} else if nbody == body && nvar == prod_var && nlo == lo && nhi == hi {
id
} else {
arena.intern(ExprNode::Product_(nbody, nvar, nlo, nhi))
}
}
ExprNode::DefiniteIntegral(body, int_var, lo, hi) => {
let nbody = cache.get(&body).copied().unwrap_or(body);
let nvar = cache.get(&int_var).copied().unwrap_or(int_var);
let nlo = cache.get(&lo).copied().unwrap_or(lo);
let nhi = cache.get(&hi).copied().unwrap_or(hi);
arena.definite_integral(nbody, nvar, nlo, nhi)
}
ExprNode::RootSum(poly, body, sumvar) => {
let poly = cache.get(&poly).copied().unwrap_or(poly);
let body = cache.get(&body).copied().unwrap_or(body);
let sumvar = cache.get(&sumvar).copied().unwrap_or(sumvar);
if let Some(expanded) =
crate::calculus::risch::log_to_real::rootsum_doit(arena, poly, body, sumvar)
{
tracing::debug!("eval: RootSum expanded via rootsum_doit");
expanded
} else if let Some(rational_val) =
crate::calculus::risch::log_to_real::vieta_rootsum_poly_body(
arena, poly, body, sumvar,
)
{
tracing::debug!("eval: RootSum evaluated via Vieta's formulas");
let num_id = arena.intern_num(rational_val);
arena.intern(ExprNode::Num(num_id))
} else {
arena.intern(ExprNode::RootSum(poly, body, sumvar))
}
}
_ => id,
};
cache.insert(id, evaluated);
}
cache.get(&expr).copied().unwrap_or(expr)
}
fn log10_factorial_lower(n: f64) -> f64 {
if n < 1.0 {
0.0
} else {
n * (n / std::f64::consts::E).log10().max(0.0)
}
}
fn beyond_digit_guard(arena: &Arena, digits: f64) -> bool {
digits.is_nan() || digits > arena.config.max_result_digits as f64
}
fn guarded_num(arena: &mut Arena, r: Q) -> Option<ExprId> {
let limit = arena.config.max_result_digits as f64;
let est = (r.numer().bits() + r.denom().bits()) as f64 * std::f64::consts::LOG10_2;
if est > limit + 2.0 {
return None;
}
if est >= limit - 2.0 {
let digits = r.numer().to_string().len() + r.denom().to_string().len();
if digits > arena.config.max_result_digits {
return None;
}
}
let nid = arena.intern_num(r);
Some(arena.intern(ExprNode::Num(nid)))
}
fn progression_product(a: &BigInt, d: &BigInt, n: u64) -> BigInt {
let mut acc = BigInt::one();
let mut term = a.clone();
for _ in 0..n {
acc *= &term;
term += d;
}
acc
}
fn progression_digits_lower(a: &BigInt, d: &BigInt, n: u64) -> f64 {
if n < 2 {
return 0.0;
}
let n = n as f64;
let log_d = d.abs().to_f64().unwrap_or(f64::INFINITY).log10();
if a.sign() == d.sign() {
log10_factorial_lower(n - 1.0) + (n - 1.0) * log_d
} else {
2.0 * log10_factorial_lower(((n - 1.0) / 2.0).floor()) + (n - 2.0).max(0.0) * log_d
}
}
fn exact_factorial_power(arena: &mut Arena, x: &Q, n: u64, step: i32) -> Option<ExprId> {
let (p, q) = (x.numer(), x.denom());
let d = q * BigInt::from(step);
if x.is_integer() {
let reach = BigInt::from(n) - BigInt::one();
let hits_zero = if step > 0 {
!p.is_positive() && -p <= reach
} else {
!p.is_negative() && *p <= reach
};
if hits_zero {
return Some(arena.zero);
}
}
let q_digits = (n as f64) * q.to_f64().unwrap_or(f64::INFINITY).log10();
if beyond_digit_guard(arena, progression_digits_lower(p, &d, n) + q_digits) {
return None;
}
let numer = progression_product(p, &d, n);
let denom = num_traits::Pow::pow(q.clone(), n);
guarded_num(arena, Ratio::new_raw(numer, denom))
}
fn eval_factorial(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let r = arena.as_num(inner)?;
if !r.is_integer() || r.is_negative() {
return None;
}
let n: u64 = r.to_integer().try_into().ok()?;
if beyond_digit_guard(arena, log10_factorial_lower(n as f64)) {
return None;
}
let result = Ratio::from_integer(crate::base::combinatorics::factorial(n));
guarded_num(arena, result)
}
fn eval_gamma(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let r = arena.as_num(inner)?.clone();
if *r.denom() == BigInt::from(2) && r.is_positive() {
let p = r.numer().clone();
let k_big = (&p - BigInt::from(1)) / BigInt::from(2);
let k: u64 = k_big.try_into().ok()?;
let kf = k as f64;
if beyond_digit_guard(
arena,
log10_factorial_lower(kf) + kf * std::f64::consts::LOG10_2,
) {
return None;
}
let mut double_fact = BigInt::from(1);
for i in 0..k {
double_fact *= BigInt::from(2 * i + 1);
}
let two_pow_k = BigInt::from(1) << (k as usize);
let coeff = Ratio::new(double_fact, two_pow_k);
if coeff.is_one() {
let pi = arena.pi;
return Some(arena.sqrt(pi));
}
let coeff_node = guarded_num(arena, coeff)?;
let pi = arena.pi;
let sqrt_pi = arena.sqrt(pi);
let result = arena.mul(&[coeff_node, sqrt_pi]);
return Some(result);
}
if r.is_integer() && r.is_positive() {
let n: u64 = r.to_integer().try_into().ok()?;
if beyond_digit_guard(arena, log10_factorial_lower((n - 1) as f64)) {
return None;
}
let result = Ratio::from_integer(crate::base::combinatorics::factorial(n - 1));
return guarded_num(arena, result);
}
if r.is_positive() && !r.is_integer() && *r.denom() != BigInt::from(2) {
let n_big = r.to_integer(); if n_big >= BigInt::from(1) {
let n: u64 = n_big.clone().try_into().ok()?;
let frac = &r - Ratio::from_integer(n_big);
let prod_node = exact_factorial_power(arena, &frac, n, 1)?;
let frac_id = {
let nid = arena.intern_num(frac);
arena.intern(ExprNode::Num(nid))
};
let gamma_frac = eval_gamma(arena, frac_id).unwrap_or_else(|| arena.gamma(frac_id));
if prod_node == arena.one {
return Some(gamma_frac);
}
return Some(arena.mul(&[prod_node, gamma_frac]));
}
}
if r.is_negative() && !r.is_integer() {
let neg_r = -&r; let m_big = if neg_r.is_integer() {
neg_r.to_integer()
} else {
neg_r.to_integer() + BigInt::from(1)
};
let m: u64 = m_big.clone().try_into().ok()?;
let frac = &r + Ratio::from_integer(m_big); let denom_node = exact_factorial_power(arena, &r, m, 1)?;
let denom_product = arena.as_num(denom_node)?.clone();
if denom_product.is_zero() {
return None;
}
let frac_id = {
let nid = arena.intern_num(frac);
arena.intern(ExprNode::Num(nid))
};
let gamma_frac = eval_gamma(arena, frac_id).unwrap_or_else(|| arena.gamma(frac_id));
let inv_denom = Ratio::one() / denom_product;
let coeff_nid = arena.intern_num(inv_denom);
let coeff_node = arena.intern(ExprNode::Num(coeff_nid));
return Some(arena.mul(&[coeff_node, gamma_frac]));
}
None
}
fn order_against_zero(arena: &Arena, a: ExprId, b: ExprId, strict: bool) -> Option<bool> {
use crate::base::assumptions::{AssumptionCache, Props};
let is_zero = |id: ExprId| arena.as_num(id).is_some_and(Zero::is_zero);
let (x, flipped) = if is_zero(b) {
(a, false)
} else if is_zero(a) {
(b, true)
} else {
return None;
};
if !walk::free_symbols(arena, x).is_empty() {
return None;
}
let mut signs = AssumptionCache::new();
let mut known = |p: Props| signs.query(arena, x, p) == Some(true);
let (holds, fails) = match (strict, flipped) {
(true, false) => (Props::POSITIVE, Props::NONPOSITIVE),
(false, false) => (Props::NONNEGATIVE, Props::NEGATIVE),
(true, true) => (Props::NEGATIVE, Props::NONNEGATIVE),
(false, true) => (Props::NONPOSITIVE, Props::POSITIVE),
};
if known(holds) {
Some(true)
} else if known(fails) {
Some(false)
} else {
None
}
}
fn order_by_difference(arena: &mut Arena, a: ExprId, b: ExprId, strict: bool) -> Option<bool> {
let d = arena.sub(a, b);
let rational = arena.as_num(d).cloned();
if rational.is_none() && !walk::free_symbols(arena, d).is_empty() {
return None;
}
if !known_real(arena, a) || !known_real(arena, b) {
return None;
}
let q = match rational {
Some(q) => q,
None => match algebraic_rational_value(arena, d)? {
Some(q) => q,
None => {
return Some(arena.sign_of_real_constant(d)? == std::cmp::Ordering::Greater);
}
},
};
Some(if strict {
q.is_positive()
} else {
!q.is_negative()
})
}
fn known_real(arena: &Arena, x: ExprId) -> bool {
use crate::base::assumptions::{AssumptionCache, Props};
use crate::transforms::evalf::{Settled, ZeroSearch, evalf_settled};
match AssumptionCache::new().query(arena, x, Props::REAL) {
Some(real) => real,
None => {
walk::free_symbols(arena, x).is_empty()
&& matches!(
evalf_settled(arena, x, 16, ZeroSearch::Cap),
Ok((z, Settled::Certified)) if z.1.is_zero()
)
}
}
}
fn equal_by_difference(arena: &mut Arena, a: ExprId, b: ExprId) -> Option<bool> {
let non_scalar = |id: ExprId| {
let node = arena.node(id);
node.is_set_node()
|| matches!(
node,
ExprNode::BoolTrue
| ExprNode::BoolFalse
| ExprNode::Gt(..)
| ExprNode::Ge(..)
| ExprNode::Eq_(..)
| ExprNode::Ne(..)
| ExprNode::And(_)
| ExprNode::Or(_)
| ExprNode::Not(_)
)
};
if non_scalar(a) || non_scalar(b) {
return None;
}
let d = arena.sub(a, b);
if let Some((re, im)) = gaussian_rational(arena, d) {
return Some(re.is_zero() && im.is_zero());
}
if !walk::free_symbols(arena, d).is_empty() {
return None;
}
match algebraic_rational_value(arena, d)? {
Some(q) => Some(q.is_zero()),
None => Some(false),
}
}
fn algebraic_rational_value(arena: &mut Arena, d: ExprId) -> Option<Option<Q>> {
if !small_algebraic(arena, d) {
return None;
}
let mp = crate::poly::algebraic::minimal_polynomial(arena, d)?;
match mp.degree()? {
1 => Some(Some(-mp.coeff(0) / mp.coeff(1))),
_ => Some(None),
}
}
fn small_algebraic(arena: &Arena, d: ExprId) -> bool {
let order = walk::post_order_ids(arena, d);
if order.len() > 160 {
return false;
}
let mut degree: u64 = 1;
for &id in &order {
let factor = match arena.node(id) {
ExprNode::Num(_) | ExprNode::Add(_) | ExprNode::Mul(_) | ExprNode::Neg(_) => 1,
ExprNode::ImaginaryUnit | ExprNode::GoldenRatio => 2,
ExprNode::Pow(_, e) => match arena.as_num(*e) {
Some(q) => q.denom().to_u64().unwrap_or(u64::MAX),
None => return false,
},
_ => return false,
};
degree = degree.saturating_mul(factor);
if degree > 64 {
return false;
}
}
true
}
fn eval_log_gamma(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let r = arena.as_num(inner)?.clone();
if !r.is_integer() || !r.is_positive() {
return None;
}
let n: u64 = r.to_integer().try_into().ok()?;
if beyond_digit_guard(arena, log10_factorial_lower((n - 1) as f64)) {
return None;
}
let fact = Ratio::from_integer(crate::base::combinatorics::factorial(n - 1));
if fact == Ratio::<BigInt>::one() {
return Some(arena.zero);
}
let fact_id = guarded_num(arena, fact)?;
Some(arena.ln(fact_id))
}
fn eval_erf(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(r) = arena.as_num(inner)
&& r.is_zero()
{
return Some(arena.zero);
}
if matches!(arena.node(inner), ExprNode::Infinity) {
return Some(arena.one);
}
if matches!(arena.node(inner), ExprNode::NegInfinity) {
return Some(arena.neg_one);
}
if let ExprNode::Neg(pos_inner) = arena.node(inner).clone() {
let erf_pos = arena.erf(pos_inner);
return Some(arena.neg(erf_pos));
}
None
}
fn eval_lambertw(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(r) = arena.as_num(inner)
&& r.is_zero()
{
tracing::debug!("eval: LambertW(0) = 0");
return Some(arena.zero);
}
if inner == arena.e_const {
tracing::debug!("eval: LambertW(e) = 1");
return Some(arena.one);
}
if inner == arena.infinity {
tracing::debug!("eval: LambertW(∞) = ∞");
return Some(arena.infinity);
}
{
let e = arena.e_const;
let neg1 = arena.neg_one;
let neg_inv_e = arena.div(neg1, e);
if inner == neg_inv_e {
tracing::debug!("eval: LambertW(-1/e) = -1");
return Some(arena.neg_one);
}
}
{
let two = arena.int(2);
let ln2 = arena.ln(two);
let neg_ln2 = arena.neg(ln2);
let target = arena.div(neg_ln2, two);
if inner == target {
tracing::debug!("eval: LambertW(-ln(2)/2) = -ln(2)");
return Some(neg_ln2);
}
}
{
let two = arena.int(2);
let ln2 = arena.ln(two);
let target = arena.mul(&[two, ln2]);
if inner == target {
tracing::debug!("eval: LambertW(2*ln(2)) = ln(2)");
return Some(ln2);
}
}
{
let pi = arena.pi;
let two = arena.int(2);
let neg_pi = arena.neg(pi);
let target = arena.div(neg_pi, two);
if inner == target {
tracing::debug!("eval: LambertW(-π/2) = iπ/2");
let i = arena.i_unit;
let pi_over_2 = arena.div(pi, two);
return Some(arena.mul(&[i, pi_over_2]));
}
}
{
let e = arena.e_const;
let one = arena.one;
let one_plus_e = arena.add(&[one, e]);
let target_exp = arena.exp(one_plus_e);
if inner == target_exp {
tracing::debug!("eval: LambertW(e^(1+e)) = e");
return Some(e);
}
let target_pow = arena.pow(e, one_plus_e);
if inner == target_pow {
tracing::debug!("eval: LambertW(e^(1+e)) = e");
return Some(e);
}
}
if let Some(w) = lambert_fixed_point(arena, inner, LambertRealBranch::Principal) {
tracing::debug!("eval: LambertW(w*exp(w)) = w");
return Some(w);
}
None
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LambertRealBranch {
Principal,
MinusOne,
}
fn lambert_fixed_point(arena: &mut Arena, x: ExprId, branch: LambertRealBranch) -> Option<ExprId> {
let on_branch = |w_plus_one: std::cmp::Ordering| match branch {
LambertRealBranch::Principal => w_plus_one.is_ge(),
LambertRealBranch::MinusOne => w_plus_one.is_le(),
};
let ExprNode::Mul(factors) = arena.node(x).clone() else {
return None;
};
let [a, b] = factors[..] else {
return None;
};
let q = arena.as_num(a)?.clone();
match arena.node(b).clone() {
ExprNode::Exp(c) => {
let c = arena.as_num(c)?.clone();
if c != q {
return None;
}
let w_plus_one = (&c + Q::from_integer(BigInt::from(1))).cmp(&Q::zero());
on_branch(w_plus_one).then(|| arena.num_ratio(c))
}
ExprNode::Ln(p_id) => {
let p = arena.as_num(p_id)?.clone();
if !p.is_integer() || p <= Q::from_integer(BigInt::from(1)) {
return None;
}
let base = p.to_integer();
let ln_p = base.to_f64()?.ln();
for negative in [false, true] {
let limit = if negative { q.denom() } else { q.numer() }.bits() + 8;
let mut power = BigInt::from(1);
for t_abs in 1i32..=64 {
power *= &base;
if power.bits() > limit {
break;
}
let t = if negative { -t_abs } else { t_abs };
let candidate = if negative {
Q::new(BigInt::from(t), power.clone())
} else {
Q::from_integer(BigInt::from(t) * &power)
};
if candidate != q {
continue;
}
let w_plus_one = (f64::from(t) * ln_p + 1.0)
.partial_cmp(&0.0)
.unwrap_or(std::cmp::Ordering::Equal);
if on_branch(w_plus_one) {
let t_id = arena.int(i64::from(t));
let ln_id = arena.ln(p_id);
return Some(arena.mul(&[t_id, ln_id]));
}
}
}
None
}
_ => None,
}
}
fn eval_lambertw_call(arena: &mut Arena, args: &[ExprId]) -> Option<ExprId> {
let x = args[0];
let principal =
|arena: &mut Arena| eval_lambertw(arena, x).unwrap_or_else(|| arena.lambertw(x));
let Some(&k) = args.get(1) else {
return Some(principal(arena));
};
let kq = arena.as_num(k)?.clone();
if !kq.is_integer() {
return None;
}
if kq.is_zero() {
return Some(principal(arena));
}
if arena.as_num(x).is_some_and(|q| q.is_zero()) {
tracing::debug!("eval: LambertW(0, k) = -oo for k != 0");
return Some(arena.neg_infinity);
}
if kq != Q::from_integer(BigInt::from(-1)) {
return None;
}
let e = arena.e_const;
let neg1 = arena.neg_one;
let neg_inv_e = arena.div(neg1, e);
let exp_neg1 = arena.exp(neg1);
let neg_exp_neg1 = arena.neg(exp_neg1);
if x == neg_inv_e || x == neg_exp_neg1 {
tracing::debug!("eval: LambertW(-1/e, -1) = -1");
return Some(neg1);
}
let pi = arena.pi;
let neg_half = arena.rational(-1, 2);
let neg_pi_2 = arena.mul(&[neg_half, pi]);
if x == neg_pi_2 {
tracing::debug!("eval: LambertW(-pi/2, -1) = -i*pi/2");
let i = arena.i_unit;
return Some(arena.mul(&[neg_half, i, pi]));
}
lambert_fixed_point(arena, x, LambertRealBranch::MinusOne)
}
fn eval_erfc(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(r) = arena.as_num(inner)
&& r.is_zero()
{
return Some(arena.one);
}
if matches!(arena.node(inner), ExprNode::Infinity) {
return Some(arena.zero);
}
if matches!(arena.node(inner), ExprNode::NegInfinity) {
return Some(arena.int(2));
}
None
}
fn eval_beta(arena: &mut Arena, a: ExprId, b: ExprId) -> Option<ExprId> {
let ra = arena.as_num(a)?.clone();
let rb = arena.as_num(b)?.clone();
if !ra.is_integer() || !ra.is_positive() || !rb.is_integer() || !rb.is_positive() {
return None;
}
let a_int = ra.to_integer();
let b_int = rb.to_integer();
let n = &a_int + &b_int - BigInt::one();
let j = (&a_int - BigInt::one()).min(b_int.clone());
let (nf, jf) = (n.to_f64()?, j.to_f64()?);
if jf >= 1.0 && beyond_digit_guard(arena, jf * (nf / jf).log10()) {
return None;
}
let c = crate::base::combinatorics::binomial(n, j);
guarded_num(arena, Ratio::new(BigInt::one(), c * b_int))
}
fn gaussian_rational(arena: &Arena, id: ExprId) -> Option<(Q, Q)> {
let imag = |t: ExprId| -> Option<Q> {
if t == arena.i_unit {
return Some(Q::one());
}
match arena.node(t) {
ExprNode::Mul(cs) if cs.len() == 2 => {
let (c, u) = (cs[0], cs[1]);
match (arena.as_num(c), arena.as_num(u)) {
(Some(q), None) if u == arena.i_unit => Some(q.clone()),
(None, Some(q)) if c == arena.i_unit => Some(q.clone()),
_ => None,
}
}
_ => None,
}
};
if let Some(q) = arena.as_num(id) {
return Some((q.clone(), Q::zero()));
}
if let Some(q) = imag(id) {
return Some((Q::zero(), q));
}
if let ExprNode::Add(cs) = arena.node(id)
&& cs.len() == 2
{
for (r, i) in [(cs[0], cs[1]), (cs[1], cs[0])] {
if let (Some(re), Some(im)) = (arena.as_num(r), imag(i)) {
return Some((re.clone(), im));
}
}
}
None
}
fn gaussian_equal(arena: &Arena, a: ExprId, b: ExprId) -> Option<bool> {
let x = gaussian_rational(arena, a)?;
let y = gaussian_rational(arena, b)?;
Some(x == y)
}
fn eval_binomial(arena: &mut Arena, n: ExprId, k: ExprId) -> Option<ExprId> {
let nr = arena.as_num(n)?.clone();
let kr = arena.as_num(k)?.clone();
if !kr.is_integer() {
return None;
}
if kr.is_negative() {
if nr.is_integer() && nr.is_negative() {
return None;
}
return Some(arena.zero);
}
if !nr.is_integer() || nr.is_negative() {
let k_u64: u64 = kr.to_integer().try_into().ok()?;
let falling = exact_factorial_power(arena, &nr, k_u64, -1)?;
let falling = arena.as_num(falling)?.clone();
let k_digits = log10_factorial_lower(k_u64 as f64);
if beyond_digit_guard(arena, k_digits) {
return None;
}
let mut fact = BigInt::one();
for i in 2..=k_u64 {
fact *= BigInt::from(i);
}
return guarded_num(arena, falling / Ratio::from_integer(fact));
}
let (n_big, k_big) = (nr.to_integer(), kr.to_integer());
if k_big > n_big {
return Some(arena.zero);
}
let j_big = (&n_big - &k_big).min(k_big.clone());
let j: u64 = j_big.try_into().ok()?;
let n_f = n_big.to_f64().unwrap_or(f64::INFINITY);
if j >= 1 && beyond_digit_guard(arena, j as f64 * (n_f / j as f64).log10()) {
return None;
}
let result = match (u64::try_from(&n_big), u64::try_from(&k_big)) {
(Ok(n_u64), Ok(k_u64)) => crate::base::combinatorics::binomial(n_u64, k_u64),
_ => {
let mut numer = BigInt::one();
for i in 0..j {
numer *= &n_big - BigInt::from(i);
}
numer / crate::base::combinatorics::factorial(j)
}
};
guarded_num(arena, Ratio::from_integer(result))
}
fn as_negated_general(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if let ExprNode::Neg(inner) = arena.node(x) {
return Some(*inner);
}
let (coeff, term) = arena.as_coeff_term(x);
if coeff.is_negative() {
Some(arena.make_coeff_term(-coeff, term))
} else {
None
}
}
pub(crate) fn eval_si(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if x == arena.zero {
return Some(arena.zero);
}
if x == arena.infinity {
let half = arena.rational(1, 2);
return Some(arena.mul(&[half, arena.pi]));
}
if x == arena.neg_infinity {
let neg_half = arena.rational(-1, 2);
return Some(arena.mul(&[neg_half, arena.pi]));
}
if x == arena.nan {
return Some(arena.nan);
}
if let Some(y) = as_negated_general(arena, x) {
let si_y = arena.si(y);
return Some(arena.neg(si_y));
}
None
}
pub(crate) fn eval_ci(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if x == arena.infinity {
return Some(arena.zero);
}
if x == arena.zero {
return Some(arena.complex_infinity);
}
if x == arena.nan {
return Some(arena.nan);
}
None
}
pub(crate) fn eval_ei(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if x == arena.neg_infinity {
return Some(arena.zero);
}
if x == arena.infinity {
return Some(arena.infinity);
}
if x == arena.zero {
return Some(arena.neg_infinity);
}
if x == arena.nan {
return Some(arena.nan);
}
None
}
pub(crate) fn eval_li(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if x == arena.zero {
return Some(arena.zero);
}
if x == arena.one {
return Some(arena.neg_infinity);
}
if x == arena.infinity {
return Some(arena.infinity);
}
if x == arena.nan {
return Some(arena.nan);
}
if x == arena.e_const {
return Some(arena.ei(arena.one));
}
if let ExprNode::Exp(y) = arena.node(x).clone()
&& log_exp_is_identity(arena, y)
{
return Some(arena.ei(y));
}
None
}
fn log_exp_is_identity(arena: &mut Arena, y: ExprId) -> bool {
use crate::base::assumptions::{AssumptionCache, Props};
if AssumptionCache::new().query(arena, y, Props::REAL) == Some(true) {
return true;
}
if !walk::free_symbols(arena, y).is_empty() {
return false;
}
let parts = crate::base::complex::decompose(arena, y);
if !parts.exact {
return false;
}
let im = eval(arena, parts.im);
if im == arena.pi || arena.is_zero_structural(im) {
return true;
}
crate::transforms::evalf::evalf_f64(arena, im)
.is_ok_and(|v| v.is_finite() && v.abs() < std::f64::consts::PI * (1.0 - 1e-9))
}
const MAX_EXACT_ZETA_EVEN: i64 = 200;
pub(crate) fn eval_zeta(arena: &mut Arena, s: ExprId) -> Option<ExprId> {
if s == arena.infinity {
return Some(arena.one);
}
if s == arena.nan {
return Some(arena.nan);
}
let r = arena.as_num(s)?.clone();
if !r.is_integer() {
return None;
}
let n: i64 = r.to_integer().try_into().ok()?;
if n == 1 {
return Some(arena.complex_infinity);
}
if n == 0 {
return Some(arena.rational(-1, 2));
}
if n < 0 {
let m = (-n) as usize;
if m.is_multiple_of(2) {
return Some(arena.zero);
}
let b = exact_bernoulli(arena, m as u64 + 1)?;
let val = -b / Ratio::from_integer(BigInt::from(m as i64 + 1));
return guarded_num(arena, val);
}
if n % 2 == 0 && n <= MAX_EXACT_ZETA_EVEN {
let two_k = n as usize;
let b = crate::base::bernoulli::bernoulli(two_k).abs();
let mut fact = BigInt::from(1);
for i in 2..=two_k {
fact *= BigInt::from(i as u64);
}
let pow2 = BigInt::from(1) << (two_k - 1);
let coeff = b * Ratio::from_integer(pow2) / Ratio::from_integer(fact);
let nid = arena.intern_num(coeff);
let coeff_id = arena.intern(ExprNode::Num(nid));
let exp_id = arena.int(n);
let pi_pow = arena.pow(arena.pi, exp_id);
return Some(arena.mul(&[coeff_id, pi_pow]));
}
None
}
const MAX_POLYGAMMA_SHIFT: i64 = 64;
const MAX_POLYGAMMA_CANCEL_BITS: f64 = 200.0;
fn factorial_ratio(n: usize) -> Q {
Ratio::from_integer(crate::base::combinatorics::factorial(n as u64))
}
pub(crate) fn eval_digamma(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
let r = arena.as_num(x)?.clone();
let two = BigInt::from(2);
if r.is_integer() {
let m: i64 = r.to_integer().try_into().ok()?;
if m <= 0 {
return Some(arena.complex_infinity);
}
if m > MAX_POLYGAMMA_SHIFT {
return None;
}
let mut h = Ratio::<BigInt>::zero();
for k in 1..m {
h += Ratio::new(BigInt::from(1), BigInt::from(k));
}
let neg_gamma = arena.neg(arena.euler_gamma);
let nid = arena.intern_num(h);
let h_id = arena.intern(ExprNode::Num(nid));
return Some(arena.add(&[neg_gamma, h_id]));
}
if *r.denom() == two {
let m: i64 = r.floor().to_integer().try_into().ok()?;
if !(0..=MAX_POLYGAMMA_SHIFT).contains(&m) {
return None;
}
let neg_gamma = arena.neg(arena.euler_gamma);
let two_id = arena.int(2);
let ln2 = arena.ln(two_id);
let neg_two = arena.int(-2);
let neg_two_ln2 = arena.mul(&[neg_two, ln2]);
let mut h = Ratio::<BigInt>::zero();
for k in 0..m {
h += Ratio::new(BigInt::from(2), BigInt::from(2 * k + 1));
}
let nid = arena.intern_num(h);
let h_id = arena.intern(ExprNode::Num(nid));
return Some(arena.add(&[neg_gamma, neg_two_ln2, h_id]));
}
None
}
pub(crate) fn eval_polygamma(arena: &mut Arena, n: ExprId, x: ExprId) -> Option<ExprId> {
if n == arena.nan || x == arena.nan {
return Some(arena.nan);
}
let nr = arena.as_num(n)?.clone();
if !nr.is_integer() || nr.is_negative() {
return None;
}
if nr.is_zero() {
return Some(arena.digamma(x));
}
let n_usize: usize = nr.to_integer().try_into().ok()?;
let xr = arena.as_num(x)?.clone();
if xr.is_integer() && !xr.is_positive() {
return Some(arena.complex_infinity);
}
let half = *xr.denom() == BigInt::from(2);
let m: i64 = if xr.is_integer() || half {
xr.floor().to_integer().try_into().ok()?
} else {
return None;
};
if !(0..=MAX_POLYGAMMA_SHIFT).contains(&m) {
return None;
}
if beyond_digit_guard(arena, log10_factorial_lower(n_usize as f64)) {
return None;
}
let shifted_from = if half { 2 * m + 1 } else { m };
if shifted_from >= 2
&& (n_usize as f64 + 1.0) * (shifted_from as f64).log2() > MAX_POLYGAMMA_CANCEL_BITS
{
return None;
}
let sign = if n_usize.is_multiple_of(2) { -1 } else { 1 }; let n_fact = factorial_ratio(n_usize);
let np1 = arena.int(n_usize as i64 + 1);
let zeta_np1 = arena.zeta(np1);
if xr.is_integer() {
let base_coeff = &n_fact * Ratio::from_integer(BigInt::from(sign));
let nid = arena.intern_num(base_coeff);
let base_coeff_id = arena.intern(ExprNode::Num(nid));
let base = arena.mul(&[base_coeff_id, zeta_np1]);
let mut shift = Ratio::<BigInt>::zero();
for k in 1..m {
let kp = BigInt::from(k).pow((n_usize + 1) as u32);
shift += Ratio::new(BigInt::from(1), kp);
}
let shift = shift * &n_fact * Ratio::from_integer(BigInt::from(-sign));
let nid = arena.intern_num(shift);
let shift_id = arena.intern(ExprNode::Num(nid));
return Some(arena.add(&[base, shift_id]));
}
if half {
let two_pow = (BigInt::from(1) << (n_usize + 1)) - BigInt::from(1);
let base_coeff =
&n_fact * Ratio::from_integer(two_pow) * Ratio::from_integer(BigInt::from(sign));
let nid = arena.intern_num(base_coeff);
let base_coeff_id = arena.intern(ExprNode::Num(nid));
let base = arena.mul(&[base_coeff_id, zeta_np1]);
let mut shift = Ratio::<BigInt>::zero();
for k in 0..m {
let denom = BigInt::from(2 * k + 1).pow((n_usize + 1) as u32);
let numer = BigInt::from(1) << (n_usize + 1);
shift += Ratio::new(numer, denom);
}
let shift = shift * &n_fact * Ratio::from_integer(BigInt::from(-sign));
let nid = arena.intern_num(shift);
let shift_id = arena.intern(ExprNode::Num(nid));
return Some(arena.add(&[base, shift_id]));
}
None
}
pub(crate) fn eval_kronecker_delta(arena: &mut Arena, i: ExprId, j: ExprId) -> Option<ExprId> {
if i == arena.nan || j == arena.nan {
return Some(arena.nan);
}
if i == j {
return Some(arena.one);
}
let d = arena.sub(i, j);
let r = arena.as_num(d)?;
if r.is_zero() {
Some(arena.one)
} else {
Some(arena.zero)
}
}
const LOG10_PHI: f64 = 0.208_987_640_249_978_73;
fn nonneg_int_arg(arena: &Arena, id: ExprId) -> Option<u64> {
let r = arena.as_num(id)?;
if !r.is_integer() || r.is_negative() {
return None;
}
r.to_integer().try_into().ok()
}
fn guarded_int(arena: &mut Arena, n: BigInt) -> Option<ExprId> {
guarded_num(arena, Ratio::from_integer(n))
}
fn eval_factorial2(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let r = arena.as_num(inner)?;
if !r.is_integer() {
return None;
}
let n: i64 = r.to_integer().try_into().ok()?;
if n < -1 && n % 2 != 0 {
let m = (-n - 1) / 2;
if beyond_digit_guard(
arena,
(m as f64) * std::f64::consts::LOG10_2 + log10_factorial_lower(m as f64),
) {
return None;
}
let mut odd = BigInt::one();
let mut k = 2 * m - 1;
while k > 1 {
odd *= BigInt::from(k);
k -= 2;
}
let sign = if m % 2 == 0 {
BigInt::one()
} else {
-BigInt::one()
};
return guarded_num(arena, Ratio::new(sign, odd));
}
if n < -1 {
return None;
}
let m = (n.max(0) / 2) as f64;
if beyond_digit_guard(
arena,
m * std::f64::consts::LOG10_2 + log10_factorial_lower(m),
) {
return None;
}
let mut result = BigInt::from(1);
let mut k = n;
while k > 1 {
result *= BigInt::from(k);
k -= 2;
}
guarded_int(arena, result)
}
fn eval_subfactorial(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let n = nonneg_int_arg(arena, inner)?;
if n >= 4 && beyond_digit_guard(arena, log10_factorial_lower(n as f64) - 3f64.log10()) {
return None;
}
let result = if n == 0 {
BigInt::from(1)
} else {
let mut prev = BigInt::from(1); let mut curr = BigInt::from(0); for i in 2..=n {
let next = BigInt::from(i - 1) * (&curr + &prev);
prev = curr;
curr = next;
}
curr
};
guarded_int(arena, result)
}
fn eval_rising_factorial(arena: &mut Arena, x_id: ExprId, n_id: ExprId) -> Option<ExprId> {
let xr = arena.as_num(x_id)?.clone();
let nr = arena.as_num(n_id)?;
if !nr.is_integer() {
return None;
}
if nr.is_negative() {
let m: u64 = (-nr.to_integer()).try_into().ok()?;
let below = xr - Ratio::one();
return reciprocal_factorial_power(arena, &below, m, -1);
}
let n: u64 = nr.to_integer().try_into().ok()?;
exact_factorial_power(arena, &xr, n, 1)
}
fn reciprocal_factorial_power(arena: &mut Arena, x: &Q, m: u64, step: i32) -> Option<ExprId> {
let p = exact_factorial_power(arena, x, m, step)?;
let v = arena.as_num(p)?.clone();
if v.is_zero() {
return Some(arena.complex_infinity);
}
guarded_num(arena, v.recip())
}
fn eval_falling_factorial(arena: &mut Arena, x_id: ExprId, n_id: ExprId) -> Option<ExprId> {
let xr = arena.as_num(x_id)?.clone();
let nr = arena.as_num(n_id)?;
if !nr.is_integer() {
return None;
}
if nr.is_negative() {
let m: u64 = (-nr.to_integer()).try_into().ok()?;
let above = xr + Ratio::one();
return reciprocal_factorial_power(arena, &above, m, 1);
}
let n: u64 = nr.to_integer().try_into().ok()?;
exact_factorial_power(arena, &xr, n, -1)
}
fn int_arg(arena: &Arena, id: ExprId) -> Option<i64> {
let r = arena.as_num(id)?;
if !r.is_integer() {
return None;
}
r.to_integer().try_into().ok()
}
fn eval_fibonacci(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let n = int_arg(arena, inner)?;
if beyond_digit_guard(arena, (n.unsigned_abs() as f64 - 2.0) * LOG10_PHI) {
return None;
}
guarded_int(arena, crate::base::combinatorics::fibonacci(i128::from(n)))
}
fn eval_lucas(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let n = int_arg(arena, inner)?;
if beyond_digit_guard(arena, (n.unsigned_abs() as f64 - 1.0) * LOG10_PHI) {
return None;
}
guarded_int(arena, crate::base::combinatorics::lucas(i128::from(n)))
}
pub(crate) fn exact_bernoulli(arena: &Arena, n: u64) -> Option<Q> {
match n {
0 => return Some(Ratio::one()),
1 => return Some(Ratio::new(BigInt::from(-1), BigInt::from(2))),
_ if n % 2 == 1 => return Some(Ratio::zero()),
_ => {}
}
let nf = n as f64;
let lower = std::f64::consts::LOG10_2 + log10_factorial_lower(nf)
- nf * (2.0 * std::f64::consts::PI).log10();
if beyond_digit_guard(arena, lower) {
return None;
}
let m = usize::try_from(n / 2).ok()?;
let r = crate::transforms::evalf::bernoulli::even(m);
let digits = r.numer().to_string().len() + r.denom().to_string().len();
(digits <= arena.config.max_result_digits).then_some(r)
}
fn eval_bernoulli(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let n = nonneg_int_arg(arena, inner)?;
let b = exact_bernoulli(arena, n)?;
guarded_num(arena, b)
}
fn harmonic_digits_lower(n: u64) -> f64 {
if n < 82 {
return 0.0;
}
let x = n as f64;
let h = x / 2.0;
let theta_diff = x * (1.0 - 1.0 / x.ln()) - h * (1.0 + 1.0 / (2.0 * h.ln()));
(theta_diff / std::f64::consts::LN_10).max(0.0)
}
fn eval_harmonic(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if arena
.as_num(inner)
.is_some_and(|r| r.is_integer() && r.is_negative())
{
return Some(arena.complex_infinity);
}
let n = nonneg_int_arg(arena, inner)?;
if beyond_digit_guard(arena, harmonic_digits_lower(n)) {
return None;
}
let mut lcm = BigInt::one();
for k in 1..=n {
let kb = BigInt::from(k);
let g = num_integer::Integer::gcd(&(&lcm % &kb), &kb);
lcm *= kb / g;
}
let mut numer = BigInt::zero();
for k in 1..=n {
numer += &lcm / BigInt::from(k);
}
guarded_num(arena, Ratio::new(numer, lcm))
}
fn eval_catalan(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(r) = arena.as_num(inner)
&& r.is_negative()
{
if r.is_integer() {
return Some(if r.to_integer() == BigInt::from(-1) {
arena.rational(-1, 2)
} else {
arena.zero
});
}
if *r.denom() == BigInt::from(2) {
return Some(arena.complex_infinity);
}
return None;
}
let n = nonneg_int_arg(arena, inner)?;
let nf = n as f64;
if beyond_digit_guard(
arena,
nf * 4f64.log10() - ((2.0 * nf + 1.0) * (nf + 1.0)).log10(),
) {
return None;
}
let mut binom = BigInt::from(1); for i in 0..n {
binom *= BigInt::from(2 * n - i);
binom /= BigInt::from(i + 1);
}
guarded_num(arena, Ratio::new(binom, BigInt::from(n + 1)))
}
const MAX_STIRLING_WORK: f64 = 2.0e8;
fn stirling_args(
arena: &Arena,
n_id: ExprId,
k_id: ExprId,
first_kind: bool,
) -> Option<(u64, u64)> {
let n = nonneg_int_arg(arena, n_id)?;
let k = nonneg_int_arg(arena, k_id)?;
if k == 0 || k > n {
return Some((n, k));
}
let (nf, kf) = (n as f64, k as f64);
let mut lower = (nf - kf) * kf.log10();
if first_kind && k == 1 {
lower = log10_factorial_lower(nf - 1.0);
}
if beyond_digit_guard(arena, lower) {
return None;
}
let closed_form = k + 1 >= n || k == 1 || (!first_kind && k == 2);
if !closed_form && nf * kf * (1.0 + lower / 19.0) > MAX_STIRLING_WORK {
return None;
}
Some((n, k))
}
fn eval_stirling2(arena: &mut Arena, n_id: ExprId, k_id: ExprId) -> Option<ExprId> {
let (n, k) = stirling_args(arena, n_id, k_id, false)?;
let result = crate::domains::combinatorics::stirling2(n, k)?;
guarded_int(arena, result)
}
fn eval_stirling1(arena: &mut Arena, n_id: ExprId, k_id: ExprId) -> Option<ExprId> {
let (n, k) = stirling_args(arena, n_id, k_id, true)?;
let result = crate::domains::combinatorics::stirling1(n, k)?;
guarded_int(arena, result)
}
const MAX_PARTITION_N: u64 = 40_000;
fn eval_partition_count(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let n = nonneg_int_arg(arena, inner)?;
if n > MAX_PARTITION_N {
return None;
}
let result = crate::domains::combinatorics::partition_count(n)?;
guarded_int(arena, result)
}
fn eval_bell(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
let n = nonneg_int_arg(arena, inner)?;
if n >= 3 {
let nf = n as f64;
let k = (nf / nf.ln()).round().clamp(1.0, nf);
if beyond_digit_guard(arena, (nf - k) * k.log10()) {
return None;
}
}
if n == 0 {
let nid = arena.intern_num(Ratio::from_integer(BigInt::from(1)));
return Some(arena.intern(ExprNode::Num(nid)));
}
let mut row = vec![BigInt::from(1)]; for _ in 1..n {
let mut new_row = Vec::with_capacity(row.len() + 1);
new_row.push(row.last()?.clone()); for j in 1..=row.len() {
let val = &new_row[j - 1] + &row[j - 1];
new_row.push(val);
}
row = new_row;
}
let result = row.pop()?;
guarded_int(arena, result)
}
fn eval_euler_number(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if arena
.as_num(inner)
.is_some_and(|r| r.is_integer() && r.to_integer() == BigInt::from(-1))
{
let half = arena.rational(1, 2);
let pi = arena.pi;
return Some(arena.mul(&[half, pi]));
}
let n = nonneg_int_arg(arena, inner)?;
if n % 2 == 1 {
let nid = arena.intern_num(Ratio::from_integer(BigInt::from(0)));
return Some(arena.intern(ExprNode::Num(nid)));
}
let nf = n as f64;
let lower = log10_factorial_lower(nf) + (nf + 2.0) * std::f64::consts::LOG10_2
- (nf + 1.0) * std::f64::consts::PI.log10()
+ (2.0f64 / 3.0).log10();
if n >= 2 && beyond_digit_guard(arena, lower) {
return None;
}
let result = crate::base::combinatorics::euler_even(usize::try_from(n / 2).ok()?);
guarded_int(arena, result)
}
fn is_pi(arena: &Arena, id: ExprId) -> bool {
id == arena.pi
}
fn is_e(arena: &Arena, id: ExprId) -> bool {
id == arena.e_const
}
fn as_pi_multiple(arena: &Arena, id: ExprId) -> Option<Q> {
if is_pi(arena, id) {
return Some(Ratio::one());
}
if let ExprNode::Mul(ref children) = arena.node(id).clone()
&& children.len() == 2
&& let ExprNode::Num(nid) = arena.node(children[0])
{
let nid = *nid;
if children[1] == arena.pi {
return Some(arena.num(nid).clone());
}
}
if let Some(r) = arena.as_num(id)
&& r.is_zero()
{
return Some(Ratio::zero());
}
None
}
fn eval_sin(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(pos_inner) = as_negated(arena, inner) {
let sin_pos = eval_sin(arena, pos_inner).unwrap_or_else(|| arena.sin(pos_inner));
return Some(arena.neg(sin_pos));
}
if let Some(real_part) = as_pure_imaginary(arena, inner) {
let sinh_val = arena.sinh(real_part);
return Some(arena.mul(&[arena.i_unit, sinh_val]));
}
let coeff = as_pi_multiple(arena, inner)?;
if !denominator_divides(&coeff, 12) {
return None;
}
let two: Q = Ratio::from_integer(2.into());
let coeff = mod_positive(&coeff, &two);
if coeff.is_zero() {
return Some(arena.zero);
}
if coeff == Ratio::new(1.into(), 2.into()) {
return Some(arena.one);
}
if coeff == Ratio::one() {
return Some(arena.zero);
}
if coeff == Ratio::new(3.into(), 2.into()) {
return Some(arena.neg_one);
}
if coeff == Ratio::new(1.into(), 6.into()) {
let half = arena.rational(1, 2);
return Some(half);
}
if coeff == Ratio::new(5.into(), 6.into()) {
let half = arena.rational(1, 2);
return Some(half);
}
if coeff == Ratio::new(7.into(), 6.into()) {
let neg_half = arena.rational(-1, 2);
return Some(neg_half);
}
if coeff == Ratio::new(11.into(), 6.into()) {
let neg_half = arena.rational(-1, 2);
return Some(neg_half);
}
if coeff == Ratio::new(1.into(), 4.into()) {
let two = arena.int(2);
let half_exp = arena.rational(1, 2);
let sqrt2 = arena.pow(two, half_exp);
let half = arena.rational(1, 2);
return Some(arena.mul(&[half, sqrt2]));
}
if coeff == Ratio::new(1.into(), 3.into()) {
let three = arena.int(3);
let half_exp = arena.rational(1, 2);
let sqrt3 = arena.pow(three, half_exp);
let half = arena.rational(1, 2);
return Some(arena.mul(&[half, sqrt3]));
}
let half = Ratio::new(1.into(), 2.into());
if coeff > half && coeff < Ratio::one() {
let reflected = Ratio::one() - &coeff;
let nid = arena.intern_num(reflected);
let coeff_id = arena.intern(ExprNode::Num(nid));
let reflected_id = arena.mul(&[coeff_id, arena.pi]);
return eval_sin(arena, reflected_id);
}
let three_half = Ratio::new(3.into(), 2.into());
if coeff > Ratio::one() && coeff < three_half {
let reduced = &coeff - Ratio::one();
let nid = arena.intern_num(reduced);
let coeff_id = arena.intern(ExprNode::Num(nid));
let reduced_id = arena.mul(&[coeff_id, arena.pi]);
if let Some(val) = eval_sin(arena, reduced_id) {
return Some(arena.neg(val));
}
}
let two: Q = Ratio::from_integer(2.into());
if coeff > three_half && coeff < two {
let reduced = two - &coeff;
let nid = arena.intern_num(reduced);
let coeff_id = arena.intern(ExprNode::Num(nid));
let reduced_id = arena.mul(&[coeff_id, arena.pi]);
if let Some(val) = eval_sin(arena, reduced_id) {
return Some(arena.neg(val));
}
}
None
}
fn eval_cos(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(pos_inner) = as_negated(arena, inner) {
return Some(eval_cos(arena, pos_inner).unwrap_or_else(|| arena.cos(pos_inner)));
}
if let Some(real_part) = as_pure_imaginary(arena, inner) {
return Some(arena.cosh(real_part));
}
let coeff = as_pi_multiple(arena, inner)?;
if !denominator_divides(&coeff, 12) {
return None;
}
let two: Q = Ratio::from_integer(2.into());
let coeff = mod_positive(&coeff, &two);
if coeff.is_zero() {
return Some(arena.one);
}
if coeff == Ratio::new(1.into(), 2.into()) {
return Some(arena.zero);
}
if coeff == Ratio::one() {
return Some(arena.neg_one);
}
if coeff == Ratio::new(3.into(), 2.into()) {
return Some(arena.zero);
}
if coeff == Ratio::new(1.into(), 3.into()) {
let half = arena.rational(1, 2);
return Some(half);
}
if coeff == Ratio::new(2.into(), 3.into()) {
let neg_half = arena.rational(-1, 2);
return Some(neg_half);
}
if coeff == Ratio::new(4.into(), 3.into()) {
let neg_half = arena.rational(-1, 2);
return Some(neg_half);
}
if coeff == Ratio::new(5.into(), 3.into()) {
let half = arena.rational(1, 2);
return Some(half);
}
if coeff == Ratio::new(1.into(), 4.into()) {
let two = arena.int(2);
let half_exp = arena.rational(1, 2);
let sqrt2 = arena.pow(two, half_exp);
let half = arena.rational(1, 2);
return Some(arena.mul(&[half, sqrt2]));
}
if coeff == Ratio::new(1.into(), 6.into()) {
let three = arena.int(3);
let half_exp = arena.rational(1, 2);
let sqrt3 = arena.pow(three, half_exp);
let half = arena.rational(1, 2);
return Some(arena.mul(&[half, sqrt3]));
}
let half = Ratio::new(1.into(), 2.into());
if coeff > half && coeff < Ratio::one() {
let reflected = Ratio::one() - &coeff;
let nid = arena.intern_num(reflected);
let coeff_id = arena.intern(ExprNode::Num(nid));
let reflected_id = arena.mul(&[coeff_id, arena.pi]);
if let Some(val) = eval_cos(arena, reflected_id) {
return Some(arena.neg(val));
}
}
let three_half = Ratio::new(3.into(), 2.into());
if coeff > Ratio::one() && coeff < three_half {
let reduced = &coeff - Ratio::one();
let nid = arena.intern_num(reduced);
let coeff_id = arena.intern(ExprNode::Num(nid));
let reduced_id = arena.mul(&[coeff_id, arena.pi]);
if let Some(val) = eval_cos(arena, reduced_id) {
return Some(arena.neg(val));
}
}
let two: Q = Ratio::from_integer(2.into());
if coeff > three_half && coeff < two {
let reduced = two - &coeff;
let nid = arena.intern_num(reduced);
let coeff_id = arena.intern(ExprNode::Num(nid));
let reduced_id = arena.mul(&[coeff_id, arena.pi]);
return eval_cos(arena, reduced_id);
}
None
}
fn eval_tan(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(pos_inner) = as_negated(arena, inner) {
let tan_pos = eval_tan(arena, pos_inner).unwrap_or_else(|| arena.tan(pos_inner));
return Some(arena.neg(tan_pos));
}
let coeff = as_pi_multiple(arena, inner)?;
if !denominator_divides(&coeff, 12) {
return None;
}
let one_ratio: Q = Ratio::one();
let coeff = mod_positive(&coeff, &one_ratio);
if coeff.is_zero() {
return Some(arena.zero);
}
if coeff == Ratio::new(1.into(), 2.into()) {
return Some(arena.complex_infinity);
}
if coeff == Ratio::new(1.into(), 4.into()) {
return Some(arena.one);
}
if coeff == Ratio::new(3.into(), 4.into()) {
return Some(arena.neg_one);
}
if coeff == Ratio::new(1.into(), 6.into()) {
let three = arena.int(3);
let half_exp = arena.rational(1, 2);
let sqrt3 = arena.pow(three, half_exp);
let third = arena.rational(1, 3);
return Some(arena.mul(&[third, sqrt3]));
}
if coeff == Ratio::new(1.into(), 3.into()) {
let three = arena.int(3);
let half_exp = arena.rational(1, 2);
return Some(arena.pow(three, half_exp));
}
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
if coeff > half && coeff < Ratio::one() {
let reflected = Ratio::one() - &coeff;
let nid = arena.intern_num(reflected);
let coeff_id = arena.intern(ExprNode::Num(nid));
let reflected_angle = arena.mul(&[coeff_id, arena.pi()]);
if let Some(val) = eval_tan(arena, reflected_angle) {
return Some(arena.neg(val));
}
}
None
}
fn eval_exp(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if inner == arena.zero {
return Some(arena.one);
}
if inner == arena.one {
return Some(arena.e_const);
}
if let ExprNode::Ln(w) = *arena.node(inner)
&& (!walk::free_symbols(arena, w).is_empty() || constant_is_nonzero(arena, w))
{
return Some(w);
}
if let Some(pi_coeff) = as_imaginary_pi_multiple(arena, inner) {
let coeff_id = {
let nid = arena.intern_num(pi_coeff.clone());
arena.intern(ExprNode::Num(nid))
};
let angle = arena.mul(&[coeff_id, arena.pi]);
let cos_val = eval_cos(arena, angle);
let sin_val = eval_sin(arena, angle);
if let (Some(c), Some(s)) = (cos_val, sin_val) {
if s == arena.zero {
return Some(c); }
if c == arena.zero {
return Some(arena.mul(&[arena.i_unit, s]));
}
let i_sin = arena.mul(&[arena.i_unit, s]);
return Some(arena.add(&[c, i_sin]));
}
}
if let ExprNode::Add(terms) = arena.node(inner).clone() {
for (k, &t) in terms.iter().enumerate() {
if let Some(pi_coeff) = as_imaginary_pi_multiple(arena, t)
&& denominator_divides(&pi_coeff, 2)
&& let Some(unit) = eval_exp(arena, t)
{
let rest: smallvec::SmallVec<[ExprId; 6]> = terms
.iter()
.enumerate()
.filter(|&(j, _)| j != k)
.map(|(_, &c)| c)
.collect();
let rest = arena.add(&rest);
let rest_exp = eval_exp(arena, rest).unwrap_or_else(|| arena.exp(rest));
return Some(arena.mul(&[unit, rest_exp]));
}
}
}
None
}
fn constant_is_nonzero(arena: &Arena, w: ExprId) -> bool {
use crate::base::assumptions::{AssumptionCache, Props};
if AssumptionCache::new().query(arena, w, Props::NONZERO) == Some(true) {
return true;
}
matches!(
crate::transforms::evalf::evalf_settled(
arena,
w,
16,
crate::transforms::evalf::ZeroSearch::Cap
),
Ok((ref z, crate::transforms::evalf::Settled::Certified))
if !(z.0.is_zero() && z.1.is_zero())
)
}
fn as_imaginary_pi_multiple(arena: &mut Arena, id: ExprId) -> Option<Q> {
let children = match arena.node(id).clone() {
ExprNode::Mul(children) => children,
_ => return None,
};
let mut has_i = false;
let mut remaining: Vec<ExprId> = Vec::new();
for &child in &children {
if child == arena.i_unit {
if has_i {
return None;
} has_i = true;
} else {
remaining.push(child);
}
}
if !has_i {
return None;
}
let product = if remaining.is_empty() {
return None;
} else if remaining.len() == 1 {
remaining[0]
} else {
arena.mul(&remaining)
};
as_pi_multiple(arena, product)
}
fn eval_ln(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if inner == arena.one {
return Some(arena.zero);
}
if is_e(arena, inner) {
return Some(arena.one);
}
if inner == arena.neg_one {
let i_pi = arena.mul(&[arena.i_unit, arena.pi]);
return Some(i_pi);
}
if let Some(r) = arena.as_num(inner)
&& r.is_negative()
{
let abs_r = -r.clone();
if abs_r.is_one() {
} else {
let abs_id = {
let nid = arena.intern_num(abs_r);
arena.intern(ExprNode::Num(nid))
};
let ln_abs = arena.ln(abs_id);
let i_pi = arena.mul(&[arena.i_unit, arena.pi]);
return Some(arena.add(&[ln_abs, i_pi]));
}
}
ln_of_special_complex(arena, inner)
}
fn ln_of_special_complex(arena: &mut Arena, z: ExprId) -> Option<ExprId> {
if crate::base::complex::is_real_node(arena, z)
|| !crate::base::walk::free_symbols(arena, z).is_empty()
{
return None;
}
let parts = crate::base::complex::decompose(arena, z);
if !parts.exact || arena.is_zero_structural(parts.im) {
return None;
}
let (re, im) = (eval(arena, parts.re), eval(arena, parts.im));
let tower =
|arena: &Arena, v: ExprId| arena.as_num(v).is_some() || is_square_root_tower(arena, v);
if !tower(arena, re) || !tower(arena, im) {
return None;
}
let im_sign = crate::poly::algebraic::sign_checked(arena, im)?;
let (re_sign, q) = if arena.is_zero_structural(re) {
(0, Ratio::new(BigInt::from(1), BigInt::from(2)))
} else {
let re_sign = crate::poly::algebraic::sign_checked(arena, re)?;
let ratio = arena.div(im, re);
let ratio = eval(arena, ratio);
let q = match arena.as_num(ratio) {
Some(r) if r.abs().is_one() => Ratio::new(BigInt::from(1), BigInt::from(4)),
Some(_) => return None,
None => special_angle_of(arena, ratio, SpecialTable::Tan)?.1,
};
(re_sign, q)
};
if im_sign == 0 || (re_sign != 0 && q.is_zero()) {
return None;
}
let pi_q = if re_sign >= 0 { q } else { Ratio::one() - q };
let arg = if im_sign > 0 { pi_q } else { -pi_q };
let i_arg = {
let t = pi_times(arena, arg);
arena.mul(&[arena.i_unit, t])
};
let two = arena.int(2);
let (re2, im2) = (arena.pow(re, two), arena.pow(im, two));
let m2 = arena.add(&[re2, im2]);
let half = arena.rational(1, 2);
let modulus = arena.pow(m2, half);
let modulus = eval(arena, modulus);
let ln_abs = arena.ln(modulus);
let out = arena.add(&[ln_abs, i_arg]);
Some(eval(arena, out))
}
fn exp_pow_merges(arena: &Arena, f: ExprId, g: ExprId) -> bool {
if arena.as_num(g).is_some_and(|r| r.is_integer()) {
return true;
}
let mut cache = crate::base::assumptions::AssumptionCache::new();
cache.query(arena, g, crate::base::assumptions::Props::INTEGER) == Some(true)
|| cache.query(arena, f, crate::base::assumptions::Props::REAL) == Some(true)
}
fn eval_pow_root(arena: &mut Arena, base: ExprId, exp: ExprId) -> Option<ExprId> {
let base_r = arena.as_num(base)?.clone();
let exp_r = arena.as_num(exp)?;
if !exp_r.numer().is_one() {
return None;
}
let n: u32 = exp_r.denom().to_u32()?;
if n < 2 {
return None;
}
if base_r.is_negative() && base_r != -Ratio::from_integer(BigInt::from(1)) {
let abs_r = -base_r;
let abs_root = positive_root(arena, &abs_r, exp, n).unwrap_or_else(|| {
let abs_id = arena.num_ratio(abs_r);
arena.pow(abs_id, exp)
});
let neg_one_root = arena.pow(arena.neg_one, exp);
return Some(arena.mul(&[abs_root, neg_one_root]));
}
positive_root(arena, &base_r, exp, n)
}
fn positive_root(arena: &mut Arena, r: &Q, exp: ExprId, n: u32) -> Option<ExprId> {
use crate::base::canon::split_perfect_power;
if r.is_zero() {
return Some(arena.zero);
}
if r.is_one() {
return Some(arena.one);
}
if r.is_negative() {
return None;
}
let (kn, mn) = split_perfect_power(r.numer(), n);
if r.is_integer() {
if mn.is_one() {
return Some(arena.big_int(kn)); }
if kn.is_one() {
return None;
}
let k_id = arena.big_int(kn);
let m_id = arena.big_int(mn);
let root = arena.pow(m_id, exp);
return Some(arena.mul(&[k_id, root]));
}
if !mn.is_one() {
return None;
}
let (kd, md) = split_perfect_power(r.denom(), n);
if !md.is_one() {
return None;
}
Some(arena.num_ratio(Ratio::new(kn, kd)))
}
fn eval_abs(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if inner == arena.i_unit {
return Some(arena.one);
}
if let Some(r) = arena.as_num(inner) {
let r = r.clone();
if r.is_negative() {
let pos = -r;
let nid = arena.intern_num(pos);
return Some(arena.intern(ExprNode::Num(nid)));
} else {
return Some(inner);
}
}
if let ExprNode::Exp(z) = *arena.node(inner)
&& crate::base::walk::free_symbols(arena, z).is_empty()
{
let parts = crate::base::complex::decompose(arena, z);
if parts.exact {
let re = eval(arena, parts.re);
let e = arena.exp(re);
return Some(eval(arena, e));
}
}
if !crate::base::complex::is_real_node(arena, inner)
&& crate::base::walk::free_symbols(arena, inner).is_empty()
{
let parts = crate::base::complex::decompose(arena, inner);
if parts.exact
&& parts.im != arena.zero
&& is_arithmetic_constant(arena, parts.re)
&& is_arithmetic_constant(arena, parts.im)
{
tracing::trace!("eval_abs: folding modulus of a numeric complex constant");
let two = arena.int(2);
let re2 = arena.pow(parts.re, two);
let im2 = arena.pow(parts.im, two);
let sum = arena.add(&[re2, im2]);
let sum = eval(arena, sum);
let half = arena.rational(1, 2);
return Some(arena.pow(sum, half));
}
}
let (coeff, term) = arena.as_coeff_term(inner);
if term != inner && !coeff.is_zero() && !coeff.is_one() {
let t = eval_abs(arena, term)?;
return Some(arena.make_coeff_term(coeff.abs(), t));
}
None
}
fn is_arithmetic_constant(arena: &Arena, id: ExprId) -> bool {
crate::base::walk::post_order_ids(arena, id)
.iter()
.all(|&n| {
matches!(
arena.node(n),
ExprNode::Num(_)
| ExprNode::Pi
| ExprNode::E
| ExprNode::EulerGamma
| ExprNode::Catalan
| ExprNode::GoldenRatio
| ExprNode::Add(_)
| ExprNode::Mul(_)
| ExprNode::Neg(_)
| ExprNode::Pow(_, _)
)
})
}
fn eval_sign(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(r) = arena.as_num(inner) {
use num_traits::{Signed, Zero};
if r.is_positive() {
return Some(arena.one);
}
if r.is_negative() {
return Some(arena.neg_one);
}
if r.is_zero() {
return Some(arena.zero);
}
}
None
}
fn eval_asin(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if inner == arena.zero {
return Some(arena.zero);
} if inner == arena.one {
let half = arena.rational(1, 2);
return Some(arena.mul(&[half, arena.pi]));
}
if inner == arena.neg_one {
let neg_half = arena.rational(-1, 2);
return Some(arena.mul(&[neg_half, arena.pi]));
}
if let Some(r) = arena.as_num(inner) {
let half = Ratio::new(1.into(), 2.into());
if *r == half {
let sixth = arena.rational(1, 6);
return Some(arena.mul(&[sixth, arena.pi]));
}
let neg_half = Ratio::new((-1).into(), 2.into());
if *r == neg_half {
let neg_sixth = arena.rational(-1, 6);
return Some(arena.mul(&[neg_sixth, arena.pi]));
}
}
let (s, q) = special_angle_of(arena, inner, SpecialTable::Cos)?;
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
Some(pi_times(arena, (half - q) * s))
}
fn eval_acos(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if inner == arena.one {
return Some(arena.zero);
} if inner == arena.zero {
let half = arena.rational(1, 2);
return Some(arena.mul(&[half, arena.pi]));
}
if inner == arena.neg_one {
return Some(arena.pi);
} if let Some(r) = arena.as_num(inner) {
let half = Ratio::new(1.into(), 2.into());
if *r == half {
let third = arena.rational(1, 3);
return Some(arena.mul(&[third, arena.pi]));
}
let neg_half = Ratio::new((-1).into(), 2.into());
if *r == neg_half {
let two_thirds = arena.rational(2, 3);
return Some(arena.mul(&[two_thirds, arena.pi]));
}
}
let (s, q) = special_angle_of(arena, inner, SpecialTable::Cos)?;
let angle = if s.is_negative() { Ratio::one() - q } else { q };
Some(pi_times(arena, angle))
}
fn eval_atan(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if inner == arena.zero {
return Some(arena.zero);
} if let Some(inf) = crate::base::canon::infinite_argument(arena, inner) {
let positive = if inf.imaginary {
match inf.side? {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Equal => !inf.negative,
}
} else {
!inf.negative
};
let half = arena.rational(if positive { 1 } else { -1 }, 2);
return Some(arena.mul(&[half, arena.pi]));
}
if inner == arena.one {
let quarter = arena.rational(1, 4);
return Some(arena.mul(&[quarter, arena.pi]));
}
if inner == arena.neg_one {
let neg_quarter = arena.rational(-1, 4);
return Some(arena.mul(&[neg_quarter, arena.pi]));
}
let (s, q) = special_angle_of(arena, inner, SpecialTable::Tan)?;
Some(pi_times(arena, q * s))
}
fn pi_times(arena: &mut Arena, r: Q) -> ExprId {
if r.is_zero() {
return arena.zero;
}
let c = arena.num_ratio(r);
arena.mul(&[c, arena.pi])
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum SpecialTable {
Cos,
Tan,
}
struct SpecialValue {
k: i64,
n: i64,
a: i64,
b: i64,
m: i64,
d: i64,
root: bool,
}
const fn sv(k: i64, n: i64, a: i64, b: i64, m: i64, d: i64, root: bool) -> SpecialValue {
SpecialValue {
k,
n,
a,
b,
m,
d,
root,
}
}
const SPECIAL_COS: &[SpecialValue] = &[
sv(0, 1, 1, 0, 0, 1, false), sv(1, 12, 2, 1, 3, 2, true), sv(1, 10, 10, 2, 5, 4, true), sv(1, 8, 2, 1, 2, 2, true), sv(1, 6, 0, 1, 3, 2, false), sv(1, 5, 1, 1, 5, 4, false), sv(1, 4, 0, 1, 2, 2, false), sv(3, 10, 10, -2, 5, 4, true), sv(1, 3, 1, 0, 0, 2, false), sv(3, 8, 2, -1, 2, 2, true), sv(2, 5, -1, 1, 5, 4, false), sv(5, 12, 2, -1, 3, 2, true), sv(1, 2, 0, 0, 0, 1, false), ];
const SPECIAL_TAN: &[SpecialValue] = &[
sv(0, 1, 0, 0, 0, 1, false), sv(1, 12, 2, -1, 3, 1, false), sv(1, 10, 25, -10, 5, 5, true), sv(1, 8, -1, 1, 2, 1, false), sv(1, 6, 0, 1, 3, 3, false), sv(1, 5, 5, -2, 5, 1, true), sv(1, 4, 1, 0, 0, 1, false), sv(3, 10, 25, 10, 5, 5, true), sv(1, 3, 0, 1, 3, 1, false), sv(3, 8, 1, 1, 2, 1, false), sv(2, 5, 5, 2, 5, 1, true), sv(5, 12, 2, 1, 3, 1, false), ];
const SPECIAL_LCM: i64 = 120;
fn special_value_node(arena: &mut Arena, e: &SpecialValue) -> ExprId {
let a = arena.int(e.a);
let mut inner = if e.b == 0 {
a
} else {
let m = arena.int(e.m);
let half = arena.rational(1, 2);
let sqrt_m = arena.pow(m, half);
let b = arena.int(e.b);
let b_sqrt_m = arena.mul(&[b, sqrt_m]);
arena.add(&[a, b_sqrt_m])
};
if e.root {
let half = arena.rational(1, 2);
inner = arena.pow(inner, half);
}
let inv_d = arena.rational(1, e.d);
let v = arena.mul(&[inv_d, inner]);
eval(arena, v)
}
fn is_square_root_tower(arena: &Arena, id: ExprId) -> bool {
let mut stack = vec![id];
let mut visited = 0usize;
let mut irrational = false;
while let Some(n) = stack.pop() {
visited += 1;
if visited > 64 {
return false;
}
match arena.node(n) {
ExprNode::Num(_) => {}
ExprNode::Add(cs) | ExprNode::Mul(cs) => stack.extend(cs.iter().copied()),
ExprNode::Neg(a) => stack.push(*a),
ExprNode::GoldenRatio => irrational = true,
ExprNode::Pow(b, e) => {
match arena.as_num(*e) {
Some(q) if q.is_integer() => {}
Some(q) if *q.denom() == BigInt::from(2) => irrational = true,
_ => return false,
}
stack.push(*b);
}
_ => return false,
}
}
irrational
}
fn monic_minimal_polynomial(arena: &mut Arena, id: ExprId) -> Option<crate::poly::Poly> {
let p = crate::poly::algebraic::minimal_polynomial(arena, id)?;
(!p.is_zero()).then(|| p.make_monic())
}
fn special_angle_of(arena: &mut Arena, v: ExprId, table: SpecialTable) -> Option<(Q, Q)> {
if !is_square_root_tower(arena, v) {
return None;
}
let x = crate::transforms::evalf::evalf_f64(arena, v).ok()?;
if !x.is_finite() || x.abs() < 1e-12 {
return None;
}
let theta = match table {
SpecialTable::Cos if x.abs() <= 1.0 => x.abs().acos(),
SpecialTable::Cos => return None,
SpecialTable::Tan => x.abs().atan(),
} / std::f64::consts::PI;
let steps = theta * SPECIAL_LCM as f64;
let r = steps.round();
if (steps - r).abs() > 1e-6 {
return None;
}
let q = Ratio::new(BigInt::from(r as i64), BigInt::from(SPECIAL_LCM));
let entries = match table {
SpecialTable::Cos => SPECIAL_COS,
SpecialTable::Tan => SPECIAL_TAN,
};
let entry = entries
.iter()
.find(|e| Ratio::new(BigInt::from(e.k), BigInt::from(e.n)) == q)?;
let abs_v = if x < 0.0 {
let neg = arena.neg(v);
eval(arena, neg)
} else {
v
};
let target = special_value_node(arena, entry);
if abs_v != target {
let p = monic_minimal_polynomial(arena, abs_v)?;
if monic_minimal_polynomial(arena, target)? != p {
return None;
}
}
let s: Q = if x < 0.0 { -Ratio::one() } else { Ratio::one() };
Some((s, q))
}
pub(crate) fn eval_atan2(arena: &mut Arena, y: ExprId, x: ExprId) -> Option<ExprId> {
if let Some(angle) = eval_atan2_infinite(arena, y, x) {
return Some(angle);
}
let y_val = arena.as_num(y).cloned();
let x_val = arena.as_num(x).cloned();
match (y_val, x_val) {
(Some(yv), Some(xv)) => {
let y_zero = yv.is_zero();
let x_zero = xv.is_zero();
let y_pos = !yv.is_negative() && !y_zero;
let x_pos = !xv.is_negative() && !x_zero;
let y_neg = yv.is_negative();
let x_neg = xv.is_negative();
if y_zero && x_zero {
Some(arena.nan)
} else if y_zero && x_pos {
Some(arena.zero)
} else if y_zero && x_neg {
Some(arena.pi)
} else if y_pos && x_zero {
let half = arena.rational(1, 2);
Some(arena.mul(&[half, arena.pi]))
} else if y_neg && x_zero {
let neg_half = arena.rational(-1, 2);
Some(arena.mul(&[neg_half, arena.pi]))
} else if x_pos {
let ratio = arena.div(y, x);
let atan_node = arena.atan(ratio);
Some(eval_atan(arena, ratio).unwrap_or(atan_node))
} else if x_neg && !y_neg {
let ratio = arena.div(y, x);
let atan_val = eval_atan(arena, ratio).unwrap_or_else(|| arena.atan(ratio));
Some(arena.add(&[atan_val, arena.pi]))
} else {
let ratio = arena.div(y, x);
let atan_val = eval_atan(arena, ratio).unwrap_or_else(|| arena.atan(ratio));
let neg_pi = arena.neg(arena.pi);
Some(arena.add(&[atan_val, neg_pi]))
}
}
_ => eval_atan2_special(arena, y, x),
}
}
fn extremum_of_real_constants(arena: &mut Arena, args: &[ExprId], min: bool) -> Option<ExprId> {
use crate::base::assumptions::{AssumptionCache, Props};
if args.len() < 2 || args.len() > 8 {
return None;
}
let mut cache = AssumptionCache::new();
for &a in args {
if cache.query(arena, a, Props::REAL) != Some(true)
|| !crate::base::walk::free_symbols(arena, a).is_empty()
{
return None;
}
}
let mut best = args[0];
for &a in &args[1..] {
let d = arena.sub(a, best);
let s = arena.sign_of_real_constant(d)?;
if (min && s == std::cmp::Ordering::Less) || (!min && s == std::cmp::Ordering::Greater) {
best = a;
}
}
Some(best)
}
fn eval_atan2_infinite(arena: &mut Arena, y: ExprId, x: ExprId) -> Option<ExprId> {
use crate::base::assumptions::{AssumptionCache, Props};
use std::cmp::Ordering;
let is_inf = |arena: &Arena, id: ExprId| id == arena.infinity || id == arena.neg_infinity;
if is_inf(arena, y) == is_inf(arena, x) {
return None;
}
let finite = if is_inf(arena, y) { x } else { y };
let mut cache = AssumptionCache::new();
if cache.query(arena, finite, Props::REAL) != Some(true) {
return None;
}
let sign = if let Some(q) = arena.as_num(finite) {
q.cmp(&Q::zero())
} else if cache.query(arena, finite, Props::POSITIVE) == Some(true) {
Ordering::Greater
} else if cache.query(arena, finite, Props::NEGATIVE) == Some(true) {
Ordering::Less
} else if cache.query(arena, finite, Props::ZERO) == Some(true) {
Ordering::Equal
} else {
arena.sign_of_real_constant(finite)?
};
let angle: Q = if is_inf(arena, x) {
if x == arena.infinity {
Q::zero()
} else if sign == Ordering::Less {
-Q::one()
} else {
Q::one()
}
} else {
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
if y == arena.infinity { half } else { -half }
};
Some(pi_times(arena, angle))
}
fn eval_atan2_special(arena: &mut Arena, y: ExprId, x: ExprId) -> Option<ExprId> {
let tower_or_rational =
|arena: &Arena, id: ExprId| arena.as_num(id).is_some() || is_square_root_tower(arena, id);
if !tower_or_rational(arena, y) || !tower_or_rational(arena, x) {
return None;
}
let sign_of = |arena: &Arena, id: ExprId| -> Option<i8> {
if let Some(r) = arena.as_num(id) {
return Some(if r.is_zero() {
0
} else if r.is_negative() {
-1
} else {
1
});
}
let v = crate::transforms::evalf::evalf_f64(arena, id).ok()?;
(v.is_finite() && v.abs() >= 1e-12).then_some(if v < 0.0 { -1 } else { 1 })
};
let sy = sign_of(arena, y)?;
let sx = sign_of(arena, x)?;
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
let angle: Q = match (sy, sx) {
(0, 0) => return None,
(0, 1) => Ratio::zero(),
(0, _) => Ratio::one(),
(1, 0) => half,
(_, 0) => -half,
_ => {
let ratio = arena.div(y, x);
let ratio = eval(arena, ratio);
let a = eval_atan(arena, ratio)?;
let q = as_pi_multiple(arena, a)?;
match (sy, sx) {
(_, 1) => q,
(1, _) => q + Ratio::one(),
_ => q - Ratio::one(),
}
}
};
Some(pi_times(arena, angle))
}
fn eval_sinh(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(pos_inner) = as_negated(arena, inner) {
let sinh_pos = eval_sinh(arena, pos_inner).unwrap_or_else(|| arena.sinh(pos_inner));
return Some(arena.neg(sinh_pos));
}
if inner == arena.zero {
return Some(arena.zero);
} None
}
fn eval_cosh(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(pos_inner) = as_negated(arena, inner) {
return Some(eval_cosh(arena, pos_inner).unwrap_or_else(|| arena.cosh(pos_inner)));
}
if inner == arena.zero {
return Some(arena.one);
} None
}
fn eval_tanh(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if let Some(pos_inner) = as_negated(arena, inner) {
let tanh_pos = eval_tanh(arena, pos_inner).unwrap_or_else(|| arena.tanh(pos_inner));
return Some(arena.neg(tanh_pos));
}
if inner == arena.zero {
return Some(arena.zero);
} None
}
fn eval_asinh(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if inner == arena.zero {
return Some(arena.zero);
} None
}
fn eval_acosh(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if inner == arena.one {
return Some(arena.zero);
} None
}
fn eval_atanh(arena: &mut Arena, inner: ExprId) -> Option<ExprId> {
if inner == arena.zero {
return Some(arena.zero);
} if let Some(inf) = crate::base::canon::infinite_argument(arena, inner) {
let positive = if inf.imaginary {
!inf.negative
} else {
match inf.side? {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Equal => inf.negative,
}
};
let half = arena.rational(if positive { 1 } else { -1 }, 2);
return Some(arena.mul(&[half, arena.i_unit, arena.pi]));
}
None
}
fn as_pure_imaginary(arena: &mut Arena, id: ExprId) -> Option<ExprId> {
if let ExprNode::Mul(ref children) = arena.node(id).clone() {
let mut has_i = false;
let mut others: Vec<ExprId> = Vec::new();
for &child in children {
if child == arena.i_unit {
if has_i {
return None;
}
has_i = true;
} else {
others.push(child);
}
}
if has_i {
return Some(match others.len() {
0 => arena.one,
1 => others[0],
_ => arena.mul(&others),
});
}
}
None
}
fn as_negated(arena: &mut Arena, id: ExprId) -> Option<ExprId> {
match arena.node(id).clone() {
ExprNode::Neg(inner) => Some(inner),
ExprNode::Mul(ref children) if children.len() >= 2 => {
if let ExprNode::Num(nid) = arena.node(children[0]) {
let r = arena.num(*nid);
if r.is_negative() {
let pos_coeff = -r.clone();
if pos_coeff.is_one() {
if children.len() == 2 {
return Some(children[1]);
} else {
let rest: smallvec::SmallVec<[crate::base::node::ExprId; 6]> =
children[1..].iter().copied().collect();
return Some(arena.mul(&rest));
}
}
}
}
None
}
_ => None,
}
}
fn mod_positive(a: &Q, m: &Q) -> Q {
use num_integer::Integer;
if m.is_integer() && m.is_positive() {
let r = a.numer().mod_floor(&(a.denom() * m.numer()));
return if r.is_zero() {
Q::zero()
} else {
Ratio::new_raw(r, a.denom().clone())
};
}
let mut result = a % m;
if result.is_negative() {
result += m;
}
result
}
fn denominator_divides(q: &Q, n: u32) -> bool {
q.denom()
.to_u32()
.is_some_and(|d| d != 0 && n.is_multiple_of(d))
}
#[derive(Clone, Debug)]
enum OrthoFamily {
Legendre,
ChebyshevT,
ChebyshevU,
Hermite,
Laguerre,
Gegenbauer(Q),
AssocLaguerre(Q),
}
fn q_int(n: i64) -> Q {
Ratio::from_integer(BigInt::from(n))
}
fn within_guard_bits(arena: &Arena, r: &Q) -> bool {
let bits = r.numer().bits() + r.denom().bits();
(bits as f64) * std::f64::consts::LOG10_2 <= arena.config.max_result_digits as f64 + 2.0
}
fn ortho_coeffs(arena: &Arena, family: &OrthoFamily, n: usize) -> Option<Vec<Q>> {
let mut c = vec![Q::zero(); n + 1];
let ni = n as i64;
let even_family = !matches!(
family,
OrthoFamily::Laguerre | OrthoFamily::AssocLaguerre(_)
);
if even_family {
let mut lead = match family {
OrthoFamily::Legendre => {
Ratio::new(
crate::base::combinatorics::binomial(2 * n as u64, n as u64),
BigInt::one() << n,
)
}
OrthoFamily::ChebyshevT => {
if n == 0 {
Q::one()
} else {
Ratio::from_integer(BigInt::one() << (n - 1))
}
}
OrthoFamily::ChebyshevU | OrthoFamily::Hermite => {
Ratio::from_integer(BigInt::one() << n)
}
OrthoFamily::Gegenbauer(a) => {
let mut acc = Q::one();
for j in 0..ni {
acc *= a + q_int(j);
acc /= q_int(j + 1);
acc *= q_int(2);
}
acc
}
OrthoFamily::Laguerre | OrthoFamily::AssocLaguerre(_) => return None,
};
if matches!(family, OrthoFamily::ChebyshevT) && n == 0 {
c[0] = Q::one();
return Some(c);
}
for k in 0..=(ni / 2) {
if !within_guard_bits(arena, &lead) {
return None;
}
c[(ni - 2 * k) as usize] = lead.clone();
if 2 * k + 2 > ni {
break;
}
let (m, m1) = (q_int(ni - 2 * k), q_int(ni - 2 * k - 1));
let kp1 = q_int(k + 1);
let den = match family {
OrthoFamily::Legendre => q_int(2) * &kp1 * q_int(2 * ni - 2 * k - 1),
OrthoFamily::ChebyshevT => q_int(4) * &kp1 * q_int(ni - k - 1),
OrthoFamily::ChebyshevU => q_int(4) * &kp1 * q_int(ni - k),
OrthoFamily::Hermite => q_int(4) * &kp1,
OrthoFamily::Gegenbauer(a) => q_int(4) * &kp1 * (q_int(ni - k - 1) + a),
OrthoFamily::Laguerre | OrthoFamily::AssocLaguerre(_) => return None,
};
if den.is_zero() {
return None;
}
lead = -(lead * m * m1) / den;
}
return Some(c);
}
let alpha = match family {
OrthoFamily::AssocLaguerre(a) => a.clone(),
_ => Q::zero(),
};
let mut term = Q::one();
for j in 0..ni {
term *= &alpha + q_int(j + 1);
term /= q_int(j + 1);
}
for k in 0..=ni {
if !within_guard_bits(arena, &term) {
return None;
}
c[k as usize] = term.clone();
if k == ni {
break;
}
let den = q_int(k + 1) * (&alpha + q_int(k + 1));
if den.is_zero() {
return None;
}
term = -(term * q_int(ni - k)) / den;
}
Some(c)
}
fn jacobi_shifted_coeffs(arena: &Arena, n: usize, a: &Q, b: &Q) -> Option<Vec<Q>> {
let ni = n as i64;
let mut d = Vec::with_capacity(n + 1);
let mut term = Q::one();
for j in 0..ni {
term *= a + q_int(j + 1);
term /= q_int(j + 1);
}
for l in 0..=ni {
if !within_guard_bits(arena, &term) {
return None;
}
d.push(term.clone());
if l == ni {
break;
}
let den = q_int(l + 1) * (a + q_int(l + 1));
if den.is_zero() {
return None;
}
term = term * (q_int(ni + 1 + l) + a + b) * q_int(ni - l) / den;
}
Some(d)
}
fn horner(arena: &Arena, c: &[Q], t: &Q) -> Option<Q> {
let limit_bits =
2.0 * arena.config.max_result_digits as f64 / std::f64::consts::LOG10_2 + 256.0;
let mut acc = Q::zero();
for ci in c.iter().rev() {
acc = acc * t + ci;
if (acc.numer().bits() + acc.denom().bits()) as f64 > limit_bits {
return None;
}
}
Some(acc)
}
fn poly_from_coeffs(arena: &mut Arena, c: &[Q], x: ExprId) -> Option<ExprId> {
let mut terms = Vec::with_capacity(c.len());
for (i, ci) in c.iter().enumerate() {
if ci.is_zero() {
continue;
}
let coeff = guarded_num(arena, ci.clone())?;
let term = match i {
0 => coeff,
1 => arena.mul(&[coeff, x]),
_ => {
let e = arena.int(i as i64);
let p = arena.pow(x, e);
arena.mul(&[coeff, p])
}
};
terms.push(term);
}
let sum = match terms.len() {
0 => arena.zero,
1 => terms[0],
_ => arena.add(&terms),
};
if matches!(arena.node(x), ExprNode::Symbol(_)) {
Some(sum)
} else {
Some(expand_eval(arena, sum))
}
}
fn ortho_value(arena: &mut Arena, family: &OrthoFamily, n: usize, x: ExprId) -> Option<ExprId> {
let c = ortho_coeffs(arena, family, n)?;
if let Some(xr) = arena.as_num(x).cloned() {
let v = horner(arena, &c, &xr)?;
return guarded_num(arena, v);
}
poly_from_coeffs(arena, &c, x)
}
fn jacobi_value(arena: &mut Arena, n: usize, a: &Q, b: &Q, x: ExprId) -> Option<ExprId> {
let d = jacobi_shifted_coeffs(arena, n, a, b)?;
if let Some(xr) = arena.as_num(x).cloned() {
let y = (xr - Q::one()) / q_int(2);
let v = horner(arena, &d, &y)?;
return guarded_num(arena, v);
}
let e: Vec<Q> = d
.iter()
.enumerate()
.map(|(l, dl)| dl / Ratio::from_integer(BigInt::one() << l))
.collect();
let mut den = BigInt::one();
for el in &e {
den = num_integer::Integer::lcm(&den, el.denom());
}
let mut a: Vec<BigInt> = e
.iter()
.map(|el| el.numer() * (&den / el.denom()))
.collect();
for i in 0..n {
for j in (i..n).rev() {
let next = a[j + 1].clone();
a[j] -= next;
}
}
let c: Vec<Q> = a
.into_iter()
.map(|aj| Ratio::new(aj, den.clone()))
.collect();
poly_from_coeffs(arena, &c, x)
}
#[cfg(test)]
fn eval_legendre(arena: &mut Arena, n: usize, x: ExprId) -> ExprId {
if n == 0 {
return arena.one;
}
if n == 1 {
return x;
}
let mut p_prev = arena.one;
let mut p_curr = x;
for k in 1..n {
let two_k_plus_1 = arena.int(2 * k as i64 + 1);
let k_val = arena.int(k as i64);
let k_plus_1 = arena.int(k as i64 + 1);
let term1 = arena.mul(&[two_k_plus_1, x, p_curr]);
let term2 = arena.mul(&[k_val, p_prev]);
let numer = arena.sub(term1, term2);
let p_next = arena.div(numer, k_plus_1);
let p_next = crate::transforms::expand::expand(arena, p_next);
let p_next = eval(arena, p_next);
p_prev = p_curr;
p_curr = p_next;
}
p_curr
}
#[cfg(test)]
fn eval_chebyshev_t(arena: &mut Arena, n: usize, x: ExprId) -> ExprId {
if n == 0 {
return arena.one;
}
if n == 1 {
return x;
}
let two = arena.int(2);
let mut t_prev = arena.one;
let mut t_curr = x;
for _ in 1..n {
let term1 = arena.mul(&[two, x, t_curr]);
let t_next = arena.sub(term1, t_prev);
let t_next = crate::transforms::expand::expand(arena, t_next);
let t_next = eval(arena, t_next);
t_prev = t_curr;
t_curr = t_next;
}
t_curr
}
#[cfg(test)]
fn eval_chebyshev_u(arena: &mut Arena, n: usize, x: ExprId) -> ExprId {
if n == 0 {
return arena.one;
}
let two = arena.int(2);
if n == 1 {
return arena.mul(&[two, x]);
}
let mut u_prev = arena.one;
let mut u_curr = arena.mul(&[two, x]);
for _ in 1..n {
let term1 = arena.mul(&[two, x, u_curr]);
let u_next = arena.sub(term1, u_prev);
let u_next = crate::transforms::expand::expand(arena, u_next);
let u_next = eval(arena, u_next);
u_prev = u_curr;
u_curr = u_next;
}
u_curr
}
#[cfg(test)]
fn eval_hermite(arena: &mut Arena, n: usize, x: ExprId) -> ExprId {
if n == 0 {
return arena.one;
}
let two = arena.int(2);
if n == 1 {
return arena.mul(&[two, x]);
}
let mut h_prev = arena.one;
let mut h_curr = arena.mul(&[two, x]);
for k in 1..n {
let two_k = arena.int(2 * k as i64);
let term1 = arena.mul(&[two, x, h_curr]);
let term2 = arena.mul(&[two_k, h_prev]);
let h_next = arena.sub(term1, term2);
let h_next = crate::transforms::expand::expand(arena, h_next);
let h_next = eval(arena, h_next);
h_prev = h_curr;
h_curr = h_next;
}
h_curr
}
#[cfg(test)]
fn eval_laguerre(arena: &mut Arena, n: usize, x: ExprId) -> ExprId {
if n == 0 {
return arena.one;
}
if n == 1 {
return arena.sub(arena.one, x);
}
let mut l_prev = arena.one;
let mut l_curr = arena.sub(arena.one, x);
for k in 1..n {
let two_k_plus_1 = arena.int(2 * k as i64 + 1);
let k_val = arena.int(k as i64);
let k_plus_1 = arena.int(k as i64 + 1);
let coeff = arena.sub(two_k_plus_1, x); let term1 = arena.mul(&[coeff, l_curr]);
let term2 = arena.mul(&[k_val, l_prev]);
let numer = arena.sub(term1, term2);
let l_next = arena.div(numer, k_plus_1);
let l_next = crate::transforms::expand::expand(arena, l_next);
let l_next = eval(arena, l_next);
l_prev = l_curr;
l_curr = l_next;
}
l_curr
}
fn eval_lib_fn(arena: &mut Arena, f: LibFn, args: &[ExprId]) -> Option<ExprId> {
match f {
LibFn::Factorial2 => eval_factorial2(arena, args[0]),
LibFn::Subfactorial => eval_subfactorial(arena, args[0]),
LibFn::RisingFactorial => eval_rising_factorial(arena, args[0], args[1]),
LibFn::FallingFactorial => eval_falling_factorial(arena, args[0], args[1]),
LibFn::Fibonacci => eval_fibonacci(arena, args[0]),
LibFn::Lucas => eval_lucas(arena, args[0]),
LibFn::Bernoulli => eval_bernoulli(arena, args[0]),
LibFn::Harmonic => eval_harmonic(arena, args[0]),
LibFn::Catalan => eval_catalan(arena, args[0]),
LibFn::Bell => eval_bell(arena, args[0]),
LibFn::EulerNumber => eval_euler_number(arena, args[0]),
LibFn::Stirling1 => eval_stirling1(arena, args[0], args[1]),
LibFn::Stirling2 => eval_stirling2(arena, args[0], args[1]),
LibFn::PartitionCount => eval_partition_count(arena, args[0]),
LibFn::LambertW => eval_lambertw_call(arena, args),
LibFn::BesselJ | LibFn::BesselI => eval_bessel_regular_at_zero(arena, args[0], args[1]),
LibFn::BesselY | LibFn::BesselK => None,
LibFn::Legendre => eval_orthopoly(arena, args[0], args[1], OrthoFamily::Legendre),
LibFn::ChebyshevT => eval_orthopoly(arena, args[0], args[1], OrthoFamily::ChebyshevT),
LibFn::ChebyshevU => eval_orthopoly(arena, args[0], args[1], OrthoFamily::ChebyshevU),
LibFn::Hermite => eval_orthopoly(arena, args[0], args[1], OrthoFamily::Hermite),
LibFn::Laguerre => eval_orthopoly(arena, args[0], args[1], OrthoFamily::Laguerre),
LibFn::Erfi => eval_erfi(arena, args[0]),
LibFn::ErfInv => eval_erfinv(arena, args[0]),
LibFn::ErfcInv => eval_erfcinv(arena, args[0]),
LibFn::ExpInt => eval_expint(arena, args[0], args[1]),
LibFn::Shi => eval_shi(arena, args[0]),
LibFn::Chi => eval_chi(arena, args[0]),
LibFn::FresnelS | LibFn::FresnelC => eval_fresnel(arena, f, args[0]),
LibFn::LowerGamma => eval_lowergamma(arena, args[0], args[1]),
LibFn::UpperGamma => eval_uppergamma(arena, args[0], args[1]),
LibFn::PolyLog => eval_polylog(arena, args[0], args[1]),
LibFn::DirichletEta => eval_dirichlet_eta(arena, args[0]),
LibFn::AiryAi | LibFn::AiryBi | LibFn::AiryAiPrime | LibFn::AiryBiPrime => {
eval_airy(arena, f, args[0])
}
LibFn::EllipticK => eval_elliptic_k(arena, args[0]),
LibFn::EllipticE => eval_elliptic_e(arena, args[0]),
LibFn::EllipticF => eval_elliptic_f(arena, args[0], args[1]),
LibFn::EllipticPi => eval_elliptic_pi(arena, args[0], args[1]),
LibFn::Gegenbauer => eval_gegenbauer(arena, args[0], args[1], args[2]),
LibFn::Jacobi => eval_jacobi(arena, args[0], args[1], args[2], args[3]),
LibFn::AssocLegendre => eval_assoc_legendre(arena, args[0], args[1], args[2]),
LibFn::AssocLaguerre => eval_assoc_laguerre(arena, args[0], args[1], args[2]),
LibFn::BetaInc | LibFn::BetaIncRegularized => eval_betainc(
arena,
f == LibFn::BetaIncRegularized,
args[0],
args[1],
args[2],
args[3],
),
}
}
fn eval_bessel_regular_at_zero(arena: &mut Arena, order: ExprId, x: ExprId) -> Option<ExprId> {
let order_num = arena.as_num(order)?;
let arg_num = arena.as_num(x)?;
if !arg_num.is_zero() {
return None;
}
if order_num.is_zero() {
Some(arena.one)
} else if order_num.is_positive() || order_num.is_integer() {
Some(arena.zero)
} else {
Some(arena.complex_infinity)
}
}
fn eval_orthopoly(arena: &mut Arena, n: ExprId, x: ExprId, family: OrthoFamily) -> Option<ExprId> {
let n_num = arena.as_num(n)?;
if !n_num.is_integer() || n_num.is_negative() {
return None;
}
let n_int = n_num.to_integer().to_u64()?;
if n_int > arena.config.max_pow_exponent as u64 {
return None;
}
ortho_value(arena, &family, n_int as usize, x)
}
pub(crate) fn apply_named(arena: &mut Arena, f: LibFn, args: &[ExprId]) -> ExprId {
arena.lib_apply(f, args)
}
const MAX_SPECIAL_EXPANSION: i64 = 64;
fn as_int_in(arena: &Arena, id: ExprId, lo: i64, hi: i64) -> Option<i64> {
let r = arena.as_num(id)?;
if !r.is_integer() {
return None;
}
let n: i64 = r.to_integer().try_into().ok()?;
(lo..=hi).contains(&n).then_some(n)
}
fn as_ratio(arena: &Arena, id: ExprId) -> Option<Q> {
arena.as_num(id).cloned()
}
fn half_pi(arena: &mut Arena) -> ExprId {
let half = arena.rational(1, 2);
arena.mul(&[half, arena.pi])
}
fn eval_erfi(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if x == arena.zero {
return Some(arena.zero);
}
if x == arena.infinity {
return Some(arena.infinity);
}
if x == arena.neg_infinity {
return Some(arena.neg_infinity);
}
if let Some(y) = as_negated_general(arena, x) {
let e = apply_named(arena, LibFn::Erfi, &[y]);
return Some(arena.neg(e));
}
None
}
fn eval_erfinv(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if x == arena.zero {
return Some(arena.zero);
}
if x == arena.one {
return Some(arena.infinity);
}
if x == arena.neg_one {
return Some(arena.neg_infinity);
}
if let Some(y) = as_negated_general(arena, x) {
let e = apply_named(arena, LibFn::ErfInv, &[y]);
return Some(arena.neg(e));
}
None
}
fn eval_erfcinv(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if x == arena.one {
return Some(arena.zero);
}
if x == arena.zero {
return Some(arena.infinity);
}
if as_int_in(arena, x, 2, 2).is_some() {
return Some(arena.neg_infinity);
}
None
}
fn eval_expint(arena: &mut Arena, n: ExprId, x: ExprId) -> Option<ExprId> {
if x == arena.infinity {
return Some(arena.zero);
}
if n == arena.zero {
let neg_x = arena.neg(x);
let e = arena.exp(neg_x);
let v = arena.div(e, x);
return Some(eval(arena, v));
}
if x == arena.zero
&& let Some(r) = as_ratio(arena, n)
&& r > Ratio::one()
{
let v = (r - Ratio::one()).recip();
let nid = arena.intern_num(v);
return Some(arena.intern(ExprNode::Num(nid)));
}
None
}
fn eval_shi(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if x == arena.zero {
return Some(arena.zero);
}
if x == arena.infinity {
return Some(arena.infinity);
}
if x == arena.neg_infinity {
return Some(arena.neg_infinity);
}
if let Some(y) = as_negated_general(arena, x) {
let e = apply_named(arena, LibFn::Shi, &[y]);
return Some(arena.neg(e));
}
None
}
fn eval_chi(arena: &mut Arena, x: ExprId) -> Option<ExprId> {
if x == arena.zero {
return Some(arena.complex_infinity);
}
if x == arena.infinity {
return Some(arena.infinity);
}
None
}
fn eval_fresnel(arena: &mut Arena, f: LibFn, x: ExprId) -> Option<ExprId> {
if x == arena.zero {
return Some(arena.zero);
}
if x == arena.infinity {
return Some(arena.rational(1, 2));
}
if x == arena.neg_infinity {
return Some(arena.rational(-1, 2));
}
if let Some(y) = as_negated_general(arena, x) {
let e = apply_named(arena, f, &[y]);
return Some(arena.neg(e));
}
None
}
fn shift_uppergamma(arena: &mut Arena, base: Q, base_val: ExprId, k: i64, x: ExprId) -> ExprId {
let neg_x = arena.neg(x);
let e_neg_x = arena.exp(neg_x);
let mut s = base;
let mut val = base_val;
if k >= 0 {
for _ in 0..k {
let s_id = arena.intern_num(s.clone());
let s_id = arena.intern(ExprNode::Num(s_id));
let x_pow = arena.pow(x, s_id);
let a = arena.mul(&[s_id, val]);
let b = arena.mul(&[x_pow, e_neg_x]);
val = arena.add(&[a, b]);
s += Ratio::one();
}
} else {
for _ in 0..(-k) {
s -= Ratio::one();
let s_id = arena.intern_num(s.clone());
let s_id = arena.intern(ExprNode::Num(s_id));
let x_pow = arena.pow(x, s_id);
let b = arena.mul(&[x_pow, e_neg_x]);
let numer = arena.sub(val, b);
val = arena.div(numer, s_id);
}
}
let expanded = crate::transforms::expand::expand(arena, val);
eval(arena, expanded)
}
fn uppergamma_closed(arena: &mut Arena, s: &Q, x: ExprId) -> Option<ExprId> {
let two = BigInt::from(2);
let bound = Ratio::from_integer(BigInt::from(MAX_SPECIAL_EXPANSION));
if s.abs() > bound {
return None;
}
let neg_x = arena.neg(x);
let e_neg_x = arena.exp(neg_x);
if s.is_integer() {
let n: i64 = s.to_integer().try_into().ok()?;
if n >= 1 {
let mut terms: Vec<ExprId> = Vec::with_capacity(n as usize);
let mut inv_fact = Ratio::<BigInt>::one();
for k in 0..n {
if k > 0 {
inv_fact /= Ratio::from_integer(BigInt::from(k));
}
let c = arena.intern_num(inv_fact.clone());
let c = arena.intern(ExprNode::Num(c));
let k_id = arena.int(k);
let xk = arena.pow(x, k_id);
terms.push(arena.mul(&[c, xk]));
}
let sum = arena.add(&terms);
let fact = factorial_ratio((n - 1) as usize);
let f = arena.intern_num(fact);
let f = arena.intern(ExprNode::Num(f));
let v = arena.mul(&[f, e_neg_x, sum]);
let v = crate::transforms::expand::expand(arena, v);
return Some(eval(arena, v));
}
let e1 = apply_named(arena, LibFn::ExpInt, &[arena.one, x]);
return Some(shift_uppergamma(arena, Ratio::zero(), e1, n, x));
}
if s.denom() == &two {
let half = Ratio::new(BigInt::one(), two);
let k: i64 = (s - &half).to_integer().try_into().ok()?;
let sqrt_pi = arena.sqrt(arena.pi);
let sqrt_x = arena.sqrt(x);
let erfc = arena.erfc(sqrt_x);
let base = arena.mul(&[sqrt_pi, erfc]);
return Some(shift_uppergamma(arena, half, base, k, x));
}
None
}
fn known_nonpositive(arena: &Arena, s: ExprId) -> bool {
if s == arena.neg_infinity {
return true;
}
arena.as_num(s).is_some_and(|r| !r.is_positive())
}
const LOWERGAMMA_FOLD_MAX_LOSS_BITS: f64 = 64.0;
fn ln_positive_ratio(q: &Q) -> f64 {
fn ln_big(n: &BigInt) -> f64 {
let bits = n.bits();
if bits <= 1000 {
return n.to_f64().unwrap_or(f64::INFINITY).ln();
}
let shift = bits - 64;
let top: BigInt = n >> shift;
top.to_f64().unwrap_or(f64::INFINITY).ln() + shift as f64 * std::f64::consts::LN_2
}
ln_big(q.numer()) - ln_big(q.denom())
}
fn lowergamma_closure_cancels(s: &Q, x: &Q) -> bool {
if !x.is_positive() || x >= s {
return false;
}
let Some(x_f) = x.to_f64() else {
return false;
};
lowergamma_closure_cancels_at(s, x_f, ln_positive_ratio(x))
}
fn lowergamma_closure_cancels_at(s: &Q, x_f: f64, ln_x: f64) -> bool {
let half_integer = s.denom() == &BigInt::from(2);
if !(s.is_integer() || half_integer)
|| *s > Q::from_integer(BigInt::from(MAX_SPECIAL_EXPANSION))
{
return false;
}
let Some(s_f) = s.to_f64() else {
return false;
};
let mut ln_gamma = if s.is_integer() {
0.0
} else {
0.5 * std::f64::consts::PI.ln()
};
let mut k = if s.is_integer() { 2.0 } else { 0.5 };
while k <= s_f + 0.25 {
ln_gamma += f64::ln(k);
k += 1.0;
}
let ln_p = s_f * ln_x - x_f - ln_gamma + ((s_f + 1.0) / (s_f + 1.0 - x_f)).ln();
-ln_p / std::f64::consts::LN_2 > LOWERGAMMA_FOLD_MAX_LOSS_BITS
}
fn eval_lowergamma(arena: &mut Arena, s: ExprId, x: ExprId) -> Option<ExprId> {
if x == arena.zero {
if known_nonpositive(arena, s) {
return None;
}
return Some(arena.zero);
}
if x == arena.infinity {
return Some(arena.gamma(s));
}
let r = as_ratio(arena, s)?;
if !r.is_positive() {
return None;
}
if let Some(xr) = as_ratio(arena, x) {
if lowergamma_closure_cancels(&r, &xr) {
return None;
}
} else if let Some(ln_x) = crate::transforms::evalf::ln_of_positive_constant(arena, x)
&& r.to_f64().is_some_and(|s_f| ln_x < s_f.ln())
&& lowergamma_closure_cancels_at(&r, ln_x.exp(), ln_x)
{
return None;
}
let upper = uppergamma_closed(arena, &r, x)?;
let gamma_s = eval_gamma(arena, s).unwrap_or_else(|| arena.gamma(s));
let v = arena.sub(gamma_s, upper);
let v = crate::transforms::expand::expand(arena, v);
Some(eval(arena, v))
}
fn eval_uppergamma(arena: &mut Arena, s: ExprId, x: ExprId) -> Option<ExprId> {
if x == arena.infinity {
return Some(arena.zero);
}
if x == arena.zero {
if known_nonpositive(arena, s) {
return None;
}
return Some(arena.gamma(s));
}
let r = as_ratio(arena, s)?;
uppergamma_closed(arena, &r, x)
}
fn eval_polylog(arena: &mut Arena, s: ExprId, z: ExprId) -> Option<ExprId> {
if z == arena.zero {
return Some(arena.zero);
}
if z == arena.one {
return Some(arena.zeta(s));
}
if z == arena.neg_one {
let eta = eval_dirichlet_eta(arena, s)
.unwrap_or_else(|| apply_named(arena, LibFn::DirichletEta, &[s]));
return Some(arena.neg(eta));
}
if s == arena.one {
let one_minus_z = arena.sub(arena.one, z);
let l = eval_ln(arena, one_minus_z).unwrap_or_else(|| arena.ln(one_minus_z));
return Some(arena.neg(l));
}
if s == arena.zero {
let one_minus_z = arena.sub(arena.one, z);
return Some(arena.div(z, one_minus_z));
}
if as_int_in(arena, s, 2, 2).is_some()
&& let Some(r) = as_ratio(arena, z)
&& r == Ratio::new(BigInt::one(), BigInt::from(2))
{
let two = arena.int(2);
let pi2 = arena.pow(arena.pi, two);
let c12 = arena.rational(1, 12);
let a = arena.mul(&[c12, pi2]);
let ln2 = arena.ln(two);
let ln2_sq = arena.pow(ln2, two);
let half = arena.rational(1, 2);
let b = arena.mul(&[half, ln2_sq]);
return Some(arena.sub(a, b));
}
if let Some(neg_n) = as_int_in(arena, s, -MAX_SPECIAL_EXPANSION, -1) {
let n = (-neg_n) as usize;
let one_minus_z = arena.sub(arena.one, z);
let mut terms: Vec<ExprId> = Vec::with_capacity(n + 1);
let mut k_fact = BigInt::one();
for k in 0..=n {
if k > 0 {
k_fact *= BigInt::from(k as u64);
}
let s2 = crate::domains::combinatorics::stirling2(n as u64 + 1, k as u64 + 1)?;
let coeff = Ratio::from_integer(&k_fact * s2);
let c = arena.intern_num(coeff);
let c = arena.intern(ExprNode::Num(c));
let e1 = arena.int(k as i64 + 1);
let zk = arena.pow(z, e1);
let e2 = arena.int((n - k) as i64);
let wk = arena.pow(one_minus_z, e2);
terms.push(arena.mul(&[c, zk, wk]));
}
let numer = arena.add(&terms);
let numer = crate::transforms::expand::expand(arena, numer);
let numer = eval(arena, numer);
let e = arena.int(n as i64 + 1);
let denom = arena.pow(one_minus_z, e);
return Some(arena.div(numer, denom));
}
None
}
fn eval_dirichlet_eta(arena: &mut Arena, s: ExprId) -> Option<ExprId> {
if s == arena.one {
let two = arena.int(2);
return Some(arena.ln(two));
}
if s == arena.infinity {
return Some(arena.one);
}
let r = as_ratio(arena, s)?;
if !r.is_integer() {
return None;
}
let n: i64 = r.to_integer().try_into().ok()?;
if n < -MAX_SPECIAL_EXPANSION {
return (n % 2 == 0).then_some(arena.zero);
}
let zeta = eval_zeta(arena, s)?;
if matches!(arena.node(zeta), ExprNode::Zeta(_)) {
return None;
}
let e = 1 - n;
let two_pow = if e >= 0 {
Ratio::from_integer(BigInt::one() << (e as usize))
} else {
Ratio::new(BigInt::one(), BigInt::one() << ((-e) as usize))
};
let coeff = Ratio::one() - two_pow;
let c = arena.intern_num(coeff);
let c = arena.intern(ExprNode::Num(c));
Some(arena.mul(&[c, zeta]))
}
fn eval_airy(arena: &mut Arena, f: LibFn, x: ExprId) -> Option<ExprId> {
if x == arena.zero {
let three = arena.int(3);
let third = arena.rational(1, 3);
let two_thirds = arena.rational(2, 3);
let g13 = arena.gamma(third);
let g23 = arena.gamma(two_thirds);
return Some(match f {
LibFn::AiryAi => {
let e = arena.rational(-2, 3);
let p = arena.pow(three, e);
arena.div(p, g23)
}
LibFn::AiryBi => {
let e = arena.rational(-1, 6);
let p = arena.pow(three, e);
arena.div(p, g23)
}
LibFn::AiryAiPrime => {
let e = arena.rational(-1, 3);
let p = arena.pow(three, e);
let q = arena.div(p, g13);
arena.neg(q)
}
_ => {
let e = arena.rational(1, 6);
let p = arena.pow(three, e);
arena.div(p, g13)
}
});
}
if x == arena.infinity {
return Some(match f {
LibFn::AiryAi | LibFn::AiryAiPrime => arena.zero,
_ => arena.infinity,
});
}
if x == arena.neg_infinity && matches!(f, LibFn::AiryAi | LibFn::AiryBi) {
return Some(arena.zero);
}
None
}
fn eval_elliptic_k(arena: &mut Arena, m: ExprId) -> Option<ExprId> {
if m == arena.zero {
return Some(half_pi(arena));
}
if m == arena.one {
return Some(arena.complex_infinity);
}
if m == arena.infinity || m == arena.neg_infinity {
return Some(arena.zero);
}
if let Some(r) = as_ratio(arena, m)
&& r == Ratio::new(BigInt::one(), BigInt::from(2))
{
let quarter = arena.rational(1, 4);
let g = arena.gamma(quarter);
let two = arena.int(2);
let g2 = arena.pow(g, two);
let sqrt_pi = arena.sqrt(arena.pi);
let four = arena.int(4);
let denom = arena.mul(&[four, sqrt_pi]);
return Some(arena.div(g2, denom));
}
None
}
fn eval_elliptic_e(arena: &mut Arena, m: ExprId) -> Option<ExprId> {
if m == arena.zero {
return Some(half_pi(arena));
}
if m == arena.one {
return Some(arena.one);
}
None
}
fn eval_elliptic_f(arena: &mut Arena, phi: ExprId, m: ExprId) -> Option<ExprId> {
if phi == arena.zero {
return Some(arena.zero);
}
if m == arena.zero {
return Some(phi);
}
if let Some(r) = as_pi_multiple(arena, phi)
&& r == Ratio::new(BigInt::one(), BigInt::from(2))
{
return Some(
eval_elliptic_k(arena, m).unwrap_or_else(|| apply_named(arena, LibFn::EllipticK, &[m])),
);
}
if let Some(y) = as_negated_general(arena, phi) {
let e = eval_elliptic_f(arena, y, m)
.unwrap_or_else(|| apply_named(arena, LibFn::EllipticF, &[y, m]));
return Some(arena.neg(e));
}
None
}
fn eval_elliptic_pi(arena: &mut Arena, n: ExprId, m: ExprId) -> Option<ExprId> {
if n == arena.zero {
return Some(
eval_elliptic_k(arena, m).unwrap_or_else(|| apply_named(arena, LibFn::EllipticK, &[m])),
);
}
if n == arena.one {
return Some(arena.complex_infinity);
}
if m == arena.zero {
let one_minus_n = arena.sub(arena.one, n);
let root = arena.sqrt(one_minus_n);
let two = arena.int(2);
let denom = arena.mul(&[two, root]);
return Some(arena.div(arena.pi, denom));
}
if n == m {
let e =
eval_elliptic_e(arena, n).unwrap_or_else(|| apply_named(arena, LibFn::EllipticE, &[n]));
let one_minus_n = arena.sub(arena.one, n);
return Some(arena.div(e, one_minus_n));
}
None
}
fn as_poly_degree(arena: &Arena, n: ExprId) -> Option<usize> {
let r = arena.as_num(n)?;
if !r.is_integer() || r.is_negative() {
return None;
}
let d = r.to_integer().to_u64()?;
(d <= arena.config.max_pow_exponent as u64).then_some(d as usize)
}
const MAX_SYMBOLIC_PARAM_DEGREE: usize = 16;
fn as_param_poly_degree(arena: &Arena, n: ExprId, params: &[ExprId]) -> Option<usize> {
let deg = as_poly_degree(arena, n)?;
let numeric = params.iter().all(|&p| arena.as_num(p).is_some());
(numeric || deg <= MAX_SYMBOLIC_PARAM_DEGREE).then_some(deg)
}
fn degenerate_param_degree(arena: &Arena, n: ExprId, params: &[ExprId]) -> Option<usize> {
let deg = as_param_poly_degree(arena, n, params)?;
let numeric = params.iter().all(|&p| arena.as_num(p).is_some());
(!numeric || deg <= MAX_DEGENERATE_DEGREE).then_some(deg)
}
const MAX_DEGENERATE_DEGREE: usize = 64;
fn expand_eval(arena: &mut Arena, e: ExprId) -> ExprId {
let e = crate::transforms::expand::expand(arena, e);
eval(arena, e)
}
fn expand_mul2(arena: &mut Arena, p: ExprId, q: ExprId) -> ExprId {
let m = arena.mul(&[p, q]);
crate::transforms::expand::expand(arena, m)
}
fn eval_gegenbauer(arena: &mut Arena, n: ExprId, a: ExprId, x: ExprId) -> Option<ExprId> {
if let (Some(deg), Some(alpha)) = (as_poly_degree(arena, n), as_ratio(arena, a))
&& !(alpha.is_integer() && !alpha.is_positive())
{
return ortho_value(arena, &OrthoFamily::Gegenbauer(alpha), deg, x);
}
if let Some(deg) = degenerate_param_degree(arena, n, &[a]) {
if deg == 0 {
return Some(arena.one);
}
let two = arena.int(2);
let c1 = arena.mul(&[two, a, x]);
if deg == 1 {
return Some(expand_eval(arena, c1));
}
let mut prev = arena.one;
let mut curr = expand_eval(arena, c1);
for k in 1..deg {
let k_id = arena.int(k as i64);
let k_plus_a = arena.add(&[k_id, a]);
let t1 = arena.mul(&[two, k_plus_a, x, curr]);
let two_a = arena.mul(&[two, a]);
let km1 = arena.int(k as i64 - 1);
let coeff = arena.add(&[km1, two_a]);
let t2 = arena.mul(&[coeff, prev]);
let numer = arena.sub(t1, t2);
let kp1 = arena.int(k as i64 + 1);
let next = arena.div(numer, kp1);
let next = expand_eval(arena, next);
prev = curr;
curr = next;
}
return Some(curr);
}
if let Some(r) = as_ratio(arena, a) {
if r == Ratio::new(BigInt::one(), BigInt::from(2)) {
return Some(arena.legendre(n, x));
}
if r.is_one() {
return Some(arena.chebyshev_u(n, x));
}
}
None
}
fn expanded_powers(arena: &mut Arena, p: ExprId, n: usize) -> Vec<ExprId> {
let mut pows: Vec<ExprId> = Vec::with_capacity(n + 1);
pows.push(arena.one);
for k in 1..=n {
let next = expand_mul2(arena, pows[k - 1], p);
pows.push(next);
}
pows
}
fn eval_jacobi(arena: &mut Arena, n: ExprId, a: ExprId, b: ExprId, x: ExprId) -> Option<ExprId> {
if let (Some(deg), Some(ar), Some(br)) = (
as_poly_degree(arena, n),
as_ratio(arena, a),
as_ratio(arena, b),
) && !(ar.is_integer() && ar.is_negative())
{
return jacobi_value(arena, deg, &ar, &br, x);
}
if let Some(deg) = degenerate_param_degree(arena, n, &[a, b]) {
if deg == 0 {
return Some(arena.one);
}
let half = arena.rational(1, 2);
let x_minus_1 = arena.sub(x, arena.one);
let x_plus_1 = arena.add(&[x, arena.one]);
let lo = arena.mul(&[half, x_minus_1]);
let lo = expand_eval(arena, lo);
let hi = arena.mul(&[half, x_plus_1]);
let hi = expand_eval(arena, hi);
let lo_pows = expanded_powers(arena, lo, deg);
let hi_pows = expanded_powers(arena, hi, deg);
let mut pa: Vec<ExprId> = vec![arena.one; deg + 1];
for s in (0..deg).rev() {
let j = arena.int(s as i64 + 1);
let factor = arena.add(&[a, j]);
pa[s] = expand_mul2(arena, pa[s + 1], factor);
}
let mut pb: Vec<ExprId> = vec![arena.one; deg + 1];
for s in 1..=deg {
let j = arena.int((deg - s) as i64 + 1);
let factor = arena.add(&[b, j]);
pb[s] = expand_mul2(arena, pb[s - 1], factor);
}
let mut terms: Vec<ExprId> = Vec::with_capacity(deg + 1);
for s in 0..=deg {
let denom = factorial_ratio(deg - s) * factorial_ratio(s);
let c = arena.intern_num(denom.recip());
let c = arena.intern(ExprNode::Num(c));
let ab = arena.mul(&[c, pa[s]]);
let ab = expand_mul2(arena, ab, pb[s]);
let xp = expand_mul2(arena, lo_pows[s], hi_pows[deg - s]);
terms.push(expand_mul2(arena, ab, xp));
}
let sum = arena.add(&terms);
return Some(eval(arena, sum));
}
if a == arena.zero && b == arena.zero {
return Some(arena.legendre(n, x));
}
None
}
fn eval_assoc_legendre(arena: &mut Arena, n: ExprId, m: ExprId, x: ExprId) -> Option<ExprId> {
let deg = match as_poly_degree(arena, n) {
Some(d) => d,
None => {
return (m == arena.zero).then(|| arena.legendre(n, x));
}
};
let order = as_int_in(arena, m, -(deg as i64), deg as i64);
let order = match order {
Some(o) => o,
None => {
let r = as_ratio(arena, m)?;
return r.is_integer().then_some(arena.zero);
}
};
if order == 0 {
return ortho_value(arena, &OrthoFamily::Legendre, deg, x);
}
let mu = order.unsigned_abs() as usize;
let two = arena.int(2);
let x2 = arena.pow(x, two);
let one_minus_x2 = arena.sub(arena.one, x2);
let half_mu = arena.rational(mu as i64, 2);
let root_pow = arena.pow(one_minus_x2, half_mu);
let c = ortho_coeffs(arena, &OrthoFamily::Legendre, deg)?;
let mut r = vec![Q::zero(); deg - mu + 1];
let mut falling = factorial_ratio(mu); for i in mu..=deg {
if i > mu {
falling = falling * q_int(i as i64) / q_int((i - mu) as i64);
}
let mut v = &c[i] * &falling;
if mu % 2 == 1 {
v = -v;
}
if !within_guard_bits(arena, &v) {
return None;
}
r[i - mu] = v;
}
let poly = match arena.as_num(x).cloned() {
Some(xr) => {
let v = horner(arena, &r, &xr)?;
guarded_num(arena, v)?
}
None => poly_from_coeffs(arena, &r, x)?,
};
let prod = arena.mul(&[poly, root_pow]);
let value = expand_eval(arena, prod);
if order < 0 {
let mut ratio = factorial_ratio(deg - mu) / factorial_ratio(deg + mu);
if mu % 2 == 1 {
ratio = -ratio;
}
let c = arena.intern_num(ratio);
let c = arena.intern(ExprNode::Num(c));
let v = arena.mul(&[c, value]);
return Some(expand_eval(arena, v));
}
Some(value)
}
fn eval_assoc_laguerre(arena: &mut Arena, n: ExprId, a: ExprId, x: ExprId) -> Option<ExprId> {
if let (Some(deg), Some(alpha)) = (as_poly_degree(arena, n), as_ratio(arena, a))
&& !(alpha.is_integer() && alpha.is_negative())
{
return ortho_value(arena, &OrthoFamily::AssocLaguerre(alpha), deg, x);
}
if let Some(deg) = degenerate_param_degree(arena, n, &[a]) {
if deg == 0 {
return Some(arena.one);
}
let one_plus_a = arena.add(&[arena.one, a]);
let l1 = arena.sub(one_plus_a, x);
let l1 = expand_eval(arena, l1);
if deg == 1 {
return Some(l1);
}
let mut prev = arena.one;
let mut curr = l1;
for k in 1..deg {
let two_k_p1 = arena.int(2 * k as i64 + 1);
let coeff = arena.add(&[two_k_p1, a]);
let coeff = arena.sub(coeff, x);
let t1 = arena.mul(&[coeff, curr]);
let k_id = arena.int(k as i64);
let k_plus_a = arena.add(&[k_id, a]);
let t2 = arena.mul(&[k_plus_a, prev]);
let numer = arena.sub(t1, t2);
let kp1 = arena.int(k as i64 + 1);
let next = arena.div(numer, kp1);
let next = expand_eval(arena, next);
prev = curr;
curr = next;
}
return Some(curr);
}
if a == arena.zero {
return Some(arena.laguerre(n, x));
}
None
}
const MAX_BETAINC_POLY_PARAM: i64 = 20;
fn eval_betainc(
arena: &mut Arena,
regularized: bool,
a: ExprId,
b: ExprId,
x1: ExprId,
x2: ExprId,
) -> Option<ExprId> {
if x1 == x2 {
return Some(arena.zero);
}
if x1 == arena.zero && x2 == arena.one {
if known_nonpositive(arena, a) || known_nonpositive(arena, b) {
return None;
}
return Some(if regularized {
arena.one
} else {
let beta = arena.beta(a, b);
eval_beta(arena, a, b).unwrap_or(beta)
});
}
let a_int = as_int_in(arena, a, 1, MAX_BETAINC_POLY_PARAM)?;
let b_int = as_int_in(arena, b, 1, MAX_BETAINC_POLY_PARAM)?;
let scale = if regularized {
factorial_ratio((a_int + b_int - 1) as usize)
/ (factorial_ratio((a_int - 1) as usize) * factorial_ratio((b_int - 1) as usize))
} else {
Ratio::one()
};
let mut coeffs: Vec<Q> = Vec::with_capacity(b_int as usize);
let mut binom = BigInt::one();
for k in 0..b_int {
if k > 0 {
binom = binom * BigInt::from(b_int - k) / BigInt::from(k);
}
let mut c = Ratio::new(binom.clone(), BigInt::from(a_int + k)) * &scale;
if k % 2 == 1 {
c = -c;
}
coeffs.push(c);
}
let antiderivative = |arena: &mut Arena, t: ExprId| -> ExprId {
if t == arena.zero {
return arena.zero;
}
let mut terms: Vec<ExprId> = Vec::with_capacity(coeffs.len());
for (k, c) in coeffs.iter().enumerate() {
let cid = arena.intern_num(c.clone());
let cid = arena.intern(ExprNode::Num(cid));
let e = arena.int(a_int + k as i64);
let p = arena.pow(t, e);
terms.push(arena.mul(&[cid, p]));
}
arena.add(&terms)
};
let upper = antiderivative(arena, x2);
let lower = antiderivative(arena, x1);
let v = arena.sub(upper, lower);
Some(expand_eval(arena, v))
}
#[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 eval_sin_zero() {
let mut a = Arena::new();
let expr = a.sin(a.zero);
let result = eval(&mut a, expr);
assert_eq!(result, a.zero, "sin(0) should be 0");
}
#[test]
fn eval_sin_pi() {
let mut a = Arena::new();
let pi = a.pi;
let expr = a.sin(pi);
let result = eval(&mut a, expr);
assert_eq!(result, a.zero, "sin(π) should be 0");
}
#[test]
fn eval_sin_pi_over_2() {
let mut a = Arena::new();
let half = a.rational(1, 2);
let pi = a.pi;
let pi_over_2 = a.mul(&[half, pi]);
let expr = a.sin(pi_over_2);
let result = eval(&mut a, expr);
assert_eq!(result, a.one, "sin(π/2) should be 1");
}
#[test]
fn eval_sin_3pi_over_2() {
let mut a = Arena::new();
let three_halves = a.rational(3, 2);
let pi = a.pi;
let arg = a.mul(&[three_halves, pi]);
let expr = a.sin(arg);
let result = eval(&mut a, expr);
assert_eq!(result, a.neg_one, "sin(3π/2) should be -1");
}
#[test]
fn eval_sin_symbolic_unchanged() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.sin(x);
let result = eval(&mut a, expr);
assert_eq!(result, expr, "sin(x) should stay unevaluated");
}
#[test]
fn eval_cos_zero() {
let mut a = Arena::new();
let expr = a.cos(a.zero);
let result = eval(&mut a, expr);
assert_eq!(result, a.one, "cos(0) should be 1");
}
#[test]
fn eval_cos_pi() {
let mut a = Arena::new();
let pi = a.pi;
let expr = a.cos(pi);
let result = eval(&mut a, expr);
assert_eq!(result, a.neg_one, "cos(π) should be -1");
}
#[test]
fn eval_cos_pi_over_2() {
let mut a = Arena::new();
let half = a.rational(1, 2);
let pi = a.pi;
let pi_over_2 = a.mul(&[half, pi]);
let expr = a.cos(pi_over_2);
let result = eval(&mut a, expr);
assert_eq!(result, a.zero, "cos(π/2) should be 0");
}
#[test]
fn eval_cos_pi_over_4() {
let mut a = Arena::new();
let quarter = a.rational(1, 4);
let pi = a.pi;
let arg = a.mul(&[quarter, pi]);
let expr = a.cos(arg);
let result = eval(&mut a, expr);
let two = a.int(2);
let half_exp = a.rational(1, 2);
let sqrt2 = a.pow(two, half_exp);
let half = a.rational(1, 2);
let expected = a.mul(&[half, sqrt2]);
assert_eq!(result, expected, "cos(π/4) should be √2/2");
}
#[test]
fn eval_tan_zero() {
let mut a = Arena::new();
let expr = a.tan(a.zero);
let result = eval(&mut a, expr);
assert_eq!(result, a.zero, "tan(0) should be 0");
}
#[test]
fn eval_tan_pi() {
let mut a = Arena::new();
let pi = a.pi;
let expr = a.tan(pi);
let result = eval(&mut a, expr);
assert_eq!(result, a.zero, "tan(π) should be 0");
}
#[test]
fn eval_exp_zero() {
let mut a = Arena::new();
let expr = a.exp(a.zero);
let result = eval(&mut a, expr);
assert_eq!(result, a.one, "exp(0) should be 1");
}
#[test]
fn eval_exp_one() {
let mut a = Arena::new();
let expr = a.exp(a.one);
let result = eval(&mut a, expr);
assert_eq!(result, a.e_const, "exp(1) should be E");
}
#[test]
fn eval_exp_symbolic_unchanged() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.exp(x);
let result = eval(&mut a, expr);
assert_eq!(result, expr, "exp(x) should stay unevaluated");
}
#[test]
fn eval_ln_one() {
let mut a = Arena::new();
let expr = a.ln(a.one);
let result = eval(&mut a, expr);
assert_eq!(result, a.zero, "ln(1) should be 0");
}
#[test]
fn eval_ln_e() {
let mut a = Arena::new();
let e = a.e_const;
let expr = a.ln(e);
let result = eval(&mut a, expr);
assert_eq!(result, a.one, "ln(E) should be 1");
}
#[test]
fn eval_sqrt_zero() {
let mut a = Arena::new();
let expr = a.sqrt(a.zero);
let result = eval(&mut a, expr);
assert_eq!(result, a.zero, "sqrt(0) should be 0");
}
#[test]
fn eval_sqrt_one() {
let mut a = Arena::new();
let expr = a.sqrt(a.one);
let result = eval(&mut a, expr);
assert_eq!(result, a.one, "sqrt(1) should be 1");
}
#[test]
fn eval_sqrt_four() {
let mut a = Arena::new();
let four = a.int(4);
let expr = a.sqrt(four);
let result = eval(&mut a, expr);
let two = a.int(2);
assert_eq!(result, two, "sqrt(4) should be 2");
}
#[test]
fn eval_sqrt_nine() {
let mut a = Arena::new();
let nine = a.int(9);
let expr = a.sqrt(nine);
let result = eval(&mut a, expr);
let three = a.int(3);
assert_eq!(result, three, "sqrt(9) should be 3");
}
#[test]
fn eval_sqrt_two_unchanged() {
let mut a = Arena::new();
let two = a.int(2);
let expr = a.sqrt(two);
let result = eval(&mut a, expr);
assert_eq!(
display(&a, result),
"sqrt(2)",
"sqrt(2) should stay unevaluated (irrational)"
);
}
#[test]
fn eval_abs_positive() {
let mut a = Arena::new();
let five = a.int(5);
let expr = a.abs(five);
let result = eval(&mut a, expr);
assert_eq!(result, five, "abs(5) should be 5");
}
#[test]
fn eval_abs_negative() {
let mut a = Arena::new();
let neg_three = a.int(-3);
let expr = a.abs(neg_three);
let result = eval(&mut a, expr);
let three = a.int(3);
assert_eq!(result, three, "abs(-3) should be 3");
}
#[test]
fn eval_abs_zero() {
let mut a = Arena::new();
let expr = a.abs(a.zero);
let result = eval(&mut a, expr);
assert_eq!(result, a.zero, "abs(0) should be 0");
}
#[test]
fn eval_abs_symbolic_unchanged() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.abs(x);
let result = eval(&mut a, expr);
assert_eq!(result, expr, "abs(x) should stay unevaluated");
}
#[test]
fn eval_nested_sin_of_zero_in_add() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sin_zero = a.sin(a.zero);
let expr = a.add(&[x, sin_zero]);
let result = eval(&mut a, expr);
assert_eq!(result, x, "x + sin(0) should simplify to x");
}
#[test]
fn eval_exp_zero_in_mul() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let exp_zero = a.exp(a.zero);
let expr = a.mul(&[x, exp_zero]);
let result = eval(&mut a, expr);
assert_eq!(result, x, "x * exp(0) should simplify to x");
}
#[test]
fn eval_cos_pi_in_expression() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let pi = a.pi;
let cos_pi = a.cos(pi);
let expr = a.add(&[x, cos_pi]);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "x - 1");
}
#[test]
fn eval_multiple_functions() {
let mut a = Arena::new();
let sin_0 = a.sin(a.zero);
let cos_0 = a.cos(a.zero);
let ln_1 = a.ln(a.one);
let expr = a.add(&[sin_0, cos_0, ln_1]);
let result = eval(&mut a, expr);
assert_eq!(result, a.one, "sin(0) + cos(0) + ln(1) should be 1");
}
#[test]
fn eval_is_idempotent() {
let mut a = Arena::new();
let pi = a.pi;
let expr = a.sin(pi);
let first = eval(&mut a, expr);
let second = eval(&mut a, first);
assert_eq!(first, second, "eval should be idempotent");
}
#[test]
fn eval_sin_pi_over_6() {
let mut a = Arena::new();
let sixth = a.rational(1, 6);
let pi = a.pi;
let arg = a.mul(&[sixth, pi]);
let expr = a.sin(arg);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "1/2", "sin(π/6) should be 1/2");
}
#[test]
fn eval_cos_pi_over_3() {
let mut a = Arena::new();
let third = a.rational(1, 3);
let pi = a.pi;
let arg = a.mul(&[third, pi]);
let expr = a.cos(arg);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "1/2", "cos(π/3) should be 1/2");
}
#[test]
fn eval_tan_pi_over_4() {
let mut a = Arena::new();
let quarter = a.rational(1, 4);
let pi = a.pi;
let arg = a.mul(&[quarter, pi]);
let expr = a.tan(arg);
let result = eval(&mut a, expr);
assert_eq!(result, a.one, "tan(π/4) should be 1");
}
#[test]
fn eval_sqrt_rational_perfect_square() {
let mut a = Arena::new();
let nine_fourths = a.rational(9, 4);
let expr = a.sqrt(nine_fourths);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "3/2", "sqrt(9/4) should be 3/2");
}
#[test]
fn eval_cube_root_8() {
let mut a = Arena::new();
let eight = a.int(8);
let third = a.rational(1, 3);
let expr = a.pow(eight, third);
let result = eval(&mut a, expr);
let two = a.int(2);
assert_eq!(result, two, "8^(1/3) should be 2");
}
#[test]
fn eval_cube_root_27() {
let mut a = Arena::new();
let twenty_seven = a.int(27);
let third = a.rational(1, 3);
let expr = a.pow(twenty_seven, third);
let result = eval(&mut a, expr);
let three = a.int(3);
assert_eq!(result, three, "27^(1/3) should be 3");
}
#[test]
fn eval_fourth_root_16() {
let mut a = Arena::new();
let sixteen = a.int(16);
let quarter = a.rational(1, 4);
let expr = a.pow(sixteen, quarter);
let result = eval(&mut a, expr);
let two = a.int(2);
assert_eq!(result, two, "16^(1/4) should be 2");
}
#[test]
fn eval_cube_root_7_unchanged() {
let mut a = Arena::new();
let seven = a.int(7);
let third = a.rational(1, 3);
let expr = a.pow(seven, third);
let result = eval(&mut a, expr);
assert_eq!(result, expr, "7^(1/3) should stay unevaluated");
}
#[test]
fn eval_cube_root_rational_perfect() {
let mut a = Arena::new();
let base = a.rational(27, 8);
let third = a.rational(1, 3);
let expr = a.pow(base, third);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "3/2", "(27/8)^(1/3) should be 3/2");
}
#[test]
fn eval_sin_pi_over_4() {
let mut a = Arena::new();
let quarter = a.rational(1, 4);
let pi = a.pi;
let arg = a.mul(&[quarter, pi]);
let expr = a.sin(arg);
let result = eval(&mut a, expr);
let two = a.int(2);
let half_exp = a.rational(1, 2);
let sqrt2 = a.pow(two, half_exp);
let half = a.rational(1, 2);
let expected = a.mul(&[half, sqrt2]);
assert_eq!(result, expected, "sin(π/4) should be √2/2");
}
#[test]
fn eval_sin_pi_over_3() {
let mut a = Arena::new();
let third = a.rational(1, 3);
let pi = a.pi;
let arg = a.mul(&[third, pi]);
let expr = a.sin(arg);
let result = eval(&mut a, expr);
let three = a.int(3);
let half_exp = a.rational(1, 2);
let sqrt3 = a.pow(three, half_exp);
let half = a.rational(1, 2);
let expected = a.mul(&[half, sqrt3]);
assert_eq!(result, expected, "sin(π/3) should be √3/2");
}
#[test]
fn eval_cos_pi_over_6() {
let mut a = Arena::new();
let sixth = a.rational(1, 6);
let pi = a.pi;
let arg = a.mul(&[sixth, pi]);
let expr = a.cos(arg);
let result = eval(&mut a, expr);
let three = a.int(3);
let half_exp = a.rational(1, 2);
let sqrt3 = a.pow(three, half_exp);
let half = a.rational(1, 2);
let expected = a.mul(&[half, sqrt3]);
assert_eq!(result, expected, "cos(π/6) should be √3/2");
}
#[test]
fn eval_sinh_neg_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let neg_x = a.neg(x);
let expr = a.sinh(neg_x);
let result = eval(&mut a, expr);
let sinh_x = a.sinh(x);
let expected = a.neg(sinh_x);
assert_eq!(result, expected, "sinh(-x) should be -sinh(x)");
}
#[test]
fn eval_cosh_neg_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let neg_x = a.neg(x);
let expr = a.cosh(neg_x);
let result = eval(&mut a, expr);
let expected = a.cosh(x);
assert_eq!(result, expected, "cosh(-x) should be cosh(x)");
}
#[test]
fn eval_tanh_neg_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let neg_x = a.neg(x);
let expr = a.tanh(neg_x);
let result = eval(&mut a, expr);
let tanh_x = a.tanh(x);
let expected = a.neg(tanh_x);
assert_eq!(result, expected, "tanh(-x) should be -tanh(x)");
}
#[test]
fn eval_sin_2pi_over_3() {
let mut a = Arena::new();
let coeff = a.rational(2, 3);
let angle = a.mul(&[coeff, a.pi]);
let expr = a.sin(angle);
let result = eval(&mut a, expr);
let s = display(&a, result);
assert!(
s.contains("3") && s.contains("1/2"),
"sin(2π/3) should be √3/2, got: {s}"
);
}
#[test]
fn eval_sin_3pi_over_4() {
let mut a = Arena::new();
let coeff = a.rational(3, 4);
let angle = a.mul(&[coeff, a.pi]);
let expr = a.sin(angle);
let result = eval(&mut a, expr);
let s = display(&a, result);
assert!(
s.contains("2") && s.contains("1/2"),
"sin(3π/4) should be √2/2, got: {s}"
);
}
#[test]
fn eval_cos_3pi_over_4() {
let mut a = Arena::new();
let coeff = a.rational(3, 4);
let angle = a.mul(&[coeff, a.pi]);
let expr = a.cos(angle);
let result = eval(&mut a, expr);
let s = display(&a, result);
assert!(
s.contains("2") && s.contains("1/2"),
"cos(3π/4) should be -√2/2, got: {s}"
);
}
#[test]
fn eval_cos_5pi_over_6() {
let mut a = Arena::new();
let coeff = a.rational(5, 6);
let angle = a.mul(&[coeff, a.pi]);
let expr = a.cos(angle);
let result = eval(&mut a, expr);
let s = display(&a, result);
assert!(
s.contains("3") && s.contains("1/2"),
"cos(5π/6) should be -√3/2, got: {s}"
);
}
#[test]
fn eval_tan_pi_over_6() {
let mut a = Arena::new();
let coeff = a.rational(1, 6);
let angle = a.mul(&[coeff, a.pi]);
let expr = a.tan(angle);
let result = eval(&mut a, expr);
let s = display(&a, result);
assert!(s.contains("3"), "tan(π/6) should involve √3, got: {s}");
}
#[test]
fn eval_tan_pi_over_3() {
let mut a = Arena::new();
let coeff = a.rational(1, 3);
let angle = a.mul(&[coeff, a.pi]);
let expr = a.tan(angle);
let result = eval(&mut a, expr);
let s = display(&a, result);
assert!(s.contains("3"), "tan(π/3) should be √3, got: {s}");
}
#[test]
fn eval_exp_i_pi() {
let mut a = Arena::new();
let i = a.i_unit;
let pi = a.pi;
let i_pi = a.mul(&[i, pi]);
let expr = a.exp(i_pi);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "-1");
}
#[test]
fn eval_exp_i_pi_over_2() {
let mut a = Arena::new();
let i = a.i_unit;
let half = a.rational(1, 2);
let pi = a.pi;
let half_pi = a.mul(&[half, pi]);
let i_half_pi = a.mul(&[i, half_pi]);
let expr = a.exp(i_half_pi);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "I");
}
#[test]
fn eval_abs_i() {
let mut a = Arena::new();
let expr = a.abs(a.i_unit);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "1");
}
#[test]
fn eval_ln_neg_one() {
let mut a = Arena::new();
let expr = a.ln(a.neg_one);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "pi*I");
}
#[test]
fn eval_ln_neg_two() {
let mut a = Arena::new();
let neg_two = a.rational(-2, 1);
let expr = a.ln(neg_two);
let result = eval(&mut a, expr);
let two = a.rational(2, 1);
let ln2 = a.ln(two);
let i_pi = a.mul(&[a.i_unit, a.pi]);
let expected = a.add(&[ln2, i_pi]);
assert_eq!(display(&a, result), display(&a, expected));
}
#[test]
fn special_angle_tables_are_right_and_invert() {
use std::f64::consts::PI;
for (table, entries) in [
(SpecialTable::Cos, SPECIAL_COS),
(SpecialTable::Tan, SPECIAL_TAN),
] {
for e in entries {
let mut a = Arena::new();
let v = special_value_node(&mut a, e);
let angle = PI * e.k as f64 / e.n as f64;
let exact = match table {
SpecialTable::Cos => angle.cos(),
SpecialTable::Tan => angle.tan(),
};
let got = crate::transforms::evalf::evalf_f64(&a, v).unwrap();
assert!(
(got - exact).abs() < 1e-14,
"{}/{}: {got} vs {exact}",
e.k,
e.n
);
let q = Ratio::new(BigInt::from(e.k), BigInt::from(e.n));
let neg = a.neg(v);
let neg = eval(&mut a, neg);
let (f_pos, f_neg, want_pos, want_neg) = match table {
SpecialTable::Cos => {
(a.acos(v), a.acos(neg), q.clone(), Ratio::one() - q.clone())
}
SpecialTable::Tan => (a.atan(v), a.atan(neg), q.clone(), -q.clone()),
};
let got_pos = eval(&mut a, f_pos);
let got_neg = eval(&mut a, f_neg);
assert_eq!(
as_pi_multiple(&a, got_pos),
Some(want_pos),
"{}/{}",
e.k,
e.n
);
assert_eq!(
as_pi_multiple(&a, got_neg),
Some(want_neg),
"-{}/{}",
e.k,
e.n
);
}
}
}
#[test]
fn eval_asin_half() {
let mut a = Arena::new();
let half = a.rational(1, 2);
let expr = a.asin(half);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "1/6*pi");
}
#[test]
fn eval_asin_neg_half() {
let mut a = Arena::new();
let neg_half = a.rational(-1, 2);
let expr = a.asin(neg_half);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "-1/6*pi");
}
#[test]
fn eval_acos_half() {
let mut a = Arena::new();
let half = a.rational(1, 2);
let expr = a.acos(half);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "1/3*pi");
}
#[test]
fn eval_acos_neg_half() {
let mut a = Arena::new();
let neg_half = a.rational(-1, 2);
let expr = a.acos(neg_half);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "2/3*pi");
}
#[test]
fn eval_sqrt_8() {
let mut a = Arena::new();
let eight = a.int(8);
let half = a.rational(1, 2);
let expr = a.pow(eight, half);
let result = eval(&mut a, expr);
let d = display(&a, result);
assert!(
d.contains("2") && d != "8^(1/2)",
"sqrt(8) should simplify to 2*sqrt(2), got: {d}"
);
}
#[test]
fn eval_sqrt_12() {
let mut a = Arena::new();
let twelve = a.int(12);
let half = a.rational(1, 2);
let expr = a.pow(twelve, half);
let result = eval(&mut a, expr);
let d = display(&a, result);
assert!(
d.contains("2") && d.contains("3"),
"sqrt(12) should simplify to 2*sqrt(3), got: {d}"
);
}
#[test]
fn eval_sqrt_50() {
let mut a = Arena::new();
let fifty = a.int(50);
let half = a.rational(1, 2);
let expr = a.pow(fifty, half);
let result = eval(&mut a, expr);
let d = display(&a, result);
assert!(
d.contains("5") && d.contains("2"),
"sqrt(50) should simplify to 5*sqrt(2), got: {d}"
);
}
#[test]
fn eval_sin_ix() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let ix = a.mul(&[a.i_unit, x]);
let expr = a.sin(ix);
let result = eval(&mut a, expr);
let d = display(&a, result);
assert!(
d.contains("sinh") && d.contains("I"),
"sin(i*x) should be i*sinh(x), got: {d}"
);
}
#[test]
fn eval_cos_ix() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let ix = a.mul(&[a.i_unit, x]);
let expr = a.cos(ix);
let result = eval(&mut a, expr);
let d = display(&a, result);
assert!(d.contains("cosh"), "cos(i*x) should be cosh(x), got: {d}");
}
#[test]
fn eval_factorial_5() {
let mut a = Arena::new();
let five = a.int(5);
let expr = a.factorial(five);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "120");
}
#[test]
fn eval_factorial_0() {
let mut a = Arena::new();
let zero = a.zero;
let expr = a.factorial(zero);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "1");
}
#[test]
fn eval_binomial_5_2() {
let mut a = Arena::new();
let five = a.int(5);
let two = a.int(2);
let expr = a.binomial(five, two);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "10");
}
#[test]
fn eval_sign_positive() {
let mut a = Arena::new();
let five = a.int(5);
let expr = a.sign(five);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "1");
}
#[test]
fn eval_sign_negative() {
let mut a = Arena::new();
let neg = a.int(-3);
let expr = a.sign(neg);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "-1");
}
#[test]
fn eval_sign_zero() {
let mut a = Arena::new();
let expr = a.sign(a.zero);
let result = eval(&mut a, expr);
assert_eq!(display(&a, result), "0");
}
#[test]
fn eval_tan_two_thirds_pi() {
let mut arena = Arena::new();
let two_thirds = arena.rational(2, 3);
let angle = arena.mul(&[two_thirds, arena.pi()]);
let tan_expr = arena.tan(angle);
let result = eval(&mut arena, tan_expr);
let d = display(&arena, result);
assert!(
d.contains("3") || d.contains("sqrt"),
"tan(2π/3) should evaluate, got: {d}"
);
}
#[test]
fn eval_negative_cube_root_is_principal() {
let mut arena = Arena::new();
let neg8 = arena.int(-8);
let third = arena.rational(1, 3);
let expr = arena.pow(neg8, third);
let result = eval(&mut arena, expr);
let two = arena.int(2);
let neg_one_root = arena.pow(arena.neg_one, third);
let expected = arena.mul(&[two, neg_one_root]);
assert_eq!(result, expected, "(-8)^(1/3) should be 2*(-1)^(1/3)");
}
#[test]
fn eval_zeta_table() {
let mut a = Arena::new();
let cases: [(i64, &str); 8] = [
(0, "-1/2"),
(-1, "-1/12"),
(-2, "0"),
(-3, "1/120"),
(-9, "-1/132"),
(2, "1/6*pi^2"),
(4, "1/90*pi^4"),
(14, "2/18243225*pi^14"),
];
for (s, expected) in cases {
let sid = a.int(s);
let z = a.zeta(sid);
assert_eq!(display(&a, z), expected, "zeta({s})");
}
let one = a.one;
assert_eq!(a.zeta(one), a.complex_infinity);
let three = a.int(3);
let z3 = a.zeta(three);
assert!(matches!(a.node(z3), ExprNode::Zeta(_)));
let big = a.int(400);
let zb = a.zeta(big);
assert!(matches!(a.node(zb), ExprNode::Zeta(_)));
}
#[test]
fn eval_si_ci_ei_li_special_points() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
assert_eq!(a.si(a.zero), a.zero);
let si_inf = a.si(a.infinity);
assert_eq!(display(&a, si_inf), "1/2*pi");
let neg_x = a.neg(x);
let si_neg = a.si(neg_x);
let si_x = a.si(x);
assert_eq!(si_neg, a.neg(si_x));
assert_eq!(a.ci(a.infinity), a.zero);
assert_eq!(a.ei(a.neg_infinity), a.zero);
assert_eq!(a.li(a.zero), a.zero);
assert_eq!(a.li(a.one), a.neg_infinity);
let t = sym(&mut a, "t");
let ExprNode::Symbol(sid) = *a.node(t) else {
panic!("symbol expected")
};
let mut real = a.symbol_assumptions(sid);
real.assert_true(crate::base::assumptions::Props::REAL);
a.set_symbol_assumptions(sid, real);
let et = a.exp(t);
let li_et = a.li(et);
let ei_t = a.ei(t);
assert_eq!(li_et, ei_t);
let ex = a.exp(x);
let li_ex = a.li(ex);
assert!(matches!(a.node(li_ex), ExprNode::Li(_)));
let si_x = a.si(x);
let sub = a.subs_structural(si_x, x, a.zero);
assert_eq!(sub, a.zero);
}
#[test]
fn eval_polygamma_and_digamma_values() {
let mut a = Arena::new();
let one = a.one;
let two = a.int(2);
let half = a.rational(1, 2);
let p11 = a.polygamma(one, one);
let z2 = a.zeta(two);
assert_eq!(p11, z2);
let p2h = a.polygamma(two, half);
assert_eq!(display(&a, p2h), "-14*zeta(3)");
let four = a.int(4);
let p14 = a.polygamma(one, four);
assert_eq!(display(&a, p14), "1/6*pi^2 - 49/36");
let d4 = a.digamma(four);
let d4e = eval(&mut a, d4);
assert_eq!(display(&a, d4e), "-EulerGamma + 11/6");
let x = sym(&mut a, "x");
let p0 = a.polygamma(a.zero, x);
assert!(matches!(a.node(p0), ExprNode::Digamma(_)));
let ph = a.polygamma(half, x);
assert!(matches!(a.node(ph), ExprNode::Polygamma(_, _)));
}
#[test]
fn eval_kronecker_delta_values() {
let mut a = Arena::new();
let i = sym(&mut a, "i");
let j = sym(&mut a, "j");
assert_eq!(a.kronecker_delta(i, i), a.one);
let two = a.int(2);
let three = a.int(3);
assert_eq!(a.kronecker_delta(two, three), a.zero);
assert_eq!(a.kronecker_delta(two, two), a.one);
let ip1 = a.add(&[i, a.one]);
assert_eq!(a.kronecker_delta(ip1, i), a.zero);
let d1 = a.kronecker_delta(i, j);
let d2 = a.kronecker_delta(j, i);
assert_eq!(d1, d2, "canonical argument order");
assert!(matches!(a.node(d1), ExprNode::KroneckerDelta(_, _)));
}
#[test]
fn eval_refolds_complex_nodes() {
let mut a = Arena::new();
let z = sym(&mut a, "z");
let re_z = a.re(z);
assert!(matches!(a.node(re_z), ExprNode::Re(_)));
let three = a.int(3);
let four = a.int(4);
let four_i = a.mul(&[a.i_unit, four]);
let w = a.add(&[three, four_i]);
let sub = a.subs_structural(re_z, z, w);
let folded = eval(&mut a, sub);
assert_eq!(folded, three);
}
#[test]
fn explicit_orthogonal_coefficients_match_the_recurrences() {
type Old = fn(&mut Arena, usize, ExprId) -> ExprId;
let families: [(OrthoFamily, Old); 5] = [
(OrthoFamily::Legendre, eval_legendre),
(OrthoFamily::ChebyshevT, eval_chebyshev_t),
(OrthoFamily::ChebyshevU, eval_chebyshev_u),
(OrthoFamily::Hermite, eval_hermite),
(OrthoFamily::Laguerre, eval_laguerre),
];
let mut a = Arena::new();
let x = a.symbol("x");
let pts = [a.rational(1, 3), a.rational(-7, 5), a.int(2)];
for (family, old) in families {
for n in 0..=14 {
let new = ortho_value(&mut a, &family, n, x).unwrap();
let reference = old(&mut a, n, x);
assert_eq!(new, reference, "{family:?} n = {n}");
for &p in &pts {
let new = ortho_value(&mut a, &family, n, p).unwrap();
let reference = old(&mut a, n, p);
let reference = eval(&mut a, reference);
assert_eq!(new, reference, "{family:?} n = {n} at {}", display(&a, p));
}
}
}
}
#[test]
fn tangent_numbers_give_the_bernoulli_numbers() {
let a = Arena::new();
for n in 0..=120u64 {
let fast = exact_bernoulli(&a, n).unwrap();
let slow = crate::base::bernoulli::bernoulli(n as usize);
assert_eq!(fast, slow, "B_{n}");
}
}
}