Skip to main content

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