dreid_kernel/potentials/nonbonded/
hbond.rs1use crate::math::Real;
2use crate::traits::HybridKernel;
3use crate::types::HybridEnergyDiff;
4
5#[derive(Clone, Copy, Debug, Default)]
39pub struct HydrogenBond<const N: usize>;
40
41impl<const N: usize> HydrogenBond<N> {
42 #[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 #[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 #[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 #[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#[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 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 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 #[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 #[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 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 #[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 #[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 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}