dreid_kernel/potentials/nonbonded/
hbond.rs1use crate::math::Real;
2use crate::traits::HybridKernel;
3use crate::types::HybridEnergyDiff;
4
5#[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 #[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 #[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 #[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#[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 const H: f64 = 1e-6;
207 const TOL_DIFF: f64 = 1e-4;
208
209 const D_HB: f64 = 4.0; const R_HB: f64 = 2.75; const R_HB_SQ: f64 = R_HB * R_HB;
213
214 fn params() -> (f64, f64) {
215 (D_HB, R_HB_SQ)
216 }
217
218 mod hydrogen_bond_n4 {
223 use super::*;
224
225 type HBond4 = HydrogenBond<4>;
226
227 #[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 #[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 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 #[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 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}