#![cfg(feature = "periodic")]
use integral::periodic::{collocate_density, hartree, RealSpaceGrid};
use integral::{Basis, Shell};
use latx::Cell;
fn erf(x: f64) -> f64 {
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs();
let t = 1.0 / (1.0 + 0.327_591_1 * x);
let y = 1.0
- (((((1.061_405_429 * t - 1.453_152_027) * t) + 1.421_413_741) * t - 0.284_496_736) * t
+ 0.254_829_592)
* t
* (-x * x).exp();
sign * y
}
fn gaussian_potential(alpha: f64, c: [f64; 3], r: [f64; 3]) -> f64 {
let d = ((r[0] - c[0]).powi(2) + (r[1] - c[1]).powi(2) + (r[2] - c[2]).powi(2)).sqrt();
if d < 1e-12 {
2.0 * (alpha / std::f64::consts::PI).sqrt()
} else {
erf(alpha.sqrt() * d) / d
}
}
#[test]
fn grid_and_poisson_public_api_smoke() {
let grid = RealSpaceGrid::new(Cell::cubic(5.0).unwrap(), [10, 10, 10]);
assert_eq!(grid.n_points(), 1000);
let (v, e_h) = hartree(&vec![0.0; grid.n_points()], &grid);
assert_eq!(v.len(), grid.n_points());
assert_eq!(e_h, 0.0);
}
#[test]
fn poisson_matches_analytic_gaussian_dipole() {
let l = 18.0;
let n = 72; let cell = Cell::cubic(l).unwrap();
let grid = RealSpaceGrid::new(cell, [n, n, n]);
let alpha = 1.5;
let norm = (alpha / std::f64::consts::PI).powf(1.5);
let center = l / 2.0;
let plus = [center + 0.7, center, center]; let minus = [center - 0.7, center, center];
let gauss = |c: [f64; 3], r: [f64; 3]| {
let d2 = (r[0] - c[0]).powi(2) + (r[1] - c[1]).powi(2) + (r[2] - c[2]).powi(2);
norm * (-alpha * d2).exp()
};
let mut n_r = vec![0.0; grid.n_points()];
for i in 0..n {
for j in 0..n {
for k in 0..n {
let r = grid.point([i, j, k]);
n_r[grid.linear_index([i, j, k])] = gauss(plus, r) - gauss(minus, r);
}
}
}
let q: f64 = n_r.iter().sum::<f64>() * grid.dv();
assert!(q.abs() < 1e-9, "net charge should vanish, got {q}");
let (v, _e_h) = hartree(&n_r, &grid);
let off = (2.0 / (l / n as f64)).round() as usize; let mid = n / 2;
let p_plus = [mid + off, mid, mid];
let p_minus = [mid - off, mid, mid];
let v_plus = v[grid.linear_index(p_plus)];
let v_minus = v[grid.linear_index(p_minus)];
let analytic = |idx: [usize; 3]| {
let r = grid.point(idx);
gaussian_potential(alpha, plus, r) - gaussian_potential(alpha, minus, r)
};
let want = analytic(p_plus) - analytic(p_minus);
let got = v_plus - v_minus;
assert!(
want > 0.5,
"sanity: analytic dipole potential difference should be O(1): {want}"
);
assert!(
(got - want).abs() < 2.0e-2,
"periodic Hartree vs analytic Gaussian dipole: got {got}, want {want} (Δ {})",
(got - want).abs()
);
}
#[test]
fn grid_hartree_converges_to_analytic_eri() {
const MADELUNG_HALF: f64 = 1.418_648; let alpha = 0.8;
let shell0 = Shell::new(0, [0.0, 0.0, 0.0], vec![alpha], vec![1.0]).unwrap();
let eri = Basis::new(vec![shell0]).eri();
let e_iso = 0.5 * eri[0];
let spacing = 0.25;
let lengths = [12.0_f64, 16.0, 20.0];
let mut residuals = Vec::new();
for &l in &lengths {
let n = (l / spacing).round() as usize; let c = l / 2.0;
let basis = Basis::new(vec![
Shell::new(0, [c, c, c], vec![alpha], vec![1.0]).unwrap()
]);
let grid = RealSpaceGrid::new(Cell::cubic(l).unwrap(), [n, n, n]);
let n_r = collocate_density(&basis, &[1.0], &grid);
let q: f64 = n_r.iter().sum::<f64>() * grid.dv();
assert!((q - 1.0).abs() < 1e-4, "∫n should be 1, got {q} (L={l})");
let (_v, e_h) = hartree(&n_r, &grid);
residuals.push(e_iso - e_h); }
assert!(
residuals[0] > residuals[1] && residuals[1] > residuals[2] && residuals[2] > 0.0,
"Hartree energy should converge upward to ½(00|00): residuals {residuals:?}"
);
let scaled = residuals[2] * lengths[2];
assert!(
(scaled - MADELUNG_HALF).abs() < 0.12,
"residual·L = {scaled} should match ξ/2 = {MADELUNG_HALF} (cubic Madelung)"
);
}