dreid_kernel/potentials/nonbonded/
hbond.rs

1use crate::math::Real;
2use crate::traits::HybridKernel;
3use crate::types::HybridEnergyDiff;
4
5/// DREIDING Hydrogen Bond potential (12-10).
6///
7/// # Physics
8///
9/// Models the explicit hydrogen bonding interaction (typically D-H...A) using a
10/// specific 12-10 Radial potential modulated by a $\cos^N\theta$ Angular term.
11/// Standard DREIDING uses $N=4$.
12///
13/// - **Formula**: $$ E = D_{hb} \left[ 5 \left(\frac{R_{hb}}{r}\right)^{12} - 6 \left(\frac{R_{hb}}{r}\right)^{10} \right] \cos^N \theta $$
14/// - **Derivative Factor (Radial)**: $$ D_{rad} = - \frac{1}{r} \frac{\partial E}{\partial r} = \frac{60 D_{hb}}{r^2} \left[ \left(\frac{R_{hb}}{r}\right)^{12} - \left(\frac{R_{hb}}{r}\right)^{10} \right] \cos^N \theta $$
15/// - **Derivative Factor (Angular)**: $$ D_{ang} = \frac{\partial E}{\partial (\cos\theta)} = N \cdot E_{rad} \cos^{N-1} \theta $$
16///
17/// # Parameters
18///
19/// - `d_hb`: The energy well depth $D_{hb}$.
20/// - `r_hb_sq`: The squared equilibrium distance $R_{hb}^2$.
21/// - `N`: The cosine power exponent (const generic).
22///
23/// # Inputs
24///
25/// - `r_sq`: Squared distance $r^2$ between Donor (D) and Acceptor (A).
26/// - `cos_theta`: Cosine of the angle $\theta_{DHA}$ (at Hydrogen).
27///
28/// # Implementation Notes
29///
30/// - **Cutoff**: If $\cos\theta \le 0$, energy and forces represent 0.
31/// - **Optimization**: Uses $s = (R_{hb}/r)^2$ recurrence to compute $r^{-10}$ and $r^{-12}$ efficiently.
32/// - **Generics**: Uses `const N: usize` to unroll power calculations at compile time.
33#[derive(Clone, Copy, Debug, Default)]
34pub struct HydrogenBond<const N: usize>;
35
36impl<T: Real, const N: usize> HybridKernel<T> for HydrogenBond<N> {
37    type Params = (T, T);
38
39    /// Computes only the potential energy.
40    ///
41    /// # Formula
42    ///
43    /// $$ E = D_{hb} (5s^6 - 6s^5) \cos^N \theta, \quad \text{where } s = (R_{hb}/r)^2 $$
44    #[inline(always)]
45    fn energy(r_sq: T, cos_theta: T, (d_hb, r_hb_sq): Self::Params) -> T {
46        let effective_cos = cos_theta.max(T::from(0.0));
47
48        let cos_n = pow_n_helper(effective_cos, N);
49
50        let s = r_hb_sq * r_sq.recip();
51        let s2 = s * s;
52        let s4 = s2 * s2;
53        let s5 = s4 * s;
54        let s6 = s4 * s2;
55
56        let term12 = T::from(5.0) * s6;
57        let term10 = T::from(6.0) * s5;
58
59        (d_hb * (term12 - term10)) * cos_n
60    }
61
62    /// Computes only the derivative factors.
63    ///
64    /// # Formula
65    ///
66    /// $$ D_{rad} = \frac{60 D_{hb}}{r^2} (s^6 - s^5) \cos^N \theta, \quad \text{where } s = (R_{hb}/r)^2 $$
67    /// $$ D_{ang} = N E_{rad} \cos^{N-1} \theta $$
68    ///
69    /// - `force_factor_rad` ($D_{rad}$): Used to compute the central force along the D-A axis:
70    ///   $ \vec{F}_{rad} = -D\_{rad} \cdot \vec{r}\_{DA} $
71    /// - `force_factor_ang` ($D_{ang}$): Used to compute torque-like forces on the D-H-A angle
72    ///   via the Wilson B-matrix gradient chain rule:
73    ///   $ \vec{F}_i = -D\_{ang} \cdot \nabla_i (\cos\theta) $
74    #[inline(always)]
75    fn diff(r_sq: T, cos_theta: T, (d_hb, r_hb_sq): Self::Params) -> (T, T) {
76        let effective_cos = cos_theta.max(T::from(0.0));
77
78        let inv_r2 = r_sq.recip();
79        let s = r_hb_sq * inv_r2;
80        let s2 = s * s;
81        let s4 = s2 * s2;
82        let s5 = s4 * s;
83        let s6 = s4 * s2;
84
85        let term12 = T::from(5.0) * s6;
86        let term10 = T::from(6.0) * s5;
87        let e_rad_pure = d_hb * (term12 - term10);
88
89        let cos_n_minus_1 = if N == 0 {
90            T::from(0.0)
91        } else if N == 1 {
92            T::from(1.0)
93        } else {
94            pow_n_helper(effective_cos, N - 1)
95        };
96
97        let cos_n = if N == 0 {
98            T::from(1.0)
99        } else {
100            cos_n_minus_1 * effective_cos
101        };
102
103        let diff_rad_pure = T::from(60.0) * d_hb * inv_r2 * (s6 - s5);
104        let force_factor_rad = diff_rad_pure * cos_n;
105
106        let force_factor_ang = T::from(N as f32) * e_rad_pure * cos_n_minus_1;
107
108        (force_factor_rad, force_factor_ang)
109    }
110
111    /// Computes both energy and derivative factors efficiently.
112    ///
113    /// This method reuses intermediate calculations to minimize operations.
114    #[inline(always)]
115    fn compute(r_sq: T, cos_theta: T, (d_hb, r_hb_sq): Self::Params) -> HybridEnergyDiff<T> {
116        let effective_cos = cos_theta.max(T::from(0.0));
117
118        let inv_r2 = r_sq.recip();
119        let s = r_hb_sq * inv_r2;
120        let s2 = s * s;
121        let s4 = s2 * s2;
122        let s5 = s4 * s;
123        let s6 = s4 * s2;
124
125        let term12 = T::from(5.0) * s6;
126        let term10 = T::from(6.0) * s5;
127        let e_rad_pure = d_hb * (term12 - term10);
128
129        let cos_n_minus_1 = if N == 0 {
130            T::from(0.0)
131        } else if N == 1 {
132            T::from(1.0)
133        } else {
134            pow_n_helper(effective_cos, N - 1)
135        };
136
137        let cos_n = if N == 0 {
138            T::from(1.0)
139        } else {
140            cos_n_minus_1 * effective_cos
141        };
142
143        let energy = e_rad_pure * cos_n;
144
145        let diff_rad_pure = T::from(60.0) * d_hb * inv_r2 * (s6 - s5);
146        let force_factor_rad = diff_rad_pure * cos_n;
147
148        let force_factor_ang = T::from(N as f32) * e_rad_pure * cos_n_minus_1;
149
150        HybridEnergyDiff {
151            energy,
152            force_factor_rad,
153            force_factor_ang,
154        }
155    }
156}
157
158/// Helper to compute x^n using explicit unrolling for small common powers,
159/// and fast exponentiation for larger n.
160#[inline(always)]
161fn pow_n_helper<T: Real>(base: T, n: usize) -> T {
162    match n {
163        0 => T::from(1.0),
164        1 => base,
165        2 => base * base,
166        3 => base * base * base,
167        4 => {
168            let x2 = base * base;
169            x2 * x2
170        }
171        5 => {
172            let x2 = base * base;
173            let x4 = x2 * x2;
174            x4 * base
175        }
176        6 => {
177            let x2 = base * base;
178            let x4 = x2 * x2;
179            x4 * x2
180        }
181        _ => {
182            let mut acc = T::from(1.0);
183            let mut b = base;
184            let mut e = n;
185            while e > 0 {
186                if e & 1 == 1 {
187                    acc = acc * b;
188                }
189                b = b * b;
190                e >>= 1;
191            }
192            acc
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use approx::assert_relative_eq;
201
202    // ------------------------------------------------------------------------
203    // Test Constants
204    // ------------------------------------------------------------------------
205
206    const H: f64 = 1e-6;
207    const TOL_DIFF: f64 = 1e-4;
208
209    // Typical H-bond parameters
210    const D_HB: f64 = 4.0; // kcal/mol
211    const R_HB: f64 = 2.75; // Å
212    const R_HB_SQ: f64 = R_HB * R_HB;
213
214    fn params() -> (f64, f64) {
215        (D_HB, R_HB_SQ)
216    }
217
218    // ========================================================================
219    // HydrogenBond<4> Tests (Standard DREIDING)
220    // ========================================================================
221
222    mod hydrogen_bond_n4 {
223        use super::*;
224
225        type HBond4 = HydrogenBond<4>;
226
227        // --------------------------------------------------------------------
228        // 1. Sanity Checks
229        // --------------------------------------------------------------------
230
231        #[test]
232        fn sanity_compute_equals_separate() {
233            let r_sq = 9.0_f64;
234            let cos_theta = 0.9;
235            let p = params();
236
237            let result = HBond4::compute(r_sq, cos_theta, p);
238            let energy_only = HBond4::energy(r_sq, cos_theta, p);
239            let (rad_only, ang_only) = HBond4::diff(r_sq, cos_theta, p);
240
241            assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
242            assert_relative_eq!(result.force_factor_rad, rad_only, epsilon = 1e-12);
243            assert_relative_eq!(result.force_factor_ang, ang_only, epsilon = 1e-12);
244        }
245
246        #[test]
247        fn sanity_f32_f64_consistency() {
248            let r_sq = 9.0;
249            let cos_theta = 0.8;
250            let p64 = params();
251            let p32 = (D_HB as f32, R_HB_SQ as f32);
252
253            let e64 = HBond4::energy(r_sq, cos_theta, p64);
254            let e32 = HBond4::energy(r_sq as f32, cos_theta as f32, p32);
255
256            assert_relative_eq!(e64, e32 as f64, epsilon = 1e-4);
257        }
258
259        #[test]
260        fn sanity_equilibrium_energy_minimum() {
261            let e = HBond4::energy(R_HB_SQ, 1.0, params());
262            assert_relative_eq!(e, -D_HB, epsilon = 1e-10);
263        }
264
265        // --------------------------------------------------------------------
266        // 2. Numerical Stability
267        // --------------------------------------------------------------------
268
269        #[test]
270        fn stability_cos_theta_zero() {
271            let result = HBond4::compute(R_HB_SQ, 0.0, params());
272
273            assert!(result.energy.is_finite());
274            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
275            assert_relative_eq!(result.force_factor_rad, 0.0, epsilon = 1e-14);
276        }
277
278        #[test]
279        fn stability_cos_theta_negative() {
280            let result = HBond4::compute(R_HB_SQ, -0.5, params());
281
282            assert!(result.energy.is_finite());
283            assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
284        }
285
286        #[test]
287        fn stability_large_distance() {
288            let result = HBond4::compute(1e4, 1.0, params());
289
290            assert!(result.energy.is_finite());
291            assert!(result.force_factor_rad.is_finite());
292            assert!(result.energy.abs() < 1e-10);
293        }
294
295        // --------------------------------------------------------------------
296        // 3. Finite Difference Verification
297        // --------------------------------------------------------------------
298
299        fn finite_diff_radial(r: f64, cos_theta: f64) {
300            let p = params();
301            let r_sq = r * r;
302
303            let r_plus = r + H;
304            let r_minus = r - H;
305            let e_plus = HBond4::energy(r_plus * r_plus, cos_theta, p);
306            let e_minus = HBond4::energy(r_minus * r_minus, cos_theta, p);
307            let de_dr_numerical = (e_plus - e_minus) / (2.0 * H);
308
309            let (d_rad, _) = HBond4::diff(r_sq, cos_theta, p);
310            let de_dr_analytic = -d_rad * r;
311
312            assert_relative_eq!(de_dr_numerical, de_dr_analytic, epsilon = TOL_DIFF);
313        }
314
315        fn finite_diff_angular(r_sq: f64, cos_theta: f64) {
316            let p = params();
317
318            let c_plus = cos_theta + H;
319            let c_minus = cos_theta - H;
320            let e_plus = HBond4::energy(r_sq, c_plus, p);
321            let e_minus = HBond4::energy(r_sq, c_minus, p);
322            let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
323
324            let (_, d_ang) = HBond4::diff(r_sq, cos_theta, p);
325
326            assert_relative_eq!(de_dcos_numerical, d_ang, epsilon = TOL_DIFF);
327        }
328
329        #[test]
330        fn finite_diff_radial_short() {
331            finite_diff_radial(2.0, 0.9);
332        }
333
334        #[test]
335        fn finite_diff_radial_equilibrium() {
336            finite_diff_radial(R_HB, 0.8);
337        }
338
339        #[test]
340        fn finite_diff_radial_long() {
341            finite_diff_radial(5.0, 0.95);
342        }
343
344        #[test]
345        fn finite_diff_angular_strong() {
346            finite_diff_angular(R_HB_SQ, 0.9);
347        }
348
349        #[test]
350        fn finite_diff_angular_weak() {
351            finite_diff_angular(R_HB_SQ, 0.3);
352        }
353
354        // --------------------------------------------------------------------
355        // 4. H-Bond Specific
356        // --------------------------------------------------------------------
357
358        #[test]
359        fn specific_cos4_scaling() {
360            let e1 = HBond4::energy(R_HB_SQ, 1.0, params());
361            let e_half = HBond4::energy(R_HB_SQ, 0.5, params());
362
363            assert_relative_eq!(e1 / e_half, 16.0, epsilon = 1e-10);
364        }
365
366        #[test]
367        fn specific_12_10_radial_form() {
368            let e_large = HBond4::energy(100.0, 1.0, params());
369            let e_small = HBond4::energy(1.0, 1.0, params());
370
371            assert!(e_large < 0.0);
372            assert!(e_small > 0.0);
373        }
374    }
375
376    // ========================================================================
377    // HydrogenBond<0> Tests (No Angular Dependence)
378    // ========================================================================
379
380    mod hydrogen_bond_n0 {
381        use super::*;
382
383        type HBond0 = HydrogenBond<0>;
384
385        #[test]
386        fn n0_energy_independent_of_angle() {
387            let p = params();
388            let e1 = HBond0::energy(R_HB_SQ, 0.0, p);
389            let e2 = HBond0::energy(R_HB_SQ, 0.5, p);
390            let e3 = HBond0::energy(R_HB_SQ, 1.0, p);
391
392            assert_relative_eq!(e1, e2, epsilon = 1e-14);
393            assert_relative_eq!(e2, e3, epsilon = 1e-14);
394        }
395
396        #[test]
397        fn n0_angular_derivative_zero() {
398            let (_, d_ang) = HBond0::diff(R_HB_SQ, 0.8, params());
399            assert_relative_eq!(d_ang, 0.0, epsilon = 1e-14);
400        }
401    }
402
403    mod pow_helper {
404        use super::*;
405
406        #[test]
407        fn pow_n_0() {
408            assert_relative_eq!(pow_n_helper(5.0_f64, 0), 1.0, epsilon = 1e-14);
409        }
410
411        #[test]
412        fn pow_n_1() {
413            assert_relative_eq!(pow_n_helper(5.0_f64, 1), 5.0, epsilon = 1e-14);
414        }
415
416        #[test]
417        fn pow_n_4() {
418            assert_relative_eq!(pow_n_helper(2.0_f64, 4), 16.0, epsilon = 1e-14);
419        }
420
421        #[test]
422        fn pow_n_large() {
423            assert_relative_eq!(pow_n_helper(2.0_f64, 10), 1024.0, epsilon = 1e-10);
424        }
425    }
426}