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, ¯ostate);
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}