Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-05-27 07:24:15

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 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 https://mozilla.org/MPL/2.0/.
0008 
0009 #pragma once
0010 
0011 // Project include(s)
0012 #include "detray/utils/logging.hpp"
0013 #include "detray/utils/ranges.hpp"
0014 
0015 // Detray plugin include(s)
0016 #include "detray/plugins/svgtools/illustrator.hpp"
0017 
0018 // Detray test include(s)
0019 #include "detray/test/validation/svg_display.hpp"
0020 
0021 // System include(s)
0022 #include <algorithm>
0023 #include <functional>
0024 #include <iostream>
0025 #include <iterator>
0026 #include <map>
0027 #include <ranges>
0028 #include <set>
0029 #include <sstream>
0030 #include <string>
0031 #include <tuple>
0032 #include <type_traits>
0033 #include <unordered_set>
0034 #include <utility>
0035 #include <vector>
0036 
0037 namespace detray::detector_scanner {
0038 
0039 /// @brief Filter the intersection trace for overlapping surfaces and
0040 /// remove duplicates
0041 ///
0042 /// ACTS geometries can produce multiple overlapping portals, since some portals
0043 /// are larger than the volumes they belong to. This leads to duplicate
0044 /// intersections in the traces.
0045 template <typename record_container>
0046 inline dindex_range overlaps_removal(record_container &intersection_records,
0047                                      const float tol = 1e-4f *
0048                                                        unit<float>::mm) {
0049   // Document any overlaps on the range
0050   constexpr auto inv_idx{detray::detail::invalid_value<dindex>()};
0051   dindex_range overlap_idx{inv_idx, inv_idx};
0052 
0053   const std::size_t n_rec{intersection_records.size()};
0054   std::size_t n_eq_intrs{1u};  //< Number of consecutive overlapping inters.
0055   for (std::size_t i = 0u; i < n_rec - 1u; ++i) {
0056     const auto &rec = intersection_records.at(i);
0057     const auto &next_rec = intersection_records.at(i + 1u);
0058 
0059     // This record and the following are overlapping: Count until we reach
0060     // the end of the overlapping surfaces or end of trace
0061     if (math::fabs(next_rec.intersection.path() - rec.intersection.path()) <
0062             tol &&
0063         i != (n_rec - 2u)) {
0064       ++n_eq_intrs;
0065       continue;
0066     }
0067 
0068     // No overlap: continue
0069     if (n_eq_intrs == 1u) {
0070       continue;
0071     }
0072     // Found two overlapping surfaces
0073     if (n_eq_intrs == 2u) {
0074       // Two overlapping portals form a valid, connected volume boundary
0075       if (const auto &prev_rec = intersection_records.at(i - 1u);
0076           !(rec.intersection.surface().is_portal() &&
0077             prev_rec.intersection.surface().is_portal())) {
0078         auto prev_sf_desc = prev_rec.intersection.surface();
0079         auto sf_desc = rec.intersection.surface();
0080 
0081         // Other types of surfaces must not overlap!
0082         std::stringstream err_stream;
0083         err_stream << "The following surfaces overlap at\n"
0084                    << "POS:\n"
0085                    << "glob: " << prev_rec.track_param.pos()
0086                    << ", loc: " << prev_rec.intersection.local()
0087                    << "\nvs.\nglob: " << rec.track_param.pos()
0088                    << ", loc: " << rec.intersection.local() << std::endl;
0089         err_stream << "SURFACES:\n -> " << prev_sf_desc << std::endl;
0090         err_stream << " -> " << sf_desc << std::endl;
0091 
0092         // TODO: Fix wire_chamber geometry
0093         // throw std::invalid_argument(err_stream.str());
0094         DETRAY_ERROR_HOST(err_stream.str());
0095         overlap_idx = {static_cast<dindex>(i - 1u), static_cast<dindex>(i)};
0096       }
0097       // Reset to find the next range
0098       n_eq_intrs = 1u;
0099       continue;
0100     }
0101 
0102     // Found more than two overlapping surfaces: Remove oversized portal
0103     // intersections
0104     std::size_t first{(i - (n_eq_intrs - 1u))};
0105     std::size_t last{i};
0106 
0107     // Map of the volume index the portals link to and the portal
0108     // intersection indices in the intersection trace
0109     std::multimap<std::size_t, std::size_t> pt_buckets{};
0110 
0111     bool is_all_portals{true};
0112     for (std::size_t j = first; j <= last; ++j) {
0113       const auto &intr = intersection_records.at(j).intersection;
0114       if (!intr.surface().is_portal()) {
0115         is_all_portals = false;
0116       }
0117       pt_buckets.insert({static_cast<std::size_t>(intr.volume_link()), j});
0118     }
0119 
0120     // Remove buckets that contain only one portal: This is the valid exit
0121     // portal that the overlapping portals need to be matched against
0122     auto exit_idx{std::numeric_limits<std::size_t>::max()};
0123     const auto n_erased =
0124         std::erase_if(pt_buckets, [&pt_buckets, &exit_idx](const auto &item) {
0125           const auto &[vol_link, rec_idx] = item;
0126           if (pt_buckets.count(vol_link) == 1u) {
0127             exit_idx = rec_idx;
0128             return true;
0129           }
0130           return false;
0131         });
0132 
0133     // This is not a case of oversized portals, treat as actual overlaps
0134     if (n_erased != 1u || pt_buckets.empty() || !is_all_portals) {
0135       DETRAY_ERROR_HOST("Could not resolve exit portal in overlap correction");
0136 
0137       overlap_idx = {static_cast<dindex>(first), static_cast<dindex>(last)};
0138     } else {
0139       // Keep only the portal that forms the correct volume crossing
0140       // with the portal intersection at 'exit_idx'
0141       const auto &exit_rec = intersection_records.at(exit_idx);
0142       auto itr = intersection_records.cbegin();
0143 
0144       // Remove elements indiceated by ordered map with higher index first
0145       for (const auto &[v_link, idx] : pt_buckets | std::views::reverse) {
0146         // All these portals link against the exit portal
0147         assert(v_link == exit_rec.vol_idx);
0148 
0149         auto test_elem = std::next(itr, static_cast<int>(idx));
0150         if (exit_rec.intersection.volume_link() != test_elem->vol_idx) {
0151           intersection_records.erase(test_elem);
0152         }
0153       }
0154     }
0155 
0156     // Reset
0157     n_eq_intrs = 1u;
0158   }
0159 
0160   return overlap_idx;
0161 }
0162 
0163 /// Check if a set of volume indices from portal intersections from a path
0164 /// (works even if the pairs are not sorted). That the volume links of the
0165 /// portals at a single boundary crossing match was already checked by the
0166 /// @c trace_intersections function at this point.
0167 ///
0168 /// For example, with (a, b) modeling a valid portal crossing between volume 'a'
0169 /// and 'b' (leaving out the portal indices that are also in those pairs), then:
0170 ///     (0, 1)(1, 16)(16, 17)  is valid, (0, 1)(1, 15)(16, 17) is not
0171 ///         |__|   |__|    |_                |__|   | x |   |_
0172 ///
0173 /// Also checks that the first portal that was found, lies in the start volume.
0174 ///
0175 /// @tparam invalid_value to keep the implementation simple, all indices are of
0176 ///                       type @c dindex here, but that does not need to be the
0177 ///                       case in the intersection code, so we need to know what
0178 ///                       the invalid value is interpreted as a @c dindex
0179 /// @tparam check_sorted_trace if the trace is not sorted, perform a search for
0180 ///                            a fitting record across the trace.
0181 /// @tparam entry_type the record entry, which must contain the portal index
0182 ///                    and the volume index the portal was discovered in.
0183 ///
0184 /// @param trace the recorded portal crossings between volumes
0185 /// @param start_volume where the ray started
0186 ///
0187 /// @return true if the volumes indices form a connected chain.
0188 template <dindex invalid_value = dindex_invalid, bool check_sorted_trace = true,
0189           typename entry_type = std::pair<dindex, dindex>>
0190 inline bool check_connectivity(
0191     std::vector<std::pair<entry_type, entry_type>> trace,
0192     dindex start_volume = 0u) {
0193   /// Error messages
0194   std::stringstream err_stream;
0195 
0196   /// Print errors of this function
0197   auto print_err = [](const std::stringstream &stream) {
0198     std::cerr << "\n<<<<<<<<<<<<<<< ERROR in connectivity check\n" << std::endl;
0199     std::cerr << stream.str() << std::endl;
0200     std::cerr << "\n>>>>>>>>>>>>>>>\n" << std::endl;
0201   };
0202 
0203   // There must always be portals!
0204   if (trace.empty()) {
0205     err_stream << "Trace empty!";
0206     print_err(err_stream);
0207 
0208     return false;
0209   }
0210   // Keep record of leftovers
0211   std::stringstream record_stream;
0212 
0213   // Where are we on the trace?
0214   dindex current_volume = start_volume;
0215 
0216   // If the intersection trace comes from the ray gun/trace intersections
0217   // function it should be sorted, which is the stronger constraint
0218   using vector_t = decltype(trace);
0219   using records_iterator_t = typename vector_t::iterator;
0220   using index_t = std::iter_difference_t<records_iterator_t>;
0221   std::function<records_iterator_t(index_t)> get_connected_record;
0222   if constexpr (check_sorted_trace) {
0223     // Get the next record
0224     get_connected_record =
0225         [&trace, &current_volume](index_t next) -> records_iterator_t {
0226       // Make sure that the record contains the volume that is currently
0227       // being checked for connectivity
0228       if (auto rec = trace.begin() + next;
0229           rec != trace.end() &&
0230           ((std::get<1>(rec->first) == current_volume) ||
0231            (std::get<1>(rec->second) == current_volume))) {
0232         return rec;
0233       }
0234       return trace.end();
0235     };
0236   } else {
0237     // Search for the existence of a fitting record over the entire trace
0238     get_connected_record =
0239         [&trace, &current_volume](index_t /*next*/) -> records_iterator_t {
0240       return std::ranges::find_if(
0241           trace, [&](const std::pair<entry_type, entry_type> &rec) -> bool {
0242             return (std::get<1>(rec.first) == current_volume) ||
0243                    (std::get<1>(rec.second) == current_volume);
0244           });
0245     };
0246   }
0247 
0248   // Init chain search
0249   index_t i{0};
0250   auto record = get_connected_record(i);
0251 
0252   // Check first volume index, which has no partner otherwise
0253   if (std::get<1>(record->first) != start_volume) {
0254     err_stream << "First record does not start at given initial volume: "
0255                << std::get<1>(record->first) << " vs. " << start_volume;
0256 
0257     print_err(err_stream);
0258 
0259     return false;
0260   }
0261 
0262   // Walk along the trace as long as a connection is found
0263   while (record != trace.end()) {
0264     auto first_vol = std::get<1>(record->first);
0265     auto second_vol = std::get<1>(record->second);
0266 
0267     record_stream << "On volume: " << current_volume << " and record ("
0268                   << first_vol << ", " << second_vol << ")";
0269 
0270     // update to next volume
0271     current_volume = (current_volume == first_vol ? second_vol : first_vol);
0272 
0273     record_stream << " -> next volume: " << current_volume << std::endl;
0274 
0275     // Don't search this key again -> only one potential key with current
0276     // index left
0277     if constexpr (!check_sorted_trace) {
0278       trace.erase(record);
0279     }
0280 
0281     // find connected record for the current volume
0282     record = get_connected_record(++i);
0283   }
0284 
0285   // There are unconnected elements left (we didn't leave world before
0286   // termination)
0287   if (current_volume != invalid_value) {
0288     err_stream << "Didn't leave world or unconnected elements left in trace:"
0289                << "\n\nValid connections that were found:" << std::endl;
0290     err_stream << record_stream.str();
0291 
0292     err_stream << "\nPairs left to match:" << std::endl;
0293     for (auto j = static_cast<std::size_t>(i); j < trace.size(); ++j) {
0294       auto first_vol = std::get<1>(trace[j].first);
0295       auto second_vol = std::get<1>(trace[j].second);
0296 
0297       err_stream << "(" << first_vol << ", " << second_vol << ")" << std::endl;
0298     }
0299 
0300     print_err(err_stream);
0301 
0302     return false;
0303   }
0304 
0305   return true;
0306 }
0307 
0308 /// Check if a recording of portal/module intersections form a coherent
0309 /// trace through a geometry.
0310 ///
0311 /// Various consistency checks are performed on the trace:
0312 ///     - Was a surface intersected as part of the wrong volume?
0313 ///     - Do the volume links at adjacent portals point at each other?
0314 ///     - Is a portal crossing happening within a volume, as opposed to at its
0315 ///       boundary surfaces?
0316 ///     - Do portals always appear in pairs (exit + entry portal)?
0317 ///
0318 /// @tparam record_container contains volume indices and intersections
0319 ///
0320 /// @param volume_record the recorded portal crossings between volumes
0321 /// @param start_volume where the ray started
0322 ///
0323 /// @note the input record needs to be sorted according to the distance from the
0324 ///       ray origin
0325 ///
0326 /// @return a set of volume connections that were found by portal intersection
0327 ///         of a ray.
0328 template <dindex invalid_value = dindex_invalid, typename record_container>
0329 inline auto trace_intersections(const record_container &intersection_records,
0330                                 dindex start_volume = 0u) {
0331   /// surface index and index of the volume the intersection was found in
0332   using trace_entry = std::pair<dindex, dindex>;
0333   /// Pairs of adjacent portals along the ray
0334   std::vector<std::pair<trace_entry, trace_entry>> portal_trace = {};
0335   /// Trace of module surfaces
0336   std::vector<trace_entry> module_trace = {};
0337   /// Debug output if an error in the trace is discovered
0338   std::stringstream record_stream;
0339   /// Error messages
0340   std::stringstream err_stream;
0341   bool error_code{true};
0342 
0343   /// Readable access to the data of a recorded intersection
0344   struct record {
0345     const typename record_container::value_type &entry;
0346 
0347     /// getter
0348     /// @{
0349     inline bool is_invalid() const {
0350       return entry.intersection.surface().identifier().is_invalid();
0351     }
0352     inline auto surface_idx() const {
0353       return entry.intersection.surface().index();
0354     }
0355     inline auto surface_volume_idx() const {
0356       return entry.intersection.surface().volume();
0357     }
0358     inline auto &inters() const { return entry.intersection; }
0359     inline auto volume_idx() const { return entry.vol_idx; }
0360     inline auto volume_link() const { return entry.intersection.volume_link(); }
0361     inline auto dist() const { return entry.intersection.path(); }
0362     inline bool is_portal() const {
0363       return entry.intersection.surface().is_portal();
0364     }
0365     inline bool is_sensitive() const {
0366       return entry.intersection.surface().is_sensitive();
0367     }
0368     inline bool is_passive() const {
0369       return entry.intersection.surface().is_passive();
0370     }
0371     /// @}
0372   };
0373 
0374   /// Print errors of this function
0375   auto print_err = [&error_code](const std::stringstream &stream) {
0376     std::cerr << "\n<<<<<<<<<<<<<<< ERROR intersection trace\n" << std::endl;
0377     std::cerr << stream.str() << std::endl;
0378     std::cerr << "\n>>>>>>>>>>>>>>>\n" << std::endl;
0379     error_code = false;
0380   };
0381 
0382   // No intersections found by ray
0383   if (intersection_records.empty()) {
0384     err_stream << "No surfaces found in detector!";
0385     print_err(err_stream);
0386 
0387     return std::make_tuple(portal_trace, module_trace, error_code);
0388   }
0389 
0390   // If there is only one surface in the trace, it must be a portal
0391   if (intersection_records.size() == 1u) {
0392     const record rec{intersection_records.at(0u)};
0393 
0394     // No exit potal
0395     if (!rec.is_portal()) {
0396       if (rec.is_invalid()) {
0397         err_stream << "No surfaces found in detector!";
0398         print_err(err_stream);
0399       } else {
0400         const std::string sf_type{rec.is_sensitive() ? "sensitive" : "passive"};
0401 
0402         err_stream << "We don't leave the detector by portal!" << std::endl;
0403         err_stream << "Only found single " << sf_type
0404                    << " surface: portal(s) missing!";
0405 
0406         print_err(err_stream);
0407       }
0408 
0409       return std::make_tuple(portal_trace, module_trace, error_code);
0410     }
0411   }
0412 
0413   // Go through recorded intersection (two at a time)
0414   dindex current_vol = start_volume;
0415   // The first entry is the dummy record that preserves initial track
0416   // parameters, skip it
0417   const std::size_t start_idx{
0418       record{intersection_records.at(0)}.is_invalid() ? 1u : 0u};
0419   for (std::size_t rec = start_idx; rec < (intersection_records.size() - 1u);) {
0420     const record current_rec = record{intersection_records.at(rec)};
0421     const record next_rec = record{intersection_records.at(rec + 1u)};
0422 
0423     // Keep a debug stream
0424     record_stream << current_rec.volume_idx() << "\t" << current_rec.inters()
0425                   << std::endl;
0426 
0427     // If the current record is not a portal, add an entry to the module
0428     // trace and continue in more fine-grained steps (sensitive/passive
0429     // surfaces do not come in pairs)
0430     if (!current_rec.is_portal()) {
0431       // Check that the surface was found in the volume it claims to
0432       // belong to
0433       const bool is_in_volume =
0434           (current_rec.volume_idx() == current_rec.surface_volume_idx()) &&
0435           (current_rec.surface_volume_idx() == current_vol);
0436       if (is_in_volume) {
0437         module_trace.emplace_back(current_rec.surface_idx(),
0438                                   current_rec.volume_idx());
0439       } else {
0440         err_stream << "\n(!!) Surface " << current_rec.surface_idx()
0441                    << " outside of its volume (Found in: " << current_vol
0442                    << ", belongs in: " << current_rec.surface_volume_idx()
0443                    << ")\n";
0444 
0445         err_stream << record_stream.str();
0446 
0447         print_err(err_stream);
0448 
0449         return std::make_tuple(portal_trace, module_trace, error_code);
0450       }
0451       ++rec;
0452       continue;
0453     }
0454     // If the record is a portal, the current volume switches
0455     // the portals in the pair may be unordered, so check both
0456     else if (current_vol == current_rec.volume_idx()) {
0457       current_vol = current_rec.volume_link();
0458     } else if (current_vol == next_rec.volume_idx()) {
0459       current_vol = next_rec.volume_link();
0460     }
0461 
0462     record_stream << next_rec.volume_idx() << "\t" << next_rec.inters()
0463                   << std::endl;
0464 
0465     // Check that also the second surface was found in the volume it claims
0466     // to belong to
0467     const bool is_in_volume =
0468         next_rec.volume_idx() == next_rec.surface_volume_idx();
0469     // Is this doublet connected via a valid portal intersection?
0470     const bool is_valid =
0471         (current_rec.inters() == next_rec.inters()) &&
0472         (current_rec.volume_idx() == next_rec.volume_link()) &&
0473         (next_rec.volume_idx() == current_rec.volume_link());
0474     // Is this indeed a portal crossing, i.e. changing volumes?
0475     const bool is_self_link = current_rec.volume_idx() == next_rec.volume_idx();
0476     // Is the record doublet we picked made up of a portal and a module?
0477     const bool is_mixed = (current_rec.is_portal() && !next_rec.is_portal()) ||
0478                           (next_rec.is_portal() && !current_rec.is_portal());
0479 
0480     if (!is_in_volume) {
0481       record_stream << "\n(!!) Surface outside of its volume (Found: "
0482                     << next_rec.volume_idx()
0483                     << ", belongs in: " << next_rec.surface_volume_idx() << ")"
0484                     << std::endl;
0485     }
0486     if (!is_valid) {
0487       record_stream << "\n(!!) Not a valid portal crossing ("
0488                     << current_rec.volume_idx() << " <-> "
0489                     << next_rec.volume_idx() << "):\nPortals are not "
0490                     << "connected, either geometrically or by linking!"
0491                     << std::endl;
0492     }
0493     if (is_self_link) {
0494       record_stream << "\n(!!) Found portal crossing inside volume ("
0495                     << current_rec.volume_idx() << ")!" << std::endl;
0496     }
0497     if (is_mixed) {
0498       record_stream << "\n(!!) Portal crossing involves module surface ("
0499                     << current_rec.volume_idx() << " <-> "
0500                     << next_rec.volume_idx() << ")! The second surface in"
0501                     << " this portal crossing is not a portal!" << std::endl;
0502     }
0503     if (is_in_volume && is_valid && !is_self_link && !is_mixed) {
0504       // Insert into portal trace
0505       trace_entry lower{current_rec.surface_idx(), current_rec.volume_idx()};
0506       trace_entry upper{next_rec.surface_idx(), next_rec.volume_idx()};
0507       portal_trace.emplace_back(lower, upper);
0508     }
0509     // Something went wrong
0510     else {
0511       // Print search log
0512       err_stream << "\nError in portal matching:\n" << std::endl;
0513       err_stream << "volume id\t(intersection info)" << std::endl;
0514 
0515       err_stream << record_stream.str() << std::endl;
0516 
0517       err_stream << "-----\nINFO: Ray terminated at portal x-ing "
0518                  << (rec + 1) / 2 << ":\n"
0519                  << current_rec.inters() << " <-> " << next_rec.inters()
0520                  << std::endl;
0521 
0522       const record rec_front{record{intersection_records.front()}.is_invalid()
0523                                  ? intersection_records[1u]
0524                                  : intersection_records.front()};
0525       const record rec_back{intersection_records.back()};
0526       err_stream << "Start volume : " << start_volume << std::endl;
0527       err_stream << "- first recorded intersection: (sf id:"
0528                  << rec_front.surface_idx() << ", dist:" << rec_front.dist()
0529                  << ")," << std::endl;
0530       err_stream << "- last recorded intersection:  (sf id:"
0531                  << rec_back.surface_idx() << ", dist:" << rec_back.dist()
0532                  << "),";
0533 
0534       print_err(err_stream);
0535 
0536       return std::make_tuple(portal_trace, module_trace, error_code);
0537     }
0538 
0539     // Advance to inspect next pair
0540     rec += 2u;
0541   }
0542 
0543   // Look at the last entry, which is a single portal
0544   if (const record rec_back{intersection_records.back()};
0545       !rec_back.is_portal()) {
0546     err_stream << "We don't leave the detector by portal!";
0547     print_err(err_stream);
0548   } else {
0549     trace_entry lower(rec_back.surface_idx(), rec_back.volume_idx());
0550     trace_entry upper(rec_back.surface_idx(), rec_back.volume_link());
0551     portal_trace.emplace_back(lower, upper);
0552   }
0553 
0554   return std::make_tuple(portal_trace, module_trace, error_code);
0555 }
0556 
0557 /// Build an adjacency list from intersection traces.
0558 ///
0559 /// @tparam portal_trace_type container of portal link pairs
0560 /// @tparam module_trace_type container of module surface links
0561 ///
0562 /// @param portal_trace the portal indices and their volume links (in adjacent
0563 ///                     portal pairs)
0564 /// @param module_trace the module indices and their volume links
0565 /// @param obj_hashes record which modules/portals were already added
0566 ///
0567 /// @return an adjacency list from the traced ray scan of a given geometry.
0568 template <dindex invalid_value = dindex_invalid, typename portal_trace_type,
0569           typename module_trace_type,
0570           typename entry_type = std::pair<dindex, dindex>>
0571   requires std::is_same_v<typename portal_trace_type::value_type,
0572                           std::pair<entry_type, entry_type>> &&
0573            std::is_same_v<typename module_trace_type::value_type, entry_type>
0574 inline auto build_adjacency(
0575     const portal_trace_type &portal_trace,
0576     const module_trace_type &module_trace,
0577     std::map<dindex, std::map<dindex, dindex>> &adj_list,
0578     std::unordered_set<dindex> &obj_hashes) {
0579   // Every module that was recorded adds a link to the mother volume
0580   for (const auto &record : module_trace) {
0581     const auto sf_index = std::get<0>(record);
0582     const auto vol_index = std::get<1>(record);
0583     // Check whether we have seen this module in this volume before
0584     if (obj_hashes.find(sf_index) == obj_hashes.end()) {
0585       adj_list[vol_index][vol_index]++;
0586       obj_hashes.insert(sf_index);
0587     }
0588   }
0589 
0590   // Portal in first volume links to second volume in the record
0591   for (const auto &record : portal_trace) {
0592     const auto pt_index_1 = std::get<0>(record.first);
0593     const auto vol_index_1 = std::get<1>(record.first);
0594     const auto pt_index_2 = std::get<0>(record.second);
0595     const auto vol_index_2 = std::get<1>(record.second);
0596 
0597     if (obj_hashes.find(pt_index_1) == obj_hashes.end()) {
0598       adj_list[vol_index_1][vol_index_2]++;
0599       obj_hashes.insert(pt_index_1);
0600     }
0601     // Assume the return link for now (filter out portal that leaves world)
0602     if (vol_index_2 != invalid_value &&
0603         obj_hashes.find(pt_index_2) == obj_hashes.end()) {
0604       adj_list[vol_index_2][vol_index_1]++;
0605       obj_hashes.insert(pt_index_2);
0606     }
0607   }
0608 
0609   return adj_list;
0610 }
0611 
0612 /// Build an adjacency list from intersection traces.
0613 ///
0614 /// @tparam portal_trace_type container of portal link pairs
0615 /// @tparam module_trace_type container of module links
0616 ///
0617 /// @param portal_trace the portal indices and their volume links (in adjacent
0618 ///                     portal pairs)
0619 /// @param module_trace the module indices and their volume links
0620 /// @param obj_hashes record which modules/portals were already added
0621 ///
0622 /// @return an adjacency list from the traced ray scan of a given geometry.
0623 template <dindex invalid_value = dindex_invalid, typename portal_trace_type,
0624           typename module_trace_type,
0625           typename entry_type = std::pair<dindex, dindex>>
0626   requires std::is_same_v<typename portal_trace_type::value_type,
0627                           std::pair<entry_type, entry_type>> &&
0628            std::is_same_v<typename module_trace_type::value_type, entry_type>
0629 inline auto build_adjacency(const portal_trace_type &portal_trace,
0630                             const module_trace_type &module_trace,
0631                             dvector<dindex> &adj_matrix,
0632                             std::unordered_set<dindex> &obj_hashes) {
0633   const auto dim = static_cast<dindex>(math::sqrt(adj_matrix.size()));
0634 
0635   // Every module that was recorded adds a link to the mother volume
0636   for (const auto &record : module_trace) {
0637     const auto sf_index = std::get<0>(record);
0638     const auto vol_index = std::get<1>(record);
0639     // Check whether we have seen this module in this volume before
0640     if (obj_hashes.find(sf_index) == obj_hashes.end()) {
0641       adj_matrix[dim * vol_index + vol_index]++;
0642       obj_hashes.insert(sf_index);
0643     }
0644   }
0645 
0646   // Portal in first volume links to second volume in the record
0647   for (const auto &record : portal_trace) {
0648     const auto pt_index_1 = std::get<0>(record.first);
0649     const auto vol_index_1 = std::get<1>(record.first);
0650     const auto pt_index_2 = std::get<0>(record.second);
0651     const auto vol_index_2 = std::get<1>(record.second);
0652 
0653     if (obj_hashes.find(pt_index_1) == obj_hashes.end()) {
0654       dindex mat_elem_vol1{dindex_invalid};
0655       // Assume the return link for now (filtering out portals that leave
0656       // world)
0657       if (vol_index_2 != invalid_value) {
0658         mat_elem_vol1 = dim * vol_index_1 + vol_index_2;
0659 
0660         if (obj_hashes.find(pt_index_2) == obj_hashes.end()) {
0661           adj_matrix[dim * vol_index_2 + vol_index_1]++;
0662           obj_hashes.insert(pt_index_2);
0663         }
0664       } else {
0665         mat_elem_vol1 = dim * vol_index_1 + dim - 1;
0666       }
0667       adj_matrix[mat_elem_vol1]++;
0668       obj_hashes.insert(pt_index_1);
0669     }
0670   }
0671 
0672   return adj_matrix;
0673 }
0674 
0675 /// Run all checks on an intersection trace.
0676 ///
0677 /// @param[in] intersection_trace the intersection records along the track
0678 /// @param[in] start_index the index of the intended start volume
0679 /// @param[out] adj_mat_scan adjacency matrix to be filled for the detector
0680 /// @param[out] obj_hashes objects in a volume that were already visisted
0681 ///
0682 /// @return true if the checks were successful
0683 template <typename detector_t, typename record_t>
0684 inline bool check_trace(const std::vector<record_t> &intersection_trace,
0685                         const dindex start_index, dvector<dindex> &adj_mat_scan,
0686                         std::unordered_set<dindex> &obj_hashes) {
0687   using nav_link_t = typename detector_t::surface_type::navigation_link;
0688   static constexpr auto leaving_world{
0689       detray::detail::invalid_value<nav_link_t>()};
0690 
0691   // Create a trace of the volume indices that were encountered
0692   // and check that portal intersections are connected
0693   auto [portal_trace, surface_trace, err_code] =
0694       detector_scanner::trace_intersections<leaving_world>(intersection_trace,
0695                                                            start_index);
0696 
0697   // Is the succession of volumes consistent ?
0698   err_code = err_code &&
0699              detector_scanner::check_connectivity<leaving_world>(portal_trace);
0700 
0701   if (!adj_mat_scan.empty()) {
0702     // Build an adjacency matrix from this trace that can be checked
0703     // against the geometry hash (see 'track_geometry_changes')
0704     detector_scanner::build_adjacency<leaving_world>(
0705         portal_trace, surface_trace, adj_mat_scan, obj_hashes);
0706   }
0707 
0708   return err_code;
0709 }
0710 
0711 /// Print the failed intersection trace as svg (dumped to files)
0712 ///
0713 /// @param gctx current geometry context
0714 /// @param det the detector object
0715 /// @param vol_names the volume name map of the detector
0716 /// @param test_name the name of the test for which to print the error
0717 /// @param test_track trajectory that was used for the scan (ray or helix)
0718 /// @param truth_trace the intersection records along the test track
0719 /// @param svg_style svgtools style for the detector display
0720 /// @param i_track index of the test track
0721 /// @param n_track total number of test tracks
0722 template <typename detector_t, typename trajectory_t, typename truth_trace_t,
0723           typename recorded_trace_t>
0724 inline void display_error(
0725     const typename detector_t::geometry_context gctx, const detector_t &det,
0726     const typename detector_t::name_map &vol_names,
0727     const std::string &test_name, const trajectory_t &test_track,
0728     const truth_trace_t &truth_trace,
0729     const detray::svgtools::styling::style &svg_style,
0730     const std::size_t i_track, [[maybe_unused]] const std::size_t n_tracks,
0731     const recorded_trace_t &recorded_trace = {},
0732     const dindex_range overlap_idx = {detray::detail::invalid_value<dindex>(),
0733                                       detray::detail::invalid_value<dindex>()},
0734     const bool verbose = true) {
0735   // Creating the svg generator for the detector.
0736   detray::svgtools::illustrator il{det, vol_names, svg_style};
0737   il.show_info(true);
0738   il.hide_eta_lines(true);
0739   il.hide_portals(false);
0740   il.hide_passives(false);
0741 
0742   std::string track_type{};
0743   static constexpr auto is_ray{
0744       std::is_same_v<trajectory_t,
0745                      detray::detail::ray<typename detector_t::algebra_type>>};
0746   if constexpr (is_ray) {
0747     track_type = "ray";
0748   } else {
0749     track_type = "helix";
0750   }
0751 
0752   if (verbose) {
0753     DETRAY_ERROR_HOST("\nFailed on " << track_type << ": " << i_track << "/"
0754                                      << n_tracks << "\n"
0755                                      << test_track);
0756   }
0757 
0758   detray::detail::svg_display(gctx, il, truth_trace, test_track,
0759                               track_type + "_" + std::to_string(i_track),
0760                               test_name, recorded_trace, overlap_idx, verbose);
0761 }
0762 
0763 /// Print an intersection trace
0764 template <typename truth_trace_t>
0765 inline std::string print_trace(const truth_trace_t &truth_trace,
0766                                std::size_t n) {
0767   std::stringstream out_stream{};
0768   out_stream << "TRACE NO. " << n << std::endl;
0769 
0770   for (const auto &[idx, record] : detray::views::enumerate(truth_trace)) {
0771     out_stream << "\nRecord " << idx << std::endl;
0772 
0773     out_stream << " -> volume " << record.vol_idx << std::endl;
0774 
0775     const auto pos = record.track_param.pos();
0776     const auto dir = record.track_param.dir();
0777     out_stream << " -> track pos: [" << pos[0] << ", " << pos[1] << ", "
0778                << pos[2] << std::endl;
0779     out_stream << " -> track dir: [" << dir[0] << ", " << dir[1] << ", "
0780                << dir[2] << std::endl;
0781 
0782     out_stream << " -> intersection " << record.intersection << std::endl;
0783   }
0784 
0785   return out_stream.str();
0786 }
0787 
0788 /// Print an adjacency list
0789 inline std::string print_adj(const dvector<dindex> &adjacency_matrix) {
0790   std::size_t dim = static_cast<dindex>(math::sqrt(adjacency_matrix.size()));
0791   std::stringstream out_stream{};
0792 
0793   for (std::size_t i = 0u; i < dim - 1; ++i) {
0794     out_stream << "[>>] Node with index " << i << std::endl;
0795     out_stream << " -> edges: " << std::endl;
0796     for (std::size_t j = 0u; j < dim; ++j) {
0797       const auto degr = adjacency_matrix[dim * i + j];
0798       if (degr == 0) {
0799         continue;
0800       }
0801       std::string n_occur =
0802           degr > 1 ? "\t\t\t\t(" + std::to_string(degr) + "x)" : "";
0803 
0804       // Edge that leads out of the detector world
0805       if (j == dim - 1 && degr != 0) {
0806         out_stream << "    -> leaving world " + n_occur << std::endl;
0807       } else {
0808         out_stream << "    -> " << std::to_string(j) + "\t" + n_occur
0809                    << std::endl;
0810       }
0811     }
0812   }
0813 
0814   return out_stream.str();
0815 }
0816 
0817 }  // namespace detray::detector_scanner