hoomd_microstate/lib.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#![doc(
5 html_favicon_url = "https://raw.githubusercontent.com/glotzerlab/hoomd-rs/7352214172a490cc716492e9724ff42720a0018a/doc/theme/favicon.svg"
6)]
7#![doc(
8 html_logo_url = "https://raw.githubusercontent.com/glotzerlab/hoomd-rs/7352214172a490cc716492e9724ff42720a0018a/doc/theme/favicon.svg"
9)]
10
11//! Store and manage the simulation state.
12//!
13//! # Microstate
14//!
15//! [`Microstate`] facilitates simulations of particle systems. It is the central
16//! data structure used by MC, MD, and supporting calculations implemented in
17//! `hoomd-rs`. [`Microstate`] holds a set of bodies that exist in a space defined
18//! by the boundary conditions. The degrees of freedom consist of the properties of
19//! the bodies and the parameters of the boundary conditions.
20//!
21//! [`Microstate`] also stores many auxiliary data structures and implements
22//! convenience methods to facilitate the efficient implementation of simulation
23//! models. These include a set of all interaction sites in the system frame
24//! of reference, ghost sites near the periodic boundaries, and spatial data
25//! structures.
26//!
27//! ## Step, substep and seed
28//!
29//! [`Microstate`] tracks the current simulation step ([`Microstate::step`]),
30//! substep ([`Microstate::substep`]), and seed ([`Microstate::seed`])
31//! that allow models to generate uncorrelated random number streams via
32//! [`Microstate::counter`].
33//!
34//! The **step** is the current step in the simulation as defined by the user.
35//! For example, a single step could be one MD timestep or the application
36//! of a set of MC moves. Users should call [`Microstate::increment_step`]
37//! after each step in the model is completed. The **substep** is a running
38//! counter tracking the number of operations called so far during the current
39//! step. Model methods in *hoomd-rs* (such as the MC trial move `apply`) call
40//! [`increment_substep`](Microstate::increment_substep) internally. Users should
41//! call it at the end of any custom methods that implement new substeps.
42//! A failure to call [`increment_substep`](Microstate::increment_substep)
43//! will cause the reuse of the same random numbers from one substep to the next!**
44//!
45//! The **seed** allows users to select independent random number streams for
46//! simulations that would otherwise be identical. It may only be set once on
47//! creation by [`MicrostateBuilder::seed`].
48//!
49//! ## Bodies and sites
50//!
51//! [`Microstate`] differentiates between the degrees of freedom of the system and
52//! points where interactions take place. Each [`Body`] in the microstate has one
53//! or more interaction sites ([`Site`]). The bodies have degrees of freedom that
54//! are evolved by the simulation model, which defines how bodies interact through
55//! their sites. The properties of the sites *in the system frame* are a function
56//! of the body's properties and the site properties *in the body frame*. In a
57//! particle-only simulation model each body has one site at the origin in the body
58//! frame. In a simulation of squares, each body might be made up of four sites on
59//! the vertices. In both cases, the net force on the body is the sum of the forces
60//! applied to all of its sites.
61//!
62//! In [`Microstate`], the body properties (the generic type `B` throughout) and the
63//! site properties (the generic type `S`) do not need to be the same. For example,
64//! a body might have mass, position and velocity while that body's sites have
65//! position and type.
66//!
67//! The `property` module provides a number of property types. It also defines
68//! traits that you can use to implement custom property types. At a minimum, *both
69//! `B` and `S` MUST implement [`property::Position`]* so that [`Microstate`] can
70//! place your body's and sites inside the boundary conditions and maintain spatial
71//! data structures. Some model methods (such as shape overlap energies) will
72//! require other traits (such as [`property::Orientation`]). The `property` module
73//! documentation provides more details on using the types it provides and how to
74//! define custom types.
75//!
76//! [`Microstate::add_body`] and [`Microstate::extend_bodies`] add new bodies (and
77//! their sites) to the microstate. Similarly, [`Microstate::remove_body`] removes a
78//! body (and all associated sites). [`Microstate::update_body_properties`] modifies
79//! the properties of a given body and correspondingly the properties of all sites
80//! associated with that body. All these methods have a cost proportional to the
81//! number of sites in the body.
82//!
83//! [`Microstate::bodies`] provides direct (immutable) access to the bodies in
84//! the microstate, including their properties and sites in the body frame. While
85//! some algorithms may find this useful, many model algorithms instead operate
86//! on site properties in the system frame. [`Microstate::sites`] provides direct
87//! (immutable) access to a slice of all sites in the system frame for this
88//! purpose. Any method that adds, removes, or changes a body immediately updates
89//! [`Microstate::sites`] accordingly. [`Microstate::iter_body_sites`] iterates over
90//! all the sites (in the system frame) associated with a given body.
91//!
92//! ## Tags
93//!
94//! The elements of [`Microstate::bodies`] and [`Microstate::sites`] are stored in
95//! **no particular order** to allow efficient addition and removal of bodies and
96//! for the possibility sorting to improve cache coherency. Callers are welcome
97//! to iterate over these data structures when computing order-independent overall
98//! properties. However, a caller should never maintain indices into these vectors.
99//! Instead, the caller should store the appropriate **tag** when it needs to
100//! persistently refer to a specific body or site.
101//!
102//! Elements in [`Microstate::bodies`] have the type [`Tagged<Body>`].
103//! The [`item`](Tagged::item) field holds the body itself while the
104//! [`tag`](Tagged::tag) field is a unique identifier that identifies this
105//! specific body. The tag will remain the same even when [`Microstate::bodies`] is
106//! reordered. Use [`Microstate::body_indices`] to find the current index of a body
107//! with a given tag.
108//!
109//! Elements in [`Microstate::sites`] have the type [`Site<S>`]. As with bodies,
110//! each site has a unique [`site_tag`](Site::site_tag) that remains the same even
111//! when sites are reordered. Each site also has a [`body_tag`](Site::body_tag) that
112//! identifies which body the site is part of. Use [`Microstate::site_indices`]
113//! to find the current index of a site with a given site tag and
114//! [`Microstate::iter_body_sites`] to find all the sites associated with a given
115//! body *index*.
116//!
117//! ## Boundary conditions
118//!
119//! The positions of all bodies and all sites **must** be inside the microstate's
120//! boundary at all times. Periodic boundaries can wrap positions outside to a
121//! corresponding point on the inside. When a boundary is aperiodic (or partially
122//! aperiodic), the wrapping process may fail. MC models reject trial moves that
123//! cannot be wrapped. MD models fail with an error should bodies or sites move in a
124//! way that cannot be wrapped.
125//!
126//! [`Microstate`] is generic on the type of boundary condition. The [`boundary`]
127//! module implements standard types and explains how you can provide custom
128//! implementations.
129//!
130//! ## Spatial searches
131//!
132//! `Microstate` maintains a internal spatial data structure (see [`hoomd_spatial`]).
133//! It is kept in sync with every body insertion, removal, and update. Callers
134//! can query the spatial data directly with [`spatial_data`] and efficiently iterate
135//! over all sites near a point in space with [`iter_sites_near`].
136//!
137//! [`spatial_data`]: Microstate::spatial_data
138//! [`iter_sites_near`]: Microstate::iter_sites_near
139//!
140//! ## Ghost sites
141//!
142//! Periodic boundary conditions place **ghost sites** within a given **maximum
143//! interaction range** outside the boundary. These ghost sites are images of real
144//! sites that are inside the boundary. Access all of the ghosts with the
145//! [`ghosts`] method. [`iter_sites_near`] will find both primary and ghost sites
146//! as it searches for sites near the requested point.
147//!
148//! When using [`Open`] or [`Closed`] boundary conditions, [`ghosts`] will always
149//! be empty.
150//!
151//! [`ghosts`]: Microstate::ghosts
152//! [`Open`]: crate::boundary::Open
153//! [`Closed`]: crate::boundary::Closed
154//!
155//! ## I/O
156//!
157//! Use [`HoomdGsdFile`] and [`AppendMicrostate`] to write to GSD
158//! files that can be read by the [Ovito], [HOOMD-blue],
159//! the [GSD Python package], and other applications. There is currently no
160//! high-level API to *read* a GSD file and produce a [`Microstate`]. You can
161//! implement your own solution using the low level [`GsdFile`].
162//!
163//! [`GsdFile`]: hoomd_gsd::file_layer::GsdFile
164//! [`HoomdGsdFile`]: hoomd_gsd::hoomd::HoomdGsdFile
165//! [GSD Python package]: https://gsd.readthedocs.io
166//! [HOOMD-blue]: https://hoomd-blue.readthedocs.io
167//! [Ovito]: https://www.ovito.org
168//!
169//! [`Microstate`] derives the [serde] `Serialize` and `Deserialize` traits,
170//! along with all other types in *hoomd-rs*. You can use [serde] to read and
171//! write entire `Simulation` models. Use [serde] serialized files (in a
172//! format of your choice, [postcard] is a good starting point) to save the
173//! simulation state and continue running where it left off. The format is *NOT*
174//! well-defined for long-term use. It **will** change from one simulation model
175//! to the next, and *may* change with each major release of *hoomd-rs*. Share
176//! your simulation **code** along with GSD **data** with the community.
177//!
178//! [serde]: https://serde.rs/
179//! [postcard]: https://docs.rs/postcard/latest/postcard/
180//!
181//! # Complete documentation
182//!
183//! `hoomd-microstate` is is a part of *hoomd-rs*. Read the [complete documentation]
184//! for more information.
185//!
186//! [complete documentation]: https://hoomd-rs.readthedocs.io
187
188use serde::{Deserialize, Serialize};
189use thiserror::Error;
190
191use hoomd_gsd::hoomd::{AppendError, Frame};
192
193mod append;
194pub mod boundary;
195mod microstate;
196pub mod property;
197
198pub use microstate::{Microstate, MicrostateBuilder, SiteKey, Tagged};
199use property::Point;
200
201/// Interactions in `hoomd-rs` apply between sites.
202///
203/// A [`Site`] (often called an *atom* or a *particle* in other codes) has a
204/// `tag` that uniquely identities it in the [`Microstate`] and is associated
205/// with a given `body` (see [`Body`]). All interactions in `hoomd-rs` occur
206/// on or between sites as a function of their `properties` which has the
207/// generic type `S`. At a minimum, [`Microstate`] assumes that `S` implements
208/// [`Position`](property::Position). `S` is generic so that users can build custom
209/// types that store orientation, charge, mass, color, or whatever other fields are
210/// needed to implement their model.
211///
212/// Add sites to the [`Microstate`] as members of bodies ([`Body`]).
213///
214/// # Example
215///
216/// Find the center of all interaction sites in a [`Microstate`]:
217/// ```
218/// use hoomd_microstate::{Body, Microstate, MicrostateBuilder};
219/// use hoomd_vector::{Cartesian, Vector};
220///
221/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
222/// let microstate = Microstate::builder()
223/// .bodies([
224/// Body::point(Cartesian::from([1.0, 0.0])),
225/// Body::point(Cartesian::from([-1.0, 2.0])),
226/// ])
227/// .try_build()?;
228///
229/// let average_site_position = microstate
230/// .sites()
231/// .iter()
232/// .map(|site| site.properties.position)
233/// .sum::<Cartesian<2>>()
234/// / (microstate.sites().len() as f64);
235/// # Ok(())
236/// # }
237/// ```
238#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
239pub struct Site<S> {
240 /// Every site in a [`Microstate`] has a unique value in `site_tag`.
241 pub site_tag: usize,
242 /// The body tag of the [`Body`] associated with this site.
243 pub body_tag: usize,
244 /// The properties of the site.
245 pub properties: S,
246}
247
248/// A collection of interaction sites that can be placed in a [`Microstate`].
249///
250/// The [`Body`] `properties` have a generic type that includes all the body's
251/// degrees of freedom and any other fields needed to implement the user's
252/// model. Bodies interact indirectly via one or more `sites`. The `sites` vector
253/// stores the properties of the body's sites in the body frame. The body field
254/// `properties` stores the body's degrees of freedom (such as position and
255/// orientation) in the system frame. [`Transform`] describes how a given body
256/// transforms its sites from the body frame to the system frame.
257///
258/// In typical cases, such as those implemented in `hoomd-rs`, [`Body`] describes
259/// a rigid collection of sites that transform together. However, creative
260/// implementations of [`Transform`] could achieve other behaviors.
261///
262/// Use the properties defined in [`property`] to construct bodies that meet
263/// the needs of your model.
264///
265/// # Examples
266///
267/// Construct body with a single interaction site at one point:
268/// ```
269/// use hoomd_microstate::Body;
270/// use hoomd_vector::Cartesian;
271///
272/// let body = Body::point(Cartesian::from([-3.0, 5.0]));
273/// ```
274///
275/// Construct an oriented body:
276/// ```
277/// use hoomd_microstate::{Body, property::OrientedPoint};
278/// use hoomd_vector::{Angle, Cartesian};
279///
280/// let body_properties = OrientedPoint {
281/// position: Cartesian::from([1.0, -3.0]),
282/// orientation: Angle::from(1.2),
283/// };
284/// let site_properties = OrientedPoint {
285/// position: Cartesian::<2>::default(),
286/// orientation: Angle::default(),
287/// };
288///
289/// let body = Body {
290/// properties: body_properties,
291/// sites: vec![site_properties],
292/// };
293/// ```
294//
295/// Construct a rigid body with several point sites:
296/// ```
297/// use hoomd_microstate::{
298/// Body,
299/// property::{OrientedPoint, Point},
300/// };
301/// use hoomd_vector::{Angle, Cartesian};
302///
303/// let body_properties = OrientedPoint {
304/// position: Cartesian::from([1.0, -3.0]),
305/// orientation: Angle::from(1.2),
306/// };
307///
308/// let body = Body {
309/// properties: body_properties,
310/// sites: vec![
311/// Point::new(Cartesian::from([0.0, -1.0])),
312/// Point::new(Cartesian::from([0.0, 0.0])),
313/// Point::new(Cartesian::from([0.0, 1.0])),
314/// ],
315/// };
316/// ```
317///
318/// # Custom body and site properties
319///
320/// The [`property`] module documentation shows you how to define custom body
321/// and site property types.
322#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
323pub struct Body<B, S = B> {
324 /// The body's degrees of freedom.
325 pub properties: B,
326 /// Interaction sites in the body's frame of reference.
327 pub sites: Vec<S>,
328}
329
330impl<B, S> Clone for Body<B, S>
331where
332 B: Clone,
333 S: Clone,
334{
335 #[inline]
336 fn clone(&self) -> Self {
337 Self {
338 properties: self.properties.clone(),
339 sites: self.sites.clone(),
340 }
341 }
342
343 #[inline]
344 fn clone_from(&mut self, source: &Self) {
345 // `Sweep` and other methods use clone_from to efficiently generate
346 // trial moves while minimizing memory copies. #[derive(Clone)] does
347 // not implement `clone_from`, so it must be done manually.
348 self.properties.clone_from(&source.properties);
349 self.sites.clone_from(&source.sites);
350 }
351}
352
353impl<V> Body<Point<V>, Point<V>> {
354 /// Construct a point particle.
355 ///
356 /// A point particle is a [`Body`] with a single interaction site at the body's
357 /// origin. The body and site property types are identical and have only a
358 /// `position` field. Use point particles for simulations of monodisperse hard
359 /// spheres, identical particles with pairwise interactions, or any time you
360 /// need a [`Microstate`] that consists only of point particles.
361 ///
362 /// # Example
363 ///
364 /// ```
365 /// use hoomd_microstate::Body;
366 /// use hoomd_vector::Cartesian;
367 ///
368 /// let body = Body::point(Cartesian::from([-3.0, 5.0]));
369 /// assert_eq!(body.properties.position, [-3.0, 5.0].into());
370 /// assert_eq!(body.sites.len(), 1);
371 /// assert_eq!(body.sites[0].position, [0.0, 0.0].into());
372 /// ```
373 #[inline]
374 #[must_use]
375 pub fn point(position: V) -> Self
376 where
377 V: Default,
378 {
379 Self {
380 properties: Point::new(position),
381 sites: vec![Point::default()],
382 }
383 }
384}
385
386/// Take [`Site`] properties in the body frame into the system frame.
387///
388/// See the [`property`] module-level documentation for an example
389/// that implements [`Transform`] for a custom type.
390pub trait Transform<S> {
391 /// Transform site properties.
392 ///
393 /// Given `site_properties` in the body frame, `transform` returns the
394 /// corresponding site properties in the system frame relative to the
395 /// body properties in `&self`.
396 #[must_use]
397 fn transform(&self, site_properties: &S) -> S;
398}
399
400/// Enumerate possible sources of error in fallible microstate methods.
401#[non_exhaustive]
402#[derive(Error, PartialEq, Debug)]
403pub enum Error {
404 /// Failed to add a body to a [`Microstate`].
405 #[error("failed to add body (tag={0})")]
406 AddBody(usize, #[source] boundary::Error),
407
408 /// Failed to update a body in a [`Microstate`].
409 #[error("failed to update body (tag={0})")]
410 UpdateBody(usize, #[source] boundary::Error),
411}
412
413/// Write a frame to a GSD file with the contents of a microstate.
414///
415/// # Basic usage
416///
417/// `hoomd-microstate` implements [`AppendMicrostate`] for typical combinations
418/// of [`Point`]/[`OrientedPoint`] site types with commonly used boundary
419/// conditions. The provided implementations write all *sites* to the GSD
420/// file.
421///
422/// [`OrientedPoint`]: crate::property::OrientedPoint
423///
424/// ```
425/// use hoomd_geometry::shape::Rectangle;
426/// use hoomd_gsd::hoomd::HoomdGsdFile;
427/// use hoomd_microstate::{
428/// AppendMicrostate, Body, Microstate, boundary::Closed, property::Point,
429/// };
430/// use hoomd_vector::Cartesian;
431///
432/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
433/// # use tempfile::tempdir;
434/// # let tmp_dir = tempdir().expect("temp dir should be created");
435/// # let path = tmp_dir.path().join("test.gsd");
436/// let square = Closed(Rectangle::with_equal_edges(10.0.try_into()?));
437///
438/// let microstate = Microstate::builder()
439/// .boundary(square)
440/// .bodies([
441/// Body::point(Cartesian::from([1.0, 0.0])),
442/// Body::point(Cartesian::from([-1.0, 2.0])),
443/// ])
444/// .try_build()?;
445///
446/// // let path = "file.gsd";
447/// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
448/// hoomd_gsd_file.append_microstate(µstate)?;
449/// # Ok(())
450/// # }
451/// ```
452///
453/// # Writing additional data chunks
454///
455/// `append_microstate` returns the GSD [`Frame`] so you can add data chunks to
456/// the frame, such as log values.
457/// ```
458/// use hoomd_geometry::shape::Rectangle;
459/// use hoomd_gsd::hoomd::HoomdGsdFile;
460/// use hoomd_microstate::{
461/// AppendMicrostate, Body, Microstate, boundary::Closed, property::Point,
462/// };
463/// use hoomd_vector::Cartesian;
464///
465/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
466/// # use tempfile::tempdir;
467/// # let tmp_dir = tempdir().expect("temp dir should be created");
468/// # let path = tmp_dir.path().join("test.gsd");
469/// let square = Closed(Rectangle::with_equal_edges(10.0.try_into()?));
470///
471/// let microstate = Microstate::builder()
472/// .boundary(square)
473/// .bodies([
474/// Body::point(Cartesian::from([1.0, 0.0])),
475/// Body::point(Cartesian::from([-1.0, 2.0])),
476/// ])
477/// .try_build()?;
478///
479/// // let path = "file.gsd";
480/// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
481/// hoomd_gsd_file
482/// .append_microstate(µstate)?
483/// .log_scalar("height", 10.0_f64)?
484/// .log_scalars("energy", [1.0_f64, 2.0, 3.0])?;
485/// # Ok(())
486/// # }
487/// ```
488///
489/// # Custom implementations
490///
491/// You can implement [`AppendMicrostate`] for your custom site type and/or
492/// boundary condition. Your implementation could choose to write bodies
493/// instead of sites. See the "Type-dependent Interactions" tutorial for a complete
494/// example.
495pub trait AppendMicrostate<B, S, X, C> {
496 /// Append the contents of the microstate as a frame in a GSD file.
497 ///
498 /// # Errors
499 ///
500 /// Returns an [`AppendError`] when any of the following occur:
501 /// * The file is not opened in a write mode.
502 /// * An I/O error writing to the file.
503 fn append_microstate(
504 &mut self,
505 microstate: &Microstate<B, S, X, C>,
506 ) -> Result<Frame<'_>, AppendError>;
507}