use std::f64::consts::PI;
use std::sync::OnceLock;
use crate::math::am::cart_components;
use crate::math::norm::double_factorial;
use crate::integrals::{place_block, to_func_1e};
use crate::shell::{Basis, Shell};
use crate::IntegralError;
pub const MAX_ECP_GRAD_L: usize = 5;
const MAX_ECP_GRAD_PROJ: usize = 5;
const ARG_SCREEN: f64 = -120.0;
const WINDOW_LOG: f64 = 80.0;
const N_QUAD: usize = 128;
const K_TINY: f64 = 1e-12;
const FOUR_PI: f64 = 4.0 * PI;
#[derive(Debug, Clone, PartialEq)]
pub struct EcpPrimitive {
pub n: i32,
pub zeta: f64,
pub coef: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Ecp {
pub atom: usize,
pub n_core: u32,
pub max_l: usize,
pub local: Vec<EcpPrimitive>,
pub semilocal: Vec<Vec<EcpPrimitive>>,
}
impl Basis {
#[must_use]
pub fn ecp(&self, ecps: &[Ecp]) -> Vec<f64> {
let n = self.nao();
let offs = self.offsets();
let atoms = self.atoms();
let shells = self.shells();
let lsh = shells.iter().map(Shell::l).max().unwrap_or(0);
let lproj = ecps.iter().map(|e| e.semilocal.len()).max().unwrap_or(0);
let lam_max = (2 * lsh).max(lproj.saturating_sub(1) + lsh);
let rsh = Rsh::new(lam_max);
let mut mat = vec![0.0; n * n];
for (si, sa) in shells.iter().enumerate() {
for (sj, sb) in shells.iter().enumerate().take(si + 1) {
let mut block = vec![0.0; sa.n_cart() * sb.n_cart()];
for ecp in ecps {
ecp_pair_into(sa, sb, atoms[ecp.atom], ecp, &rsh, &mut block);
}
let fa = to_func_1e(block, sa, sb);
let (nfa, nfb) = (sa.n_func(), sb.n_func());
place_block(&mut mat, n, offs[si], offs[sj], &fa, nfb);
if si != sj {
let mut ft = vec![0.0; nfa * nfb];
for i in 0..nfa {
for j in 0..nfb {
ft[j * nfa + i] = fa[i * nfb + j];
}
}
place_block(&mut mat, n, offs[sj], offs[si], &ft, nfa);
}
}
}
mat
}
pub fn ecp_grad_contract(
&self,
ecps: &[Ecp],
gamma: &[f64],
) -> Result<Vec<[f64; 3]>, IntegralError> {
let shells = self.shells();
for s in shells {
if s.l() > MAX_ECP_GRAD_L {
return Err(IntegralError::AngularMomentumTooHighForGradient {
l: s.l(),
max: MAX_ECP_GRAD_L,
});
}
}
let n = self.nao();
if gamma.len() != n * n {
return Err(IntegralError::GammaLengthMismatch {
expected: n * n,
got: gamma.len(),
});
}
let atoms = self.atoms();
let mut grad = vec![[0.0; 3]; atoms.len()];
for e in ecps {
assert!(
e.atom < atoms.len(),
"Ecp::atom index {} out of range ({} atoms)",
e.atom,
atoms.len()
);
assert!(
e.semilocal.len() <= MAX_ECP_GRAD_PROJ,
"ecp_grad_contract supports up to {} projector channels \
(l = 0..=4, s…g), got {}",
MAX_ECP_GRAD_PROJ,
e.semilocal.len()
);
}
if ecps.is_empty() {
return Ok(grad);
}
let offs = self.offsets();
let shell_atom = self.shell_atom();
let lsh = shells.iter().map(Shell::l).max().unwrap_or(0);
let lproj = ecps.iter().map(|e| e.semilocal.len()).max().unwrap_or(0);
let lam_max = (2 * (lsh + 1)).max(lproj.saturating_sub(1) + lsh + 1);
let rsh = Rsh::new(lam_max);
let wts: Vec<Vec<f64>> = shells
.iter()
.map(|s| (0..s.n_prim()).map(|i| s.primitive_coeff(i)).collect())
.collect();
for (si, sa) in shells.iter().enumerate() {
let la = sa.l();
let a = sa.center();
let (naf, nca) = (sa.n_func(), sa.n_cart());
let comps_a = cart_components(la);
let comps_up = cart_components(la + 1);
let comps_dn = if la > 0 {
cart_components(la - 1)
} else {
Vec::new()
};
let wup: Vec<f64> = sa
.exponents()
.iter()
.zip(&wts[si])
.map(|(&e, &w)| 2.0 * e * w)
.collect();
for (sj, sb) in shells.iter().enumerate() {
let (nbf, ncb) = (sb.n_func(), sb.n_cart());
let mut gs = vec![0.0; naf * nbf];
let mut any = false;
for i in 0..naf {
for j in 0..nbf {
let v = gamma[(offs[si] + i) * n + offs[sj] + j]
+ gamma[(offs[sj] + j) * n + offs[si] + i];
any |= v != 0.0;
gs[i * nbf + j] = v;
}
}
if !any {
continue;
}
let b = sb.center();
for ecp in ecps {
let c = atoms[ecp.atom];
let cav = [a[0] - c[0], a[1] - c[1], a[2] - c[2]];
let cbv = [b[0] - c[0], b[1] - c[1], b[2] - c[2]];
let side_b = EcpSide {
l: sb.l(),
cv: cbv,
exps: sb.exponents(),
wts: &wts[sj],
};
let mut bup = vec![0.0; comps_up.len() * ncb];
ecp_sides_into(
&EcpSide {
l: la + 1,
cv: cav,
exps: sa.exponents(),
wts: &wup,
},
&side_b,
ecp,
&rsh,
&mut bup,
);
let bdn = if la > 0 {
let mut bd = vec![0.0; comps_dn.len() * ncb];
ecp_sides_into(
&EcpSide {
l: la - 1,
cv: cav,
exps: sa.exponents(),
wts: &wts[si],
},
&side_b,
ecp,
&rsh,
&mut bd,
);
bd
} else {
Vec::new()
};
for dir in 0..3 {
let mut dcart = vec![0.0; nca * ncb];
for (ia, pa) in comps_a.iter().enumerate() {
let mut up = *pa;
up[dir] += 1;
let iu = comps_up
.iter()
.position(|p| p == &up)
.expect("raised Cartesian component");
let idn = (pa[dir] > 0).then(|| {
let mut dn = *pa;
dn[dir] -= 1;
comps_dn
.iter()
.position(|p| p == &dn)
.expect("lowered Cartesian component")
});
for ib in 0..ncb {
let mut v = bup[iu * ncb + ib];
if let Some(id) = idn {
v -= pa[dir] as f64 * bdn[id * ncb + ib];
}
dcart[ia * ncb + ib] = v;
}
}
let df = to_func_1e(dcart, sa, sb);
let v: f64 = df.iter().zip(&gs).map(|(x, g)| x * g).sum();
grad[shell_atom[si]][dir] += v;
grad[ecp.atom][dir] -= v;
}
}
}
}
Ok(grad)
}
}
struct EcpSide<'a> {
l: usize,
cv: [f64; 3],
exps: &'a [f64],
wts: &'a [f64],
}
fn ecp_pair_into(sa: &Shell, sb: &Shell, c: [f64; 3], ecp: &Ecp, rsh: &Rsh, block: &mut [f64]) {
let (a, b) = (sa.center(), sb.center());
let wa: Vec<f64> = (0..sa.n_prim()).map(|i| sa.primitive_coeff(i)).collect();
let wb: Vec<f64> = (0..sb.n_prim()).map(|i| sb.primitive_coeff(i)).collect();
let side_a = EcpSide {
l: sa.l(),
cv: [a[0] - c[0], a[1] - c[1], a[2] - c[2]],
exps: sa.exponents(),
wts: &wa,
};
let side_b = EcpSide {
l: sb.l(),
cv: [b[0] - c[0], b[1] - c[1], b[2] - c[2]],
exps: sb.exponents(),
wts: &wb,
};
ecp_sides_into(&side_a, &side_b, ecp, rsh, block);
}
fn ecp_sides_into(sa: &EcpSide<'_>, sb: &EcpSide<'_>, ecp: &Ecp, rsh: &Rsh, block: &mut [f64]) {
type1_into(sa, sb, &ecp.local, rsh, block);
for (l, prims) in ecp.semilocal.iter().enumerate() {
if !prims.is_empty() {
type2_into(l, prims, sa, sb, rsh, block);
}
}
}
fn type1_into(
sa: &EcpSide<'_>,
sb: &EcpSide<'_>,
prims: &[EcpPrimitive],
rsh: &Rsh,
block: &mut [f64],
) {
if prims.is_empty() {
return;
}
let (la, lb) = (sa.l, sb.l);
let (cav, cbv) = (sa.cv, sb.cv);
let ltot = la + lb;
let d = ltot + 1;
let comps_a = cart_components(la);
let comps_b = cart_components(lb);
let nb = comps_b.len();
let ca2 = dot(cav, cav);
let cb2 = dot(cbv, cbv);
let mut mb = vec![0.0; d];
for (pi, &aa) in sa.exps.iter().enumerate() {
let wa = sa.wts[pi];
for (pj, &ab) in sb.exps.iter().enumerate() {
let w = wa * sb.wts[pj];
let arg0 = -aa * ca2 - ab * cb2;
let kvec = [
2.0 * (aa * cav[0] + ab * cbv[0]),
2.0 * (aa * cav[1] + ab * cbv[1]),
2.0 * (aa * cav[2] + ab * cbv[2]),
];
let kk = dot(kvec, kvec).sqrt();
let asum = aa + ab;
if asum > 0.0 && arg0 + kk * kk / (4.0 * asum) < ARG_SCREEN {
continue;
}
let (k, khat) = if kk > K_TINY {
(kk, [kvec[0] / kk, kvec[1] / kk, kvec[2] / kk])
} else {
(0.0, [0.0, 0.0, 1.0])
};
let ang = build_ang1(rsh, ltot, khat);
let v = build_v1(&comps_a, &comps_b, cav, cbv, ltot, &ang);
for ep in prims {
if ep.coef == 0.0 {
continue;
}
let alpha = asum + ep.zeta;
if alpha <= 0.0 || arg0 + k * k / (4.0 * alpha) < ARG_SCREEN {
continue;
}
let q = radial_type1(alpha, k, arg0, ep.n, ltot, &mut mb);
let scale = w * ep.coef * FOUR_PI;
for ia in 0..comps_a.len() {
for ib in 0..nb {
let vsl = &v[(ia * nb + ib) * d * d..][..d * d];
let mut acc = 0.0;
for (vv, qq) in vsl.iter().zip(&q) {
if *vv != 0.0 {
acc += vv * qq;
}
}
block[ia * nb + ib] += scale * acc;
}
}
}
}
}
}
fn build_ang1(rsh: &Rsh, ltot: usize, khat: [f64; 3]) -> Vec<Vec<f64>> {
let d = ltot + 1;
let mut ang = vec![vec![0.0; d * d * d]; d];
for (lam, slot) in ang.iter_mut().enumerate() {
let lam_i = lam as i32;
let svals: Vec<f64> = (-lam_i..=lam_i)
.map(|m| eval_poly(rsh.poly(lam, m), khat))
.collect();
for tx in 0..d {
for ty in 0..(d - tx) {
for tz in 0..(d - tx - ty) {
let ts = tx + ty + tz;
if ts < lam || (ts + lam) % 2 == 1 {
continue;
}
let mut acc = 0.0;
for (sv, m) in svals.iter().zip(-lam_i..=lam_i) {
if *sv != 0.0 {
acc += sv * omega1(rsh.poly(lam, m), [tx, ty, tz]);
}
}
slot[(tx * d + ty) * d + tz] = acc;
}
}
}
}
ang
}
fn build_v1(
comps_a: &[[usize; 3]],
comps_b: &[[usize; 3]],
cav: [f64; 3],
cbv: [f64; 3],
ltot: usize,
ang: &[Vec<f64>],
) -> Vec<f64> {
let d = ltot + 1;
let (na, nb) = (comps_a.len(), comps_b.len());
let mut v = vec![0.0; na * nb * d * d];
for (ia, pa) in comps_a.iter().enumerate() {
let cca: Vec<Vec<f64>> = (0..3).map(|c| coord_coeffs(pa[c], cav[c])).collect();
for (ib, pb) in comps_b.iter().enumerate() {
let prod: Vec<Vec<f64>> = (0..3)
.map(|c| convolve(&cca[c], &coord_coeffs(pb[c], cbv[c])))
.collect();
let base = (ia * nb + ib) * d * d;
for (tx, &px) in prod[0].iter().enumerate() {
for (ty, &py) in prod[1].iter().enumerate() {
for (tz, &pz) in prod[2].iter().enumerate() {
let cc = px * py * pz;
if cc == 0.0 {
continue;
}
let s = tx + ty + tz;
let tidx = (tx * d + ty) * d + tz;
for (lam, a) in ang.iter().enumerate() {
if a[tidx] != 0.0 {
v[base + lam * d + s] += cc * a[tidx];
}
}
}
}
}
}
}
v
}
fn radial_type1(alpha: f64, k: f64, arg0: f64, n: i32, ltot: usize, mb: &mut [f64]) -> Vec<f64> {
let d = ltot + 1;
let mut q = vec![0.0; d * d];
let (c1, c2) = quad_window(alpha, k);
for &(x, w) in gl_nodes() {
let r = c1 * x + c2;
let e = (arg0 + (k - alpha * r) * r).exp();
if e == 0.0 {
continue;
}
msb_scaled(k * r, ltot, mb);
let wr = c1 * w * e;
let mut rs = r.powi(n);
for s in 0..d {
let v = wr * rs;
for lam in 0..d {
q[lam * d + s] += v * mb[lam];
}
rs *= r;
}
}
q
}
fn type2_into(
l: usize,
prims: &[EcpPrimitive],
sa: &EcpSide<'_>,
sb: &EcpSide<'_>,
rsh: &Rsh,
block: &mut [f64],
) {
let (la, lb) = (sa.l, sb.l);
let (cav, cbv) = (sa.cv, sb.cv);
let (lama, lamb) = (l + la, l + lb);
let comps_a = cart_components(la);
let comps_b = cart_components(lb);
let nb = comps_b.len();
let ra2 = dot(cav, cav);
let rb2 = dot(cbv, cbv);
let ra = ra2.sqrt();
let rb = rb2.sqrt();
let (ra_eff, khat_a) = if ra > K_TINY {
(ra, [cav[0] / ra, cav[1] / ra, cav[2] / ra])
} else {
(0.0, [0.0, 0.0, 1.0])
};
let (rb_eff, khat_b) = if rb > K_TINY {
(rb, [cbv[0] / rb, cbv[1] / rb, cbv[2] / rb])
} else {
(0.0, [0.0, 0.0, 1.0])
};
let fa = build_f2(rsh, l, la, khat_a);
let fb = build_f2(rsh, l, lb, khat_b);
let ga = build_g2(&fa, &comps_a, cav, la, l);
let gb = build_g2(&fb, &comps_b, cbv, lb, l);
let nm = 2 * l + 1;
let smax = la + lb;
let scale16 = 16.0 * PI * PI;
let mut mba = vec![0.0; lama + 1];
let mut mbb = vec![0.0; lamb + 1];
for (pi, &aa) in sa.exps.iter().enumerate() {
let wa = sa.wts[pi];
for (pj, &ab) in sb.exps.iter().enumerate() {
let w = wa * sb.wts[pj];
let ka = 2.0 * aa * ra_eff;
let kb = 2.0 * ab * rb_eff;
let ksum = ka + kb;
let arg0 = -aa * ra2 - ab * rb2;
let asum = aa + ab;
if asum > 0.0 && arg0 + ksum * ksum / (4.0 * asum) < ARG_SCREEN {
continue;
}
for ep in prims {
if ep.coef == 0.0 {
continue;
}
let alpha = asum + ep.zeta;
if alpha <= 0.0 || arg0 + ksum * ksum / (4.0 * alpha) < ARG_SCREEN {
continue;
}
let q = radial_type2(
alpha, ka, kb, arg0, ep.n, smax, lama, lamb, &mut mba, &mut mbb,
);
let scale = w * ep.coef * scale16;
for ia in 0..comps_a.len() {
for ib in 0..nb {
let mut acc = 0.0;
for mi in 0..nm {
for laa in 0..=lama {
for pa in 0..=la {
let g = ga[((ia * nm + mi) * (lama + 1) + laa) * (la + 1) + pa];
if g == 0.0 {
continue;
}
for lbb in 0..=lamb {
for pb in 0..=lb {
let h = gb[((ib * nm + mi) * (lamb + 1) + lbb)
* (lb + 1)
+ pb];
if h != 0.0 {
acc += g
* h
* q[(laa * (lamb + 1) + lbb) * (smax + 1)
+ pa
+ pb];
}
}
}
}
}
}
block[ia * nb + ib] += scale * acc;
}
}
}
}
}
}
#[allow(clippy::needless_range_loop)] fn build_f2(rsh: &Rsh, l: usize, lside: usize, khat: [f64; 3]) -> Vec<Vec<Vec<f64>>> {
let d = lside + 1;
let lam_n = l + lside + 1;
let l_i = l as i32;
let mut f = vec![vec![vec![0.0; d * d * d]; lam_n]; 2 * l + 1];
for lam in 0..lam_n {
let lam_i = lam as i32;
let svals: Vec<f64> = (-lam_i..=lam_i)
.map(|m| eval_poly(rsh.poly(lam, m), khat))
.collect();
for tx in 0..d {
for ty in 0..(d - tx) {
for tz in 0..(d - tx - ty) {
let ts = tx + ty + tz;
if lam + ts < l || lam > l + ts || (l + ts + lam) % 2 == 1 {
continue;
}
let tidx = (tx * d + ty) * d + tz;
for mm in -l_i..=l_i {
let mut acc = 0.0;
for (sv, mu) in svals.iter().zip(-lam_i..=lam_i) {
if *sv != 0.0 {
acc +=
sv * omega2(rsh.poly(l, mm), rsh.poly(lam, mu), [tx, ty, tz]);
}
}
f[(mm + l_i) as usize][lam][tidx] = acc;
}
}
}
}
}
f
}
fn build_g2(
f: &[Vec<Vec<f64>>],
comps: &[[usize; 3]],
cv: [f64; 3],
lside: usize,
l: usize,
) -> Vec<f64> {
let d = lside + 1;
let nm = 2 * l + 1;
let lam_n = l + lside + 1;
let mut g = vec![0.0; comps.len() * nm * lam_n * d];
for (ic, p) in comps.iter().enumerate() {
let cc: Vec<Vec<f64>> = (0..3).map(|c| coord_coeffs(p[c], cv[c])).collect();
for (tx, &px) in cc[0].iter().enumerate() {
for (ty, &py) in cc[1].iter().enumerate() {
for (tz, &pz) in cc[2].iter().enumerate() {
let coef = px * py * pz;
if coef == 0.0 {
continue;
}
let pw = tx + ty + tz;
let tidx = (tx * d + ty) * d + tz;
for (mi, fm) in f.iter().enumerate() {
for (lam, fl) in fm.iter().enumerate() {
if fl[tidx] != 0.0 {
g[((ic * nm + mi) * lam_n + lam) * d + pw] += coef * fl[tidx];
}
}
}
}
}
}
}
g
}
#[allow(clippy::too_many_arguments)]
fn radial_type2(
alpha: f64,
ka: f64,
kb: f64,
arg0: f64,
n: i32,
smax: usize,
lama: usize,
lamb: usize,
mba: &mut [f64],
mbb: &mut [f64],
) -> Vec<f64> {
let mut q = vec![0.0; (lama + 1) * (lamb + 1) * (smax + 1)];
let ksum = ka + kb;
let (c1, c2) = quad_window(alpha, ksum);
for &(x, w) in gl_nodes() {
let r = c1 * x + c2;
let e = (arg0 + (ksum - alpha * r) * r).exp();
if e == 0.0 {
continue;
}
msb_scaled(ka * r, lama, mba);
msb_scaled(kb * r, lamb, mbb);
let wr = c1 * w * e;
let mut rs = r.powi(n);
for s in 0..=smax {
let v = wr * rs;
for laa in 0..=lama {
let va = v * mba[laa];
for lbb in 0..=lamb {
q[(laa * (lamb + 1) + lbb) * (smax + 1) + s] += va * mbb[lbb];
}
}
rs *= r;
}
}
q
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn quad_window(alpha: f64, k: f64) -> (f64, f64) {
let r0 = k / (2.0 * alpha);
let hw = (WINDOW_LOG / alpha).sqrt();
let lo = (r0 - hw).max(0.0);
let hi = r0 + hw;
(0.5 * (hi - lo), 0.5 * (hi + lo))
}
fn coord_coeffs(p: usize, c: f64) -> Vec<f64> {
(0..=p)
.map(|i| binom(p, i) * (-c).powi((p - i) as i32))
.collect()
}
fn convolve(a: &[f64], b: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; a.len() + b.len() - 1];
for (i, &x) in a.iter().enumerate() {
for (j, &y) in b.iter().enumerate() {
out[i + j] += x * y;
}
}
out
}
fn binom(n: usize, k: usize) -> f64 {
let mut acc = 1.0;
for i in 0..k {
acc = acc * ((n - i) as f64) / ((i + 1) as f64);
}
acc
}
fn msb_scaled(x: f64, lmax: usize, out: &mut [f64]) {
debug_assert!(x >= 0.0);
if x < 100.0 {
let emx = (-x).exp();
let x2 = x * x;
for (lam, slot) in out.iter_mut().enumerate().take(lmax + 1) {
let mut term = 1.0 / double_factorial(2 * lam as i64 + 1);
let mut sum = term;
let mut t = 0usize;
while term > 1e-18 * sum && t < 500 {
term *= x2 / (((2 * t + 2) * (2 * (lam + t) + 3)) as f64);
sum += term;
t += 1;
}
*slot = x.powi(lam as i32) * sum * emx;
}
} else {
let e2 = (-2.0 * x).exp();
out[0] = (1.0 - e2) / (2.0 * x);
if lmax >= 1 {
out[1] = ((x - 1.0) + (x + 1.0) * e2) / (2.0 * x * x);
}
for lam in 2..=lmax {
out[lam] = out[lam - 2] - (2 * lam - 1) as f64 / x * out[lam - 1];
}
}
}
fn gauss_legendre(n: usize) -> Vec<(f64, f64)> {
let mut out = Vec::with_capacity(n);
for i in 1..=n {
let mut x = (PI * (i as f64 - 0.25) / (n as f64 + 0.5)).cos();
let mut dp = 1.0;
for _ in 0..100 {
let (mut p0, mut p1) = (1.0, x);
for k in 2..=n {
let kf = k as f64;
let p2 = ((2.0 * kf - 1.0) * x * p1 - (kf - 1.0) * p0) / kf;
p0 = p1;
p1 = p2;
}
dp = n as f64 * (x * p1 - p0) / (x * x - 1.0);
let dx = p1 / dp;
x -= dx;
if dx.abs() < 5e-16 {
break;
}
}
out.push((x, 2.0 / ((1.0 - x * x) * dp * dp)));
}
out
}
fn gl_nodes() -> &'static [(f64, f64)] {
static GL: OnceLock<Vec<(f64, f64)>> = OnceLock::new();
GL.get_or_init(|| gauss_legendre(N_QUAD))
}
type Poly = Vec<(f64, [usize; 3])>;
struct Rsh {
tables: Vec<Vec<Poly>>,
}
impl Rsh {
fn new(lmax: usize) -> Self {
let mut tables = Vec::with_capacity(lmax + 1);
for l in 0..=lmax {
let pl = legendre_coeffs(l);
let l_i = l as i32;
tables.push((-l_i..=l_i).map(|m| rsh_poly(l, m, &pl)).collect());
}
Rsh { tables }
}
fn poly(&self, l: usize, m: i32) -> &Poly {
&self.tables[l][(m + l as i32) as usize]
}
}
fn legendre_coeffs(l: usize) -> Vec<f64> {
let mut c = vec![0.0; l + 1];
let scale = 0.5f64.powi(l as i32);
for k in 0..=(l / 2) {
let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
c[l - 2 * k] = scale * sign * binom(l, k) * binom(2 * l - 2 * k, l);
}
c
}
fn rsh_poly(l: usize, m: i32, pl: &[f64]) -> Poly {
let ma = m.unsigned_abs() as usize;
let mut pi_lm = pl.to_vec();
for _ in 0..ma {
let mut der = vec![0.0; pi_lm.len().saturating_sub(1)];
for (p, slot) in der.iter_mut().enumerate() {
*slot = pi_lm[p + 1] * (p + 1) as f64;
}
pi_lm = der;
}
let mut out = Poly::new();
if m == 0 {
let nrm = ((2 * l + 1) as f64 / (4.0 * PI)).sqrt();
for (p, &cz) in pi_lm.iter().enumerate() {
if cz != 0.0 {
out.push((nrm * cz, [0, 0, p]));
}
}
return out;
}
let mut ratio = 1.0;
for k in (l - ma + 1)..=(l + ma) {
ratio /= k as f64;
}
let nrm = ((2 * l + 1) as f64 / (2.0 * PI) * ratio).sqrt();
let p_start = usize::from(m < 0);
for p in (p_start..=ma).step_by(2) {
let sign = if (p / 2) % 2 == 0 { 1.0 } else { -1.0 };
let ct = sign * binom(ma, p);
for (q, &cz) in pi_lm.iter().enumerate() {
if cz != 0.0 {
out.push((nrm * ct * cz, [ma - p, p, q]));
}
}
}
out
}
fn sphere_monomial(i: usize, j: usize, k: usize) -> f64 {
if i % 2 == 1 || j % 2 == 1 || k % 2 == 1 {
return 0.0;
}
FOUR_PI
* double_factorial(i as i64 - 1)
* double_factorial(j as i64 - 1)
* double_factorial(k as i64 - 1)
/ double_factorial((i + j + k) as i64 + 1)
}
fn omega1(p: &Poly, t: [usize; 3]) -> f64 {
p.iter()
.map(|&(c, m)| c * sphere_monomial(t[0] + m[0], t[1] + m[1], t[2] + m[2]))
.sum()
}
fn omega2(p1: &Poly, p2: &Poly, t: [usize; 3]) -> f64 {
let mut acc = 0.0;
for &(c1, m1) in p1 {
for &(c2, m2) in p2 {
let i = t[0] + m1[0] + m2[0];
let j = t[1] + m1[1] + m2[1];
let k = t[2] + m1[2] + m2[2];
if i % 2 == 0 && j % 2 == 0 && k % 2 == 0 {
acc += c1 * c2 * sphere_monomial(i, j, k);
}
}
}
acc
}
fn eval_poly(p: &Poly, u: [f64; 3]) -> f64 {
p.iter()
.map(|&(c, m)| c * u[0].powi(m[0] as i32) * u[1].powi(m[1] as i32) * u[2].powi(m[2] as i32))
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rsh_orthonormal() {
let rsh = Rsh::new(12);
let mut worst = 0.0f64;
for l1 in 0..=12usize {
for l2 in 0..=12usize {
for m1 in -(l1 as i32)..=l1 as i32 {
for m2 in -(l2 as i32)..=l2 as i32 {
let s = omega2(rsh.poly(l1, m1), rsh.poly(l2, m2), [0, 0, 0]);
let expect = if l1 == l2 && m1 == m2 { 1.0 } else { 0.0 };
let d = (s - expect).abs();
assert!(d < 5e-9, "⟨{l1},{m1}|{l2},{m2}⟩ = {s}");
worst = worst.max(d);
}
}
}
}
eprintln!("Rsh orthonormality worst residual (λ ≤ 12): {worst:e}");
}
#[test]
fn gauss_legendre_integrates_polynomials() {
for &n in &[8usize, 64, 128] {
let gl = gauss_legendre(n);
let s0: f64 = gl.iter().map(|&(_, w)| w).sum();
let s2: f64 = gl.iter().map(|&(x, w)| w * x * x).sum();
let s6: f64 = gl.iter().map(|&(x, w)| w * x.powi(6)).sum();
assert!((s0 - 2.0).abs() < 1e-14, "n={n} Σw={s0}");
assert!((s2 - 2.0 / 3.0).abs() < 1e-14, "n={n}");
assert!((s6 - 2.0 / 7.0).abs() < 1e-14, "n={n}");
}
}
#[test]
fn scaled_bessel_consistency() {
let mut m = [0.0; 13];
msb_scaled(0.0, 12, &mut m);
assert_eq!(m[0], 1.0);
for &v in &m[1..] {
assert_eq!(v, 0.0);
}
msb_scaled(5.0, 12, &mut m);
for lam in 1..12 {
let lhs = m[lam - 1] - m[lam + 1];
let rhs = (2 * lam + 1) as f64 / 5.0 * m[lam];
assert!((lhs - rhs).abs() < 1e-15 * m[0], "λ={lam}");
}
let x = 90.0_f64;
let mut series = [0.0; 13];
msb_scaled(x, 12, &mut series);
let e2 = (-2.0 * x).exp();
let mut rec = [0.0; 13];
rec[0] = (1.0 - e2) / (2.0 * x);
rec[1] = ((x - 1.0) + (x + 1.0) * e2) / (2.0 * x * x);
for lam in 2..=12 {
rec[lam] = rec[lam - 2] - (2 * lam - 1) as f64 / x * rec[lam - 1];
}
for lam in 0..=12 {
assert!(
(series[lam] - rec[lam]).abs() < 1e-12 * rec[lam].abs(),
"λ={lam}: {} vs {}",
series[lam],
rec[lam]
);
}
for &x in &[0.3, 4.0, 40.0, 99.0, 150.0] {
msb_scaled(x, 2, &mut m);
let exact = (1.0 - (-2.0 * x).exp()) / (2.0 * x);
assert!((m[0] - exact).abs() < 1e-14, "x={x}");
}
}
#[test]
fn radial_type1_matches_simpson() {
let mut mb = vec![0.0; 5];
for &(alpha, k) in &[(1.7_f64, 0.0_f64), (1.7, 3.0), (24.0, 11.0), (0.4, 1.2)] {
let q = radial_type1(alpha, k, 0.0, 1, 4, &mut mb);
for lam in 0..=4usize {
for s in 0..=4usize {
let f = |r: f64| {
let mut b = vec![0.0; lam + 1];
msb_scaled(k * r, lam, &mut b);
r.powi(1 + s as i32) * ((k - alpha * r) * r).exp() * b[lam]
};
let n = 80_000;
let h = 40.0 / n as f64;
let mut acc = f(0.0) + f(40.0);
for i in 1..n {
let w = if i % 2 == 1 { 4.0 } else { 2.0 };
acc += w * f(i as f64 * h);
}
acc *= h / 3.0;
let got = q[lam * 5 + s];
assert!(
(got - acc).abs() < 1e-10 * (1.0 + acc.abs()),
"alpha={alpha} k={k} λ={lam} s={s}: {got} vs {acc}"
);
}
}
}
}
#[test]
#[ignore = "expensive heavy-exponent radial-quadrature reference; run in release"]
fn radial_quadrature_accurate_at_heavy_element_exponents() {
let ref1 = |alpha: f64, k: f64, n: i32, lam: usize, s: usize| -> f64 {
let r0 = k / (2.0 * alpha);
let hw = (WINDOW_LOG / alpha).sqrt();
let hi = (r0 + 16.0 * hw).max(24.0 * hw);
let npan = 400_000usize;
let h = hi / npan as f64;
let mut mb = vec![0.0; lam + 1];
let mut f = |r: f64| {
msb_scaled(k * r, lam, &mut mb);
r.powi(n + s as i32) * ((k - alpha * r) * r).exp() * mb[lam]
};
let mut acc = f(0.0) + f(hi);
for i in 1..npan {
let w = if i % 2 == 1 { 4.0 } else { 2.0 };
acc += w * f(i as f64 * h);
}
acc * h / 3.0
};
let ref2 = |alpha: f64, ka: f64, kb: f64, n: i32, la: usize, lb: usize, s: usize| -> f64 {
let ksum = ka + kb;
let r0 = ksum / (2.0 * alpha);
let hw = (WINDOW_LOG / alpha).sqrt();
let hi = (r0 + 16.0 * hw).max(24.0 * hw);
let npan = 400_000usize;
let h = hi / npan as f64;
let mut mba = vec![0.0; la + 1];
let mut mbb = vec![0.0; lb + 1];
let mut f = |r: f64| {
msb_scaled(ka * r, la, &mut mba);
msb_scaled(kb * r, lb, &mut mbb);
r.powi(n + s as i32) * ((ksum - alpha * r) * r).exp() * mba[la] * mbb[lb]
};
let mut acc = f(0.0) + f(hi);
for i in 1..npan {
let w = if i % 2 == 1 { 4.0 } else { 2.0 };
acc += w * f(i as f64 * h);
}
acc * h / 3.0
};
let mut worst1 = 0.0f64;
let mut worst2 = 0.0f64;
for &(alpha, k) in &[
(50.0_f64, 20.0_f64),
(300.0, 120.0),
(1.0e3, 600.0),
(5.0e3, 2.0e3),
(1.0e4, 5.0e3),
] {
let ltot = 10usize; let n = 2i32;
let mut mb = vec![0.0; ltot + 1];
let q = radial_type1(alpha, k, 0.0, n, ltot, &mut mb);
for lam in 0..=ltot {
for s in 0..=ltot {
let want = ref1(alpha, k, n, lam, s);
if want.abs() > 1e-30 {
let got = q[lam * (ltot + 1) + s];
worst1 = worst1.max((got - want).abs() / want.abs());
}
}
}
let (ka, kb) = (k, 0.6 * k);
let (lama, lamb) = (8usize, 6usize);
let smax = 10usize;
let mut mba = vec![0.0; lama + 1];
let mut mbb = vec![0.0; lamb + 1];
let q2 = radial_type2(alpha, ka, kb, 0.0, n, smax, lama, lamb, &mut mba, &mut mbb);
for laa in 0..=lama {
for lbb in 0..=lamb {
for s in 0..=smax {
let want = ref2(alpha, ka, kb, n, laa, lbb, s);
if want.abs() > 1e-30 {
let got = q2[(laa * (lamb + 1) + lbb) * (smax + 1) + s];
worst2 = worst2.max((got - want).abs() / want.abs());
}
}
}
}
}
eprintln!("heavy-exponent radial: worst type-1 rel={worst1:e}, type-2 rel={worst2:e}");
assert!(
worst1 < 1e-7,
"type-1 radial under-resolved at heavy exponents: {worst1:e}"
);
assert!(
worst2 < 1e-7,
"type-2 radial under-resolved at heavy exponents: {worst2:e}"
);
}
#[test]
fn sphere_monomial_closed_forms() {
assert!((sphere_monomial(0, 0, 0) - FOUR_PI).abs() < 1e-14);
assert!((sphere_monomial(2, 0, 0) - FOUR_PI / 3.0).abs() < 1e-14);
assert!((sphere_monomial(2, 2, 0) - FOUR_PI / 15.0).abs() < 1e-15);
assert!((sphere_monomial(4, 0, 0) - FOUR_PI / 5.0).abs() < 1e-14);
assert_eq!(sphere_monomial(1, 2, 0), 0.0);
assert_eq!(sphere_monomial(2, 3, 1), 0.0);
}
}