Skip to main content

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            let m = 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            n_voxels[i] = m.max(1);
212            dx[i] = (max[i] - min[i]) / n;
213        }
214
215        Ok(Self {
216            min: min.into(),
217            max: max.into(),
218            dx: dx.into(),
219            n_voxels: n_voxels.into(),
220            rng_seed: 0,
221        })
222    }
223
224    /// Builds a new [CartesianCuboid] from given boundaries and the number of voxels per dimension
225    /// specified.
226    pub fn from_boundaries_and_n_voxels(
227        min: impl Into<[F; D]>,
228        max: impl Into<[F; D]>,
229        n_voxels: impl Into<[usize; D]>,
230    ) -> Result<Self, BoundaryError> {
231        let min: [F; D] = min.into();
232        let max: [F; D] = max.into();
233        let n_voxels: [usize; D] = n_voxels.into();
234        Self::check_min_max(&min, &max)?;
235        let mut dx: SVector<F, D> = [F::zero(); D].into();
236        for i in 0..D {
237            let n = F::from_usize(n_voxels[i]).ok_or(BoundaryError(
238                cellular_raza_concepts::format_error_message!(
239                    "conversion error during domain setup",
240                    format!(
241                        "Cannot convert usize {} to float of type {}",
242                        n_voxels[i],
243                        std::any::type_name::<F>()
244                    )
245                ),
246            ))?;
247            dx[i] = (max[i] - min[i]) / n;
248        }
249        Ok(Self {
250            min: min.into(),
251            max: max.into(),
252            dx,
253            n_voxels: n_voxels.into(),
254            rng_seed: 0,
255        })
256    }
257}
258
259impl<F, const D: usize> CartesianCuboid<F, D> {
260    fn get_all_voxel_indices(&self) -> impl IntoIterator<Item = [usize; D]> {
261        use itertools::*;
262        (0..D)
263            .map(|i| 0..self.n_voxels[i])
264            .multi_cartesian_product()
265            .map(|x| {
266                let mut index = [0; D];
267                index.copy_from_slice(&x);
268                index
269            })
270    }
271
272    /// Get the total amount of indices in this domain
273    fn get_n_indices(&self) -> usize {
274        let mut res = 1;
275        for i in 0..D {
276            res *= self.n_voxels[i];
277        }
278        res
279    }
280}
281
282mod test_domain_setup {
283    #[test]
284    fn from_boundaries_and_interaction_range() {
285        use crate::CartesianCuboid;
286        let min = [0.0; 2];
287        let max = [2.0; 2];
288        let interaction_range = 1.0;
289        let _ = CartesianCuboid::from_boundaries_and_interaction_range(min, max, interaction_range)
290            .unwrap();
291        // TODO add actual test case here
292    }
293
294    #[test]
295    fn from_boundaries_and_n_voxels() {
296        use crate::CartesianCuboid;
297        let min = [-100.0f32; 55];
298        let max = [43000.0f32; 55];
299        let n_voxels = [22; 55];
300        let _ = CartesianCuboid::from_boundaries_and_n_voxels(min, max, n_voxels).unwrap();
301        // TODO add actual test case here
302    }
303}
304
305impl<F, const D: usize> CartesianCuboid<F, D>
306where
307    F: 'static
308        + num::Float
309        + Copy
310        + core::fmt::Debug
311        + num::FromPrimitive
312        + num::ToPrimitive
313        + core::ops::SubAssign
314        + core::ops::Div<Output = F>
315        + core::ops::DivAssign,
316{
317    /// Obtains the voxel index given a regular vector
318    ///
319    /// This function can be used in derivatives of this type.
320    pub fn get_voxel_index_of_raw(&self, pos: &SVector<F, D>) -> Result<[usize; D], BoundaryError> {
321        Self::check_min_max(&self.min.into(), &(*pos).into())?;
322        let n_vox = (pos - self.min).component_div(&self.dx);
323        let mut res = [0usize; D];
324        for i in 0..D {
325            res[i] = n_vox[i].to_usize().ok_or(BoundaryError(
326                cellular_raza_concepts::format_error_message!(
327                    "conversion error during domain setup",
328                    format!(
329                        "Cannot convert float {:?} of type {} to usize",
330                        n_vox[i],
331                        std::any::type_name::<F>()
332                    )
333                ),
334            ))?;
335        }
336        Ok(res)
337    }
338}
339
340impl<C, F, const D: usize> SortCells<C> for CartesianCuboid<F, D>
341where
342    F: 'static
343        + num::Float
344        + Copy
345        + core::fmt::Debug
346        + num::FromPrimitive
347        + num::ToPrimitive
348        + core::ops::SubAssign
349        + core::ops::Div<Output = F>
350        + core::ops::DivAssign,
351    C: Position<SVector<F, D>>,
352{
353    type VoxelIndex = [usize; D];
354
355    fn get_voxel_index_of(&self, cell: &C) -> Result<Self::VoxelIndex, BoundaryError> {
356        let pos = cell.pos();
357        self.get_voxel_index_of_raw(&pos)
358    }
359}
360
361impl<C, F, const D: usize> SortCells<C> for CartesianSubDomain<F, D>
362where
363    C: Position<nalgebra::SVector<F, D>>,
364    F: 'static + num::Float + core::fmt::Debug + core::ops::SubAssign + core::ops::DivAssign,
365{
366    type VoxelIndex = [usize; D];
367
368    fn get_voxel_index_of(&self, cell: &C) -> Result<Self::VoxelIndex, BoundaryError> {
369        let pos = cell.pos();
370        self.get_index_of(pos)
371    }
372}
373
374impl<F, const D: usize> DomainRngSeed for CartesianCuboid<F, D> {
375    fn get_rng_seed(&self) -> u64 {
376        self.rng_seed
377    }
378}
379
380#[test]
381fn generate_subdomains() {
382    use DomainCreateSubDomains;
383    let min = [0.0; 3];
384    let max = [100.0; 3];
385    let interaction_range = 20.0;
386    let domain =
387        CartesianCuboid::from_boundaries_and_interaction_range(min, max, interaction_range)
388            .unwrap();
389    let sub_domains = domain
390        .create_subdomains(4.try_into().unwrap())
391        .unwrap()
392        .into_iter()
393        .collect::<Vec<_>>();
394    assert_eq!(sub_domains.len(), 4);
395    assert_eq!(
396        sub_domains
397            .iter()
398            .map(|(_, _, voxels)| voxels.len())
399            .sum::<usize>(),
400        5usize.pow(3)
401    );
402}
403
404/// Subdomain corresponding to the [CartesianCuboid] struct.
405#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
406#[serde(bound = "
407F: 'static
408    + PartialEq
409    + Clone
410    + core::fmt::Debug
411    + Serialize
412    + for<'a> Deserialize<'a>,
413[usize; D]: Serialize + for<'a> Deserialize<'a>,
414")]
415pub struct CartesianSubDomain<F, const D: usize> {
416    min: SVector<F, D>,
417    max: SVector<F, D>,
418    dx: SVector<F, D>,
419    voxels: Vec<[usize; D]>,
420    pub(crate) domain_min: SVector<F, D>,
421    pub(crate) domain_max: SVector<F, D>,
422    domain_n_voxels: SVector<usize, D>,
423}
424
425impl<F, const D: usize> CartesianSubDomain<F, D>
426where
427    F: Clone,
428{
429    /// Get the minimum boundary of the subdomain.
430    /// Note that not all voxels which could be in the space of the subdomain need to be in it.
431    pub fn get_min(&self) -> SVector<F, D> {
432        self.min.clone()
433    }
434
435    /// Get the maximum boundary of the subdomain.
436    /// Note that not all voxels which could be in the space of the subdomain need to be in it.
437    pub fn get_max(&self) -> SVector<F, D> {
438        self.max.clone()
439    }
440
441    /// Get the discretization used to generate voxels
442    pub fn get_dx(&self) -> SVector<F, D> {
443        self.dx.clone()
444    }
445
446    /// Get all voxel indices which are currently in this subdomain
447    pub fn get_voxels(&self) -> Vec<[usize; D]> {
448        self.voxels.clone()
449    }
450
451    /// See [CartesianCuboid::get_min].
452    pub fn get_domain_min(&self) -> SVector<F, D> {
453        self.domain_min.clone()
454    }
455
456    /// See [CartesianCuboid::get_max].
457    pub fn get_domain_max(&self) -> SVector<F, D> {
458        self.domain_max.clone()
459    }
460
461    /// See [CartesianCuboid::get_n_voxels].
462    pub fn get_domain_n_voxels(&self) -> SVector<usize, D> {
463        self.domain_n_voxels
464    }
465}
466
467impl<F, const D: usize> CartesianSubDomain<F, D> {
468    /// Generic method to obtain the voxel index of any type that can be casted to an array.
469    pub fn get_index_of<P>(&self, pos: P) -> Result<[usize; D], BoundaryError>
470    where
471        [F; D]: From<P>,
472        F: 'static + num::Float + core::fmt::Debug + core::ops::SubAssign + core::ops::DivAssign,
473    {
474        let pos: [F; D] = pos.into();
475        let mut res = [0usize; D];
476        for i in 0..D {
477            let n_vox = (pos[i] - self.domain_min[i]) / self.dx[i];
478            res[i] = n_vox.to_usize().ok_or(BoundaryError(
479                cellular_raza_concepts::format_error_message!(
480                    "conversion error during domain setup",
481                    format!(
482                        "Cannot convert float {:?} of type {} to usize",
483                        n_vox,
484                        std::any::type_name::<F>()
485                    )
486                ),
487            ))?;
488        }
489        Ok(res)
490    }
491}
492
493impl<F, const D: usize> DomainCreateSubDomains<CartesianSubDomain<F, D>> for CartesianCuboid<F, D>
494where
495    F: 'static + num::Float + core::fmt::Debug + num::FromPrimitive,
496{
497    type SubDomainIndex = usize;
498    type VoxelIndex = [usize; D];
499
500    fn create_subdomains(
501        &self,
502        n_subdomains: core::num::NonZeroUsize,
503    ) -> Result<
504        impl IntoIterator<
505            Item = (
506                Self::SubDomainIndex,
507                CartesianSubDomain<F, D>,
508                Vec<Self::VoxelIndex>,
509            ),
510        >,
511        DecomposeError,
512    > {
513        let indices = self.get_all_voxel_indices();
514        let n_indices = self.get_n_indices();
515
516        let (n, _m, average_len) = get_decomp_res(n_indices, n_subdomains.into()).ok_or(
517            DecomposeError::Generic("Could not find a suiting decomposition".to_owned()),
518        )?;
519
520        // TODO Currently we are not splitting the voxels apart efficiently
521        // These are subdomains which contain n voxels
522        let switcher = n * average_len;
523        let indices_grouped = indices.into_iter().enumerate().chunk_by(|(i, _)| {
524            use num::Integer;
525            if *i < switcher {
526                i.div_rem(&average_len).0
527            } else {
528                (i - switcher).div_rem(&(average_len - 1).max(1)).0 + n
529            }
530        });
531        let mut res = Vec::new();
532        for (n_subdomain, indices) in indices_grouped.into_iter() {
533            let mut min_vox = [usize::MAX; D];
534            let mut max_vox = [0; D];
535            let voxels = indices
536                .into_iter()
537                .map(|(_, index)| {
538                    for i in 0..D {
539                        min_vox[i] = min_vox[i].min(index[i]);
540                        max_vox[i] = max_vox[i].max(index[i]);
541                    }
542                    index
543                })
544                .collect::<Vec<_>>();
545            let mut min = [F::zero(); D];
546            let mut max = [F::zero(); D];
547            for i in 0..D {
548                let n_vox_min = F::from_usize(min_vox[i]).ok_or(DecomposeError::Generic(
549                    cellular_raza_concepts::format_error_message!(
550                        "conversion error during domain setup",
551                        format!(
552                            "Cannot convert float {:?} of type {} to usize",
553                            min_vox[i],
554                            std::any::type_name::<F>()
555                        )
556                    ),
557                ))?;
558                let n_vox_max = F::from_usize(max_vox[i]).ok_or(DecomposeError::Generic(
559                    cellular_raza_concepts::format_error_message!(
560                        "conversion error during domain setup",
561                        format!(
562                            "Cannot convert float {:?} of type {} to usize",
563                            max_vox[i],
564                            std::any::type_name::<F>()
565                        )
566                    ),
567                ))?;
568                min[i] = self.min[i] + n_vox_min * self.dx[i];
569                max[i] = self.min[i] + (n_vox_max + F::one()) * self.dx[i];
570            }
571            let subdomain = CartesianSubDomain {
572                min: min.into(),
573                max: max.into(),
574                dx: self.dx,
575                voxels: voxels.clone(),
576                domain_min: self.min,
577                domain_max: self.max,
578                domain_n_voxels: self.n_voxels,
579            };
580            res.push((n_subdomain, subdomain, voxels));
581        }
582        Ok(res)
583    }
584}
585
586impl<Coord, F, const D: usize> SubDomainMechanics<Coord, Coord> for CartesianSubDomain<F, D>
587where
588    Coord: Clone,
589    [F; D]: From<Coord>,
590    Coord: From<[F; D]>,
591    Coord: std::fmt::Debug,
592    F: num::Float,
593{
594    fn apply_boundary(&self, pos: &mut Coord, vel: &mut Coord) -> Result<(), BoundaryError> {
595        let mut velocity: [F; D] = vel.clone().into();
596        let mut position: [F; D] = pos.clone().into();
597
598        // Define constant two
599        let two = F::one() + F::one();
600
601        // For each dimension
602        for i in 0..D {
603            // Check if the particle is below lower edge
604            if position[i] < self.domain_min[i] {
605                position[i] = two * self.domain_min[i] - position[i];
606                velocity[i] = velocity[i].abs();
607            }
608            // Check if the particle is over the edge
609            if position[i] > self.domain_max[i] {
610                position[i] = two * self.domain_max[i] - position[i];
611                velocity[i] = -velocity[i].abs();
612            }
613        }
614
615        for (p, (dmin, dmax)) in position
616            .iter()
617            .zip(self.domain_min.iter().zip(self.domain_max.iter()))
618        {
619            if p < dmin || p > dmax {
620                return Err(BoundaryError(format!(
621                    "Particle is out of domain at position {:?}",
622                    pos
623                )));
624            }
625        }
626
627        // Set the position and velocity
628        *pos = position.into();
629        *vel = velocity.into();
630        Ok(())
631    }
632}
633
634impl<F, const D: usize> SubDomain for CartesianSubDomain<F, D> {
635    type VoxelIndex = [usize; D];
636
637    fn get_all_indices(&self) -> Vec<Self::VoxelIndex> {
638        self.voxels.clone()
639    }
640
641    fn get_neighbor_voxel_indices(&self, voxel_index: &Self::VoxelIndex) -> Vec<Self::VoxelIndex> {
642        // Create the bounds for the following creation of all the voxel indices
643        let mut bounds = [[0; 2]; D];
644        for i in 0..D {
645            bounds[i] = [
646                (voxel_index[i] as i64 - 1).max(0) as usize,
647                (voxel_index[i] + 2).min(self.domain_n_voxels[i]),
648            ];
649        }
650
651        // Create voxel indices
652        (0..D)
653            .map(|i| bounds[i][0]..bounds[i][1])
654            .multi_cartesian_product()
655            .map(|ind_v| {
656                let mut res = [0; D];
657                <[usize]>::copy_from_slice(&mut res, &ind_v);
658                res
659            })
660            .filter(|ind| ind != voxel_index)
661            .collect()
662    }
663}
664
665#[cfg(test)]
666mod test {
667    use super::get_decomp_res;
668    use rayon::prelude::*;
669
670    #[test]
671    fn test_get_demomp_res() {
672        #[cfg(debug_assertions)]
673        let max = 500;
674        #[cfg(not(debug_assertions))]
675        let max = 5_000;
676
677        (1..max)
678            .into_par_iter()
679            .map(|n_voxel| {
680                #[cfg(debug_assertions)]
681                let max_regions = 100;
682                #[cfg(not(debug_assertions))]
683                let max_regions = 1_000;
684                for n_regions in 1..max_regions {
685                    match get_decomp_res(n_voxel, n_regions) {
686                        Some(res) => {
687                            let (n, m, average_len) = res;
688                            assert_eq!(n + m, n_regions);
689                            assert_eq!(n * average_len + m * (average_len - 1), n_voxel);
690                        }
691                        None => panic!(
692                            "No result for inputs n_voxel: {} n_regions: {}",
693                            n_voxel, n_regions
694                        ),
695                    }
696                }
697            })
698            .collect::<Vec<()>>();
699    }
700}