hoomd_microstate/microstate.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 [`Microstate`] and related types.
5
6use arrayvec::ArrayVec;
7use serde::{Deserialize, Serialize};
8use std::{cmp::Reverse, collections::BinaryHeap, fmt, mem};
9
10use crate::{
11 Body, Error, Site, Transform,
12 boundary::{GenerateGhosts, MAX_GHOSTS, Open, Wrap},
13 property::Position,
14};
15
16use hoomd_geometry::MapPoint;
17use hoomd_rand::Counter;
18use hoomd_spatial::{AllPairs, IndexFromPosition, PointUpdate, PointsNearBall};
19
20/// Either a primary site index or a ghost site index.
21#[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
22#[expect(
23 clippy::exhaustive_enums,
24 reason = "There will only ever be primary and ghost sites."
25)]
26pub enum SiteKey {
27 /// Index to a primary site.
28 Primary(usize),
29
30 /// Index to a ghost site.
31 Ghost(usize),
32}
33
34/// Track a unique identifier for an item in [`Microstate`].
35#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
36pub struct Tagged<T> {
37 /// The unique identifier.
38 pub tag: usize,
39 /// The tagged item.
40 pub item: T,
41}
42
43/// A dense vector with O(1) remove complexity.
44///
45/// Each item pushed to the vector is given a tag (in monotonically increasing
46/// order). Access items by tag when identity matters and by index order when it
47/// doesn't.
48///
49/// Items are removed using `swap_remove`. Removed tags are reused when adding new
50/// items.
51#[derive(Clone, Debug, Serialize, Deserialize)]
52struct VecWithTags<T> {
53 /// Items in index order.
54 items: Vec<T>,
55
56 /// Tags of the items, in index order.
57 tags: Vec<usize>,
58
59 /// Indices of the items, in tag order.
60 indices: Vec<Option<usize>>,
61
62 /// Tags that can be reused.
63 free_tags: BinaryHeap<Reverse<usize>>,
64}
65
66impl<T> VecWithTags<T> {
67 /// Construct an empty vector of tagged items.
68 fn new() -> Self {
69 Self {
70 items: Vec::new(),
71 tags: Vec::new(),
72 indices: Vec::new(),
73 free_tags: BinaryHeap::new(),
74 }
75 }
76
77 /// Remove all items from the vector.
78 fn clear(&mut self) {
79 self.items.clear();
80 self.tags.clear();
81 self.indices.clear();
82 self.free_tags.clear();
83 }
84
85 /// The tag that will be assigned to the next item added.
86 fn next_tag(&self) -> usize {
87 self.free_tags.peek().map_or(self.indices.len(), |t| t.0)
88 }
89
90 /// Add a new item and return the tag added.
91 fn push(&mut self, item: T) -> usize {
92 let tag = self.free_tags.pop().map_or(self.indices.len(), |t| t.0);
93 let index = self.items.len();
94
95 self.items.push(item);
96 self.tags.push(tag);
97
98 if tag == self.indices.len() {
99 self.indices.push(Some(index));
100 } else {
101 debug_assert_eq!(self.indices[tag], None);
102 self.indices[tag] = Some(index);
103 }
104
105 tag
106 }
107
108 /// Remove an item identified *by index*
109 fn remove(&mut self, index: usize) {
110 let removed_tag = self.tags[index];
111
112 self.items.swap_remove(index);
113 self.tags.swap_remove(index);
114
115 if index < self.items.len() {
116 let replaced_tag = self.tags[index];
117 self.indices[replaced_tag] = Some(index);
118 }
119 self.indices[removed_tag] = None;
120 self.free_tags.push(Reverse(removed_tag));
121 }
122
123 /// Number of items stored.
124 fn len(&self) -> usize {
125 self.items.len()
126 }
127
128 /// True when any items are stored.
129 #[cfg(test)]
130 fn is_empty(&self) -> bool {
131 self.items.is_empty()
132 }
133
134 /// Iterate over items in tag order.
135 fn iter_tag_order(&self) -> impl Iterator<Item = &T> {
136 self.indices
137 .iter()
138 .filter_map(|opt_i| opt_i.map(|i| &self.items[i]))
139 }
140}
141
142/// Store and manage all the degrees of freedom of a single microstate in phase space.
143///
144/// [`Microstate`] implements the main logic of the crate. See the [crate-level
145/// documentation](crate) for a full overview and the method-specific documentation
146/// for additional details.
147///
148/// The generic type names are:
149/// * `B`: The [`Body::properties`](crate::Body) type.
150/// * `S`: The [`Site::properties`](crate::Site) type.
151/// * `X`: The [`spatial data structure`](hoomd_spatial) type.
152/// * `C`: The [`boundary`](crate::boundary) condition type.
153///
154/// ## Constructing Microstate
155///
156/// You will find many examples in this documentation using [`Microstate::new`]. It
157/// is designed to be terse, and is inflexible as a consequence. [`Microstate::new`]
158/// always sets [`Open`](crate::boundary::Open) boundary conditions and initializes
159/// the seed and step to 0.
160/// ```
161/// use hoomd_microstate::Microstate;
162/// # use hoomd_microstate::{Body, property::Point};
163/// # use hoomd_vector::Cartesian;
164///
165/// let mut microstate = Microstate::new();
166/// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
167/// ```
168///
169/// When you need more control, use [`MicrostateBuilder`] to set the boundary conditions,
170/// use a different seed or starting step:
171///
172/// ```
173/// use hoomd_geometry::shape::Rectangle;
174/// use hoomd_microstate::{Body, Microstate, boundary::Closed};
175/// use hoomd_vector::Cartesian;
176///
177/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
178/// let square = Closed(Rectangle::with_equal_edges(10.0.try_into()?));
179///
180/// let microstate = Microstate::builder()
181/// .boundary(square)
182/// .seed(0x43abf1)
183/// .step(100_000)
184/// .bodies([Body::point(Cartesian::from([0.0, 0.0]))])
185/// .try_build()?;
186/// # Ok(())
187/// # }
188/// ```
189#[derive(Clone, Debug, Serialize, Deserialize)]
190pub struct Microstate<B, S = B, X = AllPairs<SiteKey>, C = Open> {
191 /// Total number of steps that this microstate has been advanced in a simulation model.
192 step: u64,
193
194 /// Number of substeps that the simulation has taken during the current simulation step.
195 substep: u32,
196
197 /// User chosen random number seed.
198 seed: u32,
199
200 /// Bodies in the microstate, stored in index order.
201 bodies: VecWithTags<Tagged<Body<B, S>>>,
202
203 /// Sites in the system reference frame.
204 sites: VecWithTags<Site<S>>,
205
206 /// Tags of the sites associated with the bodies (in body index order).
207 bodies_sites: Vec<Vec<usize>>,
208
209 /// Ghost sites in the system reference frame.
210 ghosts: VecWithTags<Site<S>>,
211
212 /// Tags of the ghosts associated with a given site (in site index order).
213 sites_ghosts: Vec<ArrayVec<usize, MAX_GHOSTS>>,
214
215 /// The range of allowed particle positions and a description of any periodicity.
216 boundary: C,
217
218 /// Spatial data structure.
219 spatial_data: X,
220}
221
222impl<B, S> Default for Microstate<B, S, AllPairs<SiteKey>, Open> {
223 /// Construct an empty microstate with open boundary conditions.
224 ///
225 /// See [`Microstate::new`].
226 #[inline]
227 fn default() -> Self {
228 Self::new()
229 }
230}
231
232impl<B, S> Microstate<B, S, AllPairs<SiteKey>, Open> {
233 /// Construct an empty microstate with open boundary conditions.
234 ///
235 /// The microstate starts at step 0, substep 0, random number seed 0,
236 /// and has no bodies. Use the [`AllPairs`] spatial search algorithm.
237 ///
238 /// # Example
239 ///
240 /// ```
241 /// use hoomd_microstate::Microstate;
242 /// # use hoomd_microstate::{Body, property::Point};
243 /// # use hoomd_vector::Cartesian;
244 ///
245 /// let mut microstate = Microstate::new();
246 /// assert_eq!(microstate.step(), 0);
247 /// assert_eq!(microstate.substep(), 0);
248 /// assert_eq!(microstate.seed(), 0);
249 /// assert_eq!(microstate.bodies().len(), 0);
250 /// assert_eq!(microstate.sites().len(), 0);
251 /// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
252 /// ```
253 #[inline]
254 #[must_use]
255 pub fn new() -> Self {
256 Microstate {
257 step: 0,
258 substep: 0,
259 seed: 0,
260 bodies: VecWithTags::new(),
261 sites: VecWithTags::new(),
262 bodies_sites: Vec::new(),
263 ghosts: VecWithTags::new(),
264 sites_ghosts: Vec::new(),
265 boundary: Open,
266 spatial_data: AllPairs::default(),
267 }
268 }
269
270 /// Set microstate parameters before construction.
271 ///
272 /// The builder defaults to:
273 /// * `step = 0`
274 /// * `seed = 0`
275 /// * `spatial_data` = [`AllPairs`]
276 /// * `boundary` = [`Open`]
277 /// * No bodies.
278 ///
279 /// Call [`MicrostateBuilder`] methods in a chain to set these parameters.
280 ///
281 /// # Example
282 ///
283 /// ```
284 /// use hoomd_geometry::shape::Rectangle;
285 /// use hoomd_microstate::{
286 /// Body, Microstate, boundary::Closed, property::Point,
287 /// };
288 /// use hoomd_spatial::VecCell;
289 /// use hoomd_vector::Cartesian;
290 ///
291 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
292 /// let cell_list = VecCell::builder()
293 /// .nominal_search_radius(2.5.try_into()?)
294 /// .build();
295 /// let square = Closed(Rectangle::with_equal_edges(10.0.try_into()?));
296 ///
297 /// let microstate = Microstate::builder()
298 /// .boundary(square)
299 /// .spatial_data(cell_list)
300 /// .step(100_000)
301 /// .seed(0x1234abcd)
302 /// .bodies([
303 /// Body::point(Cartesian::from([1.0, 0.0])),
304 /// Body::point(Cartesian::from([-1.0, 2.0])),
305 /// ])
306 /// .try_build()?;
307 ///
308 /// assert_eq!(microstate.boundary().0.edge_lengths[0].get(), 10.0);
309 /// assert_eq!(microstate.step(), 100_000);
310 /// assert_eq!(microstate.seed(), 0x1234abcd);
311 /// assert_eq!(microstate.bodies().len(), 2);
312 /// # Ok(())
313 /// # }
314 /// ```
315 #[inline]
316 #[must_use]
317 pub fn builder() -> MicrostateBuilder<B, S, AllPairs<SiteKey>, Open> {
318 MicrostateBuilder {
319 step: 0,
320 seed: 0,
321 bodies: Vec::new(),
322 spatial_data: AllPairs::default(),
323 boundary: Open,
324 }
325 }
326}
327
328/// Access and manage the simulation step, substep, RNG seeds.
329impl<B, S, X, C> Microstate<B, S, X, C> {
330 /// Get the simulation step.
331 ///
332 /// # Examples
333 ///
334 /// Get the step:
335 /// ```
336 /// use hoomd_microstate::Microstate;
337 /// # use hoomd_microstate::{Body, property::Point};
338 /// # use hoomd_vector::Cartesian;
339 ///
340 /// let mut microstate = Microstate::new();
341 /// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
342 /// assert_eq!(microstate.step(), 0);
343 /// ```
344 ///
345 /// Initialize a microstate with a given step:
346 /// ```
347 /// use hoomd_microstate::Microstate;
348 /// # use hoomd_microstate::{Body, property::Point};
349 /// # use hoomd_vector::Cartesian;
350 ///
351 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
352 /// let microstate = Microstate::builder()
353 /// .step(100_000)
354 /// # .bodies([Body::point(Cartesian::from([0.0, 0.0]))])
355 /// .try_build()?;
356 /// assert_eq!(microstate.step(), 100_000);
357 /// # Ok(())
358 /// # }
359 /// ```
360 #[inline]
361 #[must_use]
362 pub fn step(&self) -> u64 {
363 self.step
364 }
365
366 /// Increment the simulation step.
367 ///
368 /// Also set the substep to 0.
369 ///
370 /// # Examples
371 ///
372 /// Increment the simulation step:
373 /// ```
374 /// use hoomd_microstate::Microstate;
375 /// # use hoomd_microstate::{Body, property::Point};
376 /// # use hoomd_vector::Cartesian;
377 ///
378 /// let mut microstate = Microstate::new();
379 /// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
380 /// microstate.increment_step();
381 ///
382 /// assert_eq!(microstate.step(), 1);
383 /// ```
384 ///
385 /// Confirm that `substep` resets to 0:
386 /// ```
387 /// use hoomd_microstate::Microstate;
388 /// # use hoomd_microstate::{Body, property::Point};
389 /// # use hoomd_vector::Cartesian;
390 ///
391 /// let mut microstate = Microstate::new();
392 /// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
393 ///
394 /// microstate.increment_substep();
395 /// microstate.increment_substep();
396 /// microstate.increment_substep();
397 /// assert_eq!(microstate.substep(), 3);
398 ///
399 /// microstate.increment_step();
400 ///
401 /// assert_eq!(microstate.step(), 1);
402 /// assert_eq!(microstate.substep(), 0);
403 /// ```
404 #[inline]
405 pub fn increment_step(&mut self) {
406 self.step += 1;
407 self.substep = 0;
408 }
409
410 /// Get the simulation substep.
411 ///
412 /// # Example
413 /// ```
414 /// use hoomd_microstate::Microstate;
415 /// # use hoomd_microstate::{Body, property::Point};
416 /// # use hoomd_vector::Cartesian;
417 ///
418 /// let mut microstate = Microstate::new();
419 /// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
420 /// microstate.increment_substep();
421 ///
422 /// assert_eq!(microstate.substep(), 1);
423 /// ```
424 #[inline]
425 #[must_use]
426 pub fn substep(&self) -> u32 {
427 self.substep
428 }
429
430 /// Increment the simulation substep.
431 ///
432 /// # Example
433 /// ```
434 /// use hoomd_microstate::Microstate;
435 /// # use hoomd_microstate::{Body, property::Point};
436 /// # use hoomd_vector::Cartesian;
437 ///
438 /// let mut microstate = Microstate::new();
439 /// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
440 /// microstate.increment_substep();
441 ///
442 /// assert_eq!(microstate.substep(), 1);
443 /// ```
444 #[inline]
445 pub fn increment_substep(&mut self) {
446 self.substep += 1;
447 }
448
449 /// Get the simulation seed.
450 ///
451 /// # Examples:
452 ///
453 /// Get the simulation seed.
454 /// ```
455 /// use hoomd_microstate::Microstate;
456 /// # use hoomd_microstate::{Body, property::Point};
457 /// # use hoomd_vector::Cartesian;
458 ///
459 /// let mut microstate = Microstate::new();
460 /// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
461 ///
462 /// assert_eq!(microstate.seed(), 0);
463 /// ```
464 ///
465 /// Initialize a microstate with a given seed:
466 /// ```
467 /// use hoomd_microstate::Microstate;
468 /// # use hoomd_microstate::{Body, property::Point};
469 /// # use hoomd_vector::Cartesian;
470 ///
471 /// # type BodyProperties = Point<Cartesian<2>>;
472 /// # type SiteProperties = Point<Cartesian<2>>;
473 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
474 /// let microstate = Microstate::<BodyProperties, SiteProperties>::builder()
475 /// .seed(0x1234abcd)
476 /// # .bodies([Body::point(Cartesian::from([0.0, 0.0]))])
477 /// .try_build()?;
478 /// assert_eq!(microstate.seed(), 0x1234abcd);
479 /// # Ok(())
480 /// # }
481 /// ```
482 #[inline]
483 #[must_use]
484 pub fn seed(&self) -> u32 {
485 self.seed
486 }
487
488 /// Create a partially constructed [`Counter`] from the current step, substep, and seed.
489 ///
490 /// Use the produced [`Counter`] to make a independent random number generator at each
491 /// substep. Call additional methods on the [`Counter`] first to further differentiate
492 /// the stream.
493 ///
494 /// # Example
495 ///
496 /// Make a random number generator unique to this substep:
497 /// ```
498 /// use hoomd_microstate::Microstate;
499 /// # use hoomd_microstate::{Body, property::Point};
500 /// # use hoomd_vector::Cartesian;
501 ///
502 /// let mut microstate = Microstate::new();
503 /// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
504 ///
505 /// let rng = microstate.counter().make_rng();
506 /// ```
507 ///
508 /// Make a random number generator unique to a particular particle on this substep:
509 ///
510 /// ```
511 /// use hoomd_microstate::Microstate;
512 /// # use hoomd_microstate::{Body, property::Point};
513 /// # use hoomd_vector::Cartesian;
514 ///
515 /// let mut microstate = Microstate::new();
516 /// # microstate.add_body(Body::point(Cartesian::from([0.0, 0.0])));
517 ///
518 /// let tag = 10;
519 /// let rng = microstate.counter().index(tag).make_rng();
520 /// ```
521 #[inline]
522 pub fn counter(&self) -> Counter {
523 Counter::new(self.step, self.substep, self.seed)
524 }
525}
526
527/// Access and manage the boundary condition.
528impl<B, S, X, C> Microstate<B, S, X, C> {
529 /// Get the boundary condition.
530 ///
531 /// # Example
532 ///
533 /// ```
534 /// use hoomd_geometry::shape::Rectangle;
535 /// use hoomd_microstate::{Microstate, boundary::Closed};
536 /// # use hoomd_microstate::{Body, property::Point};
537 /// # use hoomd_vector::Cartesian;
538 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
539 ///
540 /// let square = Closed(Rectangle::with_equal_edges(10.0.try_into()?));
541 /// let microstate = Microstate::builder()
542 /// .boundary(square)
543 /// # .bodies([Body::point(Cartesian::from([0.0, 0.0]))])
544 /// .try_build()?;
545 ///
546 /// assert_eq!(microstate.boundary().0.edge_lengths[0].get(), 10.0);
547 /// # Ok(())
548 /// # }
549 /// ```
550 #[inline]
551 pub fn boundary(&self) -> &C {
552 &self.boundary
553 }
554}
555
556/// Manage bodies in the microstate.
557impl<P, B, S, X, C> Microstate<B, S, X, C>
558where
559 P: Copy,
560 B: Transform<S> + Position<Position = P>,
561 S: Position<Position = P> + Default,
562 C: Wrap<B> + Wrap<S> + GenerateGhosts<S>,
563 X: PointUpdate<P, SiteKey>,
564{
565 /// Update the ghosts of a site.
566 ///
567 /// Given a site in the boundary, update that site's ghosts to be consistent
568 /// with that site's properties. This may require adding or removing ghosts.
569 fn update_site_ghosts(
570 site: &Site<S>,
571 site_index: usize,
572 boundary: &C,
573 sites_ghosts: &mut [ArrayVec<usize, MAX_GHOSTS>],
574 ghosts: &mut VecWithTags<Site<S>>,
575 spatial_data: &mut X,
576 ) {
577 let new_ghosts = boundary.generate_ghosts(&site.properties);
578 let ghost_tags = &mut sites_ghosts[site_index];
579
580 match ghost_tags.len().cmp(&new_ghosts.len()) {
581 std::cmp::Ordering::Less => {
582 let ghosts_to_add = new_ghosts.len() - ghost_tags.len();
583 for _ in 0..ghosts_to_add {
584 let ghost_tag = ghosts.push(Site {
585 site_tag: site.site_tag,
586 body_tag: site.body_tag,
587 properties: S::default(),
588 });
589 ghost_tags.push(ghost_tag);
590 }
591 }
592 std::cmp::Ordering::Greater => {
593 let ghosts_to_remove = ghost_tags.len() - new_ghosts.len();
594 for ghost_tag in ghost_tags.iter().rev().take(ghosts_to_remove) {
595 let ghost_index = ghosts.indices[*ghost_tag]
596 .expect("sites_ghosts and ghost.indices should be consistent");
597 ghosts.remove(ghost_index);
598 spatial_data.remove(&SiteKey::Ghost(*ghost_tag));
599 }
600
601 ghost_tags.truncate(new_ghosts.len());
602 }
603 std::cmp::Ordering::Equal => {}
604 }
605
606 debug_assert_eq!(ghost_tags.len(), new_ghosts.len());
607
608 for (new_ghost, ghost_tag) in new_ghosts.into_iter().zip(ghost_tags) {
609 let ghost_index = ghosts.indices[*ghost_tag]
610 .expect("sites_ghosts and ghost.indices should be consistent");
611 spatial_data.insert(SiteKey::Ghost(*ghost_tag), *new_ghost.position());
612 ghosts.items[ghost_index].properties = new_ghost;
613 }
614 }
615
616 /// Update ghosts for all the sites of a given body (by index).
617 fn update_body_site_ghosts(&mut self, body_index: usize) {
618 for site_tag in &self.bodies_sites[body_index] {
619 let site_index = self.sites.indices[*site_tag]
620 .expect("bodies_sites and site_indices should be consistent");
621 Self::update_site_ghosts(
622 &self.sites.items[site_index],
623 site_index,
624 &self.boundary,
625 &mut self.sites_ghosts,
626 &mut self.ghosts,
627 &mut self.spatial_data,
628 );
629 }
630 }
631
632 /// Add a new body to the microstate.
633 ///
634 /// Each body is assigned a unique tag. The first body is given tag 0,
635 /// the second is given tag 1, and so on. When a body is removed (see
636 /// [`Microstate::remove_body()`]), its tag becomes unused. The next call to
637 /// `add_body` will assign the smallest unused tag.
638 ///
639 /// `add_body` also adds the body's sites to the microstate's
640 /// [`sites`](Microstate::sites) (in system coordinates) and assigns unique
641 /// tags to the sites similarly. It wraps the body's position (and the
642 /// positions of its sites in system coordinates) into the boundary (see
643 /// [`boundary`]).
644 ///
645 /// [`boundary`]: crate::boundary
646 ///
647 /// # Cost
648 ///
649 /// The cost of adding a body is proportional to the number of sites in the
650 /// body.
651 ///
652 /// # Returns
653 ///
654 /// [`Ok(tag)`](Result::Ok) with the tag of the added body on success.
655 ///
656 /// # Errors
657 ///
658 /// [`Error::AddBody`] when the body cannot be added to the microstate because
659 /// the body position or any site position cannot be wrapped into the boundary
660 ///
661 /// # Example
662 ///
663 /// ```
664 /// use hoomd_microstate::{Body, Microstate};
665 /// use hoomd_vector::Cartesian;
666 ///
667 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
668 /// let mut microstate = Microstate::new();
669 /// let first_tag =
670 /// microstate.add_body(Body::point(Cartesian::from([1.0, 0.0])))?;
671 /// let second_tag =
672 /// microstate.add_body(Body::point(Cartesian::from([-1.0, 2.0])))?;
673 ///
674 /// assert_eq!(microstate.bodies().len(), 2);
675 /// assert_eq!(first_tag, 0);
676 /// assert_eq!(second_tag, 1);
677 /// # Ok(())
678 /// # }
679 /// ```
680 #[inline]
681 #[expect(
682 clippy::missing_panics_doc,
683 reason = "Panic would occur due to a bug in hoomd-rs."
684 )]
685 pub fn add_body(&mut self, body: Body<B, S>) -> Result<usize, Error> {
686 // Find the tag of the new body.
687 let body_tag = self.bodies.next_tag();
688
689 let mut body = body;
690 body.properties = self
691 .boundary
692 .wrap(body.properties)
693 .map_err(|e| Error::AddBody(body_tag, e))?;
694
695 // An unknown site in the body might not wrap into the boundary.
696 // Check that they do first before starting to modify internal data
697 // structures. This wraps every site twice on add. Should that prove to
698 // be a performance bottleneck, we could alternately implement rollback
699 // (complicated) or a staging Vec (would require additional allocations
700 // or a reusable scratch storage).
701 for s in &body.sites {
702 self.boundary
703 .wrap(body.properties.transform(s))
704 .map_err(|e| Error::AddBody(body_tag, e))?;
705 }
706
707 // Now that all errors have been checked, it is safe to start mutating the
708 // microstate.
709
710 // Add the body's sites first.
711 // Should the Vec allocation prove a bottleneck, we could recycle the body_sites
712 // vecs along with the tags.
713 let mut body_sites = Vec::with_capacity(body.sites.len());
714 for s in &body.sites {
715 let site_tag = self.sites.next_tag();
716
717 let site = Site {
718 site_tag,
719 properties: self
720 .boundary
721 .wrap(body.properties.transform(s))
722 .expect("sites should be validated as wrappable prior to this loop"),
723 body_tag,
724 };
725 self.spatial_data
726 .insert(SiteKey::Primary(site.site_tag), *site.properties.position());
727 self.sites.push(site);
728 self.sites_ghosts.push(ArrayVec::new());
729
730 body_sites.push(site_tag);
731 }
732
733 // Add body
734 self.bodies.push(Tagged {
735 tag: body_tag,
736 item: body,
737 });
738 self.bodies_sites.push(body_sites);
739
740 self.update_body_site_ghosts(self.bodies().len() - 1);
741
742 Ok(body_tag)
743 }
744
745 /// Add multiple bodies to the microstate.
746 ///
747 /// See [`Microstate::add_body()`] for details.
748 ///
749 /// # Errors
750 ///
751 /// [`Error::AddBody`] when any of the bodies cannot be added to the microstate.
752 /// `extend_bodies` adds each body one by one. When an error occurs, it
753 /// short-circuits and does not attempt to add any further bodies. The bodies
754 /// added before the error will remain in the microstate.
755 ///
756 /// # Example
757 ///
758 /// ```
759 /// use hoomd_microstate::{Body, Microstate};
760 /// use hoomd_vector::Cartesian;
761 ///
762 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
763 /// let mut microstate = Microstate::new();
764 /// microstate.extend_bodies([
765 /// Body::point(Cartesian::from([1.0, 0.0])),
766 /// Body::point(Cartesian::from([-1.0, 2.0])),
767 /// ])?;
768 ///
769 /// assert_eq!(microstate.bodies().len(), 2);
770 /// # Ok(())
771 /// # }
772 /// ```
773 #[inline]
774 pub fn extend_bodies<T>(&mut self, bodies: T) -> Result<(), Error>
775 where
776 T: IntoIterator<Item = Body<B, S>>,
777 {
778 for body in bodies {
779 self.add_body(body)?;
780 }
781
782 Ok(())
783 }
784
785 /// Remove a body at the given *index* from the microstate.
786 ///
787 /// Also remove all the body's sites. The body's tag (and the tags of its
788 /// sites) are then free to be reused by [`Microstate::add_body`].
789 ///
790 /// Removing a body will change the index order of the
791 /// [`bodies`](Microstate::bodies) and [`sites`](Microstate::sites) arrays.
792 /// [`Microstate`] does not guarantee any specific ordering in these arrays
793 /// after calling `remove_body`.
794 ///
795 /// # Cost
796 ///
797 /// The cost of removing a body is proportional to the number of sites in the
798 /// body.
799 ///
800 /// # Panics
801 ///
802 /// Panics when `index` is out of bounds.
803 ///
804 /// # Example
805 ///
806 /// ```
807 /// use hoomd_microstate::{Body, Microstate};
808 /// use hoomd_vector::Cartesian;
809 ///
810 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
811 /// let mut microstate = Microstate::builder()
812 /// .bodies([
813 /// Body::point(Cartesian::from([1.0, 0.0])),
814 /// Body::point(Cartesian::from([-1.0, 2.0])),
815 /// ])
816 /// .try_build()?;
817 ///
818 /// microstate.remove_body(0);
819 ///
820 /// assert_eq!(microstate.bodies().len(), 1);
821 /// # Ok(())
822 /// # }
823 /// ```
824 #[inline]
825 pub fn remove_body(&mut self, body_index: usize) {
826 let body_tag = self.bodies.items[body_index].tag;
827 debug_assert_eq!(self.bodies.indices[body_tag], Some(body_index));
828
829 // Remove sites and their associated ghosts. `add_body` adds sites in
830 // increasing index order, so remove them in reverse order to avoid keep
831 // the other bodies' sites in increasing order.
832 let body_sites = self.bodies_sites.swap_remove(body_index);
833 for site_tag in body_sites.iter().rev() {
834 let site_index = self.sites.indices[*site_tag]
835 .expect("bodies_sites and sites.indices should be consistent");
836
837 let site_ghosts = self.sites_ghosts.swap_remove(site_index);
838 for ghost_tag in site_ghosts.iter().rev() {
839 let ghost_index = self.ghosts.indices[*ghost_tag]
840 .expect("sites_ghosts and ghosts.indices should be consistent");
841 self.spatial_data.remove(&SiteKey::Ghost(*ghost_tag));
842 self.ghosts.remove(ghost_index);
843 }
844
845 self.spatial_data.remove(&SiteKey::Primary(*site_tag));
846 self.sites.remove(site_index);
847 }
848
849 // Remove body
850 self.bodies.remove(body_index);
851 }
852
853 /// Sets the properties of the given body.
854 ///
855 /// `update_body_properties` also updates the properties of the sites (in the
856 /// system frame) associated with the body accordingly.
857 ///
858 /// # Errors
859 ///
860 /// [`Error::UpdateBody`] the body properties cannot be updated because the body
861 /// position or any site position cannot be wrapped into the boundary. When an
862 /// error occurs, `update_body_properties` makes no change to the [`Microstate`].
863 ///
864 /// # Example
865 ///
866 /// ```
867 /// use hoomd_microstate::{Body, Microstate, property::Point};
868 /// use hoomd_vector::Cartesian;
869 ///
870 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
871 /// let mut microstate = Microstate::builder()
872 /// .bodies([Body::point(Cartesian::from([1.0, 0.0]))])
873 /// .try_build()?;
874 ///
875 /// microstate
876 /// .update_body_properties(0, Point::new(Cartesian::from([-2.0, 3.0])))?;
877 /// assert_eq!(
878 /// microstate.bodies()[0].item.properties.position,
879 /// [-2.0, 3.0].into()
880 /// );
881 /// assert_eq!(
882 /// microstate.sites()[0].properties.position,
883 /// [-2.0, 3.0].into()
884 /// );
885 /// # Ok(())
886 /// # }
887 /// ```
888 #[inline]
889 #[expect(
890 clippy::missing_panics_doc,
891 reason = "Panic would occur due to a bug in hoomd-rs."
892 )]
893 pub fn update_body_properties(&mut self, body_index: usize, properties: B) -> Result<(), Error>
894 where
895 B: Transform<S> + Position<Position = P>,
896 S: Position<Position = P>,
897 C: Wrap<B> + Wrap<S>,
898 {
899 let body = &mut self.bodies.items[body_index];
900
901 let new_body_properties = self
902 .boundary
903 .wrap(properties)
904 .map_err(|e| Error::UpdateBody(body.tag, e))?;
905
906 // An unknown site in the body might not wrap into the boundary.
907 // Check that they do first before starting to modify internal data
908 // structures. This wraps every site twice on update. Testing
909 // shows that caching/reusing the results of the first wrap
910 // does not change performance at all.
911 for s in &body.item.sites {
912 self.boundary
913 .wrap(new_body_properties.transform(s))
914 .map_err(|e| Error::UpdateBody(body.tag, e))?;
915 }
916
917 body.item.properties = new_body_properties;
918
919 // Update site properties
920 for (i, site_tag) in self.bodies_sites[body_index].iter().enumerate() {
921 let site_index = self.sites.indices[*site_tag]
922 .expect("bodies_sites and site_indices should be consistent");
923 let site_properties = self
924 .boundary
925 .wrap(body.item.properties.transform(&body.item.sites[i]))
926 .expect("sites should be validated as wrappable prior to this loop");
927 self.spatial_data
928 .insert(SiteKey::Primary(*site_tag), *site_properties.position());
929 self.sites.items[site_index].properties = site_properties;
930
931 Self::update_site_ghosts(
932 &self.sites.items[site_index],
933 site_index,
934 &self.boundary,
935 &mut self.sites_ghosts,
936 &mut self.ghosts,
937 &mut self.spatial_data,
938 );
939 }
940
941 Ok(())
942 }
943
944 /// Remove all bodies from the microstate.
945 ///
946 /// The step, substep, seed, and boundary are left unchanged.
947 ///
948 /// # Example
949 ///
950 /// ```
951 /// use hoomd_microstate::{Body, Microstate, property::Point};
952 /// use hoomd_vector::Cartesian;
953 ///
954 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
955 /// let mut microstate = Microstate::builder()
956 /// .bodies([Body::point(Cartesian::from([1.0, 0.0]))])
957 /// .try_build()?;
958 ///
959 /// microstate.clear();
960 /// assert_eq!(microstate.bodies().len(), 0);
961 /// assert_eq!(microstate.sites().len(), 0);
962 /// # Ok(())
963 /// # }
964 /// ```
965 #[inline]
966 pub fn clear(&mut self) {
967 self.bodies.clear();
968 self.sites.clear();
969 self.bodies_sites.clear();
970 self.ghosts.clear();
971 self.sites_ghosts.clear();
972 self.spatial_data.clear();
973 }
974}
975
976/// Access contents of the microstate.
977impl<B, S, X, C> Microstate<B, S, X, C> {
978 /// Access the microstate's tagged bodies in index order.
979 ///
980 /// [`Microstate`] stores bodies in a flat memory region. The [`Tagged`] type
981 /// holds the unique identifier for each body in [`Tagged::tag`] and the
982 /// [`Body`] itself in [`Tagged::item`].
983 ///
984 /// [`bodies`](Microstate::bodies) provides direct immutable access
985 /// to this slice. To mutate a body (and by extension, its sites), see
986 /// [`Microstate::update_body_properties()`].
987 ///
988 /// # Examples
989 ///
990 /// Identify the tag of a body at a given index:
991 ///
992 /// ```
993 /// use hoomd_microstate::{Body, Microstate};
994 /// use hoomd_vector::Cartesian;
995 ///
996 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
997 /// let microstate = Microstate::builder()
998 /// .bodies([
999 /// Body::point(Cartesian::from([1.0, 0.0])),
1000 /// Body::point(Cartesian::from([-1.0, 2.0])),
1001 /// ])
1002 /// .try_build()?;
1003 ///
1004 /// assert_eq!(microstate.bodies()[0].tag, 0);
1005 /// assert_eq!(microstate.bodies()[1].tag, 1);
1006 /// # Ok(())
1007 /// # }
1008 /// ```
1009 ///
1010 /// Compute system-wide properties that are order-independent:
1011 /// ```
1012 /// use hoomd_microstate::{Body, Microstate};
1013 /// use hoomd_vector::{Cartesian, Vector};
1014 ///
1015 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1016 /// let microstate = Microstate::builder()
1017 /// .bodies([
1018 /// Body::point(Cartesian::from([1.0, 0.0])),
1019 /// Body::point(Cartesian::from([-1.0, 2.0])),
1020 /// ])
1021 /// .try_build()?;
1022 ///
1023 /// let average_position = microstate
1024 /// .bodies()
1025 /// .iter()
1026 /// .map(|tagged_body| tagged_body.item.properties.position)
1027 /// .sum::<Cartesian<2>>()
1028 /// / (microstate.bodies().len() as f64);
1029 /// # Ok(())
1030 /// # }
1031 /// ```
1032 #[inline]
1033 pub fn bodies(&self) -> &[Tagged<Body<B, S>>] {
1034 &self.bodies.items
1035 }
1036
1037 /// Identify the index of a body given a tag.
1038 ///
1039 /// Use [`body_indices`](Microstate::body_indices) to locate a specific body in
1040 /// [`Microstate::bodies`].
1041 ///
1042 /// `body_indices()[tag]` is:
1043 /// * [`None`] when there is no body with the given tag in the microstate.
1044 /// * [`Some(index)`](Option::Some) when the body with the given tag is in the
1045 /// microstate. `index` is the index of the body in [`Microstate::bodies`].
1046 ///
1047 /// # Example
1048 ///
1049 /// ```
1050 /// use anyhow::anyhow;
1051 /// use hoomd_microstate::{Body, Microstate};
1052 /// use hoomd_vector::Cartesian;
1053 ///
1054 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1055 /// let mut microstate = Microstate::builder()
1056 /// .bodies([
1057 /// Body::point(Cartesian::from([1.0, 2.0])),
1058 /// Body::point(Cartesian::from([3.0, 4.0])),
1059 /// Body::point(Cartesian::from([5.0, 6.0])),
1060 /// Body::point(Cartesian::from([7.0, 8.0])),
1061 /// ])
1062 /// .try_build()?;
1063 ///
1064 /// let index =
1065 /// microstate.body_indices()[0].ok_or(anyhow!("body 0 is not present"))?;
1066 /// microstate.remove_body(index);
1067 ///
1068 /// assert_eq!(microstate.body_indices()[0], None);
1069 /// assert!(matches!(microstate.body_indices()[3], Some(_)));
1070 ///
1071 /// let index =
1072 /// microstate.body_indices()[2].ok_or(anyhow!("body 2 is not present"))?;
1073 /// assert_eq!(
1074 /// microstate.bodies()[index].item.properties.position,
1075 /// [5.0, 6.0].into()
1076 /// );
1077 /// # Ok(())
1078 /// # }
1079 /// ```
1080 #[inline]
1081 pub fn body_indices(&self) -> &[Option<usize>] {
1082 &self.bodies.indices
1083 }
1084
1085 /// Access the microstate's sites (in the system frame) in index order.
1086 ///
1087 /// [`Microstate`] stores sites twice. Each body in
1088 /// [`bodies`](Microstate::bodies) stores its sites in the body frame of
1089 /// reference. [`Microstate`] also stores a flat vector of sites that have been
1090 /// transformed (see [`Transform`]) to the system reference frame. The [`Site`]
1091 /// type holds the unique identifier for each site in [`Site::site_tag`],
1092 /// the associated body tag in [`Site::body_tag`] and the site's properties in
1093 /// [`Site::properties`].
1094 ///
1095 /// [`sites`](Microstate::sites) provides direct immutable access to the
1096 /// slice of all sites. To mutate a body (and by extension, its sites), see
1097 /// [`Microstate::update_body_properties()`].
1098 ///
1099 /// # Examples
1100 ///
1101 /// Identify the site and body tags of a site at a given index:
1102 ///
1103 /// ```
1104 /// use hoomd_microstate::{Body, Microstate};
1105 /// use hoomd_vector::Cartesian;
1106 ///
1107 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1108 /// let microstate = Microstate::builder()
1109 /// .bodies([
1110 /// Body::point(Cartesian::from([1.0, 0.0])),
1111 /// Body::point(Cartesian::from([-1.0, 2.0])),
1112 /// ])
1113 /// .try_build()?;
1114 ///
1115 /// assert_eq!(microstate.sites()[0].site_tag, 0);
1116 /// assert_eq!(microstate.sites()[0].body_tag, 0);
1117 ///
1118 /// assert_eq!(microstate.sites()[1].body_tag, 1);
1119 /// assert_eq!(microstate.sites()[1].body_tag, 1);
1120 /// # Ok(())
1121 /// # }
1122 /// ```
1123 ///
1124 /// Compute system-wide properties that are order-independent:
1125 /// ```
1126 /// use hoomd_microstate::{Body, Microstate};
1127 /// use hoomd_vector::{Cartesian, Vector};
1128 ///
1129 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1130 /// let microstate = Microstate::builder()
1131 /// .bodies([
1132 /// Body::point(Cartesian::from([1.0, 0.0])),
1133 /// Body::point(Cartesian::from([-1.0, 2.0])),
1134 /// ])
1135 /// .try_build()?;
1136 ///
1137 /// let average_position = microstate
1138 /// .sites()
1139 /// .iter()
1140 /// .map(|site| site.properties.position)
1141 /// .sum::<Cartesian<2>>()
1142 /// / (microstate.sites().len() as f64);
1143 /// # Ok(())
1144 /// # }
1145 /// ```
1146 #[inline]
1147 pub fn sites(&self) -> &[Site<S>] {
1148 &self.sites.items
1149 }
1150
1151 /// Access the ghost sites in the system frame.
1152 ///
1153 /// Each ghost site shares a `site_tag` and `body_tag` with a primary site
1154 /// (in [`sites`]). Ghost sites are only placed when using periodic boundary
1155 /// conditions and are outside the edges of the boundary.
1156 ///
1157 /// [`sites`]: Self::sites
1158 #[inline]
1159 pub fn ghosts(&self) -> &[Site<S>] {
1160 &self.ghosts.items
1161 }
1162
1163 /// Identify the index of a site given a tag.
1164 ///
1165 /// Use [`site_indices`](Microstate::site_indices) to locate a specific site in
1166 /// [`Microstate::sites`].
1167 ///
1168 /// See [`body_indices`](Microstate::body_indices) for details.
1169 #[inline]
1170 pub fn site_indices(&self) -> &[Option<usize>] {
1171 &self.sites.indices
1172 }
1173
1174 /// Iterate over all the sites (in the system reference frame) associated with a body.
1175 ///
1176 /// Use [`iter_body_sites`](Microstate::iter_body_sites) to perform computations
1177 /// in the system reference frame on all sites that are associated with a given
1178 /// body *index*. The borrowed sites are immutable. Call
1179 /// [`Microstate::update_body_properties()`] to mutate a body.
1180 ///
1181 /// `iter_body_sites` always iterates over *primary sites*. In periodic boundary
1182 /// conditions, these sites may be split across one or more parts of the
1183 /// boundary.
1184 ///
1185 /// # Example
1186 ///
1187 /// ```
1188 /// use hoomd_microstate::{Body, Microstate};
1189 /// use hoomd_vector::{Cartesian, Vector};
1190 ///
1191 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1192 /// let microstate = Microstate::builder()
1193 /// .bodies([
1194 /// Body::point(Cartesian::from([1.0, 0.0])),
1195 /// Body::point(Cartesian::from([-1.0, 2.0])),
1196 /// ])
1197 /// .try_build()?;
1198 ///
1199 /// let average_position = microstate
1200 /// .iter_body_sites(0)
1201 /// .map(|site| site.properties.position)
1202 /// .sum::<Cartesian<2>>()
1203 /// / (microstate.bodies()[0].item.sites.len() as f64);
1204 /// # Ok(())
1205 /// # }
1206 /// ```
1207 #[inline]
1208 #[expect(
1209 clippy::missing_panics_doc,
1210 reason = "Panic would occur due to a bug in hoomd-rs."
1211 )]
1212 pub fn iter_body_sites(&self, body_index: usize) -> impl Iterator<Item = &Site<S>> {
1213 self.bodies_sites[body_index].iter().map(|site_tag| {
1214 &self.sites.items[self.sites.indices[*site_tag]
1215 .expect("bodies_sites and site_indices should be consistent")]
1216 })
1217 }
1218
1219 /// Iterate over all sites in monotonically increasing tag order.
1220 ///
1221 /// `iter_sites_tag_order` is especially useful when implementing
1222 /// [`AppendMicrostate`], as GSD files must be written in tag order.
1223 ///
1224 /// [`AppendMicrostate`]: crate::AppendMicrostate
1225 ///
1226 /// # Example
1227 ///
1228 /// ```
1229 /// use hoomd_microstate::{Body, Microstate};
1230 /// use hoomd_vector::Cartesian;
1231 ///
1232 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1233 /// let mut microstate = Microstate::builder()
1234 /// .bodies([
1235 /// Body::point(Cartesian::from([1.0, 0.0])),
1236 /// Body::point(Cartesian::from([-1.0, 2.0])),
1237 /// ])
1238 /// .try_build()?;
1239 ///
1240 /// microstate.remove_body(0);
1241 /// microstate.add_body(Body::point(Cartesian::from([3.0, 1.0])))?;
1242 ///
1243 /// let positions_tag_order: Vec<_> = microstate
1244 /// .iter_sites_tag_order()
1245 /// .map(|s| s.properties.position)
1246 /// .collect();
1247 /// assert_eq!(
1248 /// positions_tag_order,
1249 /// vec![[3.0, 1.0].into(), [-1.0, 2.0].into()]
1250 /// );
1251 ///
1252 /// # Ok(())
1253 /// # }
1254 /// ```
1255 #[inline]
1256 pub fn iter_sites_tag_order(&self) -> impl Iterator<Item = &Site<S>> {
1257 self.sites.iter_tag_order()
1258 }
1259
1260 /// Get the spatial data structure.
1261 #[inline]
1262 pub fn spatial_data(&self) -> &X {
1263 &self.spatial_data
1264 }
1265}
1266
1267impl<P, B, S, X, C> Microstate<B, S, X, C>
1268where
1269 S: Position<Position = P>,
1270 X: PointsNearBall<P, SiteKey>,
1271{
1272 /// Find sites near a point in space.
1273 ///
1274 /// Iterate over all sites and ghost sites within a distance `r` of the
1275 /// given `point`, *and possibly other sites as well*. All sites produced
1276 /// by this iterator will be in the system reference frame. No wrapping is
1277 /// required for ghost sites, which will be slightly outside the boundary
1278 /// condition. When a ghost site is provided by the iterator, its `site_tag`
1279 /// and `body_tag` will match that of the actual site.
1280 ///
1281 /// The iterator does not filter on distance to avoid duplicating effort
1282 /// as many callers already perform circumsphere checks.
1283 ///
1284 /// The caller *may* provide a value for `r` that is larger than the maximum
1285 /// interaction range. In the current implementation, this is not an error.
1286 /// However, in such cases `iter_sites_near` will only iterate over the placed
1287 /// ghosts which are within the boundary's `maximum_interaction_range`.
1288 ///
1289 /// In other words, `iter_sites_near` is meant for use with pairwise functions
1290 /// that follow the minimum image convention.
1291 #[inline]
1292 #[expect(
1293 clippy::missing_panics_doc,
1294 reason = "Will panic only due to a bug in hoomd-rs."
1295 )]
1296 pub fn iter_sites_near(&self, point: &P, r: f64) -> impl Iterator<Item = &Site<S>> {
1297 let potential_sites = self.spatial_data.points_near_ball(point, r);
1298 potential_sites.map(|k| match k {
1299 SiteKey::Primary(tag) => {
1300 let index =
1301 self.sites.indices[tag].expect("sites and spatial data should be consistent");
1302 &self.sites.items[index]
1303 }
1304 SiteKey::Ghost(tag) => {
1305 let index =
1306 self.ghosts.indices[tag].expect("ghosts and spatial data should be consistent");
1307 &self.ghosts.items[index]
1308 }
1309 })
1310 }
1311}
1312
1313/// Manipulate the microstate as a whole.
1314impl<P, B, S, X, C> Microstate<B, S, X, C>
1315where
1316 P: Copy,
1317 B: Clone + Transform<S> + Position<Position = P>,
1318 S: Clone + Position<Position = P> + Default,
1319 C: Clone + Wrap<B> + Wrap<S> + GenerateGhosts<S> + MapPoint<P>,
1320 X: Clone + PointUpdate<P, SiteKey>,
1321{
1322 /// Clone the microstate, mapping or wrapping bodies into a new boundary.
1323 ///
1324 /// The resulting microstate contains the same bodies and sites as the source.
1325 /// All bodies and sites maintain the same index order and tags.
1326 ///
1327 /// `should_map_body` will be called on every body in the microstate. When
1328 /// it returns `true`, `clone_with_boundary` will map the body's position
1329 /// from `self.boundary` to `new_boundary` using [`MapPoint`]. When
1330 /// `should_map_body` returns `false`, the `clone_with_boundary` wraps
1331 /// the body's unmodified position into `new_boundary`. That wrap may fail,
1332 /// especially in closed (or partially closed) boundary conditions.
1333 ///
1334 /// [`MapPoint`]: hoomd_geometry::MapPoint
1335 ///
1336 /// # Errors
1337 ///
1338 /// [`Error::UpdateBody`] when some body or site cannot be wrapped into the
1339 /// new boundary.
1340 ///
1341 /// # Example
1342 ///
1343 /// ```
1344 /// use hoomd_geometry::shape::Rectangle;
1345 /// use hoomd_microstate::{
1346 /// Body, Microstate,
1347 /// boundary::Closed,
1348 /// property::{Point, Position},
1349 /// };
1350 /// use hoomd_vector::Cartesian;
1351 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1352 ///
1353 /// let square = Closed(Rectangle::with_equal_edges(10.0.try_into()?));
1354 /// let microstate = Microstate::builder()
1355 /// .boundary(square)
1356 /// .bodies([Body::point(Cartesian::from([1.0, 2.0]))])
1357 /// .bodies([Body::point(Cartesian::from([3.0, 4.0]))])
1358 /// .try_build()?;
1359 ///
1360 /// let new_square = Closed(Rectangle::with_equal_edges(20.0.try_into()?));
1361 ///
1362 /// let new_microstate =
1363 /// microstate.clone_with_boundary(new_square, |body| body.tag > 0)?;
1364 ///
1365 /// assert_eq!(
1366 /// *new_microstate.bodies()[0].item.properties.position(),
1367 /// Cartesian::from([1.0, 2.0])
1368 /// );
1369 /// assert_eq!(
1370 /// *new_microstate.bodies()[1].item.properties.position(),
1371 /// Cartesian::from([6.0, 8.0])
1372 /// );
1373 /// # Ok(())
1374 /// # }
1375 /// ```
1376 #[allow(
1377 clippy::missing_inline_in_public_items,
1378 reason = "extremely expensive methods should not be inlined"
1379 )]
1380 #[expect(
1381 clippy::missing_panics_doc,
1382 reason = "Panic would occur due to a bug in hoomd-rs."
1383 )]
1384 pub fn clone_with_boundary<F>(
1385 &self,
1386 new_boundary: C,
1387 should_map_body: F,
1388 ) -> Result<Microstate<B, S, X, C>, Error>
1389 where
1390 F: Fn(&Tagged<Body<B, S>>) -> bool,
1391 {
1392 // clone_with_boundary is used in Monte Carlo methods, such as box trial
1393 // moves. Callers expect that any new microstate produced maintains
1394 // the same body/site tag associations, including the same set of free
1395 // tags as there may be external code that refers to specific bodies
1396 // by tag. Therefore, this method cannot construct a new microstate and
1397 // add bodies to it. A full clone is not strictly necessary to preserve
1398 // tags, but it is the lowest effort approach.
1399 let mut new_microstate = self.clone();
1400
1401 // MC methods require the clone as they keep the old microstate for
1402 // rejected moves. MD methods do not need to clone.
1403
1404 new_microstate.boundary = new_boundary;
1405
1406 for body_index in 0..new_microstate.bodies().len() {
1407 let tagged_body = &new_microstate.bodies()[body_index];
1408 let mut new_properties = tagged_body.item.properties.clone();
1409 if should_map_body(tagged_body) {
1410 *new_properties.position_mut() = self
1411 .boundary
1412 .map_point(*new_properties.position(), &new_microstate.boundary)
1413 .expect("body position should be inside the boundary");
1414 }
1415
1416 new_microstate.update_body_properties(body_index, new_properties)?;
1417 }
1418
1419 Ok(new_microstate)
1420 }
1421}
1422
1423impl<P, B, S, X, C, L> Microstate<B, S, X, C>
1424where
1425 S: Position<Position = P>,
1426 X: IndexFromPosition<P, Location = L>,
1427 L: Ord,
1428 Site<S>: Copy,
1429{
1430 /// Sort the sites spatially.
1431 ///
1432 /// `sort_sites` reorders the sites in memory based on their spatial location.
1433 /// `PairwiseCutoff` interactions compute in less them when the sites are sorted
1434 /// because the interacting sites are more likely to be nearby in memory.
1435 ///
1436 /// CPUs have large caches. Typical simulations start to see benefits from sorting
1437 /// when there are more than 100,000 sites. `sort` is a quick operation, so there
1438 /// is no harm in sorting the microstate every few hundred steps regardless of the
1439 /// system size.
1440 #[inline]
1441 pub fn sort_sites(&mut self) {
1442 let mut sort_order = (0..self.sites.len()).collect::<Vec<_>>();
1443 sort_order.sort_by_key(|&i| {
1444 self.spatial_data
1445 .location_from_position(self.sites.items[i].properties.position())
1446 });
1447
1448 let mut new_sites_items = Vec::new();
1449 let mut new_sites_tags = Vec::new();
1450 let mut new_sites_ghosts = Vec::new();
1451
1452 for index in sort_order {
1453 new_sites_items.push(self.sites.items[index]);
1454 new_sites_tags.push(self.sites.tags[index]);
1455 new_sites_ghosts.push(self.sites_ghosts[index].clone());
1456 }
1457
1458 for (index, tag) in new_sites_tags.iter().enumerate() {
1459 self.sites.indices[*tag] = Some(index);
1460 }
1461
1462 let _ = mem::replace(&mut self.sites.items, new_sites_items);
1463 let _ = mem::replace(&mut self.sites.tags, new_sites_tags);
1464 let _ = mem::replace(&mut self.sites_ghosts, new_sites_ghosts);
1465 }
1466}
1467
1468/// Choose parameters when constructing a [`Microstate`].
1469///
1470/// Use a [`MicrostateBuilder`] to choose the values of optional parameters when
1471/// constructing a [`Microstate`]. Some parameters, such as `seed` and `step`,
1472/// cannot be directly modified after building the [`Microstate`].
1473///
1474/// # Example
1475///
1476/// ```
1477/// use hoomd_microstate::{Body, Microstate};
1478/// use hoomd_vector::Cartesian;
1479///
1480/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1481/// let mut microstate = Microstate::builder()
1482/// .step(100_000)
1483/// .seed(0x1234abcd)
1484/// .bodies([
1485/// Body::point(Cartesian::from([1.0, 0.0])),
1486/// Body::point(Cartesian::from([-1.0, 2.0])),
1487/// ])
1488/// .try_build()?;
1489///
1490/// assert_eq!(microstate.step(), 100_000);
1491/// assert_eq!(microstate.seed(), 0x1234abcd);
1492/// assert_eq!(microstate.bodies().len(), 2);
1493/// # Ok(())
1494/// # }
1495/// ```
1496#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1497pub struct MicrostateBuilder<B, S = B, X = AllPairs<SiteKey>, C = Open> {
1498 /// The initial value for step in the resulting [`Microstate`].
1499 step: u64,
1500
1501 /// The random number seed to set in the resulting [`Microstate`].
1502 seed: u32,
1503
1504 /// Bodies to add to the resulting [`Microstate`].
1505 bodies: Vec<Body<B, S>>,
1506
1507 /// Spatial data structure to use in the resulting [`Microstate`].
1508 spatial_data: X,
1509
1510 /// Boundary conditions to apply in the resulting [`Microstate`].
1511 boundary: C,
1512}
1513
1514impl<B, S, X, C> MicrostateBuilder<B, S, X, C> {
1515 /// Choose the boundary conditions in the resulting [`Microstate`].
1516 ///
1517 /// # Example
1518 ///
1519 /// ```
1520 /// use hoomd_geometry::shape::Rectangle;
1521 /// use hoomd_microstate::{Microstate, boundary::Closed};
1522 /// use hoomd_spatial::AllPairs;
1523 /// use hoomd_vector::Cartesian;
1524 ///
1525 /// # use hoomd_microstate::property::Point;
1526 /// # type BodyProperties = Point<Cartesian<2>>;
1527 /// # type SiteProperties = Point<Cartesian<2>>;
1528 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1529 /// let square = Closed(Rectangle::with_equal_edges(10.0.try_into()?));
1530 ///
1531 /// let microstate = Microstate::<BodyProperties, SiteProperties>::builder()
1532 /// .boundary(square)
1533 /// .try_build()?;
1534 ///
1535 /// assert_eq!(microstate.boundary().0.edge_lengths[0].get(), 10.0);
1536 /// # Ok(())
1537 /// # }
1538 /// ```
1539 #[inline]
1540 pub fn boundary<C2>(self, boundary: C2) -> MicrostateBuilder<B, S, X, C2> {
1541 MicrostateBuilder::<B, S, X, C2> {
1542 step: self.step,
1543 seed: self.seed,
1544 bodies: self.bodies,
1545 spatial_data: self.spatial_data,
1546 boundary,
1547 }
1548 }
1549
1550 /// Set the spatial data structure in the resulting [`Microstate`].
1551 ///
1552 /// # Example
1553 ///
1554 /// ```
1555 /// use hoomd_microstate::{Microstate, SiteKey};
1556 /// use hoomd_spatial::VecCell;
1557 /// use hoomd_vector::Cartesian;
1558 ///
1559 /// # use hoomd_microstate::property::Point;
1560 /// # type BodyProperties = Point<Cartesian<2>>;
1561 /// # type SiteProperties = Point<Cartesian<2>>;
1562 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1563 ///
1564 /// let cell_list = VecCell::builder()
1565 /// .nominal_search_radius(2.5.try_into()?)
1566 /// .build();
1567 ///
1568 /// let microstate = Microstate::<BodyProperties, SiteProperties>::builder()
1569 /// .spatial_data(cell_list)
1570 /// .try_build()?;
1571 /// # Ok(())
1572 /// # }
1573 /// ```
1574 #[inline]
1575 pub fn spatial_data<X2>(self, spatial_data: X2) -> MicrostateBuilder<B, S, X2, C> {
1576 MicrostateBuilder::<B, S, X2, C> {
1577 step: self.step,
1578 seed: self.seed,
1579 bodies: self.bodies,
1580 spatial_data,
1581 boundary: self.boundary,
1582 }
1583 }
1584
1585 /// Choose the initial step in the resulting [`Microstate`].
1586 ///
1587 /// The default `step` is 0.
1588 ///
1589 /// # Example
1590 ///
1591 /// ```
1592 /// use hoomd_microstate::{Microstate, boundary::Open, property::Point};
1593 /// use hoomd_vector::Cartesian;
1594 ///
1595 /// # type BodyProperties = Point<Cartesian<2>>;
1596 /// # type SiteProperties = Point<Cartesian<2>>;
1597 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1598 /// let microstate = Microstate::<BodyProperties, SiteProperties>::builder()
1599 /// .step(100_000)
1600 /// .try_build()?;
1601 ///
1602 /// assert_eq!(microstate.step(), 100_000);
1603 /// # Ok(())
1604 /// # }
1605 /// ```
1606 #[inline]
1607 #[must_use]
1608 pub fn step(mut self, step: u64) -> Self {
1609 self.step = step;
1610 self
1611 }
1612
1613 /// Choose the random number seed in the resulting [`Microstate`].
1614 ///
1615 /// The default `seed` is 0.
1616 ///
1617 /// # Example
1618 ///
1619 /// ```
1620 /// use hoomd_microstate::{Microstate, boundary::Open, property::Point};
1621 /// use hoomd_vector::Cartesian;
1622 ///
1623 /// # type BodyProperties = Point<Cartesian<2>>;
1624 /// # type SiteProperties = Point<Cartesian<2>>;
1625 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1626 /// let microstate = Microstate::<BodyProperties, SiteProperties>::builder()
1627 /// .seed(0x1234abcd)
1628 /// .try_build()?;
1629 ///
1630 /// assert_eq!(microstate.seed(), 0x1234abcd);
1631 /// # Ok(())
1632 /// # }
1633 /// ```
1634 #[inline]
1635 #[must_use]
1636 pub fn seed(mut self, seed: u32) -> Self {
1637 self.seed = seed;
1638 self
1639 }
1640
1641 /// Add bodies to the resulting [`Microstate`].
1642 ///
1643 /// All bodies will be appended when this method is called multiple times.
1644 ///
1645 /// # Example
1646 ///
1647 /// ```
1648 /// use hoomd_microstate::{Body, Microstate};
1649 /// use hoomd_vector::Cartesian;
1650 ///
1651 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1652 /// let mut microstate = Microstate::builder()
1653 /// .bodies([
1654 /// Body::point(Cartesian::from([1.0, 0.0])),
1655 /// Body::point(Cartesian::from([-1.0, 2.0])),
1656 /// ])
1657 /// .try_build()?;
1658 ///
1659 /// assert_eq!(microstate.bodies().len(), 2);
1660 /// # Ok(())
1661 /// # }
1662 /// ```
1663 #[inline]
1664 #[must_use]
1665 pub fn bodies<T>(mut self, bodies: T) -> Self
1666 where
1667 T: IntoIterator<Item = Body<B, S>>,
1668 {
1669 self.bodies.extend(bodies);
1670 self
1671 }
1672
1673 /// Construct a [`Microstate`] with the chosen options.
1674 ///
1675 /// # Errors
1676 ///
1677 /// See [`Microstate::extend_bodies()`].
1678 ///
1679 /// # Example
1680 ///
1681 /// ```
1682 /// use hoomd_microstate::{Body, Microstate};
1683 /// use hoomd_vector::Cartesian;
1684 ///
1685 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1686 /// let mut microstate = Microstate::builder()
1687 /// .step(100_000)
1688 /// .seed(0x1234abcd)
1689 /// .bodies([
1690 /// Body::point(Cartesian::from([1.0, 0.0])),
1691 /// Body::point(Cartesian::from([-1.0, 2.0])),
1692 /// ])
1693 /// .try_build()?;
1694 ///
1695 /// assert_eq!(microstate.step(), 100_000);
1696 /// assert_eq!(microstate.seed(), 0x1234abcd);
1697 /// assert_eq!(microstate.bodies().len(), 2);
1698 /// # Ok(())
1699 /// # }
1700 /// ```
1701 #[inline]
1702 pub fn try_build<P>(self) -> Result<Microstate<B, S, X, C>, Error>
1703 where
1704 P: Copy,
1705 B: Transform<S> + Position<Position = P>,
1706 S: Position<Position = P> + Default,
1707 C: Wrap<B> + Wrap<S> + GenerateGhosts<S>,
1708 X: PointUpdate<P, SiteKey>,
1709 {
1710 let mut microstate = Microstate {
1711 step: self.step,
1712 substep: 0,
1713 seed: self.seed,
1714 boundary: self.boundary,
1715 bodies: VecWithTags::new(),
1716 sites: VecWithTags::new(),
1717 bodies_sites: Vec::new(),
1718 ghosts: VecWithTags::new(),
1719 sites_ghosts: Vec::new(),
1720 spatial_data: self.spatial_data,
1721 };
1722
1723 microstate.spatial_data.clear();
1724
1725 microstate.extend_bodies(self.bodies)?;
1726
1727 Ok(microstate)
1728 }
1729}
1730
1731impl<B, S, X, C> fmt::Display for Microstate<B, S, X, C>
1732where
1733 X: fmt::Display,
1734{
1735 /// Summarize the contents of the microstate.
1736 ///
1737 /// This is a slow operation. It is meant to be printed to logs only
1738 /// occasionally, such as at the end of a benchmark or simulation.
1739 ///
1740 /// # Example
1741 ///
1742 /// ```
1743 /// use hoomd_spatial::VecCell;
1744 /// use log::info;
1745 ///
1746 /// let vec_cell = VecCell::<usize, 3>::default();
1747 ///
1748 /// info!("{vec_cell}");
1749 /// ```
1750 #[allow(
1751 clippy::missing_inline_in_public_items,
1752 reason = "no need to inline display"
1753 )]
1754 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1755 writeln!(f, "Microstate:")?;
1756 writeln!(f, "- step, substep: {}, {}.", self.step, self.substep)?;
1757 writeln!(f, "- {} bodies.", self.bodies().len())?;
1758 writeln!(
1759 f,
1760 "- {} sites / {} ghosts.",
1761 self.sites.items.len(),
1762 self.ghosts.items.len()
1763 )?;
1764 write!(f, "{}", self.spatial_data)
1765 }
1766}
1767
1768#[cfg(test)]
1769mod tests {
1770 use super::*;
1771 use crate::{
1772 boundary::{self, Closed, Periodic},
1773 property::Point,
1774 };
1775 use hoomd_geometry::shape::Hypercuboid;
1776 use hoomd_spatial::{HashCell, VecCell};
1777 use hoomd_vector::Cartesian;
1778
1779 use approxim::assert_relative_eq;
1780 use rand::{Rng, SeedableRng, distr::Distribution, rngs::StdRng, seq::SliceRandom};
1781 use rstest::*;
1782 use std::collections::{HashMap, HashSet};
1783
1784 // The doc tests above cover all the trivial cases for every method which
1785 // are not repeated here. The following tests perform self-consistency
1786 // checks on the internal data structures after calling many methods randomly.
1787
1788 const N_STEPS: usize = 1024;
1789 const MAX_BODY_SIZE: usize = 20;
1790 const MAX_INITIAL_BODY_COORDINATE: f64 = 10.0;
1791 const MAX_SITE_COORDINATE: f64 = 5.0;
1792 const MAX_BODY_TRANSLATE: f64 = 0.125;
1793
1794 mod open {
1795 use super::*;
1796 use assert2::{assert, check};
1797 use rand::RngExt;
1798
1799 fn create_body<R: Rng>(rng: &mut R) -> Body<Point<Cartesian<2>>> {
1800 let mut body = Body::point(rng.random::<Cartesian<2>>() * MAX_INITIAL_BODY_COORDINATE);
1801
1802 let n = rng.random_range(1..MAX_BODY_SIZE);
1803 body.sites = (0..n)
1804 .map(|_| Point::new(rng.random::<Cartesian<2>>() * MAX_SITE_COORDINATE))
1805 .collect();
1806
1807 body
1808 }
1809
1810 fn test_consistency<X>(seed: u64)
1811 where
1812 X: PointUpdate<Cartesian<2>, SiteKey> + PointsNearBall<Cartesian<2>, SiteKey> + Default,
1813 {
1814 // Rather than crafting many corner cases by hand, generate many
1815 // microstates randomly by adding, removing, and updating bodies.
1816 // Validate the internal consistency of the microstate when compared
1817 // to an alternate reference.
1818
1819 let mut rng = StdRng::seed_from_u64(seed);
1820 let mut reference_bodies = HashMap::new();
1821 let mut microstate = Microstate::builder()
1822 .spatial_data(X::default())
1823 .try_build()
1824 .expect("default microstate should be valid");
1825
1826 for _ in 0..N_STEPS {
1827 let move_type_r: f64 = rng.random();
1828 if move_type_r > 0.7 {
1829 // Add bodies more often than removing bodies so that typical
1830 // test executions will result in a non-empty microstate.
1831 let body = create_body(&mut rng);
1832 let tag = microstate
1833 .add_body(body.clone())
1834 .expect("all bodies should be allowed with open boundary conditions");
1835 reference_bodies.insert(tag, body);
1836 } else if move_type_r > 0.5 && !microstate.bodies.is_empty() {
1837 let index = rng.random_range(..microstate.bodies.len());
1838 let tag = microstate.bodies()[index].tag;
1839 microstate.remove_body(index);
1840 reference_bodies.remove(&tag);
1841 } else if !microstate.bodies.is_empty() {
1842 let index = rng.random_range(..microstate.bodies.len());
1843 let tag = microstate.bodies()[index].tag;
1844 let body = reference_bodies
1845 .get_mut(&tag)
1846 .expect("tags in the microstate should also be present in the reference");
1847
1848 body.properties.position += rng.random::<Cartesian<2>>() * MAX_BODY_TRANSLATE;
1849 microstate
1850 .update_body_properties(index, body.properties)
1851 .expect("all bodies should be allowed with open boundary conditions");
1852 }
1853 }
1854
1855 check!(microstate.bodies.len() == reference_bodies.len());
1856 check!(
1857 microstate.sites.len()
1858 == reference_bodies.values().map(|body| body.sites.len()).sum()
1859 );
1860
1861 for (tag, optional_index) in microstate.bodies.indices.iter().enumerate() {
1862 if let Some(index) = optional_index {
1863 check!(microstate.bodies()[*index].tag == tag);
1864 check!(reference_bodies.contains_key(&tag));
1865 } else {
1866 check!(!reference_bodies.contains_key(&tag));
1867 }
1868 }
1869
1870 for (tag, body) in &reference_bodies {
1871 let body_index = microstate.body_indices()[*tag]
1872 .expect("tags in the reference should also be present in the microstate");
1873 check!(microstate.bodies()[body_index].item == *body);
1874 }
1875
1876 for (tag, optional_index) in microstate.sites.indices.iter().enumerate() {
1877 if let Some(index) = optional_index {
1878 check!(microstate.sites()[*index].site_tag == tag);
1879 }
1880 }
1881
1882 check!(microstate.spatial_data().len() == microstate.sites.len());
1883 for site in microstate.sites() {
1884 let body_index = microstate.body_indices()[site.body_tag]
1885 .expect("tags in the microstate should also be in the reference");
1886 check!(microstate.bodies_sites[body_index].contains(&site.site_tag));
1887 check!(
1888 microstate
1889 .spatial_data()
1890 .contains_key(&SiteKey::Primary(site.site_tag))
1891 );
1892 }
1893
1894 assert!(microstate.bodies().len() == microstate.bodies_sites.len());
1895 for (body, body_sites) in microstate
1896 .bodies()
1897 .iter()
1898 .zip(microstate.bodies_sites.iter())
1899 {
1900 assert!(body.item.sites.len() == body_sites.len());
1901 for site_tag in body_sites {
1902 let site_index = microstate.site_indices()[*site_tag]
1903 .expect("body_sites should be consistent with site_indices");
1904 check!(microstate.sites()[site_index].body_tag == body.tag);
1905 }
1906 }
1907
1908 for (body_index, body) in microstate.bodies().iter().enumerate() {
1909 for (system_site, local_site) in microstate
1910 .iter_body_sites(body_index)
1911 .zip(body.item.sites.iter())
1912 {
1913 check!(system_site.body_tag == microstate.bodies()[body_index].tag);
1914 check!(system_site.properties == body.item.properties.transform(local_site));
1915 }
1916 }
1917 }
1918
1919 #[rstest]
1920 fn test_consistency_all_pairs(#[values(1, 2, 3, 4)] seed: u64) {
1921 test_consistency::<AllPairs<SiteKey>>(seed);
1922 }
1923
1924 #[rstest]
1925 fn test_consistency_hash_cell(#[values(5, 6, 7, 8)] seed: u64) {
1926 test_consistency::<HashCell<SiteKey, 2>>(seed);
1927 }
1928
1929 #[rstest]
1930 fn test_consistency_vec_cell(#[values(9, 10, 11, 12)] seed: u64) {
1931 test_consistency::<VecCell<SiteKey, 2>>(seed);
1932 }
1933
1934 #[rstest]
1935 fn remove_all(#[values(1, 2, 3, 4)] seed: u64) {
1936 let mut microstate = Microstate::new();
1937 let mut rng = StdRng::seed_from_u64(seed);
1938
1939 for _ in 0..N_STEPS {
1940 let body = create_body(&mut rng);
1941 microstate
1942 .add_body(body)
1943 .expect("all bodies should be allowed in open boundary conditions");
1944 }
1945
1946 let mut removal_order = (0..N_STEPS).collect::<Vec<_>>();
1947 removal_order.shuffle(&mut rng);
1948
1949 for body_tag in removal_order {
1950 let body_index = microstate.body_indices()[body_tag]
1951 .expect("body tags should be assigned in order");
1952 microstate.remove_body(body_index);
1953 }
1954
1955 check!(microstate.bodies().is_empty());
1956 check!(microstate.bodies_sites.is_empty());
1957 check!(microstate.sites().is_empty());
1958 }
1959 }
1960
1961 mod closed {
1962 use super::*;
1963 use assert2::check;
1964
1965 #[fixture]
1966 fn square() -> Closed<Hypercuboid<2>> {
1967 let cuboid = Hypercuboid {
1968 edge_lengths: [
1969 4.0.try_into()
1970 .expect("hard-coded constant should be positive"),
1971 4.0.try_into()
1972 .expect("hard-coded constant should be positive"),
1973 ],
1974 };
1975 Closed(cuboid)
1976 }
1977
1978 #[rstest]
1979 fn add_body_outside(square: Closed<Hypercuboid<2>>) {
1980 let mut microstate = Microstate::builder()
1981 .boundary(square)
1982 .try_build()
1983 .expect("the hard-coded bodies should be in the boundary");
1984
1985 check!(
1986 microstate.add_body(Body::point(Cartesian::from([2.0, 0.0])))
1987 == Err(Error::AddBody(0, boundary::Error::CannotWrapProperties))
1988 );
1989 }
1990
1991 #[rstest]
1992 fn update_body_outside(square: Closed<Hypercuboid<2>>) {
1993 let mut microstate = Microstate::builder()
1994 .boundary(square)
1995 .bodies([Body::point(Cartesian::from([0.0, 0.0]))])
1996 .try_build()
1997 .expect("the hard-coded bodies should be in the boundary");
1998
1999 check!(
2000 microstate.update_body_properties(
2001 0,
2002 Point {
2003 position: [2.0, 0.0].into()
2004 }
2005 ) == Err(Error::UpdateBody(0, boundary::Error::CannotWrapProperties))
2006 );
2007 }
2008
2009 #[rstest]
2010 fn add_site_outside(square: Closed<Hypercuboid<2>>) {
2011 let body = Body {
2012 properties: Point::new(Cartesian::from([1.0, 0.0])),
2013 sites: [Point::new(Cartesian::from([1.0, 0.0]))].into(),
2014 };
2015
2016 let mut microstate = Microstate::builder()
2017 .boundary(square)
2018 .try_build()
2019 .expect("the hard-coded bodies should be in the boundary");
2020
2021 check!(
2022 microstate.add_body(body)
2023 == Err(Error::AddBody(0, boundary::Error::CannotWrapProperties))
2024 );
2025 }
2026
2027 #[rstest]
2028 fn update_site_outside(square: Closed<Hypercuboid<2>>) {
2029 let body = Body {
2030 properties: Point::new(Cartesian::from([0.0, 0.0])),
2031 sites: [Point::new(Cartesian::from([1.0, 0.0]))].into(),
2032 };
2033
2034 let mut microstate = Microstate::builder()
2035 .boundary(square)
2036 .bodies([body])
2037 .try_build()
2038 .expect("the hard-coded bodies should be in the boundary");
2039
2040 check!(
2041 microstate.update_body_properties(
2042 0,
2043 Point {
2044 position: [1.0, 0.0].into()
2045 }
2046 ) == Err(Error::UpdateBody(0, boundary::Error::CannotWrapProperties))
2047 );
2048 }
2049 }
2050
2051 mod periodic {
2052 use super::*;
2053 use assert2::{assert, check};
2054 use rand::RngExt;
2055
2056 fn create_body<R: Rng>(
2057 rng: &mut R,
2058 boundary: &Periodic<Hypercuboid<2>>,
2059 ) -> Body<Point<Cartesian<2>>> {
2060 let mut body = Body::point(boundary.sample(rng));
2061
2062 let n = rng.random_range(1..MAX_BODY_SIZE);
2063 body.sites = (0..n)
2064 .map(|_| Point::new(rng.random::<Cartesian<2>>() * MAX_SITE_COORDINATE))
2065 .collect();
2066
2067 body
2068 }
2069
2070 #[fixture]
2071 fn rectangle() -> Periodic<Hypercuboid<2>> {
2072 let cuboid = Hypercuboid {
2073 edge_lengths: [
2074 10.0.try_into()
2075 .expect("hard-coded constant should be positive"),
2076 20.0.try_into()
2077 .expect("hard-coded constant should be positive"),
2078 ],
2079 };
2080 Periodic::new(1.0, cuboid)
2081 .expect("hard-coded interaction range is less than the box plane distance")
2082 }
2083
2084 #[rstest]
2085 fn add_body_outside(rectangle: Periodic<Hypercuboid<2>>) {
2086 let mut microstate = Microstate::builder()
2087 .boundary(rectangle)
2088 .try_build()
2089 .expect("the hard-coded bodies should be in the boundary");
2090
2091 assert!(microstate.add_body(Body::point(Cartesian::from([11.0, -21.0]))) == Ok(0));
2092
2093 let body = µstate.bodies()[0].item;
2094 assert_relative_eq!(body.properties.position, [1.0, -1.0].into(), epsilon = 1e-6);
2095 check!(microstate.ghosts().len() == 0);
2096 }
2097
2098 #[rstest]
2099 fn update_body_outside(rectangle: Periodic<Hypercuboid<2>>) {
2100 let mut microstate = Microstate::builder()
2101 .boundary(rectangle)
2102 .bodies([Body::point(Cartesian::from([0.0, 0.0]))])
2103 .try_build()
2104 .expect("the hard-coded bodies should be in the boundary");
2105
2106 assert_eq!(
2107 microstate.update_body_properties(
2108 0,
2109 Point {
2110 position: [11.0, -21.0].into()
2111 }
2112 ),
2113 Ok(())
2114 );
2115
2116 let body = µstate.bodies()[0].item;
2117 assert_relative_eq!(body.properties.position, [1.0, -1.0].into(), epsilon = 1e-6);
2118 check!(microstate.ghosts().len() == 0);
2119 }
2120
2121 #[rstest]
2122 fn add_site_outside(rectangle: Periodic<Hypercuboid<2>>) {
2123 let body = Body {
2124 properties: Point::new(Cartesian::from([4.5, 1.0])),
2125 sites: [Point::new(Cartesian::from([1.0, 0.0]))].into(),
2126 };
2127
2128 let mut microstate = Microstate::builder()
2129 .boundary(rectangle)
2130 .try_build()
2131 .expect("the hard-coded bodies should be in the boundary");
2132
2133 check!(microstate.add_body(body) == Ok(0));
2134
2135 let body = µstate.bodies()[0].item;
2136 assert_relative_eq!(body.properties.position, [4.5, 1.0].into(), epsilon = 1e-6);
2137
2138 let site = µstate.sites()[0];
2139 assert_relative_eq!(site.properties.position, [-4.5, 1.0].into(), epsilon = 1e-6);
2140
2141 assert!(microstate.ghosts().len() == 1);
2142 let ghost = µstate.ghosts()[0];
2143 assert_relative_eq!(ghost.properties.position, [5.5, 1.0].into(), epsilon = 1e-6);
2144
2145 check!(ghost.site_tag == site.site_tag);
2146 check!(ghost.body_tag == site.body_tag);
2147 }
2148
2149 #[rstest]
2150 fn update_site_outside(rectangle: Periodic<Hypercuboid<2>>) {
2151 let body = Body {
2152 properties: Point::new(Cartesian::from([0.0, 0.0])),
2153 sites: [Point::new(Cartesian::from([1.0, 0.0]))].into(),
2154 };
2155
2156 let mut microstate = Microstate::builder()
2157 .boundary(rectangle)
2158 .bodies([body])
2159 .try_build()
2160 .expect("the hard-coded bodies should be in the boundary");
2161
2162 assert!(
2163 microstate.update_body_properties(
2164 0,
2165 Point {
2166 position: [4.5, 1.0].into()
2167 }
2168 ) == Ok(())
2169 );
2170
2171 let body = µstate.bodies()[0].item;
2172 assert_relative_eq!(body.properties.position, [4.5, 1.0].into(), epsilon = 1e-6);
2173
2174 let site = µstate.sites()[0];
2175 assert_relative_eq!(site.properties.position, [-4.5, 1.0].into(), epsilon = 1e-6);
2176
2177 assert!(microstate.ghosts().len() == 1);
2178 let ghost = µstate.ghosts()[0];
2179 assert_relative_eq!(ghost.properties.position, [5.5, 1.0].into(), epsilon = 1e-6);
2180
2181 check!(ghost.site_tag == site.site_tag);
2182 check!(ghost.body_tag == site.body_tag);
2183
2184 assert!(
2185 microstate.update_body_properties(
2186 0,
2187 Point {
2188 position: [0.0, 0.0].into()
2189 }
2190 ) == Ok(())
2191 );
2192
2193 check!(microstate.ghosts().len() == 0);
2194 }
2195
2196 fn test_consistency<X>(seed: u64, rectangle: Periodic<Hypercuboid<2>>)
2197 where
2198 X: PointUpdate<Cartesian<2>, SiteKey> + PointsNearBall<Cartesian<2>, SiteKey> + Default,
2199 {
2200 // The boundary-specific unit tests validate that the *right*
2201 // ghosts are created. This test throws random body insertions,
2202 // updates, and removals and ensures that the internal ghost/site
2203 // data structures remain consistent.
2204
2205 let mut rng = StdRng::seed_from_u64(seed);
2206 let mut microstate = Microstate::builder()
2207 .boundary(rectangle)
2208 .spatial_data(X::default())
2209 .try_build()
2210 .expect("the hard-coded bodies should be in the boundary");
2211
2212 for _ in 0..N_STEPS {
2213 let move_type_r: f64 = rng.random();
2214 if move_type_r > 0.7 {
2215 // Add bodies more often than removing bodies so that typical
2216 // test executions will result in a non-empty microstate.
2217 let body = create_body(&mut rng, microstate.boundary());
2218 microstate
2219 .add_body(body.clone())
2220 .expect("all bodies should be wrapped into the boundary");
2221 } else if move_type_r > 0.5 && !microstate.bodies.is_empty() {
2222 let index = rng.random_range(..microstate.bodies.len());
2223 microstate.remove_body(index);
2224 } else if !microstate.bodies.is_empty() {
2225 let index = rng.random_range(..microstate.bodies.len());
2226 let mut body_properties = microstate.bodies()[index].item.properties;
2227
2228 body_properties.position += rng.random::<Cartesian<2>>() * MAX_BODY_TRANSLATE;
2229 microstate
2230 .update_body_properties(index, body_properties)
2231 .expect("all bodies should be wrapped into the boundary");
2232 }
2233 }
2234
2235 // open::consistency validates most of the internal data structures
2236 // in Microstate. periodic::consistency only needs to validate
2237 // the consistency of the ghosts.
2238 let mut sites_with_ghosts = HashSet::new();
2239
2240 assert!(!microstate.ghosts().is_empty());
2241 check!(
2242 microstate.spatial_data().len() == microstate.sites.len() + microstate.ghosts.len()
2243 );
2244 for (ghost, ghost_tag) in microstate.ghosts().iter().zip(µstate.ghosts.tags) {
2245 let parent_site_index = microstate.site_indices()[ghost.site_tag]
2246 .expect("every ghost should have a parent site");
2247 sites_with_ghosts.insert(parent_site_index);
2248 let parent = µstate.sites()[parent_site_index];
2249
2250 check!(parent.site_tag == ghost.site_tag);
2251 check!(parent.body_tag == ghost.body_tag);
2252 check!(
2253 microstate
2254 .spatial_data()
2255 .contains_key(&SiteKey::Ghost(*ghost_tag))
2256 );
2257 }
2258
2259 for (site_index, site_ghosts) in microstate.sites_ghosts.iter().enumerate() {
2260 if sites_with_ghosts.contains(&site_index) {
2261 for ghost_tag in site_ghosts {
2262 let ghost_index = microstate.ghosts.indices[*ghost_tag]
2263 .expect("ghost tag in sites_ghosts should be present");
2264 let ghost = µstate.ghosts()[ghost_index];
2265 let site = µstate.sites()[site_index];
2266 check!(site.site_tag == ghost.site_tag);
2267 check!(site.body_tag == ghost.body_tag);
2268 }
2269 } else {
2270 check!(site_ghosts.is_empty());
2271 }
2272 }
2273 }
2274
2275 #[rstest]
2276 fn test_consistency_all_pairs(
2277 #[values(1, 2, 3, 4)] seed: u64,
2278 rectangle: Periodic<Hypercuboid<2>>,
2279 ) {
2280 test_consistency::<AllPairs<SiteKey>>(seed, rectangle);
2281 }
2282
2283 #[rstest]
2284 fn test_consistency_hash_cell(
2285 #[values(5, 6, 7, 8)] seed: u64,
2286 rectangle: Periodic<Hypercuboid<2>>,
2287 ) {
2288 test_consistency::<HashCell<SiteKey, 2>>(seed, rectangle);
2289 }
2290
2291 #[rstest]
2292 fn test_consistency_vec_cell(
2293 #[values(9, 10, 11, 12)] seed: u64,
2294 rectangle: Periodic<Hypercuboid<2>>,
2295 ) {
2296 test_consistency::<VecCell<SiteKey, 2>>(seed, rectangle);
2297 }
2298
2299 #[rstest]
2300 fn remove_all(#[values(1, 2, 3, 4)] seed: u64, rectangle: Periodic<Hypercuboid<2>>) {
2301 let mut microstate = Microstate::builder()
2302 .boundary(rectangle)
2303 .try_build()
2304 .expect("the hard-coded bodies should be in the boundary");
2305 let mut rng = StdRng::seed_from_u64(seed);
2306
2307 for _ in 0..N_STEPS {
2308 let body = create_body(&mut rng, microstate.boundary());
2309 microstate
2310 .add_body(body)
2311 .expect("all bodies should be allowed in open boundary conditions");
2312 }
2313
2314 let mut removal_order = (0..N_STEPS).collect::<Vec<_>>();
2315 removal_order.shuffle(&mut rng);
2316
2317 for body_tag in removal_order {
2318 let body_index = microstate.body_indices()[body_tag]
2319 .expect("body tags should be assigned in order");
2320 microstate.remove_body(body_index);
2321 }
2322
2323 check!(microstate.bodies().is_empty());
2324 check!(microstate.bodies_sites.is_empty());
2325 check!(microstate.sites().is_empty());
2326 check!(microstate.ghosts().is_empty());
2327 }
2328 }
2329
2330 // TODO: Test iter_sites_near: with and without periodic boundaries
2331}