hoomd_mc/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//! Apply the Metropolis Monte Carlo simulation method to systems of bodies.
12//!
13//! `hoomd-mc` provides building blocks that you can use to create a Monte Carlo
14//! simulation model. Start with a [`hoomd_microstate::Microstate`] to represent
15//! the properties of all the bodies and sites. Form a Hamiltonian using
16//! types from [`hoomd_interaction`] that implement [`TotalEnergy`], [`DeltaEnergyOne`],
17//! [`DeltaEnergyInsert`], and/or [`DeltaEnergyRemove`] and set the macrostate
18//! using one of the types from [`hoomd_simulation`].
19//!
20//! [`DeltaEnergyOne`]: hoomd_interaction::DeltaEnergyOne
21//! [`DeltaEnergyInsert`]: hoomd_interaction::DeltaEnergyInsert
22//! [`DeltaEnergyRemove`]: hoomd_interaction::DeltaEnergyRemove
23//! [`TotalEnergy`]: hoomd_interaction::TotalEnergy
24//!
25//! # Trial moves
26//!
27//! The [`Trial`] trait describes a type that performs trial moves on a microstate.
28//! [`Trial::apply`] takes a mutable microstate, the Hamiltonian, and the macrostate.
29//! It attempts one or more trial moves, modifies the microstate with those that are
30//! accepted, and returns a [`Count`] of the accepted and rejected moves.
31//!
32//! ## Local trial moves
33//!
34//! The [`LocalTrial`] trait describes a type that proposes trial moves as
35//! small perturbations of the body properties. `hoomd-mc` implements
36//! [`Translate`] and [`Rotate`] which propose moves that translate the body's
37//! position and rotate the body's orientation, respectively. You can implement
38//! a custom [`LocalTrial`] as needed in your simulation model.
39//!
40//! [`Sweep`] implements [`Trial`] by applying the given [`LocalTrial`] to every
41//! body in the microstate. [`ParallelSweep`] does the same, but it executes many
42//! trial moves in parallel to increase performance. [`Sweep`] is very general
43//! will work with any boundary condition and local trial move, even when the move
44//! is not actually confined to a small region in space and when all bodies interact
45//! with all other bodies. [`ParallelSweep`] works only with boundaries that can be
46//! covered ([`Cover`]) with a [`Checkerboard`] and when there is a hard cutoff
47//! distance beyond which any two bodies cannot interact.
48//!
49//! ## Global trial moves
50//!
51//! A future release of *hoomd-rs* will implement moves that apply to the
52//! simulation boundary.
53//!
54//! ## Body insertion/removal
55//!
56//! A future release of *hoomd-rs* will implement moves that insert and remove
57//! bodies.
58//!
59//! ## Tuning trial moves
60//!
61//! Most trial moves have tunable parameters. Typical simulation protocols
62//! adjust these parameters to achieve a 20% acceptance rate. Trial moves
63//! that implement the [`Tune`] trait can automatically [`Adjust`] the move
64//! sizes to achieve the target.
65//!
66//! # Algorithms
67//!
68//! `hoomd-mc` also implements a number of MC-adjacent *algorithms*. They are
69//! **not** trial moves (and therefore do not implement [`Trial`]) because they
70//! describe non-reversible processes or otherwise do not obey detailed balance.
71//! These algorithms are useful because they can achieve certain goals much faster
72//! than an equilibrium simulation can.
73//!
74//! [`QuickInsert`] inserts bodies randomly drawn from the given distribution.
75//! The [`UniformIn`] distribution randomly places a template body anywhere
76//! in the simulation boundary. You could write your own distribution for custom
77//! behavior. [`QuickCompress`] compresses the simulation boundary until it
78//! reaches a target volume.
79//!
80//! # Complete documentation
81//!
82//! `hoomd-mc` is is a part of *hoomd-rs*. Read the [complete documentation]
83//! for more information.
84//!
85//! [complete documentation]: https://hoomd-rs.readthedocs.io
86
87use rand::Rng;
88use serde::{Deserialize, Serialize};
89use std::ops::{Add, AddAssign};
90
91use hoomd_microstate::Microstate;
92use hoomd_utility::valid::{OpenUnitIntervalNumber, PositiveReal};
93
94mod hypercuboid;
95mod parallel_sweep;
96mod quick_compress;
97mod quick_insert;
98mod rotate;
99mod sweep;
100mod translate;
101pub(crate) mod tune_local;
102mod uniform_in;
103
104pub use hypercuboid::HypercuboidCheckerboard;
105pub use parallel_sweep::ParallelSweep;
106pub use quick_compress::QuickCompress;
107pub use quick_insert::QuickInsert;
108pub use rotate::Rotate;
109pub use sweep::Sweep;
110pub use translate::Translate;
111pub use uniform_in::UniformIn;
112
113/// Propose trial moves in the microstate, evaluate the changes in energy and accept or reject accordingly.
114///
115/// `Trial` describes a type that applies trial moves to microstates. Specifically,
116/// the method `apply` will attempt one or more individual trial moves to the
117/// microstate. For each individual move, it evaluates the change in energy with
118/// the given `hamiltonian`, then accepts or rejects the trial based on the `state`
119/// parameters.
120///
121/// Each type of trial move in *hoomd-rs* implements the `Trial` trait so that they
122/// may be used as generic arguments in higher level functions.
123///
124/// See [`Sweep`] or any of the other implementations of `Trial` for code examples.
125///
126/// The generic type names are:
127/// * `MI`: The [`Microstate`] type.
128/// * `H`: The Hamiltonian type.
129/// * `MA`: The [`Macrostate`](hoomd_simulation::macrostate) type.
130pub trait Trial<MI, H, MA> {
131 /// Represent the number of accepted and rejected individual trial moves.
132 ///
133 /// Most implementations of `Trial` will use [`crate::Count`] directly. Some
134 /// may provide more granular detail broken down by move type.
135 type Count;
136
137 /// Apply the trial move(s).
138 ///
139 /// A given type that implements `Trial` may perform one or many trial moves
140 /// in a single call to `apply`. The returned value informs the caller how many
141 /// trial moves were accepted and rejected (possibly broken down by type).
142 fn apply(&mut self, microstate: &mut MI, hamiltonian: &H, macrostate: &MA) -> Self::Count;
143}
144
145/// Propose a new configuration for given body properties.
146///
147/// A *local* trial move is one applied to a specific body in the microstate.
148/// Implementations of [`Trial`], such as [`Sweep`], apply a given local move
149/// to one or more bodies in the microstate.
150///
151/// Use one of the provided local trials to [`Translate`] and/or [`Rotate`]
152/// bodies or implement your own custom [`LocalTrial`].
153///
154/// Local trial moves **MUST** satisfy *local detailed balance*,
155/// as defined in [Manousiouthakis & Deem](https://doi.org/10.1063/1.477973).
156///
157/// The generic type names are:
158/// * `B`: The [`Body::properties`](hoomd_microstate::Body) type.
159pub trait LocalTrial<B> {
160 /// Propose a new configuration for the given body properties.
161 #[must_use]
162 fn propose<R: Rng>(&self, rng: &mut R, body_properties: B) -> B;
163}
164
165/// Accepted and rejected trial moves.
166///
167/// A [`Trial`] reports the number moves it accepts and rejects via `Count` (or
168/// some variation on `Count`). `Count` implements [`Add`], [`AddAssign`], and
169/// convenience methods that compute often used properties, like the acceptance
170/// rate.
171///
172/// # Example
173///
174/// Count the total number of trial moves performed over a number of sweeps:
175/// ```
176/// use hoomd_interaction::Zero;
177/// use hoomd_mc::{Count, Sweep, Translate, Trial};
178/// use hoomd_microstate::{Body, Microstate, property::Position};
179/// use hoomd_simulation::macrostate::Isothermal;
180/// use hoomd_vector::Cartesian;
181///
182/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
183/// let macrostate = Isothermal { temperature: 1.0 };
184/// let mut microstate = Microstate::new();
185/// microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
186/// let d = 0.1;
187/// let translate = Translate::with_maximum_distance(d.try_into()?);
188/// let mut translate_sweep = Sweep(translate);
189///
190/// let mut count = Count::default();
191///
192/// for _ in 0..1_000 {
193/// count += translate_sweep.apply(&mut microstate, &Zero, ¯ostate);
194/// microstate.increment_step();
195/// }
196///
197/// assert_eq!(count.total(), 1_000);
198/// # Ok(())
199/// # }
200/// ```
201#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
202pub struct Count {
203 /// The number of accepted moves.
204 pub accepted: u64,
205 /// The number of rejected moves.
206 pub rejected: u64,
207}
208
209impl Count {
210 /// The total number of trial moves.
211 ///
212 /// # Example
213 ///
214 /// ```
215 /// use hoomd_mc::Count;
216 ///
217 /// let count = Count {
218 /// accepted: 2_000,
219 /// rejected: 8_000,
220 /// };
221 /// let total = count.total();
222 ///
223 /// assert_eq!(total, 10_000);
224 /// ```
225 #[inline]
226 #[must_use]
227 pub fn total(&self) -> u64 {
228 self.accepted + self.rejected
229 }
230
231 /// The fraction of moves that were accepted.
232 ///
233 /// The acceptance ratio is the ratio of accepted moves to total moves.
234 /// `acceptance_ratio` returns `Some(ratio)` when the number of total moves is
235 /// nonzero and `None` when there are 0 moves.
236 ///
237 /// # Example
238 ///
239 /// ```
240 /// use hoomd_mc::Count;
241 ///
242 /// let count = Count {
243 /// accepted: 2_000,
244 /// rejected: 8_000,
245 /// };
246 /// let acceptance_ratio = count.acceptance_ratio();
247 ///
248 /// assert_eq!(acceptance_ratio, Some(0.2));
249 /// ```
250 ///
251 /// ```
252 /// use hoomd_mc::Count;
253 ///
254 /// let count = Count::default();
255 /// let acceptance_ratio = count.acceptance_ratio();
256 ///
257 /// assert_eq!(acceptance_ratio, None);
258 /// ```
259 #[inline]
260 #[must_use]
261 pub fn acceptance_ratio(&self) -> Option<f64> {
262 let total = self.total();
263
264 if total > 0 {
265 Some(self.accepted as f64 / total as f64)
266 } else {
267 None
268 }
269 }
270}
271
272impl AddAssign for Count {
273 #[inline]
274 fn add_assign(&mut self, rhs: Self) {
275 self.accepted += rhs.accepted;
276 self.rejected += rhs.rejected;
277 }
278}
279
280impl Add for Count {
281 type Output = Self;
282
283 #[inline]
284 fn add(self, rhs: Self) -> Self {
285 Count {
286 accepted: self.accepted + rhs.accepted,
287 rejected: self.rejected + rhs.rejected,
288 }
289 }
290}
291
292/// Partition space into sets of spaces where trial moves can safely be applied in parallel.
293///
294/// [`ParallelSweep`] uses a [`Checkerboard`] when selecting bodies for
295/// parallel trial moves. A well-behaved checkerboard:
296///
297/// 1. Colors spaces such that any body with its position in a space cannot possibly
298/// interact with any body positioned in any other space of the same color.
299/// 1. Covers all points within the boundary of the simulation.
300/// 3. Respects periodic boundary conditions (when present).
301///
302/// Given a boundary, construct a suitable [`Checkerboard`] via the [`Cover`] trait.
303pub trait Checkerboard<P> {
304 /// Determine the space index of a given point.
305 ///
306 /// Space indices must be in the range `[0,num_spaces)`. [`ParallelSweep`]
307 /// uses the space index as an array index. `point_to_space_index` maps
308 /// a real-valued, D-dimensional point to the linear index.
309 fn point_to_space_index(&self, point: &P) -> Option<usize>;
310
311 /// The indices of all spaces, grouped by color.
312 ///
313 /// In the return value, the outer slice's length is the number of colors
314 /// in the checkerboard. Element of that slice contains the indices of all
315 /// the spaces of that color.
316 fn space_indices_by_color(&self) -> &[Vec<usize>];
317
318 /// The total number of spaces in the checkerboard.
319 fn num_spaces(&self) -> usize;
320}
321
322/// Construct a [`Checkerboard`] that covers all points in this boundary.
323pub trait Cover<P> {
324 /// The checkerboard type associated with this boundary.
325 type Checkerboard: Checkerboard<P>;
326
327 /// Construct a [`Checkerboard`] that covers all points in this boundary.
328 ///
329 /// The constructed [`Checkerboard`] must place spaces assuming that
330 /// any body might interact with another body at distances less than
331 /// `interaction_range`. [`ParallelSweep`] must reject trial moves from one
332 /// space to another. To make simulations ergodic, `cover` must randomly
333 /// place the checkerboard boundaries using the provided `rng`.
334 fn cover<R: Rng + ?Sized>(
335 &self,
336 rng: &mut R,
337 interaction_range: PositiveReal,
338 ) -> Self::Checkerboard;
339
340 /// Update a given checkerboard to match this boundary.
341 ///
342 /// After calling `cover_into`, `checkerboard` will have the same properties
343 /// as the return value of `self.cover(rng, interaction_range)`. However,
344 /// `cover_into` may be able to reuse existing dynamically allocated memory
345 /// in `checkerboard` or avoid some calculations completely (e.g. when the
346 /// checkerboard dimensions remain the same).
347 fn cover_into<R: Rng + ?Sized>(
348 &self,
349 checkerboard: &mut Self::Checkerboard,
350 rng: &mut R,
351 interaction_range: PositiveReal,
352 );
353}
354
355/// Change the maximum size of a local trial move.
356pub trait Adjust {
357 /// Change the maximum trial move size by the given scale factor.
358 fn adjust(&mut self, factor: PositiveReal);
359}
360
361/// Tune trial move maximum sizes toward a target acceptance ratio.
362///
363/// [`Trial`] moves that implement [`Tune`] can automatically adjust
364/// their trial move size to achieve a desired acceptance ratio.
365/// The tuning is performed in the context of a given microstate,
366/// Hamiltonian, and macrostate **but the microstate is not modified**.
367pub trait Tune<P, B, S, X, C, L, H, MA> {
368 /// Tune the trial move maximum size to achieve a given acceptance ratio.
369 ///
370 /// Use [`tune_default`] unless you have a specific need to adjust the
371 /// tuning parameters.
372 ///
373 /// [`tune`] performs `samples` individual trial moves to measure the
374 /// current acceptance ratio. It then adjusts the trial move size
375 /// to increase or decrease the acceptance ratio as needed over
376 /// `steps` iterations.
377 ///
378 /// [`tune`]: Tune::tune
379 /// [`tune_default`]: Tune::tune_default
380 fn tune(
381 &mut self,
382 microstate: &Microstate<B, S, X, C>,
383 hamiltonian: &H,
384 macrostate: &MA,
385 target_acceptance: OpenUnitIntervalNumber,
386 samples: usize,
387 steps: usize,
388 );
389
390 /// Tune the trial move maximum size with default parameters.
391 ///
392 /// The defaults are:
393 /// - `target_acceptance`: 0.2
394 /// - `samples`: 8,000
395 /// - `steps`: 32
396 #[inline]
397 fn tune_default(
398 &mut self,
399 microstate: &Microstate<B, S, X, C>,
400 hamiltonian: &H,
401 macrostate: &MA,
402 ) {
403 self.tune(
404 microstate,
405 hamiltonian,
406 macrostate,
407 0.2.try_into().expect("hard-coded constant should be valid"),
408 8_000,
409 32,
410 );
411 }
412}
413
414/// Tune adjustable move sizes toward a target acceptance ratio.
415///
416/// Use [`Sweep::tune`] or [`ParallelSweep::tune`] when tuning local trial move
417/// sizes. [`tune_by_scaling`] is an internal implementation detail that you can
418/// use to tune custom trial moves.
419///
420/// Pass [`tune_by_scaling`] a target acceptance ratio and the move `count`
421/// obtained during a sampling period with the current `trial` move size.
422/// [`tune_by_scaling`] will scale the trial move size by the factor:
423/// ```math
424/// \frac{a + \gamma}{t + \gamma}
425/// ```
426/// where $` a `$ is the current acceptance (from `count`), $` t `$ is the target
427/// and $` \gamma = 1.5 `$.
428#[expect(
429 clippy::missing_panics_doc,
430 reason = "Panic would occur due to a bug in hoomd-rs."
431)]
432#[inline]
433pub fn tune_by_scaling<L>(trial: &mut L, target_acceptance: OpenUnitIntervalNumber, count: &Count)
434where
435 L: Adjust,
436{
437 const GAMMA: f64 = 1.5;
438
439 if let Some(acceptance_ratio) = count.acceptance_ratio() {
440 let scale = (acceptance_ratio + GAMMA) / (target_acceptance.get() + GAMMA);
441 trial.adjust(scale.try_into().expect("scale should always be positive"));
442 }
443}
444
445/// Sample random bodies.
446///
447/// The [`BodyDistribution`] trait describes a type that samples bodies randomly
448/// from a given distribution. [`BodyDistribution`] is used by [`QuickInsert`]
449/// to add new *n* bodies to the microstate. When sampling, [`QuickInsert`] passes
450/// the index (in $` [0,n) `$) of the body it is attempting to add. Implementations
451/// can use this index to e.g. place bodies with a given stoichiometry or
452/// polydispersity.
453pub trait BodyDistribution<Y> {
454 /// Sample a body from the distribution with the given index.
455 fn sample<R: Rng + ?Sized>(&self, index: usize, rng: &mut R) -> Y;
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use assert2::check;
462 use hoomd_vector::Cartesian;
463
464 #[test]
465 fn test_count() {
466 let default = Count::default();
467 assert_eq!(default.accepted, 0);
468 assert_eq!(default.rejected, 0);
469 assert_eq!(default.total(), 0);
470 assert_eq!(default.acceptance_ratio(), None);
471
472 let a = Count {
473 accepted: 1_500,
474 rejected: 500,
475 };
476 check!(a.total() == 2_000);
477 check!(a.acceptance_ratio() == Some(0.75));
478
479 let mut b = Count {
480 accepted: 500,
481 rejected: 200,
482 };
483 b += a;
484 check!(b.accepted == 2_000);
485 check!(b.rejected == 700);
486 check!(b.total() == 2_700);
487 }
488
489 #[test]
490 fn test_tune() -> anyhow::Result<()> {
491 let high = Count {
492 accepted: 1_500,
493 rejected: 500,
494 };
495
496 let mut local_trial = Translate::<Cartesian<2>>::with_maximum_distance(1.0.try_into()?);
497 tune_by_scaling(&mut local_trial, 0.2.try_into()?, &high);
498 check!(local_trial.maximum_distance().get() > 1.0);
499
500 let low = Count {
501 accepted: 100,
502 rejected: 1_900,
503 };
504
505 let mut local_trial = Translate::<Cartesian<2>>::with_maximum_distance(1.0.try_into()?);
506 tune_by_scaling(&mut local_trial, 0.2.try_into()?, &low);
507 check!(local_trial.maximum_distance().get() < 1.0);
508
509 let zero = Count {
510 accepted: 0,
511 rejected: 2_000,
512 };
513
514 let mut local_trial = Translate::<Cartesian<2>>::with_maximum_distance(1.0.try_into()?);
515 tune_by_scaling(&mut local_trial, 0.2.try_into()?, &zero);
516 check!(local_trial.maximum_distance().get() < 1.0);
517
518 Ok(())
519 }
520}