hoomd_mc/
hypercuboid.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#![expect(
5    clippy::cast_possible_truncation,
6    reason = "the necessary conversions are necessary and have been checked"
7)]
8#![expect(
9    clippy::cast_sign_loss,
10    reason = "the necessary conversions are necessary and have been checked"
11)]
12
13//! Implement Checkerboard for Hypercuboids
14
15use itertools::izip;
16use rand::{Rng, RngExt};
17use serde::{Deserialize, Serialize};
18use serde_with::serde_as;
19use std::array;
20
21use hoomd_geometry::shape::Hypercuboid;
22use hoomd_microstate::boundary::{Closed, Periodic};
23use hoomd_utility::valid::PositiveReal;
24use hoomd_vector::Cartesian;
25
26use crate::{Checkerboard, Cover};
27
28/// `2^N` color checkerboard with axis-aligned hypercuboidal cells.
29///
30/// A `HypercuboidCheckerboard` is comprised of n x m x ... axis aligned
31/// spaces. Each space has the same shape, but each axis might have a different
32/// edge length. Each axis may be periodic or not.
33///
34/// Along the non-periodic axes, the checkerboard overhangs the boundary so that
35/// the entire domain is always covered (for any origin shift up to 1 cell length).
36/// Along periodic axes, the checkerboard has exactly the same width as the domain.
37/// `HypercuboidCheckerboard` wraps points "outside" the checkerboard into the correct
38/// periodic space.
39///
40/// Obviously, `HypercuboidCheckerboard` is a suitable checkerboard for
41/// [`Hypercuboid`] boundary geometries. It can also be a good choice for
42/// other boundaries. For example: cylindrical boundaries (periodic in one
43/// direction) and closed boundaries of any shape. There may be many overhanging
44/// spaces (some completely outside the boundary) in these cases. However, the
45/// checkerboard is still valid and rayon's dynamic load balancing scheme should
46/// be able to handle the empty cells efficiently.
47#[serde_as]
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct HypercuboidCheckerboard<const N: usize> {
50    /// Position of the 0,0,0 cell's lower left corner.
51    origin: Cartesian<N>,
52
53    /// Length of each axis aligned space edge.
54    #[serde_as(as = "[_; N]")]
55    space_width: [f64; N],
56
57    /// Number of spaces along each axis.
58    #[serde_as(as = "[_; N]")]
59    shape: [usize; N],
60
61    /// True when an axis is periodic.
62    #[serde_as(as = "[_; N]")]
63    periodic: [bool; N],
64
65    /// The set of all space indices, grouped by color.
66    space_indices_by_color: Vec<Vec<usize>>,
67}
68
69impl<const N: usize> Default for HypercuboidCheckerboard<N> {
70    /// Construct a default `HypercuboidCheckerboard`.
71    ///
72    /// The default is a 2x2x... non-periodic checkerboard with 1.0 x 1.0 x ... spaces.
73    #[inline]
74    fn default() -> Self {
75        Self {
76            origin: Cartesian::default(),
77            space_width: [1.0; N],
78            shape: [2; N],
79            periodic: [false; N],
80            space_indices_by_color: Self::construct_space_indices_by_color([2; N]),
81        }
82    }
83}
84
85impl<const N: usize> Checkerboard<Cartesian<N>> for HypercuboidCheckerboard<N> {
86    #[inline]
87    fn point_to_space_index(&self, point: &Cartesian<N>) -> Option<usize> {
88        let p = *point - self.origin;
89        let mut space_multi_index: [i64; N] =
90            array::from_fn(|i| (p.coordinates[i] / self.space_width[i]).floor() as i64);
91
92        for (index, shape, periodic) in izip!(&mut space_multi_index, self.shape, self.periodic) {
93            // The origin is in the lower left corner of the box and may be up
94            // to one space width to the left of simulation boundary. Therefore,
95            // negative indices are out of bounds (and should never been seen
96            // for wrapped inputs).
97            if *index < 0 {
98                return None;
99            }
100
101            if periodic {
102                // In periodic boundaries, the checkerboard spaces end before
103                // the right side. The space at the rightmost edge is identical
104                // to space 0 to make the checkerboard coloring commensurate
105                // with the periodic boundary conditions.
106                if *index as usize == shape {
107                    *index = 0;
108                }
109                if *index as usize > shape {
110                    return None;
111                }
112            } else if *index as usize >= shape {
113                // In non-periodic boundaries, the checkerboard extends one full
114                // space to the right of the simulation boundary so that when
115                // it is shifted to the left it will still cover the boundary.
116                // Therefore, any points outside the checkerboard shape are
117                // invalid.
118                return None;
119            }
120        }
121
122        Some(Self::multi_index_to_index(
123            array::from_fn(|i| space_multi_index[i] as usize),
124            self.shape,
125        ))
126    }
127
128    #[inline]
129    fn space_indices_by_color(&self) -> &[Vec<usize>] {
130        &self.space_indices_by_color
131    }
132
133    #[inline]
134    fn num_spaces(&self) -> usize {
135        self.shape.iter().product()
136    }
137}
138
139impl<const N: usize> HypercuboidCheckerboard<N> {
140    /// Collapse a multi-dimensional index to a single value in `[0, num_spaces]`.
141    #[inline]
142    fn multi_index_to_index(multi_index: [usize; N], shape: [usize; N]) -> usize {
143        let mut index: usize = 0;
144        let mut width = 1;
145
146        for i in (0..N).rev() {
147            index += multi_index[i] * width;
148            width *= shape[i];
149        }
150
151        index
152    }
153
154    /// Compute the space width and checkerboard shape.
155    #[inline]
156    fn compute_dimensions(
157        edge_lengths: [PositiveReal; N],
158        interaction_range: PositiveReal,
159        periodic: [bool; N],
160    ) -> ([f64; N], [usize; N]) {
161        let mut shape_inside: [usize; N] =
162            array::from_fn(|i| (edge_lengths[i].get() / interaction_range.get()).floor() as usize);
163
164        assert!(
165            shape_inside.iter().all(|n| *n >= 2),
166            "body interaction range {interaction_range} is too large for the boundary dimensions {edge_lengths:?}"
167        );
168
169        for (width, periodic) in shape_inside.iter_mut().zip(periodic) {
170            // In periodic boundaries, the checkerboard must have an even number
171            // of spaces on a side and fit entirely within the given boundary.
172            // Spaces must be made larger to accommodate.
173            if periodic && !width.is_multiple_of(2) {
174                *width -= 1;
175            }
176
177            // In non-periodic boundaries, the checkerboard must have an even
178            // number of spaces on a side with exactly 1 full space outside
179            // the boundary. Therefore, there must be an odd number of spaces
180            // inside.
181            if !periodic && width.is_multiple_of(2) {
182                *width -= 1;
183            }
184        }
185
186        let space_width = array::from_fn(|i| edge_lengths[i].get() / shape_inside[i] as f64);
187        let shape = array::from_fn(|i| {
188            if periodic[i] {
189                shape_inside[i]
190            } else {
191                shape_inside[i] + 1
192            }
193        });
194
195        (space_width, shape)
196    }
197
198    /// Partition the space indices by color.
199    #[expect(
200        clippy::todo,
201        reason = "there are no known use-cases for parallel 4D, 5D, ... simulations at this time"
202    )]
203    fn construct_space_indices_by_color(shape: [usize; N]) -> Vec<Vec<usize>> {
204        for width in shape {
205            assert!(width.is_multiple_of(2));
206        }
207
208        let mut result = Vec::new();
209
210        if N == 2 {
211            for offset_j in 0..=1 {
212                for offset_i in 0..=1 {
213                    let mut space_indices = Vec::new();
214                    let mut multi_index = [0; N];
215
216                    for j in 0..shape[0] / 2 {
217                        multi_index[0] = 2 * j + offset_j;
218                        for i in 0..shape[1] / 2 {
219                            multi_index[1] = 2 * i + offset_i;
220                            space_indices.push(Self::multi_index_to_index(multi_index, shape));
221                        }
222                    }
223
224                    result.push(space_indices);
225                }
226            }
227
228            return result;
229        }
230
231        if N == 3 {
232            for offset_k in 0..=1 {
233                for offset_j in 0..=1 {
234                    for offset_i in 0..=1 {
235                        let mut space_indices = Vec::new();
236                        let mut multi_index = [0; N];
237
238                        for k in 0..shape[0] / 2 {
239                            multi_index[0] = 2 * k + offset_k;
240                            for j in 0..shape[1] / 2 {
241                                multi_index[1] = 2 * j + offset_j;
242                                for i in 0..shape[2] / 2 {
243                                    multi_index[2] = 2 * i + offset_i;
244                                    space_indices
245                                        .push(Self::multi_index_to_index(multi_index, shape));
246                                }
247                            }
248                        }
249
250                        result.push(space_indices);
251                    }
252                }
253            }
254
255            return result;
256        }
257
258        todo!("Implement a general method");
259    }
260
261    /// Construct a checkerboard with a given origin (for testing).
262    #[cfg(test)]
263    fn with_fixed_origin(
264        interaction_range: PositiveReal,
265        edge_lengths: [PositiveReal; N],
266        periodic: [bool; N],
267    ) -> Self {
268        let (space_width, shape) =
269            Self::compute_dimensions(edge_lengths, interaction_range, periodic);
270
271        Self {
272            space_width,
273            shape,
274            origin: Cartesian::from(array::from_fn(|i| -edge_lengths[i].get() / 2.0)),
275            space_indices_by_color: Self::construct_space_indices_by_color(shape),
276            periodic,
277        }
278    }
279
280    /// Construct a new `HypercuboidCheckerboard`.
281    ///
282    /// Set `interaction_range` to the largest distance between any two
283    /// interacting bodies. `new` will construct a checkerboard that covers
284    /// the range `[-edge_lengths[i]/2.0, edge_lengths[i]/2.0)` (respecting
285    /// `periodic[i]`) for each dimension `i`.
286    #[inline]
287    pub fn new<R: Rng + ?Sized>(
288        rng: &mut R,
289        interaction_range: PositiveReal,
290        edge_lengths: [PositiveReal; N],
291        periodic: [bool; N],
292    ) -> Self {
293        let (space_width, shape) =
294            Self::compute_dimensions(edge_lengths, interaction_range, periodic);
295
296        let offset: [f64; N] = array::from_fn(|_| rng.random());
297
298        let origin = Cartesian {
299            coordinates: array::from_fn(|i| {
300                -edge_lengths[i].get() / 2.0 - offset[i] * space_width[i]
301            }),
302        };
303
304        Self {
305            space_width,
306            shape,
307            origin,
308            space_indices_by_color: Self::construct_space_indices_by_color(shape),
309            periodic,
310        }
311    }
312
313    /// Update an existing checkerboard.
314    ///
315    /// `update` performs the same steps as `new`, but modifies `self` in place.
316    /// Prefer `update` when possible, as it can reuse the space index partitioning
317    /// when the shape doesn't change.
318    #[inline]
319    pub fn update<R: Rng + ?Sized>(
320        &mut self,
321        rng: &mut R,
322        interaction_range: PositiveReal,
323        edge_lengths: [PositiveReal; N],
324        periodic: [bool; N],
325    ) {
326        let (space_width, shape) =
327            Self::compute_dimensions(edge_lengths, interaction_range, periodic);
328
329        let offset: [f64; N] = array::from_fn(|_| rng.random());
330
331        let origin = Cartesian {
332            coordinates: array::from_fn(|i| {
333                -edge_lengths[i].get() / 2.0 - offset[i] * space_width[i]
334            }),
335        };
336
337        if shape != self.shape || self.space_indices_by_color.is_empty() {
338            self.space_indices_by_color = Self::construct_space_indices_by_color(shape);
339            self.shape = shape;
340        }
341
342        self.space_width = space_width;
343        self.origin = origin;
344        self.periodic = periodic;
345    }
346}
347
348impl<const N: usize> Cover<Cartesian<N>> for Closed<Hypercuboid<N>> {
349    type Checkerboard = HypercuboidCheckerboard<N>;
350
351    #[inline]
352    fn cover<R: Rng + ?Sized>(
353        &self,
354        rng: &mut R,
355        interaction_range: PositiveReal,
356    ) -> Self::Checkerboard {
357        HypercuboidCheckerboard::new(rng, interaction_range, self.0.edge_lengths, [false; N])
358    }
359
360    #[inline]
361    fn cover_into<R: Rng + ?Sized>(
362        &self,
363        checkerboard: &mut Self::Checkerboard,
364        rng: &mut R,
365        interaction_range: PositiveReal,
366    ) {
367        checkerboard.update(rng, interaction_range, self.0.edge_lengths, [false; N]);
368    }
369}
370
371impl<const N: usize> Cover<Cartesian<N>> for Periodic<Hypercuboid<N>> {
372    type Checkerboard = HypercuboidCheckerboard<N>;
373
374    #[inline]
375    fn cover<R: Rng + ?Sized>(
376        &self,
377        rng: &mut R,
378        interaction_range: PositiveReal,
379    ) -> Self::Checkerboard {
380        HypercuboidCheckerboard::new(rng, interaction_range, self.shape().edge_lengths, [true; N])
381    }
382
383    #[inline]
384    fn cover_into<R: Rng + ?Sized>(
385        &self,
386        checkerboard: &mut Self::Checkerboard,
387        rng: &mut R,
388        interaction_range: PositiveReal,
389    ) {
390        checkerboard.update(rng, interaction_range, self.shape().edge_lengths, [true; N]);
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use assert2::{assert, check};
397    use rand::{SeedableRng, rngs::StdRng};
398    use rstest::*;
399
400    use super::*;
401
402    #[test]
403    fn test_multi_index_to_index_2d() {
404        let shape = [12, 4];
405        check!(HypercuboidCheckerboard::<2>::multi_index_to_index([0, 0], shape) == 0);
406        check!(HypercuboidCheckerboard::<2>::multi_index_to_index([0, 1], shape) == 1);
407        check!(HypercuboidCheckerboard::<2>::multi_index_to_index([0, 2], shape) == 2);
408        check!(HypercuboidCheckerboard::<2>::multi_index_to_index([0, 3], shape) == 3);
409        check!(HypercuboidCheckerboard::<2>::multi_index_to_index([1, 0], shape) == 4);
410        check!(HypercuboidCheckerboard::<2>::multi_index_to_index([1, 1], shape) == 5);
411        check!(HypercuboidCheckerboard::<2>::multi_index_to_index([1, 2], shape) == 6);
412        check!(HypercuboidCheckerboard::<2>::multi_index_to_index([1, 3], shape) == 7);
413        check!(HypercuboidCheckerboard::<2>::multi_index_to_index([11, 3], shape) == 47);
414    }
415
416    #[test]
417    fn test_multi_index_to_index_3d() {
418        let shape = [12, 4, 6];
419        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([0, 0, 0], shape) == 0);
420        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([0, 0, 1], shape) == 1);
421        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([0, 0, 2], shape) == 2);
422        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([0, 0, 3], shape) == 3);
423        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([0, 0, 4], shape) == 4);
424        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([0, 0, 5], shape) == 5);
425
426        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([0, 1, 0], shape) == 6);
427        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([0, 2, 0], shape) == 12);
428        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([0, 3, 0], shape) == 18);
429
430        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([1, 0, 0], shape) == 24);
431        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([2, 0, 0], shape) == 48);
432        check!(HypercuboidCheckerboard::<3>::multi_index_to_index([3, 3, 2], shape) == 92);
433    }
434
435    #[test]
436    fn test_compute_dimensions_exact() -> anyhow::Result<()> {
437        let (space_width, shape) = HypercuboidCheckerboard::<2>::compute_dimensions(
438            [16.0.try_into()?, 24.0.try_into()?],
439            2.0.try_into()?,
440            [true; 2],
441        );
442        check!(space_width == [2.0, 2.0]);
443        check!(shape == [8, 12]);
444
445        let (space_width, shape) = HypercuboidCheckerboard::<2>::compute_dimensions(
446            [14.0.try_into()?, 22.0.try_into()?],
447            2.0.try_into()?,
448            [false; 2],
449        );
450        check!(space_width == [2.0, 2.0]);
451        check!(shape == [8, 12]);
452
453        let (space_width, shape) = HypercuboidCheckerboard::<2>::compute_dimensions(
454            [16.0.try_into()?, 22.0.try_into()?],
455            2.0.try_into()?,
456            [true, false],
457        );
458        check!(space_width == [2.0, 2.0]);
459        check!(shape == [8, 12]);
460
461        Ok(())
462    }
463
464    #[test]
465    fn test_compute_dimensions_expand() -> anyhow::Result<()> {
466        let (space_width, shape) = HypercuboidCheckerboard::<2>::compute_dimensions(
467            [15.0.try_into()?, 23.0.try_into()?],
468            1.0.try_into()?,
469            [true; 2],
470        );
471        check!(space_width == [15.0 / 14.0, 23.0 / 22.0]);
472        check!(shape == [14, 22]);
473
474        let (space_width, shape) = HypercuboidCheckerboard::<2>::compute_dimensions(
475            [14.0.try_into()?, 22.0.try_into()?],
476            1.0.try_into()?,
477            [false; 2],
478        );
479        check!(space_width == [14.0 / 13.0, 22.0 / 21.0]);
480        check!(shape == [14, 22]);
481
482        Ok(())
483    }
484
485    #[test]
486    fn test_construct_space_indices_by_color_2d() {
487        let space_indices_by_color =
488            HypercuboidCheckerboard::<2>::construct_space_indices_by_color([6, 4]);
489
490        assert!(space_indices_by_color.len() == 4);
491        assert!(space_indices_by_color[0].len() == 6);
492        check!(space_indices_by_color[0][0] == 0);
493        check!(space_indices_by_color[0][1] == 2);
494        check!(space_indices_by_color[0][2] == 8);
495        check!(space_indices_by_color[0][3] == 10);
496        check!(space_indices_by_color[0][4] == 16);
497        check!(space_indices_by_color[0][5] == 18);
498
499        assert!(space_indices_by_color[1].len() == 6);
500        check!(space_indices_by_color[1][0] == 1);
501        check!(space_indices_by_color[1][1] == 3);
502        check!(space_indices_by_color[1][2] == 9);
503        check!(space_indices_by_color[1][3] == 11);
504        check!(space_indices_by_color[1][4] == 17);
505        check!(space_indices_by_color[1][5] == 19);
506
507        assert!(space_indices_by_color[2].len() == 6);
508        check!(space_indices_by_color[2][0] == 4);
509        check!(space_indices_by_color[2][1] == 6);
510        check!(space_indices_by_color[2][2] == 12);
511        check!(space_indices_by_color[2][3] == 14);
512        check!(space_indices_by_color[2][4] == 20);
513        check!(space_indices_by_color[2][5] == 22);
514
515        assert!(space_indices_by_color[3].len() == 6);
516        check!(space_indices_by_color[3][0] == 5);
517        check!(space_indices_by_color[3][1] == 7);
518        check!(space_indices_by_color[3][2] == 13);
519        check!(space_indices_by_color[3][3] == 15);
520        check!(space_indices_by_color[3][4] == 21);
521        check!(space_indices_by_color[3][5] == 23);
522    }
523
524    #[test]
525    fn test_construct_space_indices_by_color_3d_222() {
526        let space_indices_by_color =
527            HypercuboidCheckerboard::<3>::construct_space_indices_by_color([2, 2, 2]);
528
529        assert!(space_indices_by_color.len() == 8);
530        assert!(space_indices_by_color[0].len() == 1);
531        check!(space_indices_by_color[0][0] == 0);
532
533        assert!(space_indices_by_color[1].len() == 1);
534        check!(space_indices_by_color[1][0] == 1);
535
536        assert!(space_indices_by_color[2].len() == 1);
537        check!(space_indices_by_color[2][0] == 2);
538
539        assert!(space_indices_by_color[3].len() == 1);
540        check!(space_indices_by_color[3][0] == 3);
541
542        assert!(space_indices_by_color[4].len() == 1);
543        check!(space_indices_by_color[4][0] == 4);
544
545        assert!(space_indices_by_color[5].len() == 1);
546        check!(space_indices_by_color[5][0] == 5);
547
548        assert!(space_indices_by_color[6].len() == 1);
549        check!(space_indices_by_color[6][0] == 6);
550
551        assert!(space_indices_by_color[7].len() == 1);
552        check!(space_indices_by_color[7][0] == 7);
553    }
554    #[test]
555    fn test_construct_space_indices_by_color_3d_general() {
556        let space_indices_by_color =
557            HypercuboidCheckerboard::<3>::construct_space_indices_by_color([2, 6, 4]);
558
559        assert!(space_indices_by_color.len() == 8);
560        assert!(space_indices_by_color[0].len() == 6);
561        check!(space_indices_by_color[0][0] == 0);
562        check!(space_indices_by_color[0][1] == 2);
563        check!(space_indices_by_color[0][2] == 8);
564        check!(space_indices_by_color[0][3] == 10);
565        check!(space_indices_by_color[0][4] == 16);
566        check!(space_indices_by_color[0][5] == 18);
567
568        assert!(space_indices_by_color[1].len() == 6);
569        check!(space_indices_by_color[1][0] == 1);
570        check!(space_indices_by_color[1][1] == 3);
571        check!(space_indices_by_color[1][2] == 9);
572        check!(space_indices_by_color[1][3] == 11);
573        check!(space_indices_by_color[1][4] == 17);
574        check!(space_indices_by_color[1][5] == 19);
575
576        assert!(space_indices_by_color[2].len() == 6);
577        check!(space_indices_by_color[2][0] == 4);
578        check!(space_indices_by_color[2][1] == 6);
579        check!(space_indices_by_color[2][2] == 12);
580        check!(space_indices_by_color[2][3] == 14);
581        check!(space_indices_by_color[2][4] == 20);
582        check!(space_indices_by_color[2][5] == 22);
583
584        assert!(space_indices_by_color[3].len() == 6);
585        check!(space_indices_by_color[3][0] == 5);
586        check!(space_indices_by_color[3][1] == 7);
587        check!(space_indices_by_color[3][2] == 13);
588        check!(space_indices_by_color[3][3] == 15);
589        check!(space_indices_by_color[3][4] == 21);
590        check!(space_indices_by_color[3][5] == 23);
591
592        assert!(space_indices_by_color[4].len() == 6);
593        check!(space_indices_by_color[4][0] == 24);
594        check!(space_indices_by_color[4][1] == 24 + 2);
595        check!(space_indices_by_color[4][2] == 24 + 8);
596        check!(space_indices_by_color[4][3] == 24 + 10);
597        check!(space_indices_by_color[4][4] == 24 + 16);
598        check!(space_indices_by_color[4][5] == 24 + 18);
599
600        assert!(space_indices_by_color[5].len() == 6);
601        check!(space_indices_by_color[5][0] == 24 + 1);
602        check!(space_indices_by_color[5][1] == 24 + 3);
603        check!(space_indices_by_color[5][2] == 24 + 9);
604        check!(space_indices_by_color[5][3] == 24 + 11);
605        check!(space_indices_by_color[5][4] == 24 + 17);
606        check!(space_indices_by_color[5][5] == 24 + 19);
607
608        assert!(space_indices_by_color[6].len() == 6);
609        check!(space_indices_by_color[6][0] == 24 + 4);
610        check!(space_indices_by_color[6][1] == 24 + 6);
611        check!(space_indices_by_color[6][2] == 24 + 12);
612        check!(space_indices_by_color[6][3] == 24 + 14);
613        check!(space_indices_by_color[6][4] == 24 + 20);
614        check!(space_indices_by_color[6][5] == 24 + 22);
615
616        assert!(space_indices_by_color[7].len() == 6);
617        check!(space_indices_by_color[7][0] == 24 + 5);
618        check!(space_indices_by_color[7][1] == 24 + 7);
619        check!(space_indices_by_color[7][2] == 24 + 13);
620        check!(space_indices_by_color[7][3] == 24 + 15);
621        check!(space_indices_by_color[7][4] == 24 + 21);
622        check!(space_indices_by_color[7][5] == 24 + 23);
623    }
624
625    #[test]
626    fn test_point_to_space_index_periodic() -> anyhow::Result<()> {
627        let checkerboard = HypercuboidCheckerboard::with_fixed_origin(
628            2.0.try_into()?,
629            [16.0.try_into()?, 24.0.try_into()?],
630            [true; 2],
631        );
632        check!(checkerboard.space_width == [2.0, 2.0]);
633        check!(checkerboard.shape == [8, 12]);
634
635        check!(checkerboard.point_to_space_index(&Cartesian::from([-8.0, -12.1])) == None);
636        check!(checkerboard.point_to_space_index(&Cartesian::from([-8.0, -12.0])) == Some(0));
637        check!(checkerboard.point_to_space_index(&Cartesian::from([-8.0, -10.0])) == Some(1));
638        check!(checkerboard.point_to_space_index(&Cartesian::from([-8.0, -8.0])) == Some(2));
639        check!(checkerboard.point_to_space_index(&Cartesian::from([-8.0, -6.0])) == Some(3));
640        check!(checkerboard.point_to_space_index(&Cartesian::from([-8.0, 11.9])) == Some(11));
641        check!(checkerboard.point_to_space_index(&Cartesian::from([-8.0, 12.0])) == Some(0));
642
643        check!(checkerboard.point_to_space_index(&Cartesian::from([-8.1, 0.0])) == None);
644        check!(checkerboard.point_to_space_index(&Cartesian::from([-8.0, 0.0])) == Some(6));
645        check!(checkerboard.point_to_space_index(&Cartesian::from([-6.0, 0.0])) == Some(18));
646        check!(checkerboard.point_to_space_index(&Cartesian::from([-4.0, 0.0])) == Some(30));
647        check!(checkerboard.point_to_space_index(&Cartesian::from([-2.0, 0.0])) == Some(42));
648        check!(checkerboard.point_to_space_index(&Cartesian::from([7.9, 0.0])) == Some(90));
649        check!(checkerboard.point_to_space_index(&Cartesian::from([8.0, 0.0])) == Some(6));
650
651        check!(checkerboard.point_to_space_index(&Cartesian::from([7.9, 11.9])) == Some(95));
652
653        Ok(())
654    }
655
656    #[test]
657    fn test_point_to_space_index_nonperiodic() -> anyhow::Result<()> {
658        let checkerboard = HypercuboidCheckerboard::with_fixed_origin(
659            2.0.try_into()?,
660            [14.0.try_into()?, 22.0.try_into()?],
661            [false; 2],
662        );
663        check!(checkerboard.space_width == [2.0, 2.0]);
664        check!(checkerboard.shape == [8, 12]);
665
666        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, -11.1])) == None);
667        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, -11.0])) == Some(0));
668        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, -9.0])) == Some(1));
669        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, -7.0])) == Some(2));
670        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, -5.0])) == Some(3));
671        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, 10.0])) == Some(10));
672        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, 11.0])) == Some(11));
673        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, 12.9])) == Some(11));
674        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, 13.0])) == None);
675
676        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.1, 1.0])) == None);
677        check!(checkerboard.point_to_space_index(&Cartesian::from([-7.0, 1.0])) == Some(6));
678        check!(checkerboard.point_to_space_index(&Cartesian::from([-5.0, 1.0])) == Some(18));
679        check!(checkerboard.point_to_space_index(&Cartesian::from([-3.0, 1.0])) == Some(30));
680        check!(checkerboard.point_to_space_index(&Cartesian::from([-1.0, 1.0])) == Some(42));
681        check!(checkerboard.point_to_space_index(&Cartesian::from([7.0, 1.0])) == Some(90));
682        check!(checkerboard.point_to_space_index(&Cartesian::from([9.0, 1.0])) == None);
683
684        check!(checkerboard.point_to_space_index(&Cartesian::from([8.9, 12.9])) == Some(95));
685
686        Ok(())
687    }
688
689    #[rstest]
690    fn test_all_points_inside(
691        #[values(true, false)] periodic: bool,
692        #[values(1, 2, 3, 4, 5, 6, 7)] seed: u64,
693    ) -> anyhow::Result<()> {
694        const N_SAMPLES: usize = 512;
695
696        let interaction_range = 1.5.try_into()?;
697        let edge_lengths: [PositiveReal; 2] = [10.0.try_into()?, 7.0.try_into()?];
698        let periodic = [periodic; 2];
699
700        let lower_left =
701            Cartesian::from([-edge_lengths[0].get() / 2.0, -edge_lengths[1].get() / 2.0]);
702        let upper_right = Cartesian::from([
703            edge_lengths[0].get() / 2.0 - 0.1,
704            edge_lengths[1].get() / 2.0 - 0.1,
705        ]);
706
707        let mut rng = StdRng::seed_from_u64(seed);
708
709        for _ in 0..N_SAMPLES {
710            let checkerboard =
711                HypercuboidCheckerboard::new(&mut rng, interaction_range, edge_lengths, periodic);
712
713            let boundary = Hypercuboid { edge_lengths };
714
715            for _ in 0..N_SAMPLES {
716                let v: Cartesian<2> = rng.sample(&boundary);
717
718                check!(checkerboard.point_to_space_index(&v).is_some());
719                check!(checkerboard.point_to_space_index(&lower_left) == Some(0));
720                check!(checkerboard.point_to_space_index(&upper_right).is_some());
721            }
722        }
723
724        Ok(())
725    }
726}