pub(crate) const SERIES_TERM_FLOOR: f64 = 1e-18;
pub(crate) const FAST_REGIME_MAX_TERMS: usize = 200;
const TRILOG_MU_SERIES: [(i32, f64); 7] = [
(3, -1.0 / 12.0), (4, -1.0 / 288.0), (6, 1.0 / 86_400.0), (8, -1.0 / 10_160_640.0), (10, 1.0 / 870_912_000.0), (12, -1.0 / 63_228_211_200.0), (14, 691.0 / 2_855_960_819_712_000.0), ];
#[inline]
pub(crate) fn dilog_unit(z: f64) -> f64 {
if !z.is_finite() {
return f64::NAN;
}
let z = z.clamp(0.0, 1.0);
if z == 0.0 {
return 0.0;
}
if z >= 1.0 {
return std::f64::consts::PI * std::f64::consts::PI / 6.0;
}
if z <= 0.5 {
let mut sum = 0.0_f64;
let mut zk = z;
for k in 1..=FAST_REGIME_MAX_TERMS {
let kf = k as f64;
let term = zk / (kf * kf);
sum += term;
if term < SERIES_TERM_FLOOR {
break;
}
zk *= z;
}
sum
} else {
let one_minus_z = 1.0 - z;
let pi2_6 = std::f64::consts::PI * std::f64::consts::PI / 6.0;
pi2_6 - z.ln() * one_minus_z.ln() - dilog_unit(one_minus_z)
}
}
#[inline]
pub(crate) fn trilog_unit(z: f64) -> f64 {
const ZETA3: f64 = 1.2020569031595942853997381615114499907649862923404988817922;
const ZETA2: f64 = std::f64::consts::PI * std::f64::consts::PI / 6.0;
if !z.is_finite() {
return f64::NAN;
}
let z = z.clamp(0.0, 1.0);
if z == 0.0 {
return 0.0;
}
if z >= 1.0 {
return ZETA3;
}
if z > 0.5 {
let mu = z.ln();
if mu == 0.0 {
return ZETA3;
}
let mut sum = ZETA3 + ZETA2 * mu + 0.5 * mu * mu * (1.5 - (-mu).ln());
for (k, coeff) in TRILOG_MU_SERIES {
sum += coeff * mu.powi(k);
}
return sum;
}
let mut sum = 0.0_f64;
let mut zk = z;
for k in 1..=FAST_REGIME_MAX_TERMS {
let kf = k as f64;
let term = zk / (kf * kf * kf);
sum += term;
if term < SERIES_TERM_FLOOR {
break;
}
zk *= z;
}
sum
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dilog_unit_zero_is_zero() {
assert_eq!(dilog_unit(0.0), 0.0);
}
#[test]
fn dilog_unit_at_one_is_pi_squared_over_six() {
let expected = std::f64::consts::PI * std::f64::consts::PI / 6.0;
assert!((dilog_unit(1.0) - expected).abs() < 1e-15);
}
#[test]
fn dilog_unit_nan_input_returns_nan() {
assert!(dilog_unit(f64::NAN).is_nan());
}
#[test]
fn dilog_unit_at_half_matches_reflection_identity() {
let expected = std::f64::consts::PI.powi(2) / 12.0 - (2.0_f64).ln().powi(2) / 2.0;
assert!((dilog_unit(0.5) - expected).abs() < 1e-14);
}
#[test]
fn dilog_unit_is_positive_on_open_unit_interval() {
for z in [0.1, 0.25, 0.5, 0.75, 0.9] {
assert!(dilog_unit(z) > 0.0, "dilog_unit({z}) should be positive");
}
}
#[test]
fn dilog_unit_clamps_below_zero() {
assert_eq!(dilog_unit(-1.0), dilog_unit(0.0));
}
#[test]
fn dilog_unit_clamps_above_one() {
assert!((dilog_unit(2.0) - dilog_unit(1.0)).abs() < 1e-15);
}
#[test]
fn trilog_unit_zero_is_zero() {
assert_eq!(trilog_unit(0.0), 0.0);
}
#[test]
fn trilog_unit_at_one_is_zeta3() {
const ZETA3: f64 = 1.2020569031595942853997381615114499907649862923404988817922;
assert!((trilog_unit(1.0) - ZETA3).abs() < 1e-14);
}
#[test]
fn trilog_unit_nan_input_returns_nan() {
assert!(trilog_unit(f64::NAN).is_nan());
}
#[test]
fn trilog_unit_at_half_matches_known_value() {
let expected = 0.5372131936080403_f64;
assert!((trilog_unit(0.5) - expected).abs() < 1e-13);
}
#[test]
fn trilog_unit_is_positive_on_open_unit_interval() {
for z in [0.1, 0.25, 0.5, 0.75, 0.9] {
assert!(trilog_unit(z) > 0.0, "trilog_unit({z}) should be positive");
}
}
#[test]
fn trilog_unit_clamps_below_zero() {
assert_eq!(trilog_unit(-0.5), trilog_unit(0.0));
}
#[test]
fn trilog_unit_clamps_above_one() {
assert!((trilog_unit(2.0) - trilog_unit(1.0)).abs() < 1e-14);
}
const TRILOG_REFERENCE: [(f64, f64); 16] = [
(0.05, 5.031_722_986_057_436_7e-2),
(0.125, 1.270_295_409_793_486_0e-1),
(0.25, 2.584_613_957_965_732_9e-1),
(0.5, 5.372_131_936_080_402_1e-1),
(0.500_000_000_000_000_1, 5.372_131_936_080_403_2e-1),
(0.6, 6.560_025_136_329_806_8e-1),
(0.75, 8.444_258_088_622_044_2e-1),
(0.9, 1.049_658_950_186_439_9),
(0.95, 1.123_574_584_279_198_9),
(0.99, 1.185_832_933_645_036_8),
(0.999, 1.200_415_353_995_464_3),
(0.9999, 1.201_892_455_084_581_5),
(0.999_999, 1.202_055_258_232_362_7),
(0.999_999_999, 1.202_056_901_514_660_3),
(0.999_999_999_999, 1.202_056_903_157_949_3),
(0.999_999_999_999_999, 1.202_056_903_159_592_7),
];
#[test]
fn trilog_unit_matches_high_precision_reference_across_both_branches() {
let mut worst = 0.0_f64;
let mut worst_z = f64::NAN;
println!(
"\n{:>22} {:>24} {:>24} {:>10}",
"z", "reference Li3", "trilog_unit", "rel"
);
for (z, want) in TRILOG_REFERENCE {
let got = trilog_unit(z);
let rel = (got - want).abs() / want.abs();
if rel > worst {
worst = rel;
worst_z = z;
}
println!("{z:>22.17} {want:>24.17e} {got:>24.17e} {rel:>10.2e}");
}
println!("\n worst: {worst:.3e} at z = {worst_z}\n");
assert!(
worst < 4.0 * f64::EPSILON,
"trilog_unit is off by {worst:.3e} relative at z = {worst_z}; the \
mu expansion should hold the whole interval to a few ulps"
);
}
#[test]
fn trilog_unit_is_continuous_across_the_branch_switch() {
let below = trilog_unit(0.5);
let above = trilog_unit(f64::from_bits(0.5_f64.to_bits() + 1));
let jump = (above - below).abs() / below;
println!("\n branch switch at z=0.5: {below:.17e} -> {above:.17e}, rel step {jump:.2e}\n");
assert!(
jump < 4.0 * f64::EPSILON,
"branch switch at z = 0.5 steps by {jump:.3e} relative"
);
}
#[test]
fn trilog_unit_is_monotone_increasing_up_to_zeta3() {
const ZETA3: f64 = 1.202_056_903_159_594_3;
let mut prev = 0.0_f64;
for i in 0..=2000 {
let z = i as f64 / 2000.0;
let v = trilog_unit(z);
assert!(v >= prev, "trilog_unit decreased at z = {z}: {prev} -> {v}");
assert!(
v <= ZETA3 + 4.0 * f64::EPSILON,
"trilog_unit({z}) = {v} > zeta(3)"
);
prev = v;
}
let mut prev_tail = 0.0_f64;
for k in 1..=15 {
let z = 1.0 - 10.0_f64.powi(-k); let v = trilog_unit(z);
assert!(
v >= prev_tail,
"trilog_unit decreased approaching z = 1 at 1 - 1e-{k}: {prev_tail} -> {v}"
);
assert!(v <= ZETA3, "trilog_unit(1 - 1e-{k}) = {v} exceeds zeta(3)");
prev_tail = v;
}
const ZETA2: f64 = std::f64::consts::PI * std::f64::consts::PI / 6.0;
let delta = 1e-12_f64;
let gap = ZETA3 - trilog_unit(1.0 - delta);
let predicted = ZETA2 * delta;
let rel = (gap - predicted).abs() / predicted;
println!(
"\n zeta(3) - Li3(1-1e-12) = {gap:.6e}, predicted zeta(2)*delta = {predicted:.6e}, rel {rel:.2e}\n"
);
assert!(
rel < 1e-3,
"the approach to zeta(3) has the wrong slope: gap {gap:.6e} vs \
zeta(2)*delta {predicted:.6e} (rel {rel:.3e})"
);
}
#[test]
fn dilog_unit_matches_high_precision_reference() {
const REFERENCE: [(f64, f64); 8] = [
(0.05, 5.063_929_246_449_602_7e-2),
(0.25, 2.676_526_390_827_326_2e-1),
(0.5, 5.822_405_264_650_124_5e-1),
(0.75, 9.784_693_929_303_061_0e-1),
(0.9, 1.299_714_723_004_958_8),
(0.99, 1.588_625_448_076_375_3),
(0.999, 1.637_022_605_276_117_7),
(0.999_999_999, 1.644_934_045_124_961_2),
];
let mut worst = 0.0_f64;
for (z, want) in REFERENCE {
let rel = (dilog_unit(z) - want).abs() / want.abs();
worst = worst.max(rel);
}
println!("\n dilog_unit worst relative error: {worst:.3e}\n");
assert!(
worst < 4.0 * f64::EPSILON,
"dilog_unit is off by {worst:.3e} relative"
);
}
}