use crate::ball::ArbBall;
use crate::kernel::{ExprId, ExprPool};
use crate::primitive::Primitive;
use rug::Float;
const PI2_6: f64 = std::f64::consts::PI * std::f64::consts::PI / 6.0;
const BERNOULLI_EVEN: [f64; 10] = [
2.777_777_777_777_777_6e-2, -2.777_777_777_777_778e-4, 4.724_111_866_969_01e-6, -9.185_773_074_661_964e-8, 1.897_886_998_897_1e-9, -4.064_761_645_144_225_6e-11, 8.921_691_020_456_452e-13, -1.993_929_586_072_107_4e-14, 4.518_980_029_619_918e-16, -1.035_651_761_218_124_7e-17, ];
pub fn dilog(x: f64) -> Option<f64> {
if !x.is_finite() || x > 1.0 {
return None;
}
if x == 1.0 {
return Some(PI2_6);
}
if x < -1.0 {
let l = (-x).ln();
return Some(-dilog_core(1.0 / x) - PI2_6 - 0.5 * l * l);
}
if x > 0.5 {
return Some(PI2_6 - x.ln() * (-x).ln_1p() - dilog_core(1.0 - x));
}
Some(dilog_core(x))
}
fn dilog_core(x: f64) -> f64 {
let u = -(-x).ln_1p(); let t = u * u;
let mut p = 0.0_f64;
for &c in BERNOULLI_EVEN.iter().rev() {
p = p * t + c;
}
u - 0.25 * t + u * t * p
}
pub fn dilog_ball_point(x: &Float, prec: u32) -> Option<ArbBall> {
if !x.is_finite() || *x > 1u32 {
return None;
}
let work = prec + 32;
let v = Float::with_val(work, x).li2();
let mid = Float::with_val(prec, &v);
let trunc = Float::with_val(prec, Float::with_val(work, &v - &mid).abs());
let mut b = ArbBall {
mid,
rad: trunc,
prec,
};
let mut bump = Float::with_val(prec, b.mid.abs_ref());
bump >>= prec.saturating_sub(2);
b.rad += bump;
Some(b)
}
pub struct DilogPrimitive;
impl Primitive for DilogPrimitive {
fn name(&self) -> &'static str {
"dilog"
}
fn pretty(&self, args: &[ExprId], pool: &ExprPool) -> String {
format!("Li₂({})", pool.display(args[0]))
}
fn simplify(&self, args: &[ExprId], pool: &ExprPool) -> Option<ExprId> {
match pool.get(args[0]) {
crate::kernel::expr::ExprData::Integer(n) if n.0 == 0 => Some(pool.integer(0_i32)),
_ => None,
}
}
fn diff_forward(&self, args: &[ExprId], wrt: ExprId, pool: &ExprPool) -> Option<ExprId> {
if args.len() != 1 {
return None;
}
let x = args[0];
let dx = crate::diff::diff(x, wrt, pool).ok()?.value;
Some(pool.mul(vec![dilog_local_derivative(x, pool), dx]))
}
fn diff_reverse(&self, args: &[ExprId], cotan: ExprId, pool: &ExprPool) -> Option<Vec<ExprId>> {
if args.len() != 1 {
return None;
}
Some(vec![
pool.mul(vec![cotan, dilog_local_derivative(args[0], pool)])
])
}
fn numeric_f64(&self, args: &[f64]) -> Option<f64> {
match args {
[x] => dilog(*x),
_ => None,
}
}
fn numeric_ball(&self, args: &[ArbBall]) -> Option<ArbBall> {
match args {
[only] => only.dilog(),
_ => None,
}
}
}
fn dilog_local_derivative(x: ExprId, pool: &ExprPool) -> ExprId {
let one_minus_x = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x]),
]);
let log = pool.func("log", vec![one_minus_x]);
let inv_x = pool.pow(x, pool.integer(-1_i32));
pool.mul(vec![pool.integer(-1_i32), log, inv_x])
}
#[cfg(test)]
mod tests {
use super::*;
fn mpfr_li2(x: f64) -> f64 {
Float::with_val(200, x).li2().to_f64()
}
fn rel(got: f64, want: f64) -> f64 {
if want == 0.0 {
got.abs()
} else {
(got - want).abs() / want.abs()
}
}
#[test]
fn the_published_anchors() {
let pi2 = std::f64::consts::PI * std::f64::consts::PI;
assert!(rel(dilog(1.0).unwrap(), pi2 / 6.0) < 1e-15);
assert!(rel(dilog(-1.0).unwrap(), -pi2 / 12.0) < 1e-15);
let half = pi2 / 12.0 - 0.5 * std::f64::consts::LN_2 * std::f64::consts::LN_2;
assert!(rel(dilog(0.5).unwrap(), half) < 1e-15);
assert_eq!(dilog(0.0), Some(0.0));
}
#[test]
fn matches_mpfr_across_the_real_line() {
let mut xs: Vec<f64> = Vec::new();
let mut v = -8.0_f64;
while v <= 1.0 {
xs.push(v);
v += 0.02;
}
xs.extend([
-1.0e15,
-1.0e6,
-1000.0,
-10.0,
-2.0,
-1.5,
-1.0,
-1.000_000_1,
-0.999_999_9,
-0.75,
-0.5,
-0.125,
-1e-3,
-1e-8,
0.0,
1e-8,
1e-3,
0.125,
0.25,
0.4,
0.499_999_9,
0.5,
0.500_000_1,
0.6,
0.75,
0.9,
0.99,
0.999_999,
1.0 - 1e-15,
1.0,
]);
let mut worst = 0.0_f64;
let mut worst_at = 0.0_f64;
for x in xs {
let got = dilog(x).unwrap();
let want = mpfr_li2(x);
let r = rel(got, want);
if r > worst {
worst = r;
worst_at = x;
}
assert!(r < 1e-15, "Li₂({x}): got {got}, MPFR {want}, rel {r:e}");
}
assert!(
worst < 1e-15,
"worst relative error {worst:e} at {worst_at}"
);
}
#[test]
fn continuity_across_both_switchovers() {
for seam in [-1.0_f64, 0.5] {
for d in [1e-12_f64, 1e-9, 1e-6, 1e-3] {
let lo = dilog(seam - d).unwrap();
let hi = dilog(seam + d).unwrap();
assert!(
(hi - lo).abs() < 1.5 * 2.0 * d + 1e-15,
"jump at {seam} over ±{d}: {lo} vs {hi}"
);
assert!(rel(lo, mpfr_li2(seam - d)) < 1e-15);
assert!(rel(hi, mpfr_li2(seam + d)) < 1e-15);
}
}
}
#[test]
fn the_cut_starts_at_one_and_one_itself_is_included() {
assert!(dilog(1.0).is_some());
for x in [1.0 + f64::EPSILON, 1.000_001, 2.0, 1e300] {
assert!(dilog(x).is_none(), "Li₂({x}) must decline: cut on [1, ∞)");
}
for x in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(dilog(x).is_none());
}
}
#[test]
fn the_derivative_is_minus_log1mx_over_x() {
let h = 1e-6_f64;
for x in [-3.0_f64, -1.0, -0.4, 0.3, 0.7, 0.95] {
let d = (dilog(x + h).unwrap() - dilog(x - h).unwrap()) / (2.0 * h);
let want = -(1.0 - x).ln() / x;
assert!((d - want).abs() < 1e-8, "Li₂′({x}): {d} vs {want}");
}
}
#[test]
fn the_point_enclosure_encloses() {
for x in [-5.0_f64, -1.0, -0.25, 0.0, 0.5, 0.9, 1.0] {
let high = Float::with_val(200, x).li2();
for prec in [32_u32, 53, 128] {
let b = dilog_ball_point(&Float::with_val(prec, x), prec).unwrap();
assert!(
b.lo() <= high && high <= b.hi(),
"Li₂({x}) {high} outside {b}"
);
}
assert!(rel(dilog(x).unwrap(), high.to_f64()) < 4e-16);
}
assert!(dilog_ball_point(&Float::with_val(128, 1.5), 128).is_none());
}
}