use integral::{Basis, Engine, Shell};
const FD_TOL: f64 = 1e-6;
const TI_TOL: f64 = 1e-10;
const H: f64 = 2e-4;
fn shifted(c: [f64; 3], target: [f64; 3], d: [f64; 3]) -> [f64; 3] {
if c == target {
[c[0] + d[0], c[1] + d[1], c[2] + d[2]]
} else {
c
}
}
fn shift_basis(basis: &Basis, target: [f64; 3], d: [f64; 3]) -> Basis {
let shells = basis
.shells()
.iter()
.map(|s| {
Shell::with_kind(
s.l(),
shifted(s.center(), target, d),
s.exponents().to_vec(),
s.coefficients().to_vec(),
s.kind(),
)
.unwrap()
})
.collect();
Basis::new(shells)
}
fn shift_charges(
charges: &[([f64; 3], f64)],
target: [f64; 3],
d: [f64; 3],
) -> Vec<([f64; 3], f64)> {
charges
.iter()
.map(|&(c, z)| (shifted(c, target, d), z))
.collect()
}
fn unit(axis: usize, h: f64) -> [f64; 3] {
let mut d = [0.0; 3];
d[axis] = h;
d
}
fn central_diff(plus: &[f64], minus: &[f64], h: f64) -> Vec<f64> {
plus.iter()
.zip(minus)
.map(|(p, m)| (p - m) / (2.0 * h))
.collect()
}
fn max_abs_diff(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b)
.map(|(x, y)| (x - y).abs())
.fold(0.0_f64, f64::max)
}
fn test_basis() -> Basis {
let a0 = [0.0, 0.0, 0.0];
let a1 = [0.9, 0.1, -0.2];
let a2 = [-0.3, 0.8, 0.55];
Basis::new(vec![
Shell::new(0, a0, vec![3.4, 0.8], vec![0.4, 0.6]).unwrap(),
Shell::new(1, a0, vec![1.1], vec![1.0]).unwrap(),
Shell::new(2, a1, vec![0.9], vec![1.0]).unwrap(),
Shell::new(1, a2, vec![1.3, 0.5], vec![0.5, 0.7]).unwrap(),
])
}
fn charges_of(basis: &Basis) -> Vec<([f64; 3], f64)> {
basis
.atoms()
.iter()
.enumerate()
.map(|(i, &c)| (c, (i + 1) as f64))
.collect()
}
#[test]
fn overlap_grad_matches_finite_difference() {
let basis = test_basis();
let g = basis.overlap_grad().unwrap();
let mut worst = 0.0_f64;
for (ai, &atom) in basis.atoms().iter().enumerate() {
for axis in 0..3 {
let plus = shift_basis(&basis, atom, unit(axis, H)).overlap();
let minus = shift_basis(&basis, atom, unit(axis, -H)).overlap();
let fd = central_diff(&plus, &minus, H);
worst = worst.max(max_abs_diff(g.block(ai, axis), &fd));
}
}
assert!(worst < FD_TOL, "overlap grad vs FD: worst {worst:.3e}");
}
#[test]
fn kinetic_grad_matches_finite_difference() {
let basis = test_basis();
let g = basis.kinetic_grad().unwrap();
let mut worst = 0.0_f64;
for (ai, &atom) in basis.atoms().iter().enumerate() {
for axis in 0..3 {
let plus = shift_basis(&basis, atom, unit(axis, H)).kinetic();
let minus = shift_basis(&basis, atom, unit(axis, -H)).kinetic();
let fd = central_diff(&plus, &minus, H);
worst = worst.max(max_abs_diff(g.block(ai, axis), &fd));
}
}
assert!(worst < FD_TOL, "kinetic grad vs FD: worst {worst:.3e}");
}
#[test]
fn nuclear_grad_matches_finite_difference_incl_hellmann_feynman() {
let basis = test_basis();
let charges = charges_of(&basis);
let g = basis.nuclear_grad(&charges).unwrap();
let mut worst = 0.0_f64;
for (ai, &atom) in basis.atoms().iter().enumerate() {
for axis in 0..3 {
let bp = shift_basis(&basis, atom, unit(axis, H));
let cp = shift_charges(&charges, atom, unit(axis, H));
let bm = shift_basis(&basis, atom, unit(axis, -H));
let cm = shift_charges(&charges, atom, unit(axis, -H));
let fd = central_diff(&bp.nuclear(&cp), &bm.nuclear(&cm), H);
worst = worst.max(max_abs_diff(g.block(ai, axis), &fd));
}
}
assert!(worst < FD_TOL, "nuclear grad vs FD: worst {worst:.3e}");
}
#[test]
fn nuclear_hellmann_feynman_term_isolated() {
let basis = test_basis();
let charges = charges_of(&basis);
let g = basis.nuclear_grad(&charges).unwrap();
let mut worst = 0.0_f64;
for (ai, &atom) in basis.atoms().iter().enumerate() {
for axis in 0..3 {
let cp = shift_charges(&charges, atom, unit(axis, H));
let cm = shift_charges(&charges, atom, unit(axis, -H));
let hf_fd = central_diff(&basis.nuclear(&cp), &basis.nuclear(&cm), H);
let bp = shift_basis(&basis, atom, unit(axis, H));
let bm = shift_basis(&basis, atom, unit(axis, -H));
let basis_fd = central_diff(&bp.nuclear(&charges), &bm.nuclear(&charges), H);
let hf_impl: Vec<f64> = g
.block(ai, axis)
.iter()
.zip(&basis_fd)
.map(|(tot, b)| tot - b)
.collect();
worst = worst.max(max_abs_diff(&hf_impl, &hf_fd));
}
}
assert!(worst < FD_TOL, "HF term isolated vs FD: worst {worst:.3e}");
}
fn eri_test_basis() -> Basis {
let a0 = [0.0, 0.0, 0.0];
let a1 = [0.8, -0.2, 0.3];
let a2 = [-0.4, 0.6, 0.1];
Basis::new(vec![
Shell::new(1, a0, vec![1.6, 0.5], vec![0.5, 0.6]).unwrap(),
Shell::new(2, a1, vec![0.9], vec![1.0]).unwrap(),
Shell::new(0, a2, vec![0.7], vec![1.0]).unwrap(),
])
}
fn eri_grad_fd_check(engine: Engine) {
let basis = eri_test_basis();
let g = basis.eri_grad_with(engine).unwrap();
let mut worst = 0.0_f64;
for (ai, &atom) in basis.atoms().iter().enumerate() {
for axis in 0..3 {
let plus = shift_basis(&basis, atom, unit(axis, H)).eri_with(engine);
let minus = shift_basis(&basis, atom, unit(axis, -H)).eri_with(engine);
let fd = central_diff(&plus, &minus, H);
worst = worst.max(max_abs_diff(g.block(ai, axis), &fd));
}
}
assert!(
worst < FD_TOL,
"ERI grad ({engine:?}) vs FD: worst {worst:.3e}"
);
}
#[test]
fn eri_grad_matches_finite_difference_rys() {
eri_grad_fd_check(Engine::Rys);
}
#[test]
fn eri_grad_matches_finite_difference_oshgp() {
eri_grad_fd_check(Engine::OsHgp);
}
#[test]
fn eri_grad_auto_matches_finite_difference() {
eri_grad_fd_check(Engine::Auto);
}
#[test]
fn translational_invariance_one_electron() {
let basis = test_basis();
let charges = charges_of(&basis);
assert!(basis.overlap_grad().unwrap().max_translational_residual() < TI_TOL);
assert!(basis.kinetic_grad().unwrap().max_translational_residual() < TI_TOL);
assert!(
basis
.nuclear_grad(&charges)
.unwrap()
.max_translational_residual()
< TI_TOL
);
}
#[test]
fn translational_invariance_eri_both_engines() {
let basis = eri_test_basis();
for engine in [Engine::Rys, Engine::OsHgp] {
let r = basis
.eri_grad_with(engine)
.unwrap()
.max_translational_residual();
assert!(r < TI_TOL, "ERI TI residual ({engine:?}) = {r:.3e}");
}
}
#[test]
fn both_engines_agree_on_eri_gradient() {
let basis = eri_test_basis();
let rys = basis.eri_grad_with(Engine::Rys).unwrap();
let os = basis.eri_grad_with(Engine::OsHgp).unwrap();
let mut worst = 0.0_f64;
for ai in 0..basis.atoms().len() {
for axis in 0..3 {
worst = worst.max(max_abs_diff(rys.block(ai, axis), os.block(ai, axis)));
}
}
assert!(worst < 1e-10, "Rys vs OS/HGP ERI grad: worst {worst:.3e}");
}
#[test]
fn spherical_overlap_grad_matches_finite_difference() {
let a0 = [0.0, 0.0, 0.0];
let a1 = [0.7, 0.2, -0.3];
let basis = Basis::new(vec![
Shell::new_spherical(2, a0, vec![1.2, 0.4], vec![0.5, 0.6]).unwrap(),
Shell::new_spherical(1, a1, vec![0.9], vec![1.0]).unwrap(),
]);
let g = basis.overlap_grad().unwrap();
let mut worst = 0.0_f64;
for (ai, &atom) in basis.atoms().iter().enumerate() {
for axis in 0..3 {
let plus = shift_basis(&basis, atom, unit(axis, H)).overlap();
let minus = shift_basis(&basis, atom, unit(axis, -H)).overlap();
let fd = central_diff(&plus, &minus, H);
worst = worst.max(max_abs_diff(g.block(ai, axis), &fd));
}
}
assert!(
worst < FD_TOL,
"spherical overlap grad vs FD: worst {worst:.3e}"
);
assert!(g.max_translational_residual() < TI_TOL);
}
#[test]
fn fd_step_study() {
let basis = test_basis();
let g = basis.overlap_grad().unwrap();
let atom = basis.atoms()[0];
eprintln!("overlap-grad central-difference error vs step h (atom 0, x):");
for &h in &[1e-2, 1e-3, 1e-4, 1e-5, 1e-6] {
let plus = shift_basis(&basis, atom, unit(0, h)).overlap();
let minus = shift_basis(&basis, atom, unit(0, -h)).overlap();
let fd = central_diff(&plus, &minus, h);
eprintln!(
" h={h:.0e} max|analytic - FD| = {:.3e}",
max_abs_diff(g.block(0, 0), &fd)
);
}
}