Skip to main content

sci_form/forcefield/
uff.rs

1//! Universal Force Field (UFF) energy terms: bond stretch, angle bend,
2//! torsion, inversion, and van der Waals contributions.
3//!
4//! Reference: Rappé et al., J. Am. Chem. Soc. 1992, 114, 10024–10035.
5
6use super::traits::ForceFieldContribution;
7
8/// Classical harmonic bond-stretch term in UFF.
9pub struct UffHarmonicBondStretch {
10    pub atom_i_idx: usize,
11    pub atom_j_idx: usize,
12    pub force_constant_kb: f64, // Bond stiffness constant k_b
13    pub equilibrium_r0: f64,    // Equilibrium bond length r_0
14}
15
16impl ForceFieldContribution for UffHarmonicBondStretch {
17    fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
18        let root_i = self.atom_i_idx * 3;
19        let root_j = self.atom_j_idx * 3;
20
21        let mut diff_x = coords[root_i] - coords[root_j];
22        let diff_y = coords[root_i + 1] - coords[root_j + 1];
23        let diff_z = coords[root_i + 2] - coords[root_j + 2];
24
25        let mut inter_r = (diff_x * diff_x + diff_y * diff_y + diff_z * diff_z).sqrt();
26
27        if inter_r < 1e-10 {
28            inter_r = 1e-10;
29            diff_x = 1e-10;
30        }
31
32        let spatial_delta = inter_r - self.equilibrium_r0;
33        let bond_energy = 0.5 * self.force_constant_kb * spatial_delta * spatial_delta;
34
35        let vectorial_scalar_prefactor = self.force_constant_kb * spatial_delta / inter_r;
36        let force_x = vectorial_scalar_prefactor * diff_x;
37        let force_y = vectorial_scalar_prefactor * diff_y;
38        let force_z = vectorial_scalar_prefactor * diff_z;
39
40        grad[root_i] += force_x;
41        grad[root_i + 1] += force_y;
42        grad[root_i + 2] += force_z;
43
44        grad[root_j] -= force_x;
45        grad[root_j + 1] -= force_y;
46        grad[root_j + 2] -= force_z;
47
48        bond_energy
49    }
50}
51
52/// Three-term Fourier angle-bending term used in UFF.
53pub struct UffAngleBend {
54    pub atom_i_idx: usize,
55    pub atom_j_idx: usize, // Central
56    pub atom_k_idx: usize,
57    pub force_constant_ka: f64,
58    pub equilibrium_theta0: f64,
59    pub coordination_n: usize, // 0 (linear), 3 (sp2), 4 (sp3)
60}
61
62impl ForceFieldContribution for UffAngleBend {
63    fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
64        let root_i = self.atom_i_idx * 3;
65        let root_j = self.atom_j_idx * 3;
66        let root_k = self.atom_k_idx * 3;
67
68        let r_ji = [
69            coords[root_i] - coords[root_j],
70            coords[root_i + 1] - coords[root_j + 1],
71            coords[root_i + 2] - coords[root_j + 2],
72        ];
73        let r_jk = [
74            coords[root_k] - coords[root_j],
75            coords[root_k + 1] - coords[root_j + 1],
76            coords[root_k + 2] - coords[root_j + 2],
77        ];
78
79        let d_ji = (r_ji[0] * r_ji[0] + r_ji[1] * r_ji[1] + r_ji[2] * r_ji[2]).sqrt();
80        let d_jk = (r_jk[0] * r_jk[0] + r_jk[1] * r_jk[1] + r_jk[2] * r_jk[2]).sqrt();
81
82        if d_ji < 1e-10 || d_jk < 1e-10 {
83            return 0.0;
84        }
85
86        let cos_theta = (r_ji[0] * r_jk[0] + r_ji[1] * r_jk[1] + r_ji[2] * r_jk[2]) / (d_ji * d_jk);
87        let cos_theta = cos_theta.clamp(-1.0, 1.0);
88        let sin_theta = (1.0 - cos_theta * cos_theta).sqrt().max(1e-8);
89
90        let (energy, d_e_dtheta) = match self.coordination_n {
91            0 => {
92                // Linear center (RDKit uses n=0 as a dedicated linear sentinel).
93                // Reuse the MMFF-style linear form here as a stable proxy.
94                let e = self.force_constant_ka * (1.0 + cos_theta);
95                let de = -self.force_constant_ka * sin_theta;
96                (e, de)
97            }
98            _ => {
99                // General Fourier expansion
100                let cos_theta0 = self.equilibrium_theta0.cos();
101                let sin_theta0 = self.equilibrium_theta0.sin();
102
103                let c2 = 1.0 / (4.0 * sin_theta0 * sin_theta0).max(1e-8);
104                let c1 = -4.0 * c2 * cos_theta0;
105                let c0 = c2 * (2.0 * cos_theta0 * cos_theta0 + 1.0);
106
107                let cos_2theta = 2.0 * cos_theta * cos_theta - 1.0;
108                let energy = self.force_constant_ka * (c0 + c1 * cos_theta + c2 * cos_2theta);
109
110                // dE/dtheta = ka * (-c1 * sin(theta) - 2 * c2 * sin(2*theta))
111                let sin_2theta = 2.0 * sin_theta * cos_theta;
112                let de = self.force_constant_ka * (-c1 * sin_theta - 2.0 * c2 * sin_2theta);
113                (energy, de)
114            }
115        };
116
117        // Cartesian gradient derived from the Wilson-Decius-Cross form.
118        let pre_i = d_e_dtheta / (d_ji * sin_theta);
119        let pre_k = d_e_dtheta / (d_jk * sin_theta);
120
121        for dim in 0..3 {
122            let gi = pre_i * (r_jk[dim] / d_jk - cos_theta * (r_ji[dim] / d_ji));
123            let gk = pre_k * (r_ji[dim] / d_ji - cos_theta * (r_jk[dim] / d_jk));
124
125            grad[root_i + dim] += gi;
126            grad[root_k + dim] += gk;
127            grad[root_j + dim] -= gi + gk;
128        }
129
130        energy
131    }
132}
133
134/// UFF torsion term: E = 0.5 * V * [1 - cos(phi0) * cos(n * phi)].
135pub struct UffTorsion {
136    pub atom_i_idx: usize,
137    pub atom_j_idx: usize,
138    pub atom_k_idx: usize,
139    pub atom_l_idx: usize,
140    pub force_constant_v: f64,
141    pub periodicity_n: f64,
142    pub cos_phi0: f64, // Usually 1.0 or -1.0
143}
144
145impl ForceFieldContribution for UffTorsion {
146    fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
147        let i = self.atom_i_idx * 3;
148        let j = self.atom_j_idx * 3;
149        let k = self.atom_k_idx * 3;
150        let l = self.atom_l_idx * 3;
151
152        let b1 = [
153            coords[i] - coords[j],
154            coords[i + 1] - coords[j + 1],
155            coords[i + 2] - coords[j + 2],
156        ];
157        let b2 = [
158            coords[k] - coords[j],
159            coords[k + 1] - coords[j + 1],
160            coords[k + 2] - coords[j + 2],
161        ];
162        let b3 = [
163            coords[l] - coords[k],
164            coords[l + 1] - coords[k + 1],
165            coords[l + 2] - coords[k + 2],
166        ];
167
168        let n1 = [
169            b1[1] * b2[2] - b1[2] * b2[1],
170            b1[2] * b2[0] - b1[0] * b2[2],
171            b1[0] * b2[1] - b1[1] * b2[0],
172        ];
173        let n2 = [
174            b2[1] * b3[2] - b2[2] * b3[1],
175            b2[2] * b3[0] - b2[0] * b3[2],
176            b2[0] * b3[1] - b2[1] * b3[0],
177        ];
178
179        let m1 = (n1[0] * n1[0] + n1[1] * n1[1] + n1[2] * n1[2]).sqrt();
180        let m2 = (n2[0] * n2[0] + n2[1] * n2[1] + n2[2] * n2[2]).sqrt();
181        if m1 < 1e-10 || m2 < 1e-10 {
182            return 0.0;
183        }
184
185        let cos_phi = (n1[0] * n2[0] + n1[1] * n2[1] + n1[2] * n2[2]) / (m1 * m2);
186        let cos_phi = cos_phi.clamp(-1.0, 1.0);
187        let phi = cos_phi.acos();
188
189        // Recover the sign of phi from the orientation relative to b2.
190        let cross_n1_n2 = [
191            n1[1] * n2[2] - n1[2] * n2[1],
192            n1[2] * n2[0] - n1[0] * n2[2],
193            n1[0] * n2[1] - n1[1] * n2[0],
194        ];
195        let dot_dir = cross_n1_n2[0] * b2[0] + cross_n1_n2[1] * b2[1] + cross_n1_n2[2] * b2[2];
196        let phi = if dot_dir < 0.0 { -phi } else { phi };
197
198        let energy =
199            0.5 * self.force_constant_v * (1.0 - self.cos_phi0 * (self.periodicity_n * phi).cos());
200        let d_e_dphi = 0.5
201            * self.force_constant_v
202            * self.cos_phi0
203            * self.periodicity_n
204            * (self.periodicity_n * phi).sin();
205
206        // Analytical Gradients (Blondel-Karplus)
207        let f_i = [
208            -d_e_dphi * n1[0] / (m1 * m1),
209            -d_e_dphi * n1[1] / (m1 * m1),
210            -d_e_dphi * n1[2] / (m1 * m1),
211        ];
212
213        let f_l = [
214            d_e_dphi * n2[0] / (m2 * m2),
215            d_e_dphi * n2[1] / (m2 * m2),
216            d_e_dphi * n2[2] / (m2 * m2),
217        ];
218
219        // Central atoms J and K get the remainder (lever rule)
220        // Simplified for stability:
221        for dim in 0..3 {
222            grad[i + dim] += f_i[dim];
223            grad[l + dim] += f_l[dim];
224            grad[j + dim] -= f_i[dim]; // Approximated
225            grad[k + dim] -= f_l[dim]; // Approximated
226        }
227
228        energy
229    }
230}
231
232/// Wilson-Decius-Cross inversion term for near-planar UFF centers.
233pub struct UffInversion {
234    pub idx_i: usize,
235    pub idx_j: usize, // Central
236    pub idx_k: usize,
237    pub idx_l: usize,
238    pub k_inv: f64,
239    pub c0: f64,
240    pub c1: f64,
241    pub c2: f64,
242}
243
244impl ForceFieldContribution for UffInversion {
245    fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
246        let j = self.idx_j * 3;
247        let i = self.idx_i * 3;
248        let k = self.idx_k * 3;
249        let l = self.idx_l * 3;
250
251        let r_ji = [
252            coords[i] - coords[j],
253            coords[i + 1] - coords[j + 1],
254            coords[i + 2] - coords[j + 2],
255        ];
256        let r_jk = [
257            coords[k] - coords[j],
258            coords[k + 1] - coords[j + 1],
259            coords[k + 2] - coords[j + 2],
260        ];
261        let r_jl = [
262            coords[l] - coords[j],
263            coords[l + 1] - coords[j + 1],
264            coords[l + 2] - coords[j + 2],
265        ];
266
267        // Normal al plano I-J-K
268        let n = [
269            r_ji[1] * r_jk[2] - r_ji[2] * r_jk[1],
270            r_ji[2] * r_jk[0] - r_ji[0] * r_jk[2],
271            r_ji[0] * r_jk[1] - r_ji[1] * r_jk[0],
272        ];
273        let n_len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
274        if n_len < 1e-10 {
275            return 0.0;
276        }
277
278        let r_jl_len = (r_jl[0] * r_jl[0] + r_jl[1] * r_jl[1] + r_jl[2] * r_jl[2]).sqrt();
279        if r_jl_len < 1e-10 {
280            return 0.0;
281        }
282
283        let sin_psi = (n[0] * r_jl[0] + n[1] * r_jl[1] + n[2] * r_jl[2]) / (n_len * r_jl_len);
284        let sin_psi = sin_psi.clamp(-1.0, 1.0);
285        let psi = sin_psi.asin();
286
287        let energy = self.k_inv * (self.c0 + self.c1 * sin_psi + self.c2 * (2.0 * psi).cos());
288        let d_e_dpsi = self.k_inv * (self.c1 * psi.cos() - 2.0 * self.c2 * (2.0 * psi).sin());
289
290        let cos_psi = psi.cos().max(1e-8);
291        let pre_l = d_e_dpsi / (n_len * r_jl_len * cos_psi);
292
293        for dim in 0..3 {
294            let gi = pre_l * (n[dim] - sin_psi * r_jl[dim] / r_jl_len);
295            grad[l + dim] += gi;
296            grad[j + dim] -= gi;
297        }
298
299        energy
300    }
301}
302
303/// UFF Lennard-Jones 12-6 non-bonded interaction.
304///
305/// E = ε · [(x_ij / r)¹² − 2 · (x_ij / r)⁶]
306///
307/// Combining rules (Rappé Table 1):
308///   x_ij  = (x_i + x_j) / 2          [arithmetic mean of VDW distances]
309///   ε_ij  = √(D_i · D_j) · scale     [geometric mean of well depths; scale = 0.5 for 1-4]
310pub struct UffLennardJones {
311    pub atom_i_idx: usize,
312    pub atom_j_idx: usize,
313    pub r_star: f64,  // x_ij = (x_i + x_j) / 2
314    pub epsilon: f64, // ε_ij = √(D_i · D_j) [already scaled before construction]
315}
316
317impl ForceFieldContribution for UffLennardJones {
318    fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
319        let ri = self.atom_i_idx * 3;
320        let rj = self.atom_j_idx * 3;
321
322        let dx = coords[ri] - coords[rj];
323        let dy = coords[ri + 1] - coords[rj + 1];
324        let dz = coords[ri + 2] - coords[rj + 2];
325        let r2 = (dx * dx + dy * dy + dz * dz).max(1e-6);
326        let r = r2.sqrt();
327
328        let u = self.r_star / r;
329        let u6 = u * u * u * u * u * u;
330        let u12 = u6 * u6;
331
332        let energy = self.epsilon * (u12 - 2.0 * u6);
333
334        // dE/dr = ε · (-12 u¹² + 12 u⁶) / r  →  scatter onto grad
335        let de_dr = self.epsilon * 12.0 * (u6 - u12) / r;
336        let pre = de_dr / r;
337
338        grad[ri] += pre * dx;
339        grad[ri + 1] += pre * dy;
340        grad[ri + 2] += pre * dz;
341        grad[rj] -= pre * dx;
342        grad[rj + 1] -= pre * dy;
343        grad[rj + 2] -= pre * dz;
344
345        energy
346    }
347}