hoomd_mc/
quick_insert.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//! Implement `QuickInsert`
5
6use serde::{Deserialize, Serialize};
7
8use crate::BodyDistribution;
9
10use super::Count;
11use hoomd_interaction::{DeltaEnergyInsert, TotalEnergy};
12use hoomd_microstate::{
13    Body, Microstate, SiteKey, Transform,
14    boundary::{GenerateGhosts, Wrap},
15    property::Position,
16};
17
18use hoomd_spatial::PointUpdate;
19
20/// Track the state of a given `QuickInsert` instance.
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
22enum State {
23    /// Inserting bodies or performing trial moves to separate them.
24    Running,
25    /// All bodies have been inserted and all overlaps removed.
26    Complete,
27}
28
29/// Quickly add bodies to the microstate with random configurations.
30///
31/// [`QuickInsert`] allows you to *quickly* insert many bodies into the microstate.
32/// It does so by *breaking detailed balance*, so you should use it only during
33/// the initialization phase of your simulation where you prepare a microstate
34/// for later equilibration.
35///
36/// [`QuickInsert`]  works best with the [`OverlapPenalty`] potential that
37/// allows sites to overlap a small amount and for trial moves to partially
38/// reduce that overlap.
39///
40/// As a **algorithm**, [`QuickInsert`] is more than just a trial move, it
41/// stores internal state to track its progress. Therefore, you should only
42/// use a given [`QuickInsert`] on one [`Microstate`]. After initialization,
43/// a [`QuickInsert`] knows the *target* number of bodies it should add a
44/// distribution that places those bodies in the simulation boundary. New
45/// [`QuickInsert`] instances start in the running state.
46///
47/// When you [`apply`] a running [`QuickInsert`] to a microstate, it:
48/// 1. Checks the total energy of the given Hamiltonian.
49/// 2. If the total energy is less than or equal to zero *and there are still
50///    bodies to insert*, generate a random body and attempt to insert it into
51///    the microstate. Reject any insertion that would result in an infinite
52///    energy. Accept in all other cases.
53/// 3. Repeat step 2 until inserted bodies overlap with others `allowed_overlaps`
54///    times, the target number of bodies have been inserted, or a total of `target`
55///    attempts have been made during this call, whichever comes first.
56///
57/// When *both* `target` bodies have been inserted *and* the energy is
58/// 0, [`QuickInsert`] transitions to the complete state. When complete,
59/// [`is_complete`] returns `true` and [`apply`] returns immediately.
60///
61/// For spherical particles, [`QuickInsert`] combined with [`OverlapPenalty`]
62/// can achieve a packing fraction of 56% in 3D and 72% in 2D. You might achieve
63/// slightly higher densities if you are willing to run many steps, though
64/// [`QuickCompress`] is a better solution.
65///
66/// The generic type names are:
67/// * `D`: The body distribution.
68///
69/// [`apply`]: Self::apply
70/// [`is_complete`]: Self::is_complete
71/// [`OverlapPenalty`]: hoomd_interaction::univariate::OverlapPenalty
72/// [`QuickCompress`]: crate::QuickCompress
73///
74/// # Example
75///
76/// ```
77/// use hoomd_geometry::shape::Rectangle;
78/// use hoomd_mc::{QuickInsert, UniformIn};
79/// use hoomd_microstate::property::Point;
80/// use hoomd_vector::Cartesian;
81///
82/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
83/// let rectangle = Rectangle::with_equal_edges(10.0.try_into()?);
84///
85/// let distribution = UniformIn {
86///     boundary: rectangle,
87///     template_sites: vec![Point::<Cartesian<2>>::default()],
88/// };
89/// let mut quick_insert = QuickInsert::new(distribution, 256);
90/// # Ok(())
91/// # }
92/// ```
93#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
94pub struct QuickInsert<D> {
95    /// Sample random bodies to insert.
96    distribution: D,
97
98    /// Total number of particles to insert.
99    target: usize,
100
101    /// Maximum number of overlapping inserts allowed.
102    allowed_overlaps: usize,
103
104    /// Count of insertions completed
105    inserted: usize,
106
107    /// Current stage of the method.
108    state: State,
109}
110
111impl<D> QuickInsert<D> {
112    /// Build a new quick insert algorithm.
113    ///
114    /// After construction, the `QuickInsert` starts in a running state. On
115    /// successive calls to `apply`, it will attempt to insert `target` bodies into
116    /// the microstate randomly sampled from the given `distribution`. The default
117    /// number of `allowed_overlaps` is `target/8`.
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use hoomd_geometry::shape::Rectangle;
123    /// use hoomd_mc::{QuickInsert, UniformIn};
124    /// use hoomd_microstate::property::Point;
125    /// use hoomd_vector::Cartesian;
126    ///
127    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
128    /// let rectangle = Rectangle::with_equal_edges(10.0.try_into()?);
129    ///
130    /// let distribution = UniformIn {
131    ///     boundary: rectangle,
132    ///     template_sites: vec![Point::<Cartesian<2>>::default()],
133    /// };
134    /// let quick_insert = QuickInsert::new(distribution, 256);
135    /// # Ok(())
136    /// # }
137    /// ```
138    #[inline]
139    pub fn new(distribution: D, target: usize) -> Self {
140        Self {
141            distribution,
142            target,
143            allowed_overlaps: (target / 8).max(1),
144            inserted: 0,
145            state: State::Running,
146        }
147    }
148
149    /// Check if the quick insert algorithm is complete.
150    ///
151    /// `QuickInsert` completes after it has inserted all `target` bodies **and**
152    /// the total energy of the system is less than or equal to 0. When using the
153    /// recommended `OverlapPenalty` potential, this means that that there are no
154    /// overlaps between any particles.
155    ///
156    /// # Example
157    ///
158    /// ```
159    /// use hoomd_geometry::shape::Rectangle;
160    /// use hoomd_mc::{QuickInsert, UniformIn};
161    /// use hoomd_microstate::property::Point;
162    /// use hoomd_vector::Cartesian;
163    ///
164    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
165    /// let rectangle = Rectangle::with_equal_edges(10.0.try_into()?);
166    ///
167    /// let distribution = UniformIn {
168    ///     boundary: rectangle,
169    ///     template_sites: vec![Point::<Cartesian<2>>::default()],
170    /// };
171    /// let quick_insert = QuickInsert::new(distribution, 256);
172    ///
173    /// let complete = quick_insert.is_complete();
174    /// assert!(!complete);
175    /// # Ok(())
176    /// # }
177    /// ```
178    #[inline]
179    pub fn is_complete(&self) -> bool {
180        self.state == State::Complete
181    }
182
183    /// The target number of bodies to insert.
184    ///
185    /// # Example
186    ///
187    /// ```
188    /// use hoomd_geometry::shape::Rectangle;
189    /// use hoomd_mc::{QuickInsert, UniformIn};
190    /// use hoomd_microstate::property::Point;
191    /// use hoomd_vector::Cartesian;
192    ///
193    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
194    /// let rectangle = Rectangle::with_equal_edges(10.0.try_into()?);
195    ///
196    /// let distribution = UniformIn {
197    ///     boundary: rectangle,
198    ///     template_sites: vec![Point::<Cartesian<2>>::default()],
199    /// };
200    /// let quick_insert = QuickInsert::new(distribution, 256);
201    ///
202    /// let target = quick_insert.target();
203    /// assert_eq!(target, 256);
204    /// # Ok(())
205    /// # }
206    /// ```
207    #[inline]
208    pub fn target(&self) -> usize {
209        self.target
210    }
211
212    /// Apply the quick insert algorithm to a microstate.
213    ///
214    /// Combine [`QuickInsert::apply`] with local trial moves that translate and/or
215    /// rotate bodies by small amounts to relieve the stress caused by inserting
216    /// overlapping sites.
217    ///
218    /// # Example
219    ///
220    /// Hard spheres
221    /// ```
222    /// use hoomd_geometry::shape::Rectangle;
223    /// use hoomd_interaction::{
224    ///     PairwiseCutoff,
225    ///     pairwise::Isotropic,
226    ///     univariate::{Expanded, OverlapPenalty},
227    /// };
228    /// use hoomd_mc::{QuickInsert, Sweep, Translate, Trial, UniformIn};
229    /// use hoomd_microstate::{
230    ///     Body, Microstate, boundary::Periodic, property::Point,
231    /// };
232    /// use hoomd_simulation::macrostate::Isothermal;
233    /// use hoomd_vector::Cartesian;
234    ///
235    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
236    /// let rectangle = Rectangle::with_equal_edges(10.0.try_into()?);
237    ///
238    /// let distribution = UniformIn {
239    ///     boundary: rectangle.clone(),
240    ///     template_sites: vec![Point::default()],
241    /// };
242    /// let mut quick_insert = QuickInsert::new(distribution, 256);
243    ///
244    /// let translate = Translate::with_maximum_distance(0.1.try_into()?);
245    /// let mut translate_sweep = Sweep(translate);
246    ///
247    /// let pairwise_cutoff = PairwiseCutoff(Isotropic {
248    ///     interaction: Expanded {
249    ///         delta: 1.0,
250    ///         f: OverlapPenalty::default(),
251    ///     },
252    ///     r_cut: 1.0,
253    /// });
254    ///
255    /// let macrostate = Isothermal { temperature: 1.0 };
256    /// let mut microstate = Microstate::builder()
257    ///     .boundary(Periodic::new(1.0, rectangle)?)
258    ///     .bodies([Body::point(Cartesian::from([0.0, 0.0]))])
259    ///     .try_build()?;
260    ///
261    /// quick_insert.apply(&mut microstate, &pairwise_cutoff);
262    ///
263    /// translate_sweep.apply(&mut microstate, &pairwise_cutoff, &macrostate);
264    ///
265    /// assert!(microstate.bodies().len() > 1);
266    /// # Ok(())
267    /// # }
268    /// ```
269    #[inline]
270    pub fn apply<P, B, S, X, C, H>(
271        &mut self,
272        microstate: &mut Microstate<B, S, X, C>,
273        hamiltonian: &H,
274    ) -> Count
275    where
276        P: Copy,
277        B: Position<Position = P> + Transform<S>,
278        S: Position<Position = P> + Default,
279        X: PointUpdate<P, SiteKey>,
280        D: BodyDistribution<Body<B, S>>,
281        H: DeltaEnergyInsert<B, S, X, C> + TotalEnergy<Microstate<B, S, X, C>>,
282        C: Wrap<B> + Wrap<S> + GenerateGhosts<S>,
283    {
284        let mut count = Count::default();
285
286        // Perform no work at all if already complete.
287        if self.is_complete() {
288            return count;
289        }
290
291        let energy = hamiltonian.total_energy(microstate);
292
293        // The quick insert algorithm is not complete until the energy has reached 0.
294        if energy <= 0.0 && self.inserted >= self.target {
295            self.state = State::Complete;
296            return count;
297        }
298
299        // Scaling the number of insertion attempts with the target number of insertions
300        // is a good way to ensure that there are sufficient attempts on each call to
301        // apply. Larger boxes will naturally get more insertion attempts. At the same
302        // time, we need to limit the total strain caused by the insertions. Count
303        // the number of insertions that cause overlaps and exit early when there
304        // are too many.
305        if energy <= 0.0 {
306            let mut rng = microstate.counter().make_rng();
307            let mut insertions_with_overlaps = 0;
308
309            for _ in 0..self.target {
310                let new_body = self.distribution.sample(self.inserted, &mut rng);
311
312                let delta_energy = hamiltonian.delta_energy_insert(microstate, &new_body);
313                if delta_energy.is_finite() && microstate.add_body(new_body).is_ok() {
314                    count.accepted += 1;
315                    self.inserted += 1;
316
317                    if delta_energy > 0.0 {
318                        insertions_with_overlaps += 1;
319                    }
320
321                    if self.inserted == self.target
322                        || insertions_with_overlaps >= self.allowed_overlaps
323                    {
324                        break;
325                    }
326                } else {
327                    count.rejected += 1;
328                }
329            }
330        }
331
332        microstate.increment_substep();
333
334        count
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use crate::{QuickInsert, Sweep, Translate, Trial, UniformIn};
342    use hoomd_geometry::shape::Rectangle;
343    use hoomd_interaction::{PairwiseCutoff, pairwise::Isotropic, univariate::Boxcar};
344    use hoomd_microstate::{Microstate, boundary::Closed, property::Point};
345    use hoomd_simulation::macrostate::Isothermal;
346    use hoomd_vector::Cartesian;
347
348    #[test]
349    fn hard_spheres() {
350        let sigma = 1.0;
351        let epsilon = f64::INFINITY;
352        let kt = 1.0;
353
354        let hamiltonian = PairwiseCutoff(Isotropic {
355            interaction: Boxcar {
356                left: 0.0,
357                right: sigma,
358                epsilon,
359            },
360            r_cut: sigma,
361        });
362
363        let translate =
364            Translate::with_maximum_distance(0.1.try_into().expect("hard-coded value is non-zero"));
365        let mut translate_sweep = Sweep(translate);
366
367        let rectangle = Closed(Rectangle::with_equal_edges(
368            6.0.try_into().expect("hard-coded value is non-zero"),
369        ));
370
371        let mut microstate = Microstate::builder()
372            .boundary(rectangle.clone())
373            .bodies(vec![Body::point(Cartesian::from([0.0, 0.0]))])
374            .try_build()
375            .expect("hard-coded point is in the boundary");
376        let macrostate = Isothermal { temperature: kt };
377
378        let distribution = UniformIn {
379            boundary: rectangle,
380            template_sites: vec![Point::new([0.0, 0.0].into())],
381        };
382        let mut quick_insert = QuickInsert::new(distribution, 10);
383
384        assert_eq!(quick_insert.target, 10);
385        assert_eq!(quick_insert.state, State::Running);
386
387        for _ in 0..100 {
388            quick_insert.apply(&mut microstate, &hamiltonian);
389            if quick_insert.is_complete() {
390                break;
391            }
392        }
393
394        translate_sweep.apply(&mut microstate, &hamiltonian, &macrostate);
395
396        assert_eq!(quick_insert.inserted, 10);
397        assert_eq!(quick_insert.state, State::Complete);
398        assert!(quick_insert.is_complete());
399        assert_eq!(microstate.bodies().len(), 11);
400        assert_eq!(hamiltonian.total_energy(&microstate), 0.0);
401    }
402}