Skip to main content

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 = &microstate.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 = &microstate.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 = &microstate.bodies()[0].item;
2273            assert_relative_eq!(body.properties.position, [4.5, 1.0].into(), epsilon = 1e-6);
2274
2275            let site = &microstate.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 = &microstate.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 = &microstate.bodies()[0].item;
2309            assert_relative_eq!(body.properties.position, [4.5, 1.0].into(), epsilon = 1e-6);
2310
2311            let site = &microstate.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 = &microstate.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(&microstate.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 = &microstate.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 = &microstate.ghosts()[ghost_index];
2403                        let site = &microstate.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}