hoomd_microstate/boundary/
periodic.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 periodic boundary conditions.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9use hoomd_geometry::{MapPoint, Scale, Volume};
10use hoomd_utility::valid::PositiveReal;
11use rand::{Rng, distr::Distribution};
12
13use super::{Error, MaximumAllowableInteractionRange};
14
15mod cuboid;
16mod eighteight;
17
18/// Describe a simulation space that repeats in one or more directions.
19///
20/// [`Periodic`] is a newtype that wraps a shape. Use it to set the `boundary`
21/// for a [`Microstate`].
22///
23/// When bodies or sites exit the shape through one of the periodic sides of the
24/// shape, they are wrapped to the other side. Similarly, sites that are within
25/// the interaction range of one of the periodic sides appear as ghost sites just
26/// outside the opposite side. Depending on the shape type `T`, `Periodic<T>` might
27/// implement fully periodic boundaries or ones that are periodic in some directions
28/// and closed in others.
29///
30/// [`Periodic`] is implemented for the following shapes:
31/// * [`EightEight`]
32/// * [`Hypercuboid<2>`] (also known as [`Rectangle`])
33/// * [`Hypercuboid<3>`] (also known as [`Cuboid`])
34///
35/// [`EightEight`]: hoomd_geometry::shape::EightEight
36/// [`Hypercuboid<2>`]: hoomd_geometry::shape::Hypercuboid
37/// [`Hypercuboid<3>`]: hoomd_geometry::shape::Hypercuboid
38/// [`Cuboid`]: hoomd_geometry::shape::Cuboid
39/// [`Rectangle`]: hoomd_geometry::shape::Rectangle
40/// [`Microstate`]: crate::Microstate
41///
42/// # Example
43///
44/// ```
45/// use hoomd_geometry::shape::Rectangle;
46/// use hoomd_microstate::boundary::Periodic;
47///
48/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
49/// let periodic =
50///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
51/// # Ok(())
52/// # }
53/// ```
54#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
55pub struct Periodic<T> {
56    /// The largest interaction distance between two sites.
57    maximum_interaction_range: f64,
58
59    /// Bound the points that belong to the primary image.
60    shape: T,
61}
62
63impl<T> Periodic<T>
64where
65    T: MaximumAllowableInteractionRange,
66{
67    /// Construct a new periodic boundary condition.
68    ///
69    /// # Errors
70    ///
71    /// [`Error::InteractionRangeTooLarge`] when `maximum_interaction_range` is
72    /// larger than the maximum allowable interaction range by the shape.
73    ///
74    /// # Example
75    ///
76    /// ```
77    /// use hoomd_geometry::shape::Rectangle;
78    /// use hoomd_microstate::boundary::Periodic;
79    ///
80    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
81    /// let periodic =
82    ///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
83    /// # Ok(())
84    /// # }
85    /// ```
86    #[inline]
87    pub fn new(maximum_interaction_range: f64, shape: T) -> Result<Self, Error> {
88        if maximum_interaction_range > shape.maximum_allowable_interaction_range() {
89            return Err(Error::InteractionRangeTooLarge(
90                maximum_interaction_range,
91                shape.maximum_allowable_interaction_range(),
92            ));
93        }
94
95        Ok(Self {
96            maximum_interaction_range,
97            shape,
98        })
99    }
100}
101
102impl<T> Periodic<T> {
103    /// Access the boundary's shape.
104    ///
105    /// # Example
106    ///
107    /// ```
108    /// use hoomd_geometry::shape::Rectangle;
109    /// use hoomd_microstate::boundary::Periodic;
110    ///
111    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
112    /// let periodic =
113    ///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
114    ///
115    /// assert_eq!(periodic.shape().edge_lengths[0].get(), 10.0);
116    /// # Ok(())
117    /// # }
118    /// ```
119    #[inline]
120    pub fn shape(&self) -> &T {
121        &self.shape
122    }
123
124    /// Access the boundary's maximum interaction range.
125    ///
126    /// # Example
127    ///
128    /// ```
129    /// use hoomd_geometry::shape::Rectangle;
130    /// use hoomd_microstate::boundary::Periodic;
131    ///
132    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
133    /// let periodic =
134    ///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
135    ///
136    /// assert_eq!(periodic.maximum_interaction_range(), 2.5);
137    /// # Ok(())
138    /// # }
139    /// ```
140    #[expect(
141        clippy::same_name_method,
142        reason = "MaximumInteractionRange is a trait in hoomd-interaction"
143    )]
144    #[inline]
145    pub fn maximum_interaction_range(&self) -> f64 {
146        self.maximum_interaction_range
147    }
148}
149
150impl<T, V> Distribution<V> for Periodic<T>
151where
152    T: Distribution<V>,
153{
154    /// Generate points uniformly distributed in the wrapped shape.
155    ///
156    /// # Example
157    ///
158    /// ```
159    /// use rand::{SeedableRng, distr::Distribution, rngs::StdRng};
160    ///
161    /// use hoomd_geometry::{IsPointInside, shape::Hypercuboid};
162    /// use hoomd_microstate::boundary::Periodic;
163    ///
164    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
165    /// let cuboid = Hypercuboid {
166    ///     edge_lengths: [6.0.try_into()?, 8.0.try_into()?],
167    /// };
168    /// let periodic = Periodic::new(2.5, cuboid)?;
169    /// let mut rng = StdRng::seed_from_u64(1);
170    ///
171    /// let point = periodic.sample(&mut rng);
172    /// assert!(periodic.shape().is_point_inside(&point));
173    /// # Ok(())
174    /// # }
175    /// ```
176    #[inline]
177    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> V {
178        self.shape.sample(rng)
179    }
180}
181
182impl<T> Scale for Periodic<T>
183where
184    T: fmt::Debug + Scale + MaximumAllowableInteractionRange,
185{
186    /// Scale the wrapped shape.
187    ///
188    /// # Panics
189    ///
190    /// When scaling the wrapped shape, `scale_length` will panic if
191    /// the scaled maximum allowable interaction range is smaller than
192    /// `maximum_interaction_range`.
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// use hoomd_geometry::{Scale, shape::Rectangle};
198    /// use hoomd_microstate::boundary::Periodic;
199    ///
200    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
201    /// let periodic =
202    ///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
203    ///
204    /// let scaled_periodic = periodic.scale_length(0.5.try_into()?);
205    ///
206    /// assert_eq!(scaled_periodic.maximum_interaction_range(), 2.5);
207    /// assert_eq!(scaled_periodic.shape().edge_lengths[0].get(), 5.0);
208    /// # Ok(())
209    /// # }
210    /// ```
211    ///
212    /// ```should_panic
213    /// use hoomd_geometry::{Scale, shape::Rectangle};
214    /// use hoomd_microstate::boundary::Periodic;
215    ///
216    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
217    /// let periodic =
218    ///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
219    ///
220    /// let scaled_periodic = periodic.scale_length(0.2.try_into()?);
221    /// # Ok(())
222    /// # }
223    /// ```
224    #[inline]
225    fn scale_length(&self, v: PositiveReal) -> Self {
226        let new_shape = self.shape.scale_length(v);
227        assert!(
228            new_shape.maximum_allowable_interaction_range() >= self.maximum_interaction_range,
229            "The scaled periodic boundary {new_shape:?} is too small for the maximum interaction range {}",
230            self.maximum_interaction_range
231        );
232
233        Self {
234            shape: new_shape,
235            ..*self
236        }
237    }
238
239    /// Scale the wrapped shape.
240    ///
241    /// # Panics
242    ///
243    /// When scaling the wrapped shape, `scale_length` will panic if
244    /// the scaled maximum allowable interaction range is smaller than
245    /// `maximum_interaction_range`.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use hoomd_geometry::{Scale, shape::Rectangle};
251    /// use hoomd_microstate::boundary::Periodic;
252    ///
253    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
254    /// let periodic =
255    ///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
256    ///
257    /// let scaled_periodic = periodic.scale_volume(4.0.try_into()?);
258    ///
259    /// assert_eq!(scaled_periodic.maximum_interaction_range(), 2.5);
260    /// assert_eq!(scaled_periodic.shape().edge_lengths[0].get(), 20.0);
261    /// # Ok(())
262    /// # }
263    /// ```
264    ///
265    /// ```should_panic
266    /// use hoomd_geometry::{Scale, shape::Rectangle};
267    /// use hoomd_microstate::boundary::Periodic;
268    ///
269    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
270    /// let periodic =
271    ///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
272    ///
273    /// let scaled_periodic = periodic.scale_volume(0.2.try_into()?);
274    /// # Ok(())
275    /// # }
276    /// ```
277    #[inline]
278    fn scale_volume(&self, v: PositiveReal) -> Self {
279        let new_shape = self.shape.scale_volume(v);
280        assert!(
281            new_shape.maximum_allowable_interaction_range() >= self.maximum_interaction_range,
282            "The scaled periodic boundary {new_shape:?} is too small for the maximum interaction range {}",
283            self.maximum_interaction_range
284        );
285
286        Self {
287            shape: new_shape,
288            ..*self
289        }
290    }
291}
292
293impl<P, T> MapPoint<P> for Periodic<T>
294where
295    T: MapPoint<P>,
296{
297    /// Map points in from the wrapped shape into another periodic boundary.
298    ///
299    /// # Errors
300    ///
301    /// [`hoomd_geometry::Error::PointOutsideShape`] when `point` is outside
302    /// `self.shape()`.
303    ///
304    /// # Example
305    ///
306    /// ```
307    /// use hoomd_geometry::{MapPoint, shape::Rectangle};
308    /// use hoomd_microstate::boundary::Periodic;
309    /// use hoomd_vector::Cartesian;
310    ///
311    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
312    /// let periodic_a =
313    ///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
314    /// let periodic_b =
315    ///     Periodic::new(2.5, Rectangle::with_equal_edges(20.0.try_into()?))?;
316    ///
317    /// let mapped_point =
318    ///     periodic_a.map_point(Cartesian::from([-1.0, 1.0]), &periodic_b);
319    ///
320    /// assert_eq!(mapped_point, Ok(Cartesian::from([-2.0, 2.0])));
321    /// assert_eq!(
322    ///     periodic_a.map_point(Cartesian::from([-100.0, 1.0]), &periodic_b),
323    ///     Err(hoomd_geometry::Error::PointOutsideShape)
324    /// );
325    /// # Ok(())
326    /// # }
327    /// ```
328    #[inline]
329    fn map_point(&self, point: P, other: &Self) -> Result<P, hoomd_geometry::Error> {
330        self.shape.map_point(point, &other.shape)
331    }
332}
333
334impl<T> Volume for Periodic<T>
335where
336    T: Volume,
337{
338    /// Volume of the wrapped shape.
339    ///
340    /// # Examples
341    ///
342    /// ```
343    /// use hoomd_geometry::{Volume, shape::Rectangle};
344    /// use hoomd_microstate::boundary::Periodic;
345    ///
346    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
347    /// let periodic =
348    ///     Periodic::new(2.5, Rectangle::with_equal_edges(10.0.try_into()?))?;
349    ///
350    /// let volume = periodic.volume();
351    ///
352    /// assert_eq!(volume, 100.0);
353    /// # Ok(())
354    /// # }
355    /// ```
356    #[inline]
357    fn volume(&self) -> f64 {
358        self.shape.volume()
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    use hoomd_geometry::shape::Rectangle;
367
368    #[test]
369    fn interaction_range_validation() {
370        let rectangle = Rectangle {
371            edge_lengths: [
372                10.0.try_into()
373                    .expect("hard-coded constant should be positive"),
374                6.0.try_into()
375                    .expect("hard-coded constant should be positive"),
376            ],
377        };
378
379        let result = Periodic::new(1.0, rectangle.clone());
380        assert!(result.is_ok());
381
382        let result = Periodic::new(3.0, rectangle.clone());
383        assert!(result.is_ok());
384
385        let result = Periodic::new(3.0_f64.next_up(), rectangle);
386        assert!(matches!(result, Err(Error::InteractionRangeTooLarge(_, _))));
387    }
388}