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}