1#![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
13use 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#[serde_as]
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct HypercuboidCheckerboard<const N: usize> {
50 origin: Cartesian<N>,
52
53 #[serde_as(as = "[_; N]")]
55 space_width: [f64; N],
56
57 #[serde_as(as = "[_; N]")]
59 shape: [usize; N],
60
61 #[serde_as(as = "[_; N]")]
63 periodic: [bool; N],
64
65 space_indices_by_color: Vec<Vec<usize>>,
67}
68
69impl<const N: usize> Default for HypercuboidCheckerboard<N> {
70 #[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 if *index < 0 {
98 return None;
99 }
100
101 if periodic {
102 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 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 #[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 #[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 if periodic && !width.is_multiple_of(2) {
174 *width -= 1;
175 }
176
177 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 #[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 #[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 #[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 #[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}