Skip to main content

hoomd_interaction/pairwise/
isotropic.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 Isotropic
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    MaximumInteractionRange, SitePairEnergy, SitePairForceAndVirial, SitePairForceVirialAndTorque,
10    univariate::{UnivariateEnergy, UnivariateForce},
11};
12use hoomd_microstate::property::Position;
13use hoomd_vector::{InnerProduct, Metric, Outer, Wedge};
14
15/// Compute isotropic interactions between a pair of sites.
16///
17/// [`Isotropic`] provides a single implementation that computes pairwise
18/// interactions that are a function only of the distance between sites. It
19/// fills the gap between traits like [`SitePairEnergy`] / [`SitePairForceAndVirial`]
20/// which operate on site properties and [`UnivariateEnergy`] /
21/// [`UnivariateForce`] which is a function only of the separation distance.
22///
23/// [`Isotropic`] cuts all interactions off when the distance between sites
24/// is greater than or equal to `r_cut`:
25/// ```math
26/// U_{ij} =
27/// \begin{cases}
28/// U(r_{ij}) & r_{ij} < r_\mathrm{cut} \\
29/// 0 & r_{ij} \ge r_\mathrm{cut}
30/// \end{cases}
31/// ```
32/// ```math
33/// \vec{F_{ij}} =
34/// \begin{cases}
35/// -\frac{\mathrm{d} U}{\mathrm{d} r} \biggr\rvert_{r=r_{ji}} \hat{r}_{ji} & r_{ij} < r_\mathrm{cut} \\
36/// \vec{0} & r_{ij} \ge r_\mathrm{cut}
37/// \end{cases}
38/// ```
39/// where $` U `$ is given by `E`'s [`UnivariateEnergy`] implementation and
40/// $` -\frac{\mathrm{d} U}{\mathrm{d} r} `$ is given by `E`'s [`UnivariateForce`]
41/// implementation.
42///
43/// Use [`Isotropic`] with [`PairwiseCutoff`] in MD and MC simulations.
44///
45/// [`PairwiseCutoff`]: crate::PairwiseCutoff
46/// # Example
47///
48/// ```
49/// use approxim::assert_relative_eq;
50/// use hoomd_interaction::{
51///     SitePairEnergy, SitePairForceAndVirial, pairwise::Isotropic,
52///     univariate::LennardJones,
53/// };
54/// use hoomd_microstate::property::Point;
55/// use hoomd_vector::Cartesian;
56///
57/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
58/// let a = Point {
59///     position: Cartesian::from([0.0, 0.0]),
60/// };
61/// let b = Point {
62///     position: Cartesian::from([0.0, 2.0 * 2.0_f64.powf(1.0 / 6.0)]),
63/// };
64///
65/// let lennard_jones: LennardJones = LennardJones {
66///     epsilon: 1.5,
67///     sigma: 2.0,
68/// };
69/// let lennard_jones = Isotropic {
70///     interaction: lennard_jones,
71///     r_cut: 2.5,
72/// };
73///
74/// let energy = lennard_jones.site_pair_energy(&a, &b);
75/// let (force_ab, virial_ab) =
76///     lennard_jones.site_pair_force_and_virial(&a, &b);
77/// let (force_ba, virial_ba) =
78///     lennard_jones.site_pair_force_and_virial(&b, &a);
79///
80/// assert_eq!(energy, -1.5);
81/// assert_eq!(force_ab, -force_ba);
82/// assert_relative_eq!(force_ab, Cartesian::from([0.0, 0.0]), epsilon = 1e-14);
83///
84/// # Ok(())
85/// # }
86/// ```
87#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
88pub struct Isotropic<E> {
89    /// The site-site interaction.
90    pub interaction: E,
91    /// Maximum distance between two interacting sites.
92    pub r_cut: f64,
93}
94
95impl<P, S, E> SitePairEnergy<S> for Isotropic<E>
96where
97    S: Position<Position = P>,
98    P: Metric,
99    E: UnivariateEnergy,
100{
101    /// Compute the energy contribution from a pair of sites.
102    ///
103    /// ```math
104    /// U_{ij} =
105    /// \begin{cases}
106    /// U(r_{ij}) & r_{ij} < r_\mathrm{cut} \\
107    /// 0 & r_{ij} \ge r_\mathrm{cut}
108    /// \end{cases}
109    /// ```
110    /// where $` U `$ is given by `E`'s [`UnivariateEnergy`] implementation.
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// use approxim::assert_relative_eq;
116    /// use hoomd_interaction::{
117    ///     SitePairEnergy, pairwise::Isotropic, univariate::LennardJones,
118    /// };
119    /// use hoomd_microstate::property::Point;
120    /// use hoomd_vector::Cartesian;
121    ///
122    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
123    /// let a = Point {
124    ///     position: Cartesian::from([0.0, 0.0]),
125    /// };
126    /// let b = Point {
127    ///     position: Cartesian::from([0.0, 2.0 * 2.0_f64.powf(1.0 / 6.0)]),
128    /// };
129    ///
130    /// let lennard_jones: LennardJones = LennardJones {
131    ///     epsilon: 1.5,
132    ///     sigma: 2.0,
133    /// };
134    /// let lennard_jones = Isotropic {
135    ///     interaction: lennard_jones,
136    ///     r_cut: 2.5,
137    /// };
138    ///
139    /// let energy = lennard_jones.site_pair_energy(&a, &b);
140    ///
141    /// assert_eq!(energy, -1.5);
142    ///
143    /// # Ok(())
144    /// # }
145    /// ```
146    #[inline]
147    fn site_pair_energy(&self, site_properties_i: &S, site_properties_j: &S) -> f64 {
148        let r = site_properties_i
149            .position()
150            .distance(site_properties_j.position());
151        if r >= self.r_cut {
152            return 0.0;
153        }
154
155        self.interaction.energy(r)
156    }
157}
158
159impl<E> MaximumInteractionRange for Isotropic<E> {
160    /// The maximum interaction range for `Isotropic` is the given `r_cut`.
161    ///
162    /// # Example
163    ///
164    /// ```
165    /// use hoomd_interaction::{
166    ///     MaximumInteractionRange, pairwise::Isotropic, univariate::LennardJones,
167    /// };
168    ///
169    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
170    /// let lennard_jones: LennardJones = LennardJones {
171    ///     epsilon: 1.5,
172    ///     sigma: 2.0,
173    /// };
174    /// let lennard_jones = Isotropic {
175    ///     interaction: lennard_jones,
176    ///     r_cut: 2.5,
177    /// };
178    ///
179    /// assert_eq!(lennard_jones.maximum_interaction_range(), 2.5);
180    /// # Ok(())
181    /// # }
182    /// ```
183    #[inline]
184    fn maximum_interaction_range(&self) -> f64 {
185        self.r_cut
186    }
187}
188
189impl<V, S, E> SitePairForceAndVirial<S> for Isotropic<E>
190where
191    V: Default + InnerProduct + Outer,
192    S: Position<Position = V>,
193    E: UnivariateForce,
194    V::Tensor: Default,
195{
196    type Force = V;
197
198    /// Evaluate the force and virial on site `i` caused by site `j`.
199    ///
200    /// Isotropic forces always act along the radial direction:
201    /// ```math
202    /// \vec{F_{ij}} =
203    /// \begin{cases}
204    /// -\frac{\mathrm{d} U}{\mathrm{d} r} \biggr\rvert_{r=r_{ji}} \hat{r}_{ji} & r_{ij} < r_\mathrm{cut} \\
205    /// \vec{0} & r_{ij} \ge r_\mathrm{cut}
206    /// \end{cases}
207    /// ```
208    /// where $` -\frac{\mathrm{d} U}{\mathrm{d} r} `$ is given by `E`'s [`UnivariateForce`]
209    /// implementation.
210    ///
211    /// # Example
212    ///
213    /// ```
214    /// use approxim::assert_relative_eq;
215    /// use hoomd_interaction::{
216    ///     SitePairForceAndVirial, pairwise::Isotropic, univariate::LennardJones,
217    /// };
218    /// use hoomd_microstate::property::Point;
219    /// use hoomd_vector::Cartesian;
220    ///
221    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
222    /// let a = Point {
223    ///     position: Cartesian::from([0.0, 0.0]),
224    /// };
225    /// let b = Point {
226    ///     position: Cartesian::from([0.0, 2.0 * 2.0_f64.powf(1.0 / 6.0)]),
227    /// };
228    ///
229    /// let lennard_jones: LennardJones = LennardJones {
230    ///     epsilon: 1.5,
231    ///     sigma: 2.0,
232    /// };
233    /// let lennard_jones = Isotropic {
234    ///     interaction: lennard_jones,
235    ///     r_cut: 2.5,
236    /// };
237    ///
238    /// let (force_ab, virial_ab) =
239    ///     lennard_jones.site_pair_force_and_virial(&a, &b);
240    /// let (force_ba, virial_ba) =
241    ///     lennard_jones.site_pair_force_and_virial(&b, &a);
242    ///
243    /// assert_eq!(force_ab, -force_ba);
244    /// assert_relative_eq!(force_ab, Cartesian::from([0.0, 0.0]), epsilon = 1e-14);
245    /// # Ok(())
246    /// # }
247    /// ```
248    #[inline]
249    fn site_pair_force_and_virial(
250        &self,
251        site_properties_i: &S,
252        site_properties_j: &S,
253    ) -> (Self::Force, <Self::Force as Outer>::Tensor) {
254        let r_ji = *site_properties_i.position() - *site_properties_j.position();
255        let distance = r_ji.norm();
256
257        if distance >= self.r_cut {
258            (V::default(), V::Tensor::default())
259        } else {
260            let force = (r_ji / distance) * self.interaction.force(distance);
261            let virial = (force / 2.0).outer(&r_ji);
262            (force, virial)
263        }
264    }
265}
266
267impl<V, S, E> SitePairForceVirialAndTorque<S> for Isotropic<E>
268where
269    V: Default + InnerProduct + Wedge + Outer,
270    V::Bivector: Default,
271    S: Position<Position = V>,
272    E: UnivariateForce,
273    V::Tensor: Default,
274{
275    type Force = V;
276
277    /// Evaluate the force, virial, and torque on site `i` caused by site `j`.
278    ///
279    /// Isotropic forces always act along the radial direction:
280    /// ```math
281    /// \vec{F_{ij}} =
282    /// \begin{cases}
283    /// -\frac{\mathrm{d} U}{\mathrm{d} r} \biggr\rvert_{r=r_{ji}} \hat{r}_{ji} & r_{ij} < r_\mathrm{cut} \\
284    /// \vec{0} & r_{ij} \ge r_\mathrm{cut}
285    /// \end{cases}
286    /// ```
287    /// where $` -\frac{\mathrm{d} U}{\mathrm{d} r} `$ is given by `E`'s [`UnivariateForce`]
288    /// implementation.
289    ///
290    /// Radial forces produce zero torque.
291    #[inline]
292    fn site_pair_force_virial_and_torque(
293        &self,
294        site_properties_i: &S,
295        site_properties_j: &S,
296    ) -> (V, <Self::Force as Outer>::Tensor, V::Bivector) {
297        let r_ji = *site_properties_i.position() - *site_properties_j.position();
298        let distance = r_ji.norm();
299
300        if distance >= self.r_cut {
301            (V::default(), V::Tensor::default(), V::Bivector::default())
302        } else {
303            let force = (r_ji / distance) * self.interaction.force(distance);
304            let virial = (force / 2.0).outer(&r_ji);
305            let torque = V::Bivector::default();
306            (force, virial, torque)
307        }
308    }
309}