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, ¯ostate);
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, ¯ostate);
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(µstate), 0.0);
401 }
402}