Repository: acts-project/detray
Pull request #979: feat: abort messages
Author: login: niermann999
Labels: name: enhancement
description: New feature or request
Created: 2025-04-16T17:53:00Z | Merged: 2025-04-29T12:21:03Z

Allow to pass additional messages to the navigator abort function that contain the reason for the failure. This will make it easier to debug, because it can be seen right away from where `abort` was called and why. The reason for the error is also preserved in the print inspector, which makes it easier to print it to the user in tests outside the debug file.


Diff:
diff --git a/core/include/detray/navigation/direct_navigator.hpp b/core/include/detray/navigation/direct_navigator.hpp
index d6846c8c4d..6afdf56ef6 100644
--- a/core/include/detray/navigation/direct_navigator.hpp
+++ b/core/include/detray/navigation/direct_navigator.hpp
@@ -222,12 +222,18 @@ class direct_navigator {
         }
 
         DETRAY_HOST_DEVICE
-        inline auto abort() -> bool {
+        inline auto abort(const char * = nullptr) -> bool {
             m_status = navigation::status::e_abort;
             m_heartbeat = false;
             return m_heartbeat;
         }
 
+        template <typename debug_msg_generator_t>
+        DETRAY_HOST_DEVICE inline auto abort(const debug_msg_generator_t &)
+            -> bool {
+            return abort();
+        }
+
         DETRAY_HOST_DEVICE
         inline auto exit() -> bool {
             m_status = navigation::status::e_on_target;
diff --git a/core/include/detray/navigation/navigator.hpp b/core/include/detray/navigation/navigator.hpp
index 57f07a8894..e000f238ea 100644
--- a/core/include/detray/navigation/navigator.hpp
+++ b/core/include/detray/navigation/navigator.hpp
@@ -394,15 +394,54 @@ class navigator {
         /// Navigation state that cannot be recovered from. Leave the other
         /// data for inspection.
         ///
+        /// @param custom_msg additional information on the reason for the error
+        ///
         /// @return navigation heartbeat (dead)
         DETRAY_HOST_DEVICE
-        inline auto abort() -> bool {
+        inline auto abort(const char *custom_msg = "Navigator (unkown reason)")
+            -> bool {
             m_status = navigation::status::e_abort;
             m_heartbeat = false;
             // Don't do anything if aborted
             m_trust_level = navigation::trust_level::e_full;
+
+            /// Wrapper around the custom message that a print inspector can
+            /// understand
+            struct message_wrapper {
+                const char *const m_msg{nullptr};
+
+                DETRAY_HOST_DEVICE
+                constexpr const char *operator()() const { return m_msg; }
+            };
+
+            assert(custom_msg != nullptr);
+            run_inspector({}, point3_type{0.f, 0.f, 0.f},
+                          vector3_type{0.f, 0.f, 0.f},
+                          "Aborted: ", message_wrapper{custom_msg});
+
+            return m_heartbeat;
+        }
+
+        /// Navigation state that cannot be recovered from. Leave the other
+        /// data for inspection.
+        ///
+        /// @param debug_msg_generator functor that returns additional
+        ///                            information on the reason for the error
+        ///
+        /// @return navigation heartbeat (dead)
+        template <typename debug_msg_generator_t>
+        requires(!std::same_as<char *, debug_msg_generator_t>)
+            DETRAY_HOST_DEVICE
+            inline auto abort(const debug_msg_generator_t &debug_msg_generator)
+                -> bool {
+            m_status = navigation::status::e_abort;
+            m_heartbeat = false;
+            m_trust_level = navigation::trust_level::e_full;
+
             run_inspector({}, point3_type{0.f, 0.f, 0.f},
-                          vector3_type{0.f, 0.f, 0.f}, "Aborted: ");
+                          vector3_type{0.f, 0.f, 0.f},
+                          "Aborted: ", debug_msg_generator);
+
             return m_heartbeat;
         }
 
@@ -579,6 +618,20 @@ class navigator {
             }
         }
 
+        /// Call the navigation inspector
+        template <typename debug_msg_generator_t>
+        DETRAY_HOST_DEVICE inline void run_inspector(
+            [[maybe_unused]] const navigation::config &cfg,
+            [[maybe_unused]] const point3_type &track_pos,
+            [[maybe_unused]] const vector3_type &track_dir,
+            [[maybe_unused]] const char *message,
+            [[maybe_unused]] const debug_msg_generator_t &msg_gen) {
+            if constexpr (!std::is_same_v<inspector_t,
+                                          navigation::void_inspector>) {
+                m_inspector(*this, cfg, track_pos, track_dir, message, msg_gen);
+            }
+        }
+
         /// Our cache of candidates (intersections with any kind of surface)
         candidate_cache_t m_candidates;
 
@@ -788,7 +841,7 @@ class navigator {
         // Unrecoverable
         if (navigation.trust_level() != navigation::trust_level::e_full ||
             navigation.is_exhausted()) {
-            navigation.abort();
+            navigation.abort("Navigator: No reachable surfaces");
         }
 
         return is_init;
diff --git a/core/include/detray/propagator/actors/aborters.hpp b/core/include/detray/propagator/actors/aborters.hpp
index 944af771ca..fcfcfabf2d 100644
--- a/core/include/detray/propagator/actors/aborters.hpp
+++ b/core/include/detray/propagator/actors/aborters.hpp
@@ -58,7 +58,8 @@ struct pathlimit_aborter : actor {
         // Check the path limit
         if (step_limit <= 0.f) {
             // Stop navigation
-            prop_state._heartbeat &= nav_state.abort();
+            prop_state._heartbeat &=
+                nav_state.abort("Aborter: Maximal path length reached");
         }
 
         // Don't go over the path limit in the next step
@@ -123,7 +124,8 @@ struct momentum_aborter : actor {
 
         if (mag <= abrt_state.p_limit()) {
             // Stop navigation
-            prop_state._heartbeat &= nav_state.abort();
+            prop_state._heartbeat &=
+                nav_state.abort("Aborter: Minimum momentum reached");
         }
     }
 };
@@ -153,7 +155,8 @@ struct target_aborter : actor {
         if (navigation.is_on_surface() &&
             (navigation.barcode() == abrt_state._target_surface) &&
             (stepping.path_length() > 0.f)) {
-            prop_state._heartbeat &= navigation.abort();
+            prop_state._heartbeat &=
+                navigation.abort("Aborter: Reached target surface");
         }
     }
 };
diff --git a/tests/include/detray/test/utils/inspectors.hpp b/tests/include/detray/test/utils/inspectors.hpp
index dbfdbcd802..4e1b99e29d 100644
--- a/tests/include/detray/test/utils/inspectors.hpp
+++ b/tests/include/detray/test/utils/inspectors.hpp
@@ -56,32 +56,38 @@ struct aggregate_inspector {
 
     /// Inspector interface
     template <unsigned int current_id = 0, typename state_type,
-              concepts::point3D point3_t, concepts::vector3D vector3_t>
+              concepts::point3D point3_t, concepts::vector3D vector3_t,
+              typename... Args>
     DETRAY_HOST_DEVICE auto operator()(state_type &state,
                                        const navigation::config &cfg,
                                        const point3_t &pos,
                                        const vector3_t &dir,
-                                       const char *message) {
+                                       const char *message, Args &&... args) {
         // Call inspector
-        std::get<current_id>(_inspectors)(state, cfg, pos, dir, message);
+        std::get<current_id>(_inspectors)(state, cfg, pos, dir, message,
+                                          std::forward<Args>(args)...);
 
         // Next inspector
         if constexpr (current_id <
                       std::tuple_size<inspector_tuple_t>::value - 1) {
-            return operator()<current_id + 1>(state, cfg, pos, dir, message);
+            return operator()<current_id + 1>(state, cfg, pos, dir, message,
+                                              std::forward<Args>(args)...);
         }
     }
 
     /// Inspector interface
-    template <unsigned int current_id = 0, typename state_type>
-    DETRAY_HOST_DEVICE auto operator()(state_type &state, const char *message) {
+    template <unsigned int current_id = 0, typename state_type,
+              typename... Args>
+    DETRAY_HOST_DEVICE auto operator()(state_type &state, const char *message,
+                                       Args &&... args) {
         // Call inspector
         std::get<current_id>(_inspectors)(state, message);
 
         // Next inspector
         if constexpr (current_id <
                       std::tuple_size<inspector_tuple_t>::value - 1) {
-            return operator()<current_id + 1>(state, message);
+            return operator()<current_id + 1>(state, message,
+                                              std::forward<Args>(args)...);
         }
     }
 
@@ -191,12 +197,12 @@ struct object_tracer {
 
     /// Inspector interface
     template <typename state_type, concepts::point3D point3_t,
-              concepts::vector3D vector3_t>
+              concepts::vector3D vector3_t, typename... Args>
     DETRAY_HOST_DEVICE auto operator()(const state_type &state,
                                        const navigation::config &,
                                        const point3_t &pos,
                                        const vector3_t &dir,
-                                       const char * /*message*/) {
+                                       const char * /*message*/, Args &&...) {
 
         // Record the candidate of an encountered object
         if ((is_status(state.status(), navigation_status) || ...)) {
@@ -245,6 +251,8 @@ struct print_inspector {
     using view_type = dvector_view<char>;
     using const_view_type = dvector_view<const char>;
 
+    struct void_generator {};
+
     /// Default constructor
     print_inspector() = default;
 
@@ -272,17 +280,24 @@ struct print_inspector {
     /// Move assignemten operator
     print_inspector &operator=(print_inspector &&other) = default;
 
-    /// Gathers navigation information accross navigator update calls
-    std::stringstream debug_stream{};
-
     /// Inspector interface. Gathers detailed information during navigation
     template <typename state_type, concepts::point3D point3_t,
-              concepts::vector3D vector3_t>
+              concepts::vector3D vector3_t,
+              typename message_generator_t = void_generator>
     auto operator()(const state_type &state, const navigation::config &cfg,
                     const point3_t &track_pos, const vector3_t &track_dir,
-                    const char *message) {
+                    const char *message,
+                    const message_generator_t &msg_gen = {}) {
         std::string msg(message);
-        debug_stream << msg << std::endl;
+        debug_stream << msg;
+        if constexpr (!std::same_as<message_generator_t, void_generator>) {
+            debug_stream << msg_gen();
+
+            if (state.status() == navigation::status::e_abort) {
+                fata_error_msg = msg_gen();
+            }
+        }
+        debug_stream << std::endl;
         debug_stream << "----------------------------------------" << std::endl;
 
         debug_stream << navigation::print_state(state);
@@ -293,10 +308,20 @@ struct print_inspector {
     }
 
     /// Inspector interface. Print basic state information
-    template <typename state_type>
-    auto operator()(const state_type &state, const char *message) {
+    template <typename state_type,
+              typename message_generator_t = void_generator>
+    auto operator()(const state_type &state, const char *message,
+                    const message_generator_t &msg_gen = {}) {
         std::string msg(message);
-        debug_stream << msg << std::endl;
+        debug_stream << msg;
+        if constexpr (!std::same_as<message_generator_t, void_generator>) {
+            debug_stream << msg_gen();
+
+            if (state.status() == navigation::status::e_abort) {
+                fata_error_msg = msg_gen();
+            }
+        }
+        debug_stream << std::endl;
         debug_stream << "----------------------------------------" << std::endl;
 
         debug_stream << navigation::print_state(state);
@@ -306,6 +331,11 @@ struct print_inspector {
 
     /// @returns a string representation of the gathered information
     std::string to_string() const { return debug_stream.str(); }
+
+    /// Gathers navigation information accross navigator update calls
+    std::stringstream debug_stream{};
+    /// Special message that is collected if the navigator hits a fatal error
+    std::string fata_error_msg{""};
 };
 
 }  // namespace navigation
diff --git a/tests/include/detray/test/validation/navigation_validation_utils.hpp b/tests/include/detray/test/validation/navigation_validation_utils.hpp
index 683df5a9bb..332aa32174 100644
--- a/tests/include/detray/test/validation/navigation_validation_utils.hpp
+++ b/tests/include/detray/test/validation/navigation_validation_utils.hpp
@@ -1074,10 +1074,10 @@ auto compare_to_navigation(
 
         // Fatal propagation error: Data unreliable
         if (!success) {
-            std::cout << "ERROR: Propagation failure" << std::endl;
+            std::cout << "ERROR: Propagation aborted! "
+                      << nav_printer.fata_error_msg << std::endl;
 
-            *debug_file << "ERROR: Propagation failure:\n"
-                        << "TEST TRACK " << i;
+            *debug_file << "ERROR: Propagation aborted:" << std::endl;
 
             n_fatal_error++;
         }


Reviews:
user: login: stephenswat
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T12:22:27Z
user: login: niermann999
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T12:55:45Z
user: login: niermann999
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T13:04:03Z
user: login: niermann999
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T13:11:17Z
user: login: stephenswat
body: I trust your judgement that this is the superior solution. :+1: 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T11:33:16Z

Review comments:
diff_hunk: @@ -222,12 +222,17 @@ class direct_navigator {
         }
 
         DETRAY_HOST_DEVICE
-        inline auto abort() -> bool {
+        inline auto abort(const char * = nullptr) -> bool {
path: core/include/detray/navigation/direct_navigator.hpp
user: login: stephenswat
body: Is there a reason this doesn't use the string at all?
created_at: 2025-04-28T12:20:33Z
author_association: MEMBER
diff_hunk: @@ -222,12 +222,17 @@ class direct_navigator {
         }
 
         DETRAY_HOST_DEVICE
-        inline auto abort() -> bool {
+        inline auto abort(const char * = nullptr) -> bool {
             m_status = navigation::status::e_abort;
             m_heartbeat = false;
             return m_heartbeat;
         }
 
+        template <typename G>
+        DETRAY_HOST_DEVICE inline auto abort(const G &) -> bool {
+            return abort();
+        }
path: core/include/detray/navigation/direct_navigator.hpp
user: login: stephenswat
body: What `G` supposed to be here?
created_at: 2025-04-28T12:20:47Z
author_association: MEMBER
diff_hunk: @@ -58,7 +58,8 @@ struct pathlimit_aborter : actor {
         // Check the path limit
         if (step_limit <= 0.f) {
             // Stop navigation
-            prop_state._heartbeat &= nav_state.abort();
+            prop_state._heartbeat &=
+                nav_state.abort("Aborter: Maximal path length reached");
path: core/include/detray/propagator/actors/aborters.hpp
user: login: stephenswat
body: Is there a reason these are strings and not enums? Especially if this is all defined in detray?
created_at: 2025-04-28T12:21:29Z
author_association: MEMBER
diff_hunk: @@ -222,12 +222,17 @@ class direct_navigator {
         }
 
         DETRAY_HOST_DEVICE
-        inline auto abort() -> bool {
+        inline auto abort(const char * = nullptr) -> bool {
path: core/include/detray/navigation/direct_navigator.hpp
user: login: niermann999
body: The direct navigator does not have an inspector, but it still needs a matching call interface to compile
created_at: 2025-04-28T12:55:45Z
author_association: COLLABORATOR
diff_hunk: @@ -58,7 +58,8 @@ struct pathlimit_aborter : actor {
         // Check the path limit
         if (step_limit <= 0.f) {
             // Stop navigation
-            prop_state._heartbeat &= nav_state.abort();
+            prop_state._heartbeat &=
+                nav_state.abort("Aborter: Maximal path length reached");
path: core/include/detray/propagator/actors/aborters.hpp
user: login: niermann999
body: Isn't that just a more complicated way of achieving the same thing, given that atm I just need to know which string to print (and there is only one choice here)? 
Not all aborters will be defined in detray (e.g. ckf and Kalman step aborters live in traccc), so I cannot add their error status codes to the navigator up front unfortunately
created_at: 2025-04-28T13:04:03Z
author_association: COLLABORATOR
diff_hunk: @@ -222,12 +222,17 @@ class direct_navigator {
         }
 
         DETRAY_HOST_DEVICE
-        inline auto abort() -> bool {
+        inline auto abort(const char * = nullptr) -> bool {
             m_status = navigation::status::e_abort;
             m_heartbeat = false;
             return m_heartbeat;
         }
 
+        template <typename G>
+        DETRAY_HOST_DEVICE inline auto abort(const G &) -> bool {
+            return abort();
+        }
path: core/include/detray/navigation/direct_navigator.hpp
user: login: niermann999
body: This is the call interface impl corresponding to the call containing the functor `debug_msg_generator_t` in the navigator. So it stands for "generator" I guess, but I can spell it out
created_at: 2025-04-28T13:11:17Z
author_association: COLLABORATOR

PR comments:
user: login: niermann999
created_at: 2025-04-18T16:52:10Z
body: Based on #975 
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-28T13:14:17Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=acts-project_detray&pullRequest=979) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=acts-project_detray&pullRequest=979&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=acts-project_detray&pullRequest=979&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=acts-project_detray&pullRequest=979&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=acts-project_detray&pullRequest=979&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=acts-project_detray&pullRequest=979&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=acts-project_detray&pullRequest=979)


author_association: NONE
user: login: niermann999
created_at: 2025-04-29T12:20:32Z
body: > I trust your judgement that this is the superior solution. 👍

Maybe I am just confused by what you mean. But lets for now move ahead with this PR
author_association: COLLABORATOR