use std::sync::OnceLock;
use crate::math::boys::asymptotic_boundary;
#[path = "rys_tables.rs"]
mod rys_tables;
pub const MAX_RYS_ROOTS: usize = 13;
const GL_POINTS: usize = 128;
pub trait Scalar:
Copy
+ core::ops::Add<Output = Self>
+ core::ops::Sub<Output = Self>
+ core::ops::Mul<Output = Self>
+ core::ops::Div<Output = Self>
{
fn from_f64(x: f64) -> Self;
fn to_f64(self) -> f64;
}
impl Scalar for f64 {
#[inline]
fn from_f64(x: f64) -> Self {
x
}
#[inline]
fn to_f64(self) -> f64 {
self
}
}
pub fn rys_roots_weights(n: usize, t: f64, roots: &mut [f64], weights: &mut [f64]) {
debug_assert!(n >= 1, "Rys quadrature needs at least one root");
debug_assert!(n <= MAX_RYS_ROOTS, "n exceeds MAX_RYS_ROOTS");
debug_assert!(roots.len() >= n && weights.len() >= n, "output too short");
debug_assert!(t >= 0.0, "Rys parameter T must be >= 0");
if t >= asymptotic_boundary(2 * n - 1) {
laguerre_half_branch(n, t, roots, weights);
} else {
interp_finite(n, t, roots, weights);
}
}
pub fn rys_roots_weights_reference(n: usize, t: f64, roots: &mut [f64], weights: &mut [f64]) {
debug_assert!(n >= 1, "Rys quadrature needs at least one root");
debug_assert!(n <= MAX_RYS_ROOTS, "n exceeds MAX_RYS_ROOTS");
debug_assert!(roots.len() >= n && weights.len() >= n, "output too short");
debug_assert!(t >= 0.0, "Rys parameter T must be >= 0");
if t >= asymptotic_boundary(2 * n - 1) {
laguerre_half_branch(n, t, roots, weights);
} else {
finite_branch::<f64>(n, t, roots, weights);
}
}
pub fn rys_finite_reference(n: usize, t: f64, roots: &mut [f64], weights: &mut [f64]) {
debug_assert!(n >= 1, "Rys quadrature needs at least one root");
debug_assert!(n <= MAX_RYS_ROOTS, "n exceeds MAX_RYS_ROOTS");
debug_assert!(roots.len() >= n && weights.len() >= n, "output too short");
debug_assert!(t >= 0.0, "Rys parameter T must be >= 0");
finite_branch::<f64>(n, t, roots, weights);
}
fn interp_finite(n: usize, t: f64, roots: &mut [f64], weights: &mut [f64]) {
let t_hi = asymptotic_boundary(2 * n - 1);
let root_coeffs = rys_tables::RYS_ROOT_COEFFS[n - 1];
let weight_coeffs = rys_tables::RYS_WEIGHT_COEFFS[n - 1];
let n_sub = root_coeffs.len() / n;
let width = t_hi / n_sub as f64;
let mut s = (t / width) as usize;
if s >= n_sub {
s = n_sub - 1;
}
let a = s as f64 * width;
let b = a + width;
let xi = (2.0 * t - (a + b)) / (b - a);
let base = s * n;
for i in 0..n {
roots[i] = chebev(&root_coeffs[base + i], xi);
weights[i] = chebev(&weight_coeffs[base + i], xi);
}
}
#[inline]
fn chebev(c: &[f64; rys_tables::RYS_CHEB_DEGREE + 1], xi: f64) -> f64 {
let two_xi = 2.0 * xi;
let mut d = 0.0f64;
let mut dd = 0.0f64;
for &ck in c[1..].iter().rev() {
let sv = d;
d = two_xi * d - dd + ck;
dd = sv;
}
xi * d - dd + 0.5 * c[0]
}
fn finite_branch<S: Scalar>(n: usize, t: f64, roots: &mut [f64], weights: &mut [f64]) {
let (u_nodes, u_wts) = gauss_legendre_01();
let mut x = [0.0f64; GL_POINTS];
let mut w = [0.0f64; GL_POINTS];
for m in 0..GL_POINTS {
let u = u_nodes[m];
let xm = u * u;
x[m] = xm;
w[m] = u_wts[m] * (-t * xm).exp();
}
let mut alpha = [0.0f64; MAX_RYS_ROOTS];
let mut beta = [0.0f64; MAX_RYS_ROOTS];
stieltjes::<S>(n, &x, &w, &mut alpha, &mut beta);
golub_welsch(&alpha[..n], &beta[..n], roots, weights);
}
fn stieltjes<S: Scalar>(n: usize, x: &[f64], w: &[f64], alpha: &mut [f64], beta: &mut [f64]) {
let m = x.len();
let zero = S::from_f64(0.0);
let moments = |p: &[S]| {
let mut norm = zero;
let mut xnorm = zero;
for ((&xi, &wi), &pi) in x.iter().zip(w.iter()).zip(p.iter()).take(m) {
let wp = S::from_f64(wi) * pi * pi;
norm = norm + wp;
xnorm = xnorm + S::from_f64(xi) * wp;
}
(norm, xnorm)
};
let mut p_prev = [zero; GL_POINTS];
let mut p_curr = [zero; GL_POINTS];
for v in p_curr.iter_mut().take(m) {
*v = S::from_f64(1.0);
}
let (mut norm, xnorm) = moments(&p_curr); beta[0] = norm.to_f64();
alpha[0] = (xnorm / norm).to_f64();
for k in 1..n {
let a_km1 = S::from_f64(alpha[k - 1]);
let b_km1 = S::from_f64(beta[k - 1]);
let mut p_next = [zero; GL_POINTS];
for (((pn, &xi), &pc), &pp) in p_next
.iter_mut()
.zip(x.iter())
.zip(p_curr.iter())
.zip(p_prev.iter())
.take(m)
{
*pn = (S::from_f64(xi) - a_km1) * pc - b_km1 * pp;
}
let (nn, xn) = moments(&p_next); alpha[k] = (xn / nn).to_f64();
beta[k] = (nn / norm).to_f64();
norm = nn;
p_prev = p_curr;
p_curr = p_next;
}
}
fn gauss_legendre_01() -> &'static ([f64; GL_POINTS], [f64; GL_POINTS]) {
static RULE: OnceLock<([f64; GL_POINTS], [f64; GL_POINTS])> = OnceLock::new();
RULE.get_or_init(|| {
let n = GL_POINTS;
let mut d = [0.5f64; GL_POINTS];
let mut e = [0.0f64; GL_POINTS];
let mut z0 = [0.0f64; GL_POINTS];
z0[0] = 1.0;
for i in 1..n {
let l = i as f64;
let b = 0.25 * l * l / (4.0 * l * l - 1.0);
e[i - 1] = b.sqrt();
}
e[n - 1] = 0.0;
tridiagonal_ql(&mut d, &mut e, &mut z0);
let mut idx: Vec<usize> = (0..n).collect();
idx.sort_by(|&i, &j| d[i].partial_cmp(&d[j]).unwrap());
let mut nodes = [0.0f64; GL_POINTS];
let mut wts = [0.0f64; GL_POINTS];
for (out, &i) in idx.iter().enumerate() {
nodes[out] = d[i];
wts[out] = z0[i] * z0[i];
}
(nodes, wts)
})
}
fn golub_welsch(alpha: &[f64], beta: &[f64], roots: &mut [f64], weights: &mut [f64]) {
let n = alpha.len();
let mu0 = beta[0];
if n == 1 {
roots[0] = alpha[0];
weights[0] = mu0;
return;
}
let mut d = [0.0f64; MAX_RYS_ROOTS];
let mut e = [0.0f64; MAX_RYS_ROOTS];
let mut z0 = [0.0f64; MAX_RYS_ROOTS];
for i in 0..n {
d[i] = alpha[i];
z0[i] = 0.0;
}
z0[0] = 1.0;
for i in 1..n {
e[i - 1] = beta[i].max(0.0).sqrt();
}
e[n - 1] = 0.0;
tridiagonal_ql(&mut d[..n], &mut e[..n], &mut z0[..n]);
let mut idx = [0usize; MAX_RYS_ROOTS];
for (i, s) in idx.iter_mut().enumerate().take(n) {
*s = i;
}
idx[..n].sort_by(|&i, &j| {
d[i].partial_cmp(&d[j])
.unwrap_or(core::cmp::Ordering::Equal)
});
for (out, &i) in idx[..n].iter().enumerate() {
roots[out] = d[i];
weights[out] = mu0 * z0[i] * z0[i];
}
}
fn tridiagonal_ql(d: &mut [f64], e: &mut [f64], z0: &mut [f64]) {
let n = d.len();
if n <= 1 {
return;
}
const MAX_ITER: usize = 60;
for l in 0..n {
let mut iter = 0usize;
loop {
let mut m = l;
while m < n - 1 {
let dd = d[m].abs() + d[m + 1].abs();
if e[m].abs() <= f64::EPSILON * dd {
break;
}
m += 1;
}
if m == l {
break; }
iter += 1;
debug_assert!(iter <= MAX_ITER, "tridiagonal QL failed to converge");
if iter > MAX_ITER {
break;
}
let mut g = (d[l + 1] - d[l]) / (2.0 * e[l]);
let r = g.hypot(1.0);
g = d[m] - d[l] + e[l] / (g + r.copysign(g));
let mut s = 1.0;
let mut c = 1.0;
let mut p = 0.0;
let mut underflow = false;
let mut i = m;
while i > l {
i -= 1;
let mut f = s * e[i];
let b = c * e[i];
let r2 = f.hypot(g);
e[i + 1] = r2;
if r2 == 0.0 {
d[i + 1] -= p;
e[m] = 0.0;
underflow = true;
break;
}
s = f / r2;
c = g / r2;
let dd = d[i + 1] - p;
let r3 = (d[i] - dd) * s + 2.0 * c * b;
p = s * r3;
d[i + 1] = dd + p;
g = c * r3 - b;
f = z0[i + 1];
z0[i + 1] = s * z0[i] + c * f;
z0[i] = c * z0[i] - s * f;
}
if underflow {
continue;
}
d[l] -= p;
e[l] = g;
e[m] = 0.0;
}
}
}
fn laguerre_half_branch(n: usize, t: f64, roots: &mut [f64], weights: &mut [f64]) {
let (xi, om) = laguerre_half_nodes(n);
let inv_t = 1.0 / t;
let scale = 0.5 / t.sqrt();
for i in 0..n {
roots[i] = xi[i] * inv_t;
weights[i] = om[i] * scale;
}
}
fn laguerre_half_nodes(n: usize) -> (&'static [f64], &'static [f64]) {
#[allow(clippy::type_complexity)]
static CACHE: OnceLock<([Vec<f64>; MAX_RYS_ROOTS], [Vec<f64>; MAX_RYS_ROOTS])> =
OnceLock::new();
let (nodes, wts) = CACHE.get_or_init(|| {
let mut all_nodes: [Vec<f64>; MAX_RYS_ROOTS] = Default::default();
let mut all_wts: [Vec<f64>; MAX_RYS_ROOTS] = Default::default();
for m in 1..=MAX_RYS_ROOTS {
let alpha_param = -0.5;
let mut a = [0.0f64; MAX_RYS_ROOTS];
let mut b = [0.0f64; MAX_RYS_ROOTS];
for k in 0..m {
let kf = k as f64;
a[k] = 2.0 * kf + alpha_param + 1.0;
b[k] = kf * (kf + alpha_param);
}
b[0] = core::f64::consts::PI.sqrt();
let mut xi = [0.0f64; MAX_RYS_ROOTS];
let mut om = [0.0f64; MAX_RYS_ROOTS];
golub_welsch(&a[..m], &b[..m], &mut xi[..m], &mut om[..m]);
all_nodes[m - 1] = xi[..m].to_vec();
all_wts[m - 1] = om[..m].to_vec();
}
(all_nodes, all_wts)
});
(&nodes[n - 1], &wts[n - 1])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::boys::boys_array;
fn moment_residual(n: usize, t: f64) -> f64 {
let mut roots = [0.0; MAX_RYS_ROOTS];
let mut wts = [0.0; MAX_RYS_ROOTS];
rys_roots_weights(n, t, &mut roots, &mut wts);
let mut fm = [0.0; 2 * MAX_RYS_ROOTS];
boys_array(2 * n - 1, t, &mut fm[..2 * n]);
let mut worst = 0.0_f64;
for (k, &fk) in fm.iter().enumerate().take(2 * n) {
let mut q = 0.0;
for (&wi, &ri) in wts.iter().zip(roots.iter()).take(n) {
q += wi * ri.powi(k as i32);
}
let rel = (q - fk).abs() / fk.abs().max(1e-300);
worst = worst.max(rel);
}
worst
}
#[test]
fn nodes_in_unit_interval_and_weights_positive() {
for &t in &[0.0, 1.0, 13.0, 80.0, 500.0] {
for n in 1..=MAX_RYS_ROOTS {
let mut roots = [0.0; MAX_RYS_ROOTS];
let mut wts = [0.0; MAX_RYS_ROOTS];
rys_roots_weights(n, t, &mut roots, &mut wts);
for i in 0..n {
assert!(
roots[i] > 0.0 && roots[i] < 1.0,
"node {i} out of (0,1) at n={n} T={t}: {}",
roots[i]
);
assert!(
wts[i] > 0.0,
"weight {i} not positive at n={n} T={t}: {}",
wts[i]
);
if i > 0 {
assert!(
roots[i] > roots[i - 1],
"nodes not ascending at n={n} T={t}"
);
}
}
}
}
}
#[test]
fn reproduces_boys_moments_low_order() {
for &t in &[0.0, 1e-4, 0.5, 2.0, 7.0, 20.0] {
for n in 1..=6 {
let r = moment_residual(n, t);
assert!(r < 1e-11, "moment residual {r:e} at n={n} T={t}");
}
}
}
#[test]
fn reproduces_boys_moments_large_t_branch() {
for &t in &[120.0, 300.0, 1000.0] {
for n in 1..=MAX_RYS_ROOTS {
let r = moment_residual(n, t);
assert!(r < 1e-10, "large-T moment residual {r:e} at n={n} T={t}");
}
}
}
#[test]
fn one_point_quadrature_is_mean_and_mass() {
for &t in &[0.3, 5.0] {
let mut roots = [0.0; 1];
let mut wts = [0.0; 1];
rys_roots_weights(1, t, &mut roots, &mut wts);
let mut fm = [0.0; 2];
boys_array(1, t, &mut fm);
assert!((roots[0] - fm[1] / fm[0]).abs() < 1e-14);
assert!((wts[0] - fm[0]).abs() < 1e-14);
}
}
#[test]
fn interp_matches_reference_full_grid() {
let mut worst = 0.0_f64;
let mut at = (0usize, 0.0f64);
for n in 1..=MAX_RYS_ROOTS {
let t_hi = asymptotic_boundary(2 * n - 1);
let steps = 600usize;
for k in 0..steps {
let t = t_hi * (k as f64) / (steps as f64);
let mut rp = [0.0; MAX_RYS_ROOTS];
let mut wp = [0.0; MAX_RYS_ROOTS];
let mut rr = [0.0; MAX_RYS_ROOTS];
let mut wr = [0.0; MAX_RYS_ROOTS];
rys_roots_weights(n, t, &mut rp, &mut wp);
rys_roots_weights_reference(n, t, &mut rr, &mut wr);
for i in 0..n {
let e = (rp[i] - rr[i]).abs().max((wp[i] - wr[i]).abs());
if e > worst {
worst = e;
at = (n, t);
}
}
}
}
eprintln!(
"interp vs Stieltjes reference: worst abs err {worst:.3e} at n={} T={:.3}",
at.0, at.1
);
assert!(worst < 1e-13, "interp vs reference {worst:.3e} at {at:?}");
}
#[test]
fn large_t_branch_is_bit_identical_to_reference() {
for n in 1..=MAX_RYS_ROOTS {
let seam = asymptotic_boundary(2 * n - 1);
for &t in &[seam, seam + 1.0, 120.0, 300.0, 1000.0] {
let mut rp = [0.0; MAX_RYS_ROOTS];
let mut wp = [0.0; MAX_RYS_ROOTS];
let mut rr = [0.0; MAX_RYS_ROOTS];
let mut wr = [0.0; MAX_RYS_ROOTS];
rys_roots_weights(n, t, &mut rp, &mut wp);
rys_roots_weights_reference(n, t, &mut rr, &mut wr);
for i in 0..n {
assert_eq!(rp[i], rr[i], "root {i} differs at n={n} T={t}");
assert_eq!(wp[i], wr[i], "weight {i} differs at n={n} T={t}");
}
}
}
}
#[test]
fn production_invariants_dense_grid() {
for n in 1..=MAX_RYS_ROOTS {
let t_hi = asymptotic_boundary(2 * n - 1);
for k in 0..=500 {
let t = 1.3 * t_hi * (k as f64) / 500.0;
let mut roots = [0.0; MAX_RYS_ROOTS];
let mut wts = [0.0; MAX_RYS_ROOTS];
rys_roots_weights(n, t, &mut roots, &mut wts);
for i in 0..n {
assert!(
roots[i] > 0.0 && roots[i] < 1.0,
"node {i} out of (0,1) at n={n} T={t}: {}",
roots[i]
);
assert!(wts[i] > 0.0, "weight {i} ≤ 0 at n={n} T={t}: {}", wts[i]);
if i > 0 {
assert!(roots[i] > roots[i - 1], "nodes not ascending n={n} T={t}");
}
}
}
}
}
#[test]
fn moment_property_holds_full_grid() {
let mut worst = 0.0_f64;
let mut at = (0usize, 0.0f64);
for n in 1..=MAX_RYS_ROOTS {
let t_hi = asymptotic_boundary(2 * n - 1);
for k in 0..=260 {
let t = 1.2 * t_hi * (k as f64) / 260.0;
let r = moment_residual(n, t);
if r > worst {
worst = r;
at = (n, t);
}
}
}
eprintln!(
"production moment (★) worst rel residual {worst:.3e} at n={} T={:.3}",
at.0, at.1
);
assert!(worst < 1e-9, "moment residual {worst:.3e} at {at:?}");
}
}