PointSampler library (C++)
Loading...
Searching...
No Matches
metrics.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 <cstddef>
6#include <optional>
7
10
11namespace ps
12{
13
47template <typename T, size_t N>
48std::pair<std::vector<T>, std::vector<T>> angle_distribution_neighbors(
49 const std::vector<Point<T, N>> &points,
51 size_t k_neighbors = 8)
52{
53 if constexpr (N < 2)
54 throw std::runtime_error("Angle distribution requires dimension >= 2");
55
56 size_t num_bins = static_cast<size_t>(std::ceil(M_PI / bin_width));
57 std::vector<T> angles(num_bins);
58 std::vector<T> g_theta(num_bins, T(0));
59
60 // Precompute bin centers
61 for (size_t i = 0; i < num_bins; ++i)
62 angles[i] = (i + T(0.5)) * bin_width;
63
64 // Get nearest neighbors once
66
67 // Loop over all points
68 for (size_t i = 0; i < points.size(); ++i)
69 {
70 const auto &nbrs = neighbors[i];
71 size_t n_nbrs = nbrs.size();
72
73 // Compute angles between all neighbor pairs
74 for (size_t a = 0; a < n_nbrs; ++a)
75 {
76 for (size_t b = a + 1; b < n_nbrs; ++b)
77 {
78 size_t j = nbrs[a];
79 size_t k = nbrs[b];
80
81 // Vectors from center i to j and k
82 std::array<T, N> v1, v2;
83 for (size_t d = 0; d < N; ++d)
84 {
85 v1[d] = points[j][d] - points[i][d];
86 v2[d] = points[k][d] - points[i][d];
87 }
88
89 // Compute angle
90 T dot = T(0), norm1 = T(0), norm2 = T(0);
91 for (size_t d = 0; d < N; ++d)
92 {
93 dot += v1[d] * v2[d];
94 norm1 += v1[d] * v1[d];
95 norm2 += v2[d] * v2[d];
96 }
97
98 if (norm1 > T(0) && norm2 > T(0))
99 {
100 T cos_theta = std::clamp(dot / (std::sqrt(norm1) * std::sqrt(norm2)),
101 T(-1),
102 T(1));
103 T theta = std::acos(cos_theta);
104
105 size_t bin = static_cast<size_t>(theta / bin_width);
106 if (bin < num_bins)
107 g_theta[bin] += T(1);
108 }
109 }
110 }
111 }
112
113 // Normalize histogram
114 T total = std::accumulate(g_theta.begin(), g_theta.end(), T(0));
115 if (total > T(0))
116 for (auto &val : g_theta)
117 val /= total;
118
119 return {angles, g_theta};
120}
121
150template <typename T, size_t N>
151std::vector<T> distance_to_boundary(const std::vector<Point<T, N>> &points,
152 const std::array<std::pair<T, T>, N> &axis_ranges)
153{
154 std::vector<T> distances;
155 distances.reserve(points.size());
156
157 for (const auto &p : points)
158 {
159 // Find min distance to a boundary plane
160 T min_dist = std::numeric_limits<T>::max();
161
162 for (size_t d = 0; d < N; ++d)
163 {
164 T dist_to_min = std::abs(p[d] - axis_ranges[d].first);
165 T dist_to_max = std::abs(axis_ranges[d].second - p[d]);
166
168 }
169
170 distances.push_back(min_dist);
171 }
172
173 return distances;
174}
175
216template <typename T, size_t N>
218{
220 KDTree<T, N> index(N, adaptor);
221 index.buildIndex();
222
223 std::vector<T> distance_sq;
224 distance_sq.reserve(points.size());
225
226 // first neighbor search only
227 const size_t k_neighbors = 1;
228
229 for (size_t i = 0; i < points.size(); ++i)
230 {
231 const auto &p = points[i];
232
233 std::vector<size_t> ret_indexes(k_neighbors + 1);
234 std::vector<T> out_dists_sqr(k_neighbors + 1);
235
236 nanoflann::KNNResultSet<T> result_set(k_neighbors + 1);
237 result_set.init(ret_indexes.data(), out_dists_sqr.data());
238
239 std::array<T, N> query;
240 for (size_t d = 0; d < N; ++d)
241 query[d] = p[d];
242
243 index.findNeighbors(result_set, query.data(), nanoflann::SearchParameters());
244
245 T dist_sq = 0.f;
246 if (ret_indexes.size() > 1)
247 {
248 const auto &q = points[ret_indexes[1]];
249 auto delta = p - q;
250 dist_sq = length_squared(delta) + T(1e-6);
251 }
252
253 distance_sq.push_back(dist_sq);
254 }
255
256 return distance_sq;
257}
258
293template <typename T, size_t N>
294std::vector<T> local_density_knn(const std::vector<Point<T, N>> &points, size_t k = 8)
295{
297
298 std::vector<T> densities(points.size());
299
300 // Compute unit N-ball volume using gamma function
301 T volume_unit_ball = std::pow(M_PI, T(N) / 2) / std::tgamma(T(N) / 2 + 1);
302
303 for (size_t i = 0; i < points.size(); ++i)
304 {
305 // Distance to k-th nearest neighbor
306 const auto &nk_idx = neighbors[i].back();
307 T dist2 = 0;
308 for (size_t d = 0; d < N; ++d)
309 {
310 T diff = points[i][d] - points[nk_idx][d];
311 dist2 += diff * diff;
312 }
313 T r = std::sqrt(dist2);
314
315 // Density = k / (V_N * r^N)
316 T volume = volume_unit_ball * std::pow(r, T(N));
317 densities[i] = k / volume;
318 }
319
320 return densities;
321}
322
358template <typename T, size_t N>
359std::vector<std::vector<size_t>> nearest_neighbors_indices(
360 const std::vector<Point<T, N>> &points,
361 size_t k_neighbors = 8)
362{
364 KDTree<T, N> index(N, adaptor);
365 index.buildIndex();
366
367 std::vector<std::vector<size_t>> all_neighbors;
368 all_neighbors.resize(points.size());
369
370 for (size_t i = 0; i < points.size(); ++i)
371 {
372 const auto &p = points[i];
373
374 std::vector<size_t> ret_indexes(k_neighbors + 1);
375 std::vector<T> out_dists_sqr(k_neighbors + 1);
376
377 nanoflann::KNNResultSet<T> result_set(k_neighbors + 1);
378 result_set.init(ret_indexes.data(), out_dists_sqr.data());
379
380 std::array<T, N> query;
381 for (size_t d = 0; d < N; ++d)
382 query[d] = p[d];
383
384 index.findNeighbors(result_set, query.data(), nanoflann::SearchParameters());
385
386 // Skip self at index 0
387 all_neighbors[i].assign(ret_indexes.begin() + 1, ret_indexes.end());
388 }
389
390 return all_neighbors;
391}
392
433template <typename T, size_t N>
434std::pair<std::vector<T>, std::vector<T>> radial_distribution(
435 const std::vector<Point<T, N>> &points,
436 const std::array<std::pair<T, T>, N> &axis_ranges,
437 T bin_width,
439{
440 size_t num_bins = static_cast<size_t>(std::ceil(max_distance / bin_width));
441 std::vector<T> radii(num_bins);
442 std::vector<T> g(num_bins, T(0));
443
444 // Precompute bin centers
445 for (size_t i = 0; i < num_bins; ++i)
446 radii[i] = (i + T(0.5)) * bin_width;
447
448 // Compute volume of domain
449 T volume = T(1);
450 for (const auto &range : axis_ranges)
451 volume *= (range.second - range.first);
452
453 size_t n_points = points.size();
454 T density = static_cast<T>(n_points) / volume;
455
456 // Histogram of pair distances
457 for (size_t i = 0; i < n_points; ++i)
458 {
459 for (size_t j = i + 1; j < n_points; ++j)
460 {
461 T dist = distance(points[i], points[j]);
462 if (dist < max_distance)
463 {
464 size_t bin = static_cast<size_t>(dist / bin_width);
465 if (bin < num_bins)
466 g[bin] += T(2); // count both i→j and
467 // j→i
468 }
469 }
470 }
471
472 // Normalize
474
475 // Volume of spherical shell between r1 and r2 in N dimensions
476 auto sphere_volume = [](T radius)
477 { return std::pow(M_PI, N / 2.0) / std::tgamma(N / 2.0 + 1.0) * std::pow(radius, N); };
478
479 for (size_t i = 0; i < num_bins; ++i)
480 {
481 T r_inner = i * bin_width;
482 T r_outer = (i + 1) * bin_width;
483
484 // Volume of spherical shell
486 g[i] /= norm_factor * shell_vol;
487 }
488
489 return {radii, g};
490}
491
492} // namespace ps
Definition dbscan_clustering.hpp:11
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
T length_squared(const Point< T, N > &a)
Definition point.hpp:206
std::pair< std::vector< T >, std::vector< T > > radial_distribution(const std::vector< Point< T, N > > &points, const std::array< std::pair< T, T >, N > &axis_ranges, T bin_width, T max_distance)
Compute the normalized radial distribution function g(r).
Definition metrics.hpp:434
std::vector< T > first_neighbor_distance_squared(std::vector< Point< T, N > > &points)
Computes the squared distance to the nearest neighbor for each point.
Definition metrics.hpp:217
std::vector< T > distance_to_boundary(const std::vector< Point< T, N > > &points, const std::array< std::pair< T, T >, N > &axis_ranges)
Compute the distance of each point to the domain boundary.
Definition metrics.hpp:151
T distance(const Point< T, N > &a, const Point< T, N > &b)
Definition point.hpp:231
std::vector< std::vector< size_t > > nearest_neighbors_indices(const std::vector< Point< T, N > > &points, size_t k_neighbors=8)
Finds the nearest neighbors for each point in a set.
Definition metrics.hpp:359
nanoflann::KDTreeSingleIndexAdaptor< nanoflann::L2_Simple_Adaptor< T, PointCloudAdaptor< T, N > >, PointCloudAdaptor< T, N >, N > KDTree
Definition nanoflann_adaptator.hpp:35
std::vector< T > local_density_knn(const std::vector< Point< T, N > > &points, size_t k=8)
Compute local point density based on k-nearest neighbors in N dimensions.
Definition metrics.hpp:294
T dot(const Point< T, N > &a, const Point< T, N > &b)
Definition point.hpp:198
std::pair< std::vector< T >, std::vector< T > > angle_distribution_neighbors(const std::vector< Point< T, N > > &points, T bin_width, size_t k_neighbors=8)
Compute the angular distribution function (ADF) using nearest neighbors.
Definition metrics.hpp:48
Definition nanoflann_adaptator.hpp:13
A fixed-size N-dimensional point/vector class.
Definition point.hpp:39