poisson_diskus/
bridson.rs

1use rand::Rng;
2use std::collections::HashSet;
3
4use crate::{
5    coord::{calc_distance, calc_distance_pbc, Coord},
6    error::Error,
7    grid::Grid,
8    sample::{gen_init_coord, get_active_index, NBallGen},
9};
10
11// type Coord = Vec<f64>;
12
13/// Generate samples from a Poisson disc distribution within the given box.
14///
15/// The `box_size` array may have any non-zero dimension `D`. Samples are generated with this
16/// dimension and are separated from each other by a minimum distance `rmin`. A set number of
17/// attempts `num_attempts` is made for each sample candidate (30 is suggested as a good value by
18/// Bridson, but this can be increased to produce tighter samples).
19///
20/// This function uses `rand::thread_rng()` as a random number generator. To use another generator,
21/// use the [`bridson_rng`] function.
22///
23/// # Periodic boundary conditions
24/// If `use_pbc` is set to `true` the algorithm will look for neighbours across the periodic borders
25/// of the grid. This is slightly slower: about 25% to 35%, for the same number of generated points.
26pub fn bridson<const D: usize>(
27    box_size: &Coord<D>,
28    rmin: f64,
29    num_attempts: usize,
30    use_pbc: bool,
31) -> Result<Vec<Coord<D>>, Error<D>> {
32    let mut rng = rand::thread_rng();
33
34    bridson_rng(box_size, rmin, num_attempts, use_pbc, &mut rng)
35}
36
37/// Generate samples from a Poisson disc distribution using a specific random number generator.
38///
39/// See [`bridson`] for more information.
40pub fn bridson_rng<R: Rng, const D: usize>(
41    box_size: &Coord<D>,
42    rmin: f64,
43    num_attempts: usize,
44    use_pbc: bool,
45    rng: &mut R,
46) -> Result<Vec<Coord<D>>, Error<D>> {
47    // Validate input numbers as positive and bounded
48    validate_rmin(rmin)?;
49    validate_box_size(box_size)?;
50
51    if box_size.is_empty() {
52        return Ok(vec![]);
53    }
54
55    let shape = get_grid_shape(rmin, box_size);
56    let mut grid: Grid<D> = Grid::new(&shape, box_size);
57
58    let mut sphere_gen = NBallGen::new(rmin);
59
60    let x0 = gen_init_coord(box_size, rng);
61    let grid_index = grid
62        .get_index_from_coord(&x0, use_pbc)
63        .ok_or(Error::GenCoordOutOfBounds(x0.clone()))?;
64
65    let mut active_inds = HashSet::new();
66    let mut samples: Vec<Coord<D>> = Vec::with_capacity(grid.data.len());
67
68    add_sample_to_list_and_grid(x0, grid_index, &mut samples, &mut active_inds, &mut grid);
69
70    while let Some(grid_index) = get_active_index(&active_inds, rng) {
71        let x0 =
72            get_sample_from_grid(grid_index, &samples, &grid).ok_or(Error::InvalidActiveList)?;
73
74        match get_sample_around(
75            x0,
76            &samples,
77            &grid,
78            num_attempts,
79            rmin,
80            use_pbc,
81            &mut sphere_gen,
82            rng,
83        ) {
84            Some(coord) => {
85                let sample_grid_index = grid
86                    .get_index_from_coord(&coord, use_pbc)
87                    .ok_or(Error::GenCoordOutOfBounds(coord.clone()))?;
88
89                add_sample_to_list_and_grid(
90                    coord,
91                    sample_grid_index,
92                    &mut samples,
93                    &mut active_inds,
94                    &mut grid,
95                );
96            }
97            None => {
98                active_inds.remove(&grid_index);
99            }
100        }
101    }
102
103    Ok(samples)
104}
105
106fn add_sample_to_list_and_grid<const D: usize>(
107    coord: Coord<D>,
108    grid_index: usize,
109    samples: &mut Vec<Coord<D>>,
110    active_inds: &mut HashSet<usize>,
111    grid: &mut Grid<D>,
112) {
113    let sample_index = samples.len();
114
115    samples.push(coord);
116    active_inds.insert(grid_index);
117    grid.data[grid_index] = Some(sample_index);
118}
119
120/// Get the coordinate sample if it exists in the grid.
121fn get_sample_from_grid<'a, const D: usize>(
122    grid_index: usize,
123    samples: &'a [Coord<D>],
124    grid: &Grid<D>,
125) -> Option<&'a Coord<D>> {
126    grid.data
127        .get(grid_index)
128        .cloned()
129        .flatten()
130        .and_then(|sample_index| samples.get(sample_index))
131}
132
133fn get_sample_around<R: Rng, const D: usize>(
134    x0: &Coord<D>,
135    samples: &[Coord<D>],
136    grid: &Grid<D>,
137    num_attempts: usize,
138    rmin: f64,
139    use_pbc: bool,
140    sphere_gen: &mut NBallGen<D>,
141    rng: &mut R,
142) -> Option<Coord<D>> {
143    for _ in 0..num_attempts {
144        let x1 = sphere_gen.gen_around(x0, rng);
145
146        if check_if_coord_is_valid(&x1, samples, grid, rmin, use_pbc) {
147            return Some(x1);
148        }
149    }
150
151    None
152}
153
154fn check_if_coord_is_valid<const D: usize>(
155    coord: &Coord<D>,
156    samples: &[Coord<D>],
157    grid: &Grid<D>,
158    rmin: f64,
159    use_pbc: bool,
160) -> bool {
161    match grid.get_position_from_coord(coord) {
162        Some(position) => {
163            let index_ranges: Vec<(isize, isize)> = position
164                .iter()
165                .map(|&i| {
166                    (
167                        i - grid.num_adjacent as isize,
168                        i + grid.num_adjacent as isize,
169                    )
170                })
171                .collect();
172
173            let mut position_buf = Vec::with_capacity(position.len());
174
175            recurse_and_check(
176                &mut position_buf,
177                &index_ranges,
178                coord,
179                samples,
180                grid,
181                &position,
182                rmin,
183                use_pbc,
184            )
185        }
186        None => false,
187    }
188}
189
190/// Recurse through the position range of all dimensions and verify the grid position.
191fn recurse_and_check<const D: usize>(
192    position: &mut Vec<isize>,
193    index_ranges: &[(isize, isize)],
194    coord: &Coord<D>,
195    samples: &[Coord<D>],
196    grid: &Grid<D>,
197    original_position: &[isize],
198    rmin: f64,
199    use_pbc: bool,
200) -> bool {
201    match index_ranges.split_first() {
202        Some((&(imin, imax), tail)) => {
203            for i in imin..=imax {
204                position.push(i);
205
206                let result = if tail.is_empty() {
207                    check_coord_at_position(coord, position.as_ref(), samples, grid, rmin, use_pbc)
208                } else {
209                    recurse_and_check(
210                        position,
211                        tail,
212                        coord,
213                        samples,
214                        grid,
215                        original_position,
216                        rmin,
217                        use_pbc,
218                    )
219                };
220
221                // We are searching for any sample that is too close to the given coordinate,
222                // thus we break the loop and return false at the first sight of one.
223                if !result {
224                    return false;
225                }
226
227                position.pop();
228            }
229        }
230        None => (),
231    }
232
233    true
234}
235
236/// If the grid has a sample at the position, check if it is too close.
237///
238/// This function returns true if the grid does not have a sample at the position
239/// or if the position is further away from the given coordinate than the minimum
240/// distance. Only if there is a sample and it is closer to the coordinate than
241/// the minimum distance is false returned, since we are excluding such points
242/// from the output.
243fn check_coord_at_position<const D: usize>(
244    coord: &Coord<D>,
245    grid_position: &[isize],
246    samples: &[Coord<D>],
247    grid: &Grid<D>,
248    rmin: f64,
249    use_pbc: bool,
250) -> bool {
251    match grid
252        .get_index(grid_position, use_pbc)
253        .and_then(|grid_index| get_sample_from_grid(grid_index, samples, grid))
254    {
255        Some(existing_coord) => {
256            let r = if use_pbc {
257                calc_distance_pbc(coord, existing_coord, &grid.pbc)
258            } else {
259                calc_distance(coord, existing_coord)
260            };
261
262            r >= rmin
263        }
264        None => true,
265    }
266}
267
268/// Calculate the number of bins in each dimension for the grid.
269fn get_grid_shape<const D: usize>(rmin: f64, box_size: &[f64; D]) -> [usize; D] {
270    let max_bin_side_length = rmin / (D as f64).sqrt();
271    let mut shape = [0; D];
272
273    shape
274        .iter_mut()
275        .zip(box_size.iter())
276        .for_each(|(v, length)| {
277            let max_bin_size = length / max_bin_side_length;
278
279            *v = max_bin_size.ceil() as usize;
280        });
281
282    shape
283}
284
285/*************************
286 * User input validation *
287 *************************/
288
289fn validate_number(value: f64) -> bool {
290    value > 0.0 && value.is_finite()
291}
292
293fn validate_rmin<const D: usize>(rmin: f64) -> Result<(), Error<D>> {
294    if validate_number(rmin) {
295        Ok(())
296    } else {
297        Err(Error::InvalidRmin(rmin))
298    }
299}
300
301fn validate_box_size<const D: usize>(box_size: &Coord<D>) -> Result<(), Error<D>> {
302    for &value in box_size {
303        if !validate_number(value) {
304            return Err(Error::InvalidBoxSize {
305                value,
306                box_size: box_size.to_vec(),
307            });
308        }
309    }
310
311    Ok(())
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    fn add_sample_at_grid_position<const D: usize>(
319        position: &[isize],
320        samples: &mut Vec<Coord<D>>,
321        grid: &mut Grid<D>,
322    ) {
323        use std::convert::TryInto;
324
325        let coord: Coord<D> = position
326            .iter()
327            .zip(grid.spacing.iter())
328            .map(|(&i, dx)| (i as f64 + 0.5) * dx)
329            .collect::<Vec<_>>()
330            .as_slice()
331            .try_into()
332            .unwrap();
333
334        let grid_index = grid.get_index(position, true).unwrap();
335        let mut buf = HashSet::new();
336
337        add_sample_to_list_and_grid(coord, grid_index, samples, &mut buf, grid);
338    }
339
340    #[test]
341    fn checking_coordinate_in_empty_grid_returns_true() {
342        let shape = [4, 4];
343        let size = [8.0, 8.0];
344
345        let coord = [3.0, 3.0]; // bin: [1, 1]
346        let rmin = 2.0 * 2.0_f64.sqrt();
347
348        let grid = Grid::new(&shape, &size);
349
350        assert!(check_if_coord_is_valid(&coord, &[], &grid, rmin, true))
351    }
352
353    #[test]
354    fn checking_coordinate_in_grid_with_distant_coord_returns_true() {
355        let shape = [4, 4];
356        let size = [8.0, 8.0];
357
358        let coord = [3.0, 3.0]; // bin: [1, 1]
359        let rmin = 2.0 * 2.0_f64.sqrt();
360
361        let mut grid = Grid::new(&shape, &size);
362        let mut samples = Vec::new();
363
364        add_sample_at_grid_position(&[3, 3], &mut samples, &mut grid);
365
366        assert!(check_if_coord_is_valid(&coord, &samples, &grid, rmin, true))
367    }
368
369    #[test]
370    fn checking_coordinate_uses_num_adjacent_for_its_search_space() {
371        // Same test as above, but increase the num_adjacent value to look in the distant bin
372        let shape = [4, 4];
373        let size = [8.0, 8.0];
374
375        let coord = [1.0, 1.0]; // bin: [0, 0]
376        let rmin = 2.0 * 2.0_f64.sqrt();
377
378        let mut grid = Grid::new(&shape, &size);
379
380        let mut samples = Vec::new();
381        add_sample_at_grid_position(&[3, 3], &mut samples, &mut grid);
382
383        // Increase num_adjacent to look in [3, 3]
384        grid.num_adjacent = 3;
385
386        // Set the coordinate close enough to the candidate (cheating!)
387        samples[0] = [1.0, 2.0];
388
389        assert!(!check_if_coord_is_valid(
390            &coord, &samples, &grid, rmin, true
391        ))
392    }
393
394    #[test]
395    fn checking_coordinate_in_grid_with_close_coord_returns_false() {
396        let shape = [4, 4];
397        let size = [8.0, 8.0];
398
399        let coord = [3.0, 3.0]; // bin: [1, 1]
400        let rmin = 2.0 * 2.0_f64.sqrt();
401
402        let mut grid = Grid::new(&shape, &size);
403        let mut samples = Vec::new();
404
405        add_sample_at_grid_position(&[2, 1], &mut samples, &mut grid);
406
407        assert!(!check_if_coord_is_valid(
408            &coord, &samples, &grid, rmin, true
409        ))
410    }
411
412    #[test]
413    fn checking_coordinate_in_grid_with_coord_in_adjacent_box_but_not_within_rmin_returns_true() {
414        let shape = [4, 4];
415        let size = [8.0, 8.0];
416
417        let coord = [2.01, 2.01]; // bin: [1, 1]
418        let rmin = 2.0 * 2.0_f64.sqrt();
419
420        let mut grid = Grid::new(&shape, &size);
421        let mut samples = Vec::new();
422
423        add_sample_at_grid_position(&[2, 1], &mut samples, &mut grid);
424        samples[0] = [5.99, 3.99]; // bin: 2, 1
425
426        assert!(check_if_coord_is_valid(&coord, &samples, &grid, rmin, true))
427    }
428
429    #[test]
430    fn checking_coordinates_works_over_pbc_connected_cells() {
431        let shape = [4, 4];
432        let size = [8.0, 8.0];
433
434        let coord = [3.0, 0.0]; // bin: [1, 0]
435        let rmin = 2.0 * 2.0_f64.sqrt();
436
437        let mut grid = Grid::new(&shape, &size);
438        let mut samples = Vec::new();
439
440        add_sample_at_grid_position(&[1, 3], &mut samples, &mut grid);
441
442        samples[0] = [3.0, 4.0]; // bin: 1, 3, but out of range
443        assert!(check_if_coord_is_valid(&coord, &samples, &grid, rmin, true));
444
445        samples[0] = [3.0, 8.0]; // bin: 1, 3, in range
446        assert!(!check_if_coord_is_valid(
447            &coord, &samples, &grid, rmin, true
448        ));
449    }
450
451    /*************************
452     * USER INPUT VALIDATION *
453     *************************/
454
455    #[test]
456    fn non_positive_rmin_yields_error() {
457        let box_size = [5.0, 5.0];
458        let num_attempts = 10;
459
460        assert!(bridson(&box_size, 1.0, 10, true).is_ok());
461
462        let rmin_zero = 0.0;
463        let rmin_neg = -1.0;
464        let rmin_nan = f64::NAN;
465        let rmin_inf = f64::INFINITY;
466        let rmin_neg_inf = f64::NEG_INFINITY;
467
468        assert!(bridson(&box_size, rmin_zero, num_attempts, true)
469            .unwrap_err()
470            .is_invalid_rmin());
471
472        assert!(bridson(&box_size, rmin_neg, num_attempts, true)
473            .unwrap_err()
474            .is_invalid_rmin());
475
476        assert!(bridson(&box_size, rmin_nan, num_attempts, true)
477            .unwrap_err()
478            .is_invalid_rmin());
479
480        assert!(bridson(&box_size, rmin_inf, num_attempts, true)
481            .unwrap_err()
482            .is_invalid_rmin());
483
484        assert!(bridson(&box_size, rmin_neg_inf, num_attempts, true)
485            .unwrap_err()
486            .is_invalid_rmin());
487    }
488
489    #[test]
490    fn empty_box_size_yields_no_coords() {
491        assert!(bridson(&[], 1.0, 10, true).unwrap().is_empty());
492    }
493
494    #[test]
495    fn non_positive_box_size_yields_error() {
496        let rmin = 1.0;
497        let num_attempts = 10;
498
499        assert!(bridson(&[5.0, 5.0], rmin, num_attempts, true).is_ok());
500
501        // Use invalid box size values at the front and back of the given
502        // array, to ensure that all are tested.
503        assert!(bridson(&[-5.0, 5.0], rmin, num_attempts, true)
504            .unwrap_err()
505            .is_invalid_box_size());
506
507        assert!(bridson(&[5.0, -5.0], rmin, num_attempts, true)
508            .unwrap_err()
509            .is_invalid_box_size());
510
511        assert!(bridson(&[-5.0, -5.0], rmin, num_attempts, true)
512            .unwrap_err()
513            .is_invalid_box_size());
514
515        assert!(bridson(&[f64::NAN, 5.0], rmin, num_attempts, true)
516            .unwrap_err()
517            .is_invalid_box_size());
518
519        assert!(bridson(&[5.0, f64::NAN], rmin, num_attempts, true)
520            .unwrap_err()
521            .is_invalid_box_size());
522
523        assert!(bridson(&[f64::INFINITY, 5.0], rmin, num_attempts, true)
524            .unwrap_err()
525            .is_invalid_box_size());
526
527        assert!(bridson(&[5.0, f64::INFINITY], rmin, num_attempts, true)
528            .unwrap_err()
529            .is_invalid_box_size());
530
531        assert!(bridson(&[5.0, f64::NEG_INFINITY], rmin, num_attempts, true)
532            .unwrap_err()
533            .is_invalid_box_size());
534
535        assert!(bridson(&[f64::NEG_INFINITY, 5.0], rmin, num_attempts, true)
536            .unwrap_err()
537            .is_invalid_box_size());
538    }
539
540    #[test]
541    fn non_positive_num_attempts_works() {
542        let box_size = [5.0, 5.0];
543        let rmin = 1.0;
544
545        assert!(bridson(&box_size, rmin, 0, true).is_ok());
546        assert!(bridson(&box_size, rmin, 1, true).is_ok());
547        assert!(bridson(&box_size, rmin, 10, true).is_ok());
548    }
549}