|
|
|||
File indexing completed on 2026-07-26 08:22:00
0001 /** 0002 * TRACCC library, part of the ACTS project (R&D line) 0003 * 0004 * (c) 2021-2024 CERN for the benefit of the ACTS project 0005 * 0006 * Mozilla Public License Version 2.0 0007 */ 0008 0009 #pragma once 0010 0011 // Project include(s). 0012 #include "traccc/definitions/primitives.hpp" 0013 0014 // System include(s). 0015 #include <algorithm> 0016 #include <iostream> 0017 #include <map> 0018 #include <vector> 0019 0020 namespace traccc { 0021 /** 0022 * @brief A fast and GPU-friendly map for consecutive module IDs to values. 0023 * 0024 * Using std::map to model the relationship between geometry identifiers and 0025 * transformation matrices works only on the CPU. std::map is not supported at 0026 * all on CUDA, and either doesn't work or is inefficient on other platforms. 0027 * 0028 * This class is a more efficient and more GPU-friendly implementation of the 0029 * geometry ID to transformation mapping concept. 0030 * 0031 * The implementation of this class relies on the idea that module IDs exist in 0032 * contiguous sub-ranges, and we use this fact to get more efficient accesses. 0033 * First, we reduce the range of module IDs to a set of contiguous sub-ranges. 0034 * In the TrackML detector, there are roughly 18,000 modules, but there are 0035 * only 71 contiguous sub-arrays. Because we can index a contiguous range in 0036 * constant time, this presents possible speed-up. 0037 * 0038 * Thus, the map is essentially two-layered. First, we determine which 0039 * contigous sub-range a given value must fall in, and then we get the value 0040 * from that sub-range in constant time. 0041 * 0042 * The first layer is implemented as a simple balanced binary search tree, 0043 * which we implement in a GPU-friendly fashion. This means that the cost of 0044 * finding the relevant sub-range is O(log_2(n)), with n the number of 0045 * sub-ranges. For the TrackML detector, this means we can find our value in 0046 * only 7 steps! 0047 * 0048 * @note In theory, this class can be used for any mapping of key to value, but 0049 * it relies quite heavily (for performance) on the contiguous nature of the 0050 * keys inserted into it. 0051 * 0052 * @note This map supports only construction. You can't update it in any way. 0053 * To build one, construct a std::map first, and then convert it. 0054 */ 0055 template <typename K = geometry_id, typename V = transform3> 0056 class module_map { 0057 public: 0058 // Default constructor 0059 module_map() = default; 0060 0061 /** 0062 * @brief Construct a module map from one created by the `io` library. 0063 * 0064 * This method constructs a module map from an existing module map 0065 * represented as a std::map. 0066 * 0067 * @param[in] input The existing module map to convert. 0068 */ 0069 module_map(const std::map<K, V>& input) { 0070 /* 0071 * First, construct a list of nodes and values, which are linked 0072 * together. 0073 */ 0074 std::vector<module_map_node> nodes; 0075 std::tie(m_values, nodes) = node_list(input); 0076 0077 /* 0078 * Lay out the nodes in memory in an efficient way. 0079 */ 0080 create_tree(nodes); 0081 } 0082 0083 /** 0084 * @brief Find a given key in the map. 0085 * 0086 * @param[in] i The key to look-up. 0087 * 0088 * @return The value associated with the given key. 0089 * 0090 * @warning This method does no bounds checking, and will result in 0091 * undefined behaviour if the key does not exist in the map. 0092 */ 0093 const V& operator[](const K& i) const { return *at_helper(i, 0); } 0094 0095 /** 0096 * @brief Find a given key in the map, with bounds checking. 0097 * 0098 * @param[in] i The key to look-up. 0099 * 0100 * @return The value associated with the given key. 0101 */ 0102 const V& at(const K& i) const { 0103 const V* r = at_helper(i, 0); 0104 0105 if (r == nullptr) { 0106 throw std::out_of_range("Index not found in module map!"); 0107 } 0108 0109 return *r; 0110 } 0111 0112 /** 0113 * @brief Get the total number of modules in the module map. 0114 * 0115 * This iterates over all of the nodes in the map and sums up their sizes. 0116 * 0117 * @return The total number of modules in this module map. 0118 */ 0119 std::size_t size(void) const { 0120 std::size_t c = 0; 0121 0122 for (const module_map_node& n : m_nodes) { 0123 c += n.size; 0124 } 0125 0126 return c; 0127 } 0128 0129 bool contains(const K& i) const { return at_helper(i, 0) != nullptr; } 0130 0131 bool empty(void) const { return m_nodes.empty(); } 0132 0133 private: 0134 /** 0135 * @brief The internal representation of nodes in our binary search tree. 0136 * 0137 * These objects carry three pieces of data. Firstly, there is the starting 0138 * ID. Then, there is the size. Since the node represents a stretch of 0139 * consecutive IDs, we know that the node ends at `start + size`. Finally, 0140 * there is the index in the value array. We keep indices instead of 0141 * pointers to make it easier to port this code to other devices. 0142 */ 0143 struct module_map_node { 0144 module_map_node() = default; 0145 0146 module_map_node(K s, std::size_t n, std::size_t i) 0147 : start(s), size(n), index(i) {} 0148 0149 K start = 0; 0150 std::size_t size = 0; 0151 std::size_t index = 0; 0152 }; 0153 0154 /** 0155 * @brief Lay out a set of nodes in a binary tree format. 0156 * 0157 * This method updates the binary tree contained in this object in an 0158 * in-place fashion. 0159 * 0160 * @param[in] nodes The nodes to create a tree from. 0161 */ 0162 void create_tree(const std::vector<module_map_node>& nodes) { 0163 /* 0164 * First, we resize the number of nodes in the tree to the number of 0165 * nodes in the input, but rounded up such that we have we end up with 0166 * 2^n - 1 spaces, where n is the ceiling of log2 of the size of the 0167 * input, plus one. 0168 */ 0169 m_nodes.resize(round_up(nodes.size())); 0170 0171 /* 0172 * This vector is our work queue for the pre-order insertion that we 0173 * will be doing. Each of these values contains a position where to 0174 * insert the next node, and the subset of nodes to insert in that 0175 * sub-tree. 0176 */ 0177 std::vector<std::tuple<std::size_t, std::size_t, std::size_t>> z; 0178 0179 /* 0180 * To kick-start the algorithm, we want to insert all nodes (from 0 to 0181 * the last one) at the first spot in the array. 0182 */ 0183 z.emplace_back(0, 0, nodes.size()); 0184 0185 /* 0186 * We are done when the work queue is empty, but we will keep adding 0187 * things to it for a while at the start. 0188 */ 0189 while (!z.empty()) { 0190 /* 0191 * Pop the current index and node range off the stack. 0192 */ 0193 std::size_t i, s, e; 0194 std::tie(i, s, e) = z.back(); 0195 z.pop_back(); 0196 0197 /* 0198 * Pick a node roughly in the middle of the node range. This will 0199 * be our next child. 0200 */ 0201 std::size_t m = (s + e) / 2; 0202 0203 /* 0204 * Insert the node we selected as our center in the given position. 0205 */ 0206 m_nodes[i] = nodes[m]; 0207 0208 /* 0209 * If we have any nodes on the left side, we will put an element in 0210 * the work queue to place the left nodes at index 2i + 1. 0211 */ 0212 if (m - s > 0) { 0213 z.emplace_back(2 * i + 1, s, m); 0214 } 0215 0216 /* 0217 * Same thing for the right side, but we insert the right side 0218 * starting at 2i + 2 instead. 0219 */ 0220 if (e - (m + 1) > 0) { 0221 z.emplace_back(2 * i + 2, m + 1, e); 0222 } 0223 } 0224 } 0225 0226 /** 0227 * @brief Special rounding function for binary trees. 0228 * 0229 * This function computes 2^n - 1, where n is the ceiling of log2 of the 0230 * input plus one. 0231 * 0232 * This sounds like an odd operation, but consider that a binary tree (when 0233 * complete) of depth k, can contain at most 2^k - 1 nodes. That is the 0234 * number of spaces we need. However, our bottom level may have some empty 0235 * slots, because it is rather unlikely that we will have exactly 2^k - 1 0236 * nodes. Thus, we need to round up k from log2 of the input. However, we 0237 * need to also keep in mind that 2^l (for some integer l) should be 0238 * rounded up to 2^(l+1), not 2^l! Since we are computing l, this would 0239 * mean we would be short one space in some edge cases. 0240 * 0241 * @param[in] i The number to round up. 0242 * 0243 * @return The specially rounded number. 0244 */ 0245 static std::size_t round_up(std::size_t i) { 0246 std::size_t n = 1; 0247 0248 while (n < i + 1) { 0249 n *= 2; 0250 } 0251 0252 return n - 1; 0253 } 0254 0255 /** 0256 * @brief Get a list of values and nodes from an existing std::map. 0257 * 0258 * Given an existing map, this function will attempt to find all the nodes, 0259 * that is to say contiguous sub-ranges. It will also put the values into 0260 * one big, contiguous array. 0261 * 0262 * @param[in] input An existing std::map module map. 0263 * 0264 * @return A pair of vectors, one has the values, and the other has the 0265 * nodes. 0266 */ 0267 static std::pair<std::vector<V>, std::vector<module_map_node>> node_list( 0268 const std::map<K, V>& input) { 0269 /* 0270 * We start out by grabbing a list of all the keys in the map. 0271 */ 0272 std::vector<K> keys; 0273 keys.reserve(input.size()); 0274 for (const std::pair<const K, V>& i : input) { 0275 keys.push_back(i.first); 0276 } 0277 0278 /* 0279 * Next, we declare a vector of values, which is one of the two things 0280 * that we will return in the end. 0281 */ 0282 std::vector<V> values; 0283 values.reserve(keys.size()); 0284 0285 /* 0286 * To find contiguous sub-ranges, the keys must be sorted. In general, 0287 * it is likely that they will be pre-sorted. 0288 */ 0289 std::sort(keys.begin(), keys.end()); 0290 0291 /* 0292 * This vector will hold our nodes. We'll update it as we go, and we 0293 * start out with an incomplete first node, which represents the single 0294 * value at the start of the key vector. We will add to this later! 0295 */ 0296 std::vector<module_map_node> nodes; 0297 nodes.push_back(module_map_node(keys.front(), 1, values.size())); 0298 values.push_back(input.at(keys.front())); 0299 0300 /* 0301 * Starting from the second key, we check if they are contiuous with 0302 * the last key, and store them if so. 0303 */ 0304 for (std::size_t i = 1; i < keys.size(); ++i) { 0305 /* 0306 * If the key is not adjacent to the last one, we push a new 0307 * partial node onto the node vector. 0308 */ 0309 if (keys.at(i) != keys.at(i - 1) + 1) { 0310 nodes.push_back(module_map_node(keys.at(i), 0, values.size())); 0311 } 0312 0313 /* 0314 * Push the current value into the value array, and register one 0315 * additional element in the current node. 0316 */ 0317 ++nodes.back().size; 0318 values.push_back(input.at(keys.at(i))); 0319 } 0320 0321 /* 0322 * Return both the generated values. 0323 */ 0324 return {values, nodes}; 0325 } 0326 0327 /** 0328 * @brief Helper function to retrieve a value from the map. 0329 * 0330 * This function searches recursively for a value in a given sub-tree. Due 0331 * to the formalism used for node indices, we can identify a subtree simply 0332 * by the node index of its root node. 0333 * 0334 * @param[in] i The value to look for. 0335 * @param[in] n The index of the subtree's root node. 0336 */ 0337 const V* at_helper(const K& i, std::size_t n) const { 0338 /* 0339 * For memory safety, if we are out of bounds we will exit. 0340 */ 0341 if (n >= m_nodes.size()) { 0342 return nullptr; 0343 } 0344 0345 /* 0346 * Retrieve the current root node. 0347 */ 0348 const module_map_node& node = m_nodes[n]; 0349 0350 /* 0351 * If the size is zero, it is essentially an invalid node (i.e. the 0352 * node does not exist). 0353 */ 0354 if (node.size == 0) { 0355 return nullptr; 0356 } 0357 0358 /* 0359 * If the value we are looking for is past the start of the current 0360 * node, there are three possibilities. Firstly, the value might be in 0361 * the current node. Secondly, the value might be in the right child of 0362 * the current node. Thirdly, the value might not be in the map at all. 0363 */ 0364 if (i >= node.start) { 0365 /* 0366 * Next, we check if the value is within the range represented by 0367 * the current node. 0368 */ 0369 if (i < node.start + node.size) { 0370 /* 0371 * Found it! Return a pointer to the value within the 0372 * contiguous range. 0373 */ 0374 return &m_values[node.index + (i - node.start)]; 0375 } else { 0376 /* 0377 * Two possibilties remain, we need to check the right subtree. 0378 */ 0379 return at_helper(i, 2 * n + 2); 0380 } 0381 } 0382 /* 0383 * If the value we want to find is less then the start of this node, 0384 * there are only two possibilities. Firstly, the value might be in the 0385 * left subtree, or the value might not be in the map at all. 0386 */ 0387 else { 0388 return at_helper(i, 2 * n + 1); 0389 } 0390 } 0391 0392 /** 0393 * @brief The internal storage of the nodes in our binary search tree. 0394 * 0395 * This follows the well-known formalism where the root node resides at 0396 * index 0, while for any node at position n, the left child is at index 2n 0397 * + 1, and the right child is at index 2n + 2. 0398 */ 0399 std::vector<module_map_node> m_nodes; 0400 0401 /** 0402 * @brief This vector stores the values in a contiguous manner. Our nodes 0403 * keep indices in this array instead of pointers. 0404 */ 0405 std::vector<V> m_values; 0406 }; 0407 } // namespace traccc
| [ Source navigation ] | [ Diff markup ] | [ Identifier search ] | [ general search ] |
|
This page was automatically generated by the 2.3.7 LXR engine. The LXR team |
|