#[must_use]
pub fn asymptotic_boundary(m: usize) -> f64 {
33.0 + 2.5 * m as f64
}
const BOYS_MAX_M: usize = 24;
const BOYS_K: usize = 8;
const BOYS_STRIDE: usize = BOYS_MAX_M + BOYS_K + 1;
const BOYS_ROW: usize = BOYS_STRIDE + 1;
const BOYS_H: f64 = 0.1;
const INV_FACT: [f64; BOYS_K + 1] = {
let mut a = [1.0f64; BOYS_K + 1]; let mut k = 1;
while k <= BOYS_K {
a[k] = a[k - 1] / k as f64; k += 1;
}
a
};
const INV_ODD: [f64; BOYS_MAX_M + 1] = {
let mut a = [1.0f64; BOYS_MAX_M + 1];
let mut m = 1;
while m <= BOYS_MAX_M {
a[m] = 1.0 / (2 * m - 1) as f64;
m += 1;
}
a
};
const BOYS_TABLE_TMAX: f64 = 33.0 + 2.5 * BOYS_MAX_M as f64;
const BOYS_NGRID: usize = (BOYS_TABLE_TMAX / BOYS_H) as usize;
static BOYS_TABLE: std::sync::OnceLock<Vec<f64>> = std::sync::OnceLock::new();
fn build_boys_table() -> Vec<f64> {
let mut table = vec![0.0; (BOYS_NGRID + 1) * BOYS_ROW];
for i in 0..=BOYS_NGRID {
let t0 = i as f64 * BOYS_H;
let row = &mut table[i * BOYS_ROW..(i + 1) * BOYS_ROW];
boys_reference(BOYS_STRIDE - 1, t0, &mut row[..BOYS_STRIDE]);
row[BOYS_STRIDE] = (-t0).exp();
}
table
}
pub fn boys_array(m_max: usize, t: f64, out: &mut [f64]) {
debug_assert!(out.len() > m_max, "output buffer too short for m_max");
debug_assert!(t >= 0.0, "Boys function requires T >= 0");
if t < BOYS_TABLE_TMAX && m_max + BOYS_K < BOYS_STRIDE {
let table = BOYS_TABLE.get_or_init(build_boys_table);
let i = (t * (1.0 / BOYS_H)).round() as usize; let neg_d = i as f64 * BOYS_H - t; let row = i * BOYS_ROW;
let base = row + m_max;
let mut top = table[base + BOYS_K] * INV_FACT[BOYS_K];
for k in (0..BOYS_K).rev() {
top = top * neg_d + table[base + k] * INV_FACT[k];
}
out[m_max] = top;
if m_max > 0 {
let mut ep = INV_FACT[BOYS_K];
for k in (0..BOYS_K).rev() {
ep = ep * neg_d + INV_FACT[k];
}
let et = table[row + BOYS_STRIDE] * ep;
let two_t = 2.0 * t;
let mut m = m_max;
while m > 0 {
out[m - 1] = (two_t * out[m] + et) * INV_ODD[m];
m -= 1;
}
}
} else {
boys_reference(m_max, t, out);
}
}
pub fn boys_array2(m_max: usize, t0: f64, t1: f64, out0: &mut [f64], out1: &mut [f64]) {
debug_assert!(out0.len() > m_max && out1.len() > m_max);
debug_assert!(t0 >= 0.0 && t1 >= 0.0, "Boys function requires T >= 0");
if !(t0 < BOYS_TABLE_TMAX && t1 < BOYS_TABLE_TMAX && m_max + BOYS_K < BOYS_STRIDE) {
boys_array(m_max, t0, out0);
boys_array(m_max, t1, out1);
return;
}
let table = BOYS_TABLE.get_or_init(build_boys_table);
let i0 = (t0 * (1.0 / BOYS_H)).round() as usize;
let i1 = (t1 * (1.0 / BOYS_H)).round() as usize;
let nd0 = i0 as f64 * BOYS_H - t0;
let nd1 = i1 as f64 * BOYS_H - t1;
let row0 = i0 * BOYS_ROW;
let row1 = i1 * BOYS_ROW;
let base0 = row0 + m_max;
let base1 = row1 + m_max;
let mut top0 = table[base0 + BOYS_K] * INV_FACT[BOYS_K];
let mut top1 = table[base1 + BOYS_K] * INV_FACT[BOYS_K];
for k in (0..BOYS_K).rev() {
top0 = top0 * nd0 + table[base0 + k] * INV_FACT[k];
top1 = top1 * nd1 + table[base1 + k] * INV_FACT[k];
}
out0[m_max] = top0;
out1[m_max] = top1;
if m_max > 0 {
let mut ep0 = INV_FACT[BOYS_K];
let mut ep1 = INV_FACT[BOYS_K];
for k in (0..BOYS_K).rev() {
ep0 = ep0 * nd0 + INV_FACT[k];
ep1 = ep1 * nd1 + INV_FACT[k];
}
let et0 = table[row0 + BOYS_STRIDE] * ep0;
let et1 = table[row1 + BOYS_STRIDE] * ep1;
let two_t0 = 2.0 * t0;
let two_t1 = 2.0 * t1;
let mut m = m_max;
while m > 0 {
out0[m - 1] = (two_t0 * out0[m] + et0) * INV_ODD[m];
out1[m - 1] = (two_t1 * out1[m] + et1) * INV_ODD[m];
m -= 1;
}
}
}
pub fn boys_array4(m_max: usize, t: [f64; 4], out: &mut [[f64; 4]]) {
debug_assert!(out.len() > m_max, "output buffer too short for m_max");
debug_assert!(t.iter().all(|&x| x >= 0.0), "Boys function requires T >= 0");
if !(t.iter().all(|&x| x < BOYS_TABLE_TMAX) && m_max + BOYS_K < BOYS_STRIDE) {
let mut row = [0.0f64; BOYS_STRIDE];
for lane in 0..4 {
boys_array(m_max, t[lane], &mut row[..=m_max]);
for m in 0..=m_max {
out[m][lane] = row[m];
}
}
return;
}
let table = BOYS_TABLE.get_or_init(build_boys_table);
let mut row = [0usize; 4];
let mut base = [0usize; 4];
let mut nd = [0.0f64; 4];
for lane in 0..4 {
let i = (t[lane] * (1.0 / BOYS_H)).round() as usize;
nd[lane] = i as f64 * BOYS_H - t[lane];
row[lane] = i * BOYS_ROW;
base[lane] = row[lane] + m_max;
}
let mut top = [0.0f64; 4];
for lane in 0..4 {
top[lane] = table[base[lane] + BOYS_K] * INV_FACT[BOYS_K];
}
for k in (0..BOYS_K).rev() {
for lane in 0..4 {
top[lane] = top[lane] * nd[lane] + table[base[lane] + k] * INV_FACT[k];
}
}
out[m_max] = top;
if m_max > 0 {
let mut ep = [INV_FACT[BOYS_K]; 4];
for k in (0..BOYS_K).rev() {
for lane in 0..4 {
ep[lane] = ep[lane] * nd[lane] + INV_FACT[k];
}
}
let mut et = [0.0f64; 4];
let mut two_t = [0.0f64; 4];
for lane in 0..4 {
et[lane] = table[row[lane] + BOYS_STRIDE] * ep[lane];
two_t[lane] = 2.0 * t[lane];
}
let mut m = m_max;
while m > 0 {
let cur = out[m];
let mut lower = [0.0f64; 4];
for lane in 0..4 {
lower[lane] = (two_t[lane] * cur[lane] + et[lane]) * INV_ODD[m];
}
out[m - 1] = lower;
m -= 1;
}
}
}
pub fn boys_array_erf(m_max: usize, t: f64, rho: f64, omega: f64, out: &mut [f64]) {
debug_assert!(rho > 0.0, "attenuated Boys requires rho > 0");
debug_assert!(
omega.is_finite() && omega > 0.0,
"attenuated Boys requires a finite omega > 0"
);
let s = omega * omega / (rho + omega * omega);
boys_array(m_max, s * t, out);
let mut factor = s.sqrt(); for v in out[..=m_max].iter_mut() {
*v *= factor;
factor *= s;
}
}
pub(crate) fn boys_reference(m_max: usize, t: f64, out: &mut [f64]) {
debug_assert!(out.len() > m_max, "output buffer too short for m_max");
debug_assert!(t >= 0.0, "Boys function requires T >= 0");
let et = (-t).exp();
out[m_max] = if t < asymptotic_boundary(m_max) {
boys_series(m_max, t, et)
} else {
boys_asymptotic(m_max, t)
};
let mut m = m_max;
while m > 0 {
out[m - 1] = (2.0 * t * out[m] + et) / ((2 * m - 1) as f64);
m -= 1;
}
}
#[must_use]
pub fn boys(m: usize, t: f64) -> f64 {
let mut buf = vec![0.0; m + 1];
boys_array(m, t, &mut buf);
buf[m]
}
fn boys_series(m: usize, t: f64, et: f64) -> f64 {
let two_t = 2.0 * t;
let mut term = 1.0 / (2 * m + 1) as f64;
let mut sum = term;
let mut k = 1usize;
loop {
term *= two_t / (2 * m + 2 * k + 1) as f64;
sum += term;
if term <= sum * 1e-17 {
break;
}
k += 1;
debug_assert!(k < 1000, "Boys series failed to converge");
if k >= 1000 {
break;
}
}
et * sum
}
fn boys_asymptotic(m: usize, t: f64) -> f64 {
let mut val = 0.5 * (std::f64::consts::PI / t).sqrt();
let two_t = 2.0 * t;
for k in 1..=m {
val *= (2 * k - 1) as f64 / two_t;
}
val
}
#[cfg(test)]
mod tests {
use super::*;
fn boys_quadrature_all(t: f64) -> [f64; 9] {
let n: usize = 1_000_000; let h = 1.0 / n as f64;
let term = |m: usize, x: f64, et: f64| x.powi(2 * m as i32) * et;
let (et0, et1) = ((-t * 0.0 * 0.0).exp(), (-t * 1.0 * 1.0).exp());
let mut sum: [f64; 9] = std::array::from_fn(|m| term(m, 0.0, et0) + term(m, 1.0, et1));
let mut comp = [0.0f64; 9]; for i in 1..n {
let x = i as f64 * h;
let et = (-t * x * x).exp();
let weight = if i % 2 == 0 { 2.0 } else { 4.0 };
for m in 0..9 {
let y = weight * term(m, x, et) - comp[m];
let new = sum[m] + y;
comp[m] = (new - sum[m]) - y;
sum[m] = new;
}
}
std::array::from_fn(|m| sum[m] * h / 3.0)
}
#[test]
fn value_at_zero_is_reciprocal_odd() {
let mut out = [0.0; 9];
boys_array(8, 0.0, &mut out);
for (m, &fm) in out.iter().enumerate() {
let expected = 1.0 / (2 * m + 1) as f64;
assert!((fm - expected).abs() < 1e-15, "m={m}: {fm} vs {expected}");
}
}
#[test]
fn table_matches_reference_across_grid() {
let mut worst = 0.0f64;
let mut tab = [0.0f64; BOYS_MAX_M + 1];
let mut refv = [0.0f64; BOYS_MAX_M + 1];
let mut t = 0.0f64;
while t <= BOYS_TABLE_TMAX + 20.0 {
for m_max in [0usize, 1, 2, 4, 8, 12, 16, 20, 24] {
boys_array(m_max, t, &mut tab[..=m_max]);
boys_reference(m_max, t, &mut refv[..=m_max]);
for m in 0..=m_max {
let rel = (tab[m] - refv[m]).abs() / refv[m].abs().max(1e-300);
worst = worst.max(rel);
assert!(
rel < 1e-12,
"m={m} T={t}: table {} vs ref {} (rel {rel:e})",
tab[m],
refv[m]
);
}
}
t += 0.037;
}
eprintln!("worst boys table-vs-reference rel error: {worst:e}");
assert!(
worst < 1e-12,
"worst table-vs-reference rel error {worst:e}"
);
}
#[test]
fn f0_at_one_matches_known_value() {
assert!((boys(0, 1.0) - 0.746_824_132_812_427_1).abs() < 1e-14);
}
#[test]
fn matches_quadrature_across_regimes() {
let ts = [
0.0, 1e-6, 0.01, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 20.0, 30.0, 34.9, 35.0, 35.1, 50.0,
100.0,
];
for &t in &ts {
let mut out = [0.0; 9];
boys_array(8, t, &mut out);
let quad = boys_quadrature_all(t);
for (m, &fm) in out.iter().enumerate() {
let q = quad[m];
let err = (fm - q).abs() / q.abs().max(1e-300);
assert!(err < 1e-9, "m={m} T={t}: boys={fm} quad={q} relerr={err:e}");
}
}
}
#[test]
fn satisfies_upward_recurrence() {
for &t in &[0.3, 3.0, 15.0, 37.0, 80.0] {
let mut out = [0.0; 9];
boys_array(8, t, &mut out);
let et = (-t).exp();
for m in 0..8 {
let lhs = (2 * m + 1) as f64 * out[m];
let rhs = 2.0 * t * out[m + 1] + et;
assert!(
(lhs - rhs).abs() < 1e-12 * lhs.abs().max(1.0),
"T={t} m={m}"
);
}
}
}
fn boys_erf_quadrature(m: usize, t: f64, rho: f64, omega: f64) -> f64 {
let s = omega * omega / (rho + omega * omega);
let upper = s.sqrt();
let n: usize = 200_000; let h = upper / n as f64;
let f = |x: f64| x.powi(2 * m as i32) * (-t * x * x).exp();
let mut sum = f(0.0) + f(upper);
for i in 1..n {
let w = if i % 2 == 0 { 2.0 } else { 4.0 };
sum += w * f(i as f64 * h);
}
sum * h / 3.0
}
#[test]
fn erf_matches_truncated_quadrature() {
for &t in &[0.0, 0.3, 2.5, 12.0, 40.0] {
for &omega in &[0.2, 0.5, 1.0, 4.0] {
let rho = 0.9;
let mut out = [0.0; 7];
boys_array_erf(6, t, rho, omega, &mut out);
for (m, &fm) in out.iter().enumerate() {
let q = boys_erf_quadrature(m, t, rho, omega);
let err = (fm - q).abs() / q.abs().max(1e-300);
assert!(err < 1e-9, "m={m} T={t} ω={omega}: {fm} vs {q} ({err:e})");
}
}
}
}
#[test]
fn erf_limits_and_bounds() {
let rho = 1.3;
for &t in &[0.0, 1.0, 20.0] {
let mut plain = [0.0; 9];
boys_array(8, t, &mut plain);
let mut large = [0.0; 9];
boys_array_erf(8, t, rho, 1e8, &mut large);
for m in 0..=8 {
assert!(
(large[m] - plain[m]).abs() < 1e-7 * plain[m],
"ω→∞ m={m} T={t}"
);
}
let mut att = [0.0; 9];
boys_array_erf(8, t, rho, 0.5, &mut att);
for m in 0..=8 {
assert!(att[m] > 0.0 && att[m] < plain[m], "bounds m={m} T={t}");
}
}
}
#[test]
fn erf_satisfies_recurrence() {
let (rho, omega) = (0.8, 0.6);
let s = omega * omega / (rho + omega * omega);
for &t in &[0.4, 3.0, 25.0] {
let mut out = [0.0; 9];
boys_array_erf(8, t, rho, omega, &mut out);
let est = (-s * t).exp();
for m in 0..8 {
let lhs = (2 * m + 1) as f64 * out[m];
let rhs = 2.0 * t * out[m + 1] + s.powf(m as f64 + 0.5) * est;
assert!(
(lhs - rhs).abs() < 1e-12 * lhs.abs().max(1e-300),
"T={t} m={m}"
);
}
}
}
#[test]
fn monotonic_decreasing_in_m() {
for &t in &[0.0, 1.0, 25.0, 60.0] {
let mut out = [0.0; 9];
boys_array(8, t, &mut out);
for m in 0..8 {
assert!(out[m] > out[m + 1], "T={t} m={m}");
}
}
}
#[test]
fn large_t_matches_asymptotic_closed_form() {
let odd_df = |m: usize| -> f64 {
let mut p = 1.0;
let mut i = 2 * m as i64 - 1;
while i > 1 {
p *= i as f64;
i -= 2;
}
p
};
let sqrt_pi = std::f64::consts::PI.sqrt();
for &t in &[1e3_f64, 1e4, 1e6, 1e8] {
assert_eq!((-t).exp(), 0.0, "expected e^-T underflow at T={t}");
let mut out = [0.0; 25];
boys_array(24, t, &mut out);
for (m, &got) in out.iter().enumerate() {
let expect =
odd_df(m) / 2f64.powi(m as i32 + 1) * sqrt_pi * t.powf(-(m as f64 + 0.5));
let rel = (got - expect).abs() / expect.abs().max(1e-300);
assert!(rel < 1e-12, "F_{m}({t}) = {got} vs {expect} (rel {rel:e})");
}
}
}
}