![]() |
|
|||
File indexing completed on 2025-08-06 08:10:18
0001 // This file is part of the Acts project. 0002 // 0003 // Copyright (C) 2021 CERN for the benefit of the Acts project 0004 // 0005 // This Source Code Form is subject to the terms of the Mozilla Public 0006 // License, v. 2.0. If a copy of the MPL was not distributed with this 0007 // file, You can obtain one at http://mozilla.org/MPL/2.0/. 0008 0009 #pragma once 0010 0011 #include "Acts/Utilities/RangeXD.hpp" 0012 0013 #include <algorithm> 0014 #include <array> 0015 #include <cmath> 0016 #include <functional> 0017 #include <memory> 0018 #include <string> 0019 #include <vector> 0020 0021 namespace Acts { 0022 /// @brief A general k-d tree with fast range search. 0023 /// 0024 /// This is a generalized k-d tree, with a configurable number of dimension, 0025 /// scalar type, content type, index type, vector type, and leaf size. This 0026 /// class is purposefully generalized to support a wide range of use cases. 0027 /// 0028 /// A k-d tree is, in essence, a k-dimensional binary search tree. Each internal 0029 /// node splits the content of the tree in half, with the pivot point being an 0030 /// orthogonal hyperplane in one of the k dimensions. This allows us to 0031 /// efficiently look up points within certain k-dimensional ranges. 0032 /// 0033 /// This particular class is mostly a wrapper class around an underlying node 0034 /// class which does all the actual work. 0035 /// 0036 /// @note This type is completely immutable after construction. 0037 /// 0038 /// @tparam Dims The number of dimensions. 0039 /// @tparam Type The type of value contained in the tree. 0040 /// @tparam Scalar The scalar type used to construct position vectors. 0041 /// @tparam Vector The general vector type used to construct coordinates. 0042 /// @tparam LeafSize The maximum number of elements stored in a leaf node. 0043 template <std::size_t Dims, typename Type, typename Scalar = double, 0044 template <typename, std::size_t> typename Vector = std::array, 0045 std::size_t LeafSize = 4> 0046 class KDTree { 0047 public: 0048 /// @brief The type of value contained in this k-d tree. 0049 using value_t = Type; 0050 0051 /// @brief The type describing a multi-dimensional orthogonal range. 0052 using range_t = RangeXD<Dims, Scalar>; 0053 0054 /// @brief The type of coordinates for points. 0055 using coordinate_t = Vector<Scalar, Dims>; 0056 0057 /// @brief The type of coordinate-value pairs. 0058 using pair_t = std::pair<coordinate_t, Type>; 0059 0060 /// @brief The type of a vector of coordinate-value pairs. 0061 using vector_t = std::vector<pair_t>; 0062 0063 /// @brief The type of iterators in our vectors. 0064 using iterator_t = typename vector_t::iterator; 0065 0066 using const_iterator_t = typename vector_t::const_iterator; 0067 0068 // We do not need an empty constructor - this is never useful. 0069 KDTree() = delete; 0070 0071 /// @brief Construct a k-d tree from a vector of position-value pairs. 0072 /// 0073 /// This constructor takes an r-value reference to a vector of position-value 0074 /// pairs and constructs a k-d tree from those pairs. 0075 /// 0076 /// @param d The vector of position-value pairs to construct the k-d tree 0077 /// from. 0078 KDTree(vector_t &&d) : m_elems(d) { 0079 // To start out, we need to check whether we need to construct a leaf node 0080 // or an internal node. We create a leaf only if we have at most as many 0081 // elements as the number of elements that can fit into a leaf node. 0082 // Hopefully most invocations of this constructor will have more than a few 0083 // elements! 0084 // 0085 // One interesting thing to note is that all of the nodes in the k-d tree 0086 // have a range in the element vector of the outermost node. They simply 0087 // make in-place changes to this array, and they hold no memory of their 0088 // own. 0089 m_root = std::make_unique<KDTreeNode>(m_elems.begin(), m_elems.end(), 0090 m_elems.size() > LeafSize 0091 ? KDTreeNode::NodeType::Internal 0092 : KDTreeNode::NodeType::Leaf, 0093 0UL); 0094 } 0095 0096 /// @brief Perform an orthogonal range search within the k-d tree. 0097 /// 0098 /// A range search operation is one that takes a k-d tree and an orthogonal 0099 /// range, and returns all values associated with coordinates in the k-d tree 0100 /// that lie within the orthogonal range. k-d trees can do this operation 0101 /// quickly. 0102 /// 0103 /// @param r The range to search for. 0104 /// 0105 /// @return The vector of all values that lie within the given range. 0106 std::vector<Type> rangeSearch(const range_t &r) const { 0107 std::vector<Type> out; 0108 0109 rangeSearch(r, out); 0110 0111 return out; 0112 } 0113 0114 /// @brief Perform an orthogonal range search within the k-d tree, returning 0115 /// keys as well as values. 0116 /// 0117 /// Performs the same logic as the keyless version, but includes a copy of 0118 /// the key with each element. 0119 /// 0120 /// @param r The range to search for. 0121 /// 0122 /// @return The vector of all key-value pairs that lie within the given 0123 /// range. 0124 std::vector<pair_t> rangeSearchWithKey(const range_t &r) const { 0125 std::vector<pair_t> out; 0126 0127 rangeSearchWithKey(r, out); 0128 0129 return out; 0130 } 0131 0132 /// @brief Perform an in-place orthogonal range search within the k-d tree. 0133 /// 0134 /// This range search module operates in place, writing its results to the 0135 /// given output vector. 0136 /// 0137 /// @param r The range to search for. 0138 /// @param v The vector to write the output to. 0139 void rangeSearch(const range_t &r, std::vector<Type> &v) const { 0140 rangeSearchInserter(r, std::back_inserter(v)); 0141 } 0142 0143 /// @brief Perform an in-place orthogonal range search within the k-d tree, 0144 /// including keys in the result. 0145 /// 0146 /// Performs the same operation as the keyless version, but includes the keys 0147 /// in the results. 0148 /// 0149 /// @param r The range to search for. 0150 /// @param v The vector to write the output to. 0151 void rangeSearchWithKey(const range_t &r, std::vector<pair_t> &v) const { 0152 rangeSearchInserterWithKey(r, std::back_inserter(v)); 0153 } 0154 0155 /// @brief Perform an orthogonal range search within the k-d tree, writing 0156 /// the resulting values to an output iterator. 0157 /// 0158 /// This method allows the user more control in where the result is written 0159 /// to. 0160 /// 0161 /// @tparam OutputIt The type of the output iterator. 0162 /// 0163 /// @param r The range to search for. 0164 /// @param i The iterator to write the output to. 0165 template <typename OutputIt> 0166 void rangeSearchInserter(const range_t &r, OutputIt i) const { 0167 rangeSearchMapDiscard( 0168 r, [i](const coordinate_t &, const Type &v) mutable { i = v; }); 0169 } 0170 0171 /// @brief Perform an orthogonal range search within the k-d tree, writing 0172 /// the resulting values to an output iterator, including the keys. 0173 /// 0174 /// Performs the same operation as the keyless version, but includes the key 0175 /// in the output. 0176 /// 0177 /// @tparam OutputIt The type of the output iterator. 0178 /// 0179 /// @param r The range to search for. 0180 /// @param i The iterator to write the output to. 0181 template <typename OutputIt> 0182 void rangeSearchInserterWithKey(const range_t &r, OutputIt i) const { 0183 rangeSearchMapDiscard(r, [i](const coordinate_t &c, const Type &v) mutable { 0184 i = {c, v}; 0185 }); 0186 } 0187 0188 /// @brief Perform an orthogonal range search within the k-d tree, applying 0189 /// a mapping function to the values found. 0190 /// 0191 /// In some cases, we may wish to transform the values in some way. This 0192 /// method allows the user to provide a mapping function which takes a set of 0193 /// coordinates and a value and transforms them to a new value, which is 0194 /// returned. 0195 /// 0196 /// @note Your compiler may not be able to deduce the result type 0197 /// automatically, in which case you will need to specify it manually. 0198 /// 0199 /// @tparam Result The return type of the map operation. 0200 /// 0201 /// @param r The range to search for. 0202 /// @param f The mapping function to apply to key-value pairs. 0203 /// 0204 /// @return A vector of elements matching the range after the application of 0205 /// the mapping function. 0206 template <typename Result> 0207 std::vector<Result> rangeSearchMap( 0208 const range_t &r, 0209 std::function<Result(const coordinate_t &, const Type &)> f) const { 0210 std::vector<Result> out; 0211 0212 rangeSearchMapInserter(r, f, std::back_inserter(out)); 0213 0214 return out; 0215 } 0216 0217 /// @brief Perform an orthogonal range search within the k-d tree, applying a 0218 /// mapping function to the values found, and inserting them into an 0219 /// inserter. 0220 /// 0221 /// Performs the same operation as the interter-less version, but allows the 0222 /// user additional control over the insertion process. 0223 /// 0224 /// @note Your compiler may not be able to deduce the result type 0225 /// automatically, in which case you will need to specify it manually. 0226 /// 0227 /// @tparam Result The return type of the map operation. 0228 /// @tparam OutputIt The type of the output iterator. 0229 /// 0230 /// @param r The range to search for. 0231 /// @param f The mapping function to apply to key-value pairs. 0232 /// @param i The inserter to insert the results into. 0233 template <typename Result, typename OutputIt> 0234 void rangeSearchMapInserter( 0235 const range_t &r, 0236 std::function<Result(const coordinate_t &, const Type &)> f, 0237 OutputIt i) const { 0238 rangeSearchMapDiscard(r, [i, f](const coordinate_t &c, 0239 const Type &v) mutable { i = f(c, v); }); 0240 } 0241 0242 /// @brief Perform an orthogonal range search within the k-d tree, applying a 0243 /// a void-returning function with side-effects to each key-value pair. 0244 /// 0245 /// This is the most general version of range search in this class, and every 0246 /// other operation can be reduced to this operation as long as we allow 0247 /// arbitrary side-effects. 0248 /// 0249 /// Functional programmers will know this method as mapM_. 0250 /// 0251 /// @param r The range to search for. 0252 /// @param f The mapping function to apply to key-value pairs. 0253 template <typename Callable> 0254 void rangeSearchMapDiscard(const range_t &r, Callable &&f) const { 0255 m_root->rangeSearchMapDiscard(r, std::forward<Callable>(f)); 0256 } 0257 0258 /// @brief Return the number of elements in the k-d tree. 0259 /// 0260 /// We simply defer this method to the root node of the k-d tree. 0261 /// 0262 /// @return The number of elements in the k-d tree. 0263 std::size_t size(void) const { return m_root->size(); } 0264 0265 const_iterator_t begin(void) const { return m_elems.begin(); } 0266 0267 const_iterator_t end(void) const { return m_elems.end(); } 0268 0269 private: 0270 static Scalar nextRepresentable(Scalar v) { 0271 // I'm not super happy with this bit of code, but since 1D ranges are 0272 // semi-open, we can't simply incorporate values by setting the maximum to 0273 // them. Instead, what we need to do is get the next representable value. 0274 // For integer values, this means adding one. For floating point types, we 0275 // rely on the nextafter method to get the smallest possible value that is 0276 // larger than the one we requested. 0277 if constexpr (std::is_integral_v<Scalar>) { 0278 return v + 1; 0279 } else if constexpr (std::is_floating_point_v<Scalar>) { 0280 return std::nextafter(v, std::numeric_limits<Scalar>::max()); 0281 } 0282 } 0283 0284 static range_t boundingBox(iterator_t b, iterator_t e) { 0285 // Firstly, we find the minimum and maximum value in each dimension to 0286 // construct a bounding box around this node's values. 0287 std::array<Scalar, Dims> min_v{}, max_v{}; 0288 0289 for (std::size_t i = 0; i < Dims; ++i) { 0290 min_v[i] = std::numeric_limits<Scalar>::max(); 0291 max_v[i] = std::numeric_limits<Scalar>::lowest(); 0292 } 0293 0294 for (iterator_t i = b; i != e; ++i) { 0295 for (std::size_t j = 0; j < Dims; ++j) { 0296 min_v[j] = std::min(min_v[j], i->first[j]); 0297 max_v[j] = std::max(max_v[j], i->first[j]); 0298 } 0299 } 0300 0301 // Then, we construct a k-dimensional range from the given minima and 0302 // maxima, which again is just a bounding box. 0303 range_t r; 0304 0305 for (std::size_t j = 0; j < Dims; ++j) { 0306 r[j] = Range1D<Scalar>{min_v[j], nextRepresentable(max_v[j])}; 0307 } 0308 0309 return r; 0310 } 0311 0312 /// @brief An abstract class containing common features of k-d tree node 0313 /// types. 0314 /// 0315 /// A k-d tree consists of two different node types: leaf nodes and inner 0316 /// nodes. These nodes have some common functionality, which is captured by 0317 /// this common parent node type. 0318 class KDTreeNode { 0319 public: 0320 /// @brief Enumeration type for the possible node types (internal and leaf). 0321 enum class NodeType { Internal, Leaf }; 0322 0323 /// @brief Construct the common data for all node types. 0324 /// 0325 /// The node types share a few concepts, like an n-dimensional range, and a 0326 /// begin and end of the range of elements managed. This constructor 0327 /// calculates these things so that the individual child constructors don't 0328 /// have to. 0329 KDTreeNode(iterator_t _b, iterator_t _e, NodeType _t, std::size_t _d) 0330 : m_type(_t), 0331 m_begin_it(_b), 0332 m_end_it(_e), 0333 m_range(boundingBox(m_begin_it, m_end_it)) { 0334 if (m_type == NodeType::Internal) { 0335 // This constant determines the maximum number of elements where we 0336 // still 0337 // calculate the exact median of the values for the purposes of 0338 // splitting. In general, the closer the pivot value is to the true 0339 // median, the more balanced the tree will be. However, calculating the 0340 // median exactly is an O(n log n) operation, while approximating it is 0341 // an O(1) time. 0342 constexpr std::size_t max_exact_median = 128; 0343 0344 iterator_t pivot; 0345 0346 // Next, we need to determine the pivot point of this node, that is to 0347 // say the point in the selected pivot dimension along which point we 0348 // will split the range. To do this, we check how large the set of 0349 // elements is. If it is sufficiently small, we use the median. 0350 // Otherwise we use the mean. 0351 if (size() > max_exact_median) { 0352 // In this case, we have a lot of elements, and sorting the range to 0353 // find the true median might be too expensive. Therefore, we will 0354 // just use the middle value between the minimum and maximum. This is 0355 // not nearly as accurate as using the median, but it's a nice cheat. 0356 Scalar mid = static_cast<Scalar>(0.5) * 0357 (m_range[_d].max() + m_range[_d].min()); 0358 0359 pivot = std::partition(m_begin_it, m_end_it, [=](const pair_t &i) { 0360 return i.first[_d] < mid; 0361 }); 0362 } else { 0363 // If the number of elements is fairly small, we will just calculate 0364 // the median exactly. We do this by finding the values in the 0365 // dimension, sorting it, and then taking the middle one. 0366 std::sort(m_begin_it, m_end_it, 0367 [_d](const typename iterator_t::value_type &a, 0368 const typename iterator_t::value_type &b) { 0369 return a.first[_d] < b.first[_d]; 0370 }); 0371 0372 pivot = m_begin_it + (std::distance(m_begin_it, m_end_it) / 2); 0373 } 0374 0375 // This should never really happen, but in very select cases where there 0376 // are a lot of equal values in the range, the pivot can end up all the 0377 // way at the end of the array and we end up in an infinite loop. We 0378 // check for pivot points which would not split the range, and fix them 0379 // if they occur. 0380 if (pivot == m_begin_it || pivot == std::prev(m_end_it)) { 0381 pivot = std::next(m_begin_it, LeafSize); 0382 } 0383 0384 // Calculate the number of elements on the left-hand side, as well as 0385 // the right-hand side. We do this by calculating the difference from 0386 // the begin and end of the array to the pivot point. 0387 std::size_t lhs_size = std::distance(m_begin_it, pivot); 0388 std::size_t rhs_size = std::distance(pivot, m_end_it); 0389 0390 // Next, we check whether the left-hand node should be another internal 0391 // node or a leaf node, and we construct the node recursively. 0392 m_lhs = std::make_unique<KDTreeNode>( 0393 m_begin_it, pivot, 0394 lhs_size > LeafSize ? NodeType::Internal : NodeType::Leaf, 0395 (_d + 1) % Dims); 0396 0397 // Same on the right hand side. 0398 m_rhs = std::make_unique<KDTreeNode>( 0399 pivot, m_end_it, 0400 rhs_size > LeafSize ? NodeType::Internal : NodeType::Leaf, 0401 (_d + 1) % Dims); 0402 } 0403 } 0404 0405 /// @brief Perform a range search in the k-d tree, mapping the key-value 0406 /// pairs to a side-effecting function. 0407 /// 0408 /// This is the most powerful range search method we have, assuming that we 0409 /// can use arbitrary side effects, which we can. All other range search 0410 /// methods are implemented in terms of this particular function. 0411 /// 0412 /// @param r The range to search for. 0413 /// @param f The mapping function to apply to matching elements. 0414 template <typename Callable> 0415 void rangeSearchMapDiscard(const range_t &r, Callable &&f) const { 0416 // Determine whether the range completely covers the bounding box of 0417 // this leaf node. If it is, we can copy all values without having to 0418 // check for them being inside the range again. 0419 bool contained = r >= m_range; 0420 0421 if (m_type == NodeType::Internal) { 0422 // Firstly, we can check if the range completely contains the bounding 0423 // box of this node. If that is the case, we know for certain that any 0424 // value contained below this node should end up in the output, and we 0425 // can stop recursively looking for them. 0426 if (contained) { 0427 // We can also pre-allocate space for the number of elements, since we 0428 // are inserting all of them anyway. 0429 for (iterator_t i = m_begin_it; i != m_end_it; ++i) { 0430 f(i->first, i->second); 0431 } 0432 0433 return; 0434 } 0435 0436 assert(m_lhs && m_rhs && "Did not find lhs and rhs"); 0437 0438 // If we have a left-hand node (which we should!), then we check if 0439 // there is any overlap between the target range and the bounding box of 0440 // the left-hand node. If there is, we recursively search in that node. 0441 if (m_lhs->range() && r) { 0442 m_lhs->rangeSearchMapDiscard(r, std::forward<Callable>(f)); 0443 } 0444 0445 // Then, we perform exactly the same procedure for the right hand side. 0446 if (m_rhs->range() && r) { 0447 m_rhs->rangeSearchMapDiscard(r, std::forward<Callable>(f)); 0448 } 0449 } else { 0450 // Iterate over all the elements in this leaf node. This should be a 0451 // relatively small number (the LeafSize template parameter). 0452 for (iterator_t i = m_begin_it; i != m_end_it; ++i) { 0453 // We need to check whether the element is actually inside the range. 0454 // In case this node's bounding box is fully contained within the 0455 // range, we don't actually need to check this. 0456 if (contained || r.contains(i->first)) { 0457 f(i->first, i->second); 0458 } 0459 } 0460 } 0461 } 0462 0463 /// @brief Determine the number of elements managed by this node. 0464 /// 0465 /// Conveniently, this number is always equal to the distance between the 0466 /// begin iterator and the end iterator, so we can simply delegate to the 0467 /// relevant standard library method. 0468 /// 0469 /// @return The number of elements below this node. 0470 std::size_t size() const { return std::distance(m_begin_it, m_end_it); } 0471 0472 /// @brief The axis-aligned bounding box containing all elements in this 0473 /// node. 0474 /// 0475 /// @return The minimal axis-aligned bounding box that contains all the 0476 /// elements under this node. 0477 const range_t &range() const { return m_range; } 0478 0479 protected: 0480 NodeType m_type; 0481 0482 /// @brief The start and end of the range of coordinate-value pairs under 0483 /// this node. 0484 const iterator_t m_begin_it, m_end_it; 0485 0486 /// @brief The axis-aligned bounding box of the coordinates under this 0487 /// node. 0488 const range_t m_range; 0489 0490 /// @brief Pointers to the left and right children. 0491 std::unique_ptr<KDTreeNode> m_lhs; 0492 std::unique_ptr<KDTreeNode> m_rhs; 0493 }; 0494 0495 /// @brief Vector containing all of the elements in this k-d tree, including 0496 /// the elements managed by the nodes inside of it. 0497 vector_t m_elems; 0498 0499 /// @brief Pointer to the root node of this k-d tree. 0500 std::unique_ptr<KDTreeNode> m_root; 0501 }; 0502 } // namespace Acts
[ Source navigation ] | [ Diff markup ] | [ Identifier search ] | [ general search ] |
This page was automatically generated by the 2.3.7 LXR engine. The LXR team |
![]() ![]() |