114 : m_allocator(calculatePoolSize(points.
dim(0))), m_points(points), m_dim(points.
dim(1)) {
116 const std::size_t n = points.
dim(0);
118 std::iota(m_indices.begin(), m_indices.end(), 0);
122 const std::size_t totalNodes = nodeCountFor(n);
123 KDTreeNode *arena = (totalNodes > 0) ? m_allocator.allocate(totalNodes) :
nullptr;
124 m_root = (totalNodes > 0) ? buildAt(0, n, 0, arena, 0, pool) :
nullptr;
125 m_nextNodeId =
static_cast<std::uint32_t
>(totalNodes);
132 m_points_reordered.resize(n * m_dim);
133 const T *src = points.
data();
134 T *dst = m_points_reordered.data();
135 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0}, [&](std::size_t lo, std::size_t hi) {
136 for (std::size_t k = lo; k < hi; ++k) {
137 const T *s = src + (m_indices[k] * m_dim);
138 T *d = dst + (k * m_dim);
139 for (std::size_t j = 0; j < m_dim; ++j) {
154 m_nodeBounds.assign(
static_cast<std::size_t
>(m_nextNodeId) * 2 * m_dim, T{});
155 if (m_root !=
nullptr) {
156 KDTreeNode *arenaNodes = m_root;
157 pool.parallelForBlocks(std::size_t{0}, totalNodes, std::size_t{0},
158 [&](std::size_t lo, std::size_t hi) {
159 for (std::size_t s = lo; s < hi; ++s) {
160 const KDTreeNode &node = arenaNodes[s];
161 if (node.m_left ==
nullptr && node.m_right ==
nullptr) {
162 populateLeafBounds(node);
166 std::vector<const KDTreeNode *> byId(totalNodes,
nullptr);
167 for (std::size_t s = 0; s < totalNodes; ++s) {
168 byId[arenaNodes[s].m_id] = &arenaNodes[s];
170 for (std::size_t
id = 0;
id < totalNodes; ++id) {
171 const KDTreeNode &node = *byId[id];
172 if (node.m_left !=
nullptr || node.m_right !=
nullptr) {
173 unionChildBounds(node);
192 std::int64_t limit = -1)
const {
193 std::vector<std::size_t> indices;
194 const T radius_sq = radius * radius;
196 std::vector<KDTreeNode *> stack;
197 stack.reserve(kDefaultStackReserve);
200 std::vector<T> qbuf(m_dim);
201 for (std::size_t k = 0; k < m_dim; ++k) {
202 qbuf[k] = query_point[k];
204 queryImpl(m_root, qbuf.data(), radius_sq, indices, stack, limit);
222 const std::size_t n = m_points.dim(0);
230 const T radius_sq = radius * radius;
237 if (m_dim >= kBlockQueryDimFloor && m_root !=
nullptr) {
238 blockQuery(radius_sq, minPts, pool, out);
246 const std::size_t totalNodes =
nodeCount();
248 std::vector<std::uint8_t> leafAllCore(totalNodes, 0);
250 std::size_t{0}, totalNodes, std::size_t{0}, [&](std::size_t lo, std::size_t hi) {
251 for (std::size_t nodeSlot = lo; nodeSlot < hi; ++nodeSlot) {
252 const KDTreeNode &node = arenaNodes[nodeSlot];
258 for (std::size_t j = 0; j < m_dim; ++j) {
259 const T ext = bmax[j] - bmin[j];
262 if (diagSq <= radius_sq) {
263 leafAllCore[node.
m_id] = 1;
269 std::vector<std::vector<std::pair<std::int32_t, std::int32_t>>> workerEdges(workers);
271 auto runRange = [&](std::size_t lo, std::size_t hi) {
275 std::vector<KDTreeNode *> stack;
276 stack.reserve(kDefaultStackReserve);
277 const std::size_t adjReserveFloor =
278 std::min(n, (m_dim == kWideAdjReserveDim) ? kWideAdjReserveFloor : kAdjReserveFloor);
279 const T *reordered = m_points_reordered.data();
280 std::vector<std::pair<std::int32_t, std::int32_t>> &edges =
282 const bool useSoa = !m_leafSoa.empty();
283 for (std::size_t k = lo; k < hi; ++k) {
286 const T *qp = reordered + (k * m_dim);
287 const std::size_t rowIdx = m_indices[k];
288 std::vector<std::int32_t> &row = out.
rows[rowIdx];
291 row.reserve(adjReserveFloor);
298 std::size_t bulkDegree = 0;
300 stack.push_back(m_root);
301 while (!stack.empty()) {
304 if (node ==
nullptr) {
312 const std::size_t base = node->
m_index;
313 const std::size_t count = node->
m_dim;
314 if (leafAllCore[node->
m_id] != 0 && row.size() + bulkDegree + count >= minPts &&
317 const auto rep =
static_cast<std::int32_t
>(m_indices[base]);
318 const auto self =
static_cast<std::int32_t
>(rowIdx);
320 edges.emplace_back(self, rep);
324 const T *leafPts = reordered + (base * m_dim);
325 auto emit = [&](std::size_t i)
noexcept {
326 row.push_back(
static_cast<std::int32_t
>(m_indices[base + i]));
329 math::detail::radiusScanSoa(qp, m_leafSoa.data() + (base * m_dim), count, m_dim,
332 math::detail::radiusScan(qp, leafPts, count, m_dim, radius_sq, emit);
336 const std::size_t pivotSlot = node->
m_index;
337 const T *pivotRow = reordered + (pivotSlot * m_dim);
338 if (math::detail::sqEuclideanRowPtr(qp, pivotRow, m_dim) <= radius_sq) {
339 row.push_back(
static_cast<std::int32_t
>(m_indices[pivotSlot]));
341 stack.push_back(node->
m_left);
342 stack.push_back(node->
m_right);
345 (row.size() + bulkDegree >= minPts) ? std::uint8_t{1} : std::uint8_t{0};
356 [&](std::size_t lo, std::size_t hi) { runRange(lo, hi); });
360 for (
const auto &edges : workerEdges) {
392 const std::size_t n = m_points.dim(0);
396 const auto kSz =
static_cast<std::size_t
>(k);
400 auto runRange = [&](std::size_t lo, std::size_t hi) {
403 std::vector<KDTreeNode *> stack;
404 stack.reserve(kDefaultStackReserve);
405 math::detail::TopKNeighbors<T, std::int32_t> topK(kSz);
406 const T *sourceData = m_points.data();
407 std::int32_t *idxOut = indices.data();
408 T *distOut = sqDists.data();
409 for (std::size_t i = lo; i < hi; ++i) {
410 const auto iOriginal =
static_cast<std::int32_t
>(i);
411 const T *qp = sourceData + (i * m_dim);
413 knnQueryImpl(m_root, qp, iOriginal, topK, stack);
414 topK.drainAscending(distOut + (i * kSz), idxOut + (i * kSz));
420 std::size_t{0}, n, std::size_t{0},
421 [&](std::size_t lo, std::size_t hi) { runRange(lo, hi); });
425 return {std::move(indices), std::move(sqDists)};
440 [[nodiscard]] std::pair<std::span<const T>, std::span<const T>>
442 assert(node !=
nullptr &&
"KDTree::nodeBounds on null node");
443 const std::size_t base =
static_cast<std::size_t
>(node->m_id) * 2 * m_dim;
444 const T *bounds = m_nodeBounds.data() + base;
445 return {std::span<const T>(bounds, m_dim), std::span<const T>(bounds + m_dim, m_dim)};
459 return {m_indices.data(), m_indices.size()};
474 return {m_points_reordered.data(), m_points_reordered.size()};
484 return static_cast<std::size_t
>(m_nextNodeId);
491 [[nodiscard]] std::size_t
dim() const noexcept {
return m_dim; }
501 if (m_allocator.isDeallocSupported()) {
502 doRecDealloc(m_root);
517 static size_t calculatePoolSize(
size_t numPoints) {
518 if (numPoints == 0) {
535 static void doRecDealloc(KDTreeNode *node) {
536 if (node ==
nullptr) {
540 doRecDealloc(node->m_left);
541 doRecDealloc(node->m_right);
549 static constexpr std::size_t kParallelBuildFloor = 512;
558 static std::size_t nodeCountFor(std::size_t m)
noexcept {
565 const std::size_t mLeft = (m - 1) / 2;
566 return 1 + nodeCountFor(mLeft) + nodeCountFor(m - 1 - mLeft);
589 KDTreeNode *buildAt(std::size_t start, std::size_t end, std::size_t depth, KDTreeNode *arena,
590 std::uint32_t idBase, math::Pool pool) {
596 if (end - start <= LeafSize) {
597 *arena = {.m_index = start,
598 .m_dim = end - start,
606 const std::size_t
dim = depth % m_points.dim(1);
607 const std::size_t median = start + (((end - start) - 1) / 2);
609 using diff_t = std::vector<std::size_t>::difference_type;
610 std::nth_element(m_indices.begin() +
static_cast<diff_t
>(start),
611 m_indices.begin() +
static_cast<diff_t
>(median),
612 m_indices.begin() +
static_cast<diff_t
>(end),
613 [
this,
dim](std::size_t lhs, std::size_t rhs) {
614 return m_points[lhs][dim] < m_points[rhs][dim];
617 const std::size_t nLeft = nodeCountFor(median - start);
618 const std::size_t nRight = nodeCountFor(end - median - 1);
619 KDTreeNode *left =
nullptr;
620 KDTreeNode *right =
nullptr;
621 auto buildLeft = [&] { left = buildAt(start, median, depth + 1, arena + 1, idBase, pool); };
622 auto buildRight = [&] {
623 right = buildAt(median + 1, end, depth + 1, arena + 1 + nLeft,
624 idBase +
static_cast<std::uint32_t
>(nLeft), pool);
626 if (end - start > kParallelBuildFloor && pool.pool !=
nullptr) {
627 pool.forkJoin2(buildLeft, buildRight);
633 *arena = {.m_index = median,
637 .m_id = idBase +
static_cast<std::uint32_t
>(nLeft + nRight)};
644 static constexpr std::size_t kBlockQueryDimFloor = 4;
659 void blockQuery(T radius_sq, std::size_t minPts, math::Pool pool,
660 index::CoreAdjacency &out)
const {
661 const std::size_t n = m_points.dim(0);
662 const std::size_t totalNodes =
nodeCount();
663 const KDTreeNode *arenaNodes = m_root;
664 std::vector<const KDTreeNode *> leaves;
665 std::vector<const KDTreeNode *> pivots;
666 leaves.reserve(totalNodes);
667 for (std::size_t nodeSlot = 0; nodeSlot < totalNodes; ++nodeSlot) {
668 const KDTreeNode &node = arenaNodes[nodeSlot];
669 if (node.m_left ==
nullptr && node.m_right ==
nullptr) {
670 leaves.push_back(&node);
672 pivots.push_back(&node);
676 const T *reordered = m_points_reordered.data();
677 const std::size_t adjReserveFloor =
678 std::min(n, (m_dim == kWideAdjReserveDim) ? kWideAdjReserveFloor : kAdjReserveFloor);
679 const bool useSoa = !m_leafSoa.empty();
681 auto runLeafRange = [&](std::size_t lo, std::size_t hi) {
682 std::vector<const KDTreeNode *> stack;
683 stack.reserve(kDefaultStackReserve);
684 for (std::size_t li = lo; li < hi; ++li) {
685 const KDTreeNode &source = *leaves[li];
686 const std::size_t base = source.m_index;
687 const std::size_t count = source.m_dim;
688 const auto [srcMin, srcMax] =
nodeBounds(&source);
689 for (std::size_t i = 0; i < count; ++i) {
690 out.rows[m_indices[base + i]].reserve(adjReserveFloor);
694 stack.push_back(m_root);
695 while (!stack.empty()) {
696 const KDTreeNode *node = stack.back();
698 if (node ==
nullptr) {
701 const auto [nodeMin, nodeMax] =
nodeBounds(node);
705 if (node->m_left ==
nullptr && node->m_right ==
nullptr) {
706 const std::size_t targetBase = node->m_index;
707 const std::size_t targetCount = node->m_dim;
708 const T *targetPts = reordered + (targetBase * m_dim);
709 const T *targetSoa = useSoa ? m_leafSoa.data() + (targetBase * m_dim) : nullptr;
713 for (; i + 2 <= count; i += 2) {
714 std::vector<std::int32_t> &row0 = out.rows[m_indices[base + i]];
715 std::vector<std::int32_t> &row1 = out.rows[m_indices[base + i + 1]];
716 math::detail::radiusScanSoaPair(
717 reordered + ((base + i) * m_dim), reordered + ((base + i + 1) * m_dim),
718 targetSoa, targetCount, m_dim, radius_sq,
719 [&](std::size_t j)
noexcept {
720 row0.push_back(
static_cast<std::int32_t
>(m_indices[targetBase + j]));
722 [&](std::size_t j)
noexcept {
723 row1.push_back(
static_cast<std::int32_t
>(m_indices[targetBase + j]));
727 for (; i < count; ++i) {
728 const T *qp = reordered + ((base + i) * m_dim);
729 std::vector<std::int32_t> &row = out.rows[m_indices[base + i]];
730 auto emit = [&](std::size_t j)
noexcept {
731 row.push_back(
static_cast<std::int32_t
>(m_indices[targetBase + j]));
734 math::detail::radiusScanSoa(qp, targetSoa, targetCount, m_dim, radius_sq, emit);
736 math::detail::radiusScan(qp, targetPts, targetCount, m_dim, radius_sq, emit);
741 const T *pivotRow = reordered + (node->m_index * m_dim);
742 const auto pivotIdx =
static_cast<std::int32_t
>(m_indices[node->m_index]);
743 for (std::size_t i = 0; i < count; ++i) {
744 const T *qp = reordered + ((base + i) * m_dim);
745 if (math::detail::sqEuclideanRowPtr(qp, pivotRow, m_dim) <= radius_sq) {
746 out.rows[m_indices[base + i]].push_back(pivotIdx);
749 stack.push_back(node->m_left);
750 stack.push_back(node->m_right);
753 for (std::size_t i = 0; i < count; ++i) {
754 const std::size_t rowIdx = m_indices[base + i];
756 (out.rows[rowIdx].size() >= minPts) ? std::uint8_t{1} : std::uint8_t{0};
761 auto runPivotRange = [&](std::size_t lo, std::size_t hi) {
762 std::vector<KDTreeNode *> stack;
763 stack.reserve(kDefaultStackReserve);
764 for (std::size_t pi = lo; pi < hi; ++pi) {
765 const std::size_t slot = pivots[pi]->m_index;
766 const T *qp = reordered + (slot * m_dim);
767 const std::size_t rowIdx = m_indices[slot];
768 std::vector<std::int32_t> &row = out.rows[rowIdx];
769 row.reserve(adjReserveFloor);
770 queryImpl(m_root, qp, radius_sq, row, stack, -1);
771 out.isCore[rowIdx] = (row.size() >= minPts) ? std::uint8_t{1} : std::uint8_t{0};
775 if (pool.shouldParallelize(n, 4, 2)) {
776 pool.parallelForBlocks<citor::HintsDefaults>(
777 std::size_t{0}, leaves.size(), pool.stealBlocks(leaves.size(), 1), runLeafRange);
778 pool.parallelForBlocks(std::size_t{0}, pivots.size(), std::size_t{0}, runPivotRange);
780 runLeafRange(0, leaves.size());
781 runPivotRange(0, pivots.size());
806 template <
class OutIdx>
807 void queryImpl(KDTreeNode *
root,
const T *qp, T radius_sq, std::vector<OutIdx> &indices,
808 std::vector<KDTreeNode *> &stack, std::int64_t limit = -1)
const {
809 if (
root ==
nullptr) {
814 stack.push_back(
root);
816 const T *reorderedBase = m_points_reordered.data();
818 while (!stack.empty()) {
819 const KDTreeNode *node = stack.back();
822 if (node ==
nullptr) {
826 if (limit != -1 && indices.size() ==
static_cast<std::size_t
>(limit)) {
835 if (node->m_left ==
nullptr && node->m_right ==
nullptr) {
836 const std::size_t base = node->m_index;
837 const std::size_t count = node->m_dim;
838 const T *leafPts = reorderedBase + (base * m_dim);
842 const bool useSoa = !m_leafSoa.empty();
843 const T *leafSoa = useSoa ? m_leafSoa.data() + (base * m_dim) : nullptr;
844 const bool isBounded = (limit != -1);
848 auto emit = [&](std::size_t i)
noexcept {
849 indices.push_back(
static_cast<OutIdx
>(m_indices[base + i]));
852 math::detail::radiusScanSoa(qp, leafSoa, count, m_dim, radius_sq, emit);
854 math::detail::radiusScan(qp, leafPts, count, m_dim, radius_sq, emit);
858 const auto cap =
static_cast<std::size_t
>(limit);
859 auto emit = [&](std::size_t i)
noexcept {
860 if (indices.size() >= cap) {
863 indices.push_back(
static_cast<OutIdx
>(m_indices[base + i]));
866 math::detail::radiusScanSoa(qp, leafSoa, count, m_dim, radius_sq, emit);
868 math::detail::radiusScan(qp, leafPts, count, m_dim, radius_sq, emit);
870 if (indices.size() >= cap) {
880 const std::size_t pivotSlot = node->m_index;
881 const std::size_t splitDim = node->m_dim;
882 const T *pivotRow = reorderedBase + (pivotSlot * m_dim);
883 const T dist_sq = math::detail::sqEuclideanRowPtr(qp, pivotRow, m_dim);
884 if (dist_sq <= radius_sq) {
885 indices.push_back(
static_cast<OutIdx
>(m_indices[pivotSlot]));
888 const T pivotCoord = pivotRow[splitDim];
889 const T diff = qp[splitDim] - pivotCoord;
895 const bool farWithinRadius = diff * diff <= radius_sq;
896 if (node->m_left !=
nullptr && (diff < 0 || farWithinRadius)) {
897 stack.push_back(node->m_left);
899 if (node->m_right !=
nullptr && (diff >= 0 || farWithinRadius)) {
900 stack.push_back(node->m_right);
922 void knnQueryImpl(KDTreeNode *
root,
const T *qp, std::int32_t selfIndex,
923 math::detail::TopKNeighbors<T, std::int32_t> &topK,
924 std::vector<KDTreeNode *> &stack)
const {
925 if (
root ==
nullptr) {
930 stack.push_back(
root);
932 const T *reorderedBase = m_points_reordered.data();
934 while (!stack.empty()) {
935 const KDTreeNode *node = stack.back();
938 if (node ==
nullptr) {
944 const T bound = topK.boundKey();
951 if (bound != std::numeric_limits<T>::max()) {
954 if (gapSq >= bound) {
959 if (node->m_left ==
nullptr && node->m_right ==
nullptr) {
964 const std::size_t base = node->m_index;
965 const std::size_t count = node->m_dim;
966 const T *leafPts = reorderedBase + (base * m_dim);
967 std::array<T, LeafSize> dsqBuf{};
968 math::detail::sqDistancesAosBlock<T>(qp, leafPts, count, m_dim, dsqBuf.data());
976 T minDsq = dsqBuf[0];
977 for (std::size_t i = 1; i < count; ++i) {
978 if (dsqBuf[i] < minDsq) {
982 if (minDsq >= bound) {
986 for (std::size_t i = 0; i < count; ++i) {
987 const auto pointIdx =
static_cast<std::int32_t
>(m_indices[base + i]);
988 if (pointIdx == selfIndex) {
991 topK.push(dsqBuf[i], pointIdx);
1001 const std::size_t pivotSlot = node->m_index;
1002 const std::size_t splitDim = node->m_dim;
1003 const T *pivotRow = reorderedBase + (pivotSlot * m_dim);
1004 const auto pivotIdx =
static_cast<std::int32_t
>(m_indices[pivotSlot]);
1005 const T pivotCoord = pivotRow[splitDim];
1006 const T diff = qp[splitDim] - pivotCoord;
1007 const T diffSq = diff * diff;
1008 if (pivotIdx != selfIndex && diffSq <= bound) {
1009 const T dist_sq = math::detail::sqEuclideanRowPtr(qp, pivotRow, m_dim);
1010 topK.push(dist_sq, pivotIdx);
1014 if (diffSq <= bound && node->m_right !=
nullptr) {
1015 stack.push_back(node->m_right);
1017 if (node->m_left !=
nullptr) {
1018 stack.push_back(node->m_left);
1021 if (diffSq <= bound && node->m_left !=
nullptr) {
1022 stack.push_back(node->m_left);
1024 if (node->m_right !=
nullptr) {
1025 stack.push_back(node->m_right);
1033 void ensureLeafSoa(math::Pool pool = {})
const {
1034 if (m_dim < kSoaLeafDimFloor || m_root ==
nullptr || !m_leafSoa.empty()) {
1037 m_leafSoa.resize(m_points_reordered.size());
1039 const KDTreeNode *arenaNodes = m_root;
1040 pool.parallelForBlocks(std::size_t{0},
nodeCount(), std::size_t{0},
1041 [&](std::size_t lo, std::size_t hi) {
1042 for (std::size_t nodeSlot = lo; nodeSlot < hi; ++nodeSlot) {
1043 const KDTreeNode &node = arenaNodes[nodeSlot];
1044 if (node.m_left ==
nullptr && node.m_right ==
nullptr) {
1045 transposeLeafSoa(node);
1054 void transposeLeafSoa(
const KDTreeNode &leaf)
const noexcept {
1055 const std::size_t base = leaf.m_index;
1056 const std::size_t count = leaf.m_dim;
1057 const T *aos = m_points_reordered.data() + (base * m_dim);
1058 T *soa = m_leafSoa.data() + (base * m_dim);
1059 for (std::size_t p = 0; p < count; ++p) {
1060 for (std::size_t f = 0; f < m_dim; ++f) {
1061 soa[(f * count) + p] = aos[(p * m_dim) + f];
1067 void populateLeafBounds(
const KDTreeNode &node)
noexcept {
1068 T *minOut = m_nodeBounds.data() + (
static_cast<std::size_t
>(node.m_id) * 2 * m_dim);
1069 T *maxOut = minOut + m_dim;
1070 const std::size_t base = node.m_index;
1071 const std::size_t count = node.m_dim;
1072 const T *leafPts = m_points_reordered.data() + (base * m_dim);
1073 for (std::size_t j = 0; j < m_dim; ++j) {
1074 minOut[j] = leafPts[j];
1075 maxOut[j] = leafPts[j];
1077 for (std::size_t i = 1; i < count; ++i) {
1078 const T *row = leafPts + (i * m_dim);
1079 for (std::size_t j = 0; j < m_dim; ++j) {
1080 if (row[j] < minOut[j]) {
1083 if (row[j] > maxOut[j]) {
1093 void unionChildBounds(
const KDTreeNode &node)
noexcept {
1094 T *minOut = m_nodeBounds.data() + (
static_cast<std::size_t
>(node.m_id) * 2 * m_dim);
1095 T *maxOut = minOut + m_dim;
1097 const KDTreeNode *
const seed = (node.m_left !=
nullptr) ? node.m_left : node.m_right;
1098 const T *seedMin = m_nodeBounds.data() + (
static_cast<std::size_t
>(seed->m_id) * 2 * m_dim);
1099 const T *seedMax = seedMin + m_dim;
1100 for (std::size_t j = 0; j < m_dim; ++j) {
1101 minOut[j] = seedMin[j];
1102 maxOut[j] = seedMax[j];
1105 if (node.m_left !=
nullptr && node.m_right !=
nullptr) {
1107 m_nodeBounds.data() + (
static_cast<std::size_t
>(node.m_right->m_id) * 2 * m_dim);
1108 const T *otherMax = otherMin + m_dim;
1109 for (std::size_t j = 0; j < m_dim; ++j) {
1110 if (otherMin[j] < minOut[j]) {
1111 minOut[j] = otherMin[j];
1113 if (otherMax[j] > maxOut[j]) {
1114 maxOut[j] = otherMax[j];
1120 const std::size_t pivotSlot = node.m_index;
1121 const T *pivotRow = m_points_reordered.data() + (pivotSlot * m_dim);
1122 for (std::size_t j = 0; j < m_dim; ++j) {
1123 if (pivotRow[j] < minOut[j]) {
1124 minOut[j] = pivotRow[j];
1126 if (pivotRow[j] > maxOut[j]) {
1127 maxOut[j] = pivotRow[j];
1135 static constexpr std::size_t kDefaultStackReserve = 64;
1139 static constexpr std::size_t kAdjReserveFloor = 8;
1140 static constexpr std::size_t kWideAdjReserveDim = 8;
1141 static constexpr std::size_t kWideAdjReserveFloor = 128;
1146 static constexpr std::size_t kSoaLeafDimFloor = 2;
1150 KDTreeNode *m_root =
nullptr;
1151 const NDArray<T, 2> &m_points;
1152 std::size_t m_dim = 0;
1153 std::vector<std::size_t> m_indices;
1155 std::vector<T> m_points_reordered;
1161 mutable std::vector<T> m_leafSoa;
1164 std::uint32_t m_nextNodeId = 0;
1168 std::vector<T> m_nodeBounds;