hoomd_interaction/rigid.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 Rigid.
5
6use serde::{Deserialize, Serialize};
7use std::ops::{Add, AddAssign, Sub};
8
9use crate::{
10 DeltaEnergyInsert, DeltaEnergyOne, DeltaEnergyRemove, MaximumInteractionRange,
11 NetBodyForceAndVirial, NetBodyForceVirialAndTorque, NetSiteForceAndVirial,
12 NetSiteForceVirialAndTorque, TotalEnergy,
13};
14use hoomd_microstate::{
15 Body, Microstate, Transform,
16 property::{Orientation, Position},
17};
18use hoomd_vector::{Outer, Rotate, Vector, Wedge};
19
20/// Rigid body interactions.
21///
22/// The [`Rigid`] newtype implements [`NetBodyForceAndVirial`] for wrapped force
23/// interaction model types that implement [`NetSiteForceAndVirial`]. It also implements
24/// [`NetBodyForceVirialAndTorque`] for interaction model types that implement
25/// [`NetSiteForceVirialAndTorque`].
26///
27/// [`Rigid`] computes the net force and torque on a rigid body that results
28/// from the forces/torques on all of its sites:
29/// ```math
30/// \vec{F}_\mathrm{body} = \sum_{i \in \mathrm{body}} \vec{F}_{i}
31/// ```
32/// ```math
33/// \vec{\tau}_\mathrm{body} = \sum_{i \in \mathrm{body}} (\mathbf{q}_\mathrm{body} \cdot \vec{r}_{\mathrm{body},i} \cdot \mathbf{q}_\mathrm{body}^*) \wedge \vec{F}_i + \vec{\tau}_{i}
34/// ```
35///
36/// The generic type names are:
37/// * `F`: The evaluator that implements [`NetSiteForceAndVirial`] and/or [`NetSiteForceVirialAndTorque`].
38///
39/// # Example
40///
41/// ```
42/// use hoomd_interaction::{
43/// PairwiseCutoff, Rigid, pairwise::Isotropic, univariate::LennardJones,
44/// };
45///
46/// let lennard_jones: LennardJones = LennardJones {
47/// epsilon: 1.0,
48/// sigma: 1.0,
49/// };
50/// let evaluator = Isotropic {
51/// interaction: lennard_jones,
52/// r_cut: 2.5,
53/// };
54/// let rigid = Rigid(PairwiseCutoff(evaluator));
55/// ```
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57pub struct Rigid<F>(pub F);
58
59impl<V, B, S, X, C, F> NetBodyForceAndVirial<B, S, X, C> for Rigid<F>
60where
61 V: Vector + Default + Outer,
62 B: Transform<S> + Position<Position = V>,
63 S: Position<Position = V>,
64 F: NetSiteForceAndVirial<B, S, X, C, Force = V>,
65 V::Tensor: Default + AddAssign + Sub<Output = V::Tensor>,
66{
67 type Force = V;
68
69 /// Compute the net force and virial on a body in the microstate.
70 ///
71 /// The net force and virial on a body are the sums of the net forces and
72 /// virials on all sites in the body:
73 ///
74 /// ```math
75 /// \begin{align*}
76 /// \vec{F}_\mathrm{body} &= \sum_{\mathrm{site} \in \mathrm{body}} \vec{F}_\mathrm{site} \\
77 /// \mathbf{W}_\mathrm{body} &= \sum_{\mathrm{site} \in \mathrm{body}} \mathbf{W}_\mathrm{site} - \mathbf{F}_\mathrm{site} \otimes \left( \vec{r}_\mathrm{site}^\mathrm{global} - \vec{r}_{body}^\mathrm{global} \right) \\
78 /// \end{align*}
79 /// ```
80 ///
81 /// where the net site forces and virials are given by `F`'s implementation of
82 /// [`NetSiteForceAndVirial`], and $`\vec{r}_\mathrm{site}^\mathrm{global}`$ and $`\vec{r}_\mathrm{body}^\mathrm{global}`$
83 /// are the positions in the global frame of the site and body, respectively.
84 ///
85 /// The second term in the virial summation is required to correct for
86 /// the centripetal forces implicit in the rigid body constraint. For more
87 /// information, see [Glaser et al. 2020](https://doi.org/10.1016/j.commatsci.2019.109430),
88 /// especially equations 23 and 24 and algorithm 2.
89 ///
90 /// # Example
91 /// ```
92 /// use approxim::assert_relative_eq;
93 ///
94 /// use hoomd_interaction::{
95 /// NetBodyForceAndVirial, PairwiseCutoff, Rigid, pairwise::Isotropic,
96 /// univariate::LennardJones,
97 /// };
98 /// use hoomd_linear_algebra::matrix::Matrix;
99 /// use hoomd_microstate::{
100 /// Body, Microstate,
101 /// boundary::Open,
102 /// property::{OrientedPoint, Point},
103 /// };
104 /// use hoomd_vector::{Cartesian, Versor};
105 ///
106 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
107 /// let mut microstate = Microstate::new();
108 /// microstate.extend_bodies([
109 /// Body::single_site(
110 /// OrientedPoint {
111 /// position: Cartesian::from([0.0, 0.0, 0.0]),
112 /// orientation: Versor::default(),
113 /// },
114 /// Point::new(Cartesian::<3>::default()),
115 /// ),
116 /// Body::single_site(
117 /// OrientedPoint {
118 /// position: Cartesian::from([1.0, 0.0, 0.0]),
119 /// orientation: Versor::default(),
120 /// },
121 /// Point::new(Cartesian::<3>::default()),
122 /// ),
123 /// ])?;
124 ///
125 /// let lennard_jones: LennardJones = LennardJones {
126 /// epsilon: 1.0,
127 /// sigma: 1.0,
128 /// };
129 ///
130 /// let force_interaction_model = PairwiseCutoff(Isotropic {
131 /// interaction: lennard_jones,
132 /// r_cut: 2.5,
133 /// });
134 /// let rigid = Rigid(force_interaction_model);
135 ///
136 /// let (body_force_0, body_virial_0) =
137 /// rigid.net_body_force_and_virial(µstate, 0);
138 /// let (body_force_1, body_virial_1) =
139 /// rigid.net_body_force_and_virial(µstate, 1);
140 ///
141 /// assert_relative_eq!(body_force_0, Cartesian::from([-24.0, 0.0, 0.0]));
142 /// assert_eq!(
143 /// body_virial_0,
144 /// Matrix {
145 /// rows: [[12.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]
146 /// }
147 /// );
148 ///
149 /// assert_relative_eq!(body_force_1, Cartesian::from([24.0, 0.0, 0.0]));
150 /// assert_eq!(
151 /// body_virial_1,
152 /// Matrix {
153 /// rows: [[12.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]
154 /// }
155 /// );
156 /// # Ok(())
157 /// # }
158 /// ```
159 #[inline]
160 fn net_body_force_and_virial(
161 &self,
162 microstate: &Microstate<B, S, X, C>,
163 body_index: usize,
164 ) -> (V, V::Tensor) {
165 let body_position_global = microstate.bodies()[body_index].item.properties.position();
166
167 let mut total_force = V::default();
168 let mut total_virial = V::Tensor::default();
169
170 for (body_site_index, microstate_site_index) in
171 microstate.iter_body_site_indices(body_index).enumerate()
172 {
173 let (site_force, site_virial) = self
174 .0
175 .net_site_force_and_virial(microstate, microstate_site_index);
176
177 // NetBodyForceAndVirial is implemented with as few assumptions as possible.
178 // Use Transform to discover the relative position of the site in the global
179 // frame and then subtract to find it relative to the body.
180 let body = µstate.bodies()[body_index];
181 let site_position_global = *body
182 .item
183 .properties
184 .transform(&body.item.sites[body_site_index])
185 .position();
186
187 let virial_correction =
188 site_force.outer(&(site_position_global - *body_position_global));
189
190 total_force += site_force;
191 total_virial += site_virial - virial_correction;
192 }
193 (total_force, total_virial)
194 }
195}
196
197impl<V, B, S, X, C, F, R> NetBodyForceVirialAndTorque<B, S, X, C> for Rigid<F>
198where
199 V: Vector + Wedge + Default + Outer,
200 B: Transform<S> + Orientation<Rotation = R> + Position<Position = V>,
201 S: Position<Position = V>,
202 F: NetSiteForceVirialAndTorque<B, S, X, C, Force = V>,
203 R: Rotate<V>,
204 V::Bivector: Default + Add<Output = V::Bivector> + AddAssign,
205 V::Tensor: Default + AddAssign + Sub<Output = V::Tensor>,
206{
207 type Force = V;
208
209 /// Compute the net force, virial, and torque on a body in the microstate.
210 ///
211 /// The net force and virial on a body are the sums of the net forces and
212 /// virials on all sites in the body, and the net torque is the sum of the
213 /// torques resulting from those forces *and* intrinsic torques applied to
214 /// the sites:
215 ///
216 /// ```math
217 /// \begin{align*}
218 /// \vec{F}_\mathrm{body} &= \sum_{\mathrm{site} \in \mathrm{body}} \vec{F}_\mathrm{site} \\
219 /// \mathbf{W}_\mathrm{body} &= \sum_{\mathrm{site} \in \mathrm{body}} \mathbf{W}_\mathrm{site} - \mathbf{F}_\mathrm{site} \otimes \left( \vec{r}_\mathrm{site}^\mathrm{global} - \vec{r}_\mathrm{body}^\mathrm{global} \right) \\
220 /// \vec{\tau}_\mathrm{body} &= \sum_{\mathrm{site} \in \mathrm{body}} (\mathbf{q}_{body} \cdot \vec{r}_{body,site} \cdot \mathbf{q}_{body}^*) \wedge \vec{F}_\mathrm{site} + \vec{\tau}_\mathrm{site} \\
221 /// \end{align*}
222 /// ```
223 ///
224 /// where $` \mathbf{q}_{body} `$ is the body's orientation,
225 /// $` \vec{r}_{body,site} `$ is the position of site *i* in the body
226 /// frame, and the net site forces, virials, and torques are given by `F`'s
227 /// implementation of [`NetSiteForceVirialAndTorque`].
228 ///
229 /// The symbol $` \wedge `$ denotes the [`Wedge`] product. The resulting torque
230 /// $` \vec{\tau}_{body} `$ is in the system frame.
231 ///
232 /// The second term in the virial summation is required to correct for
233 /// the centripetal forces implicit in the rigid body constraint. For more
234 /// information, see [Glaser et al. 2020](https://doi.org/10.1016/j.commatsci.2019.109430),
235 /// especially equations 23 and 24 and algorithm 2.
236 ///
237 /// # Example
238 /// ```
239 /// use hoomd_interaction::{
240 /// NetBodyForceVirialAndTorque, PairwiseCutoff, Rigid,
241 /// pairwise::Isotropic, univariate::LennardJones,
242 /// };
243 ///
244 /// use hoomd_linear_algebra::matrix::Matrix;
245 /// use hoomd_microstate::{
246 /// Body, Microstate,
247 /// boundary::Open,
248 /// property::{OrientedPoint, Point},
249 /// };
250 /// use hoomd_vector::{Cartesian, Versor};
251 ///
252 /// use approxim::assert_relative_eq;
253 ///
254 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
255 /// let mut microstate = Microstate::new();
256 /// microstate.extend_bodies([
257 /// Body::single_site(
258 /// OrientedPoint {
259 /// position: Cartesian::from([0.0, 2.0, 0.0]),
260 /// orientation: Versor::default(),
261 /// },
262 /// Point::new(Cartesian::from([0.0, -2.0, 0.0])),
263 /// ),
264 /// Body::single_site(
265 /// OrientedPoint {
266 /// position: Cartesian::from([1.0, 0.0, 0.0]),
267 /// orientation: Versor::default(),
268 /// },
269 /// Point::new(Cartesian::<3>::default()),
270 /// ),
271 /// ])?;
272 ///
273 /// let lennard_jones: LennardJones = LennardJones {
274 /// epsilon: 1.0,
275 /// sigma: 1.0,
276 /// };
277 ///
278 /// let force_interaction_model = PairwiseCutoff(Isotropic {
279 /// interaction: lennard_jones,
280 /// r_cut: 2.5,
281 /// });
282 /// let rigid = Rigid(force_interaction_model);
283 ///
284 /// let (body_force, body_virial, body_torque) =
285 /// rigid.net_body_force_virial_and_torque(µstate, 0);
286 ///
287 /// assert_relative_eq!(body_force, Cartesian::from([-24.0, 0.0, 0.0]));
288 /// assert_relative_eq!(body_torque, Cartesian::from([0.0, 0.0, -48.0]));
289 /// assert_eq!(
290 /// body_virial,
291 /// Matrix {
292 /// rows: [[12.0, -48.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]
293 /// }
294 /// );
295 ///
296 /// let (body_force, body_virial, body_torque) =
297 /// rigid.net_body_force_virial_and_torque(µstate, 1);
298 ///
299 /// assert_relative_eq!(body_force, Cartesian::from([24.0, 0.0, 0.0]));
300 /// assert_relative_eq!(body_torque, Cartesian::from([0.0, 0.0, 0.0]));
301 /// assert_eq!(
302 /// body_virial,
303 /// Matrix {
304 /// rows: [[12.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]
305 /// }
306 /// );
307 /// # Ok(())
308 /// # }
309 /// ```
310 #[inline]
311 fn net_body_force_virial_and_torque(
312 &self,
313 microstate: &Microstate<B, S, X, C>,
314 body_index: usize,
315 ) -> (V, V::Tensor, V::Bivector) {
316 let mut total_force = V::default();
317 let mut total_virial = V::Tensor::default();
318 let mut total_torque = V::Bivector::default();
319
320 let q = microstate.bodies()[body_index]
321 .item
322 .properties
323 .orientation();
324
325 for (body_site_index, microstate_site_index) in
326 microstate.iter_body_site_indices(body_index).enumerate()
327 {
328 let site_body_frame = µstate.bodies()[body_index].item.sites[body_site_index];
329 let r_body_frame = site_body_frame.position();
330 let r = q.rotate(r_body_frame);
331 let (site_force, site_virial, site_torque) = self
332 .0
333 .net_site_force_virial_and_torque(microstate, microstate_site_index);
334
335 let virial_correction = site_force.outer(&r);
336
337 total_force += site_force;
338 total_virial += site_virial - virial_correction;
339 total_torque += r.wedge(&site_force) + site_torque;
340 }
341
342 (total_force, total_virial, total_torque)
343 }
344}
345
346impl<F> MaximumInteractionRange for Rigid<F>
347where
348 F: MaximumInteractionRange,
349{
350 #[inline]
351 fn maximum_interaction_range(&self) -> f64 {
352 self.0.maximum_interaction_range()
353 }
354}
355
356impl<M, F> TotalEnergy<M> for Rigid<F>
357where
358 F: TotalEnergy<M>,
359{
360 #[inline]
361 fn total_energy(&self, microstate: &M) -> f64 {
362 self.0.total_energy(microstate)
363 }
364
365 #[inline]
366 fn delta_energy_total(&self, initial_microstate: &M, final_microstate: &M) -> f64 {
367 self.0
368 .delta_energy_total(initial_microstate, final_microstate)
369 }
370}
371
372impl<B, S, X, C, F> DeltaEnergyOne<B, S, X, C> for Rigid<F>
373where
374 F: DeltaEnergyOne<B, S, X, C>,
375{
376 #[inline]
377 fn delta_energy_one(
378 &self,
379 initial_microstate: &Microstate<B, S, X, C>,
380 body_index: usize,
381 final_body: &Body<B, S>,
382 ) -> f64 {
383 self.0
384 .delta_energy_one(initial_microstate, body_index, final_body)
385 }
386}
387
388impl<B, S, X, C, F> DeltaEnergyInsert<B, S, X, C> for Rigid<F>
389where
390 F: DeltaEnergyInsert<B, S, X, C>,
391{
392 #[inline]
393 fn delta_energy_insert(
394 &self,
395 initial_microstate: &Microstate<B, S, X, C>,
396 new_body: &Body<B, S>,
397 ) -> f64 {
398 self.0.delta_energy_insert(initial_microstate, new_body)
399 }
400}
401
402impl<B, S, X, C, F> DeltaEnergyRemove<B, S, X, C> for Rigid<F>
403where
404 F: DeltaEnergyRemove<B, S, X, C>,
405{
406 #[inline]
407 fn delta_energy_remove(
408 &self,
409 initial_microstate: &Microstate<B, S, X, C>,
410 body_index: usize,
411 ) -> f64 {
412 self.0.delta_energy_remove(initial_microstate, body_index)
413 }
414}