use super::fft::Fft3d;
use super::grid::RealSpaceGrid;
use rustfft::num_complex::Complex64;
use std::f64::consts::PI;
const G2_ZERO_TOL: f64 = 1e-12;
#[must_use]
pub fn hartree(n_r: &[f64], grid: &RealSpaceGrid) -> (Vec<f64>, f64) {
let npts = grid.n_points();
assert_eq!(
n_r.len(),
npts,
"density length {} must equal grid points {npts}",
n_r.len()
);
let mut data: Vec<Complex64> = n_r.iter().map(|&x| Complex64::new(x, 0.0)).collect();
let fft = Fft3d::new(grid.n());
fft.forward(&mut data);
let inv_npts = 1.0 / npts as f64;
let four_pi = 4.0 * PI;
for (d, g) in data.iter_mut().zip(grid.g_vectors()) {
let g2 = g[0] * g[0] + g[1] * g[1] + g[2] * g[2];
if g2 < G2_ZERO_TOL {
*d = Complex64::new(0.0, 0.0); } else {
*d *= four_pi * inv_npts / g2; }
}
fft.inverse(&mut data); let v: Vec<f64> = data.iter().map(|c| c.re).collect();
let e_h = 0.5 * grid.dv() * n_r.iter().zip(&v).map(|(&n, &vv)| n * vv).sum::<f64>();
(v, e_h)
}
#[must_use]
pub fn hartree_reciprocal_stress(n_r: &[f64], grid: &RealSpaceGrid) -> [[f64; 3]; 3] {
let npts = grid.n_points();
assert_eq!(
n_r.len(),
npts,
"density length {} must equal grid points {npts}",
n_r.len()
);
let mut data: Vec<Complex64> = n_r.iter().map(|&x| Complex64::new(x, 0.0)).collect();
let fft = Fft3d::new(grid.n());
fft.forward(&mut data);
let pref = 2.0 * PI * grid.dv() / npts as f64; let mut tau = [[0.0_f64; 3]; 3];
for (d, g) in data.iter().zip(grid.g_vectors()) {
let g2 = g[0] * g[0] + g[1] * g[1] + g[2] * g[2];
if g2 < G2_ZERO_TOL {
continue;
}
let e_g = pref * d.norm_sqr() / g2; let f = 2.0 * e_g / g2; for (a, ta) in tau.iter_mut().enumerate() {
for (b, tab) in ta.iter_mut().enumerate() {
*tab += f * g[a] * g[b];
}
}
}
tau
}
#[cfg(test)]
mod tests {
use super::*;
use latx::Cell;
use std::f64::consts::PI;
#[test]
fn hartree_metric_stress_matches_finite_difference() {
let l = 7.0;
let dims = [16usize, 16, 16];
let cell = Cell::cubic(l).unwrap();
let grid = RealSpaceGrid::new(cell, dims);
let two_pi_l = 2.0 * PI / l;
let n_r: Vec<f64> = grid
.points()
.iter()
.map(|r| {
(two_pi_l * r[0]).sin() + 0.5 * (2.0 * two_pi_l * r[1]).cos()
- 0.3 * (two_pi_l * r[2]).sin()
})
.collect();
let (_v, e_h) = hartree(&n_r, &grid);
let tau_recip = hartree_reciprocal_stress(&n_r, &grid);
let deform = |m: &[[f64; 3]; 3], lambda: f64, v: [f64; 3]| -> [f64; 3] {
let mut o = v;
for (a, oa) in o.iter_mut().enumerate() {
for (b, &vb) in v.iter().enumerate() {
*oa += lambda * m[a][b] * vb;
}
}
o
};
let dirs = [
[[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.4, 0.2, -0.1], [0.2, -0.3, 0.15], [-0.1, 0.15, 0.5]],
];
for mdir in dirs {
let e_at = |lambda: f64| -> f64 {
let (a1, a2, a3) = cell.vectors();
let dcell = Cell::from_vectors(
deform(&mdir, lambda, a1),
deform(&mdir, lambda, a2),
deform(&mdir, lambda, a3),
)
.unwrap();
hartree(&n_r, &RealSpaceGrid::new(dcell, dims)).1
};
let h = 1e-5;
let fd = (e_at(h) - e_at(-h)) / (2.0 * h);
let analytic: f64 = (0..3)
.flat_map(|a| (0..3).map(move |b| (a, b)))
.map(|(a, b)| {
let metric = if a == b { e_h } else { 0.0 };
(metric + tau_recip[a][b]) * mdir[a][b]
})
.sum();
assert!(
(analytic - fd).abs() < 1e-7 * fd.abs().max(1.0),
"strain {mdir:?}: analytic {analytic} vs FD {fd}"
);
}
}
#[test]
fn homogeneous_density_gives_zero_potential() {
let grid = RealSpaceGrid::new(Cell::cubic(6.0).unwrap(), [12, 12, 12]);
let n_r = vec![0.5; grid.n_points()];
let (v, e_h) = hartree(&n_r, &grid);
assert!(
v.iter().all(|&x| x.abs() < 1e-10),
"V_H must vanish for constant n"
);
assert!(
e_h.abs() < 1e-10,
"E_H must vanish for constant n, got {e_h}"
);
}
#[test]
fn recovers_band_limited_potential() {
let l = 7.0;
let cell = Cell::cubic(l).unwrap();
let n = [10usize, 10, 10];
let grid = RealSpaceGrid::new(cell, n);
let two_pi_l = 2.0 * PI / l;
let g1 = [two_pi_l, 0.0, 0.0];
let g2 = [0.0, two_pi_l, 2.0 * two_pi_l];
let g1_2 = g1[0] * g1[0] + g1[1] * g1[1] + g1[2] * g1[2];
let g2_2 = g2[0] * g2[0] + g2[1] * g2[1] + g2[2] * g2[2];
let dot = |g: [f64; 3], r: [f64; 3]| g[0] * r[0] + g[1] * r[1] + g[2] * r[2];
let v_exact = |r: [f64; 3]| dot(g1, r).cos() + 0.5 * dot(g2, r).cos();
let density =
|r: [f64; 3]| (g1_2 * dot(g1, r).cos() + 0.5 * g2_2 * dot(g2, r).cos()) / (4.0 * PI);
let mut n_r = vec![0.0; grid.n_points()];
for i in 0..n[0] {
for j in 0..n[1] {
for k in 0..n[2] {
let r = grid.point([i, j, k]);
n_r[grid.linear_index([i, j, k])] = density(r);
}
}
}
let (v, _e_h) = hartree(&n_r, &grid);
for i in 0..n[0] {
for j in 0..n[1] {
for k in 0..n[2] {
let r = grid.point([i, j, k]);
let got = v[grid.linear_index([i, j, k])];
assert!(
(got - v_exact(r)).abs() < 1e-10,
"Poisson mismatch at {r:?}: got {got}, want {}",
v_exact(r)
);
}
}
}
}
}