1use serde::{Deserialize, Serialize};
6use std::f64::consts::PI;
7
8use super::basis::AtomicOrbital;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct VolumetricGrid {
13 pub origin: [f64; 3],
15 pub spacing: f64,
17 pub dims: [usize; 3],
19 pub values: Vec<f32>,
21}
22
23impl VolumetricGrid {
24 pub fn num_points(&self) -> usize {
26 self.dims[0] * self.dims[1] * self.dims[2]
27 }
28
29 pub fn index(&self, ix: usize, iy: usize, iz: usize) -> usize {
31 ix * self.dims[1] * self.dims[2] + iy * self.dims[2] + iz
32 }
33
34 pub fn point_position(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
36 [
37 self.origin[0] + ix as f64 * self.spacing,
38 self.origin[1] + iy as f64 * self.spacing,
39 self.origin[2] + iz as f64 * self.spacing,
40 ]
41 }
42}
43
44pub fn compute_grid_extents(
48 positions: &[[f64; 3]],
49 padding: f64,
50 spacing: f64,
51) -> ([f64; 3], [usize; 3]) {
52 let mut min = [f64::MAX; 3];
53 let mut max = [f64::MIN; 3];
54 for pos in positions {
55 for d in 0..3 {
56 min[d] = min[d].min(pos[d]);
57 max[d] = max[d].max(pos[d]);
58 }
59 }
60
61 let origin = [min[0] - padding, min[1] - padding, min[2] - padding];
62 let dims = [
63 ((max[0] - min[0] + 2.0 * padding) / spacing).ceil() as usize + 1,
64 ((max[1] - min[1] + 2.0 * padding) / spacing).ceil() as usize + 1,
65 ((max[2] - min[2] + 2.0 * padding) / spacing).ceil() as usize + 1,
66 ];
67
68 (origin, dims)
69}
70
71fn evaluate_ao_at_point(orb: &AtomicOrbital, point_ang: &[f64; 3]) -> f64 {
75 let ang_to_bohr = 1.0 / 0.529177249;
76 let px = point_ang[0] * ang_to_bohr - orb.center[0];
77 let py = point_ang[1] * ang_to_bohr - orb.center[1];
78 let pz = point_ang[2] * ang_to_bohr - orb.center[2];
79 let r2 = px * px + py * py + pz * pz;
80
81 let mut val = 0.0;
82 for g in &orb.gaussians {
83 let exp_val = (-g.alpha * r2).exp();
84 match (orb.l, orb.m) {
85 (0, _) => {
86 let norm = (2.0 * g.alpha / PI).powf(0.75);
88 val += g.coeff * norm * exp_val;
89 }
90 (1, -1) => {
91 let norm = (128.0 * g.alpha.powi(5) / (PI * PI * PI)).powf(0.25);
93 val += g.coeff * norm * px * exp_val;
94 }
95 (1, 0) => {
96 let norm = (128.0 * g.alpha.powi(5) / (PI * PI * PI)).powf(0.25);
98 val += g.coeff * norm * py * exp_val;
99 }
100 (1, 1) => {
101 let norm = (128.0 * g.alpha.powi(5) / (PI * PI * PI)).powf(0.25);
103 val += g.coeff * norm * pz * exp_val;
104 }
105 _ => {}
106 }
107 }
108
109 val
110}
111
112pub fn evaluate_orbital_on_grid(
123 basis: &[AtomicOrbital],
124 coefficients: &[Vec<f64>],
125 mo_index: usize,
126 positions: &[[f64; 3]],
127 spacing: f64,
128 padding: f64,
129) -> VolumetricGrid {
130 let (origin, dims) = compute_grid_extents(positions, padding, spacing);
131 let total = dims[0] * dims[1] * dims[2];
132 let mut values = vec![0.0f32; total];
133
134 let c_mu: Vec<f64> = (0..basis.len())
136 .map(|mu| coefficients[mu][mo_index])
137 .collect();
138
139 for ix in 0..dims[0] {
140 for iy in 0..dims[1] {
141 for iz in 0..dims[2] {
142 let point = [
143 origin[0] + ix as f64 * spacing,
144 origin[1] + iy as f64 * spacing,
145 origin[2] + iz as f64 * spacing,
146 ];
147
148 let mut psi = 0.0f64;
149 for (mu, orb) in basis.iter().enumerate() {
150 let phi = evaluate_ao_at_point(orb, &point);
151 psi += c_mu[mu] * phi;
152 }
153
154 let idx = ix * dims[1] * dims[2] + iy * dims[2] + iz;
155 values[idx] = psi as f32;
156 }
157 }
158 }
159
160 VolumetricGrid {
161 origin,
162 spacing,
163 dims,
164 values,
165 }
166}
167
168#[cfg(feature = "parallel")]
170pub fn evaluate_orbital_on_grid_parallel(
171 basis: &[AtomicOrbital],
172 coefficients: &[Vec<f64>],
173 mo_index: usize,
174 positions: &[[f64; 3]],
175 spacing: f64,
176 padding: f64,
177) -> VolumetricGrid {
178 use rayon::prelude::*;
179
180 let (origin, dims) = compute_grid_extents(positions, padding, spacing);
181 let total = dims[0] * dims[1] * dims[2];
182 let c_mu: Vec<f64> = (0..basis.len())
183 .map(|mu| coefficients[mu][mo_index])
184 .collect();
185 let ny = dims[1];
186 let nz = dims[2];
187
188 let values: Vec<f32> = (0..total)
189 .into_par_iter()
190 .map(|flat_idx| {
191 let ix = flat_idx / (ny * nz);
192 let iy = (flat_idx / nz) % ny;
193 let iz = flat_idx % nz;
194
195 let point = [
196 origin[0] + ix as f64 * spacing,
197 origin[1] + iy as f64 * spacing,
198 origin[2] + iz as f64 * spacing,
199 ];
200
201 let mut psi = 0.0f64;
202 for (mu, orb) in basis.iter().enumerate() {
203 let phi = evaluate_ao_at_point(orb, &point);
204 psi += c_mu[mu] * phi;
205 }
206
207 psi as f32
208 })
209 .collect();
210
211 VolumetricGrid {
212 origin,
213 spacing,
214 dims,
215 values,
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct VolumeSlab {
222 pub ix: usize,
224 pub values: Vec<f32>,
226}
227
228pub fn evaluate_orbital_on_grid_chunked(
235 basis: &[AtomicOrbital],
236 coefficients: &[Vec<f64>],
237 mo_index: usize,
238 positions: &[[f64; 3]],
239 spacing: f64,
240 padding: f64,
241) -> (VolumetricGrid, Vec<VolumeSlab>) {
242 let (origin, dims) = compute_grid_extents(positions, padding, spacing);
243 let c_mu: Vec<f64> = (0..basis.len())
244 .map(|mu| coefficients[mu][mo_index])
245 .collect();
246
247 let mut slabs = Vec::with_capacity(dims[0]);
248 let mut all_values = Vec::with_capacity(dims[0] * dims[1] * dims[2]);
249
250 for ix in 0..dims[0] {
251 let slab_size = dims[1] * dims[2];
252 let mut slab_values = Vec::with_capacity(slab_size);
253
254 for iy in 0..dims[1] {
255 for iz in 0..dims[2] {
256 let point = [
257 origin[0] + ix as f64 * spacing,
258 origin[1] + iy as f64 * spacing,
259 origin[2] + iz as f64 * spacing,
260 ];
261
262 let mut psi = 0.0f64;
263 for (mu, orb) in basis.iter().enumerate() {
264 let phi = evaluate_ao_at_point(orb, &point);
265 psi += c_mu[mu] * phi;
266 }
267
268 slab_values.push(psi as f32);
269 }
270 }
271
272 all_values.extend_from_slice(&slab_values);
273 slabs.push(VolumeSlab {
274 ix,
275 values: slab_values,
276 });
277 }
278
279 let grid = VolumetricGrid {
280 origin,
281 spacing,
282 dims,
283 values: all_values,
284 };
285
286 (grid, slabs)
287}
288
289pub fn evaluate_density_on_grid(
291 basis: &[AtomicOrbital],
292 coefficients: &[Vec<f64>],
293 mo_index: usize,
294 positions: &[[f64; 3]],
295 spacing: f64,
296 padding: f64,
297) -> VolumetricGrid {
298 let mut grid =
299 evaluate_orbital_on_grid(basis, coefficients, mo_index, positions, spacing, padding);
300 for v in &mut grid.values {
301 *v = (*v) * (*v);
302 }
303 grid
304}
305
306#[cfg(test)]
307mod tests {
308 use super::super::basis::build_basis;
309 use super::super::solver::solve_eht;
310 use super::*;
311
312 #[test]
313 fn test_grid_extents_h2() {
314 let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
315 let (origin, dims) = compute_grid_extents(&positions, 4.0, 0.5);
316 assert!(origin[0] < -3.9);
317 assert!(dims[0] > 10);
318 assert!(dims[1] > 10);
319 assert!(dims[2] > 10);
320 }
321
322 #[test]
323 fn test_evaluate_orbital_h2_bonding() {
324 let elements = [1u8, 1];
325 let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
326 let result = solve_eht(&elements, &positions, None).unwrap();
327 let basis = build_basis(&elements, &positions);
328
329 let grid = evaluate_orbital_on_grid(
331 &basis,
332 &result.coefficients,
333 0, &positions,
335 0.5, 3.0, );
338
339 assert!(grid.num_points() > 0);
340 let max_val = grid.values.iter().map(|v| v.abs()).fold(0.0f32, f32::max);
342 assert!(
343 max_val > 0.01,
344 "max |Ψ_bond| = {}, expected > 0.01",
345 max_val
346 );
347 }
348
349 #[test]
350 fn test_evaluate_orbital_h2_antibonding() {
351 let elements = [1u8, 1];
352 let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
353 let result = solve_eht(&elements, &positions, None).unwrap();
354 let basis = build_basis(&elements, &positions);
355
356 let grid = evaluate_orbital_on_grid(&basis, &result.coefficients, 1, &positions, 0.5, 3.0);
358
359 let max_val = grid.values.iter().map(|v| v.abs()).fold(0.0f32, f32::max);
360 assert!(
361 max_val > 0.01,
362 "antibonding orbital should have nonzero amplitude"
363 );
364
365 let has_positive = grid.values.iter().any(|&v| v > 0.01);
367 let has_negative = grid.values.iter().any(|&v| v < -0.01);
368 assert!(
369 has_positive && has_negative,
370 "antibonding MO should have sign change"
371 );
372 }
373
374 #[test]
375 fn test_density_nonnegative() {
376 let elements = [1u8, 1];
377 let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
378 let result = solve_eht(&elements, &positions, None).unwrap();
379 let basis = build_basis(&elements, &positions);
380
381 let grid = evaluate_density_on_grid(&basis, &result.coefficients, 0, &positions, 0.5, 3.0);
382
383 for v in &grid.values {
384 assert!(*v >= 0.0, "Density should never be negative: {}", v);
385 }
386 }
387
388 #[test]
389 fn test_grid_dimensions_consistent() {
390 let positions = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]];
391 let (_, dims) = compute_grid_extents(&positions, 2.0, 0.25);
392 assert!(dims[0] == 21, "dims[0] = {}", dims[0]);
394 }
395
396 #[test]
397 fn test_chunked_matches_full() {
398 let elements = [1u8, 1];
399 let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
400 let result = solve_eht(&elements, &positions, None).unwrap();
401 let basis = build_basis(&elements, &positions);
402
403 let full_grid =
404 evaluate_orbital_on_grid(&basis, &result.coefficients, 0, &positions, 0.5, 3.0);
405 let (chunked_grid, slabs) =
406 evaluate_orbital_on_grid_chunked(&basis, &result.coefficients, 0, &positions, 0.5, 3.0);
407
408 assert_eq!(full_grid.dims, chunked_grid.dims);
410 assert_eq!(full_grid.origin, chunked_grid.origin);
411 assert_eq!(slabs.len(), chunked_grid.dims[0]);
412
413 assert_eq!(full_grid.values.len(), chunked_grid.values.len());
415 for (a, b) in full_grid.values.iter().zip(chunked_grid.values.iter()) {
416 assert!((a - b).abs() < 1e-10, "mismatch: {} vs {}", a, b);
417 }
418
419 let slab_size = chunked_grid.dims[1] * chunked_grid.dims[2];
421 for slab in &slabs {
422 assert_eq!(slab.values.len(), slab_size);
423 }
424 }
425}