PointSampler library (C++)
Loading...
Searching...
No Matches
poisson_disk_sampling.hpp
Go to the documentation of this file.
1/* Copyright (c) 2025 Otto Link. Distributed under the terms of the GNU General
2 Public License. The full license is in the file LICENSE, distributed with
3 this software. */
4#pragma once
5#include <functional>
6#include <optional>
7
9
10namespace ps
11{
12
13template <typename T, size_t N> struct GridND
14{
15 std::vector<std::optional<Point<T, N>>> cells;
16 std::array<size_t, N> grid_size{};
18
19 GridND(const std::array<size_t, N> &size, T cell_size_)
21 {
22 size_t total = 1;
23 for (auto s : size)
24 total *= s;
25 cells.resize(total);
26 }
27
28 // Convert a point coordinate to grid index in each dimension
29 std::array<size_t, N> point_to_grid(const Point<T, N> &p,
30 const std::array<std::pair<T, T>, N> &ranges) const
31 {
32 std::array<size_t, N> idx{};
33 for (size_t i = 0; i < N; ++i)
34 {
35 T clamped = std::min(std::max(p[i], ranges[i].first), ranges[i].second);
36 T pos = (clamped - ranges[i].first) / cell_size;
37 idx[i] = static_cast<size_t>(std::floor(pos));
38 if (idx[i] >= grid_size[i])
39 idx[i] = grid_size[i] - 1;
40 }
41 return idx;
42 }
43
44 // Linear index for grid cell
45 size_t linear_index(const std::array<size_t, N> &idx) const
46 {
47 size_t lin_idx = 0;
48 size_t stride = 1;
49 for (size_t i = 0; i < N; ++i)
50 {
51 lin_idx += idx[i] * stride;
52 stride *= grid_size[i];
53 }
54 return lin_idx;
55 }
56
57 std::optional<Point<T, N>> operator[](const std::array<size_t, N> &idx) const
58 {
59 return cells[linear_index(idx)];
60 }
61
62 std::optional<Point<T, N>> &operator[](const std::array<size_t, N> &idx)
63 {
64 return cells[linear_index(idx)];
65 }
66};
67
68template <typename T, size_t N, typename ScaleFn>
70 const Point<T, N> &p,
72 const std::array<std::pair<T, T>, N> &ranges,
74{
76
77 // Convert point to grid coordinates
78 auto idx = grid.point_to_grid(p, ranges);
79
80 // Calculate the radius in cells to check neighbors
81 int radius = static_cast<int>(std::ceil(scaled_min_dist_p / grid.cell_size));
82
83 // Iterate neighbor cells in N dimensions (hypercube)
84 // We'll do a recursive iteration over N dims
85
86 std::array<int, N> offsets{};
87 for (size_t i = 0; i < N; ++i)
88 offsets[i] = -radius;
89
90 bool result = false;
91
92 // Recursive lambda to iterate neighbors
93 std::function<void(size_t)> check_neighbors;
94 check_neighbors = [&](size_t dim)
95 {
96 if (dim == N)
97 {
98 // Compute neighbor cell index
99 std::array<size_t, N> neighbor_idx{};
100 for (size_t d = 0; d < N; ++d)
101 {
102 int val = static_cast<int>(idx[d]) + offsets[d];
103 if (val < 0 || val >= static_cast<int>(grid.grid_size[d]))
104 return; // out of grid
105 // bounds
106 neighbor_idx[d] = static_cast<size_t>(val);
107 }
108
109 const auto &slot = grid[neighbor_idx];
110 if (slot)
111 {
115
116 Point<T, N> diff = p - slot.value();
117 T dist_sq = T(0);
118 for (size_t i = 0; i < N; ++i)
119 dist_sq += diff[i] * diff[i];
120
122 result = true;
123 }
124 return;
125 }
126
127 for (offsets[dim] = -radius; offsets[dim] <= radius; ++offsets[dim])
128 {
129 check_neighbors(dim + 1);
130 if (result)
131 return;
132 }
133 };
134
136 return result;
137}
138
139template <typename T, size_t N>
142 std::mt19937 &gen,
143 std::function<T(const Point<T, N> &)> scale_fn)
144{
145 std::uniform_real_distribution<T> dist_angle(0, 2 * M_PI);
146 std::uniform_real_distribution<T> dist_radius(0, 1);
147
148 // Generate random direction in N dimensions using normal distribution
149 // method
150 std::normal_distribution<T> normal(0, 1);
151
153 T length_dir = 0;
154 do
155 {
156 length_dir = 0;
157 for (size_t i = 0; i < N; ++i)
158 {
159 dir[i] = normal(gen);
160 length_dir += dir[i] * dir[i];
161 }
162 length_dir = std::sqrt(length_dir);
163 } while (length_dir == 0);
164
165 for (size_t i = 0; i < N; ++i)
166 dir[i] /= length_dir;
167
169 std::uniform_real_distribution<T> dist_r(scaled_min_dist, 2 * scaled_min_dist);
170 T r = dist_r(gen);
171
173 for (size_t i = 0; i < N; ++i)
174 p[i] = center[i] + dir[i] * r;
175
176 return p;
177}
178
219template <typename T, size_t N, typename ScaleFn>
220std::vector<Point<T, N>> poisson_disk_sampling(
221 size_t count,
222 const std::array<std::pair<T, T>, N> &ranges,
225 std::optional<unsigned int> seed = std::nullopt,
226 size_t new_points_attempts = 30)
227{
228 if (count == 0)
229 return {};
230
231 std::mt19937 gen(seed ? *seed : std::random_device{}());
232
233 T cell_size = base_min_dist / std::sqrt(static_cast<T>(N));
234
235 // Compute grid size per axis
236 std::array<size_t, N> grid_size{};
237 for (size_t i = 0; i < N; ++i)
238 {
239 T axis_len = ranges[i].second - ranges[i].first;
240 grid_size[i] = static_cast<size_t>(std::ceil(axis_len / cell_size));
241 }
242
243 GridND<T, N> grid(grid_size, cell_size);
244
245 std::vector<Point<T, N>> sample_points;
246 sample_points.reserve(count);
247
248 std::vector<Point<T, N>> process_list;
249 process_list.reserve(count);
250
251 // Generate first point randomly inside ranges
253 for (size_t i = 0; i < N; ++i)
254 {
255 std::uniform_real_distribution<T> dist_axis(ranges[i].first, ranges[i].second);
257 }
258
259 sample_points.push_back(first_point);
260 process_list.push_back(first_point);
261
262 grid[grid.point_to_grid(first_point, ranges)] = first_point;
263
264 while (!process_list.empty() && sample_points.size() < count)
265 {
266 // Pop a random element from process_list
267 std::uniform_int_distribution<size_t> dist_idx(0, process_list.size() - 1);
268 size_t idx = dist_idx(gen);
270
271 // Remove from process_list (swap-pop)
272 process_list[idx] = process_list.back();
273 process_list.pop_back();
274
275 for (size_t i = 0; i < new_points_attempts && sample_points.size() < count; ++i)
276 {
279 gen,
280 scale_fn);
281
282 // Check bounds
283 bool in_bounds = true;
284 for (size_t d = 0; d < N; ++d)
285 {
286 if (new_point[d] < ranges[d].first || new_point[d] > ranges[d].second)
287 {
288 in_bounds = false;
289 break;
290 }
291 }
292
293 if (!in_bounds)
294 continue;
295
296 // Check neighbors with scaled distance
298 {
299 sample_points.push_back(new_point);
300 process_list.push_back(new_point);
301 grid[grid.point_to_grid(new_point, ranges)] = new_point;
302 }
303 }
304 }
305
306 return sample_points;
307}
308
339template <typename T, size_t N>
340std::vector<Point<T, N>> poisson_disk_sampling_uniform(
341 size_t count,
342 const std::array<std::pair<T, T>, N> &ranges,
344 std::optional<unsigned int> seed = std::nullopt,
345 size_t new_points_attempts = 30)
346{
347 auto scale_fn = [](const ps::Point<T, N> & /*p*/) -> float { return 1.f; };
348
350 ranges,
352 scale_fn,
353 seed,
355}
356
411template <typename T, size_t N, typename RadiusGen>
413 size_t n_points,
414 const std::array<std::pair<T, T>, N> &axis_ranges,
416 std::optional<unsigned int> seed = std::nullopt,
417 size_t max_attempts = 30)
418{
419 std::mt19937 gen(seed ? *seed : std::random_device{}());
420 std::uniform_real_distribution<T> uniform01(0.0, 1.0);
421
422 std::vector<Point<T, N>> points;
423 std::vector<T> radii;
424 points.reserve(n_points);
425 radii.reserve(n_points);
426
427 size_t attempts = 0;
428 while (points.size() < n_points && attempts < n_points * max_attempts)
429 {
430 attempts++;
431
432 // candidate
434 for (size_t d = 0; d < N; ++d)
435 {
436 auto [a, b] = axis_ranges[d];
437 p[d] = a + uniform01(gen) * (b - a);
438 }
439 T r = radius_gen(); // draw radius
440
441 // check exclusion
442 bool valid = true;
443 for (size_t j = 0; j < points.size(); ++j)
444 {
445 T dist = std::sqrt(distance_squared(p, points[j]));
446 if (dist < r + radii[j])
447 {
448 valid = false;
449 break;
450 }
451 }
452
453 if (valid)
454 {
455 points.push_back(p);
456 radii.push_back(r);
457 }
458 }
459
460 return points;
461}
462
504template <typename T, size_t N>
505std::vector<Point<T, N>> poisson_disk_sampling_power_law(
506 size_t n_points,
507 T dist_min,
508 T dist_max,
509 T alpha,
510 const std::array<std::pair<T, T>, N> &axis_ranges,
511 std::optional<unsigned int> seed = std::nullopt,
512 size_t max_attempts = 30)
513{
514 std::mt19937 gen(seed ? *seed : std::random_device{}());
515 std::uniform_real_distribution<T> uni(0.0, 1.0);
516
517 auto power_law_radius = [&]()
518 {
519 T u = uni(gen);
520 return std::pow(std::pow(dist_min, 1 - alpha) + u * (std::pow(dist_max, 1 - alpha) -
521 std::pow(dist_min, 1 - alpha)),
522 1.0 / (1 - alpha));
523 };
524
528 seed,
529 max_attempts = 30);
530}
531
576template <typename T, size_t N>
577std::vector<Point<T, N>> poisson_disk_sampling_weibull(
578 size_t n_points,
579 T lambda,
580 T k,
581 const std::array<std::pair<T, T>, N> &axis_ranges,
582 std::optional<unsigned int> seed = std::nullopt,
583 size_t max_attempts = 30)
584{
585 std::mt19937 gen(seed ? *seed : std::random_device{}());
586 std::uniform_real_distribution<T> uni(0.0, 1.0);
587
588 // Weibull radius generator via inverse CDF
589 auto weibull_radius = [&]()
590 {
591 T u = uni(gen);
592 return lambda * std::pow(-std::log(1.0 - u), T(1) / k);
593 };
594
598 seed,
600}
601
634template <typename T, size_t N>
635std::vector<Point<T, N>> poisson_disk_sampling_weibull(
636 size_t n_points,
637 T lambda,
638 T k,
639 T dist_min,
640 const std::array<std::pair<T, T>, N> &axis_ranges,
641 std::optional<unsigned int> seed = std::nullopt,
642 size_t max_attempts = 30)
643{
644 std::mt19937 gen(seed ? *seed : std::random_device{}());
645 std::uniform_real_distribution<T> uni(0.0, 1.0);
646
647 auto weibull_radius = [&]()
648 {
649 T u = uni(gen);
650 // Inverse CDF sampling: r = λ * (-ln(1 - u))^(1/k)
651 T r = lambda * std::pow(-std::log(1 - u), T(1) / k);
652 return std::max(r, dist_min);
653 };
654
658 seed,
660}
661
662} // namespace ps
Definition dbscan_clustering.hpp:11
std::vector< Point< T, N > > poisson_disk_sampling_power_law(size_t n_points, T dist_min, T dist_max, T alpha, const std::array< std::pair< T, T >, N > &axis_ranges, std::optional< unsigned int > seed=std::nullopt, size_t max_attempts=30)
Generate N-dimensional points using Poisson disk sampling with a power-law radius distribution.
Definition poisson_disk_sampling.hpp:505
std::vector< Point< T, N > > poisson_disk_sampling_distance_distribution(size_t n_points, const std::array< std::pair< T, T >, N > &axis_ranges, RadiusGen &&radius_gen, std::optional< unsigned int > seed=std::nullopt, size_t max_attempts=30)
Generate random points with a variable-radius Poisson disk sampling.
Definition poisson_disk_sampling.hpp:412
std::vector< Point< T, N > > random(size_t count, const std::array< std::pair< T, T >, N > &axis_ranges, std::optional< unsigned int > seed=std::nullopt)
Generates a specified number of uniformly distributed random points in N-dimensional space.
Definition random.hpp:66
std::vector< Point< T, N > > poisson_disk_sampling_weibull(size_t n_points, T lambda, T k, const std::array< std::pair< T, T >, N > &axis_ranges, std::optional< unsigned int > seed=std::nullopt, size_t max_attempts=30)
Generate N-dimensional points using Poisson disk sampling with a Weibull-distributed radius.
Definition poisson_disk_sampling.hpp:577
std::vector< Point< T, N > > poisson_disk_sampling(size_t count, const std::array< std::pair< T, T >, N > &ranges, T base_min_dist, ScaleFn scale_fn, std::optional< unsigned int > seed=std::nullopt, size_t new_points_attempts=30)
Generate a set of Poisson disk samples in N-dimensional space, possibly with a warped metric.
Definition poisson_disk_sampling.hpp:220
std::vector< Point< T, N > > poisson_disk_sampling_uniform(size_t count, const std::array< std::pair< T, T >, N > &ranges, T base_min_dist, std::optional< unsigned int > seed=std::nullopt, size_t new_points_attempts=30)
Generate uniformly distributed Poisson disk samples in N-dimensional space.
Definition poisson_disk_sampling.hpp:340
Point< T, N > generate_random_point_around(const Point< T, N > &center, T base_min_dist, std::mt19937 &gen, std::function< T(const Point< T, N > &)> scale_fn)
Definition poisson_disk_sampling.hpp:140
T distance_squared(const Point< T, N > &a, const Point< T, N > &b)
Definition point.hpp:226
bool in_neighborhood(const GridND< T, N > &grid, const Point< T, N > &p, T base_min_dist, const std::array< std::pair< T, T >, N > &ranges, ScaleFn scale_fn)
Definition poisson_disk_sampling.hpp:69
Definition poisson_disk_sampling.hpp:14
std::vector< std::optional< Point< T, N > > > cells
Definition poisson_disk_sampling.hpp:15
std::array< size_t, N > grid_size
Definition poisson_disk_sampling.hpp:16
GridND(const std::array< size_t, N > &size, T cell_size_)
Definition poisson_disk_sampling.hpp:19
size_t linear_index(const std::array< size_t, N > &idx) const
Definition poisson_disk_sampling.hpp:45
std::array< size_t, N > point_to_grid(const Point< T, N > &p, const std::array< std::pair< T, T >, N > &ranges) const
Definition poisson_disk_sampling.hpp:29
std::optional< Point< T, N > > operator[](const std::array< size_t, N > &idx) const
Definition poisson_disk_sampling.hpp:57
T cell_size
Definition poisson_disk_sampling.hpp:17
std::optional< Point< T, N > > & operator[](const std::array< size_t, N > &idx)
Definition poisson_disk_sampling.hpp:62
A fixed-size N-dimensional point/vector class.
Definition point.hpp:39