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