dreid_kernel/potentials/bonded/
inversion.rs1use crate::math::Real;
2use crate::traits::AngleKernel;
3use crate::types::EnergyDiff;
4
5#[derive(Clone, Copy, Debug, Default)]
32pub struct PlanarInversion;
33
34impl<T: Real> AngleKernel<T> for PlanarInversion {
35 type Params = T;
36
37 #[inline(always)]
43 fn energy(cos_psi: T, c_half: Self::Params) -> T {
44 c_half * cos_psi * cos_psi
45 }
46
47 #[inline(always)]
56 fn diff(cos_psi: T, c_half: Self::Params) -> T {
57 let c = c_half + c_half;
58 c * cos_psi
59 }
60
61 #[inline(always)]
65 fn compute(cos_psi: T, c_half: Self::Params) -> EnergyDiff<T> {
66 let energy = c_half * cos_psi * cos_psi;
67
68 let c = c_half + c_half;
69 let diff = c * cos_psi;
70
71 EnergyDiff { energy, diff }
72 }
73}
74
75#[derive(Clone, Copy, Debug, Default)]
102pub struct UmbrellaInversion;
103
104impl<T: Real> AngleKernel<T> for UmbrellaInversion {
105 type Params = (T, T);
106
107 #[inline(always)]
113 fn energy(cos_psi: T, (c_half, cos_psi0): Self::Params) -> T {
114 let delta = cos_psi - cos_psi0;
115 c_half * delta * delta
116 }
117
118 #[inline(always)]
127 fn diff(cos_psi: T, (c_half, cos_psi0): Self::Params) -> T {
128 let c = c_half + c_half;
129 c * (cos_psi - cos_psi0)
130 }
131
132 #[inline(always)]
136 fn compute(cos_psi: T, (c_half, cos_psi0): Self::Params) -> EnergyDiff<T> {
137 let delta = cos_psi - cos_psi0;
138
139 let energy = c_half * delta * delta;
140
141 let c = c_half + c_half;
142 let diff = c * delta;
143
144 EnergyDiff { energy, diff }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use approx::assert_relative_eq;
152
153 const H: f64 = 1e-6;
158 const TOL_DIFF: f64 = 1e-4;
159
160 mod planar_inversion {
165 use super::*;
166
167 const C_HALF: f64 = 20.0; fn params() -> f64 {
170 C_HALF
171 }
172
173 #[test]
178 fn sanity_compute_equals_separate() {
179 let cos_psi = 0.3_f64;
180 let p = params();
181
182 let result = PlanarInversion::compute(cos_psi, p);
183 let energy_only = PlanarInversion::energy(cos_psi, p);
184 let diff_only = PlanarInversion::diff(cos_psi, p);
185
186 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
187 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
188 }
189
190 #[test]
191 fn sanity_f32_f64_consistency() {
192 let cos_psi = 0.4;
193 let p64 = params();
194 let p32 = C_HALF as f32;
195
196 let e64 = PlanarInversion::energy(cos_psi, p64);
197 let e32 = PlanarInversion::energy(cos_psi as f32, p32);
198
199 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
200 }
201
202 #[test]
203 fn sanity_equilibrium_planar() {
204 let result = PlanarInversion::compute(0.0, params());
205
206 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
207 assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
208 }
209
210 #[test]
215 fn stability_cos_one() {
216 let result = PlanarInversion::compute(1.0, params());
217
218 assert!(result.energy.is_finite());
219 assert!(result.diff.is_finite());
220 assert_relative_eq!(result.energy, C_HALF, epsilon = 1e-10);
221 }
222
223 #[test]
224 fn stability_cos_minus_one() {
225 let result = PlanarInversion::compute(-1.0, params());
226
227 assert!(result.energy.is_finite());
228 assert!(result.diff.is_finite());
229 assert_relative_eq!(result.energy, C_HALF, epsilon = 1e-10);
230 }
231
232 fn finite_diff_check(cos_psi: f64) {
237 let p = params();
238
239 let c_plus = cos_psi + H;
240 let c_minus = cos_psi - H;
241 let e_plus = PlanarInversion::energy(c_plus, p);
242 let e_minus = PlanarInversion::energy(c_minus, p);
243 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
244
245 let gamma = PlanarInversion::diff(cos_psi, p);
246
247 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
248 }
249
250 #[test]
251 fn finite_diff_small_deviation() {
252 finite_diff_check(0.1);
253 }
254
255 #[test]
256 fn finite_diff_medium_deviation() {
257 finite_diff_check(0.5);
258 }
259
260 #[test]
261 fn finite_diff_large_deviation() {
262 finite_diff_check(0.9);
263 }
264
265 #[test]
266 fn finite_diff_negative() {
267 finite_diff_check(-0.3);
268 }
269
270 #[test]
275 fn specific_quadratic_in_cos() {
276 let cos_psi = 0.6;
277 let expected = C_HALF * cos_psi * cos_psi;
278
279 assert_relative_eq!(
280 PlanarInversion::energy(cos_psi, params()),
281 expected,
282 epsilon = 1e-14
283 );
284 }
285
286 #[test]
287 fn specific_symmetric_deviation() {
288 let e_pos = PlanarInversion::energy(0.5, params());
289 let e_neg = PlanarInversion::energy(-0.5, params());
290
291 assert_relative_eq!(e_pos, e_neg, epsilon = 1e-14);
292 }
293
294 #[test]
295 fn specific_restoring_force_direction() {
296 let d_pos = PlanarInversion::diff(0.5, params());
297 let d_neg = PlanarInversion::diff(-0.5, params());
298
299 assert!(d_pos > 0.0);
300 assert!(d_neg < 0.0);
301 }
302 }
303
304 mod umbrella_inversion {
309 use super::*;
310
311 const C_HALF: f64 = 15.0; const COS_PSI0: f64 = 0.33; fn params() -> (f64, f64) {
315 (C_HALF, COS_PSI0)
316 }
317
318 #[test]
323 fn sanity_compute_equals_separate() {
324 let cos_psi = 0.5_f64;
325 let p = params();
326
327 let result = UmbrellaInversion::compute(cos_psi, p);
328 let energy_only = UmbrellaInversion::energy(cos_psi, p);
329 let diff_only = UmbrellaInversion::diff(cos_psi, p);
330
331 assert_relative_eq!(result.energy, energy_only, epsilon = 1e-12);
332 assert_relative_eq!(result.diff, diff_only, epsilon = 1e-12);
333 }
334
335 #[test]
336 fn sanity_f32_f64_consistency() {
337 let cos_psi = 0.4;
338 let p64 = params();
339 let p32 = (C_HALF as f32, COS_PSI0 as f32);
340
341 let e64 = UmbrellaInversion::energy(cos_psi, p64);
342 let e32 = UmbrellaInversion::energy(cos_psi as f32, p32);
343
344 assert_relative_eq!(e64, e32 as f64, epsilon = 1e-5);
345 }
346
347 #[test]
348 fn sanity_equilibrium() {
349 let result = UmbrellaInversion::compute(COS_PSI0, params());
350
351 assert_relative_eq!(result.energy, 0.0, epsilon = 1e-14);
352 assert_relative_eq!(result.diff, 0.0, epsilon = 1e-14);
353 }
354
355 #[test]
360 fn stability_cos_one() {
361 let result = UmbrellaInversion::compute(1.0, params());
362
363 assert!(result.energy.is_finite());
364 assert!(result.diff.is_finite());
365 }
366
367 #[test]
368 fn stability_cos_minus_one() {
369 let result = UmbrellaInversion::compute(-1.0, params());
370
371 assert!(result.energy.is_finite());
372 assert!(result.diff.is_finite());
373 }
374
375 fn finite_diff_check(cos_psi: f64) {
380 let p = params();
381
382 let c_plus = cos_psi + H;
383 let c_minus = cos_psi - H;
384 let e_plus = UmbrellaInversion::energy(c_plus, p);
385 let e_minus = UmbrellaInversion::energy(c_minus, p);
386 let de_dcos_numerical = (e_plus - e_minus) / (2.0 * H);
387
388 let gamma = UmbrellaInversion::diff(cos_psi, p);
389
390 assert_relative_eq!(de_dcos_numerical, gamma, epsilon = TOL_DIFF);
391 }
392
393 #[test]
394 fn finite_diff_above_equilibrium() {
395 finite_diff_check(COS_PSI0 + 0.2);
396 }
397
398 #[test]
399 fn finite_diff_below_equilibrium() {
400 finite_diff_check(COS_PSI0 - 0.2);
401 }
402
403 #[test]
404 fn finite_diff_at_zero() {
405 finite_diff_check(0.0);
406 }
407
408 #[test]
409 fn finite_diff_negative() {
410 finite_diff_check(-0.5);
411 }
412
413 #[test]
418 fn specific_quadratic_in_delta() {
419 let cos_psi = 0.6;
420 let delta = cos_psi - COS_PSI0;
421 let expected = C_HALF * delta * delta;
422
423 assert_relative_eq!(
424 UmbrellaInversion::energy(cos_psi, params()),
425 expected,
426 epsilon = 1e-14
427 );
428 }
429
430 #[test]
431 fn specific_reduces_to_planar_when_cos0_zero() {
432 let p_umbrella = (C_HALF, 0.0);
433 let p_planar = C_HALF;
434 let cos_psi = 0.5;
435
436 let e_umbrella = UmbrellaInversion::energy(cos_psi, p_umbrella);
437 let e_planar = PlanarInversion::energy(cos_psi, p_planar);
438
439 assert_relative_eq!(e_umbrella, e_planar, epsilon = 1e-14);
440 }
441
442 #[test]
443 fn specific_restoring_force_direction() {
444 let d_above = UmbrellaInversion::diff(COS_PSI0 + 0.3, params());
445 let d_below = UmbrellaInversion::diff(COS_PSI0 - 0.3, params());
446
447 assert!(d_above > 0.0);
448 assert!(d_below < 0.0);
449 }
450 }
451}