use crate::math::am::{cart_components, n_cart};
use crate::shell::Basis;
use crate::spherical::shell_transform;
use super::grid::RealSpaceGrid;
pub(super) const SCREEN_EXP: f64 = 40.0;
pub(super) struct ShellEval {
pub(super) offset: usize,
pub(super) n_cart: usize,
pub(super) n_func: usize,
pub(super) center: [f64; 3],
pub(super) prim_coeff: Vec<f64>,
pub(super) exps: Vec<f64>,
pub(super) comps: Vec<[usize; 3]>,
pub(super) transform: Option<Vec<f64>>,
pub(super) alpha_min: f64,
}
pub(super) fn build_shell_evals(basis: &Basis) -> Vec<ShellEval> {
let offsets = basis.offsets();
basis
.shells()
.iter()
.zip(offsets)
.map(|(s, offset)| {
let l = s.l();
let prim_coeff: Vec<f64> = (0..s.n_prim()).map(|i| s.primitive_coeff(i)).collect();
let exps = s.exponents().to_vec();
let alpha_min = exps.iter().copied().fold(f64::INFINITY, f64::min);
ShellEval {
offset,
n_cart: n_cart(l),
n_func: s.n_func(),
center: s.center(),
prim_coeff,
exps,
comps: cart_components(l),
transform: shell_transform(s),
alpha_min,
}
})
.collect()
}
impl ShellEval {
fn eval_into(
&self,
r: [f64; 3],
grid: &RealSpaceGrid,
scratch: &mut Vec<f64>,
active: &mut Vec<(usize, f64)>,
) {
let dr = grid.cell().min_image([
r[0] - self.center[0],
r[1] - self.center[1],
r[2] - self.center[2],
]);
self.emit(dr, scratch, &mut |ao, v| active.push((ao, v)));
}
pub(super) fn eval_at<F: FnMut(usize, f64)>(
&self,
r: [f64; 3],
image_center: [f64; 3],
scratch: &mut Vec<f64>,
emit: F,
) {
let dr = [
r[0] - image_center[0],
r[1] - image_center[1],
r[2] - image_center[2],
];
self.emit(dr, scratch, emit);
}
pub(super) fn emit<F: FnMut(usize, f64)>(
&self,
dr: [f64; 3],
scratch: &mut Vec<f64>,
mut emit: F,
) {
let rho2 = dr[0] * dr[0] + dr[1] * dr[1] + dr[2] * dr[2];
if self.alpha_min * rho2 > SCREEN_EXP {
return;
}
let mut radial = 0.0;
for (c, &a) in self.prim_coeff.iter().zip(&self.exps) {
radial += c * (-a * rho2).exp();
}
if radial == 0.0 {
return;
}
scratch.clear();
for comp in &self.comps {
let mono = dr[0].powi(comp[0] as i32)
* dr[1].powi(comp[1] as i32)
* dr[2].powi(comp[2] as i32);
scratch.push(radial * mono);
}
match &self.transform {
None => {
for (c, &val) in scratch.iter().enumerate() {
if val != 0.0 {
emit(self.offset + c, val);
}
}
}
Some(m) => {
for f in 0..self.n_func {
let row = &m[f * self.n_cart..(f + 1) * self.n_cart];
let mut acc = 0.0;
for (mc, &cart) in row.iter().zip(scratch.iter()) {
acc += mc * cart;
}
if acc != 0.0 {
emit(self.offset + f, acc);
}
}
}
}
}
pub(super) fn emit_grad<F: FnMut(usize, f64, [f64; 3])>(
&self,
r: [f64; 3],
image_center: [f64; 3],
scratch: &mut Vec<f64>,
mut emit: F,
) {
let dr = [
r[0] - image_center[0],
r[1] - image_center[1],
r[2] - image_center[2],
];
let rho2 = dr[0] * dr[0] + dr[1] * dr[1] + dr[2] * dr[2];
if self.alpha_min * rho2 > SCREEN_EXP {
return;
}
let mut radial = 0.0;
let mut dradial = 0.0; for (c, &a) in self.prim_coeff.iter().zip(&self.exps) {
let e = c * (-a * rho2).exp();
radial += e;
dradial += -2.0 * a * e;
}
if radial == 0.0 && dradial == 0.0 {
return;
}
scratch.clear();
for comp in &self.comps {
let (lx, ly, lz) = (comp[0] as i32, comp[1] as i32, comp[2] as i32);
let px = dr[0].powi(lx);
let py = dr[1].powi(ly);
let pz = dr[2].powi(lz);
let mono = px * py * pz;
let val = radial * mono;
let dmx = if lx == 0 {
0.0
} else {
f64::from(lx) * dr[0].powi(lx - 1) * py * pz
};
let dmy = if ly == 0 {
0.0
} else {
f64::from(ly) * px * dr[1].powi(ly - 1) * pz
};
let dmz = if lz == 0 {
0.0
} else {
f64::from(lz) * px * py * dr[2].powi(lz - 1)
};
scratch.push(val);
scratch.push(dradial * dr[0] * mono + radial * dmx);
scratch.push(dradial * dr[1] * mono + radial * dmy);
scratch.push(dradial * dr[2] * mono + radial * dmz);
}
match &self.transform {
None => {
for (c, chunk) in scratch.chunks_exact(4).enumerate() {
emit(self.offset + c, chunk[0], [chunk[1], chunk[2], chunk[3]]);
}
}
Some(m) => {
for f in 0..self.n_func {
let row = &m[f * self.n_cart..(f + 1) * self.n_cart];
let (mut v, mut gx, mut gy, mut gz) = (0.0, 0.0, 0.0, 0.0);
for (mc, chunk) in row.iter().zip(scratch.chunks_exact(4)) {
v += mc * chunk[0];
gx += mc * chunk[1];
gy += mc * chunk[2];
gz += mc * chunk[3];
}
emit(self.offset + f, v, [gx, gy, gz]);
}
}
}
}
}
#[must_use]
pub fn collocate_density(basis: &Basis, p: &[f64], grid: &RealSpaceGrid) -> Vec<f64> {
let nao = basis.nao();
assert_eq!(
p.len(),
nao * nao,
"density matrix must be nao×nao = {}²",
nao
);
let shells = build_shell_evals(basis);
let mut n_r = vec![0.0; grid.n_points()];
let [n0, n1, n2] = grid.n();
let mut scratch = Vec::with_capacity(16);
let mut active: Vec<(usize, f64)> = Vec::with_capacity(nao);
for i in 0..n0 {
for j in 0..n1 {
for k in 0..n2 {
let r = grid.point([i, j, k]);
active.clear();
for sh in &shells {
sh.eval_into(r, grid, &mut scratch, &mut active);
}
if active.is_empty() {
continue;
}
let mut nn = 0.0;
for &(mu, fmu) in &active {
let row = &p[mu * nao..(mu + 1) * nao];
let mut t = 0.0;
for &(nu, fnu) in &active {
t += row[nu] * fnu;
}
nn += fmu * t;
}
n_r[grid.linear_index([i, j, k])] = nn;
}
}
}
n_r
}
#[must_use]
pub fn integrate_potential(basis: &Basis, v: &[f64], grid: &RealSpaceGrid) -> Vec<f64> {
assert_eq!(
v.len(),
grid.n_points(),
"potential length must equal grid points"
);
let nao = basis.nao();
let shells = build_shell_evals(basis);
let dv = grid.dv();
let mut mat = vec![0.0; nao * nao];
let [n0, n1, n2] = grid.n();
let mut scratch = Vec::with_capacity(16);
let mut active: Vec<(usize, f64)> = Vec::with_capacity(nao);
for i in 0..n0 {
for j in 0..n1 {
for k in 0..n2 {
let lin = grid.linear_index([i, j, k]);
let vg = v[lin];
if vg == 0.0 {
continue;
}
let r = grid.point([i, j, k]);
active.clear();
for sh in &shells {
sh.eval_into(r, grid, &mut scratch, &mut active);
}
let w = dv * vg;
for &(mu, fmu) in &active {
let wfmu = w * fmu;
let row = &mut mat[mu * nao..(mu + 1) * nao];
for &(nu, fnu) in &active {
row[nu] += wfmu * fnu;
}
}
}
}
}
mat
}
#[must_use]
pub fn project_function(basis: &Basis, f: &[f64], grid: &RealSpaceGrid) -> Vec<f64> {
assert_eq!(
f.len(),
grid.n_points(),
"function length must equal grid points"
);
let nao = basis.nao();
let shells = build_shell_evals(basis);
let dv = grid.dv();
let mut out = vec![0.0; nao];
let [n0, n1, n2] = grid.n();
let mut scratch = Vec::with_capacity(16);
let mut active: Vec<(usize, f64)> = Vec::with_capacity(nao);
for i in 0..n0 {
for j in 0..n1 {
for k in 0..n2 {
let lin = grid.linear_index([i, j, k]);
let fg = f[lin];
if fg == 0.0 {
continue;
}
let r = grid.point([i, j, k]);
active.clear();
for sh in &shells {
sh.eval_into(r, grid, &mut scratch, &mut active);
}
let w = dv * fg;
for &(mu, fmu) in &active {
out[mu] += w * fmu;
}
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Basis, Shell};
use latx::Cell;
#[test]
fn integrate_constant_reproduces_overlap_s() {
let basis = Basis::new(vec![
Shell::new(0, [8.0, 8.0, 8.0], vec![0.8], vec![1.0]).unwrap()
]);
let grid = RealSpaceGrid::new(Cell::cubic(16.0).unwrap(), [64, 64, 64]);
let s_grid = integrate_potential(&basis, &vec![1.0; grid.n_points()], &grid);
assert!((s_grid[0] - 1.0).abs() < 1e-6, "S_grid = {}", s_grid[0]);
}
#[test]
fn collocation_trace_matches_overlap() {
let basis = Basis::new(vec![
Shell::new(0, [7.0, 8.0, 8.0], vec![0.7], vec![1.0]).unwrap(),
Shell::new(0, [9.0, 8.0, 8.0], vec![1.1], vec![1.0]).unwrap(),
]);
let s = basis.overlap();
let grid = RealSpaceGrid::new(Cell::cubic(16.0).unwrap(), [72, 72, 72]);
let nao = basis.nao();
let mut p = vec![0.0; nao * nao];
for i in 0..nao {
p[i * nao + i] = 1.0;
}
let n_r = collocate_density(&basis, &p, &grid);
let integral_n: f64 = n_r.iter().sum::<f64>() * grid.dv();
let trace_s: f64 = (0..nao).map(|i| s[i * nao + i]).sum();
assert!(
(integral_n - trace_s).abs() < 1e-5,
"∫n = {integral_n}, Tr S = {trace_s}"
);
}
#[test]
fn project_function_reproduces_overlap_column() {
let alpha = 0.7;
let c = [8.0, 8.0, 8.0];
let basis = Basis::new(vec![Shell::new(0, c, vec![alpha], vec![1.0]).unwrap()]);
let grid = RealSpaceGrid::new(Cell::cubic(16.0).unwrap(), [64, 64, 64]);
let norm = (2.0 * alpha / std::f64::consts::PI).powf(0.75);
let phi: Vec<f64> = grid
.points()
.iter()
.map(|r| {
let d2 = (r[0] - c[0]).powi(2) + (r[1] - c[1]).powi(2) + (r[2] - c[2]).powi(2);
norm * (-alpha * d2).exp()
})
.collect();
let b = project_function(&basis, &phi, &grid);
assert!(
(b[0] - 1.0).abs() < 1e-6,
"⟨φ₀|φ₀⟩ = {} (want S₀₀ = 1)",
b[0]
);
}
#[test]
fn collocate_integrate_adjoint() {
let basis = Basis::new(vec![
Shell::new(0, [8.0, 8.0, 8.0], vec![0.9], vec![1.0]).unwrap(),
Shell::new_spherical(1, [8.0, 8.5, 8.0], vec![0.6], vec![1.0]).unwrap(),
]);
let grid = RealSpaceGrid::new(Cell::cubic(16.0).unwrap(), [60, 60, 60]);
let nao = basis.nao();
let mut p = vec![0.0; nao * nao];
for a in 0..nao {
for b in 0..nao {
p[a * nao + b] = 0.1 * (a as f64 + 1.0) * (b as f64 + 1.0);
}
}
let v: Vec<f64> = grid
.points()
.iter()
.map(|r| (0.3 * r[0]).sin() + 0.5 * (0.2 * r[1]).cos())
.collect();
let n_r = collocate_density(&basis, &p, &grid);
let vmat = integrate_potential(&basis, &v, &grid);
let lhs: f64 = (0..nao * nao).map(|i| vmat[i] * p[i]).sum();
let rhs: f64 = grid.dv() * v.iter().zip(&n_r).map(|(&vv, &nn)| vv * nn).sum::<f64>();
assert!(
(lhs - rhs).abs() < 1e-9,
"adjoint: Σ V·P = {lhs}, Σ dV·V·n = {rhs}"
);
}
}