From 46fdb38cd27901a9ea81a1039ebeb0ef54430bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 25 May 2019 03:06:49 +0200 Subject: [PATCH] Math: make batch min() / max() / minmax() ignore NaNs. --- doc/changelog.dox | 6 ++ src/Magnum/Math/FunctionsBatch.h | 75 +++++++++++++---- src/Magnum/Math/Test/FunctionsBatchTest.cpp | 92 ++++++++++++++++++++- 3 files changed, 156 insertions(+), 17 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 3d15114d8..4a1860f3f 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -216,6 +216,12 @@ See also: special types such as @ref Deg or @ref Rad --- the only exception is power functions such as @ref Math::sqrt() or @ref Math::log(), as the resulting unit can't be represented. Those accept only unitless types. +- Batch @ref Math::min(Containers::ArrayView), + @ref Math::max(Containers::ArrayView) and + @ref Math::minmax(Containers::ArrayView) functions now ignore NaNs + in the data, if possible. Use the batch + @ref Math::isNan(Containers::ArrayView) to detect presence of NaN + values if needed. - Changed the way @ref Math::operator<<(Corrade::Utility::Debug&, const BoolVector&) works --- the output now has the same bit order as when constructing it using binary literals diff --git a/src/Magnum/Math/FunctionsBatch.h b/src/Magnum/Math/FunctionsBatch.h index 0dd5a93ef..1c62287ef 100644 --- a/src/Magnum/Math/FunctionsBatch.h +++ b/src/Magnum/Math/FunctionsBatch.h @@ -111,19 +111,55 @@ template inline bool isNan(const T(&array)[size]) { return isNan(Corrade::Containers::arrayView(array)); } +namespace Implementation { + /* Non-floating-point types, the first is a non-NaN for sure */ + template constexpr std::pair firstNonNan(Corrade::Containers::ArrayView range, std::false_type, std::integral_constant) { + return {0, range.front()}; + } + /* Floating-point scalars, return the first that's not NaN */ + template inline std::pair firstNonNan(Corrade::Containers::ArrayView range, std::true_type, std::false_type) { + /* Find the first non-NaN value to compare against. If all are NaN, + return the last value so the following loop in min/max/minmax() + doesn't even execute. */ + for(std::size_t i = 0; i != range.size(); ++i) + if(!isNan(range[i])) return {i, range[i]}; + return {range.size() - 1, range.back()}; + } + /* Floating-point vectors. Try to gather non-NaN values for each component + and exit as soon as all are found (or the input is exhausted). Return + the index of first item with at least one non-NaN value as we need to go + through all at least partially valid values again anyway in order to + apply the min/max/minmax operation. I expect the cases of heavily + NaN-filled vectors (and thus the need to loop twice through most of the + range) to be very rare, so this shouldn't be a problem. */ + template inline std::pair firstNonNan(Corrade::Containers::ArrayView range, std::true_type, std::true_type) { + T out = range[0]; + std::size_t firstValid = 0; + for(std::size_t i = 1; i != range.size(); ++i) { + BoolVector nans = isNan(out); + if(nans.none()) break; + if(nans.all() && firstValid + 1 == i) ++firstValid; + out = Math::lerp(out, range[i], isNan(out)); + } + return {firstValid, out}; + } +} + /** @brief Minimum of a range -If the range is empty, returns default-constructed value. -@see @ref min(T, T) +If the range is empty, returns default-constructed value. NaNs are +ignored, unless the range is all NaNs. +@see @ref min(T, T), @ref isNan(Corrade::Containers::ArrayView) */ template inline T min(Corrade::Containers::ArrayView range) { if(range.empty()) return {}; - T out(range[0]); - for(std::size_t i = 1; i != range.size(); ++i) - out = Math::min(out, range[i]); - return out; + std::pair iOut = Implementation::firstNonNan(range, IsFloatingPoint{}, IsVector{}); + for(++iOut.first; iOut.first != range.size(); ++iOut.first) + iOut.second = Math::min(iOut.second, range[iOut.first]); + + return iOut.second; } /** @overload */ @@ -139,15 +175,18 @@ template inline T min(const T(&array)[size]) { /** @brief Maximum of a range -If the range is empty, returns default-constructed value. +If the range is empty, returns default-constructed value. NaNs are +ignored, unless the range is all NaNs. +@see @ref max(T, T), @ref isNan(Corrade::Containers::ArrayView) */ template inline T max(Corrade::Containers::ArrayView range) { if(range.empty()) return {}; - T out(range[0]); - for(std::size_t i = 1; i != range.size(); ++i) - out = Math::max(out, range[i]); - return out; + std::pair iOut = Implementation::firstNonNan(range, IsFloatingPoint{}, IsVector{}); + for(++iOut.first; iOut.first != range.size(); ++iOut.first) + iOut.second = Math::max(iOut.second, range[iOut.first]); + + return iOut.second; } /** @overload */ @@ -176,15 +215,19 @@ namespace Implementation { /** @brief Minimum and maximum of a range -If the range is empty, returns default-constructed values. -@see @ref Range::Range(const std::pair&) +If the range is empty, returns default-constructed values. NaNs are +ignored, unless the range is all NaNs. +@see @ref minmax(T, T), + @ref Range::Range(const std::pair&), + @ref isNan(Corrade::Containers::ArrayView) */ template inline std::pair minmax(Corrade::Containers::ArrayView range) { if(range.empty()) return {}; - T min{range[0]}, max{range[0]}; - for(std::size_t i = 1; i != range.size(); ++i) - Implementation::minmax(min, max, range[i]); + std::pair iOut = Implementation::firstNonNan(range, IsFloatingPoint{}, IsVector{}); + T min{iOut.second}, max{iOut.second}; + for(++iOut.first; iOut.first != range.size(); ++iOut.first) + Implementation::minmax(min, max, range[iOut.first]); return {min, max}; } diff --git a/src/Magnum/Math/Test/FunctionsBatchTest.cpp b/src/Magnum/Math/Test/FunctionsBatchTest.cpp index 6ba6f4f8c..173272d77 100644 --- a/src/Magnum/Math/Test/FunctionsBatchTest.cpp +++ b/src/Magnum/Math/Test/FunctionsBatchTest.cpp @@ -39,6 +39,9 @@ struct FunctionsBatchTest: Corrade::TestSuite::Tester { void min(); void max(); void minmax(); + + void nanIgnoring(); + void nanIgnoringVector(); }; using namespace Literals; @@ -54,7 +57,11 @@ FunctionsBatchTest::FunctionsBatchTest() { &FunctionsBatchTest::min, &FunctionsBatchTest::max, - &FunctionsBatchTest::minmax}); + &FunctionsBatchTest::minmax, + + &FunctionsBatchTest::nanIgnoring, + &FunctionsBatchTest::nanIgnoringVector, + }); } void FunctionsBatchTest::isInf() { @@ -153,6 +160,89 @@ void FunctionsBatchTest::minmax() { CORRADE_COMPARE(Math::minmax({1.0_radf, 2.0_radf, 3.0_radf}), std::make_pair(1.0_radf, 3.0_radf)); } +void FunctionsBatchTest::nanIgnoring() { + auto oneNan = {1.0f, Constants::nan(), -3.0f}; + auto firstNan = {Constants::nan(), 1.0f, -3.0f}; + auto allNan = {Constants::nan(), Constants::nan(), Constants::nan()}; + + CORRADE_COMPARE(Math::min(oneNan), -3.0f); + CORRADE_COMPARE(Math::min(firstNan), -3.0f); + CORRADE_COMPARE(Math::min(allNan), Constants::nan()); + + CORRADE_COMPARE(Math::max(oneNan), 1.0f); + CORRADE_COMPARE(Math::max(firstNan), 1.0f); + CORRADE_COMPARE(Math::max(allNan), Constants::nan()); + + CORRADE_COMPARE(Math::minmax(oneNan), std::make_pair(-3.0f, 1.0f)); + CORRADE_COMPARE(Math::minmax(firstNan), std::make_pair(-3.0f, 1.0f)); + /* Need to compare this way because of NaNs */ + CORRADE_COMPARE(Math::minmax(allNan).first, Constants::nan()); + CORRADE_COMPARE(Math::minmax(allNan).second, Constants::nan()); +} + +void FunctionsBatchTest::nanIgnoringVector() { + auto oneNan = {Vector2{1.0f, 0.5f}, + Vector2{Constants::nan(), -3.0f}, + Vector2{0.4f, -1.0f}}; + auto firstNan = {Vector2{1.0f, -Constants::nan()}, + Vector2{2.2f, -1.0f}, + Vector2{0.4f, -3.0f}}; + auto nanEveryComponent = {Vector2{0.4f, -Constants::nan()}, + Vector2{Constants::nan(), -1.0f}, + Vector2{2.2f, -3.0f}}; + auto oneComponentNan = {Vector2{Constants::nan(), 1.5f}, + Vector2{Constants::nan(), Constants::nan()}, + Vector2{Constants::nan(), 0.3f}}; + auto firstFullNan = {Vector2{Constants::nan(), Constants::nan()}, + Vector2{1.5f, Constants::nan()}, + Vector2{0.3f, Constants::nan()}}; + auto allNan = {Vector2{Constants::nan(), Constants::nan()}, + Vector2{Constants::nan(), Constants::nan()}, + Vector2{Constants::nan(), Constants::nan()}}; + + CORRADE_COMPARE(Math::min(oneNan), (Vector2{0.4f, -3.0f})); + CORRADE_COMPARE(Math::min(firstNan), (Vector2{0.4f, -3.0f})); + CORRADE_COMPARE(Math::min(nanEveryComponent), (Vector2{0.4f, -3.0f})); + /* Need to compare this way because of NaNs */ + CORRADE_COMPARE(Math::min(oneComponentNan)[0], Constants::nan()); + CORRADE_COMPARE(Math::min(oneComponentNan)[1], 0.3f); + CORRADE_COMPARE(Math::min(firstFullNan)[0], 0.3f); + CORRADE_COMPARE(Math::min(firstFullNan)[1], Constants::nan()); + CORRADE_COMPARE(Math::min(allNan)[0], Constants::nan()); + CORRADE_COMPARE(Math::min(allNan)[1], Constants::nan()); + + CORRADE_COMPARE(Math::max(oneNan), (Vector2{1.0f, 0.5f})); + CORRADE_COMPARE(Math::max(firstNan), (Vector2{2.2f, -1.0f})); + CORRADE_COMPARE(Math::max(nanEveryComponent), (Vector2{2.2f, -1.0f})); + /* Need to compare this way because of NaNs */ + CORRADE_COMPARE(Math::max(oneComponentNan)[0], Constants::nan()); + CORRADE_COMPARE(Math::max(oneComponentNan)[1], 1.5f); + CORRADE_COMPARE(Math::max(firstFullNan)[0], 1.5f); + CORRADE_COMPARE(Math::max(firstFullNan)[1], Constants::nan()); + CORRADE_COMPARE(Math::max(allNan)[0], Constants::nan()); + CORRADE_COMPARE(Math::max(allNan)[1], Constants::nan()); + + CORRADE_COMPARE(Math::minmax(oneNan), std::make_pair( + Vector2{0.4f, -3.0f}, Vector2{1.0f, 0.5f})); + CORRADE_COMPARE(Math::minmax(firstNan), std::make_pair( + Vector2{0.4f, -3.0f}, Vector2{2.2f, -1.0f})); + CORRADE_COMPARE(Math::minmax(nanEveryComponent), std::make_pair( + Vector2{0.4f, -3.0f}, Vector2{2.2f, -1.0f})); + /* Need to compare this way because of NaNs */ + CORRADE_COMPARE(Math::minmax(oneComponentNan).first[0], Constants::nan()); + CORRADE_COMPARE(Math::minmax(oneComponentNan).first[1], 0.3f); + CORRADE_COMPARE(Math::minmax(oneComponentNan).second[0], Constants::nan()); + CORRADE_COMPARE(Math::minmax(oneComponentNan).second[1], 1.5f); + CORRADE_COMPARE(Math::minmax(firstFullNan).first[0], 0.3f); + CORRADE_COMPARE(Math::minmax(firstFullNan).first[1], Constants::nan()); + CORRADE_COMPARE(Math::minmax(firstFullNan).second[0], 1.5f); + CORRADE_COMPARE(Math::minmax(firstFullNan).second[1], Constants::nan()); + CORRADE_COMPARE(Math::minmax(allNan).first[0], Constants::nan()); + CORRADE_COMPARE(Math::minmax(allNan).first[1], Constants::nan()); + CORRADE_COMPARE(Math::minmax(allNan).second[0], Constants::nan()); + CORRADE_COMPARE(Math::minmax(allNan).second[1], Constants::nan()); +} + }}}} CORRADE_TEST_MAIN(Magnum::Math::Test::FunctionsBatchTest)