Clustering
C++20 header-only: DBSCAN, HDBSCAN, k-means.
Loading...
Searching...
No Matches
brute_force_pairwise.h
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <type_traits>
7#include <utility>
8#include <vector>
9
13#include "clustering/ndarray.h"
14
15namespace clustering {
16
31template <class T> class BruteForcePairwise {
32public:
33 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
34 "BruteForcePairwise<T> requires T to be float or double");
35
41 explicit BruteForcePairwise(const NDArray<T, 2> &points) noexcept : m_points(points) {}
42
57 [[nodiscard]] index::CoreAdjacency query(T radius, std::size_t minPts, math::Pool pool) const {
58 const std::size_t n = m_points.dim(0);
60 out.rows.resize(n);
61 out.isCore.assign(n, 0);
62 if (n == 0) {
63 return out;
64 }
65 std::vector<std::vector<std::int32_t>> &adj = out.rows;
66
67 const std::size_t d = m_points.dim(1);
68 std::size_t adjReserveFloor = 16;
69 if (d == 32) {
70 adjReserveFloor = 24;
71 } else if (d == 64) {
72 adjReserveFloor = 20;
73 }
74 // Reserve a small floor per row so the first push_backs do not trigger the vector-doubling
75 // reallocation cascade that otherwise dominates adjacency construction on dense fixtures.
76 // Fanning the reserves out matters at high worker counts: the row allocations are a
77 // serial malloc train that idles every worker before the sweep starts, and the allocator's
78 // per-thread arenas let the fan-out scale.
79 const auto reserveRows = [&](std::size_t lo, std::size_t hi) {
80 for (std::size_t i = lo; i < hi; ++i) {
81 adj[i].reserve(adjReserveFloor);
82 }
83 };
84 const bool fanOut = pool.shouldParallelize(n, 256, 2);
85 if (fanOut) {
86 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0}, reserveRows);
87 } else {
88 reserveRows(0, n);
89 }
90
91 const T radiusSq = radius * radius;
92 // Symmetric eps-neighbour graph: the kernel emits each unique upper-triangular cell once
93 // with `row <= col`. The kernel-side emit only touches `adj[row]` so workers writing
94 // disjoint row chunks remain race-free.
95 auto emit = [&adj](std::size_t row, std::size_t col) {
96 adj[row].push_back(static_cast<std::int32_t>(col));
97 };
98 math::pairwiseSqEuclideanThresholdedSymmetric(m_points, radiusSq, pool, emit);
99
100 // Mirror degrees without mirror edges: workers accumulate `j < i` neighbour counts in
101 // private histograms that stay cache-resident, then a row-partitioned reduction folds them.
102 // Scattering the mirrored edges themselves would be a latency-bound write per edge into a
103 // random destination row; the degree is all the core verdict needs.
104 const std::size_t workers = pool.workerCount();
105 std::vector<std::vector<std::uint32_t>> workerCounts(workers);
106 const auto countRange = [&](std::size_t lo, std::size_t hi) {
107 std::vector<std::uint32_t> &counts = workerCounts[math::Pool::workerIndex()];
108 if (counts.empty()) {
109 counts.assign(n, 0);
110 }
111 for (std::size_t i = lo; i < hi; ++i) {
112 for (const std::int32_t neighbor : adj[i]) {
113 const auto j = static_cast<std::size_t>(neighbor);
114 if (j > i) {
115 ++counts[j];
116 }
117 }
118 }
119 };
120 std::vector<std::uint32_t> mirrorDeg(n, 0);
121 const auto reduceRange = [&](std::size_t lo, std::size_t hi) {
122 for (const std::vector<std::uint32_t> &counts : workerCounts) {
123 if (counts.empty()) {
124 continue;
125 }
126 for (std::size_t i = lo; i < hi; ++i) {
127 mirrorDeg[i] += counts[i];
128 }
129 }
130 };
131 const auto flagRange = [&](std::size_t lo, std::size_t hi) {
132 for (std::size_t i = lo; i < hi; ++i) {
133 out.isCore[i] =
134 (adj[i].size() + mirrorDeg[i] >= minPts) ? std::uint8_t{1} : std::uint8_t{0};
135 }
136 };
137 if (fanOut) {
138 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0}, countRange);
139 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0}, reduceRange);
140 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0}, flagRange);
141 } else {
142 countRange(0, n);
143 reduceRange(0, n);
144 flagRange(0, n);
145 }
146
147 // Complete the non-core rows only. Workers collect the surviving `(dest, src)` mirrors
148 // into private lists; the survivor set is a vanishing fraction of the edge set, so the
149 // ordered serial apply costs a handful of pushes and keeps row contents deterministic.
150 std::vector<std::vector<std::pair<std::int32_t, std::int32_t>>> workerMirrors(workers);
151 const auto collectRange = [&](std::size_t lo, std::size_t hi) {
152 std::vector<std::pair<std::int32_t, std::int32_t>> &mirrors =
153 workerMirrors[math::Pool::workerIndex()];
154 for (std::size_t i = lo; i < hi; ++i) {
155 for (const std::int32_t neighbor : adj[i]) {
156 const auto j = static_cast<std::size_t>(neighbor);
157 if (j > i && out.isCore[j] == 0) {
158 mirrors.emplace_back(static_cast<std::int32_t>(j), static_cast<std::int32_t>(i));
159 }
160 }
161 }
162 };
163 if (fanOut) {
164 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0}, collectRange);
165 } else {
166 collectRange(0, n);
167 }
168 std::vector<std::pair<std::int32_t, std::int32_t>> mirrors;
169 for (const auto &local : workerMirrors) {
170 mirrors.insert(mirrors.end(), local.begin(), local.end());
171 }
172 std::sort(mirrors.begin(), mirrors.end());
173 for (const auto &[dest, src] : mirrors) {
174 adj[static_cast<std::size_t>(dest)].push_back(src);
175 }
176 return out;
177 }
178
179private:
180 const NDArray<T, 2> &m_points;
181};
182
183} // namespace clustering
BruteForcePairwise(const NDArray< T, 2 > &points) noexcept
Constructs the backend over a borrowed point matrix.
index::CoreAdjacency query(T radius, std::size_t minPts, math::Pool pool) const
Returns the core-aware radius adjacency over the indexed point cloud.
Represents a multidimensional array (NDArray) of a fixed number of dimensions N and element type T.
Definition ndarray.h:136
void pairwiseSqEuclideanThresholdedSymmetric(const NDArray< T, 2, LX > &X, T radiusSq, Pool pool, Emit &&emit)
Symmetric variant of pairwiseSqEuclideanThresholded for the X == Y case.
Definition pairwise.h:626
Radius-neighborhood adjacency with per-point core flags.
Definition range_query.h:30
std::vector< std::vector< std::int32_t > > rows
Per-point neighbour lists.
Definition range_query.h:31
std::vector< std::uint8_t > isCore
Per-point core flag from the full degree.
Definition range_query.h:32
Thin compile-time-templated wrapper around the underlying OwnedPool.
Definition thread.h:109
static std::size_t workerIndex() noexcept
Stable index of the calling worker thread within the owning pool.
Definition thread.h:131
std::size_t workerCount() const noexcept
Number of worker threads available, or 1 in serial mode.
Definition thread.h:118
void parallelForBlocks(std::size_t first, std::size_t last, std::size_t numBlocks, Body body)
Run body in parallel over [first, last) partitioned into numBlocks blocks.
Definition thread.h:239
bool shouldParallelize(std::size_t totalWork, std::size_t minChunk, std::size_t minTasksPerWorker=2) const noexcept
Decide whether totalWork warrants parallel dispatch.
Definition thread.h:147