use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::Zero;
use rustc_hash::FxHashMap;
use astro_float::{BigFloat, Consts, Radix, RoundingMode, Sign, WORD_BIT_SIZE};
use num_complex::Complex64;
use crate::base::arena::Arena;
use crate::base::errors::SymplexError;
use crate::base::libfn::LibFn;
use crate::base::node::{ExprId, ExprNode};
use crate::base::walk;
use tracing::debug;
mod accuracy;
pub(crate) mod bernoulli;
mod bessel_debye;
mod bessel_order;
mod bessel_recur;
mod conjugate;
mod emsum;
mod exact;
mod extended;
mod factorials;
mod hypsum;
mod lambertw;
mod loggamma;
mod polylog;
mod sensitivity;
use crate::base::bigcomplex::Complex;
pub(crate) const QUADRATURE_MAX_DIGITS: u32 = 16;
pub(crate) fn evalf(arena: &Arena, expr: ExprId, digits: u32) -> Result<String, SymplexError> {
evalf_with(arena, expr, digits, ZeroSearch::Number, Underflow::Refuse)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Underflow {
Refuse,
Zero,
}
fn evalf_with(
arena: &Arena,
expr: ExprId,
digits: u32,
search: ZeroSearch,
underflow: Underflow,
) -> Result<String, SymplexError> {
let binary_prec = (digits as usize) * 34 / 10 + 64;
let prec = binary_prec.max(128);
debug!(
digits = digits,
binary_precision = prec,
"evalf: starting numerical evaluation"
);
let max_prec = arena.config.max_evalf_precision as usize;
if prec > max_prec {
return Err(SymplexError::PrecisionExhausted {
requested: digits,
achieved: (max_prec * 10 / 34).saturating_sub(6) as u32,
});
}
let free = walk::free_symbols(arena, expr);
if !free.is_empty() {
let mut names: Vec<&str> = free
.iter()
.filter_map(|&id| match arena.node(id) {
ExprNode::Symbol(sid) => Some(arena.symbol_name(*sid)),
_ => None,
})
.collect();
names.sort_unstable();
if let Some(name) = names.first() {
debug!(symbol = name, "evalf: expression has free symbols");
return Err(SymplexError::FreeSymbol {
name: (*name).to_owned(),
});
}
}
if digits > QUADRATURE_MAX_DIGITS && contains_definite_integral(arena, expr) {
return Err(SymplexError::NotImplemented(format!(
"arbitrary-precision evaluation of a definite integral is not implemented \
({digits} digits requested, at most {QUADRATURE_MAX_DIGITS} available from \
f64 Gauss–Kronrod quadrature); use eval_f64 or integrate_definite"
)));
}
let rm = RoundingMode::ToEven;
let mut cc = Consts::new().map_err(|e| {
SymplexError::NotImplemented(format!("astro-float constants init failed: {e:?}"))
})?;
match evaluate_adaptive(arena, expr, digits, search, rm, &mut cc)? {
Adaptive::Settled(result, _) => format_complex(&result, digits, prec, rm, &mut cc),
Adaptive::Underflow(_)
| Adaptive::Beyond(_)
| Adaptive::Undecided
| Adaptive::Hidden { .. }
if underflow == Underflow::Zero =>
{
Ok("0".to_owned())
}
Adaptive::Underflow(_) | Adaptive::Beyond(_) => Err(underflow_error()),
Adaptive::Undecided => Err(undecided_error()),
Adaptive::Hidden { below_range: true } => Err(hidden_error()),
Adaptive::Hidden { below_range: false } => Err(hidden_deep_error(digits)),
}
}
fn working_precision(digits: u32) -> usize {
((digits as usize) * 34 / 10 + 64).max(128)
}
fn evaluate_once(
arena: &Arena,
expr: ExprId,
post_order: &[ExprId],
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Evaluated, SymplexError> {
evaluate_tree_full(arena, expr, post_order, None, false, prec, rm, cc)
}
struct Evaluated {
value: Complex,
bound: accuracy::Bound,
precision_limited: bool,
underflowed_nonzero: Option<accuracy::Nonzero>,
beyond: Option<extended::Scaled>,
absorbed: Option<accuracy::Absorbed>,
exact_bits: u64,
second_bits: u64,
}
type ErrMap = FxHashMap<ExprId, accuracy::Bound>;
type Seed = (ExprId, Complex, accuracy::Bound);
#[allow(clippy::too_many_arguments)]
fn evaluate_tree(
arena: &Arena,
root: ExprId,
post_order: &[ExprId],
seed: Option<Seed>,
strict: bool,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(Complex, accuracy::Bound), SymplexError> {
evaluate_tree_full(arena, root, post_order, seed, strict, prec, rm, cc)
.map(|ev| (ev.value, ev.bound))
}
#[allow(clippy::too_many_arguments)]
fn evaluate_tree_full(
arena: &Arena,
root: ExprId,
post_order: &[ExprId],
seed: Option<Seed>,
strict: bool,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Evaluated, SymplexError> {
let mut cache: FxHashMap<ExprId, Complex> = FxHashMap::default();
let mut errs: ErrMap = FxHashMap::default();
let mut failed: FxHashMap<ExprId, SymplexError> = FxHashMap::default();
let mut exact_values = exact::ExactMap::default();
let mut limited = false;
let mut tainted: rustc_hash::FxHashSet<ExprId> = rustc_hash::FxHashSet::default();
let mut sigs = conjugate::Sigs::new();
let mut nonzero: FxHashMap<ExprId, accuracy::Nonzero> = FxHashMap::default();
let mut exts = extended::ExtMap::default();
let mut absorbed: Option<accuracy::Absorbed> = None;
let mut exact_bits = 0u64;
let mut second_bits = 0u64;
if let Some((id, value, err)) = seed {
sigs.record_leaf(id, &value, err);
cache.insert(id, value);
errs.insert(id, err);
}
for &id in post_order {
if cache.contains_key(&id) || (id != root && is_condition(arena.node(id))) {
continue;
}
let exact_z = exact::exact_value(arena, id, &exact_values);
match (&exact_z, arena.node(id)) {
(Some(z), _) => {
exact_bits = exact_bits.max(input_bits(&z.re)).max(input_bits(&z.im));
second_bits = second_bits
.max(second_order_bits(&z.re, arena))
.max(second_order_bits(&z.im, arena));
}
(None, ExprNode::Num(n)) => {
let q = arena.num(*n);
exact_bits = exact_bits.max(input_bits(q));
second_bits = second_bits.max(second_order_bits(q, arena));
}
_ => {}
}
let mut overflowed = false;
let mut evaluated =
eval_node_with_error(arena, id, &cache, &errs, prec, rm, cc).and_then(|(value, e)| {
match exponent_range_error(arena, id, &value, &cache, &errs) {
Some(err) => {
overflowed = true;
Err(err)
}
None => Ok((value, e)),
}
});
let mut extended_value = false;
if exact_z.is_none() {
let underflowed = matches!(&evaluated, Ok((v, b))
if accuracy::mag(v).is_none() && accuracy::is_underflow(b.joint()));
let edge = extended::Edge {
underflowed,
overflowed,
};
if extended::wanted(arena, id, edge, &cache, &exts)
&& let Some(outcome) = extended::extend(
arena,
id,
edge,
&cache,
&errs,
&exts,
&exact_values,
prec,
rm,
cc,
&mut absorbed,
)
{
extended_value = true;
evaluated = match outcome {
extended::Outcome::InRange(v, b) => Ok((v, b)),
extended::Outcome::Beyond(s) => {
let placeholder = s.placeholder(prec);
exts.insert(id, extended::Ext::Beyond(s));
Ok(placeholder)
}
extended::Outcome::Above(s) => {
let err = match evaluated {
Err(e) => e,
Ok(_) => extended::overflow_error(&s),
};
exts.insert(id, extended::Ext::Above(s));
Err(err)
}
extended::Outcome::Near(p, d) => {
let ordinary = near_ordinary(evaluated, &p, &d, prec, rm);
exts.insert(id, extended::Ext::Near(p, d));
Ok(ordinary)
}
};
}
}
let evaluated = match (exact_z, evaluated) {
(Some(z), evaluated) => {
let refined = match evaluated {
Ok((value, e)) => exact::refine(&z, value, e, prec, rm),
Err(_) => exact::to_value(&z, prec, rm),
};
exact_values.insert(id, z);
Ok(refined)
}
(None, evaluated) => evaluated,
};
let tainted_child = || {
arena
.node(id)
.children()
.iter()
.any(|c| tainted.contains(c))
};
let evaluated = match evaluated {
Err(SymplexError::Unevaluable { .. }) if tainted_child() => {
Ok((c_zero(prec), accuracy::Bound::UNKNOWN))
}
other => other,
};
match evaluated {
Ok((mut value, mut e)) => {
if precision_limited(arena.node(id), &value, e, &cache, &errs)
|| (e.is_unknown() && tainted_child())
{
limited = true;
tainted.insert(id);
}
if let ExprNode::Add(children) | ExprNode::Mul(children) = arena.node(id)
&& !e.is_unknown()
&& !accuracy::exact_zero(&value.1, e.im)
&& accuracy::part_contains_zero(&value.1, e.im)
&& sigs.conjugates_pair_up(children, &cache, &errs)
{
value.1 = BigFloat::new(prec);
e.im = accuracy::EXACT;
}
sigs.record(arena, id, &value, e, &cache, &errs);
if let ExprNode::Add(children) = arena.node(id)
&& !exact_values.contains_key(&id)
&& !matches!(exts.get(&id), Some(extended::Ext::Beyond(_)))
{
note_absorbed(
children,
e.joint(),
&cache,
&errs,
&exts,
&nonzero,
&mut absorbed,
);
}
if !exts.contains_key(&id) {
note_series_tail(arena.node(id), e.joint(), &cache, &errs, &mut absorbed);
note_limit_tail(
arena,
arena.node(id),
e.joint(),
&cache,
&errs,
&mut absorbed,
);
note_cross_terms(
arena,
id,
&value,
e.joint(),
&cache,
&errs,
&exact_values,
&mut absorbed,
);
}
if !extended_value && !exact_values.contains_key(&id) {
note_flattened(arena.node(id), e.joint(), &exts, &nonzero, &mut absorbed);
}
if accuracy::mag(&value).is_none() && accuracy::is_underflow(e.joint()) {
let status = match exts.get(&id) {
Some(extended::Ext::Beyond(s)) => s.nonzero(),
_ => accuracy::underflow_nonzero(arena, id, &cache, &errs, &nonzero),
};
if let Some(s) = status {
nonzero.insert(id, s);
}
}
errs.insert(id, e);
cache.insert(id, value);
}
Err(e) => {
let e = root_cause(arena.node(id), e, &failed);
let above = matches!(exts.get(&id), Some(extended::Ext::Above(_)));
if (strict && !above) || id == root {
return Err(e);
}
failed.insert(id, e);
}
}
}
let value = cache.remove(&root).ok_or_else(|| {
SymplexError::NotImplemented("evalf: expression not found in cache".into())
})?;
let err = errs.get(&root).copied().unwrap_or(accuracy::Bound::UNKNOWN);
let beyond = match exts.remove(&root) {
Some(extended::Ext::Beyond(s)) => Some(s),
_ => None,
};
if let Some(s) = &beyond {
extended::note_hidden(s, prec, rm, &mut absorbed);
}
Ok(Evaluated {
value,
bound: err,
precision_limited: limited,
underflowed_nonzero: nonzero.get(&root).copied(),
beyond,
absorbed,
exact_bits,
second_bits,
})
}
fn near_ordinary(
evaluated: Result<(Complex, accuracy::Bound), SymplexError>,
p: &exact::Gauss,
d: &extended::Scaled,
prec: usize,
rm: RoundingMode,
) -> (Complex, accuracy::Bound) {
let (pv, pb) = exact::to_value(p, prec, rm);
if let Ok((v, b)) = evaluated
&& !b.is_unknown()
&& accuracy::mag(&v).is_some()
{
let diff = c_sub(&v, &pv, prec + 8, rm);
let within = |x: &BigFloat, e: accuracy::ErrExp, f: accuracy::ErrExp| {
accuracy::part_contains_zero(x, accuracy::shift(accuracy::lsum(e, f), 1.0))
};
if within(&diff.0, b.re, pb.re) && within(&diff.1, b.im, pb.im) {
return (v, b);
}
}
let u = accuracy::UNDERFLOW;
let real = accuracy::exactly_real(&d.m, d.eb);
let b = accuracy::Bound {
re: accuracy::lsum(pb.re, u),
im: if real {
pb.im
} else {
accuracy::lsum(pb.im, u)
},
};
(pv, b)
}
fn note_absorbed(
children: &[ExprId],
radius: accuracy::ErrExp,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
exts: &extended::ExtMap,
nonzero: &FxHashMap<ExprId, accuracy::Nonzero>,
slot: &mut Option<accuracy::Absorbed>,
) {
if !radius.is_finite() {
return;
}
for c in children {
let term = match exts.get(c) {
Some(extended::Ext::Beyond(s)) => {
if s.nonzero().is_none() {
continue;
}
s.lg_value()
}
_ => {
let (Some(v), Some(b)) = (cache.get(c), errs.get(c)) else {
continue;
};
if !b.is_unknown() && !accuracy::contains_zero(v, b.joint()) {
accuracy::lg_abs(v)
} else if nonzero.contains_key(c) {
accuracy::UNDERFLOW
} else {
continue;
}
}
};
accuracy::Absorbed::note(slot, term, radius);
}
}
fn note_flattened(
node: &ExprNode,
radius: accuracy::ErrExp,
exts: &extended::ExtMap,
nonzero: &FxHashMap<ExprId, accuracy::Nonzero>,
slot: &mut Option<accuracy::Absorbed>,
) {
if !radius.is_finite() {
return;
}
for c in node.children().iter() {
let term = match exts.get(c) {
Some(extended::Ext::Beyond(s)) => {
if s.nonzero().is_none() {
continue;
}
s.lg_value()
}
Some(extended::Ext::Near(_, d)) => {
if d.nonzero().is_none() {
continue;
}
d.lg_value()
}
None if nonzero.contains_key(c) => accuracy::UNDERFLOW,
Some(extended::Ext::Above(_)) | None => continue,
};
accuracy::Absorbed::note(slot, term, radius);
}
}
fn note_series_tail(
node: &ExprNode,
radius: accuracy::ErrExp,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
slot: &mut Option<accuracy::Absorbed>,
) {
const THIRD: f64 = -1.584_962_500_721_156; const SIXTH: f64 = -2.584_962_500_721_156; const TWO_FIFTEENTHS: f64 = -2.906_890_595_608_519; const ONE_120TH: f64 = -6.906_890_595_608_519; let (c, terms): (ExprId, &[(f64, f64)]) = match *node {
ExprNode::Sin(c) | ExprNode::Sinh(c) | ExprNode::Asin(c) | ExprNode::Asinh(c) => {
(c, &[(SIXTH, 3.0), (ONE_120TH, 5.0)])
}
ExprNode::Tan(c) | ExprNode::Tanh(c) | ExprNode::Atan(c) | ExprNode::Atanh(c) => {
(c, &[(THIRD, 3.0), (TWO_FIFTEENTHS, 5.0)])
}
ExprNode::Erf(c) => (
c,
&[
(-1.410_710_565_457_315_5, 3.0),
(-3.147_675_390_659_634, 5.0),
],
),
ExprNode::Cos(c) | ExprNode::Cosh(c) => (c, &[(-1.0, 2.0), (-4.584_962_500_721_156, 4.0)]),
ExprNode::Exp(c) => (c, &[(0.0, 1.0), (-1.0, 2.0), (SIXTH, 3.0)]),
_ => return,
};
if !radius.is_finite() {
return;
}
let (Some(x), Some(b)) = (cache.get(&c), errs.get(&c)) else {
return;
};
if b.is_unknown() || accuracy::contains_zero(x, b.joint()) {
return;
}
let lx = accuracy::lg_abs(x);
if lx.is_nan() || lx >= -8.0 {
return;
}
if let Some(t) = terms
.iter()
.map(|&(lc, n)| lc + n * lx)
.find(|&t| t < radius)
{
accuracy::Absorbed::note(slot, t, radius);
}
}
fn note_limit_tail(
arena: &Arena,
node: &ExprNode,
radius: accuracy::ErrExp,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
slot: &mut Option<accuracy::Absorbed>,
) {
if !radius.is_finite() {
return;
}
let real = |c: ExprId| -> Option<(f64, f64, bool)> {
let (v, b) = (cache.get(&c)?, errs.get(&c)?);
if b.is_unknown() || !accuracy::exactly_real(v, *b) || v.0.is_zero() {
return None;
}
let lx = accuracy::lg_abs(v);
let far = accuracy::lsum(lx, b.re + 1.0);
(lx.is_finite() && far.is_finite()).then_some((lx, far, v.0.is_negative()))
};
let e2 = |l: f64| l.exp2();
const LOG2_E: f64 = std::f64::consts::LOG2_E;
let tail = || -> Option<f64> {
Some(match *node {
ExprNode::Tanh(c) => {
let (lx, far, _) = real(c)?;
(lx >= 0.0).then_some(())?;
-2.0 * e2(far) * LOG2_E
}
ExprNode::Atan(c) => {
let (lx, far, _) = real(c)?;
(lx >= 0.0).then_some(())?;
-1.0 - far
}
ExprNode::Erf(c) | ExprNode::Erfc(c) => {
let (lx, far, negative) = real(c)?;
(lx >= 0.0 && (negative || matches!(node, ExprNode::Erf(_)))).then_some(())?;
-e2(2.0 * far) * LOG2_E - 1.83 - far
}
ExprNode::Asinh(c) | ExprNode::Acosh(c) => {
let (lx, far, negative) = real(c)?;
(lx >= 1.0 && !(negative && matches!(node, ExprNode::Acosh(_)))).then_some(())?;
-3.0 - 2.0 * far
}
ExprNode::Zeta(c) => {
let (lx, far, negative) = real(c)?;
(!negative && lx >= 1.0).then_some(())?;
-e2(far)
}
ExprNode::Apply(sid, ref args) => match (arena.lib_fn(sid), args.as_slice()) {
(Some(LibFn::DirichletEta), &[s]) => {
let (ls, far, negative) = real(s)?;
(!negative && ls >= 1.0).then_some(())?;
-e2(far) - 1.0
}
(Some(LibFn::PolyLog), &[s, z]) => {
let (ls, _, negative) = real(s)?;
(!negative && ls >= 2.0).then_some(())?;
polylog_tail(
cache.get(&s)?,
*errs.get(&s)?,
cache.get(&z)?,
*errs.get(&z)?,
radius,
)
}
_ => return None,
},
_ => return None,
})
};
if let Some(t) = tail()
&& !t.is_nan()
{
accuracy::Absorbed::note(slot, t.max(-1e300), radius);
}
}
fn polylog_tail(
s: &Complex,
bs: accuracy::Bound,
z: &Complex,
bz: accuracy::Bound,
radius: accuracy::ErrExp,
) -> f64 {
let (Ok(sf), Ok(x), Ok(y)) = (
bigfloat_to_f64_rounded(&s.0, RoundingMode::ToEven),
bigfloat_to_f64_rounded(&z.0, RoundingMode::ToEven),
bigfloat_to_f64_rounded(&z.1, RoundingMode::ToEven),
) else {
return -1e300;
};
let estimate = 2.0 * x.hypot(y).log2() - sf - 1.0;
match polylog_tail_proved(sf, s, bs, x, y, bz) {
Some(t) => t,
None if estimate - 64.0 >= radius => estimate - 64.0,
None => -1e300,
}
}
fn polylog_tail_proved(
sf: f64,
s: &Complex,
bs: accuracy::Bound,
x: f64,
y: f64,
bz: accuracy::Bound,
) -> Option<f64> {
const UNKNOWN_TAIL: f64 = -1e300;
let rz = x.hypot(y);
let rel_s = if bs.is_exact() {
0.0
} else {
bs.joint().exp2() / sf.max(1.0)
};
let rel_z = if bz.is_exact() {
0.0
} else {
bz.joint().exp2() / rz
};
if rel_s.is_nan() || rel_z.is_nan() || rel_s > 1e-6 || rel_z > 1e-6 {
return None;
}
let s_up = sf * (1.0 + rel_s) + rel_s;
let s_int = s.0.is_int() && bs.is_exact();
let near_int = {
let d = (sf - sf.round()).abs();
d < 1.0 / 256.0 + rel_s * sf.max(1.0)
};
let shrink = 1.0 - 2.0 * rel_z;
let grow = 2.0 * rel_z;
let outside = rz > 1.0;
if (outside && rz * shrink <= 1.0) || (!outside && rz * (1.0 + grow) > 1.0) {
return None;
}
let near = near_int && !s_int;
let t = polylog::tail_lower_bound(s_up, s_int, near, x * shrink, y * shrink);
if t <= UNKNOWN_TAIL {
return None;
}
if !outside {
return Some(t);
}
let t2 = polylog::tail_lower_bound(
s_up,
s_int,
near,
x * (1.0 + grow),
y * (1.0 + grow) + grow * rz,
);
(t2 > UNKNOWN_TAIL).then_some(t)
}
fn small_part(
arena: &Arena,
id: ExprId,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
exact: &exact::ExactMap,
) -> Option<f64> {
let ExprNode::Add(children) = arena.node(id) else {
return match exact::operand(arena, id, exact) {
Some(z) if z.im.is_zero() && input_bits(&z.re) >= 64 => rational_small_part(&z.re),
_ => None,
};
};
let total = cache.get(&id)?;
let tb = errs.get(&id)?;
if tb.is_unknown() || accuracy::contains_zero(total, tb.joint()) {
return None;
}
let smallest = children
.iter()
.filter_map(|c| {
let (v, b) = (cache.get(c)?, errs.get(c)?);
(!b.is_unknown() && !accuracy::contains_zero(v, b.joint())).then(|| accuracy::lg_abs(v))
})
.fold(f64::INFINITY, f64::min);
let ratio = smallest - accuracy::lg_abs_low(total);
ratio.is_finite().then_some(ratio)
}
fn lg_int(n: &BigInt) -> f64 {
use num_traits::{Signed, ToPrimitive};
match n.abs().to_f64() {
Some(f) if f.is_finite() && f > 0.0 => f.log2(),
_ => n.bits() as f64,
}
}
fn integer_small_part(n: &BigInt) -> Option<f64> {
use num_integer::Integer;
use num_traits::Signed;
let a = n.abs();
let bits = a.bits();
if bits < 128 {
return None;
}
let split = |g: &BigInt| -> Option<Option<f64>> {
let (quot, rem) = a.div_mod_floor(g);
let up = &rem + &rem >= *g;
let u = if up { (quot + 1u32) * g } else { quot * g };
let h = &a - &u;
if h.is_zero() {
return Some(None);
}
(lg_int(&h) < lg_int(g) - 64.0).then(|| Some(lg_int(&h) - lg_int(&u)))
};
for k in 1..=64u64 {
let g = BigInt::from(1u32) << (bits - k);
if let Some(found) = split(&g) {
return found;
}
}
let digits = (bits as f64 * std::f64::consts::LOG10_2).floor() as u32 + 1;
for k in 1..=8u32 {
let Some(e) = digits.checked_sub(k).filter(|&e| e > 0) else {
break;
};
if let Some(found) = split(&BigInt::from(10u32).pow(e)) {
return found;
}
}
None
}
fn rational_small_part(x: &crate::base::numeric::Q) -> Option<f64> {
use num_integer::Integer;
if x.is_zero() {
return None;
}
let digits = [x.numer(), x.denom()]
.into_iter()
.filter_map(integer_small_part)
.fold(None, |acc: Option<f64>, r| {
Some(acc.map_or(r, |a| a.max(r)))
});
let lg = lg_int;
let (mut n, mut d) = (x.numer().clone(), x.denom().clone());
let (mut p, mut q) = (BigInt::from(1), BigInt::from(0));
let (mut p_prev, mut q_prev) = (BigInt::from(0), BigInt::from(1));
for step in 0..64 {
if d.is_zero() {
return digits;
}
let (a, r) = n.div_mod_floor(&d);
if step > 0 && a.bits() >= 16 && !p.is_zero() {
let lg_u = lg(&p) - lg(&q);
let lg_h = -(2.0 * lg(&q) + lg(&a));
let cf = lg_h - lg_u;
return Some(digits.map_or(cf, |d| d.max(cf)));
}
let next_p = &a * &p + &p_prev;
let next_q = &a * &q + &q_prev;
p_prev = std::mem::replace(&mut p, next_p);
q_prev = std::mem::replace(&mut q, next_q);
n = std::mem::replace(&mut d, r);
}
digits
}
#[allow(clippy::too_many_arguments)]
fn note_cross_terms(
arena: &Arena,
id: ExprId,
value: &Complex,
radius: accuracy::ErrExp,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
exact: &exact::ExactMap,
slot: &mut Option<accuracy::Absorbed>,
) {
if !radius.is_finite() {
return;
}
let lv = accuracy::lg_abs(value);
if !lv.is_finite() {
return;
}
let lost = match arena.node(id) {
ExprNode::Pow(b, e) => {
let Some(q) = arena
.as_num(*e)
.and_then(crate::base::numeric::ratio_to_f64)
else {
return;
};
let Some(r) = small_part(arena, *b, cache, errs, exact) else {
return;
};
let c = (q * (q - 1.0) / 2.0).abs();
if c == 0.0 || r > -8.0 {
return;
}
lv + c.log2() + 2.0 * r
}
ExprNode::Mul(children) => {
let mut parts: Vec<f64> = children
.iter()
.filter_map(|&c| small_part(arena, c, cache, errs, exact))
.filter(|&r| r <= -8.0)
.collect();
if parts.len() < 2 {
return;
}
parts.sort_by(f64::total_cmp);
lv + parts[parts.len() - 1] + parts[parts.len() - 2]
}
ExprNode::Sin(c)
| ExprNode::Cos(c)
| ExprNode::Tan(c)
| ExprNode::Exp(c)
| ExprNode::Ln(c)
| ExprNode::Asin(c)
| ExprNode::Acos(c)
| ExprNode::Atan(c)
| ExprNode::Sinh(c)
| ExprNode::Cosh(c)
| ExprNode::Tanh(c)
| ExprNode::Asinh(c)
| ExprNode::Acosh(c)
| ExprNode::Atanh(c)
| ExprNode::Gamma(c)
| ExprNode::LogGamma(c)
| ExprNode::Digamma(c)
| ExprNode::Erf(c)
| ExprNode::Erfc(c)
| ExprNode::LambertW(c)
| ExprNode::Si(c)
| ExprNode::Ci(c)
| ExprNode::Ei(c)
| ExprNode::Li(c)
| ExprNode::Zeta(c)
| ExprNode::Factorial(c) => {
let Some(r) = small_part(arena, *c, cache, errs, exact) else {
return;
};
if r > -8.0 {
return;
}
lv.max(0.0) + 2.0 * r
}
_ => return,
};
accuracy::Absorbed::note(slot, lost, radius);
}
fn precision_limited(
node: &ExprNode,
value: &Complex,
e: accuracy::Bound,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
) -> bool {
let finite = |z: &Complex| !(z.0.is_inf() || z.0.is_nan() || z.1.is_inf() || z.1.is_nan());
if !e.is_unknown() && finite(value) {
return false;
}
let mut inexact = false;
for c in node.children() {
let (Some(v), Some(b)) = (cache.get(&c), errs.get(&c)) else {
continue;
};
if b.is_unknown() || !finite(v) || accuracy::is_underflow(b.joint()) {
return false;
}
inexact |= !b.is_exact();
}
inexact
}
fn exponent_range_error(
arena: &Arena,
id: ExprId,
value: &Complex,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
) -> Option<SymplexError> {
if !(value.0.is_inf() || value.1.is_inf()) {
return None;
}
let node = arena.node(id);
let what = match node {
ExprNode::Exp(_) => "exp",
ExprNode::Pow(..) => "a power",
ExprNode::Mul(_) => "a product",
ExprNode::Add(_) => "a sum",
ExprNode::Gamma(_) => "Gamma",
ExprNode::Factorial(_) => "factorial",
ExprNode::Binomial(..) => "binomial",
ExprNode::Beta(..) => "beta",
ExprNode::Sinh(_) => "sinh",
ExprNode::Cosh(_) => "cosh",
ExprNode::Ei(_) => "Ei",
ExprNode::Apply(sid, _) => match arena.lib_fn(*sid) {
Some(LibFn::Erfi) => "erfi",
Some(LibFn::Shi) => "Shi",
Some(LibFn::Chi) => "Chi",
Some(LibFn::BesselI) => "besseli",
Some(LibFn::AiryBi) => "airybi",
Some(LibFn::AiryBiPrime) => "airybiprime",
Some(LibFn::RisingFactorial) => "rising_factorial",
Some(LibFn::Subfactorial) => "subfactorial",
_ => return None,
},
_ => return None,
};
let mut lg_sum = 0.0f64;
for c in node.children() {
let Some(v) = cache.get(&c) else {
continue;
};
if v.0.is_inf() || v.0.is_nan() || v.1.is_inf() || v.1.is_nan() {
return None;
}
if accuracy::mag(v).is_none() {
let b = errs.get(&c).copied().unwrap_or(accuracy::Bound::UNKNOWN);
if b.is_exact() {
continue;
}
let underflow = accuracy::is_underflow(b.joint());
return underflow.then(|| SymplexError::Unevaluable {
reason: format!(
"{what}: an argument underflows the arbitrary-precision exponent range \
(below 2^{:e}), and its reciprocal overflows it",
f64::from(astro_float::EXPONENT_MIN)
),
});
}
lg_sum += accuracy::lg_abs(v);
}
let lg = match node {
ExprNode::Exp(a) => cache
.get(a)
.and_then(|v| bigfloat_to_f64_rounded(&v.0, RoundingMode::ToEven).ok())
.map(|x| x * std::f64::consts::LOG2_E),
ExprNode::Gamma(a) | ExprNode::Factorial(a) => cache
.get(a)
.and_then(|v| bigfloat_to_f64_rounded(&v.0, RoundingMode::ToEven).ok())
.filter(|x| *x > 1.0)
.map(|x| {
let x = if matches!(node, ExprNode::Factorial(_)) {
x + 1.0
} else {
x
};
((x - 0.5) * x.ln() - x) * std::f64::consts::LOG2_E
}),
ExprNode::Mul(_) => Some(lg_sum),
_ => None,
}
.filter(|lg| lg.is_finite() && *lg > f64::from(astro_float::EXPONENT_MAX) / 2.0);
Some(SymplexError::Unevaluable {
reason: match lg {
Some(lg) => format!(
"{what}: the value (about 2^{lg:.3e}) overflows the arbitrary-precision exponent range"
),
None => format!("{what}: the value overflows the arbitrary-precision exponent range"),
},
})
}
fn root_cause(
node: &ExprNode,
e: SymplexError,
failed: &FxHashMap<ExprId, SymplexError>,
) -> SymplexError {
if let SymplexError::Unevaluable { reason } = &e {
for c in node.children() {
if let Some(child_error) = failed.get(&c)
&& *reason == not_evaluated_reason(c)
{
return duplicate_error(child_error);
}
}
}
e
}
fn duplicate_error(e: &SymplexError) -> SymplexError {
match e {
SymplexError::PrecisionExhausted {
requested,
achieved,
} => SymplexError::PrecisionExhausted {
requested: *requested,
achieved: *achieved,
},
SymplexError::FreeSymbol { name } => SymplexError::FreeSymbol { name: name.clone() },
SymplexError::Unevaluable { reason } => SymplexError::Unevaluable {
reason: reason.clone(),
},
SymplexError::ComputationFailed { operation, reason } => SymplexError::ComputationFailed {
operation,
reason: reason.clone(),
},
SymplexError::NotImplemented(s) => SymplexError::NotImplemented(s.clone()),
SymplexError::Divergent { operation, reason } => SymplexError::Divergent {
operation,
reason: reason.clone(),
},
other => SymplexError::Unevaluable {
reason: other.to_string(),
},
}
}
fn is_condition(node: &ExprNode) -> bool {
matches!(
node,
ExprNode::BoolTrue
| ExprNode::BoolFalse
| ExprNode::Gt(_, _)
| ExprNode::Ge(_, _)
| ExprNode::Eq_(_, _)
| ExprNode::Ne(_, _)
| ExprNode::And(_)
| ExprNode::Or(_)
| ExprNode::Not(_)
)
}
fn eval_node_with_error(
arena: &Arena,
id: ExprId,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(Complex, accuracy::Bound), SymplexError> {
let (value, err) = match arena.node(id) {
ExprNode::Add(children) | ExprNode::Mul(children) => {
let mut parts: Vec<(&Complex, accuracy::Bound)> = Vec::with_capacity(children.len());
for &c in children.iter() {
let v = get_cached(cache, c)?;
parts.push((v, errs.get(&c).copied().unwrap_or(accuracy::Bound::UNKNOWN)));
}
return Ok(if matches!(arena.node(id), ExprNode::Add(_)) {
accuracy::add_with_bound(&parts, prec, rm)
} else {
accuracy::mul_with_bound(&parts, prec, rm)
});
}
ExprNode::Sum(body, var, lo, hi) => {
eval_sum(arena, *body, *var, *lo, *hi, cache, prec, rm, cc)?
}
ExprNode::Product_(body, var, lo, hi) => {
eval_product(arena, *body, *var, *lo, *hi, cache, prec, rm, cc)?
}
ExprNode::Piecewise(pairs) => eval_piecewise(arena, pairs, cache, errs, prec, rm, cc)?,
ExprNode::RootOf(poly, var, index) => eval_rootof(arena, *poly, *var, *index, prec)?,
ExprNode::RootSum(poly, body, sumvar) => {
eval_rootsum(arena, *poly, *body, *sumvar, prec, rm, cc)?
}
ExprNode::DefiniteIntegral(body, var, lo, hi) => {
eval_definite_integral(arena, *body, *var, *lo, *hi, cache, errs, prec)?
}
node if inverse_identity(arena, node, cache, errs, prec, rm).is_some() => {
let u = inverse_identity(arena, node, cache, errs, prec, rm).unwrap_or(id);
return Ok((
get_cached(cache, u)?.clone(),
errs.get(&u).copied().unwrap_or(accuracy::Bound::UNKNOWN),
));
}
node if lambertw::lambert_parts(arena, node).is_some() => {
let value = match eval_node(arena, id, cache, prec, rm, cc) {
Ok(value) => value,
Err(SymplexError::Unevaluable { .. }) if inexact_zero_child(node, cache, errs) => {
return Ok((c_zero(prec), accuracy::Bound::UNKNOWN));
}
Err(e) => return Err(e),
};
let err = match lambertw::lambert_parts(arena, node) {
Some(Ok((x, k))) => {
let bz = errs.get(&x).copied().unwrap_or(accuracy::Bound::UNKNOWN);
lambertw::error_bound(get_cached(cache, x)?, bz, &value, k, prec)
}
_ => accuracy::Bound::UNKNOWN,
};
(value, err)
}
ExprNode::LogGamma(c) if cache.get(c).is_some_and(|v| !v.1.is_zero()) => {
let value = eval_node(arena, id, cache, prec, rm, cc)?;
let bz = errs.get(c).copied().unwrap_or(accuracy::Bound::UNKNOWN);
let err = loggamma::error_bound(get_cached(cache, *c)?, bz, &value, prec);
(value, err)
}
node @ ExprNode::Apply(_, args) if polylog::is_general(arena, node, cache) => {
let value = match eval_node(arena, id, cache, prec, rm, cc) {
Ok(value) => value,
Err(SymplexError::Unevaluable { .. })
if inexact_zero_child(node, cache, errs)
|| rounded_onto_pole(arena, node, cache, errs) =>
{
return Ok((c_zero(prec), accuracy::Bound::UNKNOWN));
}
Err(e) => return Err(e),
};
let bound = |c: ExprId| errs.get(&c).copied().unwrap_or(accuracy::Bound::UNKNOWN);
let err = polylog::error_bound(
&get_cached(cache, args[0])?.0,
bound(args[0]),
get_cached(cache, args[1])?,
bound(args[1]),
&value,
prec,
rm,
cc,
);
(value, err)
}
node @ ExprNode::Apply(_, args) if polylog::is_complex_order(arena, node, cache) => {
let value = eval_node(arena, id, cache, prec, rm, cc)?;
let bound = |c: ExprId| errs.get(&c).copied().unwrap_or(accuracy::Bound::UNKNOWN);
let err = polylog::complex_order_bound(
get_cached(cache, args[0])?,
bound(args[0]),
get_cached(cache, args[1])?,
bound(args[1]),
&value,
prec,
);
(value, err)
}
ExprNode::PhysicalConstant(_, value_id) => match cache.get(value_id) {
Some(v) => (
v.clone(),
errs.get(value_id)
.copied()
.unwrap_or(accuracy::Bound::UNKNOWN),
),
None => {
let order = walk::post_order_ids(arena, *value_id);
evaluate_tree(arena, *value_id, &order, None, true, prec, rm, cc)?
}
},
_ => {
let value = match eval_node(arena, id, cache, prec, rm, cc) {
Ok(value) => value,
Err(SymplexError::Unevaluable { .. })
if inexact_zero_child(arena.node(id), cache, errs)
|| rounded_onto_pole(arena, arena.node(id), cache, errs) =>
{
return Ok((c_zero(prec), accuracy::Bound::UNKNOWN));
}
Err(e) => return Err(e),
};
let err = accuracy::node_error(arena, id, &value, cache, errs, prec, || {
sensitivity::special_error(arena, id, &value, cache, errs, prec, rm, cc)
});
return Ok((value, err));
}
};
let err = accuracy::reported(&value, err, prec);
Ok((value, err))
}
fn inverse_identity(
arena: &Arena,
node: &ExprNode,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
prec: usize,
rm: RoundingMode,
) -> Option<ExprId> {
let (outer, inner) = match node {
ExprNode::Sin(a)
| ExprNode::Cos(a)
| ExprNode::Tan(a)
| ExprNode::Sinh(a)
| ExprNode::Cosh(a)
| ExprNode::Tanh(a)
| ExprNode::Exp(a) => (node, *a),
_ => return None,
};
let u = match (outer, arena.node(inner)) {
(ExprNode::Sin(_), ExprNode::Asin(u))
| (ExprNode::Cos(_), ExprNode::Acos(u))
| (ExprNode::Tan(_), ExprNode::Atan(u))
| (ExprNode::Sinh(_), ExprNode::Asinh(u))
| (ExprNode::Cosh(_), ExprNode::Acosh(u))
| (ExprNode::Tanh(_), ExprNode::Atanh(u))
| (ExprNode::Exp(_), ExprNode::Ln(u)) => *u,
_ => return None,
};
if !cache.contains_key(&inner) {
return None;
}
let uv = cache.get(&u)?;
let singular: &[(i32, i32)] = match outer {
ExprNode::Exp(_) => &[(0, 0)],
ExprNode::Tan(_) => &[(0, 1), (0, -1)],
ExprNode::Tanh(_) => &[(1, 0), (-1, 0)],
_ => &[],
};
if !singular.is_empty() {
let err = errs
.get(&u)
.copied()
.unwrap_or(accuracy::Bound::UNKNOWN)
.joint();
if accuracy::is_unknown(err) {
return None;
}
for &(a, b) in singular {
let d = (
uv.0.sub(&BigFloat::from_i32(a, prec), prec, rm),
uv.1.sub(&BigFloat::from_i32(b, prec), prec, rm),
);
if accuracy::contains_zero(&d, accuracy::shift(err, 1.0)) {
return None;
}
}
}
Some(u)
}
fn rounded_onto_pole(
arena: &Arena,
node: &ExprNode,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
) -> bool {
if let ExprNode::Apply(sid, _) = node {
let args = node.children();
let at = |k: usize, points: &[i32]| -> bool {
args.get(k).is_some_and(|c| {
let inexact = !errs
.get(c)
.copied()
.unwrap_or(accuracy::Bound::UNKNOWN)
.is_exact();
inexact
&& cache.get(c).is_some_and(|v| {
v.1.is_zero()
&& points
.iter()
.any(|&p| v.0.cmp(&BigFloat::from_i32(p, 64)) == Some(0))
})
})
};
match arena.lib_fn(*sid) {
Some(LibFn::PolyLog) => return at(1, &[1]),
Some(LibFn::EllipticK) => return at(0, &[1]),
Some(LibFn::EllipticPi) => return at(0, &[1]) || at(1, &[1]),
Some(LibFn::ErfcInv) => return at(0, &[2]),
Some(LibFn::ErfInv) => return at(0, &[-1, 1]),
_ => {}
}
}
let poles = match node {
ExprNode::Gamma(_)
| ExprNode::LogGamma(_)
| ExprNode::Digamma(_)
| ExprNode::Polygamma(_, _)
| ExprNode::Zeta(_)
| ExprNode::Factorial(_)
| ExprNode::Beta(_, _)
| ExprNode::Binomial(_, _) => true,
ExprNode::Apply(sid, _) => matches!(
arena.lib_fn(*sid),
Some(LibFn::RisingFactorial | LibFn::FallingFactorial | LibFn::Harmonic)
),
_ => false,
};
poles
&& node.children().into_iter().any(|c| {
cache.get(&c).is_some_and(|v| v.1.is_zero() && v.0.is_int())
&& !errs
.get(&c)
.copied()
.unwrap_or(accuracy::Bound::UNKNOWN)
.is_exact()
})
}
fn inexact_zero_child(node: &ExprNode, cache: &FxHashMap<ExprId, Complex>, errs: &ErrMap) -> bool {
node.children().into_iter().any(|c| {
cache.get(&c).is_some_and(|v| {
accuracy::mag(v).is_none()
&& !errs
.get(&c)
.copied()
.unwrap_or(accuracy::Bound::UNKNOWN)
.is_exact()
})
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ZeroSearch {
Cap,
Deep,
Number,
}
fn input_bits(q: &crate::base::numeric::Q) -> u64 {
let (p, d) = (q.numer(), q.denom());
if d.bits() == d.trailing_zeros().unwrap_or(0) + 1 {
p.bits() - p.trailing_zeros().unwrap_or(0)
} else {
p.bits().max(d.bits())
}
}
fn second_order_bits(q: &crate::base::numeric::Q, arena: &Arena) -> u64 {
let bits = input_bits(q);
if !(256..=u64::from(arena.config.max_evalf_precision)).contains(&bits) {
return 0;
}
rational_small_part(q).map_or(0, |r| (-2.0 * r).ceil().max(0.0) as u64)
}
pub(crate) const ZERO_SEARCH_BITS: usize = 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Settled {
Certified,
ZeroToPrecision,
}
enum Adaptive {
Settled(Complex, Settled),
Underflow(accuracy::Nonzero),
Beyond(extended::Scaled),
Undecided,
Hidden { below_range: bool },
}
fn hidden_error() -> SymplexError {
SymplexError::Unevaluable {
reason: format!(
"the value is 0 to the precision reached, but a term that is not 0 and lies below the \
arbitrary-precision exponent range (2^{:e}) was lost in a cancellation: not known \
to be 0",
f64::from(astro_float::EXPONENT_MIN)
),
}
}
fn hidden_deep_error(digits: u32) -> SymplexError {
SymplexError::PrecisionExhausted {
requested: digits,
achieved: 0,
}
}
fn undecided_error() -> SymplexError {
SymplexError::Unevaluable {
reason: format!(
"the value underflows the arbitrary-precision exponent range (its magnitude is \
below 2^{:e}) and is not known to be 0 or not: a sum of such values of both signs",
f64::from(astro_float::EXPONENT_MIN)
),
}
}
fn underflow_error() -> SymplexError {
SymplexError::Unevaluable {
reason: format!(
"the value is not 0 but underflows the arbitrary-precision exponent range \
(its magnitude is below 2^{:e})",
f64::from(astro_float::EXPONENT_MIN)
),
}
}
fn evaluate_adaptive(
arena: &Arena,
expr: ExprId,
digits: u32,
search: ZeroSearch,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Adaptive, SymplexError> {
let prec0 = working_precision(digits);
let max_prec = arena.config.max_evalf_precision as usize;
let cap = (2 * prec0).max(prec0 + 256).min(max_prec).max(prec0);
let deep = match search {
ZeroSearch::Cap => cap,
ZeroSearch::Deep | ZeroSearch::Number => (prec0 + ZERO_SEARCH_BITS).min(max_prec).max(cap),
};
let needed = i64::from(digits) * 3322 / 1000 + 4;
let post_order = walk::post_order_ids(arena, expr);
let mut prec = prec0;
let mut previous: Option<(Option<i64>, usize, accuracy::ErrExp)> = None;
let mut hidden_pursuits = 0u32;
loop {
let Evaluated {
value,
bound,
precision_limited,
underflowed_nonzero,
beyond,
absorbed,
exact_bits,
second_bits,
} = evaluate_once(arena, expr, &post_order, prec, rm, cc)?;
let (inputs, second) = match search {
ZeroSearch::Number => (
usize::try_from(exact_bits).unwrap_or(usize::MAX),
usize::try_from(second_bits).unwrap_or(usize::MAX),
),
ZeroSearch::Cap | ZeroSearch::Deep => (0, 0),
};
let (value, bound, scale) = match &beyond {
Some(s) => (s.m.clone(), s.eb, s.k),
None => (value, bound, 0),
};
let err = bound.joint();
let below =
beyond.is_none() && accuracy::mag(&value).is_none() && accuracy::is_underflow(err);
if below && let Some(s) = underflowed_nonzero {
debug!(prec, err, "evalf: a nonzero value underflowed");
return Ok(Adaptive::Underflow(s));
}
if below {
debug!(prec, err, "evalf: an underflow of unknown sign");
return Ok(Adaptive::Undecided);
}
let nan = value.0.is_nan() || value.1.is_nan();
let finite = !(nan || value.0.is_inf() || value.1.is_inf());
let limited = precision_limited && (!finite || accuracy::is_unknown(err));
if nan && !limited {
return Err(SymplexError::PrecisionExhausted {
requested: digits,
achieved: 0,
});
}
if finite && let Some(v) = settle(&value, bound, digits, needed, prec) {
if let Some(s) = beyond {
debug!(prec, scale, "evalf: a value below the exponent range");
return Ok(Adaptive::Beyond(extended::Scaled { m: v, ..s }));
}
return Ok(Adaptive::Settled(v, Settled::Certified));
}
let lift = |e: accuracy::ErrExp| accuracy::shift(e, scale as f64);
let acc = accuracy::accurate_bits(&value, err);
let known = finite && !accuracy::is_unknown(err);
let (ball_shrinking, value_shrinking) = match &previous {
None => (true, accuracy::mag(&value).is_none()),
Some((prev_mag, prev_prec, prev_err)) => {
let gained = i64::try_from(prec - prev_prec).unwrap_or(0);
let half = (gained / 2) as f64;
let ball = accuracy::is_exact(err)
|| accuracy::is_underflow(err)
|| lift(err) <= *prev_err - half;
let value = match (accuracy::mag(&value).map(|m| m + scale), prev_mag) {
(None, _) => true,
(Some(m), Some(pm)) => m <= pm - gained / 2,
(Some(m), None) => m as f64 <= *prev_err - half,
};
(ball, value)
}
};
let noise = known && value_shrinking;
let zero_ball = noise && ball_shrinking && accuracy::contains_zero(&value, err);
let small_zero = zero_ball && lift(err) <= -(needed as f64);
let wide_zero = zero_ball && !small_zero;
let converging = known && ball_shrinking && previous.is_some();
let pursue = (noise || converging || limited) && !accuracy::is_underflow(err);
let limit = if wide_zero && search != ZeroSearch::Cap {
max_prec.max(deep)
} else if pursue || prec > cap {
deep
} else {
cap
};
if let Some(a) = acc
&& known
&& !noise
&& prec < limit
&& prec as f64 + (needed as f64 - a) > deep as f64
{
debug!(
prec,
err,
?acc,
"evalf: the bound needs more than the search allows"
);
return Err(SymplexError::PrecisionExhausted {
requested: digits,
achieved: achieved_digits(Some(a), digits),
});
}
let margin = usize::try_from(needed).unwrap_or(0) + 32;
let lost_target = absorbed.and_then(|a| a.nearest).map(|nearest| {
prec.saturating_add(nearest.ceil() as usize)
.saturating_add(margin)
});
let input_target = if inputs > prec {
Some(inputs.saturating_add(margin))
} else {
(second > prec).then(|| second.saturating_add(margin))
};
let lost_below_range = absorbed.is_some_and(|a| a.below_range);
let hidden = if !small_zero || search == ZeroSearch::Cap {
None
} else if lost_below_range {
Some(None)
} else {
lost_target
.into_iter()
.chain(input_target)
.min()
.map(|target| (target <= max_prec && hidden_pursuits < 16).then_some(target))
};
if prec >= limit {
if small_zero {
match hidden {
None => {
debug!(prec, err, "evalf: zero to the working precision");
return Ok(Adaptive::Settled(c_zero(prec0), Settled::ZeroToPrecision));
}
Some(Some(target)) => {
debug!(prec, target, "evalf: a zero ball lost a term; pursued");
previous = Some((None, prec, lift(err)));
prec = target;
hidden_pursuits += 1;
continue;
}
Some(None) => {
debug!(prec, err, ?absorbed, "evalf: a zero ball lost a term");
return Ok(Adaptive::Hidden {
below_range: lost_below_range,
});
}
}
}
debug!(prec, err, ?acc, "evalf: precision exhausted");
return Err(SymplexError::PrecisionExhausted {
requested: digits,
achieved: achieved_digits(acc, digits),
});
}
let step = match acc {
Some(a) if known => (needed as f64 - a).clamp(0.0, prec as f64).ceil() as usize + 32,
None if known && accuracy::mag(&value).is_none() && prec < cap => cap - prec,
_ => prec,
};
previous = Some((accuracy::mag(&value).map(|m| m + scale), prec, lift(err)));
prec = (prec + step.max(64)).min(limit);
if prec > cap && 4 * (limit - prec) < limit {
prec = limit;
}
debug!(prec, ?acc, "evalf: re-evaluating at a higher precision");
}
}
fn achieved_digits(acc: Option<f64>, digits: u32) -> u32 {
acc.map_or(0.0, |a| (a.max(0.0) * 0.301).min(f64::from(digits))) as u32
}
fn settle(
value: &Complex,
bound: accuracy::Bound,
digits: u32,
needed: i64,
prec: usize,
) -> Option<Complex> {
if accuracy::mag(value).is_none() {
return bound.is_exact().then(|| value.clone());
}
if accuracy::accurate_bits(value, bound.joint())? < needed as f64 {
return None;
}
let part = |p: &BigFloat, e: accuracy::ErrExp, other: &BigFloat| -> Option<bool> {
if accuracy::exact_zero(p, e) {
return Some(false);
}
if accuracy::part_contains_zero(p, e) {
return Some(true);
}
if is_negligible_part(p, other, digits) || accuracy::is_exact(e) {
return Some(false);
}
(accuracy::part_lg_low(p) - e >= needed as f64).then_some(false)
};
let zero_re = part(&value.0, bound.re, &value.1)?;
let zero_im = part(&value.1, bound.im, &value.0)?;
let mut out = value.clone();
if zero_re {
out.0 = BigFloat::new(prec);
}
if zero_im {
out.1 = BigFloat::new(prec);
}
Some(out)
}
pub(crate) const F64_DIGITS: u32 = 16;
thread_local! {
static F64_CONSTS: std::cell::RefCell<Option<Consts>> = const { std::cell::RefCell::new(None) };
}
fn with_f64_consts<T>(
f: impl FnOnce(&mut Consts) -> Result<T, SymplexError>,
) -> Result<T, SymplexError> {
let mut cc = match F64_CONSTS.with(|slot| slot.borrow_mut().take()) {
Some(cc) => cc,
None => Consts::new().map_err(|e| {
SymplexError::NotImplemented(format!("astro-float constants init failed: {e:?}"))
})?,
};
let out = f(&mut cc);
F64_CONSTS.with(|slot| {
let mut slot = slot.borrow_mut();
if slot.is_none() {
*slot = Some(cc);
}
});
out
}
fn evalf_value(
arena: &Arena,
expr: ExprId,
digits: u32,
search: ZeroSearch,
) -> Result<(Complex, Settled), SymplexError> {
let prec = ((digits as usize) * 34 / 10 + 64).max(128);
evalf_adaptive_value(arena, expr, digits, search).map(|a| match a {
Adaptive::Settled(z, settled) => (z, settled),
Adaptive::Underflow(_)
| Adaptive::Beyond(_)
| Adaptive::Undecided
| Adaptive::Hidden { .. } => (c_zero(prec), Settled::ZeroToPrecision),
})
}
fn evalf_adaptive_value(
arena: &Arena,
expr: ExprId,
digits: u32,
search: ZeroSearch,
) -> Result<Adaptive, SymplexError> {
let prec = ((digits as usize) * 34 / 10 + 64).max(128);
let max_prec = arena.config.max_evalf_precision as usize;
if prec > max_prec {
return Err(SymplexError::PrecisionExhausted {
requested: digits,
achieved: (max_prec * 10 / 34).saturating_sub(6) as u32,
});
}
let free = walk::free_symbols(arena, expr);
if !free.is_empty() {
let mut names: Vec<&str> = free
.iter()
.filter_map(|&id| match arena.node(id) {
ExprNode::Symbol(sid) => Some(arena.symbol_name(*sid)),
_ => None,
})
.collect();
names.sort_unstable();
if let Some(name) = names.first() {
debug!(symbol = name, "evalf_value: expression has free symbols");
return Err(SymplexError::FreeSymbol {
name: (*name).to_owned(),
});
}
}
let rm = RoundingMode::ToEven;
with_f64_consts(|cc| evaluate_adaptive(arena, expr, digits, search, rm, cc))
}
pub(crate) fn evalf_settled(
arena: &Arena,
expr: ExprId,
digits: u32,
search: ZeroSearch,
) -> Result<(Complex, Settled), SymplexError> {
let prec = ((digits as usize) * 34 / 10 + 64).max(128);
match evalf_adaptive_value(arena, expr, digits, search)? {
Adaptive::Settled(z, settled) => Ok((z, settled)),
Adaptive::Beyond(s) => s
.representative(prec)
.map(|z| (z, Settled::Certified))
.ok_or_else(underflow_error),
Adaptive::Underflow(accuracy::Nonzero::Positive) => Ok((
(extended::tiny(false, prec), BigFloat::new(prec)),
Settled::Certified,
)),
Adaptive::Underflow(accuracy::Nonzero::Negative) => Ok((
(extended::tiny(true, prec), BigFloat::new(prec)),
Settled::Certified,
)),
Adaptive::Underflow(accuracy::Nonzero::Unsigned) => Err(underflow_error()),
Adaptive::Undecided => Err(undecided_error()),
Adaptive::Hidden { below_range: true } => Err(hidden_error()),
Adaptive::Hidden { below_range: false } => Err(hidden_deep_error(digits)),
}
}
pub(crate) fn ln_of_positive_constant(arena: &Arena, x: ExprId) -> Option<f64> {
let z = match evalf_adaptive_value(arena, x, F64_DIGITS, ZeroSearch::Cap).ok()? {
Adaptive::Settled(z, _) => z,
Adaptive::Beyond(s) => {
let real_positive = s.m.1.is_zero() && bf_strictly_positive(&s.m.0);
return real_positive.then(|| s.ln_abs()).flatten();
}
Adaptive::Underflow(_) | Adaptive::Undecided | Adaptive::Hidden { .. } => return None,
};
if !bf_strictly_positive(&z.0) || !is_real_to_digits(&z, F64_DIGITS) {
return None;
}
let e = z.0.exponent()?;
let mut m = z.0.clone();
m.set_exponent(0); let m = bigfloat_to_f64_rounded(&m, RoundingMode::ToEven).ok()?;
Some(m.ln() + f64::from(e) * std::f64::consts::LN_2)
}
pub(crate) fn evalf_complex(
arena: &Arena,
expr: ExprId,
digits: u32,
) -> Result<Complex, SymplexError> {
evalf_value(arena, expr, digits, ZeroSearch::Cap).map(|(z, _)| z)
}
pub(crate) fn abs_to_f64(z: &Complex) -> Option<f64> {
let rm = RoundingMode::ToEven;
bigfloat_to_f64_rounded(&crate::base::bigcomplex::c_abs(z, 128, rm), rm).ok()
}
pub(crate) fn distance_to_f64(a: &Complex, b: &Complex) -> Option<f64> {
let prec = [&a.0, &a.1, &b.0, &b.1]
.iter()
.filter_map(|x| x.mantissa_max_bit_len())
.max()
.unwrap_or(128)
.max(128)
+ 64;
abs_to_f64(&crate::base::bigcomplex::c_sub(
a,
b,
prec,
RoundingMode::ToEven,
))
}
pub(crate) fn is_real_to_digits(z: &Complex, digits: u32) -> bool {
is_negligible_part(&z.1, &z.0, digits)
}
fn finite_part_to_f64(part: &BigFloat, other: &BigFloat, digits: u32) -> Result<f64, SymplexError> {
if is_negligible_part(part, other, digits) {
return Ok(0.0);
}
if part.is_inf() {
return Err(SymplexError::Unevaluable {
reason: "the value overflowed the arbitrary-precision exponent range".into(),
});
}
bigfloat_to_f64_rounded(part, RoundingMode::ToEven)
}
pub(crate) fn evalf_complex64(arena: &Arena, expr: ExprId) -> Result<Complex64, SymplexError> {
let digits = if contains_definite_integral(arena, expr) {
QUADRATURE_F64_DIGITS
} else {
F64_DIGITS
};
let (z, _) = evalf_value(arena, expr, digits, ZeroSearch::Number)?;
let re = finite_part_to_f64(&z.0, &z.1, digits)?;
let im = finite_part_to_f64(&z.1, &z.0, digits)?;
Ok(Complex64::new(re, im))
}
pub(crate) fn evalf_f64(arena: &Arena, expr: ExprId) -> Result<f64, SymplexError> {
let digits = if contains_definite_integral(arena, expr) {
QUADRATURE_F64_DIGITS
} else {
F64_DIGITS
};
let (z, _) = evalf_value(arena, expr, digits, ZeroSearch::Number)?;
if !is_negligible_part(&z.1, &z.0, digits) {
let im = bigfloat_to_f64_rounded(&z.1, RoundingMode::ToEven).unwrap_or(f64::NAN);
return Err(SymplexError::ComputationFailed {
operation: "eval_f64",
reason: format!(
"expression has nonzero imaginary part (im={im}); use eval_complex64() for complex results"
),
});
}
finite_part_to_f64(&z.0, &z.1, digits)
}
pub(crate) fn eval_const_f64(arena: &mut Arena, expr: ExprId) -> Option<f64> {
let evaled = crate::transforms::eval::eval(arena, expr);
if let Some(r) = arena.as_num(evaled) {
let result = crate::base::numeric::ratio_to_f64(r)?;
tracing::trace!(result, "eval_const_f64: rational fast path");
return Some(result);
}
match evalf_with(arena, evaled, 16, ZeroSearch::Cap, Underflow::Zero) {
Ok(s) => {
let result = s.parse::<f64>().ok();
tracing::trace!(?result, decimal_str = %s, "eval_const_f64: evalf path");
result
}
Err(e) => {
tracing::debug!(?e, "eval_const_f64: evalf failed");
None
}
}
}
fn eval_node(
arena: &Arena,
id: ExprId,
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Complex, SymplexError> {
let node = arena.node(id);
match node {
ExprNode::Num(nid) => {
let r = arena.num(*nid);
Ok((ratio_to_bigfloat(r, prec, rm), BigFloat::new(prec)))
}
ExprNode::Pi => Ok((cc.pi(prec, rm).clone(), BigFloat::new(prec))),
ExprNode::E => Ok((cc.e(prec, rm).clone(), BigFloat::new(prec))),
ExprNode::ImaginaryUnit => Ok((BigFloat::new(prec), BigFloat::from_i32(1, prec))),
ExprNode::EulerGamma => {
debug!(prec, "evalf: EulerGamma via Brent–McMillan");
let g = arb_euler_gamma(prec, rm, cc)?;
Ok((g, BigFloat::new(prec)))
}
ExprNode::Catalan => {
debug!(prec, "evalf: Catalan via Ramanujan series");
let g = arb_catalan(prec, rm, cc)?;
Ok((g, BigFloat::new(prec)))
}
ExprNode::GoldenRatio => {
let wp = prec + 16;
let five = BigFloat::from_i32(5, wp);
let sqrt5 = five.sqrt(wp, rm);
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let phi = one.add(&sqrt5, wp, rm).div(&two, prec, rm);
Ok((phi, BigFloat::new(prec)))
}
ExprNode::Re(inner) => {
let val = get_cached(cache, *inner)?;
Ok((val.0.clone(), BigFloat::new(prec)))
}
ExprNode::Im(inner) => {
let val = get_cached(cache, *inner)?;
Ok((val.1.clone(), BigFloat::new(prec)))
}
ExprNode::Conjugate(inner) => {
let val = get_cached(cache, *inner)?;
Ok((val.0.clone(), val.1.neg()))
}
ExprNode::Arg(inner) => {
let val = get_cached(cache, *inner)?;
if val.0.is_zero() && val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "arg(0) is undefined".into(),
});
}
Ok((atan2_bf(&val.1, &val.0, prec, rm, cc), BigFloat::new(prec)))
}
ExprNode::Si(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Si of complex argument not yet supported in evalf".into(),
});
}
debug!(prec, "evalf: Si via series/asymptotic");
let (si, _ci) = arb_si_ci(&val.0, true, prec, rm, cc)?;
Ok((si, BigFloat::new(prec)))
}
ExprNode::Ci(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Ci of complex argument not yet supported in evalf".into(),
});
}
if val.0.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Ci(0) is zoo".into(),
});
}
debug!(prec, "evalf: Ci via series/asymptotic");
let x_abs = val.0.abs();
let (_si, ci) = arb_si_ci(&x_abs, false, prec, rm, cc)?;
if val.0.is_negative() {
Ok((ci, cc.pi(prec, rm).clone()))
} else {
Ok((ci, BigFloat::new(prec)))
}
}
ExprNode::Ei(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Ei of complex argument not yet supported in evalf".into(),
});
}
if val.0.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Ei(0) is -∞".into(),
});
}
debug!(prec, "evalf: Ei via series/asymptotic");
let r = arb_ei(&val.0, prec, rm, cc)?;
Ok((r, BigFloat::new(prec)))
}
ExprNode::Li(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() || val.0.is_negative() {
return Err(SymplexError::Unevaluable {
reason: "li of complex or negative argument not yet supported in evalf".into(),
});
}
if val.0.is_zero() {
return Ok(c_zero(prec));
}
let wp = prec + 32;
let ln_x = val.0.ln(wp, rm, cc);
if ln_x.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "li(1) is -∞".into(),
});
}
debug!(prec, "evalf: li via Ei(ln x)");
let r = arb_ei(&ln_x, prec, rm, cc)?;
Ok((r, BigFloat::new(prec)))
}
ExprNode::Zeta(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "zeta of complex argument not yet supported in evalf".into(),
});
}
debug!(prec, "evalf: zeta via Borwein / functional equation");
let r = arb_zeta(&val.0, prec, rm, cc)?;
Ok((r, BigFloat::new(prec)))
}
ExprNode::Polygamma(n_id, x_id) => {
let n_val = get_cached(cache, *n_id)?;
let x_val = get_cached(cache, *x_id)?;
if !n_val.1.is_zero() || !x_val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "polygamma of complex arguments not yet supported in evalf".into(),
});
}
let n_f = bigfloat_to_f64(&n_val.0, rm, cc)?;
let n_round = n_f.round();
if (n_f - n_round).abs() > 1e-12 || n_round < 0.0 {
return Err(SymplexError::Unevaluable {
reason: "polygamma order must be a non-negative integer".into(),
});
}
if n_round > 10_000.0 {
debug!(
prec,
n = n_round,
"evalf: polygamma of large order via Hurwitz zeta"
);
let n_exact = BigFloat::from_f64(n_round, 64);
let r = factorials::polygamma_large(&n_exact, &x_val.0, prec, rm, cc)?;
return Ok((r, BigFloat::new(prec)));
}
let n = n_round as u32;
if n == 0 {
let r = arb_digamma(&x_val.0, prec, rm, cc)?;
return Ok((r, BigFloat::new(prec)));
}
debug!(
prec,
n, "evalf: polygamma via recurrence + asymptotic series"
);
let r = arb_polygamma(n, &x_val.0, prec, rm, cc)?;
Ok((r, BigFloat::new(prec)))
}
ExprNode::KroneckerDelta(i_id, j_id) => {
let iv = get_cached(cache, *i_id)?;
let jv = get_cached(cache, *j_id)?;
let d_re = iv.0.sub(&jv.0, prec, rm);
let d_im = iv.1.sub(&jv.1, prec, rm);
if d_re.is_zero() && d_im.is_zero() {
Ok(c_one(prec))
} else {
Ok(c_zero(prec))
}
}
ExprNode::PhysicalConstant(..)
| ExprNode::Sum(..)
| ExprNode::Product_(..)
| ExprNode::Piecewise(_)
| ExprNode::RootOf(..)
| ExprNode::RootSum(..)
| ExprNode::DefiniteIntegral(..) => {
eval_node_with_error(arena, id, cache, &ErrMap::default(), prec, rm, cc).map(|(v, _)| v)
}
ExprNode::Infinity | ExprNode::NegInfinity | ExprNode::ComplexInfinity => {
Err(SymplexError::Unevaluable {
reason: "cannot evaluate infinity to finite precision".into(),
})
}
ExprNode::Factorial(x_id) => {
let x_val = get_cached(cache, *x_id)?;
if !x_val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "factorial of a complex argument not yet supported in evalf".into(),
});
}
debug!(
prec,
"evalf: factorial via arbitrary-precision Gamma(x + 1)"
);
let x_plus_1 = x_val
.0
.add(&BigFloat::from_i32(1, 64), exact_bits(&x_val.0, prec), rm);
let r = arb_gamma_real(&x_plus_1, prec, rm, cc)?;
Ok((r, BigFloat::new(prec)))
}
ExprNode::Binomial(n_id, k_id) => {
debug!(prec, "evalf: Binomial via arbitrary-precision Gamma");
let n_val = get_cached(cache, *n_id)?;
let k_val = get_cached(cache, *k_id)?;
if !n_val.1.is_zero() || !k_val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Binomial of complex arguments not yet supported in evalf".into(),
});
}
if n_val.0.is_int() && k_val.0.is_int() && !n_val.0.is_negative() && k_val.0 > n_val.0 {
return Ok((BigFloat::new(prec), BigFloat::new(prec)));
}
if factorials::binomial_meets_pole(&n_val.0, &k_val.0, prec, rm) {
let r = factorials::binomial(&n_val.0, &k_val.0, prec, rm, cc)?;
return Ok((r, BigFloat::new(prec)));
}
let one_bf = BigFloat::from_i32(1, 64);
let p = exact_bits(&n_val.0, prec).max(exact_bits(&k_val.0, prec)) + 64;
let n_plus_1 = n_val.0.add(&one_bf, p, rm);
let k_plus_1 = k_val.0.add(&one_bf, p, rm);
let n_minus_k = n_val.0.sub(&k_val.0, p, rm);
let n_minus_k_plus_1 = n_minus_k.add(&one_bf, p, rm);
let g_numer = arb_gamma_real(&n_plus_1, prec, rm, cc)?;
let g_k = arb_gamma_real(&k_plus_1, prec, rm, cc)?;
let g_nmk = arb_gamma_real(&n_minus_k_plus_1, prec, rm, cc)?;
let g_denom = g_k.mul(&g_nmk, prec, rm);
if g_denom.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Binomial denominator is zero (pole in Gamma)".into(),
});
}
let result = g_numer.div(&g_denom, prec, rm);
if (result.is_nan() || result.is_inf())
&& (k_val.0.is_zero() || k_val.0.is_int())
&& !k_val.0.is_negative()
&& let Some(k) = bigfloat_to_f64_rounded(&k_val.0, rm)
.ok()
.filter(|k| *k <= 64.0)
{
let k = k as i32;
let wp = prec + 16;
let mut product = BigFloat::from_i32(1, wp);
for j in 0..k {
let factor = n_val.0.sub(&BigFloat::from_i32(j, 64), p, rm);
product = product.mul(&factor, wp, rm);
}
let k_factorial = (1..=k).fold(BigFloat::from_i32(1, wp), |acc, j| {
acc.mul(&BigFloat::from_i32(j, 64), wp, rm)
});
return Ok((product.div(&k_factorial, prec, rm), BigFloat::new(prec)));
}
Ok((result, BigFloat::new(prec)))
}
ExprNode::Gamma(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Gamma of complex argument not yet supported in evalf".into(),
});
}
debug!(prec, "evalf: Gamma via Stirling series");
let result = arb_gamma_real(&val.0, prec, rm, cc)?;
Ok((result, BigFloat::new(prec)))
}
ExprNode::LogGamma(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
debug!(prec, "evalf: LogGamma of a complex argument");
return loggamma::loggamma_complex(val, prec, rm, cc);
}
debug!(prec, "evalf: LogGamma via Stirling series");
arb_log_gamma(&val.0, prec, rm, cc)
}
ExprNode::Digamma(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Digamma of complex argument not yet supported in evalf".into(),
});
}
let result = arb_digamma(&val.0, prec, rm, cc)?;
Ok((result, BigFloat::new(prec)))
}
ExprNode::Erf(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "erf of complex argument not yet supported in evalf".into(),
});
}
debug!(prec, "evalf: erf via arbitrary-precision series");
let result = arb_erf(&val.0, prec, rm, cc)?;
Ok((result, BigFloat::new(prec)))
}
ExprNode::Erfc(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "erfc of complex argument not yet supported in evalf".into(),
});
}
debug!(prec, "evalf: erfc via arbitrary-precision series");
let result = arb_erfc(&val.0, prec, rm, cc)?;
Ok((result, BigFloat::new(prec)))
}
ExprNode::LambertW(_) => eval_lambertw_node(arena, node, cache, prec, rm, cc),
ExprNode::Beta(a_id, b_id) => {
let a_val = get_cached(cache, *a_id)?;
let b_val = get_cached(cache, *b_id)?;
if !a_val.1.is_zero() || !b_val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Beta of complex arguments not yet supported in evalf".into(),
});
}
debug!(prec, "evalf: Beta via arbitrary-precision Gamma");
let ga = arb_gamma_real(&a_val.0, prec, rm, cc)?;
let gb = arb_gamma_real(&b_val.0, prec, rm, cc)?;
let p = exact_bits(&a_val.0, prec).max(exact_bits(&b_val.0, prec)) + 64;
let a_plus_b = a_val.0.add(&b_val.0, p, rm);
let gab = arb_gamma_real(&a_plus_b, prec, rm, cc)?;
if gab.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Beta function: Gamma(a+b) is zero".into(),
});
}
let numer = ga.mul(&gb, prec, rm);
let result = numer.div(&gab, prec, rm);
Ok((result, BigFloat::new(prec)))
}
ExprNode::NaN => Err(SymplexError::Unevaluable {
reason: "cannot evaluate NaN".into(),
}),
ExprNode::Symbol(sid) => {
let name = arena.symbol_name(*sid);
Err(SymplexError::FreeSymbol {
name: name.to_owned(),
})
}
ExprNode::Add(children) => {
let mut sum = c_zero(prec);
for &child in children.iter() {
let val = get_cached(cache, child)?;
sum = c_add(&sum, val, prec, rm);
}
Ok(sum)
}
ExprNode::Mul(children) => {
let mut product = c_one(prec);
for &child in children.iter() {
let val = get_cached(cache, child)?;
product = c_mul(&product, val, prec, rm);
}
Ok(product)
}
ExprNode::Pow(base, exp) => {
let b = get_cached(cache, *base)?;
let e = get_cached(cache, *exp)?;
let b_is_real = b.1.is_zero();
let e_is_real = e.1.is_zero();
if b_is_real
&& !b.0.is_negative()
&& let ExprNode::Num(nid) = arena.node(*exp)
{
let r = arena.num(*nid);
if *r == Ratio::new(BigInt::from(1), BigInt::from(2)) {
return Ok((b.0.sqrt(prec, rm), BigFloat::new(prec)));
}
}
if let Some(n) = try_as_small_int(arena, *exp) {
if b_is_real {
if n >= 0 {
return Ok((b.0.powi(n as usize, prec, rm), BigFloat::new(prec)));
} else {
let pow_pos = b.0.powi((-n) as usize, prec, rm);
let one_bf = BigFloat::from_i32(1, prec);
return Ok((one_bf.div(&pow_pos, prec, rm), BigFloat::new(prec)));
}
} else {
if n >= 0 {
return Ok(c_powi(b, n as usize, prec, rm));
} else {
let pow_pos = c_powi(b, (-n) as usize, prec, rm);
let one_c = c_one(prec);
return Ok(c_div(&one_c, &pow_pos, prec, rm));
}
}
}
if b.0.is_zero() && b.1.is_zero() && !(e.0.is_zero() && e.1.is_zero()) {
if e.1.is_zero() && !e.0.is_zero() && e.0.is_positive() {
return Ok(c_zero(prec));
}
return Err(SymplexError::Unevaluable {
reason: if e.1.is_zero() {
"cannot evaluate infinity to finite precision".into()
} else {
"0 to a non-real power has no value".into()
},
});
}
if b_is_real && e_is_real && b.0.is_positive() {
return Ok((bf_pow(&b.0, &e.0, prec, rm, cc), BigFloat::new(prec)));
}
if b_is_real
&& b.0.is_negative()
&& let ExprNode::Num(nid) = arena.node(*exp)
&& *arena.num(*nid).denom() == BigInt::from(2)
{
let n = arena.num(*nid).numer();
let r = b.0.abs();
let magnitude = bf_pow(&r, &e.0, prec, rm, cc);
let i_power_is_plus_i =
num_integer::Integer::mod_floor(n, &BigInt::from(4)) == BigInt::from(1);
let im = if i_power_is_plus_i {
magnitude
} else {
magnitude.neg()
};
return Ok((BigFloat::new(prec), im));
}
if (b.0.is_zero() && b.1.is_zero()) || (e.0.is_zero() && e.1.is_zero()) {
return Ok(c_pow(b, e, prec, rm, cc));
}
let product = c_mul(e, &c_ln(b, prec, rm, cc), prec, rm);
check_trig_arg(&product.1, 0, prec, arena)?;
Ok(c_exp(&product, prec, rm, cc))
}
ExprNode::Neg(inner) => {
let val = get_cached(cache, *inner)?;
Ok(c_neg(val))
}
ExprNode::Sin(inner) => {
let val = get_cached(cache, *inner)?;
let (arg, extra) = trig_arg(arena, *inner, val, prec, rm);
check_trig_arg(&arg, extra, prec, arena)?;
if val.1.is_zero() {
Ok((arg.sin(prec, rm, cc), BigFloat::new(prec)))
} else {
Ok(c_sin(val, prec, rm, cc))
}
}
ExprNode::Cos(inner) => {
let val = get_cached(cache, *inner)?;
let (arg, extra) = trig_arg(arena, *inner, val, prec, rm);
check_trig_arg(&arg, extra, prec, arena)?;
if val.1.is_zero() {
Ok((arg.cos(prec, rm, cc), BigFloat::new(prec)))
} else {
Ok(c_cos(val, prec, rm, cc))
}
}
ExprNode::Tan(inner) => {
let val = get_cached(cache, *inner)?;
let (arg, extra) = trig_arg(arena, *inner, val, prec, rm);
check_trig_arg(&arg, extra, prec, arena)?;
if val.1.is_zero() {
Ok((arg.tan(prec, rm, cc), BigFloat::new(prec)))
} else {
let s = c_sin(val, prec, rm, cc);
let c = c_cos(val, prec, rm, cc);
Ok(c_div(&s, &c, prec, rm))
}
}
ExprNode::Exp(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
Ok((val.0.exp(prec, rm, cc), BigFloat::new(prec)))
} else {
check_trig_arg(&val.1, 0, prec, arena)?;
Ok(c_exp(val, prec, rm, cc))
}
}
ExprNode::Ln(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() && val.0.is_positive() {
Ok((val.0.ln(prec, rm, cc), BigFloat::new(prec)))
} else {
Ok(c_ln(val, prec, rm, cc))
}
}
ExprNode::Abs(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
Ok((val.0.abs(), BigFloat::new(prec)))
} else {
Ok((c_abs(val, prec, rm), BigFloat::new(prec)))
}
}
ExprNode::Asin(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
if abs_le_one(&val.0, prec) {
return Ok((val.0.asin(prec, rm, cc), BigFloat::new(prec)));
}
let half_pi = half_pi_bf(prec, rm, cc);
let ach = val.0.abs().acosh(prec, rm, cc);
return Ok(if val.0.is_negative() {
(half_pi.neg(), ach)
} else {
(half_pi, ach.neg())
});
}
Ok(c_asin(val, prec, rm, cc))
}
ExprNode::Acos(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
if abs_le_one(&val.0, prec) {
return Ok((val.0.acos(prec, rm, cc), BigFloat::new(prec)));
}
let ach = val.0.abs().acosh(prec, rm, cc);
return Ok(if val.0.is_negative() {
(cc.pi(prec, rm).clone(), ach.neg())
} else {
(BigFloat::new(prec), ach)
});
}
Ok(c_acos(val, prec, rm, cc))
}
ExprNode::Atan(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
Ok((val.0.atan(prec, rm, cc), BigFloat::new(prec)))
} else {
Ok(c_atan(val, prec, rm, cc))
}
}
ExprNode::Atan2(y_id, x_id) => {
let yv = get_cached(cache, *y_id)?;
let xv = get_cached(cache, *x_id)?;
if yv.1.is_zero() && xv.1.is_zero() {
Ok((atan2_bf(&yv.0, &xv.0, prec, rm, cc), BigFloat::new(prec)))
} else {
Err(SymplexError::Unevaluable {
reason: "atan2 with complex arguments is not supported".into(),
})
}
}
ExprNode::Sinh(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
Ok((val.0.sinh(prec, rm, cc), BigFloat::new(prec)))
} else {
check_trig_arg(&val.1, 0, prec, arena)?;
Ok(c_sinh(val, prec, rm, cc))
}
}
ExprNode::Cosh(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
Ok((val.0.cosh(prec, rm, cc), BigFloat::new(prec)))
} else {
check_trig_arg(&val.1, 0, prec, arena)?;
Ok(c_cosh(val, prec, rm, cc))
}
}
ExprNode::Tanh(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
Ok((val.0.tanh(prec, rm, cc), BigFloat::new(prec)))
} else {
check_trig_arg(&val.1, 0, prec, arena)?;
let s = c_sinh(val, prec, rm, cc);
let c = c_cosh(val, prec, rm, cc);
Ok(c_div(&s, &c, prec, rm))
}
}
ExprNode::Asinh(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
Ok((val.0.asinh(prec, rm, cc), BigFloat::new(prec)))
} else {
Ok(c_asinh(val, prec, rm, cc))
}
}
ExprNode::Acosh(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
let one = BigFloat::from_i32(1, prec);
if val.0 >= one {
return Ok((val.0.acosh(prec, rm, cc), BigFloat::new(prec)));
}
return Ok(if val.0 >= one.neg() {
(BigFloat::new(prec), val.0.acos(prec, rm, cc))
} else {
(val.0.abs().acosh(prec, rm, cc), cc.pi(prec, rm).clone())
});
}
Ok(c_acosh(val, prec, rm, cc))
}
ExprNode::Atanh(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
if abs_le_one(&val.0, prec) {
return Ok((val.0.atanh(prec, rm, cc), BigFloat::new(prec)));
}
let recip = BigFloat::from_i32(1, prec).div(&val.0, prec, rm);
let half_pi = half_pi_bf(prec, rm, cc);
let im = if val.0.is_negative() {
half_pi
} else {
half_pi.neg()
};
return Ok((recip.atanh(prec, rm, cc), im));
}
Ok(c_atanh(val, prec, rm, cc))
}
ExprNode::Sign(inner) => {
let val = get_cached(cache, *inner)?;
if val.1.is_zero() {
if val.0.is_zero() {
Ok(c_zero(prec))
} else if val.0.is_negative() {
Ok((BigFloat::from_i32(-1, prec), BigFloat::new(prec)))
} else {
Ok((BigFloat::from_i32(1, prec), BigFloat::new(prec)))
}
} else {
let modulus = c_abs(val, prec, rm);
if modulus.is_zero() {
Ok(c_zero(prec))
} else {
let denom = (modulus, BigFloat::new(prec));
Ok(c_div(val, &denom, prec, rm))
}
}
}
ExprNode::Floor(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "floor of complex number is not defined".into(),
});
}
let truncated = val.0.int();
let result = if val.0.is_negative() && truncated != val.0 {
truncated.sub(&BigFloat::from_i32(1, prec), prec, rm)
} else {
truncated
};
Ok((result, BigFloat::new(prec)))
}
ExprNode::Ceiling(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "ceiling of complex number is not defined".into(),
});
}
let truncated = val.0.int();
let result = if val.0.is_positive() && truncated != val.0 {
truncated.add(&BigFloat::from_i32(1, prec), prec, rm)
} else {
truncated
};
Ok((result, BigFloat::new(prec)))
}
ExprNode::Min(children) => {
if children.is_empty() {
return Err(SymplexError::Unevaluable {
reason: "min of empty set".into(),
});
}
let mut best = get_cached(cache, children[0])?.clone();
if !best.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "min requires real arguments".into(),
});
}
for &child in &children[1..] {
let val = get_cached(cache, child)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "min requires real arguments".into(),
});
}
if val.0.sub(&best.0, prec, rm).is_negative() {
best = val.clone();
}
}
Ok(best)
}
ExprNode::Max(children) => {
if children.is_empty() {
return Err(SymplexError::Unevaluable {
reason: "max of empty set".into(),
});
}
let mut best = get_cached(cache, children[0])?.clone();
if !best.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "max requires real arguments".into(),
});
}
for &child in &children[1..] {
let val = get_cached(cache, child)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "max requires real arguments".into(),
});
}
if val.0.sub(&best.0, prec, rm).is_positive() {
best = val.clone();
}
}
Ok(best)
}
ExprNode::Heaviside(inner) => {
let val = get_cached(cache, *inner)?;
if !val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Heaviside of complex argument not supported in evalf".into(),
});
}
if val.0.is_zero() {
Ok((BigFloat::from_f64(0.5, prec), BigFloat::new(prec)))
} else if val.0.is_negative() {
Ok(c_zero(prec))
} else {
Ok((BigFloat::from_i32(1, prec), BigFloat::new(prec)))
}
}
ExprNode::DiracDelta(inner) => {
let _val = get_cached(cache, *inner)?;
Ok(c_zero(prec))
}
ExprNode::Apply(sid, args) => {
let name = arena.symbol_name(*sid);
match arena.lib_fn(*sid) {
Some(LibFn::LambertW) => eval_lambertw_node(arena, node, cache, prec, rm, cc),
Some(f) if f.arity().accepts(args.len()) => {
eval_lib_fn(f, args, cache, prec, rm, cc)
}
Some(_) => Err(unevaluable(format!(
"{name} called with {} argument(s)",
args.len()
))),
None => Err(unevaluable(format!("cannot evaluate function '{name}'"))),
}
}
ExprNode::Derivative(_, _) => Err(SymplexError::Unevaluable {
reason: "cannot evaluate unevaluated derivative".into(),
}),
ExprNode::Integral(_, _) => Err(SymplexError::Unevaluable {
reason: "cannot evaluate unevaluated integral".into(),
}),
ExprNode::BoolTrue
| ExprNode::BoolFalse
| ExprNode::Gt(_, _)
| ExprNode::Ge(_, _)
| ExprNode::Eq_(_, _)
| ExprNode::Ne(_, _)
| ExprNode::And(_)
| ExprNode::Or(_)
| ExprNode::Not(_) => Err(SymplexError::Unevaluable {
reason: "boolean expression".into(),
}),
ExprNode::EmptySet
| ExprNode::UniversalSet
| ExprNode::Interval(_, _, _)
| ExprNode::FiniteSet(_)
| ExprNode::SetUnion(_)
| ExprNode::SetIntersection(_)
| ExprNode::SetComplement(_, _) => Err(SymplexError::Unevaluable {
reason: "set-valued expressions cannot be numerically evaluated".into(),
}),
ExprNode::Limit(_, _, _) => Err(SymplexError::Unevaluable {
reason: "cannot numerically evaluate unevaluated Limit".into(),
}),
ExprNode::Series(_, _, _, _) => Err(SymplexError::Unevaluable {
reason: "cannot numerically evaluate unevaluated Series".into(),
}),
ExprNode::LaplaceTransform(_, _, _) => Err(SymplexError::Unevaluable {
reason: "cannot numerically evaluate unevaluated LaplaceTransform".into(),
}),
ExprNode::InverseLaplaceTransform(_, _, _) => Err(SymplexError::Unevaluable {
reason: "cannot numerically evaluate unevaluated InverseLaplaceTransform".into(),
}),
ExprNode::Residue(_, _, _) => Err(SymplexError::Unevaluable {
reason: "cannot numerically evaluate unevaluated Residue".into(),
}),
ExprNode::DSolve(_, _, _) => Err(SymplexError::Unevaluable {
reason: "cannot numerically evaluate unevaluated DSolve".into(),
}),
ExprNode::ConditionSet(_, _) => Err(SymplexError::Unevaluable {
reason: "cannot numerically evaluate ConditionSet".into(),
}),
ExprNode::Subs(_, _, _) => Err(SymplexError::Unevaluable {
reason: "cannot numerically evaluate unevaluated Subs".into(),
}),
}
}
fn contains_definite_integral(arena: &Arena, root: ExprId) -> bool {
let mut stack = vec![root];
let mut seen = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !seen.insert(id) {
continue;
}
let node = arena.node(id);
if matches!(node, ExprNode::DefiniteIntegral(..)) {
return true;
}
node.for_each_child(|c| stack.push(c));
}
false
}
fn definite_bound_f64(
arena: &Arena,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
id: ExprId,
) -> Result<(f64, f64), SymplexError> {
match arena.node(id) {
ExprNode::Infinity => Ok((f64::INFINITY, 0.0)),
ExprNode::NegInfinity => Ok((f64::NEG_INFINITY, 0.0)),
_ => {
let v = get_cached(cache, id)?;
if !v.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "integration bounds of a definite integral must be real".into(),
});
}
let x = bigfloat_to_f64_rounded(&v.0, RoundingMode::ToEven)?;
let bound = errs.get(&id).copied().unwrap_or(accuracy::Bound::UNKNOWN);
if bound.is_unknown() || !x.is_finite() {
return Err(SymplexError::PrecisionExhausted {
requested: F64_DIGITS,
achieved: 0,
});
}
let exp2 = |e: accuracy::ErrExp| {
if accuracy::is_exact(e) {
0.0
} else {
e.clamp(-1100.0, 1100.0).exp2()
}
};
let exact_in_f64 = BigFloat::from_f64(x, 64) == v.0;
let conversion = if exact_in_f64 {
0.0
} else {
x.abs() * f64::EPSILON / 2.0
};
Ok((x, conversion + exp2(bound.re) + exp2(bound.im)))
}
}
}
const QUADRATURE_ROUNDING_FACTOR: f64 = 16.0;
pub(crate) const QUADRATURE_F64_DIGITS: u32 = 8;
#[allow(clippy::too_many_arguments)]
fn eval_definite_integral(
arena: &Arena,
body: ExprId,
var: ExprId,
lo: ExprId,
hi: ExprId,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
prec: usize,
) -> Result<(Complex, accuracy::Bound), SymplexError> {
use crate::calculus::definite::{QuadOpts, QuadResult, quadrature};
debug!("evalf: DefiniteIntegral — f64 Gauss–Kronrod quadrature");
let var_name = match arena.node(var) {
ExprNode::Symbol(sid) => arena.symbol_name(*sid).to_string(),
_ => {
return Err(SymplexError::Unevaluable {
reason: "integration variable of a definite integral must be a symbol".into(),
});
}
};
let (a, a_err) = definite_bound_f64(arena, cache, errs, lo)?;
let (b, b_err) = definite_bound_f64(arena, cache, errs, hi)?;
let func = crate::output::lambdify::compile_raw(arena, body, &[&var_name]).map_err(|e| {
SymplexError::Unevaluable {
reason: format!("definite integral body cannot be compiled for quadrature: {e}"),
}
})?;
let peak = std::cell::Cell::new(0.0f64);
let f = |t: f64| {
let v = func(&[t]);
if v.is_finite() {
peak.set(peak.get().max(v.abs()));
}
v
};
let abs_f = |t: f64| func(&[t]).abs();
let loose = QuadOpts {
rel_tol: 1e-3,
abs_tol: 0.0,
max_subdivisions: 400,
};
let l1 = quadrature(&abs_f, a, b, &loose).map_or(f64::INFINITY, |r| r.value + r.error);
let floor = QUADRATURE_ROUNDING_FACTOR * f64::EPSILON * l1;
let default = QuadOpts::default();
let wanted = 2f64.powi(
-i32::try_from(prec.saturating_sub(64))
.unwrap_or(i32::MAX)
.min(1000),
);
let opts = QuadOpts {
rel_tol: wanted.max(1e-13).min(default.rel_tol),
abs_tol: if floor.is_finite() {
floor
} else {
default.abs_tol
},
..default
};
let QuadResult { value, error } = quadrature(&f, a, b, &opts)?;
let tol = default.abs_tol.max(default.rel_tol * value.abs());
let converged = error <= 1e3 * tol;
if !converged {
return Err(SymplexError::ComputationFailed {
operation: "evalf",
reason: format!(
"quadrature of the definite integral did not converge: estimate {value} \
with error {error:e} (integral may diverge)"
),
});
}
let rounding = QUADRATURE_ROUNDING_FACTOR * f64::EPSILON * l1.max(value.abs());
let endpoints = (a_err + b_err) * peak.get();
let total = error + rounding + endpoints;
debug!(
value,
error, rounding, endpoints, "evalf: DefiniteIntegral evaluated"
);
if !value.is_finite() || !total.is_finite() {
return Ok((
(BigFloat::from_f64(value, prec), BigFloat::new(prec)),
accuracy::Bound::UNKNOWN,
));
}
let e = if total > 0.0 {
total.log2() + 1e-12
} else {
-1074.0
};
Ok((
(BigFloat::from_f64(value, prec), BigFloat::new(prec)),
accuracy::Bound::real(e),
))
}
const MAX_RANGE_TERMS: u32 = 10_000;
#[allow(clippy::too_many_arguments)]
fn evaluate_at_integer(
arena: &Arena,
expr: ExprId,
post_order: &[ExprId],
var: ExprId,
k: &BigInt,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(Complex, accuracy::Bound), SymplexError> {
let value = (
crate::base::numeric::bigint_to_bigfloat(k, prec),
BigFloat::new(prec),
);
let err = if k.bits() <= prec as u64 {
accuracy::EXACT
} else {
accuracy::rounding(&value, prec)
};
evaluate_tree(
arena,
expr,
post_order,
Some((var, value, accuracy::Bound::real(err))),
true,
prec,
rm,
cc,
)
}
fn range_bound(
arena: &Arena,
cache: &FxHashMap<ExprId, Complex>,
id: ExprId,
what: &str,
rm: RoundingMode,
) -> Result<BigInt, SymplexError> {
if let Some(q) = arena.as_num(id) {
if q.is_integer() {
return Ok(q.to_integer());
}
return Err(unevaluable(format!("{what} bounds are not integers")));
}
let v = get_cached(cache, id)?;
if !v.1.is_zero() {
return Err(unevaluable(format!("{what} bounds must be real")));
}
let f = bigfloat_to_f64_rounded(&v.0, rm)?;
let r = f.round();
if !r.is_finite() || (f - r).abs() > 1e-9 || r.abs() > 9.0e15 {
return Err(unevaluable(format!("{what} bounds are not integers")));
}
Ok(BigInt::from(r as i64))
}
fn range_terms(lo: &BigInt, hi: &BigInt, what: &str) -> Result<u32, SymplexError> {
if hi < lo {
return Ok(0);
}
let n = hi - lo + 1;
match u32::try_from(n) {
Ok(n) if n <= MAX_RANGE_TERMS + 1 => Ok(n),
_ => Err(unevaluable(format!(
"{what} range too large for numerical evaluation (> {MAX_RANGE_TERMS} terms)"
))),
}
}
struct SumAcc {
value: Complex,
re: accuracy::PartSum,
im: accuracy::PartSum,
unknown: bool,
prec: usize,
}
impl SumAcc {
fn new(prec: usize) -> SumAcc {
SumAcc {
value: c_zero(prec),
re: accuracy::PartSum::new(),
im: accuracy::PartSum::new(),
unknown: false,
prec,
}
}
fn add(&mut self, term: &Complex, err: accuracy::Bound, rm: RoundingMode) {
self.value = c_add(&self.value, term, self.prec, rm);
self.unknown |= err.is_unknown();
self.re.add(&term.0, err.re, &self.value.0, self.prec);
self.im.add(&term.1, err.im, &self.value.1, self.prec);
}
fn finish(self) -> (Complex, accuracy::Bound) {
if self.unknown {
return (self.value, accuracy::Bound::UNKNOWN);
}
let bound = accuracy::Bound {
re: self.re.bound(),
im: self.im.bound(),
};
(self.value, bound)
}
}
#[allow(clippy::too_many_arguments)]
fn eval_sum(
arena: &Arena,
body: ExprId,
var: ExprId,
lo: ExprId,
hi: ExprId,
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(Complex, accuracy::Bound), SymplexError> {
if matches!(arena.node(lo), ExprNode::NegInfinity) {
return Err(unevaluable("a Sum from −∞ is not evaluated numerically"));
}
let lo_i = range_bound(arena, cache, lo, "Sum", rm)?;
let post_order = walk::post_order_ids(arena, body);
if matches!(arena.node(hi), ExprNode::Infinity) {
if let Some(result) = emsum::rational_sum(arena, body, var, &lo_i, prec) {
debug!("evalf: Sum — infinite sum of a rational term");
return result;
}
debug!("evalf: Sum — infinite hypergeometric series");
let mut term =
|k: &BigInt, p: usize| evaluate_at_integer(arena, body, &post_order, var, k, p, rm, cc);
return hypsum::infinite_sum(arena, body, var, &lo_i, prec, rm, &mut term);
}
let hi_i = range_bound(arena, cache, hi, "Sum", rm)?;
let terms = range_terms(&lo_i, &hi_i, "Sum")?;
let mut sum = SumAcc::new(prec);
let mut k = lo_i.clone();
for _ in 0..terms {
let (term, err) = evaluate_at_integer(arena, body, &post_order, var, &k, prec, rm, cc)?;
sum.add(&term, err, rm);
k += 1;
}
debug!(lo = %lo_i, hi = %hi_i, "evalf: Sum evaluated");
Ok(sum.finish())
}
#[allow(clippy::too_many_arguments)]
fn eval_product(
arena: &Arena,
body: ExprId,
var: ExprId,
lo: ExprId,
hi: ExprId,
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(Complex, accuracy::Bound), SymplexError> {
let lo_i = range_bound(arena, cache, lo, "Product", rm)?;
let hi_i = range_bound(arena, cache, hi, "Product", rm)?;
let terms = range_terms(&lo_i, &hi_i, "Product")?;
let post_order = walk::post_order_ids(arena, body);
let mut factors: Vec<(Complex, accuracy::Bound)> = Vec::with_capacity(terms as usize);
let mut k = lo_i.clone();
for _ in 0..terms {
factors.push(evaluate_at_integer(
arena,
body,
&post_order,
var,
&k,
prec,
rm,
cc,
)?);
k += 1;
}
let parts: Vec<(&Complex, accuracy::Bound)> = factors.iter().map(|(v, e)| (v, *e)).collect();
debug!(lo = %lo_i, hi = %hi_i, "evalf: Product evaluated");
Ok(accuracy::mul_with_bound(&parts, prec, rm))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Decision {
value: bool,
certain: bool,
}
fn decide_condition(
arena: &Arena,
cond: ExprId,
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
prec: usize,
rm: RoundingMode,
) -> Option<Decision> {
let bound_of = |a: &ExprId| errs.get(a).copied().unwrap_or(accuracy::Bound::UNKNOWN);
let difference = |a: &ExprId, b: &ExprId| -> Option<(Complex, accuracy::ErrExp, bool)> {
let (av, bv) = (cache.get(a)?, cache.get(b)?);
if a == b {
return Some((c_zero(prec), accuracy::EXACT, true));
}
let (ea, eb) = (bound_of(a), bound_of(b));
let d = c_sub(av, bv, prec + 64, rm);
let exact = ea.is_exact() && eb.is_exact();
let e = if ea.is_unknown() || eb.is_unknown() {
accuracy::UNKNOWN
} else {
accuracy::lsum(
accuracy::lsum(ea.joint(), eb.joint()),
accuracy::rounding(&d, prec + 64),
)
};
Some((d, e, exact))
};
let away_from_zero = |d: &Complex, e: accuracy::ErrExp| {
!accuracy::is_unknown(e) && !accuracy::contains_zero(d, e)
};
let order = |a: &ExprId, b: &ExprId, strict: bool| -> Option<Decision> {
let (av, bv) = (cache.get(a)?, cache.get(b)?);
if !(av.1.is_zero() && bv.1.is_zero()) {
return None;
}
let real =
accuracy::exactly_real(av, bound_of(a)) && accuracy::exactly_real(bv, bound_of(b));
let (d, e, exact) = difference(a, b)?;
let value = d.0.is_positive() || (!strict && d.0.is_zero());
Some(Decision {
value,
certain: exact || (real && away_from_zero(&d, e)),
})
};
let equal = |a: &ExprId, b: &ExprId| -> Option<Decision> {
let (d, e, exact) = difference(a, b)?;
let value = d.0.is_zero() && d.1.is_zero();
Some(Decision {
value,
certain: exact || (!value && away_from_zero(&d, e)),
})
};
let post_order = walk::post_order_ids(arena, cond);
let mut truth: FxHashMap<ExprId, Option<Decision>> = FxHashMap::default();
for &id in &post_order {
let kids = |ids: &[ExprId]| -> Vec<Option<Decision>> {
ids.iter()
.map(|k| truth.get(k).copied().flatten())
.collect()
};
let v: Option<Decision> = match arena.node(id) {
ExprNode::BoolTrue => Some(Decision {
value: true,
certain: true,
}),
ExprNode::BoolFalse => Some(Decision {
value: false,
certain: true,
}),
ExprNode::Gt(a, b) => order(a, b, true),
ExprNode::Ge(a, b) => order(a, b, false),
ExprNode::Eq_(a, b) => equal(a, b),
ExprNode::Ne(a, b) => equal(a, b).map(|d| Decision {
value: !d.value,
..d
}),
ExprNode::Not(inner) => truth.get(inner).copied().flatten().map(|d| Decision {
value: !d.value,
..d
}),
ExprNode::And(ks) => combine(&kids(ks), false),
ExprNode::Or(ks) => combine(&kids(ks), true),
_ => None,
};
truth.insert(id, v);
}
truth.get(&cond).copied().flatten()
}
fn combine(operands: &[Option<Decision>], absorbing: bool) -> Option<Decision> {
let certain_absorbing = Decision {
value: absorbing,
certain: true,
};
if operands.contains(&Some(certain_absorbing)) {
return Some(certain_absorbing);
}
let decided: Option<Vec<Decision>> = operands.iter().copied().collect();
let decided = decided?;
Some(Decision {
value: if absorbing {
decided.iter().any(|d| d.value)
} else {
decided.iter().all(|d| d.value)
},
certain: decided.iter().all(|d| d.certain),
})
}
fn eval_piecewise(
arena: &Arena,
pairs: &[(ExprId, ExprId)],
cache: &FxHashMap<ExprId, Complex>,
errs: &ErrMap,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(Complex, accuracy::Bound), SymplexError> {
debug!(
branches = pairs.len(),
"evalf: Piecewise — evaluating conditions"
);
let mut certain = true;
for &(value_id, cond_id) in pairs {
let Some(decision) = decide_condition(arena, cond_id, cache, errs, prec, rm) else {
return Err(unevaluable(format!(
"cannot evaluate piecewise: condition `{}` is undecided",
arena.display(cond_id)
)));
};
certain &= decision.certain;
if !decision.value {
continue;
}
let (value, err) = match cache.get(&value_id) {
Some(v) => (
v.clone(),
errs.get(&value_id)
.copied()
.unwrap_or(accuracy::Bound::UNKNOWN),
),
None => eval_node_with_error(arena, value_id, cache, errs, prec, rm, cc)?,
};
return Ok((
value,
if certain {
err
} else {
accuracy::Bound::UNKNOWN
},
));
}
if !certain {
return Ok((c_zero(prec), accuracy::Bound::UNKNOWN));
}
Err(unevaluable(
"cannot evaluate piecewise: every condition is false",
))
}
fn radius_error(ball: &crate::poly::roots::RootBall) -> accuracy::Bound {
let e = match ball.radius.as_ref() {
None => return accuracy::Bound::UNKNOWN,
Some(r) if r.is_zero() => accuracy::EXACT,
Some(r) => accuracy::part_lg(r),
};
if ball.real {
accuracy::Bound::real(e)
} else {
accuracy::Bound::both(e)
}
}
fn eval_rootof(
arena: &Arena,
poly_id: ExprId,
var_id: ExprId,
idx_id: ExprId,
prec: usize,
) -> Result<(Complex, accuracy::Bound), SymplexError> {
let idx: usize = match arena.as_num(idx_id) {
Some(r) if r.is_integer() => {
let n: i64 = r
.to_integer()
.try_into()
.map_err(|_| unevaluable("RootOf index out of range"))?;
usize::try_from(n).map_err(|_| unevaluable("RootOf index must be non-negative"))?
}
Some(_) => return Err(unevaluable("RootOf index must be an integer")),
None => return Err(unevaluable("RootOf index must be numeric")),
};
let poly = crate::poly::polybridge::expr_to_poly(arena, poly_id, var_id)
.ok_or_else(|| unevaluable("could not convert RootOf expression to polynomial"))?;
let balls = crate::poly::roots::certified_roots(&poly, prec);
let found = balls.len();
let ball = balls.into_iter().nth(idx).ok_or_else(|| {
unevaluable(format!(
"RootOf index {idx} exceeds the {found} root(s) found"
))
})?;
let err = radius_error(&ball);
Ok((ball.value, err))
}
fn eval_rootsum(
arena: &Arena,
poly_id: ExprId,
body_id: ExprId,
sumvar_id: ExprId,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(Complex, accuracy::Bound), SymplexError> {
debug!("evalf: RootSum — attempting numerical evaluation via Aberth roots");
let poly =
crate::poly::polybridge::expr_to_poly(arena, poly_id, sumvar_id).ok_or_else(|| {
unevaluable(
"RootSum: cannot convert polynomial expression to Poly (may contain free symbols)",
)
})?;
let degree = poly.degree().unwrap_or(0);
let balls = crate::poly::roots::certified_roots(&poly, prec);
if balls.len() != degree {
debug!(
expected = degree,
found = balls.len(),
"evalf: RootSum — fewer roots than expected"
);
return Err(unevaluable(
"RootSum: not every root of the polynomial was found",
));
}
let n_roots = balls.len();
let post_order = walk::post_order_ids(arena, body_id);
let mut sum = SumAcc::new(prec);
for ball in balls {
let err = radius_error(&ball);
let seed = Some((sumvar_id, ball.value, err));
let (term, e) = evaluate_tree(arena, body_id, &post_order, seed, true, prec, rm, cc)?;
sum.add(&term, e, rm);
}
debug!(n_roots, "evalf: RootSum — evaluated");
Ok(sum.finish())
}
pub(crate) use crate::base::bigcomplex::{
c_abs, c_add, c_div, c_from_real, c_i, c_mul, c_neg, c_one, c_powi, c_sub, c_zero,
};
fn c_exp(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let exp_a = z.0.exp(prec, rm, cc);
let (sin_b, cos_b) = sin_cos(&z.1, prec, rm, cc);
(exp_a.mul(&cos_b, prec, rm), exp_a.mul(&sin_b, prec, rm))
}
fn sin_cos(x: &BigFloat, prec: usize, rm: RoundingMode, cc: &mut Consts) -> (BigFloat, BigFloat) {
let zero = BigFloat::new(prec);
let one = BigFloat::from_i32(1, prec);
if x.is_zero() {
return (zero, one);
}
let pi = cc.pi(prec, rm).clone();
let half_pi = pi.div(&BigFloat::from_i32(2, prec), prec, rm);
if *x == half_pi {
return (one, zero);
}
if *x == half_pi.neg() {
return (one.neg(), zero);
}
if *x == pi || *x == pi.neg() {
return (zero, one.neg());
}
(x.sin(prec, rm, cc), x.cos(prec, rm, cc))
}
fn c_ln(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let r = c_abs(z, prec, rm);
let theta = atan2_bf(&z.1, &z.0, prec, rm, cc);
(r.ln(prec, rm, cc), theta)
}
fn c_sin(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let (sin_a, cos_a) = sin_cos(&z.0, prec, rm, cc);
let cosh_b = z.1.cosh(prec, rm, cc);
let sinh_b = z.1.sinh(prec, rm, cc);
(sin_a.mul(&cosh_b, prec, rm), cos_a.mul(&sinh_b, prec, rm))
}
fn c_cos(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let (sin_a, cos_a) = sin_cos(&z.0, prec, rm, cc);
let cosh_b = z.1.cosh(prec, rm, cc);
let sinh_b = z.1.sinh(prec, rm, cc);
(
cos_a.mul(&cosh_b, prec, rm),
sin_a.mul(&sinh_b, prec, rm).neg(),
)
}
fn c_sinh(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let sinh_a = z.0.sinh(prec, rm, cc);
let cosh_a = z.0.cosh(prec, rm, cc);
let (sin_b, cos_b) = sin_cos(&z.1, prec, rm, cc);
(sinh_a.mul(&cos_b, prec, rm), cosh_a.mul(&sin_b, prec, rm))
}
fn c_cosh(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let cosh_a = z.0.cosh(prec, rm, cc);
let sinh_a = z.0.sinh(prec, rm, cc);
let (sin_b, cos_b) = sin_cos(&z.1, prec, rm, cc);
(cosh_a.mul(&cos_b, prec, rm), sinh_a.mul(&sin_b, prec, rm))
}
fn c_pow(base: &Complex, exp: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
if base.0.is_zero() && base.1.is_zero() {
if exp.0.is_zero() && exp.1.is_zero() {
return c_one(prec);
}
return c_zero(prec);
}
let ln_base = c_ln(base, prec, rm, cc);
let product = c_mul(exp, &ln_base, prec, rm);
c_exp(&product, prec, rm, cc)
}
fn c_sqrt(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
if z.0.is_zero() && z.1.is_zero() {
return c_zero(prec);
}
let half = c_from_real(
BigFloat::from_i32(1, prec).div(&BigFloat::from_i32(2, prec), prec, rm),
prec,
);
c_pow(z, &half, prec, rm, cc)
}
fn small_argument(
z: &Complex,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
f: fn(&Complex, usize, RoundingMode, &mut Consts) -> Complex,
) -> Complex {
let m = accuracy::mag(z).unwrap_or(0);
if m >= 0 {
return f(z, prec, rm, cc);
}
let lost = usize::try_from(-m).unwrap_or(usize::MAX);
if lost > prec + 8 {
return (
round_to(z.0.clone(), prec, rm),
round_to(z.1.clone(), prec, rm),
);
}
let v = f(z, prec + lost + 16, rm, cc);
(round_to(v.0, prec, rm), round_to(v.1, prec, rm))
}
fn c_asin(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
small_argument(z, prec, rm, cc, c_asin_at)
}
fn c_asin_at(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let i_unit = c_i(prec);
let one = c_one(prec);
let z_sq = c_mul(z, z, prec, rm);
let one_minus_z_sq = c_sub(&one, &z_sq, prec, rm);
let sqrt_term = c_sqrt(&one_minus_z_sq, prec, rm, cc);
let iz = c_mul(&i_unit, z, prec, rm);
let sum = sum_without_cancellation(&iz, &sqrt_term, prec, rm);
let ln_sum = c_ln(&sum, prec, rm, cc);
let neg_i = c_neg(&i_unit);
c_mul(&neg_i, &ln_sum, prec, rm)
}
fn sum_without_cancellation(a: &Complex, s: &Complex, prec: usize, rm: RoundingMode) -> Complex {
let plus = c_add(a, s, prec, rm);
let minus = c_sub(s, a, prec, rm);
if c_abs(&plus, prec, rm) >= c_abs(&minus, prec, rm) {
plus
} else {
c_div(&c_one(prec), &minus, prec, rm)
}
}
fn c_acos(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let half_pi_val = cc.pi(prec, rm).div(&BigFloat::from_i32(2, prec), prec, rm);
let half_pi = c_from_real(half_pi_val, prec);
let asin_z = c_asin(z, prec, rm, cc);
c_sub(&half_pi, &asin_z, prec, rm)
}
fn c_atan(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
small_argument(z, prec, rm, cc, c_atan_at)
}
fn c_atan_at(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let one = c_one(prec);
let iz = c_mul(&c_i(prec), z, prec, rm);
let ln_minus = c_ln(&c_sub(&one, &iz, prec, rm), prec, rm, cc);
let ln_plus = c_ln(&c_add(&one, &iz, prec, rm), prec, rm, cc);
let diff = c_sub(&ln_minus, &ln_plus, prec, rm);
let half = BigFloat::from_i32(1, prec).div(&BigFloat::from_i32(2, prec), prec, rm);
(
diff.1.mul(&half, prec, rm).neg(),
diff.0.mul(&half, prec, rm),
)
}
fn c_asinh(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
small_argument(z, prec, rm, cc, c_asinh_at)
}
fn c_asinh_at(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let one = c_one(prec);
let z_sq = c_mul(z, z, prec, rm);
let z_sq_plus_one = c_add(&z_sq, &one, prec, rm);
let sqrt_term = c_sqrt(&z_sq_plus_one, prec, rm, cc);
let sum = sum_without_cancellation(z, &sqrt_term, prec, rm);
c_ln(&sum, prec, rm, cc)
}
fn c_acosh(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let one = c_one(prec);
let root_plus = c_sqrt(&c_add(z, &one, prec, rm), prec, rm, cc);
let root_minus = c_sqrt(&c_sub(z, &one, prec, rm), prec, rm, cc);
let sum = c_add(z, &c_mul(&root_plus, &root_minus, prec, rm), prec, rm);
c_ln(&sum, prec, rm, cc)
}
fn c_atanh(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
small_argument(z, prec, rm, cc, c_atanh_at)
}
fn c_atanh_at(z: &Complex, prec: usize, rm: RoundingMode, cc: &mut Consts) -> Complex {
let one = c_one(prec);
let ln_plus = c_ln(&c_add(&one, z, prec, rm), prec, rm, cc);
let ln_minus = c_ln(&c_sub(&one, z, prec, rm), prec, rm, cc);
let diff = c_sub(&ln_plus, &ln_minus, prec, rm);
let half = BigFloat::from_i32(1, prec).div(&BigFloat::from_i32(2, prec), prec, rm);
(diff.0.mul(&half, prec, rm), diff.1.mul(&half, prec, rm))
}
fn trig_arg(
arena: &Arena,
inner: ExprId,
val: &Complex,
prec: usize,
rm: RoundingMode,
) -> (BigFloat, usize) {
if let ExprNode::Num(nid) = arena.node(inner)
&& let Some(e) = val.0.exponent()
&& e > 0
&& val.0.inexact()
{
let extra = usize::try_from(e).unwrap_or(0);
if extra <= arena.config.max_evalf_precision as usize {
return (ratio_to_bigfloat(arena.num(*nid), prec + extra, rm), extra);
}
}
(val.0.clone(), 0)
}
fn half_pi_bf(prec: usize, rm: RoundingMode, cc: &mut Consts) -> BigFloat {
cc.pi(prec, rm).div(&BigFloat::from_i32(2, prec), prec, rm)
}
fn abs_le_one(x: &BigFloat, prec: usize) -> bool {
x.abs() <= BigFloat::from_i32(1, prec)
}
const TRIG_ARG_MAX_INEXACT_EXPONENT: i64 = 64;
fn check_trig_arg(
x: &BigFloat,
extra: usize,
prec: usize,
arena: &Arena,
) -> Result<(), SymplexError> {
let Some(e) = x.exponent() else {
return Ok(());
};
let e = i64::from(e);
let cap = i64::from(arena.config.max_evalf_precision);
let bound =
TRIG_ARG_MAX_INEXACT_EXPONENT.saturating_add(i64::try_from(extra).unwrap_or(i64::MAX));
if e > cap || (x.inexact() && e > bound) {
let digits = u32::try_from(prec.saturating_sub(64) * 10 / 34).unwrap_or(u32::MAX);
return Err(SymplexError::PrecisionExhausted {
requested: digits,
achieved: 0,
});
}
Ok(())
}
fn atan2_bf(
y: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> BigFloat {
if x.is_zero() && y.is_zero() {
return BigFloat::new(prec);
}
if y.is_zero() {
return if x.is_negative() {
cc.pi(prec, rm).clone()
} else {
BigFloat::new(prec)
};
}
let pi_val = cc.pi(prec, rm);
let two = BigFloat::from_i32(2, prec);
let half_pi = pi_val.div(&two, prec, rm);
if x.is_zero() {
return if y.is_positive() {
half_pi
} else {
half_pi.neg()
};
}
let ratio = y.div(x, prec, rm);
let atan_val = ratio.atan(prec, rm, cc);
if x.is_positive() {
atan_val
} else {
let pi_val2 = cc.pi(prec, rm);
if y.is_negative() {
atan_val.sub(&pi_val2, prec, rm)
} else {
atan_val.add(&pi_val2, prec, rm)
}
}
}
fn bigfloat_to_f64(bf: &BigFloat, rm: RoundingMode, _cc: &mut Consts) -> Result<f64, SymplexError> {
bigfloat_to_f64_rounded(bf, rm)
}
fn bigfloat_to_f64_rounded(bf: &BigFloat, rm: RoundingMode) -> Result<f64, SymplexError> {
if bf.is_zero() {
return Ok(0.0);
}
if bf.is_inf_pos() {
return Ok(f64::INFINITY);
}
if bf.is_inf_neg() {
return Ok(f64::NEG_INFINITY);
}
let Some((words, _, sign, exponent, _)) = bf.as_raw_parts() else {
return Err(SymplexError::Unevaluable {
reason: "NaN in BigFloat to f64 conversion".into(),
});
};
let negative = sign == Sign::Neg;
let signed = |v: f64| if negative { -v } else { v };
let mut acc: u128 = 0;
let mut filled = 0usize;
let mut sticky = false;
for &w in words.iter().rev() {
if filled < 64 {
acc = (acc << WORD_BIT_SIZE) | w as u128;
filled += WORD_BIT_SIZE;
} else if w != 0 {
sticky = true;
}
}
let top: u64 = if filled >= 64 {
let extra = filled - 64;
if extra > 0 && acc & ((1u128 << extra) - 1) != 0 {
sticky = true;
}
(acc >> extra) as u64
} else {
(acc << (64 - filled)) as u64
};
if top & (1 << 63) == 0 {
return Ok(signed(0.0));
}
let mut unbiased = exponent as i64 - 1;
if unbiased > 1023 {
return Ok(signed(f64::INFINITY));
}
let keep = if unbiased >= -1022 {
53
} else {
unbiased + 1075
};
let (mant, above_half, exactly_half) = if keep <= 0 {
let at_half = top == 1 << 63 && !sticky;
(0u64, keep == 0 && !at_half, keep == 0 && at_half)
} else {
let shift = (64 - keep) as u32;
let mant = top >> shift;
let rem = top & ((1u64 << shift) - 1);
let half = 1u64 << (shift - 1);
(
mant,
rem > half || (rem == half && sticky),
rem == half && !sticky,
)
};
let round_up = match rm {
RoundingMode::None => false,
_ if above_half => true,
_ if !exactly_half => false,
RoundingMode::Up => !negative,
RoundingMode::Down => negative,
RoundingMode::ToZero => false,
RoundingMode::FromZero => true,
RoundingMode::ToEven => mant & 1 == 1,
RoundingMode::ToOdd => mant & 1 == 0,
};
let mut mant = mant + u64::from(round_up);
let bits = if keep == 53 {
if mant == 1 << 53 {
mant >>= 1;
unbiased += 1;
if unbiased > 1023 {
return Ok(signed(f64::INFINITY));
}
}
(((unbiased + 1023) as u64) << 52) | (mant & ((1 << 52) - 1))
} else {
mant
};
Ok(signed(f64::from_bits(bits)))
}
#[allow(dead_code)]
#[allow(clippy::excessive_precision)]
fn lanczos_gamma_f64(x: f64) -> Result<f64, SymplexError> {
if x.is_nan() || x.is_infinite() {
return Err(SymplexError::Unevaluable {
reason: "Gamma of special float value".into(),
});
}
if x < 0.5 {
let reflected = lanczos_gamma_f64(1.0 - x)?;
let sin_pi_x = (std::f64::consts::PI * x).sin();
if sin_pi_x.abs() < 1e-300 {
return Err(SymplexError::Unevaluable {
reason: "Gamma at non-positive integer pole".into(),
});
}
let result = std::f64::consts::PI / (sin_pi_x * reflected);
if result.is_finite() {
return Ok(result);
}
return Err(SymplexError::Unevaluable {
reason: "Gamma at pole".into(),
});
}
const COEFFICIENTS: [f64; 9] = [
0.99999999999980993,
676.5203681218851,
-1259.1392167224028,
771.32342877765313,
-176.61502916214059,
12.507343278686905,
-0.13857109526572012,
9.9843695780195716e-6,
1.5056327351493116e-7,
];
let x = x - 1.0; let t = x + 7.0 + 0.5;
let mut sum = COEFFICIENTS[0];
for (i, &c) in COEFFICIENTS[1..].iter().enumerate() {
sum += c / (x + i as f64 + 1.0);
}
let result = (2.0 * std::f64::consts::PI).sqrt() * t.powf(x + 0.5) * (-t).exp() * sum;
Ok(result)
}
#[allow(dead_code)]
fn erf_f64(x: f64) -> f64 {
if x.abs() < 4.0 {
let mut sum = 0.0;
let mut term = x; sum += term;
for n in 1..50 {
term *= -x * x / n as f64;
sum += term / (2 * n + 1) as f64;
}
sum * 2.0 / std::f64::consts::PI.sqrt()
} else {
let sign = x.signum();
let ax = x.abs();
let mut sum = 1.0;
let mut term = 1.0;
for n in 1..20 {
term *= -(2 * n - 1) as f64 / (2.0 * ax * ax);
if term.abs() < 1e-16 {
break;
}
sum += term;
}
let erfc = (-ax * ax).exp() / (ax * std::f64::consts::PI.sqrt()) * sum;
sign * (1.0 - erfc)
}
}
fn arb_log_gamma(
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Complex, SymplexError> {
if x.is_nan() || x.is_inf() {
return Err(unevaluable("LogGamma of special float value"));
}
if (x.is_int() || x.is_zero()) && !bf_strictly_positive(x) {
return Err(unevaluable("LogGamma at non-positive integer pole"));
}
let exact = x.mantissa_max_bit_len().unwrap_or(prec).max(prec) + 64;
let cap = cancellation_cap(prec);
let imaginary = if bf_strictly_positive(x) {
BigFloat::new(prec)
} else {
let pi = cc.pi(prec + 16, rm).clone();
round_to(pi.mul(&x.floor(), prec + 16, rm), prec, rm)
};
let zs0 = bigfloat_to_f64(&x.abs(), rm, cc)?.min(1e300) + 0.5 * (prec + 48) as f64 + 3.0;
let mut extra = (24 + terms_bits(2.0 * zs0 * (zs0.ln() + 1.0)).max(0) as usize).min(cap);
loop {
let wp = prec + extra;
let (value, largest) = if bf_strictly_positive(x) {
let v = stirling_log_gamma(x, wp, rm, cc)?;
let zs = bigfloat_to_f64(x, rm, cc)?.max(0.0) + 0.5 * wp as f64 + 2.0;
(v, 2.0 * zs * (zs.ln() + 1.0))
} else {
let n = x.add(&BigFloat::from_f64(0.5, 64), exact, rm).floor();
let d = x.sub(&n, exact, rm);
let pi = cc.pi(wp, rm).clone();
let ln_sin = pi.mul(&d, wp, rm).sin(wp, rm, cc).abs().ln(wp, rm, cc);
let one_minus_x = BigFloat::from_i32(1, 64).sub(x, exact, rm);
let lg1 = stirling_log_gamma(&one_minus_x, wp, rm, cc)?;
let ln_pi = pi.ln(wp, rm, cc);
let v = ln_pi.sub(&ln_sin, wp, rm).sub(&lg1, wp, rm);
let zs = bigfloat_to_f64(&one_minus_x, rm, cc)? + 0.5 * wp as f64 + 2.0;
let terms = 2.0 * zs * (zs.ln() + 1.0) + bigfloat_to_f64(&ln_sin, rm, cc)?.abs() + 2.0;
(v, terms)
};
let magnitude = match (value.exponent(), imaginary.exponent()) {
_ if value.is_zero() && imaginary.is_zero() => None,
(Some(e), Some(i)) if !value.is_zero() && !imaginary.is_zero() => Some(e.max(i)),
(Some(e), _) if !value.is_zero() => Some(e),
(_, i) => i,
};
let lost = match magnitude {
Some(e) => (terms_bits(largest) - i64::from(e)).max(0) as usize,
None => wp,
};
if lost + 16 <= extra {
return Ok((round_to(value, prec, rm), imaginary));
}
if extra >= cap {
return Err(special_exhausted(prec));
}
extra = (lost + 32).max(2 * extra).min(cap);
}
}
fn exact_bits(x: &BigFloat, prec: usize) -> usize {
let m = x.mantissa_max_bit_len().unwrap_or(prec).max(prec);
let below = x.exponent().map_or(0, |e| {
usize::try_from(64 - i64::from(e)).unwrap_or(0).min(1 << 16)
});
m + below + 64
}
fn terms_bits(v: f64) -> i64 {
if v.is_finite() && v > 0.0 {
v.log2().ceil() as i64 + 1
} else {
i64::MAX / 4
}
}
fn arb_digamma(
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_nan() || x.is_inf_pos() || x.is_inf_neg() {
return Err(SymplexError::Unevaluable {
reason: "Digamma of special float value".into(),
});
}
let cap = cancellation_cap(prec);
let mut extra = 20;
loop {
let wp = prec + extra;
let (value, largest) = digamma_wp(x, wp, rm, cc)?;
let lost = match (largest.exponent(), value.exponent()) {
_ if value.is_zero() => wp,
(Some(l), Some(v)) => usize::try_from(i64::from(l) - i64::from(v)).unwrap_or(0),
_ => 0,
};
if lost + 16 <= extra {
return Ok(round_to(value, prec, rm));
}
if extra >= cap {
return Err(special_exhausted(prec));
}
extra = (lost + 32).max(2 * extra).min(cap);
}
}
fn digamma_wp(
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(BigFloat, BigFloat), SymplexError> {
let pole = || SymplexError::Unevaluable {
reason: "Digamma at non-positive integer pole".into(),
};
let exact = x.mantissa_max_bit_len().unwrap_or(wp).max(wp) + 64;
let one = BigFloat::from_i32(1, wp);
if x.is_negative() {
let n = x.add(&BigFloat::from_f64(0.5, 64), exact, rm).floor();
let d = x.sub(&n, exact, rm);
if d.is_zero() {
return Err(pole());
}
let pi = cc.pi(wp, rm).clone();
let pi_d = pi.mul(&d, wp, rm);
let pi_cot = pi.mul(
&pi_d.cos(wp, rm, cc).div(&pi_d.sin(wp, rm, cc), wp, rm),
wp,
rm,
);
let one_minus_x = BigFloat::from_i32(1, 64).sub(x, exact, rm);
let (psi, largest) = digamma_wp(&one_minus_x, wp, rm, cc)?;
let value = psi.sub(&pi_cot, wp, rm);
let largest = if pi_cot.abs() > largest {
pi_cot.abs()
} else {
largest
};
return Ok((value, largest));
}
if x.is_zero() {
return Err(pole());
}
let threshold = BigFloat::from_i32(i32::try_from(wp / 3).unwrap_or(i32::MAX).max(10), wp);
let mut shift = BigFloat::new(wp);
let mut y = x.clone();
while bf_lt(&y, &threshold) {
shift = shift.add(&one.div(&y, wp, rm), wp, rm);
y = y.add(&one, exact, rm);
}
let ln_y = y.ln(wp, rm, cc);
let largest = if shift > ln_y.abs() {
shift.clone()
} else {
ln_y.abs()
};
let two = BigFloat::from_i32(2, wp);
let mut result = ln_y
.sub(&one.div(&y.mul(&two, wp, rm), wp, rm), wp, rm)
.sub(&shift, wp, rm);
let y2 = y.mul(&y, wp, rm);
let mut y_pow = y2.clone();
let floor = largest.exponent().map_or(i64::MIN / 2, i64::from) - wp as i64 - 2;
let mut prev: Option<i32> = None;
for k in 1..=wp {
let b2k = bernoulli::even(k);
let denom = y_pow.mul(&BigFloat::from_i128(2 * k as i128, wp), wp, rm);
let term = ratio_to_bigfloat(&b2k, wp, rm).div(&denom, wp, rm);
match term.exponent() {
Some(te) if i64::from(te) < floor => break,
Some(te) if prev.is_some_and(|p| te > p + 2) => break, Some(te) => prev = Some(te),
None => {}
}
result = result.sub(&term, wp, rm);
y_pow = y_pow.mul(&y2, wp, rm);
}
Ok((result, largest))
}
fn stirling_log_gamma(
z: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
tracing::debug!(prec, "stirling_log_gamma: entry");
let guard = 20; let wp = prec + guard;
let z_approx = bigfloat_to_f64(z, rm, cc)?;
let threshold = ((wp as f64) * 0.5).ceil() as i64;
let shift = if z_approx < threshold as f64 {
(threshold - z_approx.floor() as i64).max(0) as usize
} else {
0
};
tracing::trace!(
z_approx,
threshold,
shift,
"stirling_log_gamma: argument reduction"
);
let shift_bf = BigFloat::from_i128(shift as i128, wp);
let z_shifted = z.add(&shift_bf, wp, rm);
let half = BigFloat::from_f64(0.5, wp);
let z_minus_half = z_shifted.sub(&half, wp, rm);
let log_z = z_shifted.ln(wp, rm, cc);
let mut result = z_minus_half.mul(&log_z, wp, rm);
result = result.sub(&z_shifted, wp, rm);
let two = BigFloat::from_i32(2, wp);
let pi_val = cc.pi(wp, rm).clone();
let two_pi = two.mul(&pi_val, wp, rm);
let half_log_2pi = two_pi.ln(wp, rm, cc).mul(&half, wp, rm);
result = result.add(&half_log_2pi, wp, rm);
let z_sq = z_shifted.mul(&z_shifted, wp, rm);
let mut z_pow = z_shifted.clone();
let max_terms = ((wp as f64) * 0.35) as usize + 10;
let mut prev_term_exp: Option<i32> = None;
for k in 1..=max_terms {
let b2k = bernoulli::even(k);
if b2k.is_zero() {
continue;
}
if k > 1 {
z_pow = z_pow.mul(&z_sq, wp, rm);
}
let b2k_bf = ratio_to_bigfloat(&b2k, wp, rm);
let denom_int = (2 * k * (2 * k - 1)) as i128;
let denom = BigFloat::from_i128(denom_int, wp);
let denom_full = denom.mul(&z_pow, wp, rm);
let term = b2k_bf.div(&denom_full, wp, rm);
if let Some(t_exp) = term.exponent() {
if let Some(prev) = prev_term_exp
&& t_exp > prev + 10
{
tracing::trace!(k, "stirling series: diverging, stopping");
break;
}
prev_term_exp = Some(t_exp);
if let Some(r_exp) = result.exponent()
&& (r_exp as i64 - t_exp as i64) > wp as i64
{
tracing::trace!(k, "stirling series: converged");
break;
}
}
result = result.add(&term, wp, rm);
}
if shift > 0 {
let mut product = BigFloat::from_i32(1, wp);
for i in 0..shift {
let z_plus_i = z.add(&BigFloat::from_i128(i as i128, wp), wp, rm);
product = product.mul(&z_plus_i, wp, rm);
}
result = result.sub(&product.ln(wp, rm, cc), wp, rm);
}
Ok(result)
}
fn arb_gamma_real(
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_zero() || (x.is_negative() && x.is_int()) {
return Err(SymplexError::Unevaluable {
reason: "Gamma at non-positive integer pole".into(),
});
}
if !x.is_negative() {
tracing::debug!(prec, "arb_gamma_real: positive argument");
let log_gamma = stirling_log_gamma(x, prec, rm, cc)?;
Ok(log_gamma.exp(prec, rm, cc))
} else {
tracing::debug!(prec, "arb_gamma_real: reflection formula");
let exact = x.mantissa_max_bit_len().unwrap_or(prec).max(prec) + 64;
let one_minus_x = BigFloat::from_i32(1, 64).sub(x, exact, rm);
let log_gamma_1mx = stirling_log_gamma(&one_minus_x, prec, rm, cc)?;
let gamma_1mx = log_gamma_1mx.exp(prec, rm, cc);
let n = x.add(&BigFloat::from_f64(0.5, 64), exact, rm).floor();
let d = x.sub(&n, exact, rm);
let pi_val = cc.pi(prec, rm).clone();
let mut sin_pi_x = pi_val.mul(&d, prec, rm).sin(prec, rm, cc);
let odd = !n
.div(&BigFloat::from_i32(2, 64), exact, rm)
.fract()
.is_zero();
if odd {
sin_pi_x = sin_pi_x.neg();
}
if sin_pi_x.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Gamma at non-positive integer pole".into(),
});
}
let denom = sin_pi_x.mul(&gamma_1mx, prec, rm);
Ok(pi_val.div(&denom, prec, rm))
}
}
fn erfc_asymptotic(ax: &BigFloat, wp: usize, rm: RoundingMode, cc: &mut Consts) -> BigFloat {
let x_sq = ax.mul(ax, wp, rm);
let (sum, x_sqrt_pi) = erfc_asymptotic_series(ax, wp, rm, cc);
let exp_neg_x_sq = x_sq.neg().exp(wp, rm, cc);
exp_neg_x_sq.mul(&sum, wp, rm).div(&x_sqrt_pi, wp, rm)
}
fn erfc_asymptotic_series(
ax: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> (BigFloat, BigFloat) {
let x_sq = ax.mul(ax, wp, rm);
let two_x_sq = x_sq.mul(&BigFloat::from_i32(2, wp), wp, rm);
let mut sum = BigFloat::from_i32(1, wp);
let mut term = BigFloat::from_i32(1, wp);
let mut prev_abs: Option<BigFloat> = None;
let max_terms = wp + 40;
for n in 1..=max_terms {
let factor = BigFloat::from_i128(2 * n as i128 - 1, wp);
term = term.mul(&factor, wp, rm).div(&two_x_sq, wp, rm).neg();
let t_abs = term.abs();
if let Some(ref p) = prev_abs
&& bf_gt(&t_abs, p)
{
break; }
sum = sum.add(&term, wp, rm);
if negligible(&term, &sum, wp) {
break;
}
prev_abs = Some(t_abs);
}
let sqrt_pi = cc.pi(wp, rm).clone().sqrt(wp, rm);
let x_sqrt_pi = ax.mul(&sqrt_pi, wp, rm);
(sum, x_sqrt_pi)
}
fn arb_erf(
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_zero() {
return Ok(BigFloat::new(prec));
}
let guard = 32;
let wp = prec + guard;
let x_approx = bigfloat_to_f64(x, rm, cc)?;
let threshold = ((wp as f64) * 0.35).sqrt() + 2.0;
if x_approx.abs() < threshold {
let x_sq_f = x_approx * x_approx;
let cancel = (x_sq_f * std::f64::consts::LOG2_E).ceil() as usize + 8;
let wpt = wp + cancel;
let mut xw = x.clone();
let _ = xw.set_precision(wpt, rm);
let neg_x_sq = xw.mul(&xw, wpt, rm).neg();
let mut prod = xw.clone(); let mut sum = xw.clone();
let max_terms = (6.0 * x_sq_f) as usize + wpt + 60;
let mut converged = false;
for n in 1..=max_terms {
prod = prod.mul(&neg_x_sq, wpt, rm);
prod = prod.div(&BigFloat::from_i128(n as i128, wpt), wpt, rm);
let divisor = BigFloat::from_i128(2 * n as i128 + 1, wpt);
let term = prod.div(&divisor, wpt, rm);
if negligible(&term, &sum, wpt) {
converged = true;
break;
}
sum = sum.add(&term, wpt, rm);
}
if !converged {
return Err(series_did_not_converge("erf", max_terms));
}
let two = BigFloat::from_i32(2, wpt);
let pi_val = cc.pi(wpt, rm).clone();
let sqrt_pi = pi_val.sqrt(wpt, rm);
let two_over_sqrt_pi = two.div(&sqrt_pi, wpt, rm);
Ok(round_to(sum.mul(&two_over_sqrt_pi, wpt, rm), wp, rm))
} else {
let mut ax = x.abs();
let _ = ax.set_precision(wp, rm);
let erfc_val = erfc_asymptotic(&ax, wp, rm, cc);
let one = BigFloat::from_i32(1, wp);
let erf_val = one.sub(&erfc_val, wp, rm);
if x.is_negative() {
Ok(erf_val.neg())
} else {
Ok(erf_val)
}
}
}
fn arb_euler_gamma(
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let extra = 30;
let wp = prec + extra;
let p = ((wp as f64 / 4.0) * std::f64::consts::LN_2).log2().ceil() as u64 + 1;
let n: i128 = 1i128 << p;
let n_sq = n * n;
let two_bf = BigFloat::from_i32(2, wp);
let ln2 = two_bf.ln(wp, rm, cc);
let p_bf = BigFloat::from_i128(p as i128, wp);
let neg_p_ln2 = p_bf.mul(&ln2, wp, rm).neg();
let mut a = neg_p_ln2.clone();
let mut u = neg_p_ln2;
let one = BigFloat::from_i32(1, wp);
let mut b = one.clone();
let mut v = one.clone();
let n_sq_bf = BigFloat::from_i128(n_sq, wp);
let mut k: i128 = 1;
loop {
let k_bf = BigFloat::from_i128(k, wp);
let k_sq_bf = k_bf.mul(&k_bf, wp, rm);
b = b.mul(&n_sq_bf, wp, rm).div(&k_sq_bf, wp, rm);
a = a.mul(&n_sq_bf, wp, rm).div(&k_bf, wp, rm);
a = a.add(&b, wp, rm).div(&k_bf, wp, rm);
u = u.add(&a, wp, rm);
v = v.add(&b, wp, rm);
let a_small = a.exponent().is_none_or(|e| e < -(wp as i32) + 10);
let b_small = b.exponent().is_none_or(|e| e < -(wp as i32) + 10);
if a_small && b_small {
tracing::trace!(k, "arb_euler_gamma: converged");
break;
}
k += 1;
if k > 10 * (wp as i128) {
return Err(SymplexError::ComputationFailed {
operation: "euler_gamma",
reason: "Brent-McMillan did not converge".into(),
});
}
}
Ok(u.div(&v, prec, rm))
}
fn hankel_pq(
order: &BigFloat,
z: &BigFloat,
prec: usize,
rm: RoundingMode,
_cc: &mut Consts,
) -> (BigFloat, BigFloat, bool) {
let one = BigFloat::from_i32(1, prec);
let four = BigFloat::from_i32(4, prec);
let eight = BigFloat::from_i32(8, prec);
let four_nu_sq = four.mul(&order.mul(order, prec, rm), prec, rm);
let z_inv = one.div(z, prec, rm);
let mut p_sum = BigFloat::new(prec);
let mut q_sum = BigFloat::new(prec);
let mut a_k = one.clone(); let mut z_power = one.clone();
let max_terms = (3 * prec + 100).min(100_000);
let mut prev_a_abs: Option<BigFloat> = None;
let mut converged = false;
for k in 0..max_terms {
if k > 0 {
let two_km1 = BigFloat::from_i32((2 * k as i32) - 1, prec);
let two_km1_sq = two_km1.mul(&two_km1, prec, rm);
let numer = four_nu_sq.sub(&two_km1_sq, prec, rm);
let k_bf = BigFloat::from_i32(k as i32, prec);
let denom = eight.mul(&k_bf, prec, rm);
a_k = a_k.mul(&numer, prec, rm).div(&denom, prec, rm);
if a_k.is_zero() {
converged = true;
break;
}
z_power = z_power.mul(&z_inv, prec, rm);
}
let term = a_k.mul(&z_power, prec, rm);
let term_abs = term.abs();
if let Some(ref prev) = prev_a_abs
&& term_abs.partial_cmp(prev) == Some(std::cmp::Ordering::Greater)
&& k > 2
{
tracing::trace!(k, "hankel_pq: optimal truncation (terms diverging)");
break;
}
let p_exp = p_sum.exponent().unwrap_or(0);
let q_exp = q_sum.exponent().unwrap_or(0);
let ref_exp = p_exp.max(q_exp);
let t_exp = term.exponent().unwrap_or(i32::MIN / 2);
if k > 0 && (ref_exp as i64 - t_exp as i64) > prec as i64 {
tracing::trace!(k, "hankel_pq: converged (term negligible)");
converged = true;
break;
}
prev_a_abs = Some(term_abs);
if k % 2 == 0 {
if (k / 2) % 2 == 0 {
p_sum = p_sum.add(&term, prec, rm);
} else {
p_sum = p_sum.sub(&term, prec, rm);
}
} else {
if ((k - 1) / 2) % 2 == 0 {
q_sum = q_sum.add(&term, prec, rm);
} else {
q_sum = q_sum.sub(&term, prec, rm);
}
}
}
(p_sum, q_sum, converged)
}
fn hankel_threshold(wp: usize, order_abs: f64) -> f64 {
(wp as f64 + 3.0 * order_abs) * std::f64::consts::LN_2 / 2.0 + 4.0
}
fn bessel_jy_hankel(
order: &BigFloat,
x: &BigFloat,
want_j: bool,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<BigFloat> {
let wp = wp + x.exponent().unwrap_or(0).max(0) as usize;
let mut xw = x.clone();
let _ = xw.set_precision(wp, rm);
let mut nu = order.clone();
let _ = nu.set_precision(wp, rm);
let (p_val, q_val, converged) = hankel_pq(&nu, &xw, wp, rm, cc);
if !converged {
tracing::trace!("bessel_jy_hankel: expansion did not reach target precision");
return None;
}
let pi = cc.pi(wp, rm).clone();
let two = BigFloat::from_i32(2, wp);
let four = BigFloat::from_i32(4, wp);
let two_over_pi_x = two.div(&pi.mul(&xw, wp, rm), wp, rm);
let amplitude = two_over_pi_x.sqrt(wp, rm);
let nu_pi_half = nu.mul(&pi, wp, rm).div(&two, wp, rm);
let pi_quarter = pi.div(&four, wp, rm);
let phase = xw.sub(&nu_pi_half, wp, rm).sub(&pi_quarter, wp, rm);
let cos_phase = phase.cos(wp, rm, cc);
let sin_phase = phase.sin(wp, rm, cc);
let combo = if want_j {
cos_phase
.mul(&p_val, wp, rm)
.sub(&sin_phase.mul(&q_val, wp, rm), wp, rm)
} else {
sin_phase
.mul(&p_val, wp, rm)
.add(&cos_phase.mul(&q_val, wp, rm), wp, rm)
};
Some(amplitude.mul(&combo, wp, rm))
}
fn arb_bessel_j(
order: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let classic = arb_bessel_j_classic(order, x, prec, rm, cc);
debye_fallback(bessel_debye::Kind::J, order, x, prec, rm, cc, classic)
}
fn debye_fallback(
kind: bessel_debye::Kind,
order: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
classic: Result<BigFloat, SymplexError>,
) -> Result<BigFloat, SymplexError> {
use bessel_debye::Kind;
let Err(err) = classic else {
return classic;
};
let integer = order.is_int();
let negative_x = x.is_negative();
if order.is_negative()
&& !integer
&& matches!(kind, Kind::J | Kind::Y)
&& !negative_x
&& let Some(v) = bessel_reflected(kind, &order.abs(), x, prec, rm, cc)
{
return v;
}
let nu = match kind {
Kind::K => order.abs(),
Kind::I if integer => order.abs(),
_ if order.is_negative() => return Err(err),
_ => order.clone(),
};
if negative_x && !(integer && matches!(kind, Kind::J | Kind::I)) {
return Err(err);
}
match bessel_jy_large(kind, &nu, &x.abs(), prec, rm, cc) {
Some(Ok(v)) if negative_x && bessel_order::is_odd(order) => Ok(v.neg()),
Some(r) => r,
None => Err(err),
}
}
fn bessel_jy_large(
kind: bessel_debye::Kind,
nu: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<Result<BigFloat, SymplexError>> {
use bessel_debye::Kind;
match bessel_debye::debye(kind, nu, x, prec, rm, cc) {
Some(r) => Some(r),
None if matches!(kind, Kind::J | Kind::Y) => bessel_recur::recur(kind, nu, x, prec, rm, cc),
None => None,
}
}
fn bessel_reflected(
kind: bessel_debye::Kind,
nu: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<Result<BigFloat, SymplexError>> {
use bessel_debye::Kind;
let mut wp = prec + 32;
let cap = 4 * prec + 1024;
loop {
let j = match bessel_jy_large(Kind::J, nu, x, wp, rm, cc)? {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let y = match bessel_jy_large(Kind::Y, nu, x, wp, rm, cc)? {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let n = nu.floor();
let f = nu.sub(&n, exact_bits(nu, wp), rm);
let pi = cc.pi(wp + 16, rm).clone();
let angle = f.mul(&pi, wp + 16, rm);
let odd = bessel_order::is_odd(&n);
let sign = |v: BigFloat| if odd { v.neg() } else { v };
let (c, s) = (sign(angle.cos(wp, rm, cc)), sign(angle.sin(wp, rm, cc)));
let (a, b) = match kind {
Kind::J => (c.mul(&j, wp, rm), s.mul(&y, wp, rm).neg()),
_ => (s.mul(&j, wp, rm), c.mul(&y, wp, rm)),
};
let v = a.add(&b, wp, rm);
let big = j
.exponent()
.unwrap_or(i32::MIN / 2)
.max(y.exponent().unwrap_or(i32::MIN / 2));
let lost = if v.is_zero() {
wp as i64
} else {
i64::from(big) - i64::from(v.exponent().unwrap_or(i32::MIN / 2)) + 1
};
if lost + 6 + (prec as i64) <= wp as i64 {
return Some(Ok(round_to(v, prec, rm)));
}
if wp >= cap {
return None;
}
wp = (wp + lost.max(32) as usize).min(cap);
}
}
fn arb_bessel_j_classic(
order: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let guard = 32;
let wp = prec + guard;
if x.is_zero() {
let order_f64 = bigfloat_to_f64(order, rm, cc)?;
return if order_f64.abs() < 1e-15 {
Ok(BigFloat::from_i32(1, prec))
} else if order_f64 > 0.0 {
Ok(BigFloat::new(prec))
} else {
Err(SymplexError::Unevaluable {
reason: "BesselJ at x=0 with negative order".into(),
})
};
}
refuse_bessel_argument(x, prec)?;
if x.is_negative() && !order.is_int() {
return Err(SymplexError::Unevaluable {
reason: "BesselJ of negative argument with non-integer order is complex".into(),
});
}
let x_f64 = bigfloat_to_f64(x, rm, cc)?;
let order_f64 = bigfloat_to_f64(order, rm, cc)?;
let order_int = order_f64.round() as i64;
let is_int_order = order.is_int();
let large = order_f64.abs() >= bessel_order::LARGE_ORDER;
if is_int_order && order.is_negative() {
let j = arb_bessel_j(&order.neg(), x, prec, rm, cc)?;
return Ok(if bessel_order::is_odd(order) {
j.neg()
} else {
j
});
}
if x_f64.abs() >= hankel_threshold(wp, order_f64.abs()) {
if x.is_negative() {
if !is_int_order {
return Err(SymplexError::Unevaluable {
reason: "BesselJ of negative argument with non-integer order is complex".into(),
});
}
let j = arb_bessel_j(order, &x.abs(), prec, rm, cc)?;
return Ok(if bessel_order::is_odd(order) {
j.neg()
} else {
j
});
}
if let Some(j) = bessel_jy_hankel(order, x, true, wp, rm, cc) {
return Ok(round_to(j, prec, rm));
}
}
{
let wp = wp + series_guard_bits(x_f64.abs(), prec)?;
let mut xw = x.clone();
let _ = xw.set_precision(wp, rm);
let mut order_w = order.clone();
let _ = order_w.set_precision(wp, rm);
let order = &order_w;
let two = BigFloat::from_i32(2, wp);
let x_half = xw.div(&two, wp, rm);
let x_half_sq = x_half.mul(&x_half, wp, rm);
let neg_x_half_sq = x_half_sq.neg();
let mut sum = BigFloat::new(wp);
let (prefix, mut term) = if large {
let bounds = bessel_order::series_bounds(order_f64, x_f64.abs(), true);
let Some(p) = bessel_order::power_over_gamma(
order,
&x_half.abs(),
bounds,
"besselj",
wp,
rm,
cc,
)?
else {
return Ok(BigFloat::new(prec));
};
let p = if x.is_negative() && bessel_order::is_odd(order) {
p.neg()
} else {
p
};
(p, BigFloat::from_i32(1, wp))
} else {
let prefix = if is_int_order && order_int >= 0 {
let mut p = BigFloat::from_i32(1, wp);
for _ in 0..order_int {
p = p.mul(&x_half, wp, rm);
}
p
} else {
let ln_xh = x_half.abs().ln(wp, rm, cc);
let nu_ln = order.mul(&ln_xh, wp, rm);
nu_ln.exp(wp, rm, cc)
};
let gamma_nu1 =
arb_gamma_real(&order.add(&BigFloat::from_i32(1, wp), wp, rm), wp, rm, cc)?;
(prefix, BigFloat::from_i32(1, wp).div(&gamma_nu1, wp, rm))
};
sum = sum.add(&term, wp, rm);
let max_terms = (x_f64.abs() * 1.5) as usize + wp + 40;
for k in 1..=max_terms {
let k_bf = BigFloat::from_i32(k as i32, wp);
let nu_plus_k = order.add(&k_bf, wp, rm);
let denom = k_bf.mul(&nu_plus_k, wp, rm);
term = term.mul(&neg_x_half_sq, wp, rm);
term = term.div(&denom, wp, rm);
if (k as f64) > x_f64.abs() / 2.0
&& let (Some(t_exp), Some(s_exp)) = (term.exponent(), sum.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
tracing::trace!(k, "arb_bessel_j: series converged");
break;
}
sum = sum.add(&term, wp, rm);
}
let r = prefix.mul(&sum, wp, rm);
if large && (r.is_zero() || r.is_inf()) {
return Err(SymplexError::Unevaluable {
reason: "besselj of large order: the value leaves the arbitrary-precision \
exponent range"
.into(),
});
}
Ok(round_to(r, prec, rm))
}
}
fn arb_bessel_y(
order: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let classic = arb_bessel_y_classic(order, x, prec, rm, cc);
debye_fallback(bessel_debye::Kind::Y, order, x, prec, rm, cc, classic)
}
fn arb_bessel_y_classic(
order: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let guard = 32;
let wp = prec + guard;
if x.is_zero() || x.is_negative() {
return Err(SymplexError::Unevaluable {
reason: "BesselY undefined at x ≤ 0".into(),
});
}
refuse_bessel_argument(x, prec)?;
let x_f64 = bigfloat_to_f64(x, rm, cc)?;
let order_f64 = bigfloat_to_f64(order, rm, cc)?;
let order_int = order_f64.round() as i64;
let is_int_order = order.is_int();
if is_int_order && order.is_negative() {
let y = arb_bessel_y(&order.neg(), x, prec, rm, cc)?;
return Ok(if bessel_order::is_odd(order) {
y.neg()
} else {
y
});
}
if x_f64 >= hankel_threshold(wp, order_f64.abs())
&& let Some(y) = bessel_jy_hankel(order, x, false, wp, rm, cc)
{
tracing::trace!("arb_bessel_y: used Hankel P/Q expansion");
return Ok(round_to(y, prec, rm));
}
if !is_int_order {
return Err(SymplexError::Unevaluable {
reason: format!(
"BesselY for non-integer order {order_f64} not yet implemented in small-|x| regime"
),
});
}
if order_f64 >= bessel_order::LARGE_ORDER {
return bessel_order::neumann_leading(bessel_order::Second::Y, order, x, prec, rm, cc)
.unwrap_or_else(|| Err(large_order_refused("bessely", order_f64, x_f64)));
}
let n = order_int as usize;
let guard = match series_guard_bits(x_f64, prec) {
Ok(g) => g,
Err(e) => {
return bessel_order::neumann_leading(bessel_order::Second::Y, order, x, prec, rm, cc)
.unwrap_or(Err(e));
}
};
let wp = wp + guard;
let mut xw = x.clone();
let _ = xw.set_precision(wp, rm);
let pi = cc.pi(wp, rm).clone();
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let inv_pi = one.div(&pi, wp, rm);
let x_half = xw.div(&two, wp, rm);
let x_half_sq = x_half.mul(&x_half, wp, rm);
let ln_x_half = x_half.ln(wp, rm, cc);
let euler_gamma = arb_euler_gamma(wp, rm, cc)?;
let n_bf = BigFloat::from_i128(n as i128, wp);
let j_n = arb_bessel_j(&n_bf, &xw, wp, rm, cc)?;
let mut finite = BigFloat::new(wp);
if n > 0 {
let mut f = NeumannCoefficients::new(n);
let mut pow = one.clone();
for k in 0..n {
if k > 0 {
pow = pow.mul(&x_half_sq, wp, rm);
}
let coeff = f.next(wp, rm);
finite = finite.add(&coeff.mul(&pow, wp, rm), wp, rm);
}
let x_half_pow_neg_n = one.div(&x_half.powi(n, wp, rm), wp, rm);
finite = inv_pi
.mul(&x_half_pow_neg_n, wp, rm)
.mul(&finite, wp, rm)
.neg();
}
let log_term = two
.mul(&inv_pi, wp, rm)
.mul(&ln_x_half, wp, rm)
.mul(&j_n, wp, rm);
let mut h_k = BigFloat::new(wp);
let mut h_nk = BigFloat::new(wp);
for m in 1..=n {
h_nk = h_nk.add(
&one.div(&BigFloat::from_i128(m as i128, wp), wp, rm),
wp,
rm,
);
}
let n_fact = factorial_exact(n);
let mut inv_fact = one.div(&ratio_to_bigfloat(&n_fact, wp, rm), wp, rm);
let neg_x_half_sq = x_half_sq.neg();
let mut pow = one.clone();
let mut series = BigFloat::new(wp);
let two_gamma = euler_gamma.mul(&two, wp, rm);
let max_terms = (x_f64.abs() * 2.0) as usize + wp + 40;
for k in 0..=max_terms {
if k > 0 {
let k_bf = BigFloat::from_i128(k as i128, wp);
let nk_bf = BigFloat::from_i128((n + k) as i128, wp);
h_k = h_k.add(&one.div(&k_bf, wp, rm), wp, rm);
h_nk = h_nk.add(&one.div(&nk_bf, wp, rm), wp, rm);
inv_fact = inv_fact.div(&k_bf.mul(&nk_bf, wp, rm), wp, rm);
pow = pow.mul(&neg_x_half_sq, wp, rm);
}
let psi_sum = h_k.add(&h_nk, wp, rm).sub(&two_gamma, wp, rm);
let term = psi_sum.mul(&pow, wp, rm).mul(&inv_fact, wp, rm);
series = series.add(&term, wp, rm);
if k > 2
&& let (Some(t_exp), Some(s_exp)) = (term.exponent(), series.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
tracing::trace!(k, "arb_bessel_y: Neumann series converged");
break;
}
}
let series_term = inv_pi
.mul(&x_half.powi(n, wp, rm), wp, rm)
.mul(&series, wp, rm)
.neg();
let y = finite.add(&log_term, wp, rm).add(&series_term, wp, rm);
Ok(round_to(y, prec, rm))
}
fn factorial_exact(n: usize) -> Ratio<BigInt> {
Ratio::from_integer(crate::base::combinatorics::factorial(n as u64))
}
struct NeumannCoefficients {
n: usize,
k: usize,
value: BigInt,
}
impl NeumannCoefficients {
fn new(n: usize) -> Self {
let value = crate::base::combinatorics::factorial(n.saturating_sub(1) as u64);
NeumannCoefficients { n, k: 0, value }
}
fn next(&mut self, wp: usize, rm: RoundingMode) -> BigFloat {
let (n, k) = (self.n, self.k);
self.k += 1;
if k > 0 {
let (a, b) = (BigInt::from((n - k) as u64), BigInt::from(k as u64));
if 2 * k < n {
self.value = &self.value / (a * b);
} else if 2 * k < n + 2 {
self.value = (n - k..=k).map(|j| BigInt::from(j as u64)).product();
} else {
self.value = &self.value * a * b;
}
}
if 2 * k < n {
ratio_to_bigfloat(&Ratio::from_integer(self.value.clone()), wp, rm)
} else {
ratio_to_bigfloat(&Ratio::new_raw(BigInt::from(1), self.value.clone()), wp, rm)
}
}
}
fn large_order_refused(what: &str, order: f64, x: f64) -> SymplexError {
SymplexError::NotImplemented(format!(
"{what} of integer order {order:e} at {x:e} (above sqrt(2n)): the Neumann series would \
take n terms (no uniform asymptotic expansion)"
))
}
fn arb_catalan(prec: usize, rm: RoundingMode, cc: &mut Consts) -> Result<BigFloat, SymplexError> {
let wp = prec + 32;
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let three = BigFloat::from_i32(3, wp);
let eight = BigFloat::from_i32(8, wp);
let sqrt3 = three.sqrt(wp, rm);
let ln_term = two.add(&sqrt3, wp, rm).ln(wp, rm, cc);
let pi = cc.pi(wp, rm).clone();
let first = pi.div(&eight, wp, rm).mul(&ln_term, wp, rm);
let mut b = one.clone();
let mut sum = BigFloat::new(wp);
let max_terms = wp / 2 + 64;
for n in 0..max_terms {
let two_n_plus_1 = BigFloat::from_i128(2 * n as i128 + 1, wp);
let denom = two_n_plus_1.mul(&two_n_plus_1, wp, rm);
let term = b.div(&denom, wp, rm);
sum = sum.add(&term, wp, rm);
if let (Some(t_exp), Some(s_exp)) = (term.exponent(), sum.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
break;
}
let n_plus_1 = BigFloat::from_i128(n as i128 + 1, wp);
let two_2n_plus_1 = two.mul(&two_n_plus_1, wp, rm);
b = b.mul(&n_plus_1, wp, rm).div(&two_2n_plus_1, wp, rm);
}
let second = three.div(&eight, wp, rm).mul(&sum, wp, rm);
let mut g = first.add(&second, wp, rm);
g.set_precision(prec, rm)
.map_err(|e| SymplexError::ComputationFailed {
operation: "catalan",
reason: format!("precision adjustment failed: {e:?}"),
})?;
Ok(g)
}
fn cancellation_guard_bits(x_abs: f64) -> usize {
(x_abs * std::f64::consts::LOG2_E).ceil() as usize + 32
}
fn series_guard_bits(x_abs: f64, prec: usize) -> Result<usize, SymplexError> {
let guard = cancellation_guard_bits(x_abs);
if !x_abs.is_finite() || guard > cancellation_cap(prec) {
return Err(special_exhausted(prec));
}
Ok(guard)
}
fn refuse_bessel_argument(x: &BigFloat, prec: usize) -> Result<(), SymplexError> {
let cap = i64::try_from(cancellation_cap(prec)).unwrap_or(i64::MAX);
if x.is_inf() || x.exponent().is_some_and(|e| i64::from(e) > cap) {
return Err(special_exhausted(prec));
}
Ok(())
}
fn si_ci_asymptotic_fg(x: &BigFloat, wp: usize, rm: RoundingMode) -> (BigFloat, BigFloat) {
let one = BigFloat::from_i32(1, wp);
let inv_x = one.div(x, wp, rm);
let inv_x2 = inv_x.mul(&inv_x, wp, rm);
let mut f_sum = BigFloat::new(wp);
let mut g_sum = BigFloat::new(wp);
let mut t_f = one.clone();
let mut t_g = one.clone();
let mut prev_f: Option<BigFloat> = None;
let max_terms = wp * 2 + 100;
for k in 0..max_terms {
if k > 0 {
let a = BigFloat::from_i128((2 * k as i128 - 1) * (2 * k as i128), wp);
let b = BigFloat::from_i128((2 * k as i128) * (2 * k as i128 + 1), wp);
t_f = t_f.mul(&a, wp, rm).mul(&inv_x2, wp, rm);
t_g = t_g.mul(&b, wp, rm).mul(&inv_x2, wp, rm);
}
if let Some(ref p) = prev_f
&& t_f.abs().cmp(p).is_some_and(|c| c > 0)
{
break;
}
if k % 2 == 0 {
f_sum = f_sum.add(&t_f, wp, rm);
g_sum = g_sum.add(&t_g, wp, rm);
} else {
f_sum = f_sum.sub(&t_f, wp, rm);
g_sum = g_sum.sub(&t_g, wp, rm);
}
if let (Some(t_exp), Some(s_exp)) = (t_f.exponent(), f_sum.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
break;
}
prev_f = Some(t_f.abs());
}
(f_sum.mul(&inv_x, wp, rm), g_sum.mul(&inv_x2, wp, rm))
}
fn arb_si_ci(
x: &BigFloat,
want_si: bool,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(BigFloat, BigFloat), SymplexError> {
if x.is_nan() || x.is_inf_pos() || x.is_inf_neg() {
return Err(SymplexError::Unevaluable {
reason: "Si/Ci of special float value".into(),
});
}
let x_f64 = bigfloat_to_f64(x, rm, cc)?;
let x_abs = x_f64.abs();
let base_wp = prec + 32;
if x.is_zero() {
return if want_si {
Ok((BigFloat::new(prec), BigFloat::new(prec)))
} else {
Err(SymplexError::Unevaluable {
reason: "Ci(0) is zoo".into(),
})
};
}
if !want_si && x.is_negative() {
return Err(SymplexError::Unevaluable {
reason: "Ci requires a positive argument here".into(),
});
}
let e = usize::try_from(x.exponent().map_or(0, i64::from).max(0)).unwrap_or(0);
if e > base_wp + 2 {
if !want_si {
if e > cancellation_cap(prec) {
return Err(special_exhausted(prec));
}
} else {
let half_pi = cc
.pi(base_wp, rm)
.div(&BigFloat::from_i32(2, base_wp), base_wp, rm);
let si = if x.is_negative() {
half_pi.neg()
} else {
half_pi
};
return Ok((round_to(si, prec, rm), BigFloat::new(prec)));
}
}
let switch = (base_wp as f64) * std::f64::consts::LN_2;
if x_abs > switch {
let wp = base_wp;
let ax = x.abs();
let (f, g) = si_ci_asymptotic_fg(&ax, wp, rm);
let sin_x = ax.sin(wp, rm, cc);
let cos_x = ax.cos(wp, rm, cc);
let pi = cc.pi(wp, rm).clone();
let two = BigFloat::from_i32(2, wp);
let half_pi = pi.div(&two, wp, rm);
let mut si =
half_pi
.sub(&f.mul(&cos_x, wp, rm), wp, rm)
.sub(&g.mul(&sin_x, wp, rm), wp, rm);
if x.is_negative() {
si = si.neg();
}
let ci = f.mul(&sin_x, wp, rm).sub(&g.mul(&cos_x, wp, rm), wp, rm);
return Ok((round_to(si, prec, rm), round_to(ci, prec, rm)));
}
let wp = base_wp + cancellation_guard_bits(x_abs);
let mut xw = x.clone();
let _ = xw.set_precision(wp, rm);
let x2 = xw.mul(&xw, wp, rm);
let max_terms = (x_abs * 1.5) as usize + wp + 40;
let si = if want_si {
let mut t = xw.clone();
let mut sum = BigFloat::new(wp);
for k in 0..max_terms {
if k > 0 {
let d = BigFloat::from_i128((2 * k as i128) * (2 * k as i128 + 1), wp);
t = t.mul(&x2, wp, rm).div(&d, wp, rm);
}
let term = t.div(&BigFloat::from_i128(2 * k as i128 + 1, wp), wp, rm);
if k % 2 == 0 {
sum = sum.add(&term, wp, rm);
} else {
sum = sum.sub(&term, wp, rm);
}
if let (Some(t_exp), Some(s_exp)) = (term.exponent(), sum.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
break;
}
}
round_to(sum, prec, rm)
} else {
BigFloat::new(prec)
};
let ci = if want_si {
BigFloat::new(prec)
} else {
let gamma = arb_euler_gamma(wp, rm, cc)?;
let ln_x = xw.ln(wp, rm, cc);
let mut sum = BigFloat::new(wp);
let mut u = BigFloat::from_i32(1, wp);
for k in 1..max_terms {
let d = BigFloat::from_i128((2 * k as i128 - 1) * (2 * k as i128), wp);
u = u.mul(&x2, wp, rm).div(&d, wp, rm);
let term = u.div(&BigFloat::from_i128(2 * k as i128, wp), wp, rm);
if k % 2 == 0 {
sum = sum.add(&term, wp, rm);
} else {
sum = sum.sub(&term, wp, rm);
}
if let (Some(t_exp), Some(s_exp)) = (term.exponent(), sum.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
break;
}
}
let ci = gamma.add(&ln_x, wp, rm).add(&sum, wp, rm);
round_to(ci, prec, rm)
};
Ok((si, ci))
}
fn arb_ei(
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_nan() || x.is_inf_pos() || x.is_inf_neg() || x.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Ei of zero or special float value".into(),
});
}
let x_f64 = bigfloat_to_f64(x, rm, cc)?;
let x_abs = x_f64.abs();
let base_wp = prec + 32;
let switch = (base_wp as f64) * std::f64::consts::LN_2;
if x_abs > switch {
let wp = base_wp;
let (inv_x, sum) = ei_asymptotic_series(x, wp, rm);
let ex = x.exp(wp, rm, cc);
let r = ex.mul(&inv_x, wp, rm).mul(&sum, wp, rm);
return Ok(round_to(r, prec, rm));
}
let cap = cancellation_cap(prec);
let mut extra = if x.is_negative() {
cancellation_guard_bits(2.0 * x_abs)
} else {
32
};
loop {
let wp = prec + extra;
let mut xw = x.clone();
let _ = xw.set_precision(wp, rm);
let gamma = arb_euler_gamma(wp, rm, cc)?;
let ln_abs_x = xw.abs().ln(wp, rm, cc);
let mut largest = gamma.abs().max(&ln_abs_x.abs());
let mut v = BigFloat::from_i32(1, wp); let mut sum = BigFloat::new(wp);
let max_terms = (x_abs * 3.0) as usize + wp + 40;
for k in 1..max_terms {
let k_bf = BigFloat::from_i128(k as i128, wp);
v = v.mul(&xw, wp, rm).div(&k_bf, wp, rm);
let term = v.div(&k_bf, wp, rm);
sum = sum.add(&term, wp, rm);
largest = largest.max(&term.abs()).max(&sum.abs());
if let (Some(t_exp), Some(s_exp)) = (term.exponent(), sum.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
&& (k as f64) > x_abs
{
break;
}
}
let r = gamma.add(&ln_abs_x, wp, rm).add(&sum, wp, rm);
let lost = match (largest.exponent(), r.exponent()) {
(Some(l), Some(v)) if !r.is_zero() => (i64::from(l) - i64::from(v)).max(0) as usize,
_ => wp,
};
if lost + 32 <= extra {
return Ok(round_to(r, prec, rm));
}
if extra >= cap {
return Err(special_exhausted(prec));
}
extra = (lost + 64).min(cap).max(extra + 32);
}
}
fn ei_asymptotic_series(x: &BigFloat, wp: usize, rm: RoundingMode) -> (BigFloat, BigFloat) {
let one = BigFloat::from_i32(1, wp);
let inv_x = one.div(x, wp, rm);
let mut term = one.clone();
let mut sum = BigFloat::new(wp);
let mut prev: Option<BigFloat> = None;
let max_terms = wp * 2 + 100;
for k in 0..max_terms {
if k > 0 {
term = term
.mul(&BigFloat::from_i128(k as i128, wp), wp, rm)
.mul(&inv_x, wp, rm);
}
if let Some(ref p) = prev
&& term.abs().cmp(p).is_some_and(|c| c > 0)
{
break;
}
sum = sum.add(&term, wp, rm);
if let (Some(t_exp), Some(s_exp)) = (term.exponent(), sum.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
break;
}
prev = Some(term.abs());
}
(inv_x, sum)
}
fn round_to(mut v: BigFloat, prec: usize, rm: RoundingMode) -> BigFloat {
let _ = v.set_precision(prec, rm);
v
}
fn bf_pow(b: &BigFloat, e: &BigFloat, wp: usize, rm: RoundingMode, cc: &mut Consts) -> BigFloat {
if e.is_zero() {
return BigFloat::from_i32(1, wp);
}
if e.is_int()
&& let Ok(ef) = bigfloat_to_f64(e, rm, cc)
&& ef.abs() < 1.0e9
{
let n = ef.abs() as usize;
let p = b.powi(n, wp, rm);
return if ef < 0.0 {
BigFloat::from_i32(1, wp).div(&p, wp, rm)
} else {
p
};
}
let ln_b = b.ln(wp + 32, rm, cc);
ln_b.mul(e, wp + 32, rm).exp(wp, rm, cc)
}
fn arb_zeta(
s: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if s.is_nan() || s.is_inf_neg() {
return Err(SymplexError::Unevaluable {
reason: "zeta of special float value".into(),
});
}
if s.is_inf_pos() {
return Ok(BigFloat::from_i32(1, prec));
}
if s.is_zero() {
return Ok(BigFloat::from_f64(-0.5, prec));
}
let s_f64 = bigfloat_to_f64(s, rm, cc)?;
if *s == BigFloat::from_i32(1, 64) {
return Err(SymplexError::Unevaluable {
reason: "zeta(1) is a pole".into(),
});
}
let wp = prec + 32;
if s.is_negative() {
let s_round = s_f64.round();
if (s_f64 - s_round).abs() < 1e-14 && (s_round as i64) % 2 == 0 {
let mut sw = s.clone();
let _ = sw.set_precision(wp, rm);
let diff = sw.sub(&BigFloat::from_f64(s_round, wp), wp, rm);
if diff.is_zero() {
return Ok(BigFloat::new(prec));
}
}
if s.exponent().is_some_and(|e| i64::from(e) > wp as i64) {
return Err(special_exhausted(prec));
}
let mut sw = s.clone();
let _ = sw.set_precision(wp, rm);
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let pi = cc.pi(wp, rm).clone();
let one_minus_s = BigFloat::from_i32(1, 64).sub(s, exact_bits(s, wp), rm);
let two_pow_s = bf_pow(&two, &sw, wp, rm, cc);
let s_minus_1 = sw.sub(&one, wp, rm);
let pi_pow = bf_pow(&pi, &s_minus_1, wp, rm, cc);
let half_pi_s = pi.mul(&sw, wp, rm).div(&two, wp, rm);
let sin_term = half_pi_s.sin(wp, rm, cc);
let gamma_term = arb_gamma_real(&one_minus_s, wp, rm, cc)?;
let zeta_term = arb_zeta_borwein(&one_minus_s, wp, rm, cc)?;
let r = two_pow_s
.mul(&pi_pow, wp, rm)
.mul(&sin_term, wp, rm)
.mul(&gamma_term, wp, rm)
.mul(&zeta_term, wp, rm);
return Ok(round_to(r, prec, rm));
}
let r = arb_zeta_borwein(s, wp, rm, cc)?;
Ok(round_to(r, prec, rm))
}
fn arb_zeta_borwein(
s: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let eta = arb_eta_borwein(s, wp, rm, cc)?;
let one_minus_s = BigFloat::from_i32(1, 64).sub(s, exact_bits(s, wp), rm);
let denom_factor = one_minus_pow2(&one_minus_s, wp, rm, cc);
if denom_factor.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "zeta(1) is a pole".into(),
});
}
Ok(eta.div(&denom_factor, wp, rm))
}
fn one_minus_pow2(t: &BigFloat, wp: usize, rm: RoundingMode, cc: &mut Consts) -> BigFloat {
if t.is_zero() {
return BigFloat::new(wp);
}
let u = cc.ln_2(wp + 8, rm).mul(t, wp + 8, rm);
if u.exponent().is_some_and(|e| e > -2) {
return BigFloat::from_i32(1, wp).sub(&u.exp(wp + 8, rm, cc), wp, rm);
}
let mut term = u.clone();
let mut sum = u.clone();
for k in 2..(wp + 16) {
term = term
.mul(&u, wp + 8, rm)
.div(&BigFloat::from_i128(k as i128, wp + 8), wp + 8, rm);
sum = sum.add(&term, wp + 8, rm);
if negligible(&term, &sum, wp + 8) {
break;
}
}
round_to(sum.neg(), wp, rm)
}
fn arb_eta_borwein(
s: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let n = ((wp as f64) * std::f64::consts::LN_2 / (3.0 + 8f64.sqrt()).ln()).ceil() as usize + 4;
let mut d: Vec<Ratio<BigInt>> = Vec::with_capacity(n + 1);
let mut acc = Ratio::<BigInt>::zero();
let mut term = Ratio::from_integer(BigInt::from(1));
for i in 0..=n {
if i > 0 {
let numer = BigInt::from((n + i - 1) as u64)
* BigInt::from(4u64)
* BigInt::from((n - i + 1) as u64);
let denom = BigInt::from((2 * i - 1) as u64) * BigInt::from((2 * i) as u64);
term *= Ratio::new(numer, denom);
}
acc += &term;
d.push(acc.clone());
}
let d_n = ratio_to_bigfloat(&d[n], wp, rm);
let mut sum = BigFloat::new(wp);
let s_is_int = s.is_int();
let s_int = if s_is_int {
bigfloat_to_f64(s, rm, cc)?.round() as usize
} else {
0
};
let powers: Vec<BigFloat> = if s_is_int {
Vec::new()
} else {
let mut spf = vec![0usize; n + 1];
let mut table: Vec<BigFloat> = Vec::with_capacity(n + 1);
table.push(BigFloat::new(wp));
for m in 1..=n {
if m >= 2 && spf[m] == 0 {
for multiple in (m..=n).step_by(m) {
if spf[multiple] == 0 {
spf[multiple] = m;
}
}
}
let v = if m == 1 {
BigFloat::from_i32(1, wp)
} else if spf[m] == m {
bf_pow(&BigFloat::from_i128(m as i128, wp), s, wp + 8, rm, cc)
} else {
table[spf[m]].mul(&table[m / spf[m]], wp + 8, rm)
};
table.push(v);
}
table
};
for k in 0..n {
let coeff = ratio_to_bigfloat(&(&d[k] - &d[n]), wp, rm);
let kp1 = BigFloat::from_i128(k as i128 + 1, wp);
let kp1_pow_s = if s_is_int {
kp1.powi(s_int, wp, rm)
} else {
powers[k + 1].clone()
};
let term = coeff.div(&kp1_pow_s, wp, rm);
if k % 2 == 0 {
sum = sum.add(&term, wp, rm);
} else {
sum = sum.sub(&term, wp, rm);
}
}
Ok(sum.div(&d_n, wp, rm).neg())
}
fn arb_polygamma(
n: u32,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
_cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_nan() || x.is_inf_pos() || x.is_inf_neg() {
return Err(SymplexError::Unevaluable {
reason: "polygamma of special float value".into(),
});
}
let cap = cancellation_cap(prec);
let mut extra = 32;
loop {
let (value, largest) = polygamma_wp(n, x, prec + extra, rm)?;
let lost = match (largest.exponent(), value.exponent()) {
_ if value.is_zero() => prec + extra,
(Some(l), Some(v)) => usize::try_from(i64::from(l) - i64::from(v)).unwrap_or(0),
_ => 0,
};
if lost + 16 <= extra {
return Ok(round_to(value, prec, rm));
}
if extra >= cap {
return Err(special_exhausted(prec));
}
extra = (lost + 32).max(2 * extra).min(cap);
}
}
fn times_integer(q: &Ratio<BigInt>, r: &BigInt) -> Ratio<BigInt> {
use num_integer::Integer;
use num_traits::One;
let mut num = r.clone();
let mut den = q.denom().clone();
loop {
let g = den.gcd(&(&num % &den));
if g.is_one() || g.is_zero() {
break;
}
num /= &g;
den /= &g;
}
Ratio::new_raw(q.numer() * num, den)
}
fn polygamma_wp(
n: u32,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
) -> Result<(BigFloat, BigFloat), SymplexError> {
let exact = exact_bits(x, wp);
let n_us = n as usize;
let one = BigFloat::from_i32(1, wp);
let nm1_fact = factorial_exact(n_us.saturating_sub(1));
let n_fact = Ratio::from_integer(nm1_fact.numer() * BigInt::from(n_us as u64));
let n_fact_bf = ratio_to_bigfloat(&n_fact, wp, rm);
let nm1_fact_bf = ratio_to_bigfloat(&nm1_fact, wp, rm);
let outer_sign_neg = n_us.is_multiple_of(2);
let threshold_val = ((wp / 3).max(n_us) + 10) as i128;
let threshold = BigFloat::from_i128(threshold_val, wp);
let max_steps = 10 * wp + 1_000_000;
if bf_lt(x, &BigFloat::from_i128(-(max_steps as i128), wp)) {
return Err(SymplexError::NotImplemented(
"polygamma of an argument far left of 0 (the argument shift would take more \
than a million steps)"
.into(),
));
}
let mut xw = x.clone();
let mut shift_sum = BigFloat::new(wp); let mut largest_inv = BigFloat::new(wp);
let mut steps: usize = 0;
while xw.sub(&threshold, wp, rm).is_negative() {
if xw.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "polygamma at non-positive integer pole".into(),
});
}
let p = xw.powi(n_us + 1, wp, rm);
let inv = one.div(&p, wp, rm);
if inv.abs() > largest_inv {
largest_inv = inv.abs();
}
shift_sum = shift_sum.add(&inv, wp, rm);
xw = xw.add(&one, exact, rm);
steps += 1;
if steps > max_steps {
return Err(SymplexError::NotImplemented(
"polygamma: the argument shift did not terminate".into(),
));
}
}
let shift_term = n_fact_bf.mul(&shift_sum, wp, rm);
let x_pow_n = xw.powi(n_us, wp, rm);
let x_pow_np1 = x_pow_n.mul(&xw, wp, rm);
let two = BigFloat::from_i32(2, wp);
let mut bracket = nm1_fact_bf.div(&x_pow_n, wp, rm);
bracket = bracket.add(&n_fact_bf.div(&two.mul(&x_pow_np1, wp, rm), wp, rm), wp, rm);
let x2 = xw.mul(&xw, wp, rm);
let mut x_pow = x_pow_n.mul(&x2, wp, rm); let mut ratio_fact = n_fact.numer() * BigInt::from(n_us as u64 + 1) / BigInt::from(2);
let mut prev_term_exp: Option<i32> = None;
let max_terms = wp / 2 + 20;
for k in 1..=max_terms {
if k > 1 {
let a =
BigInt::from((2 * k + n_us - 2) as u64) * BigInt::from((2 * k + n_us - 1) as u64);
let b = BigInt::from((2 * k - 1) as u64) * BigInt::from((2 * k) as u64);
ratio_fact = ratio_fact * a / b;
x_pow = x_pow.mul(&x2, wp, rm);
}
let b2k = bernoulli::even(k);
let coeff = ratio_to_bigfloat(×_integer(&b2k, &ratio_fact), wp, rm);
let term = coeff.div(&x_pow, wp, rm);
if let Some(t_exp) = term.exponent() {
if let Some(prev) = prev_term_exp
&& t_exp > prev + 10
{
break; }
prev_term_exp = Some(t_exp);
if let Some(b_exp) = bracket.exponent()
&& (b_exp as i64 - t_exp as i64) > wp as i64
{
bracket = bracket.add(&term, wp, rm);
break;
}
}
bracket = bracket.add(&term, wp, rm);
}
let total = bracket.add(&shift_term, wp, rm);
let result = if outer_sign_neg { total.neg() } else { total };
let largest_shift = n_fact_bf.mul(&largest_inv, wp, rm);
let largest = if largest_shift > bracket.abs() {
largest_shift
} else {
bracket.abs()
};
Ok((result, largest))
}
fn bessel_i_series(
order: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let two = BigFloat::from_i32(2, wp);
let x_half = x.div(&two, wp, rm);
let x_half_sq = x_half.mul(&x_half, wp, rm);
let order_f64 = bigfloat_to_f64(order, rm, cc)?;
let order_int = order_f64.round() as i64;
let is_int_order = order.is_int();
if x.is_zero() {
return if is_int_order && order_int == 0 {
Ok(BigFloat::from_i32(1, wp))
} else if order_f64 > 0.0 {
Ok(BigFloat::new(wp))
} else {
Err(SymplexError::Unevaluable {
reason: "BesselI at x=0 with negative order".into(),
})
};
}
if !is_int_order && x.is_negative() {
return Err(SymplexError::Unevaluable {
reason: "BesselI of negative argument with non-integer order is complex".into(),
});
}
let one = BigFloat::from_i32(1, wp);
let x_f64 = bigfloat_to_f64(x, rm, cc)?.abs();
let large = order_f64.abs() >= bessel_order::LARGE_ORDER;
let (prefix, eff_order, mut term) = if large {
let eff_order = if is_int_order {
order.abs()
} else {
order.clone()
};
let eff_f64 = bigfloat_to_f64_rounded(&eff_order, rm)?;
let bounds = bessel_order::series_bounds(eff_f64, x_f64, false);
let Some(p) = bessel_order::power_over_gamma(
&eff_order,
&x_half.abs(),
bounds,
"besseli",
wp,
rm,
cc,
)?
else {
return Ok(BigFloat::new(wp));
};
bessel_order::refuse_long_i_series(eff_f64, x_f64)?;
let p = if x.is_negative() && bessel_order::is_odd(order) {
p.neg()
} else {
p
};
(p, eff_order, one.clone())
} else {
let prefix = if is_int_order && order_int >= 0 {
x_half.powi(order_int as usize, wp, rm)
} else if is_int_order {
x_half.powi((-order_int) as usize, wp, rm)
} else {
let ln_xh = x_half.ln(wp, rm, cc);
order.mul(&ln_xh, wp, rm).exp(wp, rm, cc)
};
let eff_order = if is_int_order && order_int < 0 {
BigFloat::from_i128((-order_int) as i128, wp)
} else {
order.clone()
};
let gamma_nu1 = arb_gamma_real(&eff_order.add(&one, wp, rm), wp, rm, cc)?;
let term = one.div(&gamma_nu1, wp, rm);
(prefix, eff_order, term)
};
let mut sum = term.clone();
let max_terms = (x_f64 * 2.0) as usize + wp + 40;
for k in 1..=max_terms {
let k_bf = BigFloat::from_i128(k as i128, wp);
let nu_plus_k = eff_order.add(&k_bf, wp, rm);
let denom = k_bf.mul(&nu_plus_k, wp, rm);
term = term.mul(&x_half_sq, wp, rm).div(&denom, wp, rm);
sum = sum.add(&term, wp, rm);
if let (Some(t_exp), Some(s_exp)) = (term.exponent(), sum.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
break;
}
}
let r = prefix.mul(&sum, wp, rm);
if large && (r.is_zero() || r.is_inf()) {
return Err(SymplexError::Unevaluable {
reason: "besseli of large order: the value leaves the arbitrary-precision exponent \
range"
.into(),
});
}
Ok(r)
}
fn bessel_i_asymptotic(
order: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<BigFloat> {
let one = BigFloat::from_i32(1, wp);
let eight = BigFloat::from_i32(8, wp);
let four_nu_sq = BigFloat::from_i32(4, wp).mul(&order.mul(order, wp, rm), wp, rm);
let inv_x = one.div(x, wp, rm);
let mut a = one.clone(); let mut sum = one.clone();
let mut prev = one.clone();
let mut converged = false;
for k in 1..(4 * wp + 100) {
let two_km1 = BigFloat::from_i128(2 * k as i128 - 1, wp);
let numer = four_nu_sq.sub(&two_km1.mul(&two_km1, wp, rm), wp, rm);
let denom = eight.mul(&BigFloat::from_i128(k as i128, wp), wp, rm);
a = a
.mul(&numer, wp, rm)
.div(&denom, wp, rm)
.mul(&inv_x, wp, rm);
if a.is_zero() {
converged = true;
break;
}
let size = a.abs();
if size.cmp(&prev).is_some_and(|c| c > 0) || size.exponent().is_some_and(|e| e > 0) {
break;
}
sum = if k % 2 == 1 {
sum.sub(&a, wp, rm)
} else {
sum.add(&a, wp, rm)
};
if let (Some(t), Some(s)) = (a.exponent(), sum.exponent())
&& i64::from(s) - i64::from(t) > wp as i64
{
converged = true;
break;
}
prev = size;
}
if !converged {
return None;
}
let two_pi_x = cc
.pi(wp, rm)
.mul(&BigFloat::from_i32(2, wp), wp, rm)
.mul(x, wp, rm);
let amplitude = two_pi_x.sqrt(wp, rm).reciprocal(wp, rm);
Some(x.exp(wp, rm, cc).mul(&litude, wp, rm).mul(&sum, wp, rm))
}
fn arb_bessel_i(
order: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let classic = arb_bessel_i_classic(order, x, prec, rm, cc);
debye_fallback(bessel_debye::Kind::I, order, x, prec, rm, cc, classic)
}
fn arb_bessel_i_classic(
order: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let wp = prec + 32;
if x.is_inf() {
return Err(special_exhausted(prec));
}
let x_abs = bigfloat_to_f64(x, rm, cc)?.abs();
let order_f64 = bigfloat_to_f64(order, rm, cc)?;
if x_abs >= hankel_threshold(wp, order_f64.abs()) {
if x.is_negative() {
if !order.is_int() {
return Err(SymplexError::Unevaluable {
reason: "BesselI of negative argument with non-integer order is complex".into(),
});
}
let v = arb_bessel_i(order, &x.abs(), prec, rm, cc)?;
return Ok(if bessel_order::is_odd(order) {
v.neg()
} else {
v
});
}
if let Some(v) = bessel_i_asymptotic(order, x, wp, rm, cc) {
return Ok(round_to(v, prec, rm));
}
if !x_abs.is_finite() || x_abs > cancellation_cap(prec) as f64 {
return Err(special_exhausted(prec));
}
}
let r = bessel_i_series(order, x, wp, rm, cc)?;
Ok(round_to(r, prec, rm))
}
fn bessel_k_asymptotic(
order: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<BigFloat> {
let one = BigFloat::from_i32(1, wp);
let four = BigFloat::from_i32(4, wp);
let eight = BigFloat::from_i32(8, wp);
let four_nu_sq = four.mul(&order.mul(order, wp, rm), wp, rm);
let inv_x = one.div(x, wp, rm);
let mut a_k = one.clone();
let mut sum = one.clone();
let mut prev: Option<BigFloat> = None;
let mut converged = false;
let max_terms = wp * 2 + 100;
for k in 1..max_terms {
let two_km1 = BigFloat::from_i128(2 * k as i128 - 1, wp);
let two_km1_sq = two_km1.mul(&two_km1, wp, rm);
let numer = four_nu_sq.sub(&two_km1_sq, wp, rm);
let denom = eight.mul(&BigFloat::from_i128(k as i128, wp), wp, rm);
a_k = a_k
.mul(&numer, wp, rm)
.div(&denom, wp, rm)
.mul(&inv_x, wp, rm);
if a_k.is_zero() {
converged = true;
break;
}
let alternating = two_km1_sq.cmp(&four_nu_sq).is_some_and(|c| c > 0);
if alternating
&& let Some(ref p) = prev
&& a_k.abs().cmp(p).is_some_and(|c| c > 0)
{
break;
}
sum = sum.add(&a_k, wp, rm);
if let (Some(t_exp), Some(s_exp)) = (a_k.exponent(), sum.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
converged = true;
break;
}
prev = Some(a_k.abs());
}
if !converged {
return None;
}
let pi = cc.pi(wp, rm).clone();
let two = BigFloat::from_i32(2, wp);
let amp = pi.div(&two.mul(x, wp, rm), wp, rm).sqrt(wp, rm);
let e_neg_x = x.neg().exp(wp, rm, cc);
Some(amp.mul(&e_neg_x, wp, rm).mul(&sum, wp, rm))
}
fn arb_bessel_k(
order: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let classic = arb_bessel_k_classic(order, x, prec, rm, cc);
debye_fallback(bessel_debye::Kind::K, order, x, prec, rm, cc, classic)
}
fn arb_bessel_k_classic(
order: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_zero() || x.is_negative() || x.is_nan() {
return Err(SymplexError::Unevaluable {
reason: "BesselK undefined at x ≤ 0".into(),
});
}
let base_wp = prec + 32;
let x_f64 = bigfloat_to_f64(x, rm, cc)?;
let order_f64 = bigfloat_to_f64(order, rm, cc)?;
let order_int = order_f64.round() as i64;
let is_int_order = order.is_int();
let order_abs = if is_int_order && order_f64.abs() < bessel_order::LARGE_ORDER {
BigFloat::from_i128(order_int.unsigned_abs() as i128, base_wp)
} else {
order.abs()
};
if 2.0 * x_f64 > (base_wp as f64) * std::f64::consts::LN_2
&& let Some(r) = bessel_k_asymptotic(&order_abs, x, base_wp, rm, cc)
{
return Ok(round_to(r, prec, rm));
}
let wp = base_wp + 2 * series_guard_bits(x_f64, prec)?;
let mut xw = x.clone();
let _ = xw.set_precision(wp, rm);
if !is_int_order {
let exact = exact_bits(order, wp);
let n = order.add(&BigFloat::from_f64(0.5, 64), exact, rm).floor();
let d = order.sub(&n, exact, rm);
let odd = !n
.div(&BigFloat::from_i32(2, 64), exact, rm)
.fract()
.is_zero();
let neg_order = order.neg();
let cap = cancellation_cap(prec);
let mut extra = wp - prec;
loop {
let wq = prec + extra;
let mut xq = x.clone();
let _ = xq.set_precision(wq, rm);
let i_neg = bessel_i_series(&neg_order, &xq, wq, rm, cc)?;
let i_pos = bessel_i_series(order, &xq, wq, rm, cc)?;
let diff = i_neg.sub(&i_pos, wq, rm);
let big = if i_neg.abs() > i_pos.abs() {
i_neg.abs()
} else {
i_pos.abs()
};
let lost = match (big.exponent(), diff.exponent()) {
_ if diff.is_zero() => wq,
(Some(b), Some(e)) => usize::try_from(i64::from(b) - i64::from(e)).unwrap_or(0),
_ => 0,
};
if lost + 16 <= extra {
let pi = cc.pi(wq, rm).clone();
let two = BigFloat::from_i32(2, wq);
let mut sin_nu_pi = d.mul(&pi, wq, rm).sin(wq, rm, cc);
if odd {
sin_nu_pi = sin_nu_pi.neg();
}
let r = pi
.div(&two, wq, rm)
.mul(&diff, wq, rm)
.div(&sin_nu_pi, wq, rm);
return Ok(round_to(r, prec, rm));
}
if extra >= cap {
return Err(special_exhausted(prec));
}
extra = (lost + 48).max(2 * extra).min(cap);
}
}
if order_f64.abs() >= bessel_order::LARGE_ORDER {
return bessel_order::neumann_leading(bessel_order::Second::K, &order_abs, x, prec, rm, cc)
.unwrap_or_else(|| Err(large_order_refused("besselk", order_f64, x_f64)));
}
let n = order_int.unsigned_abs() as usize;
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let half = one.div(&two, wp, rm);
let x_half = xw.div(&two, wp, rm);
let x_half_sq = x_half.mul(&x_half, wp, rm);
let ln_x_half = x_half.ln(wp, rm, cc);
let gamma = arb_euler_gamma(wp, rm, cc)?;
let n_bf = BigFloat::from_i128(n as i128, wp);
let i_n = bessel_i_series(&n_bf, &xw, wp, rm, cc)?;
let mut finite = BigFloat::new(wp);
if n > 0 {
let mut f = NeumannCoefficients::new(n);
let mut pow = one.clone(); let neg_x_half_sq = x_half_sq.neg();
for k in 0..n {
if k > 0 {
pow = pow.mul(&neg_x_half_sq, wp, rm);
}
let coeff = f.next(wp, rm);
finite = finite.add(&coeff.mul(&pow, wp, rm), wp, rm);
}
let x_half_pow_neg_n = one.div(&x_half.powi(n, wp, rm), wp, rm);
finite = half.mul(&x_half_pow_neg_n, wp, rm).mul(&finite, wp, rm);
}
let log_term = ln_x_half.mul(&i_n, wp, rm);
let log_term = if n.is_multiple_of(2) {
log_term.neg()
} else {
log_term
};
let mut h_k = BigFloat::new(wp); let mut h_nk = BigFloat::new(wp); for m in 1..=n {
h_nk = h_nk.add(
&one.div(&BigFloat::from_i128(m as i128, wp), wp, rm),
wp,
rm,
);
}
let n_fact = factorial_exact(n);
let mut inv_fact = one.div(&ratio_to_bigfloat(&n_fact, wp, rm), wp, rm);
let mut pow = one.clone();
let mut series = BigFloat::new(wp);
let two_gamma = gamma.mul(&two, wp, rm);
let max_terms = (x_f64 * 2.0) as usize + wp + 40;
for k in 0..=max_terms {
if k > 0 {
let k_bf = BigFloat::from_i128(k as i128, wp);
let nk_bf = BigFloat::from_i128((n + k) as i128, wp);
h_k = h_k.add(&one.div(&k_bf, wp, rm), wp, rm);
h_nk = h_nk.add(&one.div(&nk_bf, wp, rm), wp, rm);
inv_fact = inv_fact.div(&k_bf.mul(&nk_bf, wp, rm), wp, rm);
pow = pow.mul(&x_half_sq, wp, rm);
}
let psi_sum = h_k.add(&h_nk, wp, rm).sub(&two_gamma, wp, rm);
let term = psi_sum.mul(&pow, wp, rm).mul(&inv_fact, wp, rm);
series = series.add(&term, wp, rm);
if k > 2
&& let (Some(t_exp), Some(s_exp)) = (term.exponent(), series.exponent())
&& (s_exp as i64 - t_exp as i64) > wp as i64
{
break;
}
}
let series_term = half
.mul(&x_half.powi(n, wp, rm), wp, rm)
.mul(&series, wp, rm);
let series_term = if n.is_multiple_of(2) {
series_term
} else {
series_term.neg()
};
let r = finite.add(&log_term, wp, rm).add(&series_term, wp, rm);
Ok(round_to(r, prec, rm))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OrthoPoly {
Legendre,
ChebyshevT,
ChebyshevU,
Hermite,
Laguerre,
}
fn arb_orthopoly(kind: OrthoPoly, n: u64, x: &BigFloat, prec: usize, rm: RoundingMode) -> BigFloat {
let wp = prec + 32 + (n as f64).log2().ceil().max(0.0) as usize;
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let mut xw = x.clone();
let _ = xw.set_precision(wp, rm);
if n == 0 {
return BigFloat::from_i32(1, prec);
}
let mut p_prev = one.clone();
let mut p_curr = match kind {
OrthoPoly::Legendre | OrthoPoly::ChebyshevT => xw.clone(),
OrthoPoly::ChebyshevU | OrthoPoly::Hermite => two.mul(&xw, wp, rm),
OrthoPoly::Laguerre => one.sub(&xw, wp, rm),
};
for k in 1..n {
let k_bf = BigFloat::from_i128(k as i128, wp);
let kp1 = BigFloat::from_i128(k as i128 + 1, wp);
let next = match kind {
OrthoPoly::Legendre => {
let two_k_plus_1 = BigFloat::from_i128(2 * k as i128 + 1, wp);
let a = two_k_plus_1.mul(&xw, wp, rm).mul(&p_curr, wp, rm);
let b = k_bf.mul(&p_prev, wp, rm);
a.sub(&b, wp, rm).div(&kp1, wp, rm)
}
OrthoPoly::ChebyshevT | OrthoPoly::ChebyshevU => two
.mul(&xw, wp, rm)
.mul(&p_curr, wp, rm)
.sub(&p_prev, wp, rm),
OrthoPoly::Hermite => {
let a = two.mul(&xw, wp, rm).mul(&p_curr, wp, rm);
let b = two.mul(&k_bf, wp, rm).mul(&p_prev, wp, rm);
a.sub(&b, wp, rm)
}
OrthoPoly::Laguerre => {
let two_k_plus_1 = BigFloat::from_i128(2 * k as i128 + 1, wp);
let a = two_k_plus_1.sub(&xw, wp, rm).mul(&p_curr, wp, rm);
let b = k_bf.mul(&p_prev, wp, rm);
a.sub(&b, wp, rm).div(&kp1, wp, rm)
}
};
p_prev = p_curr;
p_curr = next;
}
round_to(p_curr, prec, rm)
}
fn unevaluable(reason: impl Into<String>) -> SymplexError {
SymplexError::Unevaluable {
reason: reason.into(),
}
}
fn series_did_not_converge(operation: &'static str, max_terms: usize) -> SymplexError {
SymplexError::ComputationFailed {
operation,
reason: format!("series did not converge in {max_terms} terms"),
}
}
fn real_args(
name: &str,
args: &[ExprId],
cache: &FxHashMap<ExprId, Complex>,
) -> Result<Vec<BigFloat>, SymplexError> {
let mut out = Vec::with_capacity(args.len());
for &a in args {
let v = get_cached(cache, a)?;
if !v.1.is_zero() {
return Err(unevaluable(format!(
"{name} of complex argument not yet supported in evalf"
)));
}
out.push(v.0.clone());
}
Ok(out)
}
fn negligible(a: &BigFloat, b: &BigFloat, bits: usize) -> bool {
if a.is_zero() {
return true;
}
match (a.exponent(), b.exponent()) {
(Some(ae), Some(be)) => (be as i64 - ae as i64) > bits as i64,
_ => false,
}
}
fn bf_gt(a: &BigFloat, b: &BigFloat) -> bool {
a.cmp(b).is_some_and(|c| c > 0)
}
fn bf_lt(a: &BigFloat, b: &BigFloat) -> bool {
a.cmp(b).is_some_and(|c| c < 0)
}
fn bf_as_int(x: &BigFloat, rm: RoundingMode, cc: &mut Consts) -> Result<Option<i64>, SymplexError> {
if !(x.is_zero() || x.is_int()) {
return Ok(None);
}
let r = bigfloat_to_f64(x, rm, cc)?.round();
Ok((r.abs() < 9.0e15).then_some(r as i64))
}
fn eval_lib_fn(
f: LibFn,
args: &[ExprId],
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Complex, SymplexError> {
match f {
LibFn::BesselJ => eval_bessel(arb_bessel_j, "BesselJ", args, cache, prec, rm, cc),
LibFn::BesselY => eval_bessel(arb_bessel_y, "BesselY", args, cache, prec, rm, cc),
LibFn::BesselI => eval_bessel(arb_bessel_i, "BesselI", args, cache, prec, rm, cc),
LibFn::BesselK => eval_bessel(arb_bessel_k, "BesselK", args, cache, prec, rm, cc),
LibFn::Legendre => eval_orthopoly(OrthoPoly::Legendre, f, args, cache, prec, rm, cc),
LibFn::ChebyshevT => eval_orthopoly(OrthoPoly::ChebyshevT, f, args, cache, prec, rm, cc),
LibFn::ChebyshevU => eval_orthopoly(OrthoPoly::ChebyshevU, f, args, cache, prec, rm, cc),
LibFn::Hermite => eval_orthopoly(OrthoPoly::Hermite, f, args, cache, prec, rm, cc),
LibFn::Laguerre => eval_orthopoly(OrthoPoly::Laguerre, f, args, cache, prec, rm, cc),
LibFn::Erfi
| LibFn::ErfInv
| LibFn::ErfcInv
| LibFn::ExpInt
| LibFn::Shi
| LibFn::Chi
| LibFn::FresnelS
| LibFn::FresnelC
| LibFn::LowerGamma
| LibFn::UpperGamma
| LibFn::DirichletEta
| LibFn::AiryAi
| LibFn::AiryBi
| LibFn::AiryAiPrime
| LibFn::AiryBiPrime
| LibFn::EllipticK
| LibFn::EllipticE
| LibFn::EllipticF
| LibFn::EllipticPi
| LibFn::Gegenbauer
| LibFn::Jacobi
| LibFn::AssocLegendre
| LibFn::AssocLaguerre
| LibFn::BetaInc
| LibFn::BetaIncRegularized => eval_special_09(f, args, cache, prec, rm, cc),
LibFn::PolyLog => eval_polylog(args, cache, prec, rm, cc),
LibFn::RisingFactorial | LibFn::FallingFactorial | LibFn::Harmonic => {
let v = real_args(f.name(), args, cache)?;
debug!(prec, name = f.name(), "evalf: Gamma quotient / digamma");
let r = match f {
LibFn::RisingFactorial => factorials::rising_factorial(&v[0], &v[1], prec, rm, cc)?,
LibFn::FallingFactorial => {
factorials::falling_factorial(&v[0], &v[1], prec, rm, cc)?
}
_ => factorials::harmonic(&v[0], prec, rm, cc)?,
};
Ok((r, BigFloat::new(prec)))
}
LibFn::Factorial2
| LibFn::Subfactorial
| LibFn::Fibonacci
| LibFn::Lucas
| LibFn::Bernoulli
| LibFn::Catalan
| LibFn::Bell
| LibFn::EulerNumber
| LibFn::Stirling1
| LibFn::Stirling2
| LibFn::PartitionCount => Err(unevaluable(format!(
"evalf has no numerical routine for '{f}' (eval folds it exactly at integer arguments)"
))),
LibFn::LambertW => Err(unevaluable("lambertw is evaluated from its node")),
}
}
fn eval_polylog(
args: &[ExprId],
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Complex, SymplexError> {
let s = get_cached(cache, args[0])?;
let z = get_cached(cache, args[1])?;
if !s.1.is_zero() {
return polylog::polylog_complex_order(s, z, prec, rm, cc);
}
if polylog::off_unit_interval(z) {
return polylog::polylog_general(&s.0, z, prec, rm, cc);
}
debug!(prec, "evalf: polylog on [-1, 1]");
Ok((arb_polylog(&s.0, &z.0, prec, rm, cc)?, BigFloat::new(prec)))
}
fn eval_lambertw_node(
arena: &Arena,
node: &ExprNode,
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Complex, SymplexError> {
let (x, k) = lambertw::lambert_parts(arena, node)
.unwrap_or_else(|| Err(unevaluable("not a Lambert W node")))?;
let z = get_cached(cache, x)?;
debug!(
prec,
k, "evalf: LambertW via Halley iteration (mpmath's starting values)"
);
lambertw::lambert_w(z, k, prec, rm, cc)
}
fn eval_bessel(
arb: fn(
&BigFloat,
&BigFloat,
usize,
RoundingMode,
&mut Consts,
) -> Result<BigFloat, SymplexError>,
what: &str,
args: &[ExprId],
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Complex, SymplexError> {
let order = get_cached(cache, args[0])?;
let arg = get_cached(cache, args[1])?;
if !order.1.is_zero() || !arg.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "Bessel of complex argument not yet supported in evalf".into(),
});
}
tracing::debug!(prec, what, "evalf: Bessel function via series/asymptotic");
let result = arb(&order.0, &arg.0, prec, rm, cc)?;
Ok((result, BigFloat::new(prec)))
}
fn eval_orthopoly(
kind: OrthoPoly,
f: LibFn,
args: &[ExprId],
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Complex, SymplexError> {
let n_val = get_cached(cache, args[0])?;
let x_val = get_cached(cache, args[1])?;
if !n_val.1.is_zero() || !x_val.1.is_zero() {
return Err(SymplexError::Unevaluable {
reason: "orthogonal polynomial of complex arguments not supported".into(),
});
}
let n_f = bigfloat_to_f64(&n_val.0, rm, cc)?;
let n_round = n_f.round();
if !n_val.0.is_int() || !(0.0..=1.0e7).contains(&n_round) {
return Err(SymplexError::Unevaluable {
reason: format!("{f}: degree must be a non-negative integer"),
});
}
tracing::debug!(
prec,
n = n_round,
"evalf: orthogonal polynomial via recurrence"
);
let result = arb_orthopoly(kind, n_round as u64, &x_val.0, prec, rm);
Ok((result, BigFloat::new(prec)))
}
fn eval_special_09(
f: LibFn,
args: &[ExprId],
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Complex, SymplexError> {
let name = f.name();
let v = real_args(name, args, cache)?;
let real = |r: BigFloat| Ok((r, BigFloat::new(prec)));
debug!(prec, name, "evalf: special function (0.9)");
match f {
LibFn::Erfi => real(arb_erfi(&v[0], prec, rm, cc)?),
LibFn::ErfInv => real(arb_erfinv(&v[0], prec, rm, cc)?),
LibFn::ErfcInv => real(arb_erfcinv(&v[0], prec, rm, cc)?),
LibFn::ExpInt => real(arb_expint(&v[0], &v[1], prec, rm, cc)?),
LibFn::Shi => real(arb_shi_chi(&v[0], true, prec, rm, cc)?),
LibFn::Chi => {
if v[0].is_zero() {
return Err(unevaluable("Chi(0) is zoo"));
}
let chi = arb_shi_chi(&v[0].abs(), false, prec, rm, cc)?;
if v[0].is_negative() {
Ok((chi, cc.pi(prec, rm).clone()))
} else {
real(chi)
}
}
LibFn::FresnelS => real(arb_fresnel(&v[0], true, prec, rm, cc)?),
LibFn::FresnelC => real(arb_fresnel(&v[0], false, prec, rm, cc)?),
LibFn::LowerGamma => real(arb_lowergamma(&v[0], &v[1], prec, rm, cc)?),
LibFn::UpperGamma => real(arb_uppergamma(&v[0], &v[1], prec, rm, cc)?),
LibFn::DirichletEta => real(arb_dirichlet_eta(&v[0], prec, rm, cc)?),
LibFn::AiryAi => real(arb_airy(&v[0], AiryKind::Ai, prec, rm, cc)?),
LibFn::AiryBi => real(arb_airy(&v[0], AiryKind::Bi, prec, rm, cc)?),
LibFn::AiryAiPrime => real(arb_airy(&v[0], AiryKind::AiPrime, prec, rm, cc)?),
LibFn::AiryBiPrime => real(arb_airy(&v[0], AiryKind::BiPrime, prec, rm, cc)?),
LibFn::EllipticK => real(arb_elliptic_k(&v[0], prec, rm, cc)?),
LibFn::EllipticE => real(arb_elliptic_e(&v[0], prec, rm, cc)?),
LibFn::EllipticF => real(arb_elliptic_f(&v[0], &v[1], prec, rm, cc)?),
LibFn::EllipticPi => real(arb_elliptic_pi(&v[0], &v[1], prec, rm, cc)?),
LibFn::Gegenbauer => real(arb_gegenbauer(&v[0], &v[1], &v[2], prec, rm, cc)?),
LibFn::Jacobi => real(arb_jacobi(&v[0], &v[1], &v[2], &v[3], prec, rm, cc)?),
LibFn::AssocLegendre => real(arb_assoc_legendre(&v[0], &v[1], &v[2], prec, rm, cc)?),
LibFn::AssocLaguerre => real(arb_assoc_laguerre(&v[0], &v[1], &v[2], prec, rm, cc)?),
LibFn::BetaInc | LibFn::BetaIncRegularized => real(arb_betainc(
&v[0],
&v[1],
&v[2],
&v[3],
f == LibFn::BetaIncRegularized,
prec,
rm,
cc,
)?),
LibFn::Factorial2
| LibFn::Subfactorial
| LibFn::RisingFactorial
| LibFn::FallingFactorial
| LibFn::Fibonacci
| LibFn::Lucas
| LibFn::Bernoulli
| LibFn::Harmonic
| LibFn::Catalan
| LibFn::Bell
| LibFn::EulerNumber
| LibFn::Stirling1
| LibFn::Stirling2
| LibFn::PartitionCount
| LibFn::LambertW
| LibFn::PolyLog
| LibFn::BesselJ
| LibFn::BesselY
| LibFn::BesselI
| LibFn::BesselK
| LibFn::Legendre
| LibFn::ChebyshevT
| LibFn::ChebyshevU
| LibFn::Hermite
| LibFn::Laguerre => Err(unevaluable(format!("cannot evaluate function '{name}'"))),
}
}
fn arb_erfi(
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_zero() {
return Ok(BigFloat::new(prec));
}
if x.is_nan() || x.is_inf() {
return Err(unevaluable("erfi of special float value"));
}
let wp = prec + 32;
let x_f = bigfloat_to_f64(x, rm, cc)?;
let ax = x_f.abs();
let two = BigFloat::from_i32(2, wp);
let sqrt_pi = cc.pi(wp, rm).clone().sqrt(wp, rm);
let x_sq = x.mul(x, wp, rm);
if ax * ax > (wp as f64) * std::f64::consts::LN_2 + 4.0 {
let two_x_sq = x_sq.mul(&two, wp, rm);
let mut sum = BigFloat::from_i32(1, wp);
let mut term = BigFloat::from_i32(1, wp);
let mut prev: Option<BigFloat> = None;
for k in 1..(wp * 2 + 100) {
let f = BigFloat::from_i128(2 * k as i128 - 1, wp);
term = term.mul(&f, wp, rm).div(&two_x_sq, wp, rm);
if let Some(ref p) = prev
&& bf_gt(&term, p)
{
break;
}
sum = sum.add(&term, wp, rm);
if negligible(&term, &sum, wp) {
break;
}
prev = Some(term.clone());
}
let e = x_sq.exp(wp, rm, cc);
let denom = x.abs().mul(&sqrt_pi, wp, rm);
let r = e.mul(&sum, wp, rm).div(&denom, wp, rm);
let r = if x.is_negative() { r.neg() } else { r };
return Ok(round_to(r, prec, rm));
}
let mut prod = x.clone();
let _ = prod.set_precision(wp, rm);
let mut sum = prod.clone();
let max_terms = (3.0 * ax * ax) as usize + wp + 60;
let mut converged = false;
for n in 1..=max_terms {
let n_bf = BigFloat::from_i128(n as i128, wp);
prod = prod.mul(&x_sq, wp, rm).div(&n_bf, wp, rm);
let d = BigFloat::from_i128(2 * n as i128 + 1, wp);
let term = prod.div(&d, wp, rm);
sum = sum.add(&term, wp, rm);
if negligible(&term, &sum, wp) {
converged = true;
break;
}
}
if !converged {
return Err(series_did_not_converge("erfi", max_terms));
}
let r = sum.mul(&two, wp, rm).div(&sqrt_pi, wp, rm);
Ok(round_to(r, prec, rm))
}
fn arb_erfc(
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let wp = prec + 32;
let x_f = bigfloat_to_f64(x, rm, cc)?;
if x_f <= 0.5 {
let erf = arb_erf(x, prec, rm, cc)?;
let one = BigFloat::from_i32(1, wp);
return Ok(round_to(one.sub(&erf, wp, rm), prec, rm));
}
let x_sq_bits = (x_f * x_f * std::f64::consts::LOG2_E).ceil() as usize;
let mut xw = x.clone();
if x_sq_bits >= wp + 8 {
let _ = xw.set_precision(wp, rm);
return Ok(round_to(erfc_asymptotic(&xw, wp, rm, cc), prec, rm));
}
if x_f >= 1.25 {
let p = wp + magnitude_bits(x_f * x_f) + 8;
let _ = xw.set_precision(p, rm);
let x_sq = xw.mul(&xw, p, rm);
let half = BigFloat::from_f64(0.5, 64);
let g = uppergamma_cf(&half, &x_sq, p, rm, cc)?;
let sqrt_pi = cc.pi(wp, rm).clone().sqrt(wp, rm);
return Ok(round_to(g.div(&sqrt_pi, wp, rm), prec, rm));
}
let wp2 = wp + x_sq_bits + 8;
let _ = xw.set_precision(wp2, rm);
let erf = arb_erf(&xw, wp2, rm, cc)?;
let one = BigFloat::from_i32(1, wp2);
Ok(round_to(one.sub(&erf, wp2, rm), prec, rm))
}
#[allow(clippy::too_many_arguments)]
fn erf_inverse_halley(
y: &BigFloat,
x0: f64,
lo: f64,
hi: f64,
solve_erfc: bool,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let wp = prec + 32;
let mut yw = y.clone();
let _ = yw.set_precision(wp, rm);
let half_sqrt_pi = cc
.pi(wp, rm)
.clone()
.sqrt(wp, rm)
.div(&BigFloat::from_i32(2, wp), wp, rm);
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let (mut lo, mut hi) = (BigFloat::from_f64(lo, wp), BigFloat::from_f64(hi, wp));
let mut x = BigFloat::from_f64(x0.clamp(lo_f(&lo), hi_f(&hi)), wp);
fn lo_f(v: &BigFloat) -> f64 {
bigfloat_to_f64_rounded(v, RoundingMode::ToEven).unwrap_or(f64::NEG_INFINITY)
}
fn hi_f(v: &BigFloat) -> f64 {
bigfloat_to_f64_rounded(v, RoundingMode::ToEven).unwrap_or(f64::INFINITY)
}
for _ in 0..(4 * wp + 200) {
let f = if solve_erfc {
arb_erfc(&x, wp, rm, cc)?.sub(&yw, wp, rm)
} else {
arb_erf(&x, wp, rm, cc)?.sub(&yw, wp, rm)
};
if f.is_zero() {
return Ok(round_to(x, prec, rm));
}
let root_above = f.is_negative() != solve_erfc;
if root_above {
lo = x.clone();
} else {
hi = x.clone();
}
let x_sq = x.mul(&x, wp, rm);
let g = f
.mul(&half_sqrt_pi, wp, rm)
.mul(&x_sq.exp(wp, rm, cc), wp, rm);
let g = if solve_erfc { g.neg() } else { g };
let denom = one.add(&x.mul(&g, wp, rm), wp, rm);
let delta = if denom.is_zero() {
g.clone()
} else {
g.div(&denom, wp, rm)
};
let tol = prec + 8;
let width = hi.sub(&lo, wp, rm);
let candidate = x.sub(&delta, wp, rm);
if negligible(&delta, &x, tol) {
return Ok(round_to(candidate, prec, rm));
}
let inside = bf_gt(&candidate, &lo) && bf_lt(&candidate, &hi);
let halves = bf_lt(&delta.abs().mul(&two, wp, rm), &width);
if inside && halves {
x = candidate;
if negligible(&delta, &x, tol) {
return Ok(round_to(x, prec, rm));
}
} else {
let mid = lo.add(&hi, wp, rm).div(&two, wp, rm);
let stuck = mid == lo || mid == hi;
x = mid;
if stuck || negligible(&width, &x, tol) {
return Ok(round_to(x, prec, rm));
}
}
}
Err(SymplexError::computation_failed(
"evalf",
"the inverse error function iteration did not converge",
))
}
fn arb_erfinv(
y: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if y.is_zero() {
return Ok(BigFloat::new(prec));
}
let wp = prec + 32;
let one = BigFloat::from_i32(1, wp);
let ay = y.abs();
let one_minus_ay = one.sub(&ay, wp, rm);
if one_minus_ay.is_zero() || one_minus_ay.is_negative() {
return Err(unevaluable("erfinv: argument must satisfy |y| < 1"));
}
if let Some(e) = y.exponent()
&& i64::from(e) < -(wp as i64) / 2 - 4
{
let half_sqrt_pi =
cc.pi(wp, rm)
.clone()
.sqrt(wp, rm)
.div(&BigFloat::from_i32(2, wp), wp, rm);
return Ok(round_to(y.mul(&half_sqrt_pi, wp, rm), prec, rm));
}
let y_f = bigfloat_to_f64(y, rm, cc)?;
let r = if y_f.abs() > 0.5 {
arb_erfcinv_small(&one_minus_ay, prec, rm, cc)?
} else {
let a = 0.147_f64;
let l = (1.0 - y_f * y_f).ln();
let t = 2.0 / (std::f64::consts::PI * a) + l / 2.0;
let mut x0 = ((t * t - l / a).sqrt() - t).sqrt().copysign(y_f);
for _ in 0..3 {
let f = erf_f64(x0) - y_f;
x0 -= f * (std::f64::consts::PI.sqrt() / 2.0) * (x0 * x0).exp();
}
erf_inverse_halley(y, x0, -0.5, 0.5, false, prec, rm, cc)?
};
Ok(if y.is_negative() && !r.is_negative() && y_f.abs() > 0.5 {
r.neg()
} else {
r
})
}
fn arb_erfcinv_small(
u: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let wp = prec + 32;
let l = u.ln(wp, rm, cc).neg();
let l_f = bigfloat_to_f64(&l, rm, cc)?;
let mut x0 = l_f.max(0.5).sqrt();
for _ in 0..4 {
let v = l_f - (x0 * std::f64::consts::PI.sqrt()).ln();
x0 = v.max(0.25).sqrt();
}
let hi = l_f.max(0.0).sqrt().max(1.0) * (1.0 + f64::EPSILON * 16.0);
erf_inverse_halley(u, x0, 0.0, hi, true, prec, rm, cc)
}
fn arb_erfcinv(
u: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let wp = prec + 32;
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
if u.is_zero() || u.is_negative() || !bf_lt(u, &two) {
return Err(unevaluable("erfcinv: argument must satisfy 0 < u < 2"));
}
if bf_gt(u, &one) {
let v = two.sub(u, wp, rm);
return Ok(arb_erfcinv(&v, prec, rm, cc)?.neg());
}
let u_f = bigfloat_to_f64(u, rm, cc)?;
if u_f >= 0.5 {
let y = one.sub(u, wp, rm);
return arb_erfinv(&y, prec, rm, cc);
}
arb_erfcinv_small(u, prec, rm, cc)
}
fn bf_strictly_positive(v: &BigFloat) -> bool {
v.is_positive() && !v.is_zero()
}
fn magnitude_bits(v: f64) -> usize {
if !v.is_finite() {
1100
} else if v > 1.0 {
v.log2().ceil() as usize
} else {
0
}
}
fn inverse_bits(s: &BigFloat) -> usize {
match s.exponent() {
Some(e) if e < 1 => usize::try_from(1 - i64::from(e)).unwrap_or(0),
_ => 0,
}
}
fn lngamma_bits(s: &BigFloat, s_f: f64) -> usize {
if s_f >= 1.0 {
magnitude_bits(s_f * s_f.ln() + s_f + 2.0)
} else {
magnitude_bits(inverse_bits(s) as f64 * std::f64::consts::LN_2 + 2.0)
}
}
fn cancellation_cap(wp: usize) -> usize {
wp.saturating_mul(4).saturating_add(1024)
}
fn special_exhausted(prec: usize) -> SymplexError {
SymplexError::PrecisionExhausted {
requested: u32::try_from(prec.saturating_sub(64) * 10 / 34).unwrap_or(u32::MAX),
achieved: 0,
}
}
fn requested_at(prec: usize) -> impl Fn(SymplexError) -> SymplexError {
move |e| match e {
SymplexError::PrecisionExhausted { .. } => special_exhausted(prec),
other => other,
}
}
fn at_precision(v: &BigFloat, p: usize, rm: RoundingMode) -> BigFloat {
let mut w = v.clone();
let _ = w.set_precision(p, rm);
w
}
fn gamma_prefactor(
s: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let s_f = bigfloat_to_f64(s, rm, cc)?.abs();
let x_f = bigfloat_to_f64(x, rm, cc)?;
let ln_x = x
.exponent()
.map_or(0.0, |e| (f64::from(e).abs() + 1.0) * std::f64::consts::LN_2);
let wpe = wp + magnitude_bits(s_f * ln_x + x_f) + 8;
let e = s.mul(&x.ln(wpe, rm, cc), wpe, rm).sub(x, wpe, rm);
Ok(e.exp(wp, rm, cc))
}
fn uppergamma_cf(
s: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let s_f = bigfloat_to_f64(s, rm, cc)?;
let max_iter = (40 * wp + 2000).saturating_add((64.0 * s_f.abs().cbrt()).min(2e6) as usize);
let wp = wp + magnitude_bits(max_iter as f64) + 8;
let (s, x) = (&at_precision(s, wp, rm), &at_precision(x, wp, rm));
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let mut tiny = BigFloat::from_i32(1, wp);
tiny.set_exponent(-(4 * wp as i32));
let mut b = x.add(&one, wp, rm).sub(s, wp, rm);
let mut c = one.div(&tiny, wp, rm);
let mut d = if b.is_zero() {
one.div(&tiny, wp, rm)
} else {
one.div(&b, wp, rm)
};
let mut h = d.clone();
let mut converged = false;
for i in 1..=max_iter {
let i_bf = BigFloat::from_i128(i as i128, wp);
let an = i_bf.mul(&i_bf.sub(s, wp, rm), wp, rm).neg();
b = b.add(&two, wp, rm);
d = an.mul(&d, wp, rm).add(&b, wp, rm);
if d.is_zero() {
d = tiny.clone();
}
c = b.add(&an.div(&c, wp, rm), wp, rm);
if c.is_zero() {
c = tiny.clone();
}
d = one.div(&d, wp, rm);
let del = d.mul(&c, wp, rm);
h = h.mul(&del, wp, rm);
let dev = del.sub(&one, wp, rm);
if negligible(&dev, &one, wp) {
converged = true;
break;
}
}
if !converged {
return Err(SymplexError::ComputationFailed {
operation: "evalf",
reason: format!(
"continued fraction for the incomplete gamma function did not converge in \
{max_iter} steps"
),
});
}
let pref = gamma_prefactor(s, x, wp, rm, cc)?;
Ok(pref.mul(&h, wp, rm))
}
fn lowergamma_series(
s: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_zero() {
return Ok(BigFloat::new(wp));
}
let x_f = bigfloat_to_f64(x, rm, cc)?;
let max_terms = ((2.0 * x_f).min(1e7) as usize).saturating_add(wp + 50);
let wp = wp + magnitude_bits(max_terms as f64) + 8;
let (s, x) = (&at_precision(s, wp, rm), &at_precision(x, wp, rm));
let one = BigFloat::from_i32(1, wp);
let mut term = one.div(s, wp, rm);
let mut sum = term.clone();
let mut converged = false;
for k in 1..=max_terms {
let s_plus_k = s.add(&BigFloat::from_i128(k as i128, wp), wp, rm);
term = term.mul(x, wp, rm).div(&s_plus_k, wp, rm);
sum = sum.add(&term, wp, rm);
if negligible(&term, &sum, wp) {
converged = true;
break;
}
}
if !converged {
return Err(series_did_not_converge("lowergamma", max_terms));
}
let pref = gamma_prefactor(s, x, wp, rm, cc)?;
Ok(pref.mul(&sum, wp, rm))
}
fn uppergamma_wp(
s: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if !s.is_zero()
&& bigfloat_to_f64(x, rm, cc)? < 2.0
&& inverse_bits(&s.abs()) >= wp + 8 + 2 * magnitude_bits(ln_magnitude(x) + 1.0)
{
return Ok(arb_ei(&x.neg(), wp, rm, cc)?.neg());
}
if bf_strictly_positive(s) {
return uppergamma_positive(s, x, wp, rm, cc);
}
if bigfloat_to_f64(x, rm, cc)? >= 1.0 {
return uppergamma_cf(s, x, wp, rm, cc);
}
if bigfloat_to_f64(s, rm, cc)? < -(DOWNWARD_MAX_STEPS as f64) {
return uppergamma_cf(s, x, wp, rm, cc);
}
uppergamma_downward(s, x, wp, rm, cc)
}
const DOWNWARD_MAX_STEPS: usize = 256;
fn uppergamma_positive(
s: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let s_f = bigfloat_to_f64(s, rm, cc)?;
if !bf_lt(x, &s.add(&BigFloat::from_i32(1, 64), exact_bits(s, wp), rm)) {
return uppergamma_cf(s, x, wp, rm, cc);
}
uppergamma_by_difference(s, s_f, x, wp, rm, cc)
}
fn incomplete_gamma_overflows(s: &BigFloat, what: &str) -> Result<(), SymplexError> {
let Ok(s_f) = bigfloat_to_f64_rounded(s, RoundingMode::ToEven) else {
return Ok(());
};
if s_f.is_nan() || s_f < 1.0 {
return Ok(());
}
let lg = ((s_f - 0.5) * s_f.ln() - s_f) * std::f64::consts::LOG2_E;
if lg - 3.0 > f64::from(astro_float::EXPONENT_MAX) {
return Err(unevaluable(format!(
"{what}: the value (about 2^{lg:.3e}) overflows the arbitrary-precision exponent range"
)));
}
Ok(())
}
fn uppergamma_by_difference(
s: &BigFloat,
s_f: f64,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if inverse_bits(s) > cancellation_cap(wp) {
return Err(special_exhausted(wp));
}
let wp2 = wp + 8 + inverse_bits(s) + lngamma_bits(s, s_f);
let sw = at_precision(s, wp2, rm);
let xw = at_precision(x, wp2, rm);
let g = arb_gamma_real(&sw, wp2, rm, cc)?;
let lower = lowergamma_series(&sw, &xw, wp2, rm, cc)?;
Ok(round_to(g.sub(&lower, wp2, rm), wp, rm))
}
fn uppergamma_downward(
s: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let s_f = bigfloat_to_f64(s, rm, cc)?;
let integer = s.is_int();
let steps = if integer {
(-s_f).round() as usize
} else {
(-s_f).ceil() as usize
};
let cap = cancellation_cap(wp);
let mut reserve = steps.saturating_mul(2).saturating_add(32);
loop {
let wp2 = wp + reserve;
let sw = at_precision(s, wp2, rm);
let xw = at_precision(x, wp2, rm);
let (mut cur_s, mut val) = if integer {
(BigFloat::new(wp2), arb_ei(&xw.neg(), wp2, rm, cc)?.neg())
} else {
let s0 = sw.add(&BigFloat::from_i128(steps as i128, wp2), wp2, rm);
let v = uppergamma_positive(&s0, &xw, wp2, rm, cc)?;
(s0, v)
};
let one = BigFloat::from_i32(1, wp2);
let mut lost = 0usize;
for _ in 0..steps {
let next_s = cur_s.sub(&one, wp2, rm);
let t = gamma_prefactor(&next_s, &xw, wp2, rm, cc)?;
let diff = t.sub(&val, wp2, rm);
match (t.exponent(), diff.exponent()) {
(Some(et), Some(ed)) if bf_strictly_positive(&diff) => {
lost += usize::try_from(i64::from(et) - i64::from(ed)).unwrap_or(0);
}
_ => {
lost = wp2;
break;
}
}
val = diff.div(&next_s.neg(), wp2, rm);
cur_s = next_s;
}
if lost + 16 <= reserve {
return Ok(round_to(val, wp, rm));
}
if reserve >= cap {
return Err(special_exhausted(wp));
}
reserve = (lost + 32).max(2 * reserve).min(cap);
}
}
fn arb_uppergamma(
s: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_nan() || s.is_nan() || x.is_inf_neg() || s.is_inf() {
return Err(unevaluable("uppergamma of special float value"));
}
if x.is_inf_pos() {
return Ok(BigFloat::new(prec));
}
if x.is_negative() {
return Err(unevaluable(
"uppergamma of negative argument not yet supported in evalf",
));
}
let wp = prec + 32;
let one = BigFloat::from_i32(1, 64);
if bf_strictly_positive(s) && !bf_gt(x, &s.add(&one, exact_bits(s, wp), rm)) {
incomplete_gamma_overflows(s, "uppergamma")?;
}
if x.is_zero() {
if !bf_strictly_positive(s) {
return Err(unevaluable("uppergamma(s, 0) diverges for s ≤ 0"));
}
let s_f = bigfloat_to_f64(s, rm, cc)?;
let wp = wp + lngamma_bits(s, s_f);
return Ok(round_to(arb_gamma_real(s, wp, rm, cc)?, prec, rm));
}
let r = uppergamma_wp(s, x, wp, rm, cc).map_err(requested_at(prec))?;
Ok(round_to(r, prec, rm))
}
fn arb_lowergamma(
s: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_nan() || s.is_nan() || s.is_inf() || x.is_inf_neg() {
return Err(unevaluable("lowergamma of special float value"));
}
if !bf_strictly_positive(s) {
return Err(unevaluable(
"lowergamma requires s > 0 for numerical evaluation",
));
}
if x.is_negative() {
return Err(unevaluable(
"lowergamma of negative argument not yet supported in evalf",
));
}
let s_f = bigfloat_to_f64(s, rm, cc)?;
let wp = prec + 32;
if x.is_inf_pos() {
let wp = wp + lngamma_bits(s, s_f);
return Ok(round_to(arb_gamma_real(s, wp, rm, cc)?, prec, rm));
}
if !bf_lt(x, s) {
incomplete_gamma_overflows(s, "lowergamma")?;
}
if bf_lt(x, &s.add(&BigFloat::from_i32(1, 64), exact_bits(s, wp), rm)) {
let r = lowergamma_series(s, x, wp, rm, cc).map_err(requested_at(prec))?;
return Ok(round_to(r, prec, rm));
}
let wp = wp + 4 + lngamma_bits(s, s_f);
let g = arb_gamma_real(s, wp, rm, cc)?;
let upper = uppergamma_cf(s, x, wp, rm, cc)?;
Ok(round_to(g.sub(&upper, wp, rm), prec, rm))
}
fn arb_expint(
n: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_nan() || n.is_nan() || n.is_inf() {
return Err(unevaluable("expint of special float value"));
}
if x.is_inf_pos() {
return Ok(BigFloat::new(prec));
}
let wp = prec + 32;
let one = BigFloat::from_i32(1, wp);
if x.is_zero() {
let n_minus_1 = n.sub(&one, wp, rm);
if !n_minus_1.is_positive() {
return Err(unevaluable("expint(n, 0) diverges for n ≤ 1"));
}
return Ok(round_to(one.div(&n_minus_1, wp, rm), prec, rm));
}
if x.is_negative() {
return Err(unevaluable(
"expint of negative argument is complex; not yet supported in evalf",
));
}
let s = one.sub(n, wp, rm);
let g = uppergamma_wp(&s, x, wp, rm, cc).map_err(requested_at(prec))?;
let x_pow = bf_pow(x, &n.sub(&one, wp, rm), wp, rm, cc);
Ok(round_to(x_pow.mul(&g, wp, rm), prec, rm))
}
fn arb_shi_chi(
x: &BigFloat,
want_shi: bool,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_nan() || x.is_inf() {
return Err(unevaluable("Shi/Chi of special float value"));
}
if x.is_zero() {
return if want_shi {
Ok(BigFloat::new(prec))
} else {
Err(unevaluable("Chi(0) is zoo"))
};
}
let wp = prec + 32;
let ax = x.abs();
let x_f = bigfloat_to_f64(&ax, rm, cc)?;
let r = if x_f > 40.0 {
let ei_pos = arb_ei(&ax, wp, rm, cc)?;
let ei_neg = arb_ei(&ax.neg(), wp, rm, cc)?;
let two = BigFloat::from_i32(2, wp);
if want_shi {
ei_pos.sub(&ei_neg, wp, rm).div(&two, wp, rm)
} else {
ei_pos.add(&ei_neg, wp, rm).div(&two, wp, rm)
}
} else {
let mut xw = ax.clone();
let _ = xw.set_precision(wp, rm);
let x2 = xw.mul(&xw, wp, rm);
let max_terms = (x_f * 1.5) as usize + wp + 40;
let name = if want_shi { "Shi" } else { "Chi" };
let mut converged = false;
let series = if want_shi {
let mut t = xw.clone();
let mut sum = BigFloat::new(wp);
for k in 0..max_terms {
if k > 0 {
let d = BigFloat::from_i128((2 * k as i128) * (2 * k as i128 + 1), wp);
t = t.mul(&x2, wp, rm).div(&d, wp, rm);
}
let term = t.div(&BigFloat::from_i128(2 * k as i128 + 1, wp), wp, rm);
sum = sum.add(&term, wp, rm);
if negligible(&term, &sum, wp) {
converged = true;
break;
}
}
sum
} else {
let gamma = arb_euler_gamma(wp, rm, cc)?;
let ln_x = xw.ln(wp, rm, cc);
let mut u = BigFloat::from_i32(1, wp); let mut sum = BigFloat::new(wp);
for k in 1..max_terms {
let d = BigFloat::from_i128((2 * k as i128 - 1) * (2 * k as i128), wp);
u = u.mul(&x2, wp, rm).div(&d, wp, rm);
let term = u.div(&BigFloat::from_i128(2 * k as i128, wp), wp, rm);
sum = sum.add(&term, wp, rm);
if negligible(&term, &sum, wp) {
converged = true;
break;
}
}
gamma.add(&ln_x, wp, rm).add(&sum, wp, rm)
};
if !converged {
return Err(series_did_not_converge(name, max_terms));
}
series
};
let r = if want_shi && x.is_negative() {
r.neg()
} else {
r
};
Ok(round_to(r, prec, rm))
}
fn arb_fresnel(
x: &BigFloat,
want_s: bool,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_nan() || x.is_inf() {
return Err(unevaluable("Fresnel integral of special float value"));
}
if x.is_zero() {
return Ok(BigFloat::new(prec));
}
let base_wp = prec + 32;
let ax = x.abs();
let x_f = bigfloat_to_f64(&ax, rm, cc)?;
let arg_f = std::f64::consts::FRAC_PI_2 * x_f * x_f;
let e = usize::try_from(ax.exponent().map_or(0, i64::from).max(0)).unwrap_or(0);
let r = if e > base_wp + 2 {
BigFloat::from_f64(0.5, base_wp)
} else if arg_f > (base_wp as f64) * std::f64::consts::LN_2 {
let wp = base_wp;
let mut xw = ax.clone();
let _ = xw.set_precision(wp, rm);
let one = BigFloat::from_i32(1, wp);
let pi = cc.pi(wp, rm).clone();
let pi_x2 = pi.mul(&xw, wp, rm).mul(&xw, wp, rm);
let inv = one.div(&pi_x2, wp, rm); let inv2 = inv.mul(&inv, wp, rm);
let mut tf = one.clone();
let mut tg = inv.clone();
let mut f_sum = BigFloat::new(wp);
let mut g_sum = BigFloat::new(wp);
let mut prev: Option<BigFloat> = None;
for m in 0..(wp * 2 + 100) {
if m > 0 {
let a = BigFloat::from_i128((4 * m as i128 - 3) * (4 * m as i128 - 1), wp);
let b = BigFloat::from_i128((4 * m as i128 - 1) * (4 * m as i128 + 1), wp);
tf = tf.mul(&a, wp, rm).mul(&inv2, wp, rm);
tg = tg.mul(&b, wp, rm).mul(&inv2, wp, rm);
}
if let Some(ref p) = prev
&& bf_gt(&tf, p)
{
break;
}
if m % 2 == 0 {
f_sum = f_sum.add(&tf, wp, rm);
g_sum = g_sum.add(&tg, wp, rm);
} else {
f_sum = f_sum.sub(&tf, wp, rm);
g_sum = g_sum.sub(&tg, wp, rm);
}
if negligible(&tf, &one, wp) {
break;
}
prev = Some(tf.clone());
}
let pi_x = pi.mul(&xw, wp, rm);
let f = f_sum.div(&pi_x, wp, rm);
let g = g_sum.div(&pi_x, wp, rm);
let two = BigFloat::from_i32(2, wp);
let wt = wp + 2 * e;
let mut xt = ax.clone();
let _ = xt.set_precision(wt, rm);
let theta =
cc.pi(wt, rm)
.mul(&xt, wt, rm)
.mul(&xt, wt, rm)
.div(&BigFloat::from_i32(2, wt), wt, rm);
let s = theta.sin(wp, rm, cc);
let c = theta.cos(wp, rm, cc);
let half = one.div(&two, wp, rm);
if want_s {
half.sub(&f.mul(&c, wp, rm), wp, rm)
.sub(&g.mul(&s, wp, rm), wp, rm)
} else {
half.add(&f.mul(&s, wp, rm), wp, rm)
.sub(&g.mul(&c, wp, rm), wp, rm)
}
} else {
let wp = base_wp + (arg_f * std::f64::consts::LOG2_E).ceil() as usize + 16;
let mut xw = ax.clone();
let _ = xw.set_precision(wp, rm);
let pi = cc.pi(wp, rm).clone();
let two = BigFloat::from_i32(2, wp);
let half_pi = pi.div(&two, wp, rm);
let x2 = xw.mul(&xw, wp, rm);
let u = half_pi.mul(&x2, wp, rm);
let u2 = u.mul(&u, wp, rm);
let mut t = if want_s {
u.mul(&xw, wp, rm)
} else {
xw.clone()
};
let mut sum = BigFloat::new(wp);
let max_terms = (arg_f * 2.0) as usize + wp + 40;
let mut converged = false;
for k in 0..max_terms {
if k > 0 {
let d = if want_s {
BigFloat::from_i128((2 * k as i128) * (2 * k as i128 + 1), wp)
} else {
BigFloat::from_i128((2 * k as i128 - 1) * (2 * k as i128), wp)
};
t = t.mul(&u2, wp, rm).div(&d, wp, rm);
}
let div = BigFloat::from_i128(4 * k as i128 + if want_s { 3 } else { 1 }, wp);
let term = t.div(&div, wp, rm);
if k % 2 == 0 {
sum = sum.add(&term, wp, rm);
} else {
sum = sum.sub(&term, wp, rm);
}
if negligible(&term, &sum, wp) {
converged = true;
break;
}
}
if !converged {
let name = if want_s { "fresnels" } else { "fresnelc" };
return Err(series_did_not_converge(name, max_terms));
}
sum
};
let r = if x.is_negative() { r.neg() } else { r };
Ok(round_to(r, prec, rm))
}
fn ln_magnitude(v: &BigFloat) -> f64 {
v.exponent()
.map_or(0.0, |e| (f64::from(e).abs() + 1.0) * std::f64::consts::LN_2)
}
fn lngamma_magnitude(s: &BigFloat, s_f: f64) -> f64 {
if s_f >= 1.0 {
s_f * s_f.ln() + s_f + 2.0
} else {
ln_magnitude(s) + 2.0
}
}
fn ln_beta_wp(
a: &BigFloat,
b: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let a_f = bigfloat_to_f64(a, rm, cc)?;
let b_f = bigfloat_to_f64(b, rm, cc)?;
let ab_f = a_f + b_f;
let mag = lngamma_magnitude(a, a_f)
.max(lngamma_magnitude(b, b_f))
.max(ab_f * ab_f.ln().abs() + ab_f + 2.0);
let p = wp + magnitude_bits(mag) + 8;
let a_plus_b = a.add(b, p, rm);
let la = stirling_log_gamma(a, p, rm, cc)?;
let lb = stirling_log_gamma(b, p, rm, cc)?;
let lab = stirling_log_gamma(&a_plus_b, p, rm, cc)?;
Ok(la.add(&lb, p, rm).sub(&lab, p, rm))
}
fn beta_wp(
a: &BigFloat,
b: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
Ok(ln_beta_wp(a, b, wp, rm, cc)?.exp(wp, rm, cc))
}
fn betainc_cf(
a: &BigFloat,
b: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let ab_f = bigfloat_to_f64(&a.add(b, 64, rm), rm, cc)?;
let max_iter = (40 * wp + 2000).saturating_add((10.0 * ab_f.abs().sqrt()).min(2e6) as usize);
let wp = wp + magnitude_bits(2.0 * max_iter as f64) + 8;
let (a, b, x) = (
&at_precision(a, wp, rm),
&at_precision(b, wp, rm),
&at_precision(x, wp, rm),
);
let one = BigFloat::from_i32(1, wp);
let mut tiny = BigFloat::from_i32(1, wp);
tiny.set_exponent(-(4 * wp as i32));
let qab = a.add(b, wp, rm);
let qap = a.add(&one, wp, rm);
let qam = a.sub(&one, wp, rm);
let mut c = one.clone();
let mut d = one.sub(&qab.mul(x, wp, rm).div(&qap, wp, rm), wp, rm);
if d.is_zero() {
d = tiny.clone();
}
d = one.div(&d, wp, rm);
let mut h = d.clone();
let mut converged = false;
let step = |aa: &BigFloat, c: &mut BigFloat, d: &mut BigFloat| -> BigFloat {
*d = one.add(&aa.mul(d, wp, rm), wp, rm);
if d.is_zero() {
*d = tiny.clone();
}
*c = one.add(&aa.div(c, wp, rm), wp, rm);
if c.is_zero() {
*c = tiny.clone();
}
*d = one.div(d, wp, rm);
d.mul(c, wp, rm)
};
for m in 1..=max_iter {
let m_bf = BigFloat::from_i128(m as i128, wp);
let m2_bf = BigFloat::from_i128(2 * m as i128, wp);
let num = m_bf.mul(&b.sub(&m_bf, wp, rm), wp, rm).mul(x, wp, rm);
let den = qam.add(&m2_bf, wp, rm).mul(&a.add(&m2_bf, wp, rm), wp, rm);
let aa = num.div(&den, wp, rm);
let del = step(&aa, &mut c, &mut d);
h = h.mul(&del, wp, rm);
let num = a
.add(&m_bf, wp, rm)
.mul(&qab.add(&m_bf, wp, rm), wp, rm)
.mul(x, wp, rm)
.neg();
let den = a.add(&m2_bf, wp, rm).mul(&qap.add(&m2_bf, wp, rm), wp, rm);
let aa = num.div(&den, wp, rm);
let del = step(&aa, &mut c, &mut d);
h = h.mul(&del, wp, rm);
let dev = del.sub(&one, wp, rm);
if negligible(&dev, &one, wp) {
converged = true;
break;
}
}
if !converged {
return Err(SymplexError::ComputationFailed {
operation: "evalf",
reason: format!(
"continued fraction for the incomplete beta function did not converge in \
{max_iter} steps"
),
});
}
Ok(h)
}
fn betainc_prefactor(
a: &BigFloat,
b: &BigFloat,
x: &BigFloat,
y: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let a_f = bigfloat_to_f64(a, rm, cc)?;
let b_f = bigfloat_to_f64(b, rm, cc)?;
let ab_f = a_f + b_f;
let mag = a_f * ln_magnitude(x)
+ b_f * ln_magnitude(y)
+ ln_magnitude(a)
+ lngamma_magnitude(a, a_f)
+ lngamma_magnitude(b, b_f)
+ ab_f * ab_f.ln().abs()
+ ab_f
+ 2.0;
let p = wp + magnitude_bits(mag) + 8;
let e = a
.mul(&x.ln(p, rm, cc), p, rm)
.add(&b.mul(&y.ln(p, rm, cc), p, rm), p, rm)
.sub(&a.ln(p, rm, cc), p, rm)
.sub(&ln_beta_wp(a, b, p, rm, cc)?, p, rm);
Ok(e.exp(wp, rm, cc))
}
fn betainc_reg_direct(
a: &BigFloat,
b: &BigFloat,
x: &BigFloat,
one_minus_x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let h = betainc_cf(a, b, x, wp, rm, cc)?;
let pref = betainc_prefactor(a, b, x, one_minus_x, wp, rm, cc)?;
Ok(pref.mul(&h, wp, rm))
}
fn betainc_switch_point(a: &BigFloat, b: &BigFloat, wp: usize, rm: RoundingMode) -> BigFloat {
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let a_plus_1 = a.add(&one, wp, rm);
let a_plus_b_plus_2 = a.add(b, wp, rm).add(&two, wp, rm);
a_plus_1.div(&a_plus_b_plus_2, wp, rm)
}
fn betainc_tails_wp(
a: &BigFloat,
b: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(BigFloat, BigFloat), SymplexError> {
let direct_lower = !bf_gt(x, &betainc_switch_point(a, b, wp, rm));
let first = if direct_lower { a } else { b };
let ab_bits = magnitude_bits(bigfloat_to_f64(&a.add(b, 64, rm), rm, cc)?);
let cap = cancellation_cap(wp);
let mut extra = 16 + inverse_bits(first);
if extra > cap {
return Err(special_exhausted(wp));
}
loop {
let p = wp + extra;
let py = p + 64 + ab_bits;
let y = BigFloat::from_i32(1, py).sub(x, py, rm);
let d = if direct_lower {
betainc_reg_direct(a, b, x, &y, p, rm, cc)?
} else {
betainc_reg_direct(b, a, &y, x, p, rm, cc)?
};
let c = BigFloat::from_i32(1, p).sub(&d, p, rm);
let lost = match c.exponent() {
Some(e) if bf_strictly_positive(&c) => usize::try_from(1 - i64::from(e)).unwrap_or(0),
_ => p,
};
if lost + 8 <= extra {
let (d, c) = (round_to(d, wp, rm), round_to(c, wp, rm));
return Ok(if direct_lower { (d, c) } else { (c, d) });
}
if extra >= cap {
return Err(special_exhausted(wp));
}
extra = (lost + 16).max(2 * extra).min(cap);
}
}
fn betainc_tails_at(
a: &BigFloat,
b: &BigFloat,
t: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(BigFloat, BigFloat), SymplexError> {
let one = BigFloat::from_i32(1, wp);
if t.is_zero() {
return Ok((BigFloat::new(wp), one));
}
if t.cmp(&one) == Some(0) {
return Ok((one, BigFloat::new(wp)));
}
betainc_tails_wp(a, b, t, wp, rm, cc)
}
fn betainc_reg_diff_wp(
a: &BigFloat,
b: &BigFloat,
lo: &BigFloat,
hi: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(BigFloat, usize), SymplexError> {
let (l_lo, u_lo) = betainc_tails_at(a, b, lo, wp, rm, cc)?;
let (l_hi, u_hi) = betainc_tails_at(a, b, hi, wp, rm, cc)?;
let (p, q) = if bf_gt(&l_hi, &u_lo) {
(u_lo, u_hi)
} else {
(l_hi, l_lo)
};
let diff = p.sub(&q, wp, rm);
let lost = match (p.exponent(), q.exponent(), diff.exponent()) {
_ if q.is_zero() => 0,
_ if !bf_strictly_positive(&diff) => wp,
(Some(ep), Some(eq), Some(ed)) => {
usize::try_from(i64::from(ep.max(eq)) - i64::from(ed)).unwrap_or(0)
}
_ => 0,
};
Ok((diff, lost))
}
#[allow(clippy::too_many_arguments)]
fn arb_betainc(
a: &BigFloat,
b: &BigFloat,
x1: &BigFloat,
x2: &BigFloat,
regularized: bool,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let name = if regularized {
"betainc_regularized"
} else {
"betainc"
};
if [a, b, x1, x2].iter().any(|v| v.is_nan() || v.is_inf()) {
return Err(unevaluable(format!("{name} of special float value")));
}
if !bf_strictly_positive(a) || !bf_strictly_positive(b) {
return Err(unevaluable(format!(
"{name} requires a, b > 0 for numerical evaluation"
)));
}
let one = BigFloat::from_i32(1, prec);
if [x1, x2].iter().any(|x| x.is_negative() || bf_gt(x, &one)) {
return Err(unevaluable(format!(
"{name}: the limits x1, x2 must lie in [0, 1] for numerical evaluation"
)));
}
let (lo, hi, negate) = match x1.cmp(x2) {
Some(c) if c < 0 => (x1, x2, false),
Some(c) if c > 0 => (x2, x1, true),
_ => return Ok(BigFloat::new(prec)),
};
let cap = prec + cancellation_cap(prec);
let mut wp = prec + 32;
let mut diff = loop {
let (d, lost) =
betainc_reg_diff_wp(a, b, lo, hi, wp, rm, cc).map_err(requested_at(prec))?;
if lost + prec + 16 <= wp {
break d;
}
if wp >= cap {
return Err(special_exhausted(prec));
}
wp = (prec + 32 + lost).min(cap);
};
if !regularized {
let beta = beta_wp(a, b, wp, rm, cc)?;
diff = diff.mul(&beta, wp, rm);
}
if negate {
diff = diff.neg();
}
Ok(round_to(diff, prec, rm))
}
fn zeta_nonpositive_int(m: i64, wp: usize, rm: RoundingMode) -> BigFloat {
if m == 0 {
return BigFloat::from_f64(-0.5, wp);
}
let k = (-m) as usize;
let b = crate::base::bernoulli::bernoulli(k + 1);
if b.is_zero() {
return BigFloat::new(wp);
}
let v = -b / Ratio::from_integer(BigInt::from(k as i64 + 1));
ratio_to_bigfloat(&v, wp, rm)
}
fn int_pow_s(
k: usize,
s: &BigFloat,
s_int: Option<i64>,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> BigFloat {
let kb = BigFloat::from_i128(k as i128, wp);
match s_int {
Some(n) if n >= 0 => kb.powi(n as usize, wp, rm),
Some(n) => BigFloat::from_i32(1, wp).div(&kb.powi((-n) as usize, wp, rm), wp, rm),
None => bf_pow(&kb, s, wp, rm, cc),
}
}
fn polylog_series_terms(s_f: f64, z_f: f64, wp: usize) -> usize {
if !(z_f > 0.0 && z_f < 1.0) {
return wp * 2 + 60;
}
let a = -z_f.ln(); let target = (wp as f64) * std::f64::consts::LN_2 + a;
let mut k = target / a;
let m = (-s_f).max(0.0);
if m > 0.0 {
k = k.max(m / a);
for _ in 0..200 {
if k * a - m * k.ln() >= target {
break;
}
k *= 2.0;
}
}
(k.ceil() as usize).saturating_add(20)
}
fn polylog_series(
s: &BigFloat,
s_int: Option<i64>,
z: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let (sum, lost) = polylog_series_raw(s, s_int, z, wp, rm, cc)?;
if lost <= 4 {
return Ok(sum);
}
let wp2 = wp + lost + 8;
let mut sw = s.clone();
let _ = sw.set_precision(wp2, rm);
let mut zw = z.clone();
let _ = zw.set_precision(wp2, rm);
let (sum, _) = polylog_series_raw(&sw, s_int, &zw, wp2, rm, cc)?;
Ok(round_to(sum, wp, rm))
}
fn polylog_series_raw(
s: &BigFloat,
s_int: Option<i64>,
z: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(BigFloat, usize), SymplexError> {
let z_f = bigfloat_to_f64(z, rm, cc).unwrap_or(0.5).abs();
let s_f = bigfloat_to_f64(s, rm, cc).unwrap_or(0.0);
let max_terms = polylog_series_terms(s_f, z_f, wp);
let mut zk = z.clone();
let mut sum = BigFloat::new(wp);
let mut max_exp: Option<i64> = None;
let mut converged = false;
for k in 1..=max_terms {
if k > 1 {
zk = zk.mul(z, wp, rm);
}
let term = zk.div(&int_pow_s(k, s, s_int, wp, rm, cc), wp, rm);
if let Some(e) = term.exponent() {
max_exp = Some(max_exp.map_or(e as i64, |m| m.max(e as i64)));
}
sum = sum.add(&term, wp, rm);
if negligible(&term, &sum, wp) {
converged = true;
break;
}
}
if !converged {
return Err(series_did_not_converge("polylog", max_terms));
}
let lost = match (max_exp, sum.exponent()) {
(Some(m), Some(se)) if m > se as i64 => (m - se as i64) as usize,
_ => 0,
};
Ok((sum, lost))
}
const MAX_NONPOSITIVE_ORDER: u64 = 1000;
thread_local! {
static STIRLING_ROW: std::cell::RefCell<Option<(u64, std::rc::Rc<Vec<BigInt>>)>> =
const { std::cell::RefCell::new(None) };
}
fn stirling2_row(m: u64) -> Result<std::rc::Rc<Vec<BigInt>>, SymplexError> {
if m > MAX_NONPOSITIVE_ORDER + 1 {
return Err(SymplexError::NotImplemented(format!(
"polylog of an order below -{MAX_NONPOSITIVE_ORDER} (a finite sum of {m} terms with \
Stirling numbers)"
)));
}
if let Some(row) = STIRLING_ROW.with(|c| {
c.borrow()
.as_ref()
.filter(|(k, _)| *k == m)
.map(|(_, r)| std::rc::Rc::clone(r))
}) {
return Ok(row);
}
let mu = m as usize;
let mut row = vec![BigInt::zero(); mu + 1];
row[0] = BigInt::from(1);
for i in 1..=mu {
for j in (1..=i).rev() {
let (lower, upper) = row.split_at_mut(j);
upper[0] *= j as u64;
upper[0] += &lower[j - 1];
}
row[0] = BigInt::zero();
}
let row = std::rc::Rc::new(row);
STIRLING_ROW.with(|c| *c.borrow_mut() = Some((m, std::rc::Rc::clone(&row))));
Ok(row)
}
fn polylog_nonpositive(
n: usize,
z: &BigFloat,
wp: usize,
rm: RoundingMode,
) -> Result<(BigFloat, f64), SymplexError> {
let one = BigFloat::from_i32(1, wp);
let one_minus_z = one.sub(z, wp, rm);
if one_minus_z.is_zero() {
return Err(unevaluable("polylog(s, 1) diverges for s ≤ 1"));
}
let w = z.div(&one_minus_z, wp, rm);
let row = stirling2_row(n as u64 + 1)?;
let mut k_fact = BigInt::from(1);
let mut w_pow = w.clone();
let mut sum = BigFloat::new(wp);
let mut largest = f64::NEG_INFINITY;
for k in 0..=n {
if k > 0 {
k_fact *= BigInt::from(k as u64);
w_pow = w_pow.mul(&w, wp, rm);
}
let c = ratio_to_bigfloat(&Ratio::from_integer(&k_fact * &row[k + 1]), wp, rm);
let term = c.mul(&w_pow, wp, rm);
if let Some(e) = term.exponent().filter(|_| !term.is_zero()) {
largest = largest.max(f64::from(e));
}
sum = sum.add(&term, wp, rm);
}
Ok((sum, largest))
}
fn polylog_nonpositive_measured(
n: usize,
z: &BigFloat,
prec: usize,
rm: RoundingMode,
) -> Result<BigFloat, SymplexError> {
let cap = cancellation_cap(prec);
let mut extra = 32;
loop {
let (value, largest) = polylog_nonpositive(n, z, prec + extra, rm)?;
let lost = match value.exponent() {
Some(e) if !value.is_zero() && largest.is_finite() => {
(largest - f64::from(e)).max(0.0).ceil() as usize
}
_ if largest.is_finite() => prec + extra,
_ => 0,
};
if lost + 16 <= extra {
return Ok(round_to(value, prec, rm));
}
if extra >= cap || lost + 16 > cap {
return Err(special_exhausted(prec));
}
extra = (lost + 32).max(2 * extra).min(cap);
}
}
fn polylog_near_one(
s: &BigFloat,
s_int: Option<i64>,
z: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let cap = cancellation_cap(wp);
let mut extra = 0usize;
loop {
let p = wp + extra;
let (sum, largest) = polylog_near_one_at(s, s_int, z, p, rm, cc)?;
let lost = match (largest, sum.exponent()) {
(Some(l), Some(e)) if !sum.is_zero() => usize::try_from(l - i64::from(e)).unwrap_or(0),
(Some(_), _) => p,
_ => 0,
};
if lost <= extra + 24 {
return Ok(round_to(sum, wp, rm));
}
if extra >= cap {
return Err(special_exhausted(wp));
}
extra = (lost + 16).max(2 * extra).min(cap);
}
}
struct ZetaLeft {
wp: usize,
scale: BigFloat,
inv_two_pi: BigFloat,
sin: BigFloat,
cos: BigFloat,
gamma: BigFloat,
t: BigFloat,
coeffs: Vec<BigFloat>,
powers: Vec<BigFloat>,
inverses: Vec<BigFloat>,
two_pow: BigFloat,
}
impl ZetaLeft {
fn new(
sigma: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Self, SymplexError> {
let wp = wp + 16;
let one = BigFloat::from_i32(1, 64);
let t = one.sub(sigma, exact_bits(sigma, wp), rm);
let pi = cc.pi(wp, rm).clone();
let two = BigFloat::from_i32(2, 64);
let scale = bf_pow(&two, sigma, wp, rm, cc).mul(
&bf_pow(&pi, &sigma.sub(&one, wp, rm), wp, rm, cc),
wp,
rm,
);
let half_pi_sigma = pi.mul(sigma, wp, rm).div(&two, wp, rm);
let gamma = arb_gamma_real(&t, wp, rm, cc)?;
let n =
((wp as f64) * std::f64::consts::LN_2 / (3.0 + 8f64.sqrt()).ln()).ceil() as usize + 4;
let mut d: Vec<Ratio<BigInt>> = Vec::with_capacity(n + 1);
let mut acc = Ratio::<BigInt>::zero();
let mut term = Ratio::from_integer(BigInt::from(1));
for i in 0..=n {
if i > 0 {
let numer = BigInt::from((n + i - 1) as u64)
* BigInt::from(4u64)
* BigInt::from((n - i + 1) as u64);
let denom = BigInt::from((2 * i - 1) as u64) * BigInt::from((2 * i) as u64);
term *= Ratio::new(numer, denom);
}
acc += &term;
d.push(acc.clone());
}
let coeffs = (0..n)
.map(|k| {
let c = ratio_to_bigfloat(&((&d[k] - &d[n]) / &d[n]), wp, rm);
if k % 2 == 0 { c } else { c.neg() }
})
.collect();
let neg_t = t.neg();
let mut spf = vec![0usize; n + 1];
let mut powers: Vec<BigFloat> = Vec::with_capacity(n);
for m in 1..=n {
if m >= 2 && spf[m] == 0 {
for multiple in (m..=n).step_by(m) {
if spf[multiple] == 0 {
spf[multiple] = m;
}
}
}
let v = if m == 1 {
BigFloat::from_i32(1, wp)
} else if spf[m] == m {
bf_pow(&BigFloat::from_u64(m as u64, 64), &neg_t, wp, rm, cc)
} else {
powers[spf[m] - 1].mul(&powers[m / spf[m] - 1], wp, rm)
};
powers.push(v);
}
let inverses = (1..=n)
.map(|m| one.div(&BigFloat::from_u64(m as u64, 64), wp, rm))
.collect();
let two_pow = bf_pow(&two, &one.sub(&t, wp, rm), wp, rm, cc);
Ok(ZetaLeft {
wp,
scale,
inv_two_pi: one.div(&pi.mul(&two, wp, rm), wp, rm),
sin: half_pi_sigma.sin(wp, rm, cc),
cos: half_pi_sigma.cos(wp, rm, cc),
gamma,
t,
coeffs,
powers,
inverses,
two_pow,
})
}
fn next(&mut self, rm: RoundingMode) -> BigFloat {
let wp = self.wp;
let mut eta = BigFloat::new(wp);
for (c, p) in self.coeffs.iter().zip(self.powers.iter()) {
eta = eta.add(&c.mul(p, wp, rm), wp, rm);
}
let one = BigFloat::from_i32(1, 64);
let zeta_t = eta.neg().div(&one.sub(&self.two_pow, wp, rm), wp, rm);
let value = self
.scale
.mul(&self.sin, wp, rm)
.mul(&self.gamma, wp, rm)
.mul(&zeta_t, wp, rm);
self.scale = self.scale.mul(&self.inv_two_pi, wp, rm);
let (s, c) = (self.cos.neg(), self.sin.clone());
self.sin = s;
self.cos = c;
self.gamma = self.gamma.mul(&self.t, wp, rm);
self.t = self.t.add(&one, wp, rm);
for (p, inv) in self.powers.iter_mut().zip(self.inverses.iter()) {
*p = p.mul(inv, wp, rm);
}
self.two_pow = self.two_pow.div(&BigFloat::from_i32(2, 64), wp, rm);
value
}
}
fn polylog_near_one_at(
s: &BigFloat,
s_int: Option<i64>,
z: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(BigFloat, Option<i64>), SymplexError> {
let mut largest: Option<i64> = None;
let mut note = |v: &BigFloat| {
if !v.is_zero()
&& let Some(e) = v.exponent()
{
largest = Some(largest.map_or(i64::from(e), |m| m.max(i64::from(e))));
}
};
let one = BigFloat::from_i32(1, wp);
let mu = z.ln(wp, rm, cc); let neg_mu = mu.neg();
let max_terms = wp * 2 + 100;
let not_converged = || series_did_not_converge("polylog", max_terms);
let mut sum = BigFloat::new(wp);
let mut mu_pow_over_fact = one.clone(); let mut small_run = 0u8;
let mut converged = false;
match s_int {
Some(n) if n >= 1 => {
let n = n as usize;
for k in 0..max_terms {
if k > 0 {
mu_pow_over_fact = mu_pow_over_fact.mul(&mu, wp, rm).div(
&BigFloat::from_i128(k as i128, wp),
wp,
rm,
);
}
if k + 1 == n {
let mut h = BigFloat::new(wp);
for j in 1..n {
h = h.add(
&one.div(&BigFloat::from_i128(j as i128, wp), wp, rm),
wp,
rm,
);
}
let ln_neg_mu = neg_mu.ln(wp, rm, cc);
let t = mu_pow_over_fact.mul(&h.sub(&ln_neg_mu, wp, rm), wp, rm);
note(&t);
sum = sum.add(&t, wp, rm);
continue;
}
let arg = n as i64 - k as i64;
let zeta = if arg <= 0 {
zeta_nonpositive_int(arg, wp, rm)
} else {
arb_zeta(&BigFloat::from_i128(arg as i128, wp), wp, rm, cc)?
};
let term = zeta.mul(&mu_pow_over_fact, wp, rm);
note(&term);
sum = sum.add(&term, wp, rm);
if k >= n {
if negligible(&term, &sum, wp) {
small_run += 1;
if small_run >= 2 {
converged = true;
break;
}
} else {
small_run = 0;
}
}
}
if !converged {
return Err(not_converged());
}
Ok((sum, largest))
}
_ => {
let one_minus_s = BigFloat::from_i32(1, 64).sub(s, exact_bits(s, wp), rm);
let g = arb_gamma_real(&one_minus_s, wp, rm, cc)?;
let s_minus_1 = s.sub(&one, wp, rm);
let lead = g.mul(&bf_pow(&neg_mu, &s_minus_1, wp, rm, cc), wp, rm);
note(&lead);
let minus_one = BigFloat::from_i32(-1, 64);
let mut left: Option<ZetaLeft> = None;
for k in 0..max_terms {
if k > 0 {
mu_pow_over_fact = mu_pow_over_fact.mul(&mu, wp, rm).div(
&BigFloat::from_i128(k as i128, wp),
wp,
rm,
);
}
let arg = s.sub(&BigFloat::from_i128(k as i128, wp), exact_bits(s, wp), rm);
let zeta = if left.is_some() || bf_lt(&arg, &minus_one) {
if left.is_none() {
left = Some(ZetaLeft::new(&arg, wp, rm, cc)?);
}
match left.as_mut() {
Some(l) => l.next(rm),
None => arb_zeta(&arg, wp, rm, cc)?,
}
} else {
arb_zeta(&arg, wp, rm, cc)?
};
let term = zeta.mul(&mu_pow_over_fact, wp, rm);
note(&term);
sum = sum.add(&term, wp, rm);
if k >= 2 {
if negligible(&term, &sum, wp) {
small_run += 1;
if small_run >= 2 {
converged = true;
break;
}
} else {
small_run = 0;
}
}
}
if !converged {
return Err(not_converged());
}
Ok((lead.add(&sum, wp, rm), largest))
}
}
}
fn polylog_series_limit(s_int: Option<i64>, wp: usize) -> BigFloat {
if s_int.is_some() {
BigFloat::from_f64(0.5, wp)
} else {
BigFloat::from_f64(0.75, wp)
}
}
fn polylog_unit_interval(
s: &BigFloat,
s_int: Option<i64>,
z: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let limit = polylog_series_limit(s_int, wp);
let az = z.abs();
let large = bigfloat_to_f64_rounded(s, rm).is_ok_and(|v| v >= wp as f64 / 4.0 + 8.0);
if large || !bf_gt(&az, &limit) {
return polylog_series(s, s_int, z, wp, rm, cc);
}
if z.is_negative() {
let cap = cancellation_cap(wp);
let mut extra = 0usize;
loop {
let p = wp + extra;
let one = BigFloat::from_i32(1, p);
let x2 = az.mul(&az, 2 * p, rm);
let a = polylog_unit_interval(s, s_int, &x2, p, rm, cc)?;
let b = polylog_near_one(s, s_int, &az, p, rm, cc)?;
let two = BigFloat::from_i32(2, p);
let one_minus_s = one.sub(s, exact_bits(s, p), rm);
let scale = bf_pow(&two, &one_minus_s, p, rm, cc);
let first = scale.mul(&a, p, rm);
let r = first.sub(&b, p, rm);
let largest = first.exponent().max(b.exponent()).map_or(0, i64::from);
let lost = match r.exponent() {
Some(e) if !r.is_zero() => usize::try_from(largest - i64::from(e)).unwrap_or(0),
_ => p,
};
if lost <= extra + 24 {
return Ok(round_to(r, wp, rm));
}
if extra >= cap {
return Err(special_exhausted(wp));
}
extra = (lost + 16).max(2 * extra).min(cap);
}
}
polylog_near_one(s, s_int, z, wp, rm, cc)
}
fn arb_polylog(
s: &BigFloat,
z: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if s.is_nan() || z.is_nan() || s.is_inf() || z.is_inf() {
return Err(unevaluable("polylog of special float value"));
}
if z.is_zero() {
return Ok(BigFloat::new(prec));
}
let wp = prec + 32;
let one = BigFloat::from_i32(1, wp);
let s_int = bf_as_int(s, rm, cc)?;
if let Some(n) = s_int
&& n <= 0
{
if n.unsigned_abs() > MAX_NONPOSITIVE_ORDER && n.unsigned_abs() <= 1_000_000 {
let v = polylog::polylog_general(s, &(z.clone(), BigFloat::new(prec)), prec, rm, cc)?;
return Ok(round_to(v.0, prec, rm));
}
return polylog_nonpositive_measured(n.unsigned_abs() as usize, z, prec, rm);
}
let az = z.abs();
if bf_gt(&az, &one) {
return Err(unevaluable(
"polylog outside the unit disc not yet supported in evalf",
));
}
if az.sub(&one, wp, rm).is_zero() {
if z.is_negative() {
let eta = arb_eta(s, wp, rm, cc)?;
return Ok(round_to(eta.neg(), prec, rm));
}
if !bf_gt(s, &one) {
return Err(unevaluable("polylog(s, 1) diverges for s ≤ 1"));
}
let zeta = arb_zeta(s, wp, rm, cc)?;
return Ok(round_to(zeta, prec, rm));
}
let r = polylog_unit_interval(s, s_int, z, wp, rm, cc)?;
Ok(round_to(r, prec, rm))
}
fn arb_eta(
s: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if bf_strictly_positive(s) {
return arb_eta_borwein(s, wp, rm, cc);
}
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let one_minus_s = one.sub(s, wp, rm);
let coeff = one.sub(&bf_pow(&two, &one_minus_s, wp, rm, cc), wp, rm);
let zeta = arb_zeta(s, wp, rm, cc)?;
Ok(coeff.mul(&zeta, wp, rm))
}
fn arb_dirichlet_eta(
s: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if s.is_nan() || s.is_inf_neg() {
return Err(unevaluable("dirichlet_eta of special float value"));
}
if s.is_inf_pos() {
return Ok(BigFloat::from_i32(1, prec));
}
let wp = prec + 32;
let mut sw = s.clone();
let _ = sw.set_precision(wp, rm);
Ok(round_to(arb_eta(&sw, wp, rm, cc)?, prec, rm))
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum AiryKind {
Ai,
Bi,
AiPrime,
BiPrime,
}
fn arb_airy(
x: &BigFloat,
kind: AiryKind,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if x.is_nan() || x.is_inf() {
return Err(unevaluable("Airy function of special float value"));
}
let base_wp = prec + 32;
let x_f = bigfloat_to_f64(x, rm, cc)?;
let ax_f = x_f.abs();
let zeta_f = 2.0 / 3.0 * ax_f.powf(1.5);
if 2.0 * zeta_f > (base_wp as f64) * std::f64::consts::LN_2 {
let z_bits = 3 * x.exponent().map_or(0, i64::from).max(0) / 2;
if !zeta_f.is_finite() || z_bits > i64::try_from(cancellation_cap(prec)).unwrap_or(i64::MAX)
{
return Err(special_exhausted(prec));
}
return Ok(round_to(
airy_asymptotic(x, kind, base_wp, rm, cc),
prec,
rm,
));
}
let wp = base_wp + (2.0 * zeta_f * std::f64::consts::LOG2_E).ceil() as usize + 16;
let mut xw = x.clone();
let _ = xw.set_precision(wp, rm);
let one = BigFloat::from_i32(1, wp);
let three = BigFloat::from_i32(3, wp);
let third = one.div(&three, wp, rm);
let two_thirds = third.mul(&BigFloat::from_i32(2, wp), wp, rm);
let g13 = arb_gamma_real(&third, wp, rm, cc)?;
let g23 = arb_gamma_real(&two_thirds, wp, rm, cc)?;
let c1 = one.div(
&bf_pow(&three, &two_thirds, wp, rm, cc).mul(&g23, wp, rm),
wp,
rm,
);
let c2 = one.div(
&bf_pow(&three, &third, wp, rm, cc).mul(&g13, wp, rm),
wp,
rm,
);
let x3 = xw.mul(&xw, wp, rm).mul(&xw, wp, rm);
let derivative = matches!(kind, AiryKind::AiPrime | AiryKind::BiPrime);
let (mut f_sum, mut g_sum) = if derivative {
(BigFloat::new(wp), one.clone()) } else {
(one.clone(), xw.clone()) };
let mut fk = one.clone(); let mut gk = xw.clone(); let max_terms = if xw.is_zero() {
0
} else {
(ax_f * 2.0) as usize + wp + 40
};
for k in 1..=max_terms {
let k3 = 3 * k as i128;
fk = fk
.mul(&x3, wp, rm)
.div(&BigFloat::from_i128((k3 - 1) * k3, wp), wp, rm);
gk = gk
.mul(&x3, wp, rm)
.div(&BigFloat::from_i128(k3 * (k3 + 1), wp), wp, rm);
let (tf, tg) = if derivative {
(
fk.mul(&BigFloat::from_i128(k3, wp), wp, rm)
.div(&xw, wp, rm),
gk.mul(&BigFloat::from_i128(k3 + 1, wp), wp, rm)
.div(&xw, wp, rm),
)
} else {
(fk.clone(), gk.clone())
};
f_sum = f_sum.add(&tf, wp, rm);
g_sum = g_sum.add(&tg, wp, rm);
if negligible(&tf, &one, wp + 4) && negligible(&tg, &one, wp + 4) {
break;
}
}
let a = c1.mul(&f_sum, wp, rm);
let b = c2.mul(&g_sum, wp, rm);
let r = match kind {
AiryKind::Ai | AiryKind::AiPrime => a.sub(&b, wp, rm),
AiryKind::Bi | AiryKind::BiPrime => three.sqrt(wp, rm).mul(&a.add(&b, wp, rm), wp, rm),
};
Ok(round_to(r, prec, rm))
}
fn airy_asymptotic(
x: &BigFloat,
kind: AiryKind,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> BigFloat {
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let ax = x.abs();
let sqrt_ax = ax.sqrt(wp, rm);
let x_quarter = sqrt_ax.sqrt(wp, rm); let wz =
wp + usize::try_from(3 * ax.exponent().map_or(0, i64::from).max(0) / 2 + 8).unwrap_or(0);
let ax_z = at_precision(&ax, wz, rm);
let zeta = two.mul(&ax_z, wz, rm).mul(&ax_z.sqrt(wz, rm), wz, rm).div(
&BigFloat::from_i32(3, wz),
wz,
rm,
);
let inv_zeta = one.div(&zeta, wp, rm);
let sqrt_pi = cc.pi(wp, rm).clone().sqrt(wp, rm);
let derivative = matches!(kind, AiryKind::AiPrime | AiryKind::BiPrime);
let mut coeffs: Vec<BigFloat> = Vec::new();
let mut u = one.clone();
let mut zeta_pow = one.clone();
let mut prev: Option<BigFloat> = None;
for k in 0..(wp * 2 + 100) {
if k > 0 {
let k_i = k as i128;
let num = BigFloat::from_i128((6 * k_i - 5) * (6 * k_i - 3) * (6 * k_i - 1), wp);
let den = BigFloat::from_i128(216 * k_i * (2 * k_i - 1), wp);
u = u.mul(&num, wp, rm).div(&den, wp, rm);
zeta_pow = zeta_pow.mul(&inv_zeta, wp, rm);
}
let mut c = u.mul(&zeta_pow, wp, rm);
if derivative {
let k_i = k as i128;
let ratio = BigFloat::from_i128(6 * k_i + 1, wp).div(
&BigFloat::from_i128(6 * k_i - 1, wp),
wp,
rm,
);
c = c.mul(&ratio, wp, rm).neg();
}
if let Some(ref p) = prev
&& bf_gt(&c.abs(), p)
{
break;
}
let done = negligible(&c, &one, wp);
prev = Some(c.abs());
coeffs.push(c);
if done {
break;
}
}
if !x.is_negative() {
let mut sum = BigFloat::new(wp);
let alternate = matches!(kind, AiryKind::Ai | AiryKind::AiPrime);
for (k, c) in coeffs.iter().enumerate() {
if alternate && k % 2 == 1 {
sum = sum.sub(c, wp, rm);
} else {
sum = sum.add(c, wp, rm);
}
}
let amp = match kind {
AiryKind::Ai => zeta.neg().exp(wp, rm, cc).div(
&two.mul(&sqrt_pi, wp, rm).mul(&x_quarter, wp, rm),
wp,
rm,
),
AiryKind::Bi => zeta
.exp(wp, rm, cc)
.div(&sqrt_pi.mul(&x_quarter, wp, rm), wp, rm),
AiryKind::AiPrime => x_quarter
.mul(&zeta.neg().exp(wp, rm, cc), wp, rm)
.div(&two.mul(&sqrt_pi, wp, rm), wp, rm)
.neg(),
AiryKind::BiPrime => x_quarter
.mul(&zeta.exp(wp, rm, cc), wp, rm)
.div(&sqrt_pi, wp, rm),
};
return amp.mul(&sum, wp, rm);
}
let mut p = BigFloat::new(wp);
let mut q = BigFloat::new(wp);
for (k, c) in coeffs.iter().enumerate() {
let sign_neg = (k / 2) % 2 == 1;
let target = if k % 2 == 0 { &mut p } else { &mut q };
*target = if sign_neg {
target.sub(c, wp, rm)
} else {
target.add(c, wp, rm)
};
}
let pi = cc.pi(wz, rm).clone();
let theta = zeta.add(&pi.div(&BigFloat::from_i32(4, wz), wz, rm), wz, rm);
let s = theta.sin(wp, rm, cc);
let c = theta.cos(wp, rm, cc);
let sp_minus_cq = s.mul(&p, wp, rm).sub(&c.mul(&q, wp, rm), wp, rm);
let cp_plus_sq = c.mul(&p, wp, rm).add(&s.mul(&q, wp, rm), wp, rm);
match kind {
AiryKind::Ai => sp_minus_cq.div(&sqrt_pi.mul(&x_quarter, wp, rm), wp, rm),
AiryKind::Bi => cp_plus_sq.div(&sqrt_pi.mul(&x_quarter, wp, rm), wp, rm),
AiryKind::AiPrime => x_quarter
.mul(&cp_plus_sq, wp, rm)
.div(&sqrt_pi, wp, rm)
.neg(),
AiryKind::BiPrime => x_quarter.mul(&sp_minus_cq, wp, rm).div(&sqrt_pi, wp, rm),
}
}
fn all_close(vals: &[&BigFloat], bits: usize, wp: usize, rm: RoundingMode) -> bool {
let first = vals[0];
vals[1..]
.iter()
.all(|v| negligible(&v.sub(first, wp, rm), first, bits))
}
fn carlson_bits(wp: usize) -> usize {
wp / 2 + 8
}
fn carlson_rc(
x: &BigFloat,
y: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> BigFloat {
let one = BigFloat::from_i32(1, wp);
let diff = y.sub(x, wp, rm);
let inv_sqrt_y = one.div(&y.sqrt(wp, rm), wp, rm);
if diff.is_zero() {
return inv_sqrt_y;
}
let w = diff.div(y, wp, rm);
let w_f = bigfloat_to_f64(&w, rm, cc).unwrap_or(1.0);
if w_f.abs() < 0.1 {
let mut sum = one.clone();
let mut c = one.clone();
let mut wk = one.clone();
for k in 1..(wp * 2 + 100) {
let k_i = k as i128;
let num = BigFloat::from_i128((2 * k_i - 1) * (2 * k_i - 1), wp);
let den = BigFloat::from_i128(2 * k_i * (2 * k_i + 1), wp);
c = c.mul(&num, wp, rm).div(&den, wp, rm);
wk = wk.mul(&w, wp, rm);
let term = c.mul(&wk, wp, rm);
sum = sum.add(&term, wp, rm);
if negligible(&term, &sum, wp) {
break;
}
}
return inv_sqrt_y.mul(&sum, wp, rm);
}
let ratio = x.div(y, wp, rm).sqrt(wp, rm); if diff.is_positive() {
ratio.acos(wp, rm, cc).div(&diff.sqrt(wp, rm), wp, rm)
} else {
ratio
.acosh(wp, rm, cc)
.div(&diff.neg().sqrt(wp, rm), wp, rm)
}
}
fn carlson_rf(
x: &BigFloat,
y: &BigFloat,
z: &BigFloat,
wp: usize,
rm: RoundingMode,
) -> Result<BigFloat, SymplexError> {
let four = BigFloat::from_i32(4, wp);
let (mut x, mut y, mut z) = (x.clone(), y.clone(), z.clone());
for _ in 0..(wp * 2 + 200) {
if all_close(&[&x, &y, &z], carlson_bits(wp), wp, rm) {
let one = BigFloat::from_i32(1, wp);
let mu = x
.add(&y, wp, rm)
.add(&z, wp, rm)
.div(&BigFloat::from_i32(3, wp), wp, rm);
return Ok(one.div(&mu.sqrt(wp, rm), wp, rm));
}
let (sx, sy, sz) = (x.sqrt(wp, rm), y.sqrt(wp, rm), z.sqrt(wp, rm));
let lambda =
sx.mul(&sy, wp, rm)
.add(&sy.mul(&sz, wp, rm), wp, rm)
.add(&sz.mul(&sx, wp, rm), wp, rm);
x = x.add(&lambda, wp, rm).div(&four, wp, rm);
y = y.add(&lambda, wp, rm).div(&four, wp, rm);
z = z.add(&lambda, wp, rm).div(&four, wp, rm);
}
Err(SymplexError::ComputationFailed {
operation: "evalf",
reason: "Carlson R_F did not converge".into(),
})
}
fn carlson_rd(
x: &BigFloat,
y: &BigFloat,
z: &BigFloat,
wp: usize,
rm: RoundingMode,
) -> Result<BigFloat, SymplexError> {
let one = BigFloat::from_i32(1, wp);
let three = BigFloat::from_i32(3, wp);
let four = BigFloat::from_i32(4, wp);
let (mut x, mut y, mut z) = (x.clone(), y.clone(), z.clone());
let mut acc = BigFloat::new(wp);
let mut scale = one.clone(); for _ in 0..(wp * 2 + 200) {
if all_close(&[&x, &y, &z], carlson_bits(wp), wp, rm) {
let mu = x.add(&y, wp, rm).add(&three.mul(&z, wp, rm), wp, rm).div(
&BigFloat::from_i32(5, wp),
wp,
rm,
);
let tail = one.div(&mu.mul(&mu.sqrt(wp, rm), wp, rm), wp, rm);
return Ok(acc.add(&scale.mul(&tail, wp, rm), wp, rm));
}
let (sx, sy, sz) = (x.sqrt(wp, rm), y.sqrt(wp, rm), z.sqrt(wp, rm));
let lambda =
sx.mul(&sy, wp, rm)
.add(&sy.mul(&sz, wp, rm), wp, rm)
.add(&sz.mul(&sx, wp, rm), wp, rm);
let term = three.div(&sz.mul(&z.add(&lambda, wp, rm), wp, rm), wp, rm);
acc = acc.add(&scale.mul(&term, wp, rm), wp, rm);
scale = scale.div(&four, wp, rm);
x = x.add(&lambda, wp, rm).div(&four, wp, rm);
y = y.add(&lambda, wp, rm).div(&four, wp, rm);
z = z.add(&lambda, wp, rm).div(&four, wp, rm);
}
Err(SymplexError::ComputationFailed {
operation: "evalf",
reason: "Carlson R_D did not converge".into(),
})
}
fn carlson_rj(
x: &BigFloat,
y: &BigFloat,
z: &BigFloat,
p: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if !p.is_positive() {
return Err(unevaluable(
"elliptic_pi: the characteristic n must satisfy n < 1 for a real value",
));
}
let one = BigFloat::from_i32(1, wp);
let three = BigFloat::from_i32(3, wp);
let four = BigFloat::from_i32(4, wp);
let (mut x, mut y, mut z, mut p) = (x.clone(), y.clone(), z.clone(), p.clone());
let mut acc = BigFloat::new(wp);
let mut scale = one.clone();
for _ in 0..(wp * 2 + 200) {
if all_close(&[&x, &y, &z, &p], carlson_bits(wp), wp, rm) {
let two_p = p.mul(&BigFloat::from_i32(2, wp), wp, rm);
let mu = x.add(&y, wp, rm).add(&z, wp, rm).add(&two_p, wp, rm).div(
&BigFloat::from_i32(5, wp),
wp,
rm,
);
let tail = one.div(&mu.mul(&mu.sqrt(wp, rm), wp, rm), wp, rm);
return Ok(acc.add(&scale.mul(&tail, wp, rm), wp, rm));
}
let (sx, sy, sz, sp) = (
x.sqrt(wp, rm),
y.sqrt(wp, rm),
z.sqrt(wp, rm),
p.sqrt(wp, rm),
);
let lambda =
sx.mul(&sy, wp, rm)
.add(&sy.mul(&sz, wp, rm), wp, rm)
.add(&sz.mul(&sx, wp, rm), wp, rm);
let alpha = p.mul(&sx.add(&sy, wp, rm).add(&sz, wp, rm), wp, rm).add(
&sx.mul(&sy, wp, rm).mul(&sz, wp, rm),
wp,
rm,
);
let beta = sp.mul(&p.add(&lambda, wp, rm), wp, rm);
let rc = carlson_rc(
&alpha.mul(&alpha, wp, rm),
&beta.mul(&beta, wp, rm),
wp,
rm,
cc,
);
acc = acc.add(&scale.mul(&three.mul(&rc, wp, rm), wp, rm), wp, rm);
scale = scale.div(&four, wp, rm);
x = x.add(&lambda, wp, rm).div(&four, wp, rm);
y = y.add(&lambda, wp, rm).div(&four, wp, rm);
z = z.add(&lambda, wp, rm).div(&four, wp, rm);
p = p.add(&lambda, wp, rm).div(&four, wp, rm);
}
Err(SymplexError::ComputationFailed {
operation: "evalf",
reason: "Carlson R_J did not converge".into(),
})
}
fn one_minus_m(
m: &BigFloat,
wp: usize,
rm: RoundingMode,
what: &str,
) -> Result<BigFloat, SymplexError> {
let one = BigFloat::from_i32(1, wp);
let v = one.sub(m, wp, rm);
if v.is_negative() {
return Err(unevaluable(format!(
"{what}: parameter m must satisfy m ≤ 1 for a real value"
)));
}
Ok(v)
}
fn arb_elliptic_k(
m: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if m.is_nan() || m.is_inf() {
return Err(unevaluable("elliptic_k of special float value"));
}
let wp = prec + 32;
let y = one_minus_m(m, wp, rm, "elliptic_k")?;
if y.is_zero() {
return Err(unevaluable("elliptic_k(1) is infinite"));
}
let _ = cc;
let r = carlson_rf(&BigFloat::new(wp), &y, &BigFloat::from_i32(1, wp), wp, rm)?;
Ok(round_to(r, prec, rm))
}
fn arb_elliptic_e(
m: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if m.is_nan() || m.is_inf() {
return Err(unevaluable("elliptic_e of special float value"));
}
let wp = prec + 32;
let one = BigFloat::from_i32(1, wp);
let y = one_minus_m(m, wp, rm, "elliptic_e")?;
if y.is_zero() {
return Ok(BigFloat::from_i32(1, prec));
}
let _ = cc;
let zero = BigFloat::new(wp);
let rf = carlson_rf(&zero, &y, &one, wp, rm)?;
let rd = carlson_rd(&zero, &y, &one, wp, rm)?;
let three = BigFloat::from_i32(3, wp);
let r = rf.sub(&m.mul(&rd, wp, rm).div(&three, wp, rm), wp, rm);
Ok(round_to(r, prec, rm))
}
fn arb_elliptic_f(
phi: &BigFloat,
m: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if phi.is_nan() || phi.is_inf() || m.is_nan() || m.is_inf() {
return Err(unevaluable("elliptic_f of special float value"));
}
if phi.is_zero() {
return Ok(BigFloat::new(prec));
}
let wp = prec + 32;
let one = BigFloat::from_i32(1, wp);
let pi = cc.pi(wp, rm).clone();
let k_bf = phi
.div(&pi, wp, rm)
.add(&BigFloat::from_f64(0.5, wp), wp, rm)
.floor();
let phi0 = phi.sub(&k_bf.mul(&pi, wp, rm), wp, rm);
let s = phi0.sin(wp, rm, cc);
let c = phi0.cos(wp, rm, cc);
let s2 = s.mul(&s, wp, rm);
let c2 = c.mul(&c, wp, rm);
let y = one.sub(&m.mul(&s2, wp, rm), wp, rm);
if !y.is_positive() {
return Err(unevaluable(
"elliptic_f: 1 − m sin²φ must be positive for a real value",
));
}
let mut r = s.mul(&carlson_rf(&c2, &y, &one, wp, rm)?, wp, rm);
if !k_bf.is_zero() {
let k = arb_elliptic_k(m, wp, rm, cc)?;
let two = BigFloat::from_i32(2, wp);
r = r.add(&two.mul(&k_bf, wp, rm).mul(&k, wp, rm), wp, rm);
}
Ok(round_to(r, prec, rm))
}
fn arb_elliptic_pi(
n: &BigFloat,
m: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
if n.is_nan() || n.is_inf() || m.is_nan() || m.is_inf() {
return Err(unevaluable("elliptic_pi of special float value"));
}
let wp = prec + 32;
let one = BigFloat::from_i32(1, wp);
let zero = BigFloat::new(wp);
let y = one_minus_m(m, wp, rm, "elliptic_pi")?;
if y.is_zero() {
return Err(unevaluable("elliptic_pi(n, 1) is infinite"));
}
let p = one.sub(n, wp, rm);
let rf = carlson_rf(&zero, &y, &one, wp, rm)?;
if n.is_zero() {
return Ok(round_to(rf, prec, rm));
}
let rj = carlson_rj(&zero, &y, &one, &p, wp, rm, cc)?;
let three = BigFloat::from_i32(3, wp);
let r = rf.add(&n.mul(&rj, wp, rm).div(&three, wp, rm), wp, rm);
Ok(round_to(r, prec, rm))
}
fn poly_degree(
n: &BigFloat,
what: &str,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<u64, SymplexError> {
match bf_as_int(n, rm, cc)? {
Some(d) if n.is_int() && (0..=10_000_000).contains(&d) => Ok(d as u64),
_ => Err(unevaluable(format!(
"{what}: degree must be a non-negative integer"
))),
}
}
fn arb_gegenbauer(
n: &BigFloat,
a: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let deg = poly_degree(n, "gegenbauer", rm, cc)?;
let wp = prec + 32 + (deg as f64).log2().ceil().max(0.0) as usize;
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
if deg == 0 {
return Ok(BigFloat::from_i32(1, prec));
}
let mut prev = one.clone();
let mut curr = two.mul(a, wp, rm).mul(x, wp, rm);
for k in 1..deg {
let k_bf = BigFloat::from_i128(k as i128, wp);
let t1 = two
.mul(&k_bf.add(a, wp, rm), wp, rm)
.mul(x, wp, rm)
.mul(&curr, wp, rm);
let coeff = k_bf.add(&two.mul(a, wp, rm), wp, rm).sub(&one, wp, rm);
let t2 = coeff.mul(&prev, wp, rm);
let next = t1
.sub(&t2, wp, rm)
.div(&BigFloat::from_i128(k as i128 + 1, wp), wp, rm);
prev = curr;
curr = next;
}
Ok(round_to(curr, prec, rm))
}
const MAX_JACOBI_DEGREE: usize = 5000;
fn arb_jacobi(
n: &BigFloat,
a: &BigFloat,
b: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let deg = poly_degree(n, "jacobi", rm, cc)? as usize;
if deg > MAX_JACOBI_DEGREE {
return Err(SymplexError::NotImplemented(format!(
"jacobi of degree {deg} (at most {MAX_JACOBI_DEGREE} in evalf)"
)));
}
if deg == 0 {
return Ok(BigFloat::from_i32(1, prec));
}
if x.is_zero() && deg % 2 == 1 && a.cmp(b) == Some(0) {
return Ok(BigFloat::new(prec));
}
let rounding = 4 + 2 * (usize::BITS - (deg + 2).leading_zeros()) as usize;
let first_extra = if deg <= 2000 {
32 + 2 * deg
} else {
let xf = bigfloat_to_f64_rounded(x, rm)?.abs();
let est = if xf < 1.0 {
deg as f64 * (1.0 + (1.0 - xf * xf).sqrt()).log2()
} else {
0.0
};
40 + rounding + est.ceil() as usize
};
let cap = cancellation_cap(prec) + 2 * deg + 64;
let mut extra = first_extra;
loop {
let wp = prec + extra;
let (sum, largest) = jacobi_sum(deg, a, b, x, wp, rm, cc)?;
let lost = match (sum.exponent(), largest) {
(Some(e), Some(l)) if !sum.is_zero() => (i64::from(l) - i64::from(e)).max(0) as usize,
(_, None) => 0,
_ => wp,
};
if lost + rounding + 8 <= extra {
return Ok(round_to(sum, prec, rm));
}
if extra >= cap {
return Err(special_exhausted(prec));
}
extra = (lost + rounding + 40).max(extra + 64).min(cap);
}
}
fn jacobi_sum(
deg: usize,
a: &BigFloat,
b: &BigFloat,
x: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(BigFloat, Option<i32>), SymplexError> {
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let lo = x.sub(&one, wp, rm).div(&two, wp, rm);
let hi = x.add(&one, wp, rm).div(&two, wp, rm);
let int = |j: usize| BigFloat::from_u64(j as u64, 64);
let shifted = |p: &BigFloat, from: usize| -> BigFloat {
let mut acc = one.clone();
for j in from..=deg {
acc = acc.mul(&p.add(&int(j), wp, rm), wp, rm);
}
acc
};
let first = match bf_as_int(a, rm, cc)? {
Some(m) if a.is_int() && m < 0 && m.unsigned_abs() as usize <= deg => {
m.unsigned_abs() as usize
}
_ => 0,
};
let hi_zero = hi.is_zero();
let ratio = if hi_zero {
BigFloat::new(wp)
} else {
lo.div(&hi, wp, rm)
};
let mut pa = shifted(a, first + 1);
let mut pb = one.clone();
for s in 1..=first {
pb = pb.mul(&b.add(&int(deg - s + 1), wp, rm), wp, rm);
}
let f0 = factorial_bigint(deg - first) * factorial_bigint(first);
let mut inv_fact = one.div(&ratio_to_bigfloat(&f0, wp, rm), wp, rm);
let direct_pow = |s: usize| lo.powi(s, wp, rm).mul(&hi.powi(deg - s, wp, rm), wp, rm);
let mut pow = if hi_zero {
BigFloat::new(wp)
} else {
direct_pow(first)
};
let direct = !hi_zero && !lo.is_zero() && (pow.is_zero() || pow.is_inf());
let mut sum = BigFloat::new(wp);
let mut largest: Option<i32> = None;
for s in first..=deg {
if s > first {
pa = pa.div(&a.add(&int(s), wp, rm), wp, rm);
pb = pb.mul(&b.add(&int(deg - s + 1), wp, rm), wp, rm);
inv_fact = inv_fact.mul(&int(deg - s + 1), wp, rm).div(&int(s), wp, rm);
pow = if direct {
direct_pow(s)
} else {
pow.mul(&ratio, wp, rm)
};
}
if hi_zero && s == deg {
pow = lo.powi(deg, wp, rm);
}
let term = pa.mul(&pb, wp, rm).mul(&inv_fact, wp, rm).mul(&pow, wp, rm);
if let Some(e) = term.exponent().filter(|_| !term.is_zero()) {
largest = Some(largest.map_or(e, |l| l.max(e)));
}
sum = sum.add(&term, wp, rm);
}
Ok((sum, largest))
}
fn factorial_bigint(n: usize) -> Ratio<BigInt> {
let mut f = BigInt::from(1);
for i in 2..=n {
f *= BigInt::from(i as u64);
}
Ratio::from_integer(f)
}
fn arb_assoc_legendre(
n: &BigFloat,
m: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let deg = poly_degree(n, "assoc_legendre", rm, cc)? as i64;
let order = bf_as_int(m, rm, cc)?
.filter(|_| m.is_int())
.ok_or_else(|| unevaluable("assoc_legendre: order must be an integer"))?;
if order.abs() > deg {
return Ok(BigFloat::new(prec));
}
let wp = prec + 32 + 2 * (deg as f64).log2().ceil().max(0.0) as usize;
let one = BigFloat::from_i32(1, wp);
let mu = order.unsigned_abs() as usize;
let x2 = x.mul(x, wp, rm);
let one_minus_x2 = one.sub(&x2, wp, rm);
let root_pow = if mu.is_multiple_of(2) {
one_minus_x2.powi(mu / 2, wp, rm)
} else {
if one_minus_x2.is_negative() {
return Err(unevaluable(
"assoc_legendre with odd order is complex for |x| > 1",
));
}
one_minus_x2
.sqrt(wp, rm)
.mul(&one_minus_x2.powi(mu / 2, wp, rm), wp, rm)
};
let mut dfact = BigInt::from(1);
for j in 1..=mu {
dfact *= BigInt::from((2 * j - 1) as u64);
}
if mu % 2 == 1 {
dfact = -dfact;
}
let mut prev = ratio_to_bigfloat(&Ratio::from_integer(dfact), wp, rm).mul(&root_pow, wp, rm);
let deg_u = deg as usize;
let mut curr = if deg_u > mu {
BigFloat::from_i128(2 * mu as i128 + 1, wp)
.mul(x, wp, rm)
.mul(&prev, wp, rm)
} else {
prev.clone()
};
for k in (mu + 1)..deg_u {
let t1 = BigFloat::from_i128(2 * k as i128 + 1, wp)
.mul(x, wp, rm)
.mul(&curr, wp, rm);
let t2 = BigFloat::from_i128((k + mu) as i128, wp).mul(&prev, wp, rm);
let next = t1
.sub(&t2, wp, rm)
.div(&BigFloat::from_i128((k - mu + 1) as i128, wp), wp, rm);
prev = curr;
curr = next;
}
if order < 0 {
let mut ratio = factorial_bigint(deg_u - mu) / factorial_bigint(deg_u + mu);
if mu % 2 == 1 {
ratio = -ratio;
}
curr = curr.mul(&ratio_to_bigfloat(&ratio, wp, rm), wp, rm);
}
Ok(round_to(curr, prec, rm))
}
fn arb_assoc_laguerre(
n: &BigFloat,
a: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let deg = poly_degree(n, "assoc_laguerre", rm, cc)?;
let wp = prec + 32 + (deg as f64).log2().ceil().max(0.0) as usize;
let one = BigFloat::from_i32(1, wp);
if deg == 0 {
return Ok(BigFloat::from_i32(1, prec));
}
let mut prev = one.clone();
let mut curr = one.add(a, wp, rm).sub(x, wp, rm);
for k in 1..deg {
let k_bf = BigFloat::from_i128(k as i128, wp);
let coeff = BigFloat::from_i128(2 * k as i128 + 1, wp)
.add(a, wp, rm)
.sub(x, wp, rm);
let t1 = coeff.mul(&curr, wp, rm);
let t2 = k_bf.add(a, wp, rm).mul(&prev, wp, rm);
let next = t1
.sub(&t2, wp, rm)
.div(&BigFloat::from_i128(k as i128 + 1, wp), wp, rm);
prev = curr;
curr = next;
}
Ok(round_to(curr, prec, rm))
}
fn not_evaluated_reason(id: ExprId) -> String {
format!("sub-expression {id:?} has no numerical value")
}
fn get_cached(cache: &FxHashMap<ExprId, Complex>, id: ExprId) -> Result<&Complex, SymplexError> {
cache.get(&id).ok_or_else(|| SymplexError::Unevaluable {
reason: not_evaluated_reason(id),
})
}
fn ratio_to_bigfloat(r: &Ratio<BigInt>, prec: usize, rm: RoundingMode) -> BigFloat {
crate::base::numeric::ratio_to_bigfloat(r, prec, rm)
}
fn try_as_small_int(arena: &Arena, id: ExprId) -> Option<i32> {
let r = arena.as_num(id)?;
if !r.is_integer() {
return None;
}
let n = r.to_integer();
let val: i64 = n.try_into().ok()?;
if val >= i32::MIN as i64 && val <= i32::MAX as i64 {
Some(val as i32)
} else {
None
}
}
fn is_negligible_part(part: &BigFloat, other: &BigFloat, digits: u32) -> bool {
if part.is_zero() {
return true;
}
if other.is_zero() {
return false;
}
match (part.exponent(), other.exponent()) {
(Some(p_exp), Some(o_exp)) => {
let bit_threshold = (digits as i64) * 34 / 10 + 4;
(o_exp as i64 - p_exp as i64) > bit_threshold
}
_ => false,
}
}
fn format_complex(
z: &Complex,
digits: u32,
_prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<String, SymplexError> {
let re = &z.0;
let im = &z.1;
let re_negligible = is_negligible_part(re, im, digits);
let im_negligible = is_negligible_part(im, re, digits);
if re_negligible && im_negligible {
return Ok("0".to_string());
}
if im_negligible {
return format_decimal(re, digits, rm, cc);
}
if re_negligible {
let im_str = format_decimal(im, digits, rm, cc)?;
return if im_str == "1" {
Ok("i".to_string())
} else if im_str == "-1" {
Ok("-i".to_string())
} else {
Ok(format!("{im_str}*i"))
};
}
let re_str = format_decimal(re, digits, rm, cc)?;
if im.is_negative() {
let im_abs_str = format_decimal(&im.abs(), digits, rm, cc)?;
if im_abs_str == "1" {
Ok(format!("{re_str} - i"))
} else {
Ok(format!("{re_str} - {im_abs_str}*i"))
}
} else {
let im_str = format_decimal(im, digits, rm, cc)?;
if im_str == "1" {
Ok(format!("{re_str} + i"))
} else {
Ok(format!("{re_str} + {im_str}*i"))
}
}
}
fn round_decimal_digits(mantissa: &[u8], digits: usize) -> (Vec<u8>, i64) {
if digits == 0 || mantissa.len() <= digits {
return (mantissa.to_vec(), 0);
}
let mut kept: Vec<u8> = mantissa[..digits].to_vec();
let next = mantissa[digits];
let rest_nonzero = mantissa[digits + 1..].iter().any(|&d| d != 0);
let round_up = match next.cmp(&5) {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Equal => rest_nonzero || kept[digits - 1] % 2 == 1,
};
if !round_up {
return (kept, 0);
}
for d in kept.iter_mut().rev() {
if *d == 9 {
*d = 0;
} else {
*d += 1;
return (kept, 0);
}
}
kept[0] = 1;
(kept, 1)
}
fn format_decimal(
val: &BigFloat,
digits: u32,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<String, SymplexError> {
if val.is_zero() {
return Ok("0".to_string());
}
if val.is_inf_pos() {
return Ok("oo".to_string());
}
if val.is_inf_neg() {
return Ok("-oo".to_string());
}
let (sign, mantissa, exponent) = val
.convert_to_radix(Radix::Dec, rm, cc)
.map_err(|e| SymplexError::NotImplemented(format!("decimal conversion failed: {e:?}")))?;
let sign_str = if sign == Sign::Neg { "-" } else { "" };
let (rounded, exp_carry) = round_decimal_digits(&mantissa, digits as usize);
let n = rounded.len();
if n == 0 {
return Ok("0".to_string());
}
let digit_chars: Vec<char> = rounded.iter().map(|&d| (b'0' + d) as char).collect();
let adj_exp = exponent as i64 - 1 + exp_carry;
if adj_exp >= 0 && (adj_exp as usize) < n {
let dot_pos = adj_exp as usize + 1;
let mut s = String::with_capacity(n + 2);
s.push_str(sign_str);
for (i, &ch) in digit_chars.iter().enumerate() {
if i == dot_pos && dot_pos < n {
s.push('.');
}
s.push(ch);
}
if s.contains('.') {
let trimmed = s.trim_end_matches('0').trim_end_matches('.');
Ok(trimmed.to_string())
} else {
Ok(s)
}
} else if adj_exp >= 0 && (adj_exp as usize) < n + 6 {
let mut s = String::with_capacity(adj_exp as usize + 2);
s.push_str(sign_str);
for &ch in &digit_chars {
s.push(ch);
}
let needed_zeros = (adj_exp as usize + 1).saturating_sub(digit_chars.len());
for _ in 0..needed_zeros {
s.push('0');
}
Ok(s)
} else if (-4..0).contains(&adj_exp) {
let mut s = String::with_capacity(n + (-adj_exp) as usize + 3);
s.push_str(sign_str);
s.push_str("0.");
let leading_zeros = (-adj_exp - 1) as usize;
for _ in 0..leading_zeros {
s.push('0');
}
for &ch in &digit_chars {
s.push(ch);
}
let trimmed = s.trim_end_matches('0');
Ok(trimmed.to_string())
} else {
let mut s = String::with_capacity(n + 10);
s.push_str(sign_str);
s.push(digit_chars[0]);
if n > 1 {
s.push('.');
for &ch in &digit_chars[1..] {
s.push(ch);
}
while s.ends_with('0') {
s.pop();
}
if s.ends_with('.') {
s.pop();
}
}
s.push_str(&format!("e{adj_exp}"));
Ok(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn assert_evalf_starts_with(arena: &Arena, expr: ExprId, digits: u32, prefix: &str) {
let result = evalf(arena, expr, digits).unwrap();
assert!(
result.starts_with(prefix),
"expected to start with '{prefix}', got: '{result}'"
);
}
fn assert_evalf_eq(arena: &Arena, expr: ExprId, digits: u32, expected: &str) {
let result = evalf(arena, expr, digits).unwrap();
assert_eq!(result, expected, "evalf mismatch");
}
#[test]
fn integer_zero() {
let a = Arena::new();
assert_evalf_eq(&a, a.zero, 10, "0");
}
#[test]
fn integer_one() {
let a = Arena::new();
assert_evalf_eq(&a, a.one, 10, "1");
}
#[test]
fn integer_negative_one() {
let a = Arena::new();
assert_evalf_eq(&a, a.neg_one, 10, "-1");
}
#[test]
fn integer_five() {
let mut a = Arena::new();
let five = a.int(5);
assert_evalf_eq(&a, five, 10, "5");
}
#[test]
fn integer_large() {
let mut a = Arena::new();
let n = a.int(123456);
assert_evalf_eq(&a, n, 10, "123456");
}
#[test]
fn rational_half() {
let mut a = Arena::new();
let half = a.rational(1, 2);
assert_evalf_eq(&a, half, 10, "0.5");
}
#[test]
fn rational_one_third() {
let mut a = Arena::new();
let third = a.rational(1, 3);
assert_evalf_starts_with(&a, third, 10, "0.333333333");
}
#[test]
fn rational_negative() {
let mut a = Arena::new();
let r = a.rational(-3, 4);
assert_evalf_starts_with(&a, r, 10, "-0.75");
}
#[test]
fn constant_pi_15_digits() {
let a = Arena::new();
assert_evalf_starts_with(&a, a.pi, 15, "3.14159265358979");
}
#[test]
fn constant_pi_50_digits() {
let a = Arena::new();
let result = evalf(&a, a.pi, 50).unwrap();
assert!(
result.starts_with("3.1415926535897932384626433832795"),
"pi to 50 digits: {result}"
);
}
#[test]
fn constant_e_15_digits() {
let a = Arena::new();
assert_evalf_starts_with(&a, a.e_const, 15, "2.71828182845905");
}
#[test]
fn add_integers() {
let mut a = Arena::new();
let two = a.int(2);
let three = a.int(3);
let expr = a.add(&[two, three]);
assert_evalf_eq(&a, expr, 10, "5");
}
#[test]
fn mul_integers() {
let mut a = Arena::new();
let two = a.int(2);
let three = a.int(3);
let expr = a.mul(&[two, three]);
assert_evalf_eq(&a, expr, 10, "6");
}
#[test]
fn pow_integer() {
let mut a = Arena::new();
let two = a.int(2);
let ten = a.int(10);
let expr = a.pow(two, ten);
assert_evalf_eq(&a, expr, 10, "1024");
}
#[test]
fn pow_negative_exponent() {
let mut a = Arena::new();
let two = a.int(2);
let neg_one = a.int(-1);
let expr = a.pow(two, neg_one);
assert_evalf_eq(&a, expr, 10, "0.5");
}
#[test]
fn pi_plus_one() {
let mut a = Arena::new();
let pi = a.pi;
let one = a.one;
let expr = a.add(&[pi, one]);
assert_evalf_starts_with(&a, expr, 15, "4.14159265358979");
}
#[test]
fn sin_zero() {
let mut a = Arena::new();
let zero = a.zero;
let expr = a.sin(zero);
assert_evalf_eq(&a, expr, 10, "0");
}
#[test]
fn cos_zero() {
let mut a = Arena::new();
let zero = a.zero;
let expr = a.cos(zero);
assert_evalf_eq(&a, expr, 10, "1");
}
#[test]
fn exp_zero() {
let mut a = Arena::new();
let zero = a.zero;
let expr = a.exp(zero);
assert_evalf_eq(&a, expr, 10, "1");
}
#[test]
fn exp_one() {
let mut a = Arena::new();
let one = a.one;
let expr = a.exp(one);
assert_evalf_starts_with(&a, expr, 15, "2.71828182845905");
}
#[test]
fn ln_one() {
let mut a = Arena::new();
let one = a.one;
let expr = a.ln(one);
assert_evalf_eq(&a, expr, 10, "0");
}
#[test]
fn ln_e() {
let mut a = Arena::new();
let e = a.e_const;
let expr = a.ln(e);
let result = evalf(&a, expr, 15).unwrap();
let val: f64 = result.parse().unwrap_or(999.0);
assert!(
(val - 1.0).abs() < 1e-10,
"ln(e) should be ~1, got: {result}"
);
}
#[test]
fn sqrt_four() {
let mut a = Arena::new();
let four = a.int(4);
let expr = a.sqrt(four);
assert_evalf_eq(&a, expr, 10, "2");
}
#[test]
fn sqrt_two_30_digits() {
let mut a = Arena::new();
let two = a.int(2);
let expr = a.sqrt(two);
assert_evalf_starts_with(&a, expr, 30, "1.4142135623730950488");
}
#[test]
fn sin_pi_is_near_zero() {
let mut a = Arena::new();
let pi = a.pi;
let expr = a.sin(pi);
let result = evalf(&a, expr, 20).unwrap();
let val: f64 = result.parse().unwrap_or(999.0);
assert!(val.abs() < 1e-15, "sin(pi) should be ~0, got: {result}");
}
#[test]
fn neg_pi() {
let mut a = Arena::new();
let pi = a.pi;
let expr = a.neg(pi);
assert_evalf_starts_with(&a, expr, 10, "-3.14159265");
}
#[test]
fn abs_neg_pi() {
let mut a = Arena::new();
let pi = a.pi;
let neg_pi = a.neg(pi);
let expr = a.abs(neg_pi);
assert_evalf_starts_with(&a, expr, 10, "3.14159265");
}
#[test]
fn abs_negative_integer() {
let mut a = Arena::new();
let neg_five = a.int(-5);
let expr = a.abs(neg_five);
assert_evalf_eq(&a, expr, 10, "5");
}
#[test]
fn free_symbol_errors() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = evalf(&a, x, 10);
assert!(result.is_err(), "evalf of free symbol should error");
}
#[test]
fn infinity_errors() {
let a = Arena::new();
let result = evalf(&a, a.infinity, 10);
assert!(result.is_err(), "evalf of infinity should error");
}
#[test]
fn nan_errors() {
let a = Arena::new();
let result = evalf(&a, a.nan, 10);
assert!(result.is_err(), "evalf of NaN should error");
}
#[test]
fn imaginary_unit_works() {
let a = Arena::new();
let result = evalf(&a, a.i_unit, 10);
assert!(result.is_ok(), "evalf of i should work now");
let s = result.unwrap();
assert!(
s.contains("i") || s.contains("I"),
"should display as imaginary: {s}"
);
}
#[test]
fn evalf_one_plus_i() {
let mut a = Arena::new();
let one = a.one;
let i = a.i_unit;
let expr = a.add(&[one, i]);
let result = evalf(&a, expr, 10);
assert!(result.is_ok(), "evalf(1+i) should work: {:?}", result.err());
let s = result.unwrap();
assert!(
s.contains("i") || s.contains("I"),
"1+i should contain 'i': {s}"
);
}
#[test]
fn evalf_exp_i_pi() {
let mut a = Arena::new();
let i = a.i_unit;
let pi = a.pi;
let i_times_pi = a.mul(&[i, pi]);
let expr = a.exp(i_times_pi);
let result = evalf(&a, expr, 15);
assert!(
result.is_ok(),
"evalf(exp(i*pi)) should work: {:?}",
result.err()
);
let s = result.unwrap();
let val: f64 = s.parse().unwrap_or(999.0);
assert!(
(val - (-1.0)).abs() < 1e-10,
"exp(i*pi) should be ~-1, got: {s}"
);
}
#[test]
fn evalf_i_squared() {
let mut a = Arena::new();
let i = a.i_unit;
let two = a.int(2);
let expr = a.pow(i, two);
let result = evalf(&a, expr, 10);
assert!(result.is_ok(), "evalf(i^2) should work: {:?}", result.err());
let s = result.unwrap();
let val: f64 = s.parse().unwrap_or(999.0);
assert!((val - (-1.0)).abs() < 1e-10, "i^2 should be -1, got: {s}");
}
#[test]
fn evalf_sqrt_neg_one() {
let mut a = Arena::new();
let neg_one = a.neg_one;
let expr = a.sqrt(neg_one);
let result = evalf(&a, expr, 10);
assert!(
result.is_ok(),
"evalf(sqrt(-1)) should work: {:?}",
result.err()
);
let s = result.unwrap();
assert!(
s.contains("i") || s.contains("I"),
"sqrt(-1) should be imaginary: {s}"
);
}
#[test]
fn evalf_abs_3_plus_4i() {
let mut a = Arena::new();
let three = a.int(3);
let four = a.int(4);
let i = a.i_unit;
let four_i = a.mul(&[four, i]);
let sum = a.add(&[three, four_i]);
let expr = a.abs(sum);
let result = evalf(&a, expr, 10);
assert!(
result.is_ok(),
"evalf(|3+4i|) should work: {:?}",
result.err()
);
let s = result.unwrap();
let val: f64 = s.parse().unwrap_or(999.0);
assert!((val - 5.0).abs() < 1e-10, "|3+4i| should be 5, got: {s}");
}
#[test]
fn polynomial_at_value() {
let mut a = Arena::new();
let x = a.symbol("x");
let two = a.int(2);
let x_sq = a.pow(x, two);
let two_x = a.mul(&[two, x]);
let expr = a.add(&[x_sq, two_x, a.one]);
let three = a.int(3);
let evaluated = crate::transforms::subs::subs(&mut a, expr, x, three);
assert_evalf_eq(&a, evaluated, 10, "16");
}
#[test]
fn sin_cos_identity() {
let mut a = Arena::new();
let one = a.one;
let two = a.int(2);
let sin_1 = a.sin(one);
let cos_1 = a.cos(one);
let sin_sq = a.pow(sin_1, two);
let cos_sq = a.pow(cos_1, two);
let expr = a.add(&[sin_sq, cos_sq]);
let result = evalf(&a, expr, 15).unwrap();
let val: f64 = result.parse().unwrap_or(0.0);
assert!(
(val - 1.0).abs() < 1e-12,
"sin^2(1) + cos^2(1) should be ~1, got: {result}"
);
}
#[test]
fn same_precision_same_result() {
let a = Arena::new();
let r1 = evalf(&a, a.pi, 20).unwrap();
let r2 = evalf(&a, a.pi, 20).unwrap();
assert_eq!(r1, r2, "same precision should give same result");
}
#[test]
fn jacobi_term_by_term_matches_the_explicit_sum() {
let rm = RoundingMode::ToEven;
let mut cc = Consts::new().unwrap();
let prec = 128;
let reference = |deg: usize, a: &BigFloat, b: &BigFloat, x: &BigFloat| -> BigFloat {
let wp = prec + 32 + 2 * deg;
let one = BigFloat::from_i32(1, wp);
let two = BigFloat::from_i32(2, wp);
let lo = x.sub(&one, wp, rm).div(&two, wp, rm);
let hi = x.add(&one, wp, rm).div(&two, wp, rm);
let shifted = |p: &BigFloat, lo_j: usize, hi_j: usize| -> BigFloat {
let mut acc = one.clone();
for j in lo_j..=hi_j {
acc = acc.mul(&p.add(&BigFloat::from_i128(j as i128, wp), wp, rm), wp, rm);
}
acc
};
let mut sum = BigFloat::new(wp);
for s in 0..=deg {
let pa = shifted(a, s + 1, deg);
let pb = shifted(b, deg - s + 1, deg);
let d =
ratio_to_bigfloat(&(factorial_bigint(deg - s) * factorial_bigint(s)), wp, rm);
let term = pa
.mul(&pb, wp, rm)
.div(&d, wp, rm)
.mul(&lo.powi(s, wp, rm), wp, rm)
.mul(&hi.powi(deg - s, wp, rm), wp, rm);
sum = sum.add(&term, wp, rm);
}
round_to(sum, prec, rm)
};
let q =
|n: i32, d: i32| BigFloat::from_i32(n, 256).div(&BigFloat::from_i32(d, 256), 256, rm);
for deg in [1usize, 2, 5, 17, 60] {
for (a, b) in [
(q(1, 3), q(1, 5)),
(q(-3, 1), q(2, 7)),
(q(-1, 1), q(-2, 1)),
(q(5, 2), q(-7, 3)),
] {
for x in [q(1, 3), q(-9, 10), q(1, 1), q(-1, 1), q(3, 1), q(-5, 2)] {
let n = BigFloat::from_u64(deg as u64, 64);
let got = arb_jacobi(&n, &a, &b, &x, prec, rm, &mut cc).unwrap();
let want = reference(deg, &a, &b, &x);
let diff = got.sub(&want, prec, rm).abs();
let ok = diff.is_zero()
|| match (diff.exponent(), want.exponent()) {
(Some(d), Some(w)) => i64::from(d) < i64::from(w) - (prec as i64 - 4),
_ => false,
};
assert!(ok, "deg={deg} a={a} b={b} x={x}: {got} vs {want}");
}
}
}
}
#[test]
fn exact_coefficients_match_the_ratio_recurrences() {
let rm = RoundingMode::ToEven;
for (n, wp) in (1..60usize).flat_map(|n| [(n, 64), (n, 160)]) {
let mut f = Ratio::<BigInt>::from_integer(BigInt::from(1));
for i in 2..n {
f *= Ratio::from_integer(BigInt::from(i as u64));
}
let mut g = NeumannCoefficients::new(n);
for k in 0..n {
if k > 0 {
f /= Ratio::from_integer(BigInt::from(((n - k) * k) as u64));
}
assert_eq!(g.next(wp, rm), ratio_to_bigfloat(&f, wp, rm), "n={n} k={k}");
}
let mut fact = Ratio::<BigInt>::from_integer(BigInt::from(1));
for i in 2..=n {
fact *= Ratio::from_integer(BigInt::from(i as u64));
}
assert_eq!(factorial_exact(n), fact);
}
for k in 1..40 {
let b = bernoulli::even(k);
for r in [1u64, 2, 6, 30, 42, 1155, 1_000_003, 510_510] {
let r = BigInt::from(r) * BigInt::from(k as u64 * 7 + 1);
assert_eq!(times_integer(&b, &r), &b * Ratio::from_integer(r.clone()));
}
}
}
#[test]
fn bessel_j0_at_zero() {
let mut a = Arena::new();
let zero = a.zero;
let j0_0 = a.besselj(zero, zero);
let result = evalf(&a, j0_0, 15).unwrap();
let val: f64 = result.parse().unwrap_or(f64::NAN);
assert!(
(val - 1.0).abs() < 1e-10,
"J_0(0) should be 1.0, got {result}"
);
}
#[test]
fn bessel_j0_at_one() {
let mut a = Arena::new();
let zero = a.zero;
let one = a.one;
let j0_1 = a.besselj(zero, one);
let result = evalf(&a, j0_1, 15).unwrap();
let val: f64 = result.parse().unwrap_or(f64::NAN);
assert!(
(val - 0.7651976865579666).abs() < 1e-8,
"J_0(1) should be ~0.7652, got {result}"
);
}
#[test]
fn bessel_j1_at_zero() {
let mut a = Arena::new();
let one = a.one;
let zero = a.zero;
let j1_0 = a.besselj(one, zero);
let result = evalf(&a, j1_0, 15).unwrap();
let val: f64 = result.parse().unwrap_or(f64::NAN);
assert!(val.abs() < 1e-10, "J_1(0) should be 0, got {result}");
}
#[test]
fn bessel_j1_at_one() {
let mut a = Arena::new();
let one = a.one;
let j1_1 = a.besselj(one, one);
let result = evalf(&a, j1_1, 15).unwrap();
let val: f64 = result.parse().unwrap_or(f64::NAN);
assert!(
(val - 0.44005058574493355).abs() < 1e-8,
"J_1(1) should be ~0.4401, got {result}"
);
}
#[test]
fn bessel_y0_at_one() {
let mut a = Arena::new();
let zero = a.zero;
let one = a.one;
let y0_1 = a.bessely(zero, one);
let result = evalf(&a, y0_1, 15).unwrap();
let val: f64 = result.parse().unwrap_or(f64::NAN);
assert!(
(val - 0.08825696421567696).abs() < 1e-6,
"Y_0(1) should be ~0.0883, got {result}"
);
}
#[test]
fn euler_gamma_and_catalan_high_precision() {
let a = Arena::new();
assert_evalf_starts_with(
&a,
a.euler_gamma,
60,
"0.57721566490153286060651209008240243104215933593992359880576",
);
assert_evalf_starts_with(
&a,
a.catalan,
60,
"0.91596559417721901505460351493238411077414937428167213426649",
);
assert_evalf_starts_with(
&a,
a.golden_ratio,
40,
"1.61803398874989484820458683436563811772",
);
}
#[test]
fn bigint_to_bigfloat_is_exact_for_huge_integers() {
let n = (BigInt::from(1) << 200) + BigInt::from(1);
let bf = crate::base::numeric::bigint_to_bigfloat(&n, 256);
let two200 = BigFloat::from_i32(2, 256).powi(200, 256, RoundingMode::ToEven);
let diff = bf.sub(&two200, 256, RoundingMode::ToEven);
assert_eq!(diff, BigFloat::from_i32(1, 256));
}
#[test]
fn bf_pow_handles_exactly_representable_results() {
let mut cc = Consts::new().unwrap();
let rm = RoundingMode::ToEven;
let four = BigFloat::from_i32(4, 160);
let half = BigFloat::from_f64(0.5, 160);
let r = bf_pow(&four, &half, 160, rm, &mut cc);
let diff = r.sub(&BigFloat::from_i32(2, 160), 160, rm);
assert!(
diff.is_zero() || diff.exponent().is_none_or(|e| e < -140),
"4^0.5 = {r}"
);
let three = BigFloat::from_i32(3, 160);
let r = bf_pow(&three, &BigFloat::from_i32(5, 160), 160, rm, &mut cc);
assert_eq!(r, BigFloat::from_i32(243, 160));
let r = bf_pow(&three, &BigFloat::from_i32(-1, 160), 160, rm, &mut cc);
let back = r.mul(&three, 160, rm);
let d = back.sub(&BigFloat::from_i32(1, 160), 160, rm);
assert!(d.is_zero() || d.exponent().is_none_or(|e| e < -140));
}
#[test]
fn si_ci_ei_li_at_one_and_two() {
let mut a = Arena::new();
let one = a.one;
let two = a.int(2);
let si = a.si(one);
assert_evalf_starts_with(&a, si, 20, "0.94608307036718301494");
let ci = a.ci(one);
assert_evalf_starts_with(&a, ci, 20, "0.33740392290096813466");
let ei = a.ei(one);
assert_evalf_starts_with(&a, ei, 20, "1.8951178163559367555");
let li = a.li(two);
assert_evalf_starts_with(&a, li, 20, "1.0451637801174927848");
}
#[test]
fn si_asymptotic_and_series_regimes_agree() {
let mut a = Arena::new();
let x = a.int(200);
let si = a.si(x);
let lo = evalf(&a, si, 12).unwrap(); let hi = evalf(&a, si, 120).unwrap(); assert!(hi.starts_with(&lo[..12]), "{lo} vs {hi}");
}
#[test]
fn zeta_borwein_and_functional_equation() {
let mut a = Arena::new();
let three = a.int(3);
let z3 = a.zeta(three);
assert_evalf_starts_with(&a, z3, 30, "1.20205690315959428539973816151");
let half = a.rational(1, 2);
let zh = a.zeta(half);
assert_evalf_starts_with(&a, zh, 20, "-1.4603545088095868129");
let neg = a.rational(-5, 2);
let zn = a.zeta(neg);
assert_evalf_starts_with(&a, zn, 15, "0.00851692877785");
let neg_half = a.rational(-1, 2);
let znh = a.zeta(neg_half);
assert_evalf_starts_with(&a, znh, 15, "-0.20788622497735");
}
#[test]
fn polygamma_matches_trigamma_closed_forms() {
let mut a = Arena::new();
let one = a.one;
let x = a.rational(7, 3);
let pg = a.intern(ExprNode::Polygamma(one, x));
assert_evalf_starts_with(&a, pg, 20, "0.53309712542709408179");
let half = a.rational(1, 2);
let pg_half = a.intern(ExprNode::Polygamma(one, half));
assert_evalf_starts_with(&a, pg_half, 20, "4.9348022005446793094");
}
#[test]
fn bessel_i_k_reference() {
let mut a = Arena::new();
let zero = a.zero;
let one = a.one;
let i0 = a.besseli(zero, one);
assert_evalf_starts_with(&a, i0, 20, "1.2660658777520083356");
let k0 = a.besselk(zero, one);
assert_evalf_starts_with(&a, k0, 20, "0.42102443824070833334");
let k1 = a.besselk(one, one);
assert_evalf_starts_with(&a, k1, 20, "0.60190723019723457474");
}
#[test]
fn orthogonal_polynomials_large_degree() {
let mut a = Arena::new();
let n = a.int(100);
let third = a.rational(1, 3);
let t = a.chebyshev_t(n, third);
let s = evalf(&a, t, 15).unwrap();
let v: f64 = s.parse().unwrap();
let expected = (100.0 * (1.0f64 / 3.0).acos()).cos();
assert!((v - expected).abs() < 1e-12, "{v} vs {expected}");
let p = a.legendre(n, a.one);
assert_evalf_starts_with(&a, p, 10, "1");
}
#[test]
fn complex_nodes_evaluate_numerically() {
let mut a = Arena::new();
let z = a.symbol("z");
let re_z = a.intern(ExprNode::Re(z));
let arg_z = a.intern(ExprNode::Arg(z));
let conj_z = a.intern(ExprNode::Conjugate(z));
let three = a.int(3);
let four = a.int(4);
let four_i = a.mul(&[four, a.i_unit]);
let w = a.add(&[three, four_i]);
let _ = re_z;
let re_w = a.intern(ExprNode::Re(w));
let arg_w = a.intern(ExprNode::Arg(w));
let conj_w = a.intern(ExprNode::Conjugate(w));
let _ = (arg_z, conj_z);
assert_eq!(evalf(&a, re_w, 10).unwrap(), "3");
assert_evalf_starts_with(&a, arg_w, 15, "0.92729521800161");
let s = evalf(&a, conj_w, 10).unwrap();
assert!(s.contains('3') && s.contains('4') && s.contains('-'), "{s}");
}
#[test]
fn rootsum_over_repeated_roots_counts_multiplicity() {
let mut a = Arena::new();
let x = a.symbol("x");
let two = a.int(2);
let three = a.int(3);
let minus_two = a.int(-2);
let minus_one = a.int(-1);
let x2 = a.pow(x, two);
let q = a.add(&[x2, minus_two]);
let poly = a.pow(q, two);
let xp3 = a.add(&[x, three]);
let body = a.pow(xp3, minus_one);
let rs = a.intern(ExprNode::RootSum(poly, body, x));
assert_evalf_eq(&a, rs, 20, "1.7142857142857142857");
let one = a.int(1);
let x2p1 = a.add(&[x2, one]);
let sq = a.pow(x2p1, two);
let minus_three = a.int(-3);
let xm3 = a.add(&[x, minus_three]);
let poly = a.mul(&[sq, xm3]);
let body = a.intern(ExprNode::Exp(x));
let rs = a.intern(ExprNode::RootSum(poly, body, x));
assert_evalf_eq(&a, rs, 20, "22.246746146660226611");
}
fn apply(a: &mut Arena, f: LibFn, args: &[ExprId]) -> ExprId {
a.lib_apply(f, args)
}
#[test]
fn erfc_node_large_argument_is_relative_precision() {
let mut a = Arena::new();
let seven = a.int(7);
let e7 = a.erfc(seven);
assert_evalf_starts_with(&a, e7, 16, "4.183825607779414e-23");
let ten = a.int(10);
let e10 = a.erfc(ten);
assert_evalf_starts_with(
&a,
e10,
50,
"2.0884875837625447570007862949577886115608181193212e-45",
);
let thirty = a.int(30);
let e30 = a.erfc(thirty);
assert_evalf_starts_with(&a, e30, 16, "2.564656203756112e-393");
let m3 = a.int(-3);
let em3 = a.erfc(m3);
assert_evalf_starts_with(&a, em3, 16, "1.999977909503001");
}
#[test]
fn erf_taylor_branch_carries_cancellation_guard_bits() {
let mut a = Arena::new();
let x = a.rational(74, 10);
let e = a.erf(x);
assert_evalf_starts_with(&a, e, 30, "0.999999999999999999999999875161");
let ten = a.int(10);
let e10 = a.erf(ten);
assert_evalf_starts_with(
&a,
e10,
50,
"0.99999999999999999999999999999999999999999999791151",
);
let three = a.int(3);
let e3 = a.erf(three);
assert_evalf_starts_with(&a, e3, 40, "0.999977909503001414558627223870417679620");
}
#[test]
fn polylog_at_minus_one_is_minus_eta_for_all_s() {
let mut a = Arena::new();
let neg_one = a.neg_one;
let half = a.rational(1, 2);
let li = apply(&mut a, LibFn::PolyLog, &[half, neg_one]);
assert_evalf_starts_with(&a, li, 30, "-0.60489864342163037024726591423");
let one = a.one;
let li1 = apply(&mut a, LibFn::PolyLog, &[one, neg_one]);
assert_evalf_starts_with(&a, li1, 30, "-0.69314718055994530941723212145");
let neg_half = a.rational(-1, 2);
let lim = apply(&mut a, LibFn::PolyLog, &[neg_half, neg_one]);
assert_evalf_starts_with(&a, lim, 30, "-0.38010481260968401677754215655");
let three_halves = a.rational(3, 2);
let li32 = apply(&mut a, LibFn::PolyLog, &[three_halves, neg_one]);
assert_evalf_starts_with(&a, li32, 30, "-0.76514702462540794536726875860");
let one_id = a.one;
let div = apply(&mut a, LibFn::PolyLog, &[half, one_id]);
assert!(evalf(&a, div, 16).is_err());
}
#[test]
fn dirichlet_eta_near_one_uses_alternating_sum() {
let mut a = Arena::new();
let ten = a.int(10);
let e20 = a.int(-20);
let tiny = a.pow(ten, e20);
let s = a.add(&[a.one, tiny]);
let eta = apply(&mut a, LibFn::DirichletEta, &[s]);
assert_evalf_starts_with(&a, eta, 30, "0.69314718055994530941883081049");
let s2 = a.sub(a.one, tiny);
let eta2 = apply(&mut a, LibFn::DirichletEta, &[s2]);
assert_evalf_starts_with(&a, eta2, 30, "0.69314718055994530941563343242");
let one = a.one;
let eta1 = apply(&mut a, LibFn::DirichletEta, &[one]);
assert_evalf_starts_with(&a, eta1, 30, "0.69314718055994530941723212145");
let neg_half = a.rational(-1, 2);
let etam = apply(&mut a, LibFn::DirichletEta, &[neg_half]);
assert_evalf_starts_with(&a, etam, 30, "0.38010481260968401677754215655");
}
#[test]
fn airy_derivatives_at_exact_zero() {
let mut a = Arena::new();
let zero = a.zero;
let aip = apply(&mut a, LibFn::AiryAiPrime, &[zero]);
assert_evalf_starts_with(&a, aip, 30, "-0.25881940379280679840518356018");
let bip = apply(&mut a, LibFn::AiryBiPrime, &[zero]);
assert_evalf_starts_with(&a, bip, 30, "0.44828835735382635791482371039");
}
#[test]
fn polylog_series_bound_accounts_for_negative_order() {
let n = polylog_series_terms(2.0, 0.5, 100);
assert!((120..=122).contains(&n), "{n}");
let n = polylog_series_terms(-40.5, 0.5, 132);
let a = -(0.5f64).ln();
let k = (n - 20) as f64;
assert!(
k * a - 40.5 * k.ln() >= 132.0 * std::f64::consts::LN_2,
"{n}"
);
assert!(n > 200, "{n}");
}
}