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