sci_form/pm3/
gradients.rs1use nalgebra::DMatrix;
13use serde::{Deserialize, Serialize};
14
15use super::params::get_pm3_params;
16use super::solver::{
17 screened_coulomb_gamma_derivative_ev_per_angstrom, screened_coulomb_gamma_ev,
18 solve_pm3_with_state, sto_ss_overlap, ANGSTROM_TO_BOHR,
19};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Pm3GradientResult {
24 pub gradients: Vec<[f64; 3]>,
26 pub energy: f64,
28 pub heat_of_formation: f64,
30}
31
32pub fn compute_pm3_gradient(
39 elements: &[u8],
40 positions: &[[f64; 3]],
41) -> Result<Pm3GradientResult, String> {
42 let (result, state) = solve_pm3_with_state(elements, positions)?;
43
44 let n_atoms = elements.len();
45 let n_basis = state.basis_map.len();
46 let n_occ = state.n_occ;
47
48 let mut w_mat = DMatrix::zeros(n_basis, n_basis);
50 for i in 0..n_basis {
51 for j in 0..n_basis {
52 let mut val = 0.0;
53 for k in 0..n_occ.min(n_basis) {
54 val += state.orbital_energies[k]
55 * state.coefficients[(i, k)]
56 * state.coefficients[(j, k)];
57 }
58 w_mat[(i, j)] = 2.0 * val;
59 }
60 }
61
62 let mut atom_pop = vec![0.0f64; n_atoms];
64 for mu in 0..n_basis {
65 atom_pop[state.basis_map[mu].0] += state.density[(mu, mu)];
66 }
67
68 let mut gradients = vec![[0.0f64; 3]; n_atoms];
69 let h_step = 1e-6; let compute_pair = |a: usize, b: usize| -> [f64; 3] {
73 let pa = get_pm3_params(elements[a]).unwrap();
74 let pb = get_pm3_params(elements[b]).unwrap();
75
76 let dx = positions[a][0] - positions[b][0];
77 let dy = positions[a][1] - positions[b][1];
78 let dz = positions[a][2] - positions[b][2];
79 let r_ang = (dx * dx + dy * dy + dz * dz).sqrt();
80 if r_ang < 0.01 {
81 return [0.0; 3];
82 }
83
84 let r_bohr = r_ang * ANGSTROM_TO_BOHR;
85 let dir = [dx / r_ang, dy / r_ang, dz / r_ang];
86 let mut grad_a = [0.0f64; 3];
87
88 let gamma = screened_coulomb_gamma_ev(r_bohr);
90 let dgamma_dr_ang = screened_coulomb_gamma_derivative_ev_per_angstrom(r_bohr);
91
92 let alpha_a_term = (-pa.alpha * r_ang).exp();
93 let alpha_b_term = (-pb.alpha * r_ang).exp();
94 let alpha_term = alpha_a_term + alpha_b_term;
95 let dalpha_dr = -pa.alpha * alpha_a_term - pb.alpha * alpha_b_term;
96
97 let de_nuc_dr = pa.core_charge
98 * pb.core_charge
99 * (dgamma_dr_ang * (1.0 + alpha_term) + gamma * dalpha_dr);
100
101 for d in 0..3 {
102 grad_a[d] += de_nuc_dr * dir[d];
103 }
104
105 let de_2c_dr = atom_pop[a] * atom_pop[b] * dgamma_dr_ang;
107 for d in 0..3 {
108 grad_a[d] += de_2c_dr * dir[d];
109 }
110
111 for mu in 0..n_basis {
113 if state.basis_map[mu].0 != a {
114 continue;
115 }
116 let la = state.basis_map[mu].1;
117
118 for nu in 0..n_basis {
119 if state.basis_map[nu].0 != b {
120 continue;
121 }
122 let lb = state.basis_map[nu].1;
123
124 let za = if la == 0 { pa.zeta_s } else { pa.zeta_p };
125 let zb = if lb == 0 { pb.zeta_s } else { pb.zeta_p };
126
127 let s_plus = sto_ss_overlap(za, zb, r_bohr + h_step);
128 let s_minus = sto_ss_overlap(za, zb, r_bohr - h_step);
129 let mut ds_dr_bohr = (s_plus - s_minus) / (2.0 * h_step);
130
131 if la != 0 || lb != 0 {
132 ds_dr_bohr *= 0.5;
133 }
134
135 let ds_dr_ang = ds_dr_bohr * ANGSTROM_TO_BOHR;
136
137 let beta_mu = if la == 0 { pa.beta_s } else { pa.beta_p };
138 let beta_nu = if lb == 0 { pb.beta_s } else { pb.beta_p };
139 let dh_dr = 0.5 * (beta_mu + beta_nu) * ds_dr_ang;
140
141 let p_mn = state.density[(mu, nu)];
142 let w_mn = w_mat[(mu, nu)];
143 let force = 2.0 * (p_mn * dh_dr - w_mn * ds_dr_ang);
144
145 for d in 0..3 {
146 grad_a[d] += force * dir[d];
147 }
148 }
149 }
150
151 grad_a
152 };
153
154 let pairs: Vec<(usize, usize)> = (0..n_atoms)
156 .flat_map(|a| ((a + 1)..n_atoms).map(move |b| (a, b)))
157 .collect();
158
159 #[cfg(feature = "parallel")]
160 {
161 use rayon::prelude::*;
162 let pair_grads: Vec<(usize, usize, [f64; 3])> = pairs
163 .par_iter()
164 .map(|&(a, b)| {
165 let g = compute_pair(a, b);
166 (a, b, g)
167 })
168 .collect();
169 for (a, b, g) in pair_grads {
170 for d in 0..3 {
171 gradients[a][d] += g[d];
172 gradients[b][d] -= g[d];
173 }
174 }
175 }
176
177 #[cfg(not(feature = "parallel"))]
178 {
179 for &(a, b) in &pairs {
180 let g = compute_pair(a, b);
181 for d in 0..3 {
182 gradients[a][d] += g[d];
183 gradients[b][d] -= g[d];
184 }
185 }
186 }
187
188 Ok(Pm3GradientResult {
189 gradients,
190 energy: result.total_energy,
191 heat_of_formation: result.heat_of_formation,
192 })
193}
194
195#[cfg(test)]
196mod tests {
197 use super::super::solver::solve_pm3;
198 use super::*;
199
200 #[test]
201 fn test_pm3_gradient_h2() {
202 let elements = [1u8, 1];
203 let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
204 let result = compute_pm3_gradient(&elements, &positions).unwrap();
205 assert_eq!(result.gradients.len(), 2);
206 assert!(result.energy.is_finite());
207 for d in 0..3 {
209 assert!(
210 (result.gradients[0][d] + result.gradients[1][d]).abs() < 0.1,
211 "Forces not equal and opposite: {:?}",
212 result.gradients
213 );
214 }
215 }
216
217 #[test]
218 fn test_pm3_gradient_water() {
219 let elements = [8u8, 1, 1];
220 let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
221 let result = compute_pm3_gradient(&elements, &positions).unwrap();
222 assert_eq!(result.gradients.len(), 3);
223 for g in &result.gradients {
224 for &v in g {
225 assert!(v.is_finite(), "Gradient must be finite");
226 }
227 }
228 for d in 0..3 {
230 let sum: f64 = result.gradients.iter().map(|g| g[d]).sum();
231 assert!(
232 sum.abs() < 1.0,
233 "Net force should be near zero, got {sum:.4}"
234 );
235 }
236 }
237
238 #[test]
239 fn test_pm3_gradient_vs_numerical() {
240 let elements = [1u8, 1];
241 let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
242 let analytical = compute_pm3_gradient(&elements, &positions).unwrap();
243
244 let h = 1e-5;
245 for a in 0..2 {
246 for d in 0..3 {
247 let mut pos_p = positions.to_vec();
248 let mut pos_m = positions.to_vec();
249 pos_p[a][d] += h;
250 pos_m[a][d] -= h;
251 let e_p = solve_pm3(&elements, &pos_p).unwrap().total_energy;
252 let e_m = solve_pm3(&elements, &pos_m).unwrap().total_energy;
253 let numerical = (e_p - e_m) / (2.0 * h);
254 let diff = (analytical.gradients[a][d] - numerical).abs();
255 let scale = numerical.abs().max(1.0);
256 assert!(
257 diff / scale < 0.5,
258 "Gradient mismatch atom {a} dir {d}: analytical={:.6} numerical={:.6} diff={:.6}",
259 analytical.gradients[a][d],
260 numerical,
261 diff
262 );
263 }
264 }
265 }
266}