hoomd_interaction/univariate/
xplor.rs

1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4//! Implement [`WeeksChandlerAnderson`]
5
6use serde::{Deserialize, Serialize};
7
8use super::{UnivariateEnergy, UnivariateForce};
9
10/// Smoothly shift a potential (and its force) to 0 at some `r_cut`, beginning at `r_smooth`.
11///
12/// ```math
13/// U(r) = S(r) \cdot f(r)
14/// ```
15/// where:
16/// ```math
17/// S(r) =
18/// \begin{cases}
19/// 1 & r < r_{\mathrm{on}} \\
20/// \frac{(r_{\mathrm{cut}}^2 - r^2)^2 \cdot
21/// (r_{\mathrm{cut}}^2 + 2r^2 -
22/// 3r_{\mathrm{on}}^2)}{(r_{\mathrm{cut}}^2 -
23/// r_{\mathrm{on}}^2)^3}
24/// & r_{\mathrm{on}} < r \le r_{\mathrm{cut}} \\
25/// 0 & r \ge r_{\mathrm{cut}} \\
26/// \end{cases}
27/// ```
28/// # Example
29///
30/// ```
31/// use hoomd_interaction::univariate::{LennardJones, Xplor};
32///
33/// let epsilon = 1.5;
34/// let sigma = 1.0;
35/// let r_cut = 2.5 * sigma;
36/// let r_smooth = 1.5 * sigma;
37/// let xplor_lj = Xplor { f: LennardJones::<12,6> { epsilon, sigma }, r_cut, r_smooth };
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
39pub struct Xplor<F> {
40    /// The original potential.
41    pub f: F,
42    /// `r` value *(\[length\])* where the smoothed potential will be 0.
43    pub r_cut: f64,
44    /// `r` value *(\[length\])* where the smoothing function is enabled. Should be less than `r_cut`
45    pub r_smooth: f64,
46}
47
48impl<F> Xplor<F> {
49    /// The xplor shifting function
50    #[inline]
51    fn s(&self, r: f64) -> f64 {
52        if r < self.r_smooth {
53            1.0
54        } else if r > self.r_cut {
55            0.0
56        } else {
57            let r_sq = r.powi(2);
58            let r_cut_sq = self.r_cut.powi(2);
59            let r_smooth_sq = self.r_smooth.powi(2);
60            (r_cut_sq - r_sq).powi(2) * (r_cut_sq + 2.0 * r_sq - 3.0 * r_smooth_sq)
61                / (r_cut_sq - r_smooth_sq).powi(3)
62        }
63    }
64    /// Partial derivative of the xplor function with respect to r
65    #[inline]
66    fn ds_dr(&self, r: f64) -> f64 {
67        let r_sq = r.powi(2);
68        let r_cut_sq = self.r_cut.powi(2);
69        let r_smooth_sq = self.r_smooth.powi(2);
70        12.0 * r * ((r_cut_sq - r_sq) * (r_smooth_sq - r_sq)) / (r_cut_sq - r_smooth_sq).powi(3)
71    }
72}
73
74impl<F: UnivariateEnergy> UnivariateEnergy for Xplor<F> {
75    #[inline]
76    fn energy(&self, r: f64) -> f64 {
77        self.s(r) * self.f.energy(r)
78    }
79}
80
81impl<F: UnivariateForce + UnivariateEnergy> UnivariateForce for Xplor<F> {
82    #[inline]
83    fn force(&self, r: f64) -> f64 {
84        if r < self.r_smooth {
85            self.f.force(r)
86        } else if r > self.r_cut {
87            0.0
88        } else {
89            // Chain rule of -d/dr(s(r)*U(r))
90            self.s(r) * self.f.force(r) - self.ds_dr(r) * self.f.energy(r)
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use approxim::{assert_abs_diff_eq, assert_abs_diff_ne, assert_relative_eq};
99    use rstest::*;
100
101    use crate::univariate::LennardJones;
102
103    #[rstest]
104    fn special_points_12_6(
105        #[values(1.0, 2.0, 12.125, 0.25)] epsilon: f64,
106        #[values(1.0, 2.0, 0.5)] sigma: f64,
107    ) {
108        let lj: LennardJones = LennardJones { epsilon, sigma };
109        let r_smooth = 1.0; // Provides cases where r_smooth <, =, > sigma and epsilon
110        let r_cut = 2.5 * sigma;
111        let xplor_lj = Xplor {
112            f: lj.clone(),
113            r_cut,
114            r_smooth,
115        };
116
117        assert_eq!(xplor_lj.f.epsilon, epsilon);
118        assert_eq!(xplor_lj.f.sigma, sigma);
119        assert_eq!(xplor_lj.r_smooth, r_smooth);
120        assert_eq!(xplor_lj.r_cut, r_cut);
121
122        // Values should not be shifted below r_smooth
123        assert_abs_diff_eq!(xplor_lj.energy(r_smooth / 2.0), lj.energy(r_smooth / 2.0));
124        assert_abs_diff_eq!(
125            xplor_lj.energy(r_smooth.next_down()),
126            lj.energy(r_smooth.next_down())
127        );
128        assert_abs_diff_eq!(xplor_lj.force(r_smooth / 2.0), lj.force(r_smooth / 2.0));
129        assert_abs_diff_eq!(
130            xplor_lj.force(r_smooth.next_down()),
131            lj.force(r_smooth.next_down())
132        );
133
134        if sigma < r_smooth {
135            assert_abs_diff_eq!(xplor_lj.energy(sigma), 0.0);
136            assert_relative_eq!(xplor_lj.force(sigma), 24.0 * epsilon / sigma);
137        }
138
139        // Values should be zero at and above r_cut
140        assert_abs_diff_eq!(xplor_lj.energy(r_cut), 0.0);
141        assert_abs_diff_eq!(xplor_lj.energy(r_cut.next_up()), 0.0);
142        assert_abs_diff_eq!(xplor_lj.energy(r_cut * 2.0), 0.0);
143
144        assert_abs_diff_eq!(xplor_lj.force(r_cut), 0.0);
145        assert_abs_diff_eq!(xplor_lj.force(r_cut.next_up()), 0.0);
146        assert_abs_diff_eq!(xplor_lj.force(r_cut * 2.0), 0.0);
147
148        // Values should not be the same between r_smooth and r_cut
149        assert_abs_diff_ne!(
150            xplor_lj.energy(f64::midpoint(r_smooth, r_cut)),
151            lj.energy(f64::midpoint(r_smooth, r_cut))
152        );
153        assert_abs_diff_ne!(
154            xplor_lj.force(f64::midpoint(r_smooth, r_cut)),
155            lj.force(f64::midpoint(r_smooth, r_cut))
156        );
157
158        // Bottom of the well (without the xplor modification)
159        let r_min = 2.0_f64.powf(1.0 / 6.0) * sigma;
160        assert_relative_eq!(xplor_lj.energy(r_min), -epsilon * xplor_lj.s(r_min));
161        if sigma < r_smooth {
162            assert_abs_diff_eq!(xplor_lj.force(r_min), 0.0, epsilon = 1e-12);
163        }
164
165        // r = 2 sigma
166        assert_relative_eq!(
167            xplor_lj.energy(2.0 * sigma),
168            -63.0 / 1024.0 * epsilon * xplor_lj.s(2.0 * sigma)
169        );
170        assert_relative_eq!(
171            xplor_lj.force(2.0 * sigma),
172            -(93.0 * r_cut.powi(6) * epsilon - 279.0 * r_cut.powi(4) * r_smooth.powi(2) * epsilon
173                + 1476.0 * r_cut.powi(2) * r_smooth.powi(2) * sigma.powi(2) * epsilon
174                - 1440.0 * r_cut.powi(2) * sigma.powi(4) * epsilon
175                - 1440.0 * r_smooth.powi(2) * sigma.powi(4) * epsilon
176                - 192.0 * sigma.powi(6) * epsilon)
177                / (512.0 * r_cut.powi(6) * sigma
178                    - 1536.0 * r_cut.powi(4) * r_smooth.powi(2) * sigma
179                    + 1536.0 * r_cut.powi(2) * r_smooth.powi(4) * sigma
180                    - 512.0 * r_smooth.powi(6) * sigma)
181        );
182    }
183}