use serde::{Deserialize, Serialize};
use super::basis::{gaussian_cartesian_value, orbital_cartesian_terms, AtomicOrbital};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VolumetricGrid {
pub origin: [f64; 3],
pub spacing: f64,
pub dims: [usize; 3],
pub values: Vec<f32>,
}
impl VolumetricGrid {
pub fn num_points(&self) -> usize {
self.dims[0] * self.dims[1] * self.dims[2]
}
pub fn index(&self, ix: usize, iy: usize, iz: usize) -> usize {
ix * self.dims[1] * self.dims[2] + iy * self.dims[2] + iz
}
pub fn point_position(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
[
self.origin[0] + ix as f64 * self.spacing,
self.origin[1] + iy as f64 * self.spacing,
self.origin[2] + iz as f64 * self.spacing,
]
}
}
pub fn compute_grid_extents(
positions: &[[f64; 3]],
padding: f64,
spacing: f64,
) -> ([f64; 3], [usize; 3]) {
let mut min = [f64::MAX; 3];
let mut max = [f64::MIN; 3];
for pos in positions {
for d in 0..3 {
min[d] = min[d].min(pos[d]);
max[d] = max[d].max(pos[d]);
}
}
let origin = [min[0] - padding, min[1] - padding, min[2] - padding];
let dims = [
((max[0] - min[0] + 2.0 * padding) / spacing).ceil() as usize + 1,
((max[1] - min[1] + 2.0 * padding) / spacing).ceil() as usize + 1,
((max[2] - min[2] + 2.0 * padding) / spacing).ceil() as usize + 1,
];
(origin, dims)
}
fn evaluate_ao_at_point(orb: &AtomicOrbital, point_ang: &[f64; 3]) -> f64 {
let ang_to_bohr = 1.0 / 0.529177249;
let px = point_ang[0] * ang_to_bohr - orb.center[0];
let py = point_ang[1] * ang_to_bohr - orb.center[1];
let pz = point_ang[2] * ang_to_bohr - orb.center[2];
let mut val = 0.0;
let terms = orbital_cartesian_terms(orb.l, orb.m);
for g in &orb.gaussians {
let mut g_val = 0.0;
for (coef, [lx, ly, lz]) in &terms {
g_val += coef * gaussian_cartesian_value(g.alpha, px, py, pz, *lx, *ly, *lz);
}
val += g.coeff * g_val;
}
val
}
pub fn evaluate_orbital_on_grid(
basis: &[AtomicOrbital],
coefficients: &[Vec<f64>],
mo_index: usize,
positions: &[[f64; 3]],
spacing: f64,
padding: f64,
) -> VolumetricGrid {
let (origin, dims) = compute_grid_extents(positions, padding, spacing);
let total = dims[0] * dims[1] * dims[2];
let mut values = vec![0.0f32; total];
let c_mu: Vec<f64> = (0..basis.len())
.map(|mu| coefficients[mu][mo_index])
.collect();
for ix in 0..dims[0] {
for iy in 0..dims[1] {
for iz in 0..dims[2] {
let point = [
origin[0] + ix as f64 * spacing,
origin[1] + iy as f64 * spacing,
origin[2] + iz as f64 * spacing,
];
let mut psi = 0.0f64;
for (mu, orb) in basis.iter().enumerate() {
let phi = evaluate_ao_at_point(orb, &point);
psi += c_mu[mu] * phi;
}
let idx = ix * dims[1] * dims[2] + iy * dims[2] + iz;
values[idx] = psi as f32;
}
}
}
VolumetricGrid {
origin,
spacing,
dims,
values,
}
}
#[cfg(feature = "parallel")]
pub fn evaluate_orbital_on_grid_parallel(
basis: &[AtomicOrbital],
coefficients: &[Vec<f64>],
mo_index: usize,
positions: &[[f64; 3]],
spacing: f64,
padding: f64,
) -> VolumetricGrid {
use rayon::prelude::*;
let (origin, dims) = compute_grid_extents(positions, padding, spacing);
let total = dims[0] * dims[1] * dims[2];
let c_mu: Vec<f64> = (0..basis.len())
.map(|mu| coefficients[mu][mo_index])
.collect();
let ny = dims[1];
let nz = dims[2];
let values: Vec<f32> = (0..total)
.into_par_iter()
.map(|flat_idx| {
let ix = flat_idx / (ny * nz);
let iy = (flat_idx / nz) % ny;
let iz = flat_idx % nz;
let point = [
origin[0] + ix as f64 * spacing,
origin[1] + iy as f64 * spacing,
origin[2] + iz as f64 * spacing,
];
let mut psi = 0.0f64;
for (mu, orb) in basis.iter().enumerate() {
let phi = evaluate_ao_at_point(orb, &point);
psi += c_mu[mu] * phi;
}
psi as f32
})
.collect();
VolumetricGrid {
origin,
spacing,
dims,
values,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VolumeSlab {
pub ix: usize,
pub values: Vec<f32>,
}
pub fn evaluate_orbital_on_grid_chunked(
basis: &[AtomicOrbital],
coefficients: &[Vec<f64>],
mo_index: usize,
positions: &[[f64; 3]],
spacing: f64,
padding: f64,
) -> (VolumetricGrid, Vec<VolumeSlab>) {
let (origin, dims) = compute_grid_extents(positions, padding, spacing);
let c_mu: Vec<f64> = (0..basis.len())
.map(|mu| coefficients[mu][mo_index])
.collect();
let mut slabs = Vec::with_capacity(dims[0]);
let mut all_values = Vec::with_capacity(dims[0] * dims[1] * dims[2]);
for ix in 0..dims[0] {
let slab_size = dims[1] * dims[2];
let mut slab_values = Vec::with_capacity(slab_size);
for iy in 0..dims[1] {
for iz in 0..dims[2] {
let point = [
origin[0] + ix as f64 * spacing,
origin[1] + iy as f64 * spacing,
origin[2] + iz as f64 * spacing,
];
let mut psi = 0.0f64;
for (mu, orb) in basis.iter().enumerate() {
let phi = evaluate_ao_at_point(orb, &point);
psi += c_mu[mu] * phi;
}
slab_values.push(psi as f32);
}
}
all_values.extend_from_slice(&slab_values);
slabs.push(VolumeSlab {
ix,
values: slab_values,
});
}
let grid = VolumetricGrid {
origin,
spacing,
dims,
values: all_values,
};
(grid, slabs)
}
pub fn evaluate_density_on_grid(
basis: &[AtomicOrbital],
coefficients: &[Vec<f64>],
mo_index: usize,
positions: &[[f64; 3]],
spacing: f64,
padding: f64,
) -> VolumetricGrid {
let mut grid =
evaluate_orbital_on_grid(basis, coefficients, mo_index, positions, spacing, padding);
for v in &mut grid.values {
*v = (*v) * (*v);
}
grid
}
#[cfg(test)]
mod tests {
use super::super::basis::build_basis;
use super::super::solver::solve_eht;
use super::*;
#[test]
fn test_grid_extents_h2() {
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let (origin, dims) = compute_grid_extents(&positions, 4.0, 0.5);
assert!(origin[0] < -3.9);
assert!(dims[0] > 10);
assert!(dims[1] > 10);
assert!(dims[2] > 10);
}
#[test]
fn test_evaluate_orbital_h2_bonding() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let result = solve_eht(&elements, &positions, None).unwrap();
let basis = build_basis(&elements, &positions);
let grid = evaluate_orbital_on_grid(
&basis,
&result.coefficients,
0, &positions,
0.5, 3.0, );
assert!(grid.num_points() > 0);
let max_val = grid.values.iter().map(|v| v.abs()).fold(0.0f32, f32::max);
assert!(
max_val > 0.01,
"max |Ψ_bond| = {}, expected > 0.01",
max_val
);
}
#[test]
fn test_evaluate_orbital_h2_antibonding() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let result = solve_eht(&elements, &positions, None).unwrap();
let basis = build_basis(&elements, &positions);
let grid = evaluate_orbital_on_grid(&basis, &result.coefficients, 1, &positions, 0.5, 3.0);
let max_val = grid.values.iter().map(|v| v.abs()).fold(0.0f32, f32::max);
assert!(
max_val > 0.01,
"antibonding orbital should have nonzero amplitude"
);
let has_positive = grid.values.iter().any(|&v| v > 0.01);
let has_negative = grid.values.iter().any(|&v| v < -0.01);
assert!(
has_positive && has_negative,
"antibonding MO should have sign change"
);
}
#[test]
fn test_density_nonnegative() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let result = solve_eht(&elements, &positions, None).unwrap();
let basis = build_basis(&elements, &positions);
let grid = evaluate_density_on_grid(&basis, &result.coefficients, 0, &positions, 0.5, 3.0);
for v in &grid.values {
assert!(*v >= 0.0, "Density should never be negative: {}", v);
}
}
#[test]
fn test_grid_dimensions_consistent() {
let positions = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]];
let (_, dims) = compute_grid_extents(&positions, 2.0, 0.25);
assert!(dims[0] == 21, "dims[0] = {}", dims[0]);
}
#[test]
fn test_chunked_matches_full() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let result = solve_eht(&elements, &positions, None).unwrap();
let basis = build_basis(&elements, &positions);
let full_grid =
evaluate_orbital_on_grid(&basis, &result.coefficients, 0, &positions, 0.5, 3.0);
let (chunked_grid, slabs) =
evaluate_orbital_on_grid_chunked(&basis, &result.coefficients, 0, &positions, 0.5, 3.0);
assert_eq!(full_grid.dims, chunked_grid.dims);
assert_eq!(full_grid.origin, chunked_grid.origin);
assert_eq!(slabs.len(), chunked_grid.dims[0]);
assert_eq!(full_grid.values.len(), chunked_grid.values.len());
for (a, b) in full_grid.values.iter().zip(chunked_grid.values.iter()) {
assert!((a - b).abs() < 1e-10, "mismatch: {} vs {}", a, b);
}
let slab_size = chunked_grid.dims[1] * chunked_grid.dims[2];
for slab in &slabs {
assert_eq!(slab.values.len(), slab_size);
}
}
#[test]
fn test_metal_orbital_grid_generation_smoke() {
let elements = [26u8, 17, 17];
let positions = [[0.0, 0.0, 0.0], [2.2, 0.0, 0.0], [-2.2, 0.0, 0.0]];
let result = solve_eht(&elements, &positions, None).unwrap();
let basis = build_basis(&elements, &positions);
let grid = evaluate_orbital_on_grid(&basis, &result.coefficients, 0, &positions, 0.7, 2.5);
assert!(grid.num_points() > 0);
assert!(grid.values.iter().all(|v| v.is_finite()));
}
}