Back to home page

EIC code displayed by LXR

 
 

    


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