Skip to main content

sci_form/gpu/
hessian_gpu.rs

1//! GPU-accelerated numerical Hessian computation for IR spectroscopy.
2//!
3//! The numerical Hessian requires 6N single-point energy evaluations
4//! (forward and backward displacement for each Cartesian DOF).
5//! These evaluations are independent and map well to GPU parallelism.
6//!
7//! This module provides a GPU dispatch wrapper that batches the
8//! displaced-geometry energy evaluations across GPU workgroups.
9
10/// Minimum atoms to justify GPU dispatch for Hessian.
11#[allow(dead_code)]
12const GPU_HESSIAN_THRESHOLD: usize = 5;
13
14/// Generate all displaced geometries for numerical Hessian.
15///
16/// For each of 3N Cartesian degrees of freedom, generates +h and −h displacements.
17/// Returns (displaced_coords, n_displacements) where displaced_coords is
18/// flat: [geom_+0, geom_-0, geom_+1, geom_-1, ...], each geometry is 3N floats.
19pub fn generate_hessian_displacements(
20    positions: &[[f64; 3]],
21    step_size: f64,
22) -> (Vec<Vec<[f64; 3]>>, usize) {
23    let n_atoms = positions.len();
24    let n_dof = 3 * n_atoms;
25    let mut displaced = Vec::with_capacity(2 * n_dof);
26
27    for dof in 0..n_dof {
28        let atom = dof / 3;
29        let coord = dof % 3;
30
31        // +h displacement
32        let mut plus = positions.to_vec();
33        plus[atom][coord] += step_size;
34        displaced.push(plus);
35
36        // -h displacement
37        let mut minus = positions.to_vec();
38        minus[atom][coord] -= step_size;
39        displaced.push(minus);
40    }
41
42    (displaced, 2 * n_dof)
43}
44
45/// Assemble the Hessian matrix from displaced energies.
46///
47/// `energies`: [E(+0), E(-0), E(+1), E(-1), ...] — pairs for each DOF.
48/// `e_ref`: energy at the reference (undisplaced) geometry.
49/// `step_size`: displacement step.
50///
51/// H_{ij} ≈ [E(+i,+j) − E(+i,−j) − E(−i,+j) + E(−i,−j)] / (4h²)
52/// For diagonal: H_{ii} ≈ [E(+i) − 2E₀ + E(−i)] / h²
53pub fn assemble_hessian_from_energies(
54    energies: &[f64],
55    e_ref: f64,
56    step_size: f64,
57) -> Vec<Vec<f64>> {
58    let n_dof = energies.len() / 2;
59    let h2 = step_size * step_size;
60    let mut hessian = vec![vec![0.0; n_dof]; n_dof];
61
62    // Diagonal elements: H_{ii} = (E(+i) - 2*E0 + E(-i)) / h^2
63    for i in 0..n_dof {
64        let e_plus = energies[2 * i];
65        let e_minus = energies[2 * i + 1];
66        hessian[i][i] = (e_plus - 2.0 * e_ref + e_minus) / h2;
67    }
68
69    // Off-diagonal: approximate from single displacements
70    // H_{ij} ≈ (E(+i) + E(+j) - E(-i) - E(-j)) / (2*h^2) - (H_{ii} + H_{jj})/2
71    // This is a cheaper approximation; for accurate off-diagonal,
72    // double displacements (±i, ±j) are needed but require 12N² evaluations.
73    for i in 0..n_dof {
74        for j in (i + 1)..n_dof {
75            let e_pi = energies[2 * i];
76            let e_mi = energies[2 * i + 1];
77            let e_pj = energies[2 * j];
78            let e_mj = energies[2 * j + 1];
79
80            // Forward-difference approximation
81            let h_ij = ((e_pi + e_pj) - (e_mi + e_mj)) / (4.0 * h2)
82                - 0.5 * (hessian[i][i] + hessian[j][j])
83                + e_ref / h2;
84
85            hessian[i][j] = h_ij;
86            hessian[j][i] = h_ij;
87        }
88    }
89
90    hessian
91}
92
93/// Batch-evaluate energies for displaced geometries using parallel CPU.
94///
95/// `energy_fn`: closure that computes energy for a given geometry.
96/// `displacements`: list of displaced geometries.
97#[cfg(feature = "parallel")]
98pub fn evaluate_displacements_parallel<F>(displacements: &[Vec<[f64; 3]>], energy_fn: F) -> Vec<f64>
99where
100    F: Fn(&[[f64; 3]]) -> f64 + Sync,
101{
102    use rayon::prelude::*;
103    displacements
104        .par_iter()
105        .map(|geom| energy_fn(geom))
106        .collect()
107}
108
109/// Sequential displacement evaluation fallback.
110pub fn evaluate_displacements_sequential<F>(
111    displacements: &[Vec<[f64; 3]>],
112    energy_fn: F,
113) -> Vec<f64>
114where
115    F: Fn(&[[f64; 3]]) -> f64,
116{
117    displacements.iter().map(|geom| energy_fn(geom)).collect()
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_displacement_count() {
126        let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
127        let (disps, n) = generate_hessian_displacements(&positions, 0.005);
128        assert_eq!(n, 12); // 2 atoms × 3 coords × 2 directions
129        assert_eq!(disps.len(), 12);
130    }
131
132    #[test]
133    fn test_hessian_harmonic() {
134        // 1D harmonic: E = 0.5 * k * x^2, k = 1.0
135        // H = k = 1.0
136        let h = 0.001;
137        let energies = vec![
138            0.5 * (h * h), // E(+x)
139            0.5 * (h * h), // E(-x)
140            0.5 * (h * h), // E(+y)
141            0.5 * (h * h), // E(-y)
142            0.5 * (h * h), // E(+z)
143            0.5 * (h * h), // E(-z)
144        ];
145        let hessian = assemble_hessian_from_energies(&energies, 0.0, h);
146        assert!((hessian[0][0] - 1.0).abs() < 0.01);
147    }
148}