cellular_raza_building_blocks/domains/
cartesian_cuboid_n.rs

1// Imports from this crate
2use cellular_raza_concepts::*;
3
4// Imports from other crates
5use itertools::Itertools;
6use nalgebra::SVector;
7
8use serde::{Deserialize, Serialize};
9
10/// Helper function to calculate the decomposition of a large number N into n as evenly-sizedchunks
11/// chunks as possible
12/// Examples:
13/// N   n   decomp
14/// 10  3    1 *  4  +  3 *  3
15/// 13  4    1 *  5  +  3 *  4
16/// 100 13   4 * 13  +  4 * 12
17/// 225 16   1 * 15  + 15 * 14
18/// 225 17   4 * 14  + 13 * 13
19pub(super) fn get_decomp_res(n_voxel: usize, n_regions: usize) -> Option<(usize, usize, usize)> {
20    // We calculate how many times we need to drain how many voxels
21    // Example:
22    //      n_voxels    = 59
23    //      n_regions   = 6
24    //      average_len = (59 / 8).ceil() = (9.833 ...).ceil() = 10
25    //
26    // try to solve this equation:
27    //      n_voxels = average_len * n + (average_len-1) * m
28    //      where n,m are whole positive numbers
29    //
30    // We start with    n = n_regions = 6
31    // and with         m = min(0, n_voxel - average_len.pow(2)) = min(0, 59 - 6^2) = 23
32    let mut average_len: i64 = (n_voxel as f64 / n_regions as f64).ceil() as i64;
33
34    let residue = |n: i64, m: i64, avg: i64| n_voxel as i64 - avg * n - (avg - 1) * m;
35
36    let mut n = n_regions as i64;
37    let mut m = 0;
38
39    for _ in 0..n_regions {
40        match residue(n, m, average_len) {
41            0 => {
42                return Some((n as usize, m as usize, average_len as usize));
43            }
44            1..=i64::MAX => {
45                if n == n_regions as i64 {
46                    // Start from the beginning again but with different value for average length
47                    average_len += 1;
48                    n = n_regions as i64;
49                    m = 0;
50                }
51            }
52            i64::MIN..0 => {
53                n -= 1;
54                m += 1;
55            }
56        }
57    }
58    None
59}
60
61/// A generic Domain with a cuboid layout.
62///
63/// This struct can be used to define custom domains on top of its behaviour.
64#[derive(Clone, Debug)]
65pub struct CartesianCuboid<F, const D: usize> {
66    min: SVector<F, D>,
67    max: SVector<F, D>,
68    dx: SVector<F, D>,
69    n_voxels: SVector<usize, D>,
70    /// Seed from which all random numbers will be initially drawn
71    pub rng_seed: u64,
72}
73
74impl<F, const D: usize> CartesianCuboid<F, D>
75where
76    F: Clone,
77{
78    /// Get the minimum point which defines the simulation domain
79    pub fn get_min(&self) -> SVector<F, D> {
80        self.min.clone()
81    }
82
83    /// Get the maximum point which defines the simulation domain
84    pub fn get_max(&self) -> SVector<F, D> {
85        self.max.clone()
86    }
87
88    /// Get the discretization used to generate voxels
89    pub fn get_dx(&self) -> SVector<F, D> {
90        self.dx.clone()
91    }
92
93    /// Get the number of voxels in each dimension of the domain
94    pub fn get_n_voxels(&self) -> SVector<usize, D> {
95        self.n_voxels
96    }
97}
98
99impl<C, Ci, F, const D: usize> Domain<C, CartesianSubDomain<F, D>, Ci> for CartesianCuboid<F, D>
100where
101    C: Position<nalgebra::SVector<F, D>>,
102    F: 'static
103        + num::Float
104        + Copy
105        + core::fmt::Debug
106        + num::FromPrimitive
107        + num::ToPrimitive
108        + core::ops::SubAssign
109        + core::ops::Div<Output = F>
110        + core::ops::DivAssign,
111    Ci: IntoIterator<Item = C>,
112{
113    type SubDomainIndex = usize;
114    type VoxelIndex = [usize; D];
115
116    fn decompose(
117        self,
118        n_subdomains: core::num::NonZeroUsize,
119        cells: Ci,
120    ) -> Result<DecomposedDomain<Self::SubDomainIndex, CartesianSubDomain<F, D>, C>, DecomposeError>
121    {
122        #[derive(Clone, Domain)]
123        struct MyIntermdiatedomain<F, const D: usize>
124        where
125            F: 'static
126                + num::Float
127                + Copy
128                + core::fmt::Debug
129                + num::FromPrimitive
130                + num::ToPrimitive
131                + core::ops::SubAssign
132                + core::ops::Div<Output = F>
133                + core::ops::DivAssign,
134        {
135            #[DomainRngSeed]
136            #[DomainCreateSubDomains]
137            #[SortCells]
138            domain: CartesianCuboid<F, D>,
139        }
140        let my_intermediate_domain = MyIntermdiatedomain { domain: self };
141        my_intermediate_domain.decompose(n_subdomains, cells)
142    }
143}
144
145impl<F, const D: usize> CartesianCuboid<F, D>
146where
147    F: 'static + num::Float + Copy + core::fmt::Debug + num::FromPrimitive + num::ToPrimitive,
148{
149    fn check_min_max(min: &[F; D], max: &[F; D]) -> Result<(), BoundaryError>
150    where
151        F: core::fmt::Debug,
152    {
153        for i in 0..D {
154            if min[i] >= max[i] {
155                return Err(BoundaryError(format!(
156                    "Min {:?} must be smaller than Max {:?} for domain boundaries!",
157                    min, max
158                )));
159            }
160        }
161        Ok(())
162    }
163
164    /// Builds a new [CartesianCuboid] from given boundaries and maximum interaction ranges of the
165    /// containing cells.
166    ///
167    /// ```
168    /// # use cellular_raza_building_blocks::CartesianCuboid;
169    /// let min = [2.0, 3.0, 1.0];
170    /// let max = [10.0, 10.0, 20.0];
171    /// let interaction_range = 2.0;
172    /// let domain = CartesianCuboid::from_boundaries_and_interaction_range(
173    ///     min,
174    ///     max,
175    ///     interaction_range
176    /// )?;
177    ///
178    /// assert_eq!(domain.get_n_voxels()[0], 4);
179    /// assert_eq!(domain.get_n_voxels()[1], 3);
180    /// assert_eq!(domain.get_n_voxels()[2], 9);
181    /// # Ok::<(), Box<dyn std::error::Error>>(())
182    /// ```
183    pub fn from_boundaries_and_interaction_range(
184        min: impl Into<[F; D]>,
185        max: impl Into<[F; D]>,
186        interaction_range: F,
187    ) -> Result<Self, BoundaryError> {
188        // Perform conversions
189        let min: [F; D] = min.into();
190        let max: [F; D] = max.into();
191
192        // Check that the specified min and max are actually smaller / larger
193        Self::check_min_max(&min, &max)?;
194
195        // Calculate the number of voxels from given interaction ranges
196        let mut n_voxels = [0; D];
197        let mut dx = [F::zero(); D];
198        for i in 0..D {
199            let n = ((max[i] - min[i]) / interaction_range).floor();
200            // This conversion should hopefully never fail.
201            n_voxels[i] = n.to_usize().ok_or(BoundaryError(
202                cellular_raza_concepts::format_error_message!(
203                    format!(
204                        "Cannot convert float {:?} of type {} to usize",
205                        n,
206                        std::any::type_name::<F>()
207                    ),
208                    "conversion error during domain setup"
209                ),
210            ))?;
211            dx[i] = (max[i] - min[i]) / n;
212        }
213
214        Ok(Self {
215            min: min.into(),
216            max: max.into(),
217            dx: dx.into(),
218            n_voxels: n_voxels.into(),
219            rng_seed: 0,
220        })
221    }
222
223    /// Builds a new [CartesianCuboid] from given boundaries and the number of voxels per dimension
224    /// specified.
225    pub fn from_boundaries_and_n_voxels(
226        min: impl Into<[F; D]>,
227        max: impl Into<[F; D]>,
228        n_voxels: impl Into<[usize; D]>,
229    ) -> Result<Self, BoundaryError> {
230        let min: [F; D] = min.into();
231        let max: [F; D] = max.into();
232        let n_voxels: [usize; D] = n_voxels.into();
233        Self::check_min_max(&min, &max)?;
234        let mut dx: SVector<F, D> = [F::zero(); D].into();
235        for i in 0..D {
236            let n = F::from_usize(n_voxels[i]).ok_or(BoundaryError(
237                cellular_raza_concepts::format_error_message!(
238                    "conversion error during domain setup",
239                    format!(
240                        "Cannot convert usize {} to float of type {}",
241                        n_voxels[i],
242                        std::any::type_name::<F>()
243                    )
244                ),
245            ))?;
246            dx[i] = (max[i] - min[i]) / n;
247        }
248        Ok(Self {
249            min: min.into(),
250            max: max.into(),
251            dx,
252            n_voxels: n_voxels.into(),
253            rng_seed: 0,
254        })
255    }
256}
257
258impl<F, const D: usize> CartesianCuboid<F, D> {
259    fn get_all_voxel_indices(&self) -> impl IntoIterator<Item = [usize; D]> {
260        use itertools::*;
261        (0..D)
262            .map(|i| 0..self.n_voxels[i])
263            .multi_cartesian_product()
264            .map(|x| {
265                let mut index = [0; D];
266                index.copy_from_slice(&x);
267                index
268            })
269    }
270
271    /// Get the total amount of indices in this domain
272    fn get_n_indices(&self) -> usize {
273        let mut res = 1;
274        for i in 0..D {
275            res *= self.n_voxels[i];
276        }
277        res
278    }
279}
280
281mod test_domain_setup {
282    #[test]
283    fn from_boundaries_and_interaction_range() {
284        use crate::CartesianCuboid;
285        let min = [0.0; 2];
286        let max = [2.0; 2];
287        let interaction_range = 1.0;
288        let _ = CartesianCuboid::from_boundaries_and_interaction_range(min, max, interaction_range)
289            .unwrap();
290        // TODO add actual test case here
291    }
292
293    #[test]
294    fn from_boundaries_and_n_voxels() {
295        use crate::CartesianCuboid;
296        let min = [-100.0f32; 55];
297        let max = [43000.0f32; 55];
298        let n_voxels = [22; 55];
299        let _ = CartesianCuboid::from_boundaries_and_n_voxels(min, max, n_voxels).unwrap();
300        // TODO add actual test case here
301    }
302}
303
304impl<F, const D: usize> CartesianCuboid<F, D>
305where
306    F: 'static
307        + num::Float
308        + Copy
309        + core::fmt::Debug
310        + num::FromPrimitive
311        + num::ToPrimitive
312        + core::ops::SubAssign
313        + core::ops::Div<Output = F>
314        + core::ops::DivAssign,
315{
316    /// Obtains the voxel index given a regular vector
317    ///
318    /// This function can be used in derivatives of this type.
319    pub fn get_voxel_index_of_raw(&self, pos: &SVector<F, D>) -> Result<[usize; D], BoundaryError> {
320        Self::check_min_max(&self.min.into(), &(*pos).into())?;
321        let n_vox = (pos - self.min).component_div(&self.dx);
322        let mut res = [0usize; D];
323        for i in 0..D {
324            res[i] = n_vox[i].to_usize().ok_or(BoundaryError(
325                cellular_raza_concepts::format_error_message!(
326                    "conversion error during domain setup",
327                    format!(
328                        "Cannot convert float {:?} of type {} to usize",
329                        n_vox[i],
330                        std::any::type_name::<F>()
331                    )
332                ),
333            ))?;
334        }
335        Ok(res)
336    }
337}
338
339impl<C, F, const D: usize> SortCells<C> for CartesianCuboid<F, D>
340where
341    F: 'static
342        + num::Float
343        + Copy
344        + core::fmt::Debug
345        + num::FromPrimitive
346        + num::ToPrimitive
347        + core::ops::SubAssign
348        + core::ops::Div<Output = F>
349        + core::ops::DivAssign,
350    C: Position<SVector<F, D>>,
351{
352    type VoxelIndex = [usize; D];
353
354    fn get_voxel_index_of(&self, cell: &C) -> Result<Self::VoxelIndex, BoundaryError> {
355        let pos = cell.pos();
356        self.get_voxel_index_of_raw(&pos)
357    }
358}
359
360impl<C, F, const D: usize> SortCells<C> for CartesianSubDomain<F, D>
361where
362    C: Position<nalgebra::SVector<F, D>>,
363    F: 'static + num::Float + core::fmt::Debug + core::ops::SubAssign + core::ops::DivAssign,
364{
365    type VoxelIndex = [usize; D];
366
367    fn get_voxel_index_of(&self, cell: &C) -> Result<Self::VoxelIndex, BoundaryError> {
368        let pos = cell.pos();
369        self.get_index_of(pos)
370    }
371}
372
373impl<F, const D: usize> DomainRngSeed for CartesianCuboid<F, D> {
374    fn get_rng_seed(&self) -> u64 {
375        self.rng_seed
376    }
377}
378
379#[test]
380fn generate_subdomains() {
381    use DomainCreateSubDomains;
382    let min = [0.0; 3];
383    let max = [100.0; 3];
384    let interaction_range = 20.0;
385    let domain =
386        CartesianCuboid::from_boundaries_and_interaction_range(min, max, interaction_range)
387            .unwrap();
388    let sub_domains = domain
389        .create_subdomains(4.try_into().unwrap())
390        .unwrap()
391        .into_iter()
392        .collect::<Vec<_>>();
393    assert_eq!(sub_domains.len(), 4);
394    assert_eq!(
395        sub_domains
396            .iter()
397            .map(|(_, _, voxels)| voxels.len())
398            .sum::<usize>(),
399        5usize.pow(3)
400    );
401}
402
403/// Subdomain corresponding to the [CartesianCuboid] struct.
404#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
405#[serde(bound = "
406F: 'static
407    + PartialEq
408    + Clone
409    + core::fmt::Debug
410    + Serialize
411    + for<'a> Deserialize<'a>,
412[usize; D]: Serialize + for<'a> Deserialize<'a>,
413")]
414pub struct CartesianSubDomain<F, const D: usize> {
415    min: SVector<F, D>,
416    max: SVector<F, D>,
417    dx: SVector<F, D>,
418    voxels: Vec<[usize; D]>,
419    pub(crate) domain_min: SVector<F, D>,
420    pub(crate) domain_max: SVector<F, D>,
421    domain_n_voxels: SVector<usize, D>,
422}
423
424impl<F, const D: usize> CartesianSubDomain<F, D>
425where
426    F: Clone,
427{
428    /// Get the minimum boundary of the subdomain.
429    /// Note that not all voxels which could be in the space of the subdomain need to be in it.
430    pub fn get_min(&self) -> SVector<F, D> {
431        self.min.clone()
432    }
433
434    /// Get the maximum boundary of the subdomain.
435    /// Note that not all voxels which could be in the space of the subdomain need to be in it.
436    pub fn get_max(&self) -> SVector<F, D> {
437        self.max.clone()
438    }
439
440    /// Get the discretization used to generate voxels
441    pub fn get_dx(&self) -> SVector<F, D> {
442        self.dx.clone()
443    }
444
445    /// Get all voxel indices which are currently in this subdomain
446    pub fn get_voxels(&self) -> Vec<[usize; D]> {
447        self.voxels.clone()
448    }
449
450    /// See [CartesianCuboid::get_min].
451    pub fn get_domain_min(&self) -> SVector<F, D> {
452        self.domain_min.clone()
453    }
454
455    /// See [CartesianCuboid::get_max].
456    pub fn get_domain_max(&self) -> SVector<F, D> {
457        self.domain_max.clone()
458    }
459
460    /// See [CartesianCuboid::get_n_voxels].
461    pub fn get_domain_n_voxels(&self) -> SVector<usize, D> {
462        self.domain_n_voxels
463    }
464}
465
466impl<F, const D: usize> CartesianSubDomain<F, D> {
467    /// Generic method to obtain the voxel index of any type that can be casted to an array.
468    pub fn get_index_of<P>(&self, pos: P) -> Result<[usize; D], BoundaryError>
469    where
470        [F; D]: From<P>,
471        F: 'static + num::Float + core::fmt::Debug + core::ops::SubAssign + core::ops::DivAssign,
472    {
473        let pos: [F; D] = pos.into();
474        let mut res = [0usize; D];
475        for i in 0..D {
476            let n_vox = (pos[i] - self.domain_min[i]) / self.dx[i];
477            res[i] = n_vox.to_usize().ok_or(BoundaryError(
478                cellular_raza_concepts::format_error_message!(
479                    "conversion error during domain setup",
480                    format!(
481                        "Cannot convert float {:?} of type {} to usize",
482                        n_vox,
483                        std::any::type_name::<F>()
484                    )
485                ),
486            ))?;
487        }
488        Ok(res)
489    }
490}
491
492impl<F, const D: usize> DomainCreateSubDomains<CartesianSubDomain<F, D>> for CartesianCuboid<F, D>
493where
494    F: 'static + num::Float + core::fmt::Debug + num::FromPrimitive,
495{
496    type SubDomainIndex = usize;
497    type VoxelIndex = [usize; D];
498
499    fn create_subdomains(
500        &self,
501        n_subdomains: core::num::NonZeroUsize,
502    ) -> Result<
503        impl IntoIterator<
504            Item = (
505                Self::SubDomainIndex,
506                CartesianSubDomain<F, D>,
507                Vec<Self::VoxelIndex>,
508            ),
509        >,
510        DecomposeError,
511    > {
512        let indices = self.get_all_voxel_indices();
513        let n_indices = self.get_n_indices();
514
515        let (n, _m, average_len) = get_decomp_res(n_indices, n_subdomains.into()).ok_or(
516            DecomposeError::Generic("Could not find a suiting decomposition".to_owned()),
517        )?;
518
519        // TODO Currently we are not splitting the voxels apart efficiently
520        // These are subdomains which contain n voxels
521        let switcher = n * average_len;
522        let indices_grouped = indices.into_iter().enumerate().chunk_by(|(i, _)| {
523            use num::Integer;
524            if *i < switcher {
525                i.div_rem(&average_len).0
526            } else {
527                (i - switcher).div_rem(&(average_len - 1).max(1)).0 + n
528            }
529        });
530        let mut res = Vec::new();
531        for (n_subdomain, indices) in indices_grouped.into_iter() {
532            let mut min_vox = [usize::MAX; D];
533            let mut max_vox = [0; D];
534            let voxels = indices
535                .into_iter()
536                .map(|(_, index)| {
537                    for i in 0..D {
538                        min_vox[i] = min_vox[i].min(index[i]);
539                        max_vox[i] = max_vox[i].max(index[i]);
540                    }
541                    index
542                })
543                .collect::<Vec<_>>();
544            let mut min = [F::zero(); D];
545            let mut max = [F::zero(); D];
546            for i in 0..D {
547                let n_vox_min = F::from_usize(min_vox[i]).ok_or(DecomposeError::Generic(
548                    cellular_raza_concepts::format_error_message!(
549                        "conversion error during domain setup",
550                        format!(
551                            "Cannot convert float {:?} of type {} to usize",
552                            min_vox[i],
553                            std::any::type_name::<F>()
554                        )
555                    ),
556                ))?;
557                let n_vox_max = F::from_usize(max_vox[i]).ok_or(DecomposeError::Generic(
558                    cellular_raza_concepts::format_error_message!(
559                        "conversion error during domain setup",
560                        format!(
561                            "Cannot convert float {:?} of type {} to usize",
562                            max_vox[i],
563                            std::any::type_name::<F>()
564                        )
565                    ),
566                ))?;
567                min[i] = self.min[i] + n_vox_min * self.dx[i];
568                max[i] = self.min[i] + (n_vox_max + F::one()) * self.dx[i];
569            }
570            let subdomain = CartesianSubDomain {
571                min: min.into(),
572                max: max.into(),
573                dx: self.dx,
574                voxels: voxels.clone(),
575                domain_min: self.min,
576                domain_max: self.max,
577                domain_n_voxels: self.n_voxels,
578            };
579            res.push((n_subdomain, subdomain, voxels));
580        }
581        Ok(res)
582    }
583}
584
585impl<Coord, F, const D: usize> SubDomainMechanics<Coord, Coord> for CartesianSubDomain<F, D>
586where
587    Coord: Clone,
588    [F; D]: From<Coord>,
589    Coord: From<[F; D]>,
590    Coord: std::fmt::Debug,
591    F: num::Float,
592{
593    fn apply_boundary(&self, pos: &mut Coord, vel: &mut Coord) -> Result<(), BoundaryError> {
594        let mut velocity: [F; D] = vel.clone().into();
595        let mut position: [F; D] = pos.clone().into();
596
597        // Define constant two
598        let two = F::one() + F::one();
599
600        // For each dimension
601        for i in 0..D {
602            // Check if the particle is below lower edge
603            if position[i] < self.domain_min[i] {
604                position[i] = two * self.domain_min[i] - position[i];
605                velocity[i] = velocity[i].abs();
606            }
607            // Check if the particle is over the edge
608            if position[i] > self.domain_max[i] {
609                position[i] = two * self.domain_max[i] - position[i];
610                velocity[i] = -velocity[i].abs();
611            }
612        }
613
614        for (p, (dmin, dmax)) in position
615            .iter()
616            .zip(self.domain_min.iter().zip(self.domain_max.iter()))
617        {
618            if p < dmin || p > dmax {
619                return Err(BoundaryError(format!(
620                    "Particle is out of domain at position {:?}",
621                    pos
622                )));
623            }
624        }
625
626        // Set the position and velocity
627        *pos = position.into();
628        *vel = velocity.into();
629        Ok(())
630    }
631}
632
633impl<F, const D: usize> SubDomain for CartesianSubDomain<F, D> {
634    type VoxelIndex = [usize; D];
635
636    fn get_all_indices(&self) -> Vec<Self::VoxelIndex> {
637        self.voxels.clone()
638    }
639
640    fn get_neighbor_voxel_indices(&self, voxel_index: &Self::VoxelIndex) -> Vec<Self::VoxelIndex> {
641        // Create the bounds for the following creation of all the voxel indices
642        let mut bounds = [[0; 2]; D];
643        for i in 0..D {
644            bounds[i] = [
645                (voxel_index[i] as i64 - 1).max(0) as usize,
646                (voxel_index[i] + 2).min(self.domain_n_voxels[i]),
647            ];
648        }
649
650        // Create voxel indices
651        (0..D)
652            .map(|i| bounds[i][0]..bounds[i][1])
653            .multi_cartesian_product()
654            .map(|ind_v| {
655                let mut res = [0; D];
656                <[usize]>::copy_from_slice(&mut res, &ind_v);
657                res
658            })
659            .filter(|ind| ind != voxel_index)
660            .collect()
661    }
662}
663
664#[cfg(test)]
665mod test {
666    use super::get_decomp_res;
667    use rayon::prelude::*;
668
669    #[test]
670    fn test_get_demomp_res() {
671        #[cfg(debug_assertions)]
672        let max = 500;
673        #[cfg(not(debug_assertions))]
674        let max = 5_000;
675
676        (1..max)
677            .into_par_iter()
678            .map(|n_voxel| {
679                #[cfg(debug_assertions)]
680                let max_regions = 100;
681                #[cfg(not(debug_assertions))]
682                let max_regions = 1_000;
683                for n_regions in 1..max_regions {
684                    match get_decomp_res(n_voxel, n_regions) {
685                        Some(res) => {
686                            let (n, m, average_len) = res;
687                            assert_eq!(n + m, n_regions);
688                            assert_eq!(n * average_len + m * (average_len - 1), n_voxel);
689                        }
690                        None => panic!(
691                            "No result for inputs n_voxel: {} n_regions: {}",
692                            n_voxel, n_regions
693                        ),
694                    }
695                }
696            })
697            .collect::<Vec<()>>();
698    }
699}