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}