use crate::ball::ArbBall;
use crate::kernel::{ExprId, ExprPool};
use crate::primitive::Primitive;
use crate::special::EULER_GAMMA;
use crate::validated::taylor::TaylorModel;
use crate::validated::{is_finite, mag, symmetric, ValidatedError};
use rug::float::{Constant, Round};
use rug::{Float, Integer, Rational};
type VResult<T> = std::result::Result<T, ValidatedError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SeriesKind {
All,
OddAlt,
EvenAlt,
OddPlus,
EvenPlus,
}
impl SeriesKind {
fn sigma(self, m: usize) -> Option<i32> {
match self {
SeriesKind::All => Some(1),
SeriesKind::OddAlt => (m % 2 == 1).then(|| if ((m - 1) / 2) % 2 == 0 { 1 } else { -1 }),
SeriesKind::EvenAlt => (m % 2 == 0).then_some(if (m / 2) % 2 == 0 { 1 } else { -1 }),
SeriesKind::OddPlus => (m % 2 == 1).then_some(1),
SeriesKind::EvenPlus => (m % 2 == 0).then_some(1),
}
}
fn has_log_prefix(self) -> bool {
matches!(
self,
SeriesKind::All | SeriesKind::EvenAlt | SeriesKind::EvenPlus
)
}
fn guard_bits(self, ax: f64, x_negative: bool) -> u32 {
let ratio = match self {
SeriesKind::All if x_negative => 2.0 * ax,
SeriesKind::All | SeriesKind::OddPlus | SeriesKind::EvenPlus => 0.0,
SeriesKind::OddAlt | SeriesKind::EvenAlt => ax,
};
(ratio * std::f64::consts::LOG2_E).ceil().max(0.0) as u32
}
}
const EI_SERIES_MAX: f64 = 45.0;
const E1_CF_MIN: f64 = 1.0;
const SICI_SERIES_MAX: f64 = 6.0;
const SHICHI_SERIES_MAX: f64 = 6.0;
const F64_SERIES_EPS: f64 = 1e-18;
const F64_CF_EPS: f64 = 2.3e-16;
pub fn e1(z: f64) -> f64 {
if z.is_nan() {
return f64::NAN;
}
if z <= 0.0 {
return f64::INFINITY;
}
if z < E1_CF_MIN {
let mut term = 1.0_f64;
let mut sum = 0.0_f64;
for m in 1..=200 {
term *= z / m as f64;
let c = term / m as f64;
sum += if m % 2 == 1 { c } else { -c };
if c < F64_SERIES_EPS * (sum.abs() + 1.0) {
break;
}
}
-EULER_GAMMA - z.ln() + sum
} else {
const FPMIN: f64 = 1e-300;
let mut b = z + 1.0;
let mut c = 1.0 / FPMIN;
let mut d = 1.0 / b;
let mut h = d;
for i in 1..=1000 {
let a = -(i as f64) * (i as f64);
b += 2.0;
d = 1.0 / (a * d + b);
c = b + a / c;
let del = c * d;
h *= del;
if (del - 1.0).abs() <= F64_CF_EPS {
break;
}
}
h * (-z).exp()
}
}
fn ei_positive(x: f64) -> f64 {
if x <= EI_SERIES_MAX {
let mut term = 1.0_f64;
let mut sum = 0.0_f64;
for m in 1..=500 {
term *= x / m as f64;
let c = term / m as f64;
sum += c;
if c < F64_SERIES_EPS * sum {
break;
}
}
EULER_GAMMA + x.ln() + sum
} else {
let mut sum = 1.0_f64;
let mut term = 1.0_f64;
let mut prev = f64::INFINITY;
for k in 1..=200 {
term *= k as f64 / x;
if term > prev {
break;
}
prev = term;
sum += term;
if term < F64_SERIES_EPS * sum {
break;
}
}
x.exp() / x * sum
}
}
pub fn ei(x: f64) -> Option<f64> {
if x.is_nan() {
return None;
}
if x == 0.0 {
return Some(f64::NEG_INFINITY);
}
Some(if x < 0.0 {
-e1(-x)
} else {
ei_positive(x)
})
}
pub fn li(x: f64) -> Option<f64> {
if x.is_nan() || x < 0.0 {
return None;
}
if x == 0.0 {
return Some(0.0);
}
if x == 1.0 {
return Some(f64::NEG_INFINITY);
}
ei(x.ln())
}
fn si_ci_series(x: f64) -> (f64, f64) {
let mut term = 1.0_f64;
let mut si_sum = 0.0_f64;
let mut ci_sum = 0.0_f64;
for m in 1..=400 {
term *= x / m as f64;
let c = term / m as f64;
if m % 2 == 1 {
si_sum += if ((m - 1) / 2) % 2 == 0 { c } else { -c };
} else {
ci_sum += if (m / 2) % 2 == 0 { c } else { -c };
}
if (m as f64) > 2.0 * x + 2.0 && c < F64_SERIES_EPS * (si_sum.abs() + ci_sum.abs() + 1.0) {
break;
}
}
(si_sum, EULER_GAMMA + x.ln() + ci_sum)
}
fn si_ci_cf(x: f64) -> (f64, f64) {
const FPMIN: f64 = 1e-300;
#[inline]
fn cdiv(ar: f64, ai: f64, br: f64, bi: f64) -> (f64, f64) {
let den = br * br + bi * bi;
((ar * br + ai * bi) / den, (ai * br - ar * bi) / den)
}
let (mut br, bi) = (1.0_f64, x);
let (mut cr, mut cim) = (1.0 / FPMIN, 0.0_f64);
let (mut dr, mut dim) = cdiv(1.0, 0.0, br, bi);
let (mut hr, mut him) = (dr, dim);
for i in 2..=2000 {
let a = -(((i - 1) * (i - 1)) as f64);
br += 2.0;
let (nd_r, nd_i) = cdiv(1.0, 0.0, a * dr + br, a * dim + bi);
dr = nd_r;
dim = nd_i;
let (qr, qi) = cdiv(a, 0.0, cr, cim);
cr = br + qr;
cim = bi + qi;
let (delr, deli) = (cr * dr - cim * dim, cr * dim + cim * dr);
let (nh_r, nh_i) = (hr * delr - him * deli, hr * deli + him * delr);
hr = nh_r;
him = nh_i;
if (delr - 1.0).abs() + deli.abs() < F64_CF_EPS {
break;
}
}
let (cx, sx) = (x.cos(), x.sin());
let (re, im) = (hr * cx + him * sx, him * cx - hr * sx);
(std::f64::consts::FRAC_PI_2 + im, -re)
}
pub fn si(x: f64) -> f64 {
if x == 0.0 {
return 0.0;
}
if x.is_nan() {
return f64::NAN;
}
if x.is_infinite() {
return std::f64::consts::FRAC_PI_2 * x.signum();
}
let a = x.abs();
let s = if a <= SICI_SERIES_MAX {
si_ci_series(a).0
} else {
si_ci_cf(a).0
};
if x < 0.0 {
-s
} else {
s
}
}
pub fn ci(x: f64) -> Option<f64> {
if x.is_nan() || x < 0.0 {
return None;
}
if x == 0.0 {
return Some(f64::NEG_INFINITY);
}
if x.is_infinite() {
return Some(0.0);
}
Some(if x <= SICI_SERIES_MAX {
si_ci_series(x).1
} else {
si_ci_cf(x).1
})
}
fn shi_chi_series(x: f64) -> (f64, f64) {
let mut term = 1.0_f64;
let mut shi_sum = 0.0_f64;
let mut chi_sum = 0.0_f64;
for m in 1..=400 {
term *= x / m as f64;
let c = term / m as f64;
if m % 2 == 1 {
shi_sum += c;
} else {
chi_sum += c;
}
if (m as f64) > x + 2.0 && c < F64_SERIES_EPS * (shi_sum + chi_sum + 1.0) {
break;
}
}
(shi_sum, EULER_GAMMA + x.ln() + chi_sum)
}
pub fn shi(x: f64) -> f64 {
if x == 0.0 {
return 0.0;
}
if x.is_nan() {
return f64::NAN;
}
let a = x.abs();
let s = if a <= SHICHI_SERIES_MAX {
shi_chi_series(a).0
} else {
0.5 * (ei_positive(a) + e1(a))
};
if x < 0.0 {
-s
} else {
s
}
}
pub fn chi(x: f64) -> Option<f64> {
if x.is_nan() || x < 0.0 {
return None;
}
if x == 0.0 {
return Some(f64::NEG_INFINITY);
}
Some(if x <= SHICHI_SERIES_MAX {
shi_chi_series(x).1
} else {
0.5 * (ei_positive(x) - e1(x))
})
}
const MAX_SERIES_TERMS: usize = 4096;
const MAX_GUARD_BITS: u32 = 2048;
fn euler_gamma_ball(prec: u32) -> ArbBall {
let work = prec + 32;
let g = Float::with_val(work, Constant::Euler);
ball_around(&g, work, prec)
}
fn ball_around(v: &Float, work: u32, prec: u32) -> ArbBall {
let mid = Float::with_val(prec, v);
let trunc = Float::with_val(work, Float::with_val(work, v - &mid).abs());
let slack = Float::with_val(work, Float::with_val(work, v).abs()) >> work.saturating_sub(2);
let rad = Float::with_val_round(prec, Float::with_val(work, trunc + slack), Round::Up).0;
ArbBall { mid, rad, prec }
}
fn ball_from_interval(lo: &Float, hi: &Float, prec: u32) -> ArbBall {
let work = prec + 32;
let mid = Float::with_val(prec, Float::with_val(work, lo + hi) / 2u32);
let r1 = Float::with_val(work, Float::with_val(work, hi - &mid).abs());
let r2 = Float::with_val(work, Float::with_val(work, &mid - lo).abs());
let widest = if r1 > r2 { r1 } else { r2 };
let rad = Float::with_val_round(prec, &widest, Round::Up).0;
ArbBall { mid, rad, prec }
}
fn eval_float(kind: SeriesKind, x: &Float, prec: u32) -> Option<(Float, Float)> {
if !x.is_finite() {
return None;
}
if x.is_zero() {
return (!kind.has_log_prefix())
.then(|| (Float::with_val(prec, 0), Float::with_val(prec, 0)));
}
let negative = *x < 0;
if negative
&& !matches!(
kind,
SeriesKind::All | SeriesKind::OddAlt | SeriesKind::OddPlus
)
{
return None;
}
let ax = Float::with_val(53, x.clone().abs()).to_f64();
let guard = kind.guard_bits(ax, negative);
if guard > MAX_GUARD_BITS || !ax.is_finite() {
return None;
}
let work = prec + 96 + guard;
let xw = Float::with_val(work, x);
let axw = Float::with_val(work, xw.clone().abs());
let mut term = Float::with_val(work, 1u32); let mut aterm = Float::with_val(work, 1u32); let mut sum = Float::with_val(work, 0u32);
let mut abs_sum = Float::with_val(work, 0u32);
let mut n_terms = 0usize;
let mut converged = false;
for m in 1..=MAX_SERIES_TERMS {
term *= &xw;
term /= m as u32;
aterm *= &axw;
aterm /= m as u32;
let ac = Float::with_val(work, &aterm / m as u32);
if let Some(s) = kind.sigma(m) {
let c = Float::with_val(work, &term / m as u32);
if s > 0 {
sum += &c;
} else {
sum -= &c;
}
abs_sum += ∾
}
n_terms = m;
if (m as f64) > 2.0 * ax + 4.0 {
let thresh = abs_sum.clone() >> work;
if ac <= thresh {
converged = true;
break;
}
}
}
if !converged {
return None;
}
let mut val = sum.clone();
let mut err_scale = abs_sum;
if kind.has_log_prefix() {
let g = Float::with_val(work, Constant::Euler);
let l = Float::with_val(work, axw.ln());
err_scale += Float::with_val(work, l.clone().abs());
err_scale += 1u32;
val += g;
val += l;
}
err_scale += Float::with_val(work, val.clone().abs());
if !val.is_finite() || !err_scale.is_finite() {
return None;
}
let mut err = Float::with_val(work, &err_scale * ((4 * n_terms + 16) as u32));
err >>= work;
let val_p = Float::with_val(prec, &val);
let trunc = Float::with_val(work, Float::with_val(work, &val - &val_p).abs());
let err_p = Float::with_val_round(prec, Float::with_val(work, err + trunc), Round::Up).0;
Some((val_p, err_p))
}
fn li_float(x: &Float, prec: u32) -> Option<(Float, Float)> {
if !x.is_finite() || *x < 0 {
return None;
}
if x.is_zero() {
return Some((Float::with_val(prec, 0), Float::with_val(prec, 0)));
}
let work = prec + 96;
let l = Float::with_val(work, x.clone().ln());
let (v, e) = eval_float(SeriesKind::All, &l, prec)?;
let extra = Float::with_val(work, x.clone().abs()) >> work;
let err = Float::with_val_round(prec, Float::with_val(work, e + extra), Round::Up).0;
Some((v, err))
}
pub fn ball_ei(b: &ArbBall) -> Option<ArbBall> {
let prec = b.prec;
let (lo, hi) = (b.lo(), b.hi());
if !(lo.is_finite() && hi.is_finite()) || (lo <= 0 && hi >= 0) {
return None;
}
let (a, ea) = eval_float(SeriesKind::All, &lo, prec)?;
let (c, ec) = eval_float(SeriesKind::All, &hi, prec)?;
let work = prec + 32;
let (low, high) = if lo > 0 {
(
Float::with_val(work, &a - &ea),
Float::with_val(work, &c + &ec),
)
} else {
(
Float::with_val(work, &c - &ec),
Float::with_val(work, &a + &ea),
)
};
Some(ball_from_interval(&low, &high, prec))
}
pub fn ball_li(b: &ArbBall) -> Option<ArbBall> {
let prec = b.prec;
let (lo, hi) = (b.lo(), b.hi());
if !(lo.is_finite() && hi.is_finite()) || lo < 0 || (lo <= 1 && hi >= 1) {
return None;
}
let (a, ea) = li_float(&lo, prec)?;
let (c, ec) = li_float(&hi, prec)?;
let work = prec + 32;
let (low, high) = if lo > 1 {
(
Float::with_val(work, &a - &ea),
Float::with_val(work, &c + &ec),
)
} else {
(
Float::with_val(work, &c - &ec),
Float::with_val(work, &a + &ea),
)
};
Some(ball_from_interval(&low, &high, prec))
}
pub fn ball_si(b: &ArbBall) -> Option<ArbBall> {
let prec = b.prec;
let (v, e) = eval_float(SeriesKind::OddAlt, &b.mid, prec)?;
let work = prec + 32;
let rad = Float::with_val_round(
prec,
Float::with_val(work, Float::with_val(work, &e + &b.rad)),
Round::Up,
)
.0;
Some(ArbBall { mid: v, rad, prec })
}
pub fn ball_ci(b: &ArbBall) -> Option<ArbBall> {
let prec = b.prec;
let lo = b.lo();
if !lo.is_finite() || lo <= 0 || !b.hi().is_finite() {
return None;
}
let (v, e) = eval_float(SeriesKind::EvenAlt, &b.mid, prec)?;
let work = prec + 32;
let lip = Float::with_val(work, Float::with_val(work, 1u32) / &lo);
let rad = Float::with_val_round(
prec,
Float::with_val(work, &e + Float::with_val(work, &lip * &b.rad)),
Round::Up,
)
.0;
Some(ArbBall { mid: v, rad, prec })
}
pub fn ball_shi(b: &ArbBall) -> Option<ArbBall> {
let prec = b.prec;
let (lo, hi) = (b.lo(), b.hi());
if !(lo.is_finite() && hi.is_finite()) {
return None;
}
let (a, ea) = eval_float(SeriesKind::OddPlus, &lo, prec)?;
let (c, ec) = eval_float(SeriesKind::OddPlus, &hi, prec)?;
let work = prec + 32;
Some(ball_from_interval(
&Float::with_val(work, &a - &ea),
&Float::with_val(work, &c + &ec),
prec,
))
}
pub fn ball_chi(b: &ArbBall) -> Option<ArbBall> {
let prec = b.prec;
let (lo, hi) = (b.lo(), b.hi());
if !(lo.is_finite() && hi.is_finite()) || lo <= 0 {
return None;
}
let (a, ea) = eval_float(SeriesKind::EvenPlus, &lo, prec)?;
let (c, ec) = eval_float(SeriesKind::EvenPlus, &hi, prec)?;
let work = prec + 32;
Some(ball_from_interval(
&Float::with_val(work, &a - &ea),
&Float::with_val(work, &c + &ec),
prec,
))
}
const MAX_TAYLOR_TERMS: usize = 256;
fn series_coeffs(kind: SeriesKind, x_mag: &Float, prec: u32) -> VResult<(Vec<ArbBall>, Float)> {
let work = prec + 32;
let xf = Float::with_val(53, x_mag).to_f64();
if !xf.is_finite() {
return Err(ValidatedError::NotFinite {
what: "exponential-integral argument enclosure".into(),
});
}
let mut target = Float::with_val(work, xf).exp();
if target < 1 {
target = Float::with_val(work, 1u32);
}
target >>= prec + 4;
let xw = Float::with_val(work, x_mag);
let mut coeffs: Vec<ArbBall> = Vec::new();
let mut fact = Integer::from(1);
let mut xpow = Float::with_val(work, 1u32);
let zero = ArbBall::from_f64(0.0, prec);
for m in 1..=MAX_TAYLOR_TERMS + 1 {
fact *= m as u32;
xpow *= &xw;
let den = Integer::from(m as u32) * fact.clone();
let u = Float::with_val(work, &xpow / Float::with_val(work, &den));
if !u.is_finite() {
return Err(ValidatedError::NotFinite {
what: "exponential-integral series term".into(),
});
}
if m >= 2 && (m as f64) >= 2.0 * xf + 2.0 && u <= target {
let mut tail = Float::with_val(work, u * 2u32);
tail += Float::with_val(work, &tail) >> 16u32;
return Ok((coeffs, Float::with_val_round(prec, tail, Round::Up).0));
}
if m > MAX_TAYLOR_TERMS {
break;
}
coeffs.push(match kind.sigma(m) {
Some(s) => ArbBall::from_rational(&Rational::from((Integer::from(s), den)), prec),
None => zero.clone(),
});
}
Err(ValidatedError::NotFinite {
what: format!(
"exponential-integral series over an argument enclosure of \
magnitude {xf:.3e}: the origin-centred expansion would need more \
than {MAX_TAYLOR_TERMS} terms, and subdividing the box does not \
reduce |x|"
),
})
}
fn series_model(x: &TaylorModel, kind: SeriesKind) -> VResult<TaylorModel> {
let prec = x.prec();
let range = x.range();
if !is_finite(&range) {
return Err(ValidatedError::NotFinite {
what: "exponential-integral argument".into(),
});
}
let (coeffs, tail) = series_coeffs(kind, &mag(&range), prec)?;
let (nvars, order) = (x.nvars(), x.order());
let last = coeffs
.last()
.cloned()
.unwrap_or_else(|| ArbBall::from_f64(0.0, prec));
let mut acc = TaylorModel::constant(last, nvars, order, prec);
for c in coeffs.iter().rev().skip(1) {
acc = acc.mul(x).shift(c);
}
let poly = acc.mul(x);
Ok(poly.add(&TaylorModel::constant(
symmetric(&tail, prec),
nvars,
order,
prec,
)))
}
fn log_abs_model(x: &TaylorModel, what: &str) -> VResult<TaylorModel> {
let r = x.range();
if r.lo() > 0 {
x.log()
} else if r.hi() < 0 {
x.neg().log()
} else {
Err(ValidatedError::DomainViolation {
what: format!("{what} has a logarithmic singularity the box straddles"),
})
}
}
pub fn taylor_ei(x: &TaylorModel) -> VResult<TaylorModel> {
let g = euler_gamma_ball(x.prec());
let l = log_abs_model(x, "Ei")?;
Ok(l.shift(&g).add(&series_model(x, SeriesKind::All)?))
}
pub fn taylor_li(x: &TaylorModel) -> VResult<TaylorModel> {
let r = x.range();
if r.lo() <= 0 {
return Err(ValidatedError::DomainViolation {
what: "li(x) is complex for x < 0".into(),
});
}
if r.lo() <= 1 && r.hi() >= 1 {
return Err(ValidatedError::DomainViolation {
what: "li has a logarithmic singularity at x = 1 that the box straddles".into(),
});
}
taylor_ei(&x.log()?)
}
pub fn taylor_si(x: &TaylorModel) -> VResult<TaylorModel> {
series_model(x, SeriesKind::OddAlt)
}
pub fn taylor_shi(x: &TaylorModel) -> VResult<TaylorModel> {
series_model(x, SeriesKind::OddPlus)
}
pub fn taylor_ci(x: &TaylorModel) -> VResult<TaylorModel> {
let g = euler_gamma_ball(x.prec());
let l = positive_log_model(x, "Ci")?;
Ok(l.shift(&g).add(&series_model(x, SeriesKind::EvenAlt)?))
}
pub fn taylor_chi(x: &TaylorModel) -> VResult<TaylorModel> {
let g = euler_gamma_ball(x.prec());
let l = positive_log_model(x, "Chi")?;
Ok(l.shift(&g).add(&series_model(x, SeriesKind::EvenPlus)?))
}
fn positive_log_model(x: &TaylorModel, what: &str) -> VResult<TaylorModel> {
if x.range().lo() <= 0 {
return Err(ValidatedError::DomainViolation {
what: format!("{what}(x) is complex for x ≤ 0"),
});
}
x.log()
}
fn quotient_diff(args: &[ExprId], wrt: ExprId, pool: &ExprPool, numerator: &str) -> Option<ExprId> {
let x = args[0];
let dx = crate::diff::diff(x, wrt, pool).ok()?.value;
let num = pool.func(numerator, vec![x]);
let recip = pool.pow(x, pool.integer(-1_i32));
Some(pool.mul(vec![num, recip, dx]))
}
fn quotient_adjoint(
args: &[ExprId],
cotan: ExprId,
pool: &ExprPool,
numerator: &str,
) -> Option<Vec<ExprId>> {
let x = args[0];
let num = pool.func(numerator, vec![x]);
let recip = pool.pow(x, pool.integer(-1_i32));
Some(vec![pool.mul(vec![cotan, num, recip])])
}
macro_rules! quotient_primitive {
(
$struct_name:ident, $name:literal, $numerator:literal,
$f64_kernel:expr, $ball_kernel:expr, $doc:literal
) => {
#[doc = $doc]
pub struct $struct_name;
impl Primitive for $struct_name {
fn name(&self) -> &'static str {
$name
}
fn pretty(&self, args: &[ExprId], pool: &ExprPool) -> String {
format!("{}({})", $name, pool.display(args[0]))
}
fn diff_forward(
&self,
args: &[ExprId],
wrt: ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
quotient_diff(args, wrt, pool, $numerator)
}
fn diff_reverse(
&self,
args: &[ExprId],
cotan: ExprId,
pool: &ExprPool,
) -> Option<Vec<ExprId>> {
quotient_adjoint(args, cotan, pool, $numerator)
}
fn numeric_f64(&self, args: &[f64]) -> Option<f64> {
let f: fn(f64) -> Option<f64> = $f64_kernel;
if args.len() != 1 {
return None;
}
f(args[0])
}
fn numeric_ball(&self, args: &[ArbBall]) -> Option<ArbBall> {
let f: fn(&ArbBall) -> Option<ArbBall> = $ball_kernel;
if args.len() != 1 {
return None;
}
f(&args[0])
}
}
};
}
quotient_primitive!(
EiPrimitive,
"Ei",
"exp",
ei,
ball_ei,
"`Ei(x)`, the exponential integral (DLMF 6.2.5). `Ei′(x) = eˣ/x`."
);
quotient_primitive!(
SiPrimitive,
"Si",
"sin",
|x| Some(si(x)),
|b| ball_si(b),
"`Si(x) = ∫₀ˣ (sin t)/t dt`, the sine integral (DLMF 6.2.9). \
`Si′(x) = sin(x)/x`."
);
quotient_primitive!(
CiPrimitive,
"Ci",
"cos",
ci,
ball_ci,
"`Ci(x) = −∫ₓ^∞ (cos t)/t dt`, the cosine integral (DLMF 6.2.11). \
`Ci′(x) = cos(x)/x`. Complex for `x < 0`, where the kernels refuse."
);
quotient_primitive!(
ShiPrimitive,
"Shi",
"sinh",
|x| Some(shi(x)),
|b| ball_shi(b),
"`Shi(x) = ∫₀ˣ (sinh t)/t dt`, the hyperbolic sine integral \
(DLMF 6.2.15). `Shi′(x) = sinh(x)/x`."
);
quotient_primitive!(
ChiPrimitive,
"Chi",
"cosh",
chi,
ball_chi,
"`Chi(x) = γ + log x + ∫₀ˣ (cosh t − 1)/t dt`, the hyperbolic cosine \
integral (DLMF 6.2.16). `Chi′(x) = cosh(x)/x`. Complex for `x < 0`, \
where the kernels refuse."
);
pub struct LiPrimitive;
impl Primitive for LiPrimitive {
fn name(&self) -> &'static str {
"li"
}
fn pretty(&self, args: &[ExprId], pool: &ExprPool) -> String {
format!("li({})", pool.display(args[0]))
}
fn diff_forward(&self, args: &[ExprId], wrt: ExprId, pool: &ExprPool) -> Option<ExprId> {
let x = args[0];
let dx = crate::diff::diff(x, wrt, pool).ok()?.value;
let recip_log = pool.pow(pool.func("log", vec![x]), pool.integer(-1_i32));
Some(pool.mul(vec![recip_log, dx]))
}
fn diff_reverse(&self, args: &[ExprId], cotan: ExprId, pool: &ExprPool) -> Option<Vec<ExprId>> {
let x = args[0];
let recip_log = pool.pow(pool.func("log", vec![x]), pool.integer(-1_i32));
Some(vec![pool.mul(vec![cotan, recip_log])])
}
fn numeric_f64(&self, args: &[f64]) -> Option<f64> {
if args.len() != 1 {
return None;
}
li(args[0])
}
fn numeric_ball(&self, args: &[ArbBall]) -> Option<ArbBall> {
if args.len() != 1 {
return None;
}
ball_li(&args[0])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
use crate::primitive::{Capabilities, PrimitiveRegistry};
const REF_PREC: u32 = 200;
fn reference(kind: SeriesKind, x: f64) -> f64 {
eval_float(kind, &Float::with_val(REF_PREC, x), REF_PREC)
.map(|(v, _)| v.to_f64())
.unwrap_or(f64::NAN)
}
fn rel(a: f64, b: f64) -> f64 {
(a - b).abs() / b.abs().max(1e-300)
}
fn scaled(a: f64, b: f64) -> f64 {
(a - b).abs() / b.abs().max(1.0)
}
#[test]
fn published_reference_values() {
assert!((ei(1.0).unwrap() - 1.895_117_816_355_936_8).abs() < 1e-15);
assert!((ei(-1.0).unwrap() + 0.219_383_934_395_520_27).abs() < 1e-15);
assert!((li(2.0).unwrap() - 1.045_163_780_117_493).abs() < 1e-15);
assert!((ci(1.0).unwrap() - 0.337_403_922_900_968_1).abs() < 1e-15);
assert!((si(std::f64::consts::PI) - 1.851_937_051_982).abs() < 1e-12);
for &(x, s, c) in &[
(0.5_f64, 0.506_996_749_8_f64, -0.052_776_844_96_f64),
(1.0, 1.057_250_875, 0.837_866_941),
(2.0, 2.501_567_433, 2.452_666_923),
(5.0, 20.093_211_83, 20.092_063_53),
] {
assert!(rel(shi(x), s) < 5e-10, "Shi({x})");
assert!(rel(chi(x).unwrap(), c) < 5e-9, "Chi({x})");
}
for &z in &[0.616_505_f64, 3.384_18, 6.427_05] {
assert!(ci(z).unwrap().abs() < 1e-5, "Ci near published zero {z}");
}
assert_eq!(si(f64::INFINITY), std::f64::consts::FRAC_PI_2);
assert_eq!(si(f64::NEG_INFINITY), -std::f64::consts::FRAC_PI_2);
assert!((si(1.0e6) - std::f64::consts::FRAC_PI_2).abs() < 2e-6);
assert!(ci(1.0e6).unwrap().abs() < 2e-6);
}
#[test]
fn defining_identities_hold() {
for &x in &[0.25_f64, 0.75, 1.0, 2.5, 6.0, 6.5, 12.0, 30.0] {
assert!(rel(ei(-x).unwrap(), -e1(x)) < 1e-14, "Ei(-{x}) = -E1({x})");
let (s, c) = (shi(x), chi(x).unwrap());
assert!(rel(c + s, ei(x).unwrap()) < 1e-13, "Chi+Shi = Ei at {x}");
assert!(
((c - s) - ei(-x).unwrap()).abs() <= 1e-13 * (c.abs() + s.abs()),
"Chi-Shi = Ei(-x) at {x}"
);
}
for &x in &[0.5_f64, 3.0, 6.5, 40.0] {
assert_eq!(si(-x), -si(x));
assert_eq!(shi(-x), -shi(x));
}
}
#[test]
fn f64_kernels_match_the_high_precision_series() {
let sweep =
|lo: f64, hi: f64, n: usize| (0..=n).map(move |k| lo + (hi - lo) * k as f64 / n as f64);
for x in sweep(0.01, 60.0, 600) {
assert!(
rel(ei(x).unwrap(), reference(SeriesKind::All, x)) < 1e-13,
"Ei({x})"
);
assert!(
scaled(si(x), reference(SeriesKind::OddAlt, x)) < 1e-13,
"Si({x})"
);
assert!(
scaled(ci(x).unwrap(), reference(SeriesKind::EvenAlt, x)) < 1e-13,
"Ci({x})"
);
assert!(
rel(shi(x), reference(SeriesKind::OddPlus, x)) < 1e-13,
"Shi({x})"
);
assert!(
rel(chi(x).unwrap(), reference(SeriesKind::EvenPlus, x)) < 1e-13,
"Chi({x})"
);
}
for x in sweep(-60.0, -0.01, 600) {
assert!(
rel(ei(x).unwrap(), reference(SeriesKind::All, x)) < 1e-13,
"Ei({x})"
);
assert!(
scaled(si(x), reference(SeriesKind::OddAlt, x)) < 1e-13,
"Si({x})"
);
assert!(
rel(shi(x), reference(SeriesKind::OddPlus, x)) < 1e-13,
"Shi({x})"
);
}
for x in sweep(0.01, 0.99, 200).chain(sweep(1.01, 50.0, 400)) {
let want = li_float(&Float::with_val(REF_PREC, x), REF_PREC)
.unwrap()
.0
.to_f64();
assert!(scaled(li(x).unwrap(), want) < 1e-13, "li({x})");
}
for x in sweep(60.0, 300.0, 200) {
assert!(
rel(ei(x).unwrap(), reference(SeriesKind::All, x)) < 1e-13,
"Ei({x})"
);
assert!(
scaled(si(x), reference(SeriesKind::OddAlt, x)) < 1e-13,
"Si({x})"
);
assert!(
scaled(ci(x).unwrap(), reference(SeriesKind::EvenAlt, x)) < 1e-13,
"Ci({x})"
);
}
}
#[test]
fn branches_agree_at_every_switchover() {
for &x in &[
SICI_SERIES_MAX,
SICI_SERIES_MAX - 0.5,
SICI_SERIES_MAX + 0.5,
] {
let (s_ser, c_ser) = si_ci_series(x);
let (s_cf, c_cf) = si_ci_cf(x);
assert!(
rel(s_ser, s_cf) < 1e-13,
"Si branches at {x}: {s_ser} vs {s_cf}"
);
assert!(
scaled(c_ser, c_cf) < 1e-13,
"Ci branches at {x}: {c_ser} vs {c_cf}"
);
}
for &x in &[
SHICHI_SERIES_MAX,
SHICHI_SERIES_MAX - 0.5,
SHICHI_SERIES_MAX + 0.5,
] {
let (s_ser, c_ser) = shi_chi_series(x);
let (s_id, c_id) = (
0.5 * (ei_positive(x) + e1(x)),
0.5 * (ei_positive(x) - e1(x)),
);
assert!(rel(s_ser, s_id) < 1e-13, "Shi branches at {x}");
assert!(rel(c_ser, c_id) < 1e-13, "Chi branches at {x}");
}
for &x in &[EI_SERIES_MAX - 1.0, EI_SERIES_MAX, EI_SERIES_MAX + 1.0] {
let want = reference(SeriesKind::All, x);
assert!(rel(ei_positive(x), want) < 1e-13, "Ei kernel at {x}");
}
for &z in &[E1_CF_MIN - 0.05, E1_CF_MIN, E1_CF_MIN + 0.05] {
let want = -reference(SeriesKind::All, -z);
assert!(rel(e1(z), want) < 1e-13, "E1 kernel at {z}");
}
for (name, f, c) in [
(
"Si",
Box::new(si) as Box<dyn Fn(f64) -> f64>,
SICI_SERIES_MAX,
),
("Ci", Box::new(|v| ci(v).unwrap()), SICI_SERIES_MAX),
("Shi", Box::new(shi), SHICHI_SERIES_MAX),
("Chi", Box::new(|v| chi(v).unwrap()), SHICHI_SERIES_MAX),
("Ei", Box::new(|v| ei(v).unwrap()), EI_SERIES_MAX),
("Ei", Box::new(|v| ei(v).unwrap()), -E1_CF_MIN),
] {
let h = 1e-9;
let (lo, mid, hi) = (f(c - h), f(c), f(c + h));
let curvature = (hi - 2.0 * mid + lo).abs() / mid.abs().max(1.0);
assert!(
curvature < 1e-12,
"{name} steps at the seam {c}: {curvature:e}"
);
}
}
#[test]
fn singular_points_and_branch_cuts() {
assert_eq!(ei(0.0), Some(f64::NEG_INFINITY));
assert_eq!(ci(0.0), Some(f64::NEG_INFINITY));
assert_eq!(chi(0.0), Some(f64::NEG_INFINITY));
assert_eq!(li(1.0), Some(f64::NEG_INFINITY));
assert!(ei(1e-8).unwrap() < -17.0 && ei(-1e-8).unwrap() < -17.0);
assert!(ci(1e-8).unwrap() < -17.0);
assert!(li(1.0 - 1e-8).unwrap() < -17.0 && li(1.0 + 1e-8).unwrap() < -17.0);
assert_eq!(li(0.0), Some(0.0));
assert_eq!(si(0.0), 0.0);
assert_eq!(shi(0.0), 0.0);
for &x in &[-1e-12_f64, -0.5, -1.0, -10.0] {
assert_eq!(ci(x), None, "Ci({x}) must refuse");
assert_eq!(chi(x), None, "Chi({x}) must refuse");
assert_eq!(li(x), None, "li({x}) must refuse");
}
assert!(ei(-0.5).unwrap().is_finite());
assert!(si(-40.0).is_finite() && shi(-40.0).is_finite());
assert_eq!(ei(f64::NAN), None);
assert_eq!(li(f64::NAN), None);
assert_eq!(ci(f64::NAN), None);
assert_eq!(chi(f64::NAN), None);
assert!(si(f64::NAN).is_nan());
}
#[test]
fn derivatives_are_the_documented_elementary_functions() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let recip = pool.pow(x, pool.integer(-1_i32));
for (name, numerator) in [
("Ei", "exp"),
("Si", "sin"),
("Ci", "cos"),
("Shi", "sinh"),
("Chi", "cosh"),
] {
let d = crate::diff::diff(pool.func(name, vec![x]), x, &pool)
.expect("diff")
.value;
let want = pool.mul(vec![pool.func(numerator, vec![x]), recip]);
assert_eq!(
pool.display(d).to_string(),
pool.display(want).to_string(),
"d/dx {name}(x) should be {numerator}(x)/x"
);
}
let d = crate::diff::diff(pool.func("li", vec![x]), x, &pool)
.expect("diff")
.value;
let want = pool.pow(pool.func("log", vec![x]), pool.integer(-1_i32));
assert_eq!(
pool.display(d).to_string(),
pool.display(want).to_string(),
"d/dx li(x) = 1/log x"
);
}
#[test]
fn derivatives_match_finite_differences() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let arg = pool.add(vec![
pool.mul(vec![pool.integer(2_i32), x]),
pool.integer(1_i32),
]);
let reg = PrimitiveRegistry::default_registry();
for (name, f) in [
(
"Ei",
Box::new(|v: f64| ei(v).unwrap()) as Box<dyn Fn(f64) -> f64>,
),
("Si", Box::new(si)),
("Ci", Box::new(|v| ci(v).unwrap())),
("Shi", Box::new(shi)),
("Chi", Box::new(|v| chi(v).unwrap())),
("li", Box::new(|v| li(v).unwrap())),
] {
let d = crate::diff::diff(pool.func(name, vec![arg]), x, &pool)
.expect("diff")
.value;
for &x0 in &[0.3_f64, 0.9, 2.0] {
let h = 1e-5;
let (u, up, um) = (2.0 * x0 + 1.0, 2.0 * (x0 + h) + 1.0, 2.0 * (x0 - h) + 1.0);
let fd = (f(up) - f(um)) / (2.0 * h);
let mut env = crate::ball::IntervalEval::new(160);
env.bind(x, ArbBall::from_f64(x0, 160));
let got = env.eval(d, &pool).expect("eval derivative").mid_f64();
assert!(
(got - fd).abs() < 1e-6 * fd.abs().max(1.0),
"d/dx {name}(2x+1) at {x0}: {got} vs finite difference {fd} (u = {u})"
);
assert!(rel(reg.numeric_f64(name, &[u]).unwrap(), f(u)) < 1e-15);
}
}
}
#[test]
fn verification_gate_round_trip() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
for (name, integrand) in [
(
"Ei",
Box::new(|v: f64| v.exp() / v) as Box<dyn Fn(f64) -> f64>,
),
("Si", Box::new(|v| v.sin() / v)),
("Ci", Box::new(|v| v.cos() / v)),
("Shi", Box::new(|v| v.sinh() / v)),
("Chi", Box::new(|v| v.cosh() / v)),
("li", Box::new(|v| 1.0 / v.ln())),
] {
let big_f = pool.func(name, vec![x]);
let d = crate::diff::diff(big_f, x, &pool).expect("diff").value;
for &x0 in &[0.4_f64, 1.7, 3.25, 9.5] {
let mut env = crate::ball::IntervalEval::new(160);
env.bind(x, ArbBall::from_f64(x0, 160));
let got = env.eval(d, &pool).expect("eval").mid_f64();
let want = integrand(x0);
assert!(
rel(got, want) < 1e-13,
"d/dx {name}(x) at {x0}: {got} vs {want}"
);
}
}
}
#[test]
fn ball_kernels_enclose_and_agree_with_f64() {
let prec = 160u32;
for &x in &[0.25_f64, 1.0, 2.0, 5.9, 6.0, 6.1, 20.0, 44.9, 45.1, 120.0] {
let b = ArbBall::from_f64(x, prec);
assert!(rel(ei(x).unwrap(), ball_ei(&b).unwrap().mid_f64()) < 1e-13);
assert!(scaled(si(x), ball_si(&b).unwrap().mid_f64()) < 1e-13);
assert!(scaled(ci(x).unwrap(), ball_ci(&b).unwrap().mid_f64()) < 1e-13);
assert!(rel(shi(x), ball_shi(&b).unwrap().mid_f64()) < 1e-13);
assert!(rel(chi(x).unwrap(), ball_chi(&b).unwrap().mid_f64()) < 1e-13);
}
for &x in &[-0.25_f64, -1.0, -6.5, -30.0] {
let b = ArbBall::from_f64(x, prec);
assert!(rel(ei(x).unwrap(), ball_ei(&b).unwrap().mid_f64()) < 1e-13);
assert!(scaled(si(x), ball_si(&b).unwrap().mid_f64()) < 1e-13);
assert!(rel(shi(x), ball_shi(&b).unwrap().mid_f64()) < 1e-13);
assert_eq!(ball_ci(&b), None);
assert_eq!(ball_chi(&b), None);
assert_eq!(ball_li(&b), None);
}
let wide = ArbBall::from_midpoint_radius(4.0, 3.0, prec);
let si_b = ball_si(&wide).unwrap();
for k in 0..=400 {
let v = 1.0 + 6.0 * k as f64 / 400.0;
assert!(
si_b.contains(si(v)),
"Si ball over [1,7] misses Si({v}) = {}",
si(v)
);
}
assert_eq!(
ball_ei(&ArbBall::from_midpoint_radius(0.0, 1.0, prec)),
None
);
assert_eq!(
ball_li(&ArbBall::from_midpoint_radius(1.0, 0.5, prec)),
None
);
assert_eq!(
ball_ci(&ArbBall::from_midpoint_radius(0.5, 1.0, prec)),
None
);
}
#[test]
fn every_primitive_registers_a_full_bundle() {
let reg = PrimitiveRegistry::default_registry();
for name in ["Ei", "li", "Si", "Ci", "Shi", "Chi"] {
assert!(reg.is_registered(name), "{name} not registered");
let caps = reg.capabilities(name);
for (flag, what) in [
(Capabilities::NUMERIC_F64, "numeric_f64"),
(Capabilities::NUMERIC_BALL, "numeric_ball"),
(Capabilities::DIFF_FORWARD, "diff_forward"),
(Capabilities::DIFF_REVERSE, "diff_reverse"),
(Capabilities::TAYLOR_MODEL, "taylor_model"),
] {
assert!(caps.contains(flag), "{name} is missing {what}");
}
}
assert_eq!(reg.numeric_f64("Ei", &[1.0, 2.0]), None);
assert_eq!(reg.numeric_f64("Si", &[]), None);
}
#[test]
fn taylor_models_enclose_the_true_range() {
use crate::validated::bounds::{bound_on_box, BoundOptions};
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let opts = BoundOptions {
order: 4,
prec: 96,
tol: 1e-10,
max_subdivisions: 32,
};
for &(name, lo, hi) in &[
("Ei", 0.25_f64, 0.5_f64),
("Ei", 1.0, 3.0),
("Ei", -3.0, -1.0),
("li", 2.0, 4.0),
("li", 0.25, 0.5),
("Si", -3.0, 3.0),
("Si", 8.0, 12.0),
("Ci", 0.5, 2.0),
("Ci", 8.0, 12.0),
("Shi", -2.0, 2.0),
("Chi", 1.0, 3.0),
] {
let e = pool.func(name, vec![x]);
let r = bound_on_box(e, &pool, &[(x, lo, hi)], &opts)
.unwrap_or_else(|err| panic!("{name} on [{lo},{hi}]: {err}"));
let f = |v: f64| match name {
"Ei" => ei(v).unwrap(),
"li" => li(v).unwrap(),
"Si" => si(v),
"Ci" => ci(v).unwrap(),
"Shi" => shi(v),
"Chi" => chi(v).unwrap(),
_ => unreachable!(),
};
for k in 0..=400 {
let v = lo + (hi - lo) * k as f64 / 400.0;
let y = f(v);
assert!(
r.lower() <= y + 1e-9 && r.upper() >= y - 1e-9,
"{name} enclosure [{}, {}] misses {name}({v}) = {y}",
r.lower(),
r.upper()
);
}
}
}
#[test]
fn taylor_models_refuse_boxes_that_straddle_a_singularity() {
use crate::validated::bounds::{bound_on_box, BoundOptions};
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let opts = BoundOptions::default();
for &(name, lo, hi) in &[
("Ei", -1.0_f64, 1.0_f64),
("li", 0.5, 2.0),
("li", -1.0, 2.0),
("Ci", -1.0, 1.0),
("Ci", -2.0, -1.0),
("Chi", -1.0, 1.0),
("Chi", -2.0, -1.0),
] {
let e = pool.func(name, vec![x]);
assert!(
bound_on_box(e, &pool, &[(x, lo, hi)], &opts).is_err(),
"{name} on [{lo},{hi}] must refuse — it is singular or complex there"
);
}
}
#[test]
fn series_tail_bound_is_not_optimistic() {
for &x in &[0.5_f64, 2.0, 5.0, 12.0] {
for kind in [
SeriesKind::All,
SeriesKind::OddAlt,
SeriesKind::EvenAlt,
SeriesKind::OddPlus,
SeriesKind::EvenPlus,
] {
let xm = Float::with_val(96, x);
let (coeffs, tail) = series_coeffs(kind, &xm, 96).expect("coeffs");
let mut partial = 0.0_f64;
let mut pw = 1.0_f64;
for c in &coeffs {
pw *= x;
partial += c.mid_f64() * pw;
}
let full = reference(kind, x)
- if kind.has_log_prefix() {
EULER_GAMMA + x.ln()
} else {
0.0
};
let gap = (full - partial).abs();
let bound = tail.to_f64();
assert!(
gap <= bound * 1.000_001 + 1e-12 * full.abs().max(1.0),
"{kind:?} at x={x}: truncation gap {gap:e} exceeds claimed tail {bound:e}"
);
}
}
}
#[test]
fn display_is_stable_in_both_renderers() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
for (name, latex) in [
("Ei", r"\operatorname{Ei}"),
("li", r"\operatorname{li}"),
("Si", r"\operatorname{Si}"),
("Ci", r"\operatorname{Ci}"),
("Shi", r"\operatorname{Shi}"),
("Chi", r"\operatorname{Chi}"),
] {
let e = pool.func(name, vec![x]);
let tex = crate::kernel::display::render_latex(e, &pool);
assert!(tex.contains(latex), "latex for {name}: {tex}");
let uni = crate::kernel::display::render_unicode(e, &pool);
assert_eq!(uni, format!("{name}(x)"), "unicode for {name}");
assert!(!uni.contains('\u{3a7}') && !tex.contains(r"\Chi"));
assert_eq!(pool.display(e).to_string(), format!("{name}(x)"));
}
}
}