Browse Source

Math: make batch min() / max() / minmax() ignore NaNs.

pull/342/head
Vladimír Vondruš 7 years ago
parent
commit
46fdb38cd2
  1. 6
      doc/changelog.dox
  2. 75
      src/Magnum/Math/FunctionsBatch.h
  3. 92
      src/Magnum/Math/Test/FunctionsBatchTest.cpp

6
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<const T>),
@ref Math::max(Containers::ArrayView<const T>) and
@ref Math::minmax(Containers::ArrayView<const T>) functions now ignore NaNs
in the data, if possible. Use the batch
@ref Math::isNan(Containers::ArrayView<const T>) to detect presence of NaN
values if needed.
- Changed the way @ref Math::operator<<(Corrade::Utility::Debug&, const BoolVector<size>&)
works --- the output now has the same bit order as when constructing it
using binary literals

75
src/Magnum/Math/FunctionsBatch.h

@ -111,19 +111,55 @@ template<class T, std::size_t size> 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<class T, bool any> constexpr std::pair<std::size_t, T> firstNonNan(Corrade::Containers::ArrayView<const T> range, std::false_type, std::integral_constant<bool, any>) {
return {0, range.front()};
}
/* Floating-point scalars, return the first that's not NaN */
template<class T> inline std::pair<std::size_t, T> firstNonNan(Corrade::Containers::ArrayView<const T> 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<class T> inline std::pair<std::size_t, T> firstNonNan(Corrade::Containers::ArrayView<const T> 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<T::Size> 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. <em>NaN</em>s are
ignored, unless the range is all <em>NaN</em>s.
@see @ref min(T, T), @ref isNan(Corrade::Containers::ArrayView<const T>)
*/
template<class T> inline T min(Corrade::Containers::ArrayView<const T> 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<std::size_t, T> iOut = Implementation::firstNonNan(range, IsFloatingPoint<T>{}, IsVector<T>{});
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<class T, std::size_t size> 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. <em>NaN</em>s are
ignored, unless the range is all <em>NaN</em>s.
@see @ref max(T, T), @ref isNan(Corrade::Containers::ArrayView<const T>)
*/
template<class T> inline T max(Corrade::Containers::ArrayView<const T> 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<std::size_t, T> iOut = Implementation::firstNonNan(range, IsFloatingPoint<T>{}, IsVector<T>{});
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<VectorType, VectorType>&)
If the range is empty, returns default-constructed values. <em>NaN</em>s are
ignored, unless the range is all <em>NaN</em>s.
@see @ref minmax(T, T),
@ref Range::Range(const std::pair<VectorType, VectorType>&),
@ref isNan(Corrade::Containers::ArrayView<const T>)
*/
template<class T> inline std::pair<T, T> minmax(Corrade::Containers::ArrayView<const T> 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<std::size_t, T> iOut = Implementation::firstNonNan(range, IsFloatingPoint<T>{}, IsVector<T>{});
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};
}

92
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)

Loading…
Cancel
Save