ACTS
Experiment-independent tracking
Loading...
Searching...
No Matches
root_finding.hpp
1// This file is part of the ACTS project.
2//
3// Copyright (C) 2016 CERN for the benefit of the ACTS project
4//
5// This Source Code Form is subject to the terms of the Mozilla Public
6// License, v. 2.0. If a copy of the MPL was not distributed with this
7// file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
9#pragma once
10
11// Project include(s).
12#include "detray/definitions/algebra.hpp"
13#include "detray/definitions/containers.hpp"
14#include "detray/definitions/detail/qualifiers.hpp"
15#include "detray/definitions/math.hpp"
16#include "detray/definitions/units.hpp"
17#include "detray/navigation/intersection/intersection.hpp"
18#include "detray/navigation/intersection/intersection_config.hpp"
19#include "detray/utils/invalid_values.hpp"
20#include "detray/utils/logging.hpp"
21
22// System include(s).
23#include <algorithm>
24#include <iostream>
25#include <limits>
26#include <sstream>
27#include <stdexcept>
28#include <string>
29
30namespace detray {
31
43template <concepts::scalar scalar_t, typename function_t>
44DETRAY_HOST_DEVICE inline bool expand_bracket(const scalar_t a,
45 const scalar_t b, function_t &f,
46 darray<scalar_t, 2> &bracket,
47 const scalar_t k = 1.f) {
48 if (a == b) {
49 throw std::invalid_argument(
50 "Root bracketing: Not a valid start interval [" + std::to_string(a) +
51 ", " + std::to_string(b) + "]");
52 }
53
54 scalar_t lower{a > b ? b : a};
55 scalar_t upper{a > b ? a : b};
56
57 // Sample function points at interval
58 scalar_t f_l{f(lower)};
59 scalar_t f_u{f(upper)};
60 std::size_t n_tries{0u};
61
62 DETRAY_DEBUG_HOST("Initial bracket: [" << lower << ", " << upper << "]");
63
65 const auto check_bracket = [a, b, &bracket](std::size_t n, scalar_t fl,
66 scalar_t fu, scalar_t l,
67 scalar_t u) {
68 if ((n == 1000u) || !std::isfinite(fl) || !std::isfinite(fu) ||
69 !std::isfinite(l) || !std::isfinite(u)) {
70 DETRAY_VERBOSE_HOST("Could not bracket a root (a="
71 << l << ", b=" << u << ", f(a)=" << fl
72 << ", f(b)=" << fu
73 << ", root might not exist). Running Newton-Raphson "
74 "without bisection.");
75 // Reset value
76 bracket = {a, b};
77 return false;
78 }
79 return true;
80 };
81
82 // If there is no sign change in interval, we don't know if there is a root
83 while (!math::signbit(f_l * f_u)) {
84 // No interval could be found to bracket the root
85 // Might be correct, if there is not root
86 if (!check_bracket(n_tries, f_l, f_u, lower, upper)) {
87 return false;
88 }
89 scalar_t d{k * (upper - lower)};
90 // Make interval larger in the direction where the function is smaller
91 if (math::fabs(f_l) < math::fabs(f_u)) {
92 lower -= d;
93 f_l = f(lower);
94 } else {
95 upper += d;
96 f_u = f(upper);
97 }
98 ++n_tries;
99 }
100
101 if (!check_bracket(n_tries, f_l, f_u, lower, upper)) {
102 return false;
103 } else {
104 bracket = {lower, upper};
105 return true;
106 }
107}
108
120template <typename scalar_t, typename function_t>
121DETRAY_HOST_DEVICE inline std::pair<scalar_t, scalar_t> newton_raphson(
122 function_t &evaluate_func, scalar_t s,
123 const scalar_t convergence_tolerance = 1.f * unit<scalar_t>::um,
124 const std::size_t max_n_tries = 1000u,
125 const scalar_t max_path = 5.f * unit<scalar_t>::m) {
126 constexpr scalar_t inv{detail::invalid_value<scalar_t>()};
127 constexpr scalar_t epsilon{std::numeric_limits<scalar_t>::epsilon()};
128
129 DETRAY_DEBUG_HOST("Initial path estimate: s=" << s << "mm");
130
131 if (math::fabs(s) >= max_path) {
132 DETRAY_VERBOSE_HOST("Initial path estimate outside search area: s=" << s);
133 }
134 if (math::fabs(s) >= inv) {
135 const std::string err_msg{"Initial path estimate invalid"};
136 DETRAY_FATAL_HOST(err_msg);
137 throw std::invalid_argument(err_msg);
138 }
139
140 // Run the iteration on s
141 scalar_t s_prev{0.f};
142 std::size_t n_tries{0u};
143 auto [f_s, df_s] = evaluate_func(s);
144
145 while (math::fabs(s - s_prev) > convergence_tolerance) {
146 // Root already found?
147 if (math::fabs(f_s) < convergence_tolerance) {
148 DETRAY_DEBUG_HOST("Found: s = " << s << "mm, err = " << epsilon << "mm");
149 return std::make_pair(s, epsilon);
150 }
151
152 // No intersection can be found if dividing by zero
153 if (math::fabs(df_s) == 0.f) {
154 DETRAY_ERROR_HOST(
155 "Newton step encountered invalid derivative - skipping");
156 return std::make_pair(inv, inv);
157 }
158
159 // Newton step
160 s_prev = s;
161 s -= f_s / df_s;
162
163 // Update function evaluation
164 std::tie(f_s, df_s) = evaluate_func(s);
165
166 ++n_tries;
167
168 // No intersection found within max number of trials
169 if (n_tries >= max_n_tries) {
170 DETRAY_VERBOSE_HOST("Helix intersector did not converge after "
171 << n_tries << " steps - skipping");
172 return std::make_pair(inv, inv);
173 }
174 }
175 // Final pathlengt to root and latest step size
176 DETRAY_DEBUG_HOST("Found: s = " << s << "mm, err = " << math::fabs(s - s_prev)
177 << "mm");
178 return std::make_pair(s, math::fabs(s - s_prev));
179}
180
192template <concepts::scalar scalar_t, typename function_t>
193DETRAY_HOST_DEVICE inline std::pair<scalar_t, scalar_t> newton_raphson_safe(
194 function_t &evaluate_func, scalar_t s,
195 const scalar_t convergence_tolerance = 1.f * unit<scalar_t>::um,
196 const std::size_t max_n_tries = 1000u,
197 const scalar_t max_path = 5.f * unit<scalar_t>::m) {
198 constexpr scalar_t inv{detail::invalid_value<scalar_t>()};
199 constexpr scalar_t epsilon{std::numeric_limits<scalar_t>::epsilon()};
200
201 DETRAY_DEBUG_HOST("Initial path estimate: s=" << s << "mm");
202
203 // Evaluate the test function at point 'x'
204 auto f = [&evaluate_func](const scalar_t x) {
205 auto [f_x, df_x] = evaluate_func(x);
206
207 return f_x;
208 };
209
210 if (math::fabs(s) >= max_path) {
211 DETRAY_VERBOSE_HOST(
212 "Initial path estimate outside search area: s=" << s << "mm");
213 }
214 if (math::fabs(s) >= inv) {
215 const std::string err_msg{"Initial path estimate invalid"};
216 DETRAY_FATAL_HOST(err_msg);
217 throw std::invalid_argument(err_msg);
218 }
219
220 // Initial bracket (test a certain range around 's')
221 scalar_t a{math::fabs(s) == 0.f ? -0.2f : 0.8f * s};
222 scalar_t b{math::fabs(s) == 0.f ? 0.2f : 1.2f * s};
224 bool is_bracketed = expand_bracket(a, b, f, br);
225
226 // Update initial guess on the root after bracketing
227 s = is_bracketed ? 0.5f * (br[1] + br[0]) : s;
228
229 if (!is_bracketed) {
230 DETRAY_VERBOSE_HOST("Bracketing failed for initial path estimate: s=" << s);
231 } else {
232 // Check bracket
233 [[maybe_unused]] auto [f_a, df_a] = evaluate_func(br[0]);
234 [[maybe_unused]] auto [f_b, df_b] = evaluate_func(br[1]);
235
236 // Bracket is not guaranteed to contain a root
237 if (!math::signbit(f_a * f_b)) {
238 throw std::runtime_error(
239 "Incorrect bracket around root: No sign change!");
240 }
241
242 // No bisection algorithm possible if one bracket boundary is inf
243 // (is already checked in bracketing alg)
244 if ((math::fabs(br[0]) >= inv) || (math::fabs(br[1]) >= inv)) {
245 throw std::runtime_error(
246 "Incorrect bracket around root: Boundary reached inf!");
247 }
248
249 // Root is not within the maximal pathlength
250 bool bracket_outside_tol{math::fabs(s) > max_path &&
251 math::fabs(br[0]) >= max_path &&
252 math::fabs(br[1]) >= max_path};
253 if (bracket_outside_tol) {
254 DETRAY_VERBOSE_HOST("Root outside maximum search area (s = "
255 << s << ", a: " << br[0] << ", b: " << br[1]
256 << ") - skipping");
257 return std::make_pair(inv, inv);
258 }
259
260 // Root already found?
261 if (math::fabs(f_a) < convergence_tolerance) {
262 DETRAY_DEBUG_HOST("Found: s = " << a << "mm, err = " << epsilon << "mm");
263 return std::make_pair(a, epsilon);
264 }
265 if (math::fabs(f_b) < convergence_tolerance) {
266 DETRAY_DEBUG_HOST("Found: s = " << b << "mm, err = " << epsilon << "mm");
267 return std::make_pair(b, epsilon);
268 }
269
270 // Make 'a' the boundary for the negative function value -> easier to
271 // update
272 bool is_lower_a{math::signbit(f_a)};
273 a = br[is_lower_a ? 0u : 1u];
274 b = br[is_lower_a ? 1u : 0u];
275 }
276
277 // Run the iteration on s
278 scalar_t s_prev{0.f};
279 std::size_t n_tries{0u};
280 auto [f_s, df_s] = evaluate_func(s);
281 if (math::fabs(f_s) < convergence_tolerance) {
282 DETRAY_DEBUG_HOST(
283 "Found: s = " << s << "mm, err = " << convergence_tolerance << "mm");
284 return std::make_pair(s, epsilon);
285 }
286 if (math::signbit(f_s)) {
287 a = s;
288 } else {
289 b = s;
290 }
291
292 while (math::fabs(s - s_prev) > convergence_tolerance) {
293 // Does Newton step escape bracket?
294 bool bracket_escape{true};
295 scalar_t s_newton{0.f};
296 if (math::fabs(df_s) != 0.f) {
297 s_newton = s - f_s / df_s;
298 bracket_escape = math::signbit((s_newton - a) * (b - s_newton));
299 }
300
301 // This criterion from Numerical Recipes seems to work, but why?
302 /*const bool slow_convergence{math::fabs(2.f * f_s) >
303 math::fabs((s_prev - s) * df_s)};*/
304
305 // Take a bisection step if it converges faster than Newton
306 // |f(next_newton_s)| > |f(next_bisection_s)|
307 bool slow_convergence{true};
308 // The criterion is only well defined if the step lengths are small
309 if (const scalar_t ds_bisection{0.5f * (a + b) - s};
310 is_bracketed &&
311 (math::fabs(ds_bisection) < 10.f * unit<scalar_t>::mm)) {
312 slow_convergence =
313 (2.f * math::fabs(f_s) > math::fabs(df_s * ds_bisection + f_s));
314 }
315
316 s_prev = s;
317
318 // Run bisection if Newton-Raphson would be poor
319 if (is_bracketed &&
320 (bracket_escape || slow_convergence || math::fabs(df_s) == 0.f)) {
321 // Test the function sign in the middle of the interval
322 s = 0.5f * (a + b);
323 } else {
324 // No intersection can be found if dividing by zero
325 if (!is_bracketed && math::fabs(df_s) == 0.f) {
326 DETRAY_VERBOSE_HOST("Newton step encountered invalid derivative at s="
327 << s << " after " << n_tries
328 << " steps - skipping");
329
330 return std::make_pair(inv, inv);
331 }
332
333 s = s_newton;
334 }
335
336 // Update function and bracket
337 std::tie(f_s, df_s) = evaluate_func(s);
338 if (is_bracketed && math::signbit(f_s)) {
339 a = s;
340 } else {
341 b = s;
342 }
343
344 // Converges to a point outside the search space - early stop
345 if (math::fabs(s) > max_path && math::fabs(s_prev) > max_path &&
346 ((a < -max_path && b < -max_path) || (a > max_path && b > max_path))) {
347 DETRAY_VERBOSE_HOST("WARNING: Root finding left the search space at (s = "
348 << s << ", a: " << a << ", b: " << b << ") after "
349 << n_tries << " steps - skipping");
350
351 return std::make_pair(inv, inv);
352 }
353
354 ++n_tries;
355
356 // No intersection found within max number of trials
357 if (n_tries >= max_n_tries) {
358 // Should have found the root
359 if (is_bracketed) {
360 std::stringstream err_str{};
361 err_str << "Helix intersector did not find root for s=" << s << " in ["
362 << a << ", " << b << "]";
363
364 DETRAY_FATAL_HOST(err_str.str());
365 throw std::runtime_error(err_str.str());
366 } else {
367 DETRAY_VERBOSE_HOST("Helix intersector did not converge after "
368 << n_tries
369 << " steps unbracketed search - skipping");
370 }
371 return std::make_pair(inv, inv);
372 }
373 }
374 // Final pathlengt to root and latest step size
375 DETRAY_DEBUG_HOST("Found: s = " << s << "mm, err = " << math::fabs(s - s_prev)
376 << "mm");
377 return std::make_pair(s, math::fabs(s - s_prev));
378}
379
389template <typename intersection_t, concepts::algebra algebra_t,
390 typename surface_descr_t, typename mask_t, typename trajectory_t,
391 concepts::transform3D transform3_t, concepts::scalar scalar_t>
392DETRAY_HOST_DEVICE constexpr void resolve_mask(
393 intersection_t &is, const trajectory_t &traj,
394 const intersection_point_err<algebra_t> &ip, const surface_descr_t sf_desc,
395 const mask_t &mask, const transform3_t &trf,
396 const intersection::config &intr_cfg,
397 const scalar_t /*external_mask_tol*/ = 0.f) {
398 assert((intr_cfg.min_mask_tolerance == intr_cfg.max_mask_tolerance) &&
399 "Helix intersectors use only one mask tolerance value");
400
401 // Build intersection struct from test trajectory, if the distance is valid
402 if (!detail::is_invalid_value(ip.path)) {
403 is.set_path(ip.path);
404 is.set_local(
405 mask_t::to_local_frame3D(trf, traj.pos(ip.path), traj.dir(ip.path)));
406
407 const scalar_t cos_incidence_angle = vector::dot(
408 mask_t::get_local_frame().normal(trf, is.local()), traj.dir(ip.path));
409
410 scalar_t tol{sf_desc.is_portal() ? 0.f : intr_cfg.min_mask_tolerance};
411 // If tolerance is inf, use tolerance estimation (intr_cfg is 'float'!)
412 if (tol >= detail::invalid_value<float>()) {
413 // Due to floating point errors this can be negative if cos ~ 1
414 const scalar_t sin_inc2{
415 math::fabs(1.f - cos_incidence_angle * cos_incidence_angle)};
416
417 tol = math::fabs(ip.path_err * math::sqrt(sin_inc2));
418 }
419 // Make sure the tol. has been estimated/configured in a sensible way
420 assert(!math::signbit(tol));
421 assert(tol < 1000.f * unit<scalar_t>::mm);
422
423 is.set_status(mask.is_inside(is.local(), tol)
424 ? intersection::status::e_inside
425 : intersection::status::e_outside);
426 is.set_surface(sf_desc);
427 is.set_direction(!math::signbit(ip.path));
428 is.set_volume_link(mask.volume_link());
429 } else {
430 // Not a valid intersection
431 is.set_status(intersection::status::e_outside);
432 }
433
434 std::string intr_status{"unknown"};
435 if (is.is_inside()) {
436 intr_status = "inside";
437 } else if (is.is_edge()) {
438 intr_status = "edge";
439 } else {
440 intr_status = "outside";
441 }
442
443 DETRAY_DEBUG_HOST("Intersection status: " << intr_status);
444}
445
446} // namespace detray
Mask a region on a surface and link it to a volume.
Definition mask.hpp:46
constexpr dbool< algebra_t > is_inside(const point_t &loc_p, const scalar_type tol=std::numeric_limits< scalar_type >::epsilon()) const
Definition mask.hpp:226
auto volume_link() const -> const links_type &
Definition mask.hpp:255
Detray include(s).
Definition array_operators.hpp:18
constexpr void resolve_mask(intersection_t &is, const trajectory_t &traj, const intersection_point< algebra_t, point_t, intersection::contains_pos > &ip, const surface_descr_t sf_desc, const mask_t &mask, const transform3_t &trf, const intersection::config &cfg={}, const scalar_t external_mask_tolerance=0.f)
Fill an intersection with the result of the intersection alg.
Definition intersector_base.hpp:189
std::pair< scalar_t, scalar_t > newton_raphson_safe(function_t &evaluate_func, scalar_t s, const scalar_t convergence_tolerance=1.f *unit< scalar_t >::um, const std::size_t max_n_tries=1000u, const scalar_t max_path=5.f *unit< scalar_t >::m)
Find a root using the Newton-Raphson and Bisection algorithms.
Definition root_finding.hpp:193
std::array< value_t, kDIM > darray
Fixed-size array container.
Definition containers.hpp:30
std::pair< scalar_t, scalar_t > newton_raphson(function_t &evaluate_func, scalar_t s, const scalar_t convergence_tolerance=1.f *unit< scalar_t >::um, const std::size_t max_n_tries=1000u, const scalar_t max_path=5.f *unit< scalar_t >::m)
Find a root using the Newton-Raphson algorithm.
Definition root_finding.hpp:121
bool expand_bracket(const scalar_t a, const scalar_t b, function_t &f, darray< scalar_t, 2 > &bracket, const scalar_t k=1.f)
Try to find a bracket around a root.
Definition root_finding.hpp:44
Intersector configuration.
Definition intersection_config.hpp:21
float max_mask_tolerance
Maximal tolerance: loose tolerance when still far away from surface.
Definition intersection_config.hpp:27
float min_mask_tolerance
Tolerance on the mask 'is_inside' check:Minimal tolerance: ~ position uncertainty on surface.
Definition intersection_config.hpp:25
Result of intersector: Distance, intersection point (2D or 3D, local/global) and error estimate on di...
Definition intersection.hpp:150
scalar_type path_err
Error estimation on the path.
Definition intersection.hpp:202
static constexpr scalar_t um
Length, native unit mm.
Definition units.hpp:25
static constexpr scalar_t mm
Native length unit, the millimetre.
Definition units.hpp:27
static constexpr scalar_t m
Metre expressed in millimetres.
Definition units.hpp:31