43 static constexpr std::int32_t
NOISY = -1;
54 explicit DBSCAN(T eps, std::size_t minPts, std::size_t nJobs = 0)
55 : m_eps(eps), m_minPts(minPts), m_nJobs(
math::clampedJobCount(nJobs)), m_labels({0}) {
82 const std::size_t n = X.
dim(0);
97 const std::size_t poolJobs = effectiveWorkerCount(n, X.
dim(1), m_nJobs);
104 QueryModel queryModel = [&] {
105 if constexpr (std::is_constructible_v<QueryModel, const NDArray<T, 2> &,
math::Pool>) {
106 return QueryModel(X, pool);
108 return QueryModel(X);
114 const auto [adj, isCore, extraEdges] = queryModel.query(m_eps, m_minPts, pool);
118 const std::vector<std::uint32_t> componentRoots =
119 buildComponentRoots(adj, isCore, extraEdges, pool);
124 std::int32_t *
labels = m_labels.data();
126 for (std::size_t i = 0; i < n; ++i) {
127 if (isCore[i] == 0) {
130 const auto root = componentRoots[i];
131 std::int32_t &slot = rootCluster[root];
133 slot =
static_cast<std::int32_t
>(m_clusterId);
139 assignBorderAndNoise(adj, isCore);
146 [[nodiscard]] std::size_t
nClusters() const noexcept {
return m_clusterId; }
155 void ensureLabelsShape(std::size_t n) {
156 if (m_labels.
dim(0) != n) {
161 static constexpr std::size_t kSmall2dMaxParallelN = 25'000;
162 static constexpr std::size_t kSmall2dWorkerCap = 8;
164 [[nodiscard]]
static std::size_t effectiveWorkerCount(std::size_t n, std::size_t d,
165 std::size_t requested)
noexcept {
166 if (d <= 2 && n <= kSmall2dMaxParallelN && requested > kSmall2dWorkerCap) {
167 return kSmall2dWorkerCap;
189 static std::vector<std::uint32_t> buildComponentRoots(
190 const std::vector<std::vector<std::int32_t>> &adj,
const std::vector<std::uint8_t> &isCore,
191 const std::vector<std::pair<std::int32_t, std::int32_t>> &extraEdges, math::Pool pool) {
192 const std::size_t n = adj.size();
193 std::vector<std::uint32_t> roots(n);
195 const auto uniteRange = [&](
auto &dsu, std::size_t lo, std::size_t hi) {
196 for (std::size_t i = lo; i < hi; ++i) {
197 if (isCore[i] == 0) {
200 const auto iu =
static_cast<std::uint32_t
>(i);
203 for (
const std::int32_t neighbor : adj[i]) {
204 const auto j =
static_cast<std::size_t
>(neighbor);
205 if (j != i && isCore[j] != 0) {
206 dsu.unite(iu,
static_cast<std::uint32_t
>(j));
213 const auto uniteExtraRange = [&](
auto &dsu, std::size_t lo, std::size_t hi) {
214 for (std::size_t e = lo; e < hi; ++e) {
215 dsu.unite(
static_cast<std::uint32_t
>(extraEdges[e].first),
216 static_cast<std::uint32_t
>(extraEdges[e].second));
220 const std::size_t workers = pool.workerCount();
221 if (pool.pool ==
nullptr || workers <= 1) {
222 UnionFind<std::uint32_t> components(n);
223 uniteRange(components, 0, n);
224 uniteExtraRange(components, 0, extraEdges.size());
225 for (std::size_t i = 0; i < n; ++i) {
226 roots[i] = components.find(
static_cast<std::uint32_t
>(i));
233 AtomicUnionFind<std::uint32_t> components(n);
234 pool.parallelForBlocks(std::size_t{0}, n, workers * 4,
235 [&](std::size_t lo, std::size_t hi) { uniteRange(components, lo, hi); });
236 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0}, [&](std::size_t lo, std::size_t hi) {
237 for (std::size_t i = lo; i < hi; ++i) {
238 roots[i] = components.find(
static_cast<std::uint32_t
>(i));
255 void assignBorderAndNoise(
const std::vector<std::vector<std::int32_t>> &adj,
256 const std::vector<std::uint8_t> &isCore) {
257 const std::size_t n = adj.size();
258 std::int32_t *
labels = m_labels.data();
259 for (std::size_t p = 0; p < n; ++p) {
260 if (isCore[p] != 0) {
263 std::int32_t best =
NOISY;
264 for (
const std::int32_t neighbor : adj[p]) {
265 const auto q =
static_cast<std::size_t
>(neighbor);
266 if (isCore[q] == 0) {
269 const std::int32_t cluster =
labels[q];
270 if (best ==
NOISY || cluster < best) {
279 std::size_t m_minPts;
281 std::size_t m_clusterId = 0;
285 NDArray<std::int32_t, 1> m_labels;