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/definitions/track_parametrization.hpp"
0013 #include "detray/geometry/identifier.hpp"
0014 #include "detray/geometry/surface.hpp"
0015 #include "detray/navigation/caching_navigator.hpp"
0016 #include "detray/propagator/actor_chain.hpp"
0017 #include "detray/propagator/actors/aborters.hpp"
0018 #include "detray/propagator/propagator.hpp"
0019 #include "detray/tracks/free_track_parameters.hpp"
0020 #include "detray/tracks/helix.hpp"
0021 #include "detray/utils/logging.hpp"
0022 
0023 // Detray IO include(s)
0024 #include "detray/io/csv/intersection2D.hpp"
0025 #include "detray/io/csv/track_parameters.hpp"
0026 #include "detray/io/utils/file_handle.hpp"
0027 
0028 // Detray test include(s)
0029 #include "detray/test/utils/inspectors.hpp"
0030 #include "detray/test/utils/step_tracer.hpp"
0031 #include "detray/test/validation/detector_scan_utils.hpp"
0032 #include "detray/test/validation/material_validation_utils.hpp"
0033 #include "detray/test/validation/navigation_validation_config.hpp"
0034 
0035 // Detray plugin include(s)
0036 #include "detray/plugins/svgtools/styling/styling.hpp"
0037 
0038 // Vecmem include(s)
0039 #include <vecmem/memory/host_memory_resource.hpp>
0040 
0041 // System include(s)
0042 #include <algorithm>
0043 #include <iomanip>
0044 #include <iterator>
0045 #include <memory>
0046 #include <ranges>
0047 #include <sstream>
0048 
0049 namespace detray::navigation_validator {
0050 
0051 /// A functor to get the minimum distance to any surface boundary.
0052 struct min_dist_to_boundary {
0053   template <typename mask_group_t, typename idx_range_t, typename point_t>
0054   DETRAY_HOST_DEVICE inline auto operator()(const mask_group_t &mask_group,
0055                                             const idx_range_t idx_range,
0056                                             const point_t &loc_p) const {
0057     using scalar_t = typename mask_group_t::value_type::scalar_type;
0058 
0059     scalar_t min_dist{detail::invalid_value<scalar_t>()};
0060     for (const auto &mask : detray::ranges::subrange(mask_group, idx_range)) {
0061       min_dist = math::min(min_dist, mask.min_dist_to_boundary(loc_p));
0062     }
0063 
0064     return min_dist;
0065   }
0066 };
0067 
0068 /// B-field placeholder for straight-line navigation
0069 struct empty_bfield {};
0070 
0071 /// Struct that keeps track of the number of encountered/skipped surfaces
0072 struct surface_stats {
0073   std::size_t n_portals{0u};
0074   std::size_t n_sensitives{0u};
0075   std::size_t n_passives{0u};
0076 
0077   /// The total number of skipped surfaces
0078   std::size_t n_total() const { return n_portals + n_sensitives + n_passives; }
0079 
0080   /// Count a surface depending on its type
0081   /// @returns true of the surface type was recognized
0082   /// @{
0083   template <typename sf_descriptor_t>
0084   bool count(const sf_descriptor_t &sf_desc, const bool verbose = true) {
0085     return count(sf_desc.identifier(), verbose);
0086   }
0087 
0088   bool count(geometry::identifier geo_id, const bool verbose = true) {
0089     switch (geo_id.id()) {
0090       using enum surface_id;
0091       case e_portal: {
0092         n_portals++;
0093         return true;
0094       }
0095       case e_sensitive: {
0096         n_sensitives++;
0097         return true;
0098       }
0099       case e_passive: {
0100         n_passives++;
0101         return true;
0102       }
0103       default: {
0104         if (verbose) {
0105           DETRAY_ERROR_HOST("Surface type unknown " << geo_id);
0106         }
0107         return false;
0108       }
0109     }
0110   }
0111   /// @}
0112 
0113   /// Count up
0114   surface_stats &operator+=(const surface_stats &other) {
0115     n_portals += other.n_portals;
0116     n_sensitives += other.n_sensitives;
0117     n_passives += other.n_passives;
0118 
0119     return *this;
0120   }
0121 };
0122 
0123 /// Statistics on tracks with holes and extra surfaces
0124 struct track_statistics {
0125   std::size_t n_tracks{0u};
0126   std::size_t n_tracks_w_holes{0u};  //< Number of tracks with missing surface
0127   std::size_t n_tracks_w_extra{0u};  //< Number of tracks with extra surfaces
0128   std::size_t n_good_tracks{0u};     //< Number of tracks that match exactly
0129   std::size_t n_max_missed_per_trk{0u};  //< Max hole count
0130   std::size_t n_max_extra_per_trk{0u};   //< Max count of additional sf
0131 };
0132 
0133 /// Run the propagation and record test data along the way
0134 template <typename stepper_t, typename... actor_ts, typename detector_t,
0135           typename bfield_t = empty_bfield>
0136 inline auto record_propagation(
0137     const typename detector_t::geometry_context ctx,
0138     vecmem::memory_resource *host_mr, const detector_t &det,
0139     const propagation::config &cfg,
0140     const free_track_parameters<typename detector_t::algebra_type> &track,
0141     const pdg_particle<typename detector_t::scalar_type> ptc_hypo =
0142         muon<typename detector_t::scalar_type>(),
0143     const bfield_t &bfield = {},
0144     const navigation::direction nav_dir = navigation::direction::e_forward,
0145     typename actor_chain<actor_ts...>::state_ref_tuple state_tuple = {},
0146     const std::array<dscalar<typename detector_t::algebra_type>, e_bound_size>
0147         &stddevs = {}) {
0148   using algebra_t = typename detector_t::algebra_type;
0149   using scalar_t = dscalar<algebra_t>;
0150 
0151   /// Type that holds the intersection information
0152   using intersection_t = intersection2D<typename detector_t::surface_type,
0153                                         algebra_t, intersection::contains_pos>;
0154 
0155   /// Inspector that records all encountered surfaces
0156   using object_tracer_t =
0157       navigation::object_tracer<intersection_t, dvector,
0158                                 navigation::status::e_on_object,
0159                                 navigation::status::e_on_portal>;
0160   /// Inspector that prints the navigator state from within the
0161   /// navigator's method calls (cannot be done with an actor)
0162   using nav_print_inspector_t = navigation::print_inspector;
0163   /// Aggregation of multiple inspectors
0164   using inspector_t =
0165       aggregate_inspector<object_tracer_t, nav_print_inspector_t>;
0166 
0167   // Navigation with inspection
0168   using navigator_t =
0169       caching_navigator<detector_t, navigation::default_cache_size, inspector_t,
0170                         intersection_t>;
0171 
0172   // Propagator with pathlimit aborter and validation actors
0173   using step_tracer_t = actor::step_tracer<algebra_t, dvector>;
0174   using pathlimit_aborter_t = actor::pathlimit_aborter<scalar_t>;
0175   using material_tracer_t =
0176       material_validator::material_tracer<scalar_t, dvector>;
0177   using actor_chain_t = actor_chain<pathlimit_aborter_t, actor_ts...,
0178                                     step_tracer_t, material_tracer_t>;
0179   using propagator_t = propagator<stepper_t, navigator_t, actor_chain_t>;
0180 
0181   // Propagator
0182   propagator_t prop{cfg};
0183 
0184   // Build actor states to collect data
0185   using updater_state_t = actor::parameter_updater_state<algebra_t>;
0186   static constexpr bool has_param_transport{
0187       detail::has_type_v<updater_state_t, typename actor_chain_t::state_tuple>};
0188 
0189   typename pathlimit_aborter_t::state pathlimit_aborter_state{
0190       cfg.stepping.path_limit};
0191   typename step_tracer_t::state step_tracer_state{*host_mr};
0192   typename material_tracer_t::state mat_tracer_state{*host_mr};
0193 
0194   // Combine all actor states
0195   auto setup_actor_states =
0196       []<std::size_t... indices>(
0197           typename pathlimit_aborter_t::state &s1,
0198           typename step_tracer_t::state &s2,
0199           typename material_tracer_t::state &s3,
0200           typename actor_chain<actor_ts...>::state_ref_tuple &t,
0201           std::index_sequence<indices...> /*ids*/) {
0202         return detray::tie(s1, detail::get<indices>(t)..., s2, s3);
0203       };
0204   constexpr auto idx_seq{std::make_index_sequence<detail::tuple_size_v<
0205       typename actor_chain<actor_ts...>::state_ref_tuple>>{}};
0206 
0207   auto actor_states =
0208       setup_actor_states(pathlimit_aborter_state, step_tracer_state,
0209                          mat_tracer_state, state_tuple, idx_seq);
0210 
0211   std::unique_ptr<typename propagator_t::state> propagation{nullptr};
0212   if constexpr (std::is_same_v<bfield_t, empty_bfield>) {
0213     propagation =
0214         std::make_unique<typename propagator_t::state>(track, det, ctx);
0215   } else {
0216     typename propagator_t::stepper_type::magnetic_field_type bfield_view(
0217         bfield);
0218     propagation = std::make_unique<typename propagator_t::state>(
0219         track, bfield_view, det, ctx);
0220   }
0221 
0222   // Set the initial covariances
0223   if constexpr (has_param_transport) {
0224     if (!stddevs.empty() &&
0225         !std::ranges::all_of(stddevs, [](scalar_t s) { return s == 0.f; })) {
0226       auto &updater_state = detail::get<updater_state_t>(actor_states);
0227       updater_state.init(track);
0228 
0229       // Copy of the curvilinear params
0230       bound_track_parameters<algebra_t> bound_param =
0231           updater_state.bound_params();
0232 
0233       // Smear the covariance
0234       std::random_device rd{};
0235       std::mt19937 generator{rd()};
0236 
0237       for (std::size_t i = 0u; i < e_bound_size; i++) {
0238         // Exclude zero-stddev
0239         if (stddevs[i] != static_cast<scalar_t>(0)) {
0240           bound_param[i] = std::normal_distribution<scalar_t>(
0241               bound_param[i], stddevs[i])(generator);
0242         }
0243 
0244         getter::element(bound_param.covariance(), i, i) =
0245             stddevs[i] * stddevs[i];
0246       }
0247       assert(!bound_param.is_invalid());
0248 
0249       // Copy back into updater state
0250       updater_state =
0251           actor::parameter_updater_state<algebra_t>{cfg, bound_param};
0252     }
0253   }
0254 
0255   // Access to navigation information
0256   auto &nav_inspector = propagation->navigation().inspector();
0257   auto &obj_tracer = nav_inspector.template get<object_tracer_t>();
0258   auto &nav_printer = nav_inspector.template get<navigation::print_inspector>();
0259 
0260   // Access to the stepper information
0261   auto &step_printer = propagation->stepping().inspector();
0262 
0263   // Find the end point and direction of the track (approximately) for
0264   // backward propagation
0265   if (nav_dir == navigation::direction::e_backward) {
0266     using fw_propagator_t =
0267         propagator<stepper_t, navigator_t,
0268                    actor_chain<pathlimit_aborter_t, actor_ts...>>;
0269     std::unique_ptr<typename fw_propagator_t::state> fw_propagation{nullptr};
0270     if constexpr (std::is_same_v<bfield_t, empty_bfield>) {
0271       fw_propagation =
0272           std::make_unique<typename fw_propagator_t::state>(track, det, ctx);
0273     } else {
0274       typename fw_propagator_t::stepper_type::magnetic_field_type bfield_view(
0275           bfield);
0276       fw_propagation = std::make_unique<typename fw_propagator_t::state>(
0277           track, bfield_view, det, ctx);
0278     }
0279 
0280     // Make a deep copy of states for forward propagation, but omit the
0281     // expensive tracers for the forward pass
0282     auto copy_actor_states =
0283         []<std::size_t... indices>(
0284             typename pathlimit_aborter_t::state &s1,
0285             typename actor_chain<actor_ts...>::state_ref_tuple &t,
0286             std::index_sequence<indices...> /*ids*/) {
0287           using fw_state_tuple_t =
0288               typename actor_chain<pathlimit_aborter_t,
0289                                    actor_ts...>::state_tuple;
0290           return fw_state_tuple_t{s1, detail::get<indices>(t)...};
0291         };
0292 
0293     // Setup forward actor states
0294     auto setup_fw_actor_states =
0295         []<std::size_t... indices>(
0296             typename actor_chain<pathlimit_aborter_t, actor_ts...>::state_tuple
0297                 &t,
0298             std::index_sequence<indices...> /*ids*/) {
0299           return detray::tie(detail::get<indices>(t)...);
0300         };
0301 
0302     auto fw_state_tuple =
0303         copy_actor_states(pathlimit_aborter_state, state_tuple, idx_seq);
0304     constexpr auto fw_idx_seq{std::make_index_sequence<
0305         detail::tuple_size_v<decltype(fw_state_tuple)>>{}};
0306     auto fw_actor_states = setup_fw_actor_states(fw_state_tuple, fw_idx_seq);
0307 
0308     // Perform forward propagation
0309     fw_propagation->set_particle(update_particle_hypothesis(ptc_hypo, track));
0310 
0311     // Make sure parameters are transported, even if there is no actor to
0312     // update them and enforce the write back to the updater state
0313     if constexpr (has_param_transport) {
0314       detail::get<updater_state_t>(fw_actor_states).always_update(true);
0315     }
0316 
0317     const bool fw_success =
0318         fw_propagator_t{cfg}.propagate(*fw_propagation, fw_actor_states);
0319 
0320     if (!fw_success) {
0321       DETRAY_ERROR_HOST(
0322           "Could not propagate to end of track to "
0323           "prepare backward propagation");
0324     }
0325 
0326     // Use the result to set up main propagation run
0327     propagation->stepping()() = fw_propagation->stepping()();
0328     propagation->navigation().set_volume(fw_propagation->navigation().volume());
0329 
0330     // If parameter transport is part of the propagation, copy the final
0331     // bound parameters to the backward propagation
0332     if constexpr (has_param_transport) {
0333       auto &updater_state = detail::get<updater_state_t>(actor_states);
0334       updater_state = detail::get<updater_state_t>(fw_actor_states);
0335     }
0336   }
0337 
0338   // Run the propagation
0339   propagation->navigation().set_direction(nav_dir);
0340   propagation->set_particle(update_particle_hypothesis(ptc_hypo, track));
0341   // TODO: Copy the bound params from the forward pass into the actor state
0342   bool success = prop.propagate(*propagation, actor_states);
0343 
0344   return std::make_tuple(success, std::move(obj_tracer),
0345                          std::move(step_tracer_state).release_step_data(),
0346                          std::move(mat_tracer_state).release_material_record(),
0347                          std::move(mat_tracer_state).release_material_steps(),
0348                          std::move(nav_printer), std::move(step_printer));
0349 }
0350 
0351 /// Compare the recorded intersection trace in @param recorded_trace
0352 /// to the truth trace in @param truth_trace
0353 ///
0354 /// This function gathers and displays some statistics about missed and
0355 /// additional surfaces in the recorded trace and provides detailed outputs
0356 /// in case the traces do not match. If a hole is discovered in either trace,
0357 /// a dummy record is added, so that both traces have the same length in the
0358 /// end.
0359 ///
0360 /// @param traj initial track parameters at the beginning of the track
0361 /// @param trk_no number of the track in the sample
0362 /// @param debug_file where to output debugging information to
0363 ///
0364 /// @returns the counts on missed and additional surfaces, matching errors,
0365 /// as well as the collections of the intersections that did not match
0366 template <typename truth_trace_t, typename recorded_trace_t, typename traj_t,
0367           concepts::algebra algebra_t>
0368 auto compare_traces(
0369     const detray::test::navigation_validation_config<algebra_t> &cfg,
0370     truth_trace_t &truth_trace, recorded_trace_t &recorded_trace,
0371     const traj_t &traj, std::size_t trk_no,
0372     std::fstream *debug_file = nullptr) {
0373   using nav_record_t = typename recorded_trace_t::value_type;
0374   using truth_record_t = typename truth_trace_t::value_type;
0375   using intersection_t = typename truth_record_t::intersection_type;
0376 
0377   // Current number of entries to compare (may become more if both traces
0378   // missed several surfaces)
0379   std::size_t max_entries{math::max(recorded_trace.size(), truth_trace.size())};
0380 
0381   // Catch some debug output
0382   std::stringstream debug_stream;
0383   std::stringstream matching_stream;
0384 
0385   // Collect some statistics and additional data
0386   surface_stats missed_stats_nav{};
0387   surface_stats missed_stats_tr{};
0388   bool matching_traces{true};
0389   std::size_t n_errors{0u};
0390   std::vector<intersection_t> missed_intersections{};
0391 
0392   // An error might have occurred occurred
0393   auto handle_counting_error = [&matching_stream, &n_errors](const bool is_OK) {
0394     if (!is_OK) {
0395       matching_stream << "FATAL: Encountered surface of "
0396                          "unknown type in intersection "
0397                          "trace. Validate the geometry";
0398       ++n_errors;
0399     }
0400   };
0401 
0402   // Intersection records at portal boundary might be flipped
0403   // (the portals overlap completely)
0404   auto is_swapped_surfaces = [&recorded_trace, &truth_trace](const long j) {
0405     const auto idx_j{static_cast<std::size_t>(j)};
0406     const std::size_t next_idx{idx_j + 1u};
0407 
0408     if (next_idx < truth_trace.size() && next_idx < recorded_trace.size()) {
0409       const auto &current_nav_inters =
0410           recorded_trace.at(idx_j).intersection.surface().identifier();
0411       const auto &current_truth_inters =
0412           truth_trace.at(idx_j).intersection.surface().identifier();
0413 
0414       const auto &next_nav_inters =
0415           recorded_trace.at(next_idx).intersection.surface().identifier();
0416       const auto &next_truth_inters =
0417           truth_trace.at(next_idx).intersection.surface().identifier();
0418 
0419       return ((current_nav_inters == next_truth_inters) &&
0420               (next_nav_inters == current_truth_inters));
0421     } else {
0422       return false;
0423     }
0424   };
0425 
0426   // Iterate until 'max_entries', because dummy records will be added to the
0427   // shorter trace
0428   for (long i = 0; i < static_cast<long>(max_entries); ++i) {
0429     // Check the records at the current index
0430     const auto idx{static_cast<std::size_t>(i)};
0431 
0432     // If only sensitives are collected and the recorded trace has one
0433     // entry less than the truth trace, the navigator missed the last
0434     // sensitive surface
0435     const bool nav_has_next = (idx < recorded_trace.size());
0436     detray::geometry::identifier nav_inters{};
0437     if (nav_has_next) {
0438       nav_inters = recorded_trace.at(idx).intersection.surface().identifier();
0439     }
0440 
0441     const bool truth_has_next = (idx < truth_trace.size());
0442     detray::geometry::identifier truth_inters{};
0443     if (truth_has_next) {
0444       truth_inters = truth_trace.at(idx).intersection.surface().identifier();
0445     }
0446 
0447     // Check if size of traces is still in sync and records match
0448     bool found_same_surfaces =
0449         (nav_has_next && truth_has_next && (nav_inters == truth_inters));
0450 
0451     matching_traces = matching_traces && found_same_surfaces;
0452 
0453     if (!found_same_surfaces) {
0454       // Count the number of missed surfaces for this mismatch
0455       // Missed by navigator
0456       long missed_pt_nav{0};
0457       long missed_sn_nav{0};
0458       long missed_ps_nav{0};
0459 
0460       // Found in addition by navigation (missed by truth)
0461       long missed_pt_tr{0};
0462       long missed_sn_tr{0};
0463       long missed_ps_tr{0};
0464 
0465       // Count a missed surface by surface type
0466       auto count_one_missed = []<typename insers_t>(
0467                                   const insers_t &intr, long int &missed_pt,
0468                                   long int &missed_sn, long int &missed_ps) {
0469         switch (intr.id()) {
0470           using enum surface_id;
0471           case e_portal: {
0472             missed_pt++;
0473             break;
0474           }
0475           case e_sensitive: {
0476             missed_sn++;
0477             break;
0478           }
0479           case e_passive: {
0480             missed_ps++;
0481             break;
0482           }
0483           default: {
0484             throw std::runtime_error("Unknown surface type during counting");
0485           }
0486         }
0487       };
0488 
0489       // Compare two traces and insert dummy records for any skipped cand.
0490       auto compare_and_equalize =
0491           [&i, &handle_counting_error, &is_swapped_surfaces, &count_one_missed,
0492            &missed_intersections]<typename trace_t, typename other_trace_t>(
0493               trace_t &trace, typename trace_t::iterator last_missed_itr,
0494               other_trace_t &other_trace, surface_stats &missed_stats,
0495               long int &missed_pt, long int &missed_sn, long int &missed_ps) {
0496             // The navigator missed a(multiple) surface(s)
0497             auto first_missed = std::begin(trace) + i;
0498             const auto n_check{std::distance(first_missed, last_missed_itr)};
0499             assert(n_check > 0);
0500 
0501             // Check and record surfaces that were missed: Insert dummy
0502             // records until traces align again
0503             for (long j = i; j < i + n_check; ++j) {
0504               const auto &sfi =
0505                   trace.at(static_cast<std::size_t>(j)).intersection;
0506 
0507               // Portals may be swapped and wrongfully included in the
0508               // range of missed surfaces - skip them
0509               if (sfi.surface().is_portal() && is_swapped_surfaces(j)) {
0510                 ++j;
0511                 continue;
0512               }
0513               // Record the missed intersections for later analysis
0514               missed_intersections.push_back(sfi);
0515               // Insert dummy record to match the truth trace size
0516               using record_t = typename other_trace_t::value_type;
0517               other_trace.insert(other_trace.begin() + i, record_t{});
0518 
0519               // Count this missed intersection depending on sf. type
0520               const bool valid{missed_stats.count(sfi.surface())};
0521               handle_counting_error(valid);
0522 
0523               // Missed surfaces this time
0524               count_one_missed(sfi.surface(), missed_pt, missed_sn, missed_ps);
0525             }
0526 
0527             assert(missed_pt >= 0);
0528             assert(missed_sn >= 0);
0529             assert(missed_ps >= 0);
0530 
0531             const long n_missed{missed_pt + missed_sn + missed_ps};
0532             // We landed here because something was missed
0533             assert(n_missed > 0);
0534 
0535             // Continue checking where trace might match again
0536             i += (n_missed - 1);
0537           };
0538 
0539       // Match the identifiers to find how many surfaces were skipped
0540       //
0541       // If the current nav_inters can be found on the truth trace at a
0542       // later place, the navigator potentially missed the surfaces that
0543       // lie in between (except for swapped portals)
0544       auto search_nav_on_truth = [nav_inters](const truth_record_t &tr) {
0545         return tr.intersection.surface().identifier() == nav_inters;
0546       };
0547       // As above, but this time check if the navigator found additional
0548       // surfaces
0549       auto search_truth_on_nav = [truth_inters](const nav_record_t &nr) {
0550         return nr.intersection.surface().identifier() == truth_inters;
0551       };
0552 
0553       // Check if the portal order is swapped or the surface appears
0554       // later in the truth/navigation trace (this means one or
0555       // multiple surfaces were skipped respectively)
0556       if (is_swapped_surfaces(i)) {
0557         // Was not wrong after all
0558         matching_traces = true;
0559         if (!recorded_trace.at(idx).intersection.surface().is_portal() ||
0560             !truth_trace.at(idx).intersection.surface().is_portal()) {
0561           DETRAY_WARN_HOST(
0562               "Track " << trk_no << ", inters. " << idx
0563                        << ": Surfaces are swapped in trace, "
0564                           "possibly due to overlaps/large mask tolerances.");
0565         }
0566         // Have already checked the next record
0567         ++i;
0568       } else if (auto last_missed_by_nav = std::ranges::find_if(
0569                      std::ranges::begin(truth_trace) + i,
0570                      std::ranges::end(truth_trace), search_nav_on_truth);
0571                  last_missed_by_nav != std::end(truth_trace)) {
0572         // The navigator missed a(multiple) surface(s)
0573         compare_and_equalize(truth_trace, last_missed_by_nav, recorded_trace,
0574                              missed_stats_nav, missed_pt_nav, missed_sn_nav,
0575                              missed_ps_nav);
0576 
0577       } else if (auto last_missed_by_tr = std::ranges::find_if(
0578                      std::ranges::begin(recorded_trace) + i,
0579                      std::ranges::end(recorded_trace), search_truth_on_nav);
0580                  last_missed_by_tr != std::end(recorded_trace)) {
0581         // The navigator found a(multiple) extra surface(s)
0582         compare_and_equalize(recorded_trace, last_missed_by_tr, truth_trace,
0583                              missed_stats_tr, missed_pt_tr, missed_sn_tr,
0584                              missed_ps_tr);
0585 
0586       } else if (!truth_has_next) {
0587         // The nav_inters could not be found on the truth trace, because
0588         // the truth trace does not have anymore records left to check:
0589         // The surface was missed on the truth side
0590         truth_trace.push_back(truth_record_t{});
0591 
0592         const bool valid{missed_stats_tr.count(nav_inters)};
0593         handle_counting_error(valid);
0594         if (valid) {
0595           // Count to output error messages correctly
0596           count_one_missed(nav_inters, missed_pt_tr, missed_sn_tr,
0597                            missed_ps_tr);
0598         }
0599       } else if (!nav_has_next) {
0600         // The truth_inters could not be found on the recorded trace,
0601         // because the recorded trace does not have any records left
0602         // to check: The surface was missed by the navigator
0603         recorded_trace.push_back(nav_record_t{});
0604 
0605         const bool valid{missed_stats_nav.count(truth_inters)};
0606         handle_counting_error(valid);
0607 
0608         if (valid) {
0609           // Count to output error messages correctly
0610           count_one_missed(truth_inters, missed_pt_nav, missed_sn_nav,
0611                            missed_ps_nav);
0612         }
0613       } else {
0614         // Both missed a surface at the same time, as neither record
0615         // can be found in each others traces
0616         bool valid{missed_stats_tr.count(nav_inters)};
0617         valid = valid && missed_stats_nav.count(truth_inters);
0618         handle_counting_error(valid);
0619 
0620         if (valid) {
0621           // Count to output error messages correctly
0622           count_one_missed(truth_inters, missed_pt_nav, missed_sn_nav,
0623                            missed_ps_nav);
0624 
0625           count_one_missed(nav_inters, missed_pt_tr, missed_sn_tr,
0626                            missed_ps_tr);
0627         }
0628       }
0629 
0630       // Print error statements for the user
0631       auto print_err_extra_sf = [&matching_stream, max_entries, i](
0632                                     const std::string &sf_type, long n_sf) {
0633         matching_stream << "\n -> Detray navigator found " << n_sf
0634                         << " additional " << sf_type << "(s) at: " << i << "/"
0635                         << max_entries << " (Inserted dummy record(s))";
0636       };
0637 
0638       auto print_err_missed = [&matching_stream, max_entries, i](
0639                                   const std::string &sf_type, long n_sf) {
0640         matching_stream << "\n -> Detray navigator missed " << n_sf << " "
0641                         << sf_type << "(s) at: " << i << "/" << max_entries
0642                         << ": "
0643                         << " (Inserted dummy record(s))";
0644       };
0645 
0646       if (missed_pt_tr > 0) {
0647         print_err_extra_sf("portal", missed_pt_tr);
0648       }
0649       if (missed_sn_tr > 0) {
0650         print_err_extra_sf("sensitive", missed_sn_tr);
0651       }
0652       if (missed_ps_tr > 0) {
0653         print_err_extra_sf("passive", missed_ps_tr);
0654       }
0655 
0656       if (missed_pt_nav > 0) {
0657         print_err_missed("portal", missed_pt_nav);
0658       }
0659       if (missed_sn_nav > 0) {
0660         print_err_missed("sensitive", missed_sn_nav);
0661       }
0662       if (missed_ps_nav > 0) {
0663         print_err_missed("passive", missed_ps_nav);
0664       }
0665 
0666       // Something must have been missed (unless it was just swapped
0667       // portals)
0668       assert(matching_traces ||
0669              (missed_pt_tr + missed_sn_tr + missed_ps_tr + missed_pt_nav +
0670                   missed_sn_nav + missed_ps_nav >
0671               0));
0672     }
0673 
0674     // Re-evaluate the size after dummy records were added
0675     max_entries = math::max(recorded_trace.size(), truth_trace.size());
0676   }
0677 
0678   matching_stream << "\n\n -> Detray navigator skipped "
0679                   << missed_stats_nav.n_total() << " surface(s) and found "
0680                   << missed_stats_tr.n_total() << " extra surface(s).\n";
0681 
0682   // Fill the debug stream with the final information from both traces
0683   debug_stream << std::left;
0684   for (std::size_t intr_idx = 0u; intr_idx < max_entries; ++intr_idx) {
0685     debug_stream << "   -------Intersection ( " << intr_idx << " )\n";
0686     if (intr_idx < truth_trace.size()) {
0687       debug_stream << std::setw(20) << "\n   Reference: "
0688                    << truth_trace.at(intr_idx).intersection << ", vol id: "
0689                    << truth_trace.at(intr_idx).intersection.surface().volume()
0690                    << std::endl;
0691     } else {
0692       debug_stream << "\n   Reference: -" << std::endl;
0693     }
0694     if (intr_idx < recorded_trace.size()) {
0695       debug_stream << std::setw(20) << "\n   Detray navigator: "
0696                    << recorded_trace.at(intr_idx).intersection << std::endl
0697                    << std::endl;
0698     } else {
0699       debug_stream << "\n   Detray navigator: -\n" << std::endl;
0700     }
0701   }
0702 
0703   // Missed surfaces or errors during matching
0704   const bool any_error{(missed_stats_nav.n_total() != 0u) ||
0705                        (missed_stats_tr.n_total() != 0u) || (n_errors != 0u)};
0706 
0707   // Print debugging information if anything went wrong
0708   if (any_error) {
0709     if (cfg.verbose()) {
0710       DETRAY_ERROR_HOST("--------\n" << matching_stream.str());
0711     }
0712     if (debug_file) {
0713       *debug_file << "\n>>>>>>>>>>>>>>>>>>\nFAILURE\n<<<<<<<<<<<<<<<<<<\n"
0714                   << "\nSUMMARY:\n--------\n"
0715                   << "Track no. " << trk_no << "/" << cfg.n_tracks() << ":\n"
0716                   << traj << matching_stream.str() << "\n--------\n"
0717                   << "\nFull Trace:\n\n"
0718                   << debug_stream.str();
0719     }
0720   }
0721 
0722   // Unknown error occurred during matching
0723   if (n_errors != 0u) {
0724     std::stringstream err_str{};
0725     err_str << "Errors during matching: " << n_errors << "\n"
0726             << matching_stream.str();
0727 
0728     DETRAY_FATAL_HOST(err_str.str());
0729     throw std::runtime_error(err_str.str());
0730   }
0731 
0732   // After inserting the placeholders, do a final check on the trace sizes
0733   const bool is_size{recorded_trace.size() == truth_trace.size()};
0734   if (!is_size) {
0735     std::stringstream err_str{};
0736     err_str << "Intersection traces have different number "
0737                "of surfaces after matching! Please check unmatched elements\n"
0738             << " -> Truth: " << truth_trace.size()
0739             << "\n -> Nav. : " << recorded_trace.size() << "\n"
0740             << debug_stream.str();
0741 
0742     DETRAY_FATAL_HOST(err_str.str());
0743     throw std::runtime_error(err_str.str());
0744   }
0745 
0746   // Multiple missed surfaces are a hint that something might be off with this
0747   // track
0748   if ((missed_stats_nav.n_total() > 1u) && cfg.verbose()) {
0749     DETRAY_WARN_HOST("Detray navigator skipped multiple surfaces: "
0750                      << missed_stats_nav.n_total() << "\n");
0751   }
0752   if ((missed_stats_tr.n_total() > 1u) && cfg.verbose()) {
0753     DETRAY_WARN_HOST("Detray navigator found multiple extra surfaces: "
0754                      << missed_stats_tr.n_total() << "\n");
0755   }
0756 
0757   // Make sure the mismatches were correctly counted
0758   if (!matching_traces) {
0759     bool is_counting_error{false};
0760     // No mismatch counted for traces that don't match: Error
0761     if (missed_stats_nav.n_total() + missed_stats_tr.n_total() == 0) {
0762       is_counting_error = true;
0763 
0764       const std::string msg{
0765           "Difference to truth trace was not counted correctly"};
0766       if (cfg.fail_on_diff()) {
0767         DETRAY_FATAL_HOST("" << msg);
0768         throw std::runtime_error(msg);
0769       } else {
0770         DETRAY_ERROR_HOST("" << msg);
0771       }
0772     }
0773 
0774     // Recount on the final traces, to be absolutely sure.
0775     std::size_t n_miss_truth{0u};
0776     std::size_t n_miss_nav{0u};
0777     for (std::size_t i = 0u; i < truth_trace.size(); ++i) {
0778       const auto truth_desc{truth_trace.at(i).intersection.surface()};
0779       const auto nav_desc{recorded_trace.at(i).intersection.surface()};
0780 
0781       if (truth_desc.identifier().is_invalid()) {
0782         n_miss_truth++;
0783       }
0784       if (nav_desc.identifier().is_invalid()) {
0785         n_miss_nav++;
0786       }
0787       if (truth_desc.identifier() != nav_desc.identifier() &&
0788           !(truth_desc.identifier().is_invalid() ||
0789             nav_desc.identifier().is_invalid())) {
0790         // Swapped in trace, possibly due to overlaps
0791         if (!is_swapped_surfaces(static_cast<long>(i))) {
0792           n_miss_truth++;
0793           n_miss_nav++;
0794         } else {
0795           // If the surfaces were swapped, we already know the next
0796           // element to be correct
0797           ++i;
0798         }
0799       }
0800     }
0801 
0802     if (n_miss_truth != missed_stats_tr.n_total()) {
0803       is_counting_error = true;
0804       const std::string msg{
0805           "Missed truth surfaces not counted correctly. Was " +
0806           std::to_string(missed_stats_tr.n_total()) + ", should be " +
0807           std::to_string(n_miss_truth)};
0808       if (cfg.fail_on_diff()) {
0809         DETRAY_FATAL_HOST("" << msg);
0810         throw std::runtime_error(msg);
0811       } else {
0812         DETRAY_ERROR_HOST("" << msg);
0813       }
0814     }
0815     if (n_miss_nav != missed_stats_nav.n_total()) {
0816       is_counting_error = true;
0817       const std::string msg{
0818           "Missed navigation surfaces not counted correctly. Was " +
0819           std::to_string(missed_stats_nav.n_total()) + ", should be " +
0820           std::to_string(n_miss_nav)};
0821       if (cfg.fail_on_diff()) {
0822         DETRAY_FATAL_HOST("" << msg);
0823         throw std::runtime_error(msg);
0824       } else {
0825         DETRAY_ERROR_HOST("" << msg);
0826       }
0827     }
0828 
0829     if (is_counting_error) {
0830       n_errors++;
0831 
0832       if (debug_file) {
0833         *debug_file << "\n>>>>>>>>>>>>>>>>>>\nCOUNTING "
0834                        "ERROR\n<<<<<<<<<<<<<<<<<<\n"
0835                     << "\nSUMMARY:\n--------\n"
0836                     << "Track no. " << trk_no << "/" << cfg.n_tracks() << ":\n"
0837                     << traj << matching_stream.str() << "\n--------\n"
0838                     << "\nFull Trace:\n\n"
0839                     << debug_stream.str();
0840       }
0841     }
0842   }
0843 
0844   const bool success{!any_error && is_size};
0845   return std::make_tuple(success, missed_stats_nav, missed_stats_tr, n_errors,
0846                          missed_intersections);
0847 }
0848 
0849 /// Write the track positions of a trace @param intersection_traces to a csv
0850 /// file to the path @param track_param_file_name
0851 template <typename record_t>
0852 auto write_tracks(const std::string &track_param_file_name,
0853                   const dvector<dvector<record_t>> &intersection_traces) {
0854   using algebra_t = typename record_t::algebra_type;
0855   using scalar_t = dscalar<algebra_t>;
0856   using track_param_t = free_track_parameters<algebra_t>;
0857 
0858   std::vector<std::vector<std::pair<scalar_t, track_param_t>>> track_params{};
0859 
0860   for (const auto &trace : intersection_traces) {
0861     track_params.push_back({});
0862     track_params.back().reserve(trace.size());
0863 
0864     for (const auto &record : trace) {
0865       track_params.back().emplace_back(
0866           record.charge,
0867           track_param_t{record.pos, 0.f, record.p_mag * record.dir,
0868                         record.charge});
0869     }
0870   }
0871 
0872   // Write to file
0873   io::csv::write_free_track_params(track_param_file_name, track_params);
0874 }
0875 
0876 /// Write the distance between the intersection and the surface boundaries in
0877 /// @param missed_intersections to a csv file at the path @param file_name
0878 template <typename detector_t, typename track_t, typename intersection_t>
0879 auto write_dist_to_boundary(
0880     const detector_t &det, const typename detector_t::name_map &names,
0881     const std::string &file_name,
0882     const std::vector<std::pair<track_t, std::vector<intersection_t>>>
0883         &missed_intersections) {
0884   typename detector_t::geometry_context gctx{};
0885 
0886   // Write to csv file
0887   std::ios_base::openmode io_mode = std::ios::trunc | std::ios::out;
0888   detray::io::file_handle dist_file{file_name, io_mode};
0889   *dist_file << "track_id,volume_id,volume_name,phi,eta,path,dist,inside_wo_"
0890                 "tol,sf_type"
0891              << std::endl;
0892 
0893   for (const auto &[i, entry] :
0894        detray::views::enumerate(missed_intersections)) {
0895     const auto &missed_inters_vec = entry.second;
0896 
0897     for (const auto &missed_sfi : missed_inters_vec) {
0898       const auto &track = entry.first;
0899       const auto sf = geometry::surface{det, missed_sfi.surface().identifier()};
0900       const auto vol = tracking_volume{det, sf.volume()};
0901 
0902       const auto dist =
0903           sf.template visit_mask<min_dist_to_boundary>(missed_sfi.local());
0904       const auto glob_pos = sf.local_to_global(gctx, missed_sfi.local(),
0905                                                track.dir(missed_sfi.path()));
0906 
0907       *dist_file << i << "," << sf.volume() << ", " << vol.name(names) << ","
0908                  << vector::phi(glob_pos) << ", " << vector::eta(glob_pos)
0909                  << "," << missed_sfi.path() << ", " << dist << ", "
0910                  << std::boolalpha << sf.is_inside(missed_sfi.local(), 0.f)
0911                  << ", " << static_cast<int>(sf.shape_id()) << std::endl;
0912     }
0913   }
0914 }
0915 
0916 /// Calculate and print the navigation efficiency
0917 inline auto print_efficiency(std::size_t n_tracks,
0918                              const surface_stats &n_surfaces,
0919                              const surface_stats &n_miss_nav,
0920                              const surface_stats &n_miss_truth,
0921                              std::size_t n_fatal_error,
0922                              std::size_t n_matching_error) {
0923   // Column width in output
0924   constexpr int cw{20};
0925 
0926   // Print general information
0927   if (n_miss_nav.n_total() > 0u || n_miss_truth.n_total() > 0u ||
0928       n_fatal_error > 0u || n_matching_error > 0u) {
0929     std::clog << std::left << "-----------------------------------"
0930               << "Error Statistic:"
0931               << "\nTotal number of tracks: " << n_tracks
0932               << "\n\nTotal number of surfaces: " << n_surfaces.n_total()
0933               << std::setw(cw) << "\n      portals: " << n_surfaces.n_portals
0934               << std::setw(cw)
0935               << "\n      sensitives: " << n_surfaces.n_sensitives
0936               << std::setw(cw) << "\n      passives: " << n_surfaces.n_passives
0937               << "\n\n -> missed by navigator: " << n_miss_nav.n_total()
0938               << std::setw(cw) << "\n      portals: " << n_miss_nav.n_portals
0939               << std::setw(cw)
0940               << "\n      sensitives: " << n_miss_nav.n_sensitives
0941               << std::setw(cw) << "\n      passives: " << n_miss_nav.n_passives
0942               << "\n\n -> found in add. by navigator: "
0943               << n_miss_truth.n_total() << std::setw(cw)
0944               << "\n      portals: " << n_miss_truth.n_portals << std::setw(cw)
0945               << "\n      sensitives: " << n_miss_truth.n_sensitives
0946               << std::setw(cw)
0947               << "\n      passives: " << n_miss_truth.n_passives
0948               << "\n\nFatal propagation failures:   " << n_fatal_error
0949               << "\nErrors during truth matching: " << n_matching_error;
0950   } else {
0951     std::clog << "-----------------------------------\n"
0952               << "Tested " << n_tracks << " tracks: OK\n"
0953               << "total number of surfaces:         " << n_surfaces.n_total();
0954   }
0955 
0956   assert(n_miss_nav.n_total() <= n_surfaces.n_total());
0957   assert(n_miss_nav.n_portals <= n_surfaces.n_portals);
0958   assert(n_miss_nav.n_sensitives <= n_surfaces.n_sensitives);
0959   assert(n_miss_nav.n_passives <= n_surfaces.n_passives);
0960 
0961   /// Print the surface finding efficiency per surface type
0962   auto print_eff = [&n_surfaces](const std::string &sf_type,
0963                                  const std::size_t n_sf,
0964                                  const std::size_t n_missed) {
0965     // How many significant digits to print
0966     const auto n_sig{
0967         2 + static_cast<int>(math::ceil(math::log10(n_surfaces.n_total())))};
0968 
0969     const auto k{static_cast<double>(n_sf - n_missed)};
0970     const auto n{static_cast<double>(n_sf)};
0971 
0972     // Estimate of the surface finding efficiency by the navigator
0973     const auto eff{k / n};
0974 
0975     // Variance
0976     // const double var_binomial{eff * (1. - eff) / n};
0977     const double var_bayesian{(k + 1.) * (k + 2.) / ((n + 2.) * (n + 3.)) -
0978                               std::pow((k + 1.), 2) / std::pow((n + 2.), 2)};
0979 
0980     // In percent
0981     std::clog << "\n"
0982               << sf_type << " finding eff.: " << std::fixed
0983               << std::setprecision(n_sig) << 100. * eff << " \u00b1 "
0984               << 100. * math::sqrt(var_bayesian) << "%";
0985   };
0986 
0987   std::clog << std::endl;
0988   if (n_surfaces.n_portals != 0u) {
0989     print_eff("Portal sf.", n_surfaces.n_portals, n_miss_nav.n_portals);
0990   }
0991   if (n_surfaces.n_sensitives != 0u) {
0992     print_eff("Sensitive sf.", n_surfaces.n_sensitives,
0993               n_miss_nav.n_sensitives);
0994   }
0995   if (n_surfaces.n_passives != 0u) {
0996     print_eff("Passive sf.", n_surfaces.n_passives, n_miss_nav.n_passives);
0997   }
0998 
0999   std::clog << std::endl;
1000   if (n_surfaces.n_total() != 0u) {
1001     print_eff("Surface", n_surfaces.n_total(), n_miss_nav.n_total());
1002   } else {
1003     DETRAY_ERROR_HOST("No surfaces found in truth data!");
1004   }
1005 
1006   std::clog << "\n-----------------------------------\n" << std::endl;
1007 }
1008 
1009 /// Run the propagation and compare to an externally provided truth trace.
1010 ///
1011 /// The resulting statistics on tracks with missed surfaces etc. will be
1012 /// printed to stdout. For any track with mismatches in the surface trace, a
1013 /// detailed report will be dumped into a debug file and an svg showing the
1014 /// track and truth trace in the detector geometry will be provided in ./plots
1015 ///
1016 /// @tparam stepper_t the type of stepper to use (e.g. straight line vs. RKN)
1017 /// @tparam actor_ts types of additional actors (e.g. parameter transport)
1018 ///
1019 /// @param cfg navigation validation config
1020 /// @param host_mr host memory resource
1021 /// @param det the detector
1022 /// @param names voluem names for the detector
1023 /// @param ctx the geometry context
1024 /// @param field_view magnetic field view
1025 /// @param prop_cfg the propagation configuration
1026 /// @param truth_traces coll. of truth traces (one per track)
1027 /// @param tracks coll. of tracks
1028 /// @param state_tuples coll. of actor state tuples for actor_ts (one per track)
1029 /// @param stddevs_per_track coll. standard dev. for bound parameter smearing
1030 ///
1031 /// @returns tuple of track statistics: stats of tracks (holes etc.), stats of
1032 /// encountered surfaces, stats of missed surfaces for navigation, stats of
1033 /// missed surfaces for truth traces, recorded step traces, recorded material
1034 /// traces and integrated material
1035 template <typename stepper_t, typename... actor_ts, typename detector_t,
1036           typename field_view_t, typename intersection_t,
1037           concepts::algebra algebra_t>
1038 auto compare_to_navigation(
1039     const detray::test::navigation_validation_config<algebra_t> &cfg,
1040     vecmem::host_memory_resource &host_mr, const detector_t &det,
1041     const typename detector_t::name_map &names,
1042     const typename detector_t::geometry_context ctx,
1043     const field_view_t field_view, const propagation::config &prop_cfg,
1044     std::vector<dvector<navigation::detail::candidate_record<intersection_t>>>
1045         &truth_traces,
1046     const std::vector<free_track_parameters<algebra_t>> &tracks,
1047     dvector<typename actor_chain<actor_ts...>::state_ref_tuple> state_tuples =
1048         {{}},
1049     const dvector<std::array<dscalar<algebra_t>, e_bound_size>>
1050         &stddevs_per_track = {{0.f}}) {
1051   using scalar_t = dscalar<algebra_t>;
1052 
1053   assert(truth_traces.size() == tracks.size());
1054   assert(!state_tuples.empty());
1055   assert(!stddevs_per_track.empty());
1056 
1057   // Write navigation and stepping debug info if a track fails
1058   std::ios_base::openmode io_mode = std::ios::trunc | std::ios::out;
1059   detray::io::file_handle debug_file{
1060       "navigation_validation_" + cfg.name() + ".txt", io_mode};
1061 
1062   const std::size_t n_samples{truth_traces.size()};
1063 
1064   // Collect some statistics for consistency checking
1065   std::size_t n_trk_holes{0};
1066   std::size_t n_matching_error{0u};
1067   std::size_t n_fatal_error{0u};
1068 
1069   // Collect global track statistics
1070   track_statistics trk_stats{};
1071 
1072   // Total number of encountered surfaces
1073   surface_stats n_surfaces{};
1074   // Missed by navigator
1075   surface_stats n_miss_nav{};
1076   // Missed in truth trace
1077   surface_stats n_miss_truth{};
1078 
1079   // Collect step and material traces for all tracks
1080   std::vector<dvector<detail::step_data<algebra_t>>> step_traces{};
1081   std::vector<material_validator::material_record<scalar_t>> mat_records{};
1082   std::vector<dvector<material_validator::material_params<scalar_t>>>
1083       mat_traces{};
1084 
1085   mat_records.reserve(n_samples);
1086   mat_traces.reserve(n_samples);
1087   step_traces.reserve(n_samples);
1088 
1089   // Run the navigation on every truth trace
1090   for (std::size_t i = 0u; i < n_samples; ++i) {
1091     const auto &track = tracks.at(i);
1092     auto &truth_trace = truth_traces.at(i);
1093     auto state_tuple =
1094         state_tuples.size() > i ? state_tuples.at(i) : state_tuples.at(0);
1095     const auto &stddevs = stddevs_per_track.size() > i
1096                               ? stddevs_per_track.at(i)
1097                               : std::array<scalar_t, e_bound_size>{0.f};
1098 
1099     assert(!truth_traces.empty());
1100 
1101     // Record the propagation through the geometry
1102     auto [success, obj_tracer, step_trace, mat_record, mat_trace, nav_printer,
1103           step_printer] =
1104         record_propagation<stepper_t, actor_ts...>(
1105             ctx, &host_mr, det, prop_cfg, track, cfg.ptc_hypothesis(),
1106             field_view, cfg.navigation_direction(), state_tuple, stddevs);
1107 
1108     // Fatal propagation error: Data unreliable
1109     if (!success) {
1110       std::stringstream strm{};
1111 
1112       strm << "Track " << i << ": Propagation aborted! "
1113            << nav_printer.fata_error_msg << std::endl;
1114 
1115       DETRAY_FATAL_HOST(strm.str());
1116       *debug_file << "FATAL: " << strm.str();
1117 
1118       n_fatal_error++;
1119     }
1120 
1121     // Save material for later comparison
1122     step_traces.push_back(std::move(step_trace));
1123     mat_records.push_back(std::move(mat_record));
1124     mat_traces.push_back(mat_trace);
1125 
1126     // Compare to truth trace
1127     using obj_tracer_t = decltype(obj_tracer);
1128     using record_t = typename obj_tracer_t::candidate_record_t;
1129 
1130     detray::dvector<record_t> recorded_trace{};
1131     recorded_trace.reserve(obj_tracer.trace().size());
1132     // Get only the sensitive surfaces
1133     if (cfg.collect_sensitives_only()) {
1134       for (const auto &rec : obj_tracer.trace()) {
1135         if (rec.intersection.surface().is_sensitive()) {
1136           recorded_trace.push_back(rec);
1137         }
1138       }
1139     } else {
1140       std::ranges::copy(obj_tracer.trace(), std::back_inserter(recorded_trace));
1141     }
1142 
1143     // The recorded trace missed something
1144     if (recorded_trace.size() < truth_trace.size()) {
1145       n_trk_holes++;
1146     }
1147 
1148     // Compare recorded and truth traces and count missed surfaces for both
1149     detray::detail::helix ideal_traj{track, field_view};
1150     auto [traces_match, n_miss_trace_nav, n_miss_trace_truth, n_error_trace,
1151           missed_inters] = compare_traces(cfg, truth_trace, recorded_trace,
1152                                           ideal_traj, i, &(*debug_file));
1153     // Comparison failed or propagation failed
1154     if (!traces_match || !success) {
1155       // Write debug info to file
1156       *debug_file << "TEST TRACK " << i << ":\n\n"
1157                   << nav_printer.to_string() << step_printer.to_string();
1158 
1159       // Create SVGs for traces with mismatches
1160       if (n_miss_trace_truth.n_total() != 0u ||
1161           n_miss_trace_nav.n_total() != 0u || n_error_trace != 0u) {
1162         // Only dump SVG if navigator missed a sf. or an error occurred
1163         if (!cfg.display_only_missed() || (n_miss_trace_nav.n_total() != 0u)) {
1164           detray::detector_scanner::display_error(
1165               ctx, det, names, cfg.name(), ideal_traj, truth_trace,
1166               cfg.svg_style(), i, n_samples, recorded_trace,
1167               {dindex_invalid, dindex_invalid}, cfg.verbose());
1168         }
1169       }
1170     }
1171 
1172     // After dummy records insertion, traces should have the same size
1173     if (truth_trace.size() != recorded_trace.size()) {
1174       throw std::runtime_error(
1175           "ERROR: Track " + std::to_string(i) +
1176           ": Trace comparison failed: Trace do not have the same "
1177           "size after intersection matching");
1178     }
1179 
1180     // Internal error during trace matching
1181     if (n_error_trace != 0u) {
1182       throw std::runtime_error(
1183           "FATAL: Track " + std::to_string(i) +
1184           ": Error during track comparison. Please check log "
1185           "files");
1186     }
1187 
1188     // Add to global statistics
1189     n_miss_nav += n_miss_trace_nav;
1190     n_miss_truth += n_miss_trace_truth;
1191     n_matching_error += n_error_trace;
1192 
1193     // Count the number of surfaces per type in all traces
1194     for (std::size_t j = 0; j < truth_trace.size(); ++j) {
1195       n_surfaces.count(truth_trace[j].intersection.surface(), cfg.verbose());
1196     }
1197 
1198     // Did the navigation miss a sensitive surface? => count a hole
1199     if (n_miss_trace_nav.n_sensitives != 0u) {
1200       trk_stats.n_tracks_w_holes++;
1201     }
1202     if (n_error_trace == 0u && n_miss_trace_nav.n_total() == 0u &&
1203         n_miss_trace_truth.n_total() == 0u) {
1204       trk_stats.n_good_tracks++;
1205     }
1206     // Any additional surfaces found (might have material)
1207     if (n_miss_trace_truth.n_total() != 0u) {
1208       trk_stats.n_tracks_w_extra++;
1209     }
1210 
1211     // Maximum number of missed/extra surfaces over all tracks
1212     trk_stats.n_max_missed_per_trk = math::max(trk_stats.n_max_missed_per_trk,
1213                                                n_miss_trace_nav.n_sensitives);
1214     trk_stats.n_max_extra_per_trk =
1215         math::max(trk_stats.n_max_extra_per_trk, n_miss_trace_truth.n_total());
1216 
1217     // Count the number of tracks that were tested surccessfully
1218     trk_stats.n_tracks++;
1219   }
1220 
1221   // Print results
1222   const auto n_tracks{static_cast<double>(trk_stats.n_tracks)};
1223   const auto n_tracks_w_holes{static_cast<double>(trk_stats.n_tracks_w_holes)};
1224   const auto n_tracks_w_extra{static_cast<double>(trk_stats.n_tracks_w_extra)};
1225   const auto n_good_tracks{static_cast<double>(trk_stats.n_good_tracks)};
1226   const auto n_max_holes_per_trk{
1227       static_cast<double>(trk_stats.n_max_missed_per_trk)};
1228   const auto n_max_extra_per_trk{
1229       static_cast<double>(trk_stats.n_max_extra_per_trk)};
1230 
1231   // Self check
1232   if (static_cast<double>(n_trk_holes) > n_tracks_w_holes) {
1233     throw std::runtime_error("Number of tracks with holes is underestimated!");
1234   }
1235 
1236   // Calculate and display the surface finding efficiency
1237   if (cfg.verbose()) {
1238     print_efficiency(trk_stats.n_tracks, n_surfaces, n_miss_nav, n_miss_truth,
1239                      n_fatal_error, n_matching_error);
1240   }
1241 
1242   // Column width in output
1243   constexpr int cw{35};
1244 
1245   std::clog << std::left << std::setw(cw)
1246             << "No. Tracks with miss. surfaces: " << n_tracks_w_holes << "/"
1247             << n_tracks << " (" << 100. * n_tracks_w_holes / n_tracks << "%)"
1248             << std::endl;
1249   std::clog << std::left << std::setw(cw)
1250             << "No. Tracks with add. surfaces: " << n_tracks_w_extra << "/"
1251             << n_tracks << " (" << 100. * n_tracks_w_extra / n_tracks << "%)"
1252             << std::endl;
1253   std::clog << std::left << std::setw(cw)
1254             << "No. Good Tracks (exact match):  " << n_good_tracks << "/"
1255             << n_tracks << " (" << 100. * n_good_tracks / n_tracks << "%)\n"
1256             << std::endl;
1257   std::clog << std::left << std::setw(cw + 5)
1258             << "Max no. of miss. surfaces per track: " << n_max_holes_per_trk
1259             << " (Mean: "
1260             << ((n_tracks_w_holes == 0)
1261                     ? "-"
1262                     : std::to_string(
1263                           static_cast<double>(n_miss_nav.n_sensitives) /
1264                           n_tracks_w_holes))
1265             << ")" << std::endl;
1266   std::clog << std::left << std::setw(cw + 5)
1267             << "Max no. of add. surfaces per track: " << n_max_extra_per_trk
1268             << " (Mean: "
1269             << ((n_tracks_w_extra == 0)
1270                     ? "-"
1271                     : std::to_string(
1272                           static_cast<double>(n_miss_truth.n_total()) /
1273                           n_tracks_w_extra))
1274             << ")" << std::endl;
1275   std::clog << "\n-----------------------------------" << std::endl;
1276 
1277   return std::tuple{trk_stats,   n_surfaces, n_miss_nav, n_miss_truth,
1278                     step_traces, mat_traces, mat_records};
1279 }
1280 
1281 }  // namespace detray::navigation_validator