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