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