Skip to main content

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