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}