use molrs::spatial::region::simbox::SimBox;
use molrs::store::frame::Frame;
use molrs::types::F;
use ndarray::{Array2, ArrayView2};
use crate::compute::error::ComputeError;
use crate::compute::result::ComputeResult;
pub const BOHR_TO_ANG: F = 0.529_177_210_67;
#[derive(Debug, Clone)]
pub struct DensityGrid {
pub origin: [F; 3],
pub basis: [[F; 3]; 3],
pub dims: [usize; 3],
pub density: Vec<F>,
pub dv: F,
}
impl DensityGrid {
pub fn new(origin: [F; 3], basis: [[F; 3]; 3], dims: [usize; 3], density: Vec<F>) -> Self {
let dv = det3(basis).abs();
DensityGrid {
origin,
basis,
dims,
density,
dv,
}
}
pub fn from_cube_frame(frame: &Frame) -> Result<Self, ComputeError> {
let grid_block = frame
.get("grid")
.ok_or(ComputeError::MissingBlock { name: "grid" })?;
let shape = grid_block.shape();
if shape.len() != 3 {
return Err(ComputeError::BadShape {
expected: "3-D grid [nx,ny,nz]".into(),
got: format!("{shape:?}"),
});
}
let dims = [shape[0], shape[1], shape[2]];
let grid = grid_block
.get_float("density")
.ok_or(ComputeError::MissingColumn {
block: "grid",
col: "density",
})?;
let simbox = frame.simbox.as_ref().ok_or(ComputeError::MissingSimBox)?;
let h = simbox.h_view();
let o = simbox.origin_view();
let mut basis = [[0.0; 3]; 3];
for j in 0..3 {
let nj = dims[j].max(1) as F;
for i in 0..3 {
basis[j][i] = h[[i, j]] / nj;
}
}
let origin = [o[0], o[1], o[2]];
let bohr3 = BOHR_TO_ANG * BOHR_TO_ANG * BOHR_TO_ANG;
let is_ang = frame
.meta
.get("cube_units")
.map(|u| u == "angstrom")
.unwrap_or(false);
let scale = if is_ang { 1.0 } else { 1.0 / bohr3 };
let density: Vec<F> = grid.iter().map(|&v| v * scale).collect();
Ok(DensityGrid::new(origin, basis, dims, density))
}
#[inline]
fn point(&self, m: usize) -> [F; 3] {
let (nx, ny, nz) = (self.dims[0], self.dims[1], self.dims[2]);
debug_assert_eq!(self.density.len(), nx * ny * nz);
let i = m / (ny * nz);
let rem = m % (ny * nz);
let j = rem / nz;
let k = rem % nz;
let (fi, fj, fk) = (i as F, j as F, k as F);
[
self.origin[0] + fi * self.basis[0][0] + fj * self.basis[1][0] + fk * self.basis[2][0],
self.origin[1] + fi * self.basis[0][1] + fj * self.basis[1][1] + fk * self.basis[2][1],
self.origin[2] + fi * self.basis[0][2] + fj * self.basis[1][2] + fk * self.basis[2][2],
]
}
}
#[derive(Debug, Clone)]
pub struct MolecularMoments {
pub charges: Vec<F>,
pub dipoles: Array2<F>,
pub references: Array2<F>,
}
impl ComputeResult for MolecularMoments {}
#[derive(Debug, Clone, Copy, Default)]
pub struct VoronoiIntegration;
impl VoronoiIntegration {
#[allow(clippy::too_many_arguments)]
pub fn integrate(
&self,
positions: ArrayView2<F>,
radii: &[F],
atomic_numbers: &[i32],
atom_to_mol: &[usize],
n_mol: usize,
grid: &DensityGrid,
simbox: &SimBox,
) -> Result<MolecularMoments, ComputeError> {
let n = positions.nrows();
if positions.ncols() != 3 {
return Err(ComputeError::DimensionMismatch {
expected: 3,
got: positions.ncols(),
what: "positions columns",
});
}
for (name, len) in [
("radii", radii.len()),
("atomic_numbers", atomic_numbers.len()),
("atom_to_mol", atom_to_mol.len()),
] {
if len != n {
return Err(ComputeError::DimensionMismatch {
expected: n,
got: len,
what: leak(name),
});
}
}
if atom_to_mol.iter().any(|&m| m >= n_mol) {
return Err(ComputeError::OutOfRange {
field: "atom_to_mol",
value: "molecule index >= n_mol".into(),
});
}
let l = simbox.lengths();
let lbox = [l[0], l[1], l[2]];
let gens: Vec<[F; 3]> = (0..n)
.map(|a| [positions[[a, 0]], positions[[a, 1]], positions[[a, 2]]])
.collect();
let radii_sq: Vec<F> = radii.iter().map(|&r| r * r).collect();
let genpos = |a: usize| gens[a];
let mut anchor: Vec<Option<[F; 3]>> = vec![None; n_mol];
let mut ref_num = vec![[0.0_f64; 3]; n_mol]; let mut ref_den = vec![0.0_f64; n_mol]; for a in 0..n {
let m = atom_to_mol[a];
let ra = genpos(a);
let anc = *anchor[m].get_or_insert(ra);
let ru = unwrap(ra, anc, lbox);
let z = atomic_numbers[a] as F;
for d in 0..3 {
ref_num[m][d] += z * ru[d];
}
ref_den[m] += z;
}
let mut references = Array2::<F>::zeros((n_mol, 3));
for m in 0..n_mol {
let anc = anchor[m].unwrap_or([0.0; 3]);
for d in 0..3 {
references[[m, d]] = if ref_den[m].abs() > 0.0 {
ref_num[m][d] / ref_den[m]
} else {
anc[d] };
}
}
let mut charges = vec![0.0_f64; n_mol];
let mut dipoles = Array2::<F>::zeros((n_mol, 3));
for a in 0..n {
let m = atom_to_mol[a];
let z = atomic_numbers[a] as F;
charges[m] += z;
let rref = [references[[m, 0]], references[[m, 1]], references[[m, 2]]];
let d = min_image(sub(genpos(a), rref), lbox);
for c in 0..3 {
dipoles[[m, c]] += z * d[c];
}
}
let n_voxels = grid.dims[0] * grid.dims[1] * grid.dims[2];
if grid.density.len() != n_voxels {
return Err(ComputeError::DimensionMismatch {
expected: n_voxels,
got: grid.density.len(),
what: "density length vs grid dims",
});
}
let w_max = radii_sq.iter().copied().fold(0.0_f64, f64::max);
let r_max = radii.iter().copied().fold(0.0_f64, |m, r| m.max(r.abs()));
let vol = lbox[0] * lbox[1] * lbox[2];
let mean_spacing = if vol > 0.0 && n > 0 {
(vol / n as F).cbrt()
} else {
0.0
};
let r_cut = {
let base = 3.0 * mean_spacing + 2.0 * r_max;
if base.is_finite() && base > 0.0 {
base
} else {
lbox.iter().copied().fold(0.0_f64, f64::max).max(1.0)
}
};
let cell_list = CellList::build(&gens, lbox, r_cut);
let deposit = |m: usize, charge: &mut [F], dip: &mut Array2<F>| {
let rho = grid.density[m];
if rho == 0.0 {
return;
}
let x = grid.point(m);
let best = cell_list.nearest(x, &gens, &radii_sq, w_max, r_cut, n);
let mol = atom_to_mol[best];
let n_elec = rho * grid.dv; charge[mol] -= n_elec;
let rref = [
references[[mol, 0]],
references[[mol, 1]],
references[[mol, 2]],
];
let disp = min_image(sub(x, rref), lbox);
for c in 0..3 {
dip[[mol, c]] -= n_elec * disp[c];
}
};
let zeros = || (vec![0.0_f64; n_mol], Array2::<F>::zeros((n_mol, 3)));
#[cfg(feature = "rayon")]
let (elec_charge, elec_dipole) = {
use rayon::prelude::*;
(0..n_voxels)
.into_par_iter()
.fold(zeros, |(mut c, mut d), m| {
deposit(m, &mut c, &mut d);
(c, d)
})
.reduce(zeros, |(mut ca, mut da), (cb, db)| {
for (x, y) in ca.iter_mut().zip(cb.iter()) {
*x += *y;
}
da += &db;
(ca, da)
})
};
#[cfg(not(feature = "rayon"))]
let (elec_charge, elec_dipole) = {
let (mut c, mut d) = zeros();
for m in 0..n_voxels {
deposit(m, &mut c, &mut d);
}
(c, d)
};
for m in 0..n_mol {
charges[m] += elec_charge[m];
for c in 0..3 {
dipoles[[m, c]] += elec_dipole[[m, c]];
}
}
Ok(MolecularMoments {
charges,
dipoles,
references,
})
}
}
#[inline]
fn sub(a: [F; 3], b: [F; 3]) -> [F; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn min_image(mut d: [F; 3], l: [F; 3]) -> [F; 3] {
for c in 0..3 {
if l[c] > 0.0 {
d[c] -= l[c] * (d[c] / l[c]).round();
}
}
d
}
#[inline]
fn unwrap(r: [F; 3], anchor: [F; 3], l: [F; 3]) -> [F; 3] {
let d = min_image(sub(r, anchor), l);
[anchor[0] + d[0], anchor[1] + d[1], anchor[2] + d[2]]
}
#[inline]
fn power_dist(x: [F; 3], g: [F; 3], r_sq: F, l: [F; 3]) -> F {
let d = min_image(sub(x, g), l);
d[0] * d[0] + d[1] * d[1] + d[2] * d[2] - r_sq
}
#[inline]
fn cell_index_1d(p: F, l: F, edge: F, ncell: usize) -> usize {
if l <= 0.0 || ncell <= 1 {
return 0;
}
let w = p - l * (p / l).floor(); let c = (w / edge).floor();
if !c.is_finite() || c < 0.0 {
0
} else {
(c as usize).min(ncell - 1)
}
}
#[inline]
fn axis_cells(ic: usize, ncell: usize) -> ([usize; 3], usize) {
match ncell {
0 | 1 => ([0, 0, 0], 1),
2 => ([ic, 1 - ic, 0], 2),
_ => {
let lo = if ic == 0 { ncell - 1 } else { ic - 1 };
let hi = if ic + 1 == ncell { 0 } else { ic + 1 };
([lo, ic, hi], 3)
}
}
}
struct CellList {
ncell: [usize; 3],
cell_edge: [F; 3],
lbox: [F; 3],
cell_start: Vec<usize>,
cell_gens: Vec<usize>,
}
impl CellList {
fn build(gens: &[[F; 3]], lbox: [F; 3], r_cut: F) -> Self {
let mut ncell = [1usize; 3];
let mut cell_edge = [F::INFINITY; 3];
for d in 0..3 {
if lbox[d] > 0.0 {
let nd = (lbox[d] / r_cut).floor();
let nd = if nd.is_finite() && nd >= 1.0 {
nd as usize
} else {
1
};
ncell[d] = nd.max(1);
cell_edge[d] = lbox[d] / ncell[d] as F;
}
}
let ncells = ncell[0] * ncell[1] * ncell[2];
let cell_of = |p: [F; 3]| -> usize {
let ix = cell_index_1d(p[0], lbox[0], cell_edge[0], ncell[0]);
let iy = cell_index_1d(p[1], lbox[1], cell_edge[1], ncell[1]);
let iz = cell_index_1d(p[2], lbox[2], cell_edge[2], ncell[2]);
(ix * ncell[1] + iy) * ncell[2] + iz
};
let mut cell_start = vec![0usize; ncells + 1];
for g in gens {
cell_start[cell_of(*g) + 1] += 1;
}
for c in 0..ncells {
cell_start[c + 1] += cell_start[c];
}
let mut cursor = cell_start.clone();
let mut cell_gens = vec![0usize; gens.len()];
for (a, g) in gens.iter().enumerate() {
let c = cell_of(*g);
cell_gens[cursor[c]] = a;
cursor[c] += 1;
}
CellList {
ncell,
cell_edge,
lbox,
cell_start,
cell_gens,
}
}
fn nearest(
&self,
x: [F; 3],
gens: &[[F; 3]],
radii_sq: &[F],
w_max: F,
r_cut: F,
n: usize,
) -> usize {
let icx = cell_index_1d(x[0], self.lbox[0], self.cell_edge[0], self.ncell[0]);
let icy = cell_index_1d(x[1], self.lbox[1], self.cell_edge[1], self.ncell[1]);
let icz = cell_index_1d(x[2], self.lbox[2], self.cell_edge[2], self.ncell[2]);
let (ax, nax) = axis_cells(icx, self.ncell[0]);
let (ay, nay) = axis_cells(icy, self.ncell[1]);
let (az, naz) = axis_cells(icz, self.ncell[2]);
let mut best = usize::MAX;
let mut best_pow = F::INFINITY;
let mut any = false;
for &cx in &ax[..nax] {
for &cy in &ay[..nay] {
for &cz in &az[..naz] {
let c = (cx * self.ncell[1] + cy) * self.ncell[2] + cz;
for &a in &self.cell_gens[self.cell_start[c]..self.cell_start[c + 1]] {
any = true;
let pow = power_dist(x, gens[a], radii_sq[a], self.lbox);
if pow < best_pow || (pow <= best_pow && a < best) {
best_pow = pow;
best = a;
}
}
}
}
}
if any && r_cut * r_cut - w_max > best_pow {
return best;
}
let mut fb_best = 0usize;
let mut fb_pow = F::INFINITY;
for a in 0..n {
let pow = power_dist(x, gens[a], radii_sq[a], self.lbox);
if pow < fb_pow {
fb_pow = pow;
fb_best = a;
}
}
fb_best
}
}
#[inline]
fn det3(m: [[F; 3]; 3]) -> F {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
fn leak(s: &str) -> &'static str {
match s {
"radii" => "radii length",
"atomic_numbers" => "atomic_numbers length",
"atom_to_mol" => "atom_to_mol length",
_ => "input length",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn det3_and_point_indexing() {
let g = DensityGrid::new(
[0.0, 0.0, 0.0],
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
[2, 2, 2],
vec![0.0; 8],
);
assert!((g.dv - 1.0).abs() < 1e-12);
assert_eq!(g.point(5), [1.0, 0.0, 1.0]);
}
#[test]
fn min_image_wraps_half_box() {
let d = min_image([0.9, 0.0, 0.0], [1.0, 1.0, 1.0]);
assert!((d[0] - (-0.1)).abs() < 1e-12);
}
#[test]
fn cell_list_nearest_matches_brute_force() {
let lbox = [12.0_f64, 10.0, 8.0];
let n = 200usize;
let mut state = 0x2545_F491_4F6C_DD1Du64;
let mut rng = || {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((state >> 11) as f64) / ((1u64 << 53) as f64)
};
let gens: Vec<[F; 3]> = (0..n)
.map(|_| [rng() * lbox[0], rng() * lbox[1], rng() * lbox[2]])
.collect();
let radii_sq: Vec<F> = (0..n).map(|_| (rng() * 1.5).powi(2)).collect();
let w_max = radii_sq.iter().copied().fold(0.0_f64, f64::max);
let brute = |x: [F; 3]| -> usize {
let mut best = 0usize;
let mut best_pow = F::INFINITY;
for a in 0..n {
let pow = power_dist(x, gens[a], radii_sq[a], lbox);
if pow < best_pow {
best_pow = pow;
best = a;
}
}
best
};
let queries: Vec<[F; 3]> = (0..600)
.map(|_| {
[
rng() * lbox[0] * 1.3 - 1.0,
rng() * lbox[1] * 1.3 - 1.0,
rng() * lbox[2] * 1.3 - 1.0,
]
})
.collect();
for &r_cut in &[0.6_f64, 3.0, 5.0, 50.0] {
let cl = CellList::build(&gens, lbox, r_cut);
for &x in &queries {
assert_eq!(
cl.nearest(x, &gens, &radii_sq, w_max, r_cut, n),
brute(x),
"spatial index diverged from brute force at r_cut={r_cut}, x={x:?}"
);
}
}
}
}