diff --git a/Core/include/Acts/Utilities/AngleHelpers.hpp b/Core/include/Acts/Utilities/AngleHelpers.hpp index 02feec2323e6..8a38aeffc94f 100644 --- a/Core/include/Acts/Utilities/AngleHelpers.hpp +++ b/Core/include/Acts/Utilities/AngleHelpers.hpp @@ -9,26 +9,68 @@ #pragma once #include +#include namespace Acts::AngleHelpers { +template +struct EtaThetaConversionTraits {}; + +template <> +struct EtaThetaConversionTraits { + static constexpr float minTheta = 1e-6f; + static constexpr float maxTheta = std::numbers::pi_v - minTheta; + + static constexpr float maxEta = 80.0f; + static constexpr float minEta = -maxEta; +}; + +template <> +struct EtaThetaConversionTraits { + static constexpr double minTheta = 1e-12; + static constexpr double maxTheta = std::numbers::pi - minTheta; + + static constexpr double maxEta = 700.0; + static constexpr double minEta = -maxEta; +}; + /// Calculate the pseudorapidity from the polar angle theta. /// +/// This function aims to be FPE safe and returns infinity for theta values +/// outside the floating point precision range. +/// /// @param theta is the polar angle in radian towards the z-axis. /// /// @return the pseudorapidity towards the z-axis. template Scalar etaFromTheta(Scalar theta) { - return -std::log(std::tan(0.5 * theta)); + if (theta <= EtaThetaConversionTraits::minTheta) { + return std::numeric_limits::infinity(); + } + if (theta >= EtaThetaConversionTraits::maxTheta) { + return -std::numeric_limits::infinity(); + } + + return -std::log(std::tan(theta / 2)); } /// Calculate the polar angle theta from the pseudorapidity. /// +/// This function aims to be FPE safe and returns 0/pi for eta values outside +/// the floating point precision range. +/// /// @param eta is the pseudorapidity towards the z-axis. /// /// @return the polar angle in radian towards the z-axis. template Scalar thetaFromEta(Scalar eta) { + if (eta <= EtaThetaConversionTraits::minEta) { + return std::numbers::pi_v; + } + if (eta >= EtaThetaConversionTraits::maxEta) { + return 0; + } + return 2 * std::atan(std::exp(-eta)); } diff --git a/Tests/UnitTests/Core/TrackFinding/TrackSelectorTests.cpp b/Tests/UnitTests/Core/TrackFinding/TrackSelectorTests.cpp index f149010aef8f..af7eef540151 100644 --- a/Tests/UnitTests/Core/TrackFinding/TrackSelectorTests.cpp +++ b/Tests/UnitTests/Core/TrackFinding/TrackSelectorTests.cpp @@ -19,6 +19,7 @@ #include "Acts/Surfaces/PerigeeSurface.hpp" #include "Acts/Surfaces/PlaneSurface.hpp" #include "Acts/TrackFinding/TrackSelector.hpp" +#include "Acts/Utilities/AngleHelpers.hpp" #include @@ -83,10 +84,6 @@ struct MockTrack { TrackStateRange trackStatesReversed() const { return {}; } }; -double thetaFromEta(double eta) { - return 2 * std::atan(std::exp(-eta)); -} - BOOST_AUTO_TEST_SUITE(TrackSelectorTests) std::vector etaValues{-5.0, -4.5, -4.0, -3.5, -3.0, -2.5, -2.0, -1.5, @@ -97,7 +94,7 @@ BOOST_DATA_TEST_CASE(TestSingleBinCase, bdata::make(etaValues), eta) { TrackSelector::EtaBinnedConfig cfgBase; MockTrack baseTrack{}; - baseTrack.m_theta = thetaFromEta(eta); + baseTrack.m_theta = AngleHelpers::thetaFromEta(eta); baseTrack.m_phi = 0.5; baseTrack.m_pt = 0.5; baseTrack.m_loc0 = 0.5; @@ -206,9 +203,9 @@ BOOST_DATA_TEST_CASE(TestSingleBinCase, bdata::make(etaValues), eta) { cfg.cutSets.at(0).etaMin = {-1.0}; TrackSelector selector{cfg}; MockTrack track = baseTrack; - track.m_theta = thetaFromEta(0.5); + track.m_theta = AngleHelpers::thetaFromEta(0.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(-1.1); + track.m_theta = AngleHelpers::thetaFromEta(-1.1); BOOST_CHECK(!selector.isValidTrack(track)); } @@ -218,9 +215,9 @@ BOOST_DATA_TEST_CASE(TestSingleBinCase, bdata::make(etaValues), eta) { cfg.cutSets.at(0).etaMax = {1.0}; TrackSelector selector{cfg}; MockTrack track = baseTrack; - track.m_theta = thetaFromEta(0.5); + track.m_theta = AngleHelpers::thetaFromEta(0.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(1.1); + track.m_theta = AngleHelpers::thetaFromEta(1.1); BOOST_CHECK(!selector.isValidTrack(track)); } @@ -231,11 +228,11 @@ BOOST_DATA_TEST_CASE(TestSingleBinCase, bdata::make(etaValues), eta) { cfg.cutSets.at(0).etaMax = {1.0}; TrackSelector selector{cfg}; MockTrack track = baseTrack; - track.m_theta = thetaFromEta(0.5); + track.m_theta = AngleHelpers::thetaFromEta(0.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(-1.1); + track.m_theta = AngleHelpers::thetaFromEta(-1.1); BOOST_CHECK(!selector.isValidTrack(track)); - track.m_theta = thetaFromEta(1.1); + track.m_theta = AngleHelpers::thetaFromEta(1.1); BOOST_CHECK(!selector.isValidTrack(track)); } @@ -245,14 +242,14 @@ BOOST_DATA_TEST_CASE(TestSingleBinCase, bdata::make(etaValues), eta) { cfg.cutSets.at(0).absEtaMin = {0.2}; TrackSelector selector{cfg}; MockTrack track = baseTrack; - track.m_theta = thetaFromEta(0.5); + track.m_theta = AngleHelpers::thetaFromEta(0.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(-0.5); + track.m_theta = AngleHelpers::thetaFromEta(-0.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(0.1); + track.m_theta = AngleHelpers::thetaFromEta(0.1); BOOST_CHECK(!selector.isValidTrack(track)); - track.m_theta = thetaFromEta(-0.1); + track.m_theta = AngleHelpers::thetaFromEta(-0.1); BOOST_CHECK(!selector.isValidTrack(track)); } @@ -262,14 +259,14 @@ BOOST_DATA_TEST_CASE(TestSingleBinCase, bdata::make(etaValues), eta) { cfg.cutSets.at(0).absEtaMax = {1.0}; TrackSelector selector{cfg}; MockTrack track = baseTrack; - track.m_theta = thetaFromEta(0.5); + track.m_theta = AngleHelpers::thetaFromEta(0.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(-0.5); + track.m_theta = AngleHelpers::thetaFromEta(-0.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(1.1); + track.m_theta = AngleHelpers::thetaFromEta(1.1); BOOST_CHECK(!selector.isValidTrack(track)); - track.m_theta = thetaFromEta(-1.1); + track.m_theta = AngleHelpers::thetaFromEta(-1.1); BOOST_CHECK(!selector.isValidTrack(track)); } @@ -280,19 +277,19 @@ BOOST_DATA_TEST_CASE(TestSingleBinCase, bdata::make(etaValues), eta) { cfg.cutSets.at(0).absEtaMax = {1.0}; TrackSelector selector{cfg}; MockTrack track = baseTrack; - track.m_theta = thetaFromEta(0.5); + track.m_theta = AngleHelpers::thetaFromEta(0.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(-0.5); + track.m_theta = AngleHelpers::thetaFromEta(-0.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(0.1); + track.m_theta = AngleHelpers::thetaFromEta(0.1); BOOST_CHECK(!selector.isValidTrack(track)); - track.m_theta = thetaFromEta(-0.1); + track.m_theta = AngleHelpers::thetaFromEta(-0.1); BOOST_CHECK(!selector.isValidTrack(track)); - track.m_theta = thetaFromEta(1.1); + track.m_theta = AngleHelpers::thetaFromEta(1.1); BOOST_CHECK(!selector.isValidTrack(track)); - track.m_theta = thetaFromEta(-1.1); + track.m_theta = AngleHelpers::thetaFromEta(-1.1); BOOST_CHECK(!selector.isValidTrack(track)); } @@ -373,27 +370,27 @@ BOOST_AUTO_TEST_CASE(TestSingleBinEtaCutByBinEdge) { BOOST_TEST_INFO_SCOPE(selector.config()); MockTrack track{}; - track.m_theta = thetaFromEta(0.0); + track.m_theta = AngleHelpers::thetaFromEta(0.0); BOOST_CHECK(!selector.isValidTrack(track)); - track.m_theta = thetaFromEta(0.5); + track.m_theta = AngleHelpers::thetaFromEta(0.5); BOOST_CHECK(!selector.isValidTrack(track)); // cannot easily check on-edge behavior because of floating point arithmetic // (it won't be exactly 1.0 in selector) - track.m_theta = thetaFromEta(1.01); + track.m_theta = AngleHelpers::thetaFromEta(1.01); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(1.5); + track.m_theta = AngleHelpers::thetaFromEta(1.5); BOOST_CHECK(selector.isValidTrack(track)); - track.m_theta = thetaFromEta(2.0); + track.m_theta = AngleHelpers::thetaFromEta(2.0); BOOST_CHECK(!selector.isValidTrack(track)); } BOOST_AUTO_TEST_CASE(TestMultiBinCuts) { MockTrack baseTrack{}; - baseTrack.m_theta = thetaFromEta(1.0); + baseTrack.m_theta = AngleHelpers::thetaFromEta(1.0); baseTrack.m_phi = 0.5; baseTrack.m_pt = 0.5; baseTrack.m_loc0 = 0.5; @@ -425,7 +422,7 @@ BOOST_AUTO_TEST_CASE(TestMultiBinCuts) { { // exactly at zero MockTrack track = baseTrack; - track.m_theta = thetaFromEta(0.0); + track.m_theta = AngleHelpers::thetaFromEta(0.0); BOOST_CHECK(selector.isValidTrack(track)); @@ -439,7 +436,7 @@ BOOST_AUTO_TEST_CASE(TestMultiBinCuts) { { // first bin MockTrack track = baseTrack; - track.m_theta = thetaFromEta(1.0); + track.m_theta = AngleHelpers::thetaFromEta(1.0); BOOST_CHECK(selector.isValidTrack(track)); @@ -453,8 +450,8 @@ BOOST_AUTO_TEST_CASE(TestMultiBinCuts) { { // first bin edge MockTrack track = baseTrack; - track.m_theta = - thetaFromEta(2.0 - std::numeric_limits::epsilon()); + track.m_theta = AngleHelpers::thetaFromEta( + 2.0 - std::numeric_limits::epsilon()); BOOST_CHECK(selector.isValidTrack(track)); @@ -468,7 +465,7 @@ BOOST_AUTO_TEST_CASE(TestMultiBinCuts) { { // second bin lower edge MockTrack track = baseTrack; - track.m_theta = thetaFromEta(2.0); + track.m_theta = AngleHelpers::thetaFromEta(2.0); BOOST_CHECK(selector.isValidTrack(track)); @@ -488,7 +485,7 @@ BOOST_AUTO_TEST_CASE(TestMultiBinCuts) { { // second bin MockTrack track = baseTrack; - track.m_theta = thetaFromEta(666.0); + track.m_theta = AngleHelpers::thetaFromEta(10.0); track.*prop = -1.1; BOOST_CHECK(selector.isValidTrack(track)); diff --git a/Tests/UnitTests/Core/Utilities/AngleHelpersTests.cpp b/Tests/UnitTests/Core/Utilities/AngleHelpersTests.cpp index 5898986e6d51..78e1252ad18e 100644 --- a/Tests/UnitTests/Core/Utilities/AngleHelpersTests.cpp +++ b/Tests/UnitTests/Core/Utilities/AngleHelpersTests.cpp @@ -6,24 +6,92 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +#include #include #include "Acts/Tests/CommonHelpers/FloatComparisons.hpp" #include "Acts/Utilities/AngleHelpers.hpp" +#include #include +namespace bd = boost::unit_test::data; + using Acts::AngleHelpers::etaFromTheta; +using Acts::AngleHelpers::EtaThetaConversionTraits; using Acts::AngleHelpers::thetaFromEta; BOOST_AUTO_TEST_SUITE(AngleHelpers) BOOST_AUTO_TEST_CASE(EtaThetaConversion) { CHECK_CLOSE_ABS(0.0, etaFromTheta(std::numbers::pi / 2), 1e-6); - CHECK_CLOSE_ABS(1.0, etaFromTheta(thetaFromEta(1.0)), 1e-6); - CHECK_CLOSE_ABS(1.0, thetaFromEta(etaFromTheta(1.0)), 1e-6); } +BOOST_DATA_TEST_CASE(EtaFromThetaRobustness, bd::xrange(0, 1000, 1), exponent) { + { + // check right + + float thetaRight = exponent < 30 ? std::pow(10.0f, -1.0f * exponent) : 0.0f; + + float etaRight = etaFromTheta(thetaRight); + BOOST_CHECK(!std::isnan(etaRight)); + + // check left + + float thetaLeft = std::numbers::pi_v - thetaRight; + + float etaLeft = etaFromTheta(thetaLeft); + BOOST_CHECK(!std::isnan(etaLeft)); + } + + { + // check right + + double thetaRight = exponent < 300 ? std::pow(10.0, -1.0 * exponent) : 0.0; + + double etaRight = etaFromTheta(thetaRight); + BOOST_CHECK(!std::isnan(etaRight)); + + // check left + + double thetaLeft = std::numbers::pi - thetaRight; + + double etaLeft = etaFromTheta(thetaLeft); + BOOST_CHECK(!std::isnan(etaLeft)); + } +} + +BOOST_DATA_TEST_CASE(ThetaFromEtaRobustness, bd::xrange(1.0, 1000.0, 1.0), + etaRight) { + { + // check right + + float thetaRight = thetaFromEta(etaRight); + BOOST_CHECK(!std::isnan(thetaRight)); + + // check left + + float etaLeft = -etaRight; + + float thetaLeft = thetaFromEta(etaLeft); + BOOST_CHECK(!std::isnan(thetaLeft)); + } + + { + // check right + + double thetaRight = thetaFromEta(etaRight); + BOOST_CHECK(!std::isnan(thetaRight)); + + // check left + + double etaLeft = -etaRight; + + double thetaLeft = thetaFromEta(etaLeft); + BOOST_CHECK(!std::isnan(thetaLeft)); + } +} + BOOST_AUTO_TEST_SUITE_END()