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