diff --git a/include/rabitqlib/utils/input_checks.hpp b/include/rabitqlib/utils/input_checks.hpp index ba883018..dd888e9c 100644 --- a/include/rabitqlib/utils/input_checks.hpp +++ b/include/rabitqlib/utils/input_checks.hpp @@ -38,7 +38,12 @@ static inline bool all_finite(const T* values, size_t count) { // Largest coordinate magnitude whose squared distances, their products in graph // pruning, and rotated residuals stay inside float32; shared with clustering. +// dim == 0 has no such limit; FLT_MAX avoids dividing by zero and still leaves +// +/-Inf out of range. static inline float max_coordinate_magnitude(size_t dim) { + if (dim == 0) { + return std::numeric_limits::max(); + } return static_cast(std::sqrt( std::sqrt(static_cast(std::numeric_limits::max())) / (64.0 * static_cast(dim)) @@ -49,15 +54,17 @@ static inline float max_coordinate_magnitude(size_t dim) { static inline void check_coordinates( const float* values, size_t count, size_t dim, const char* what ) { - if (!all_finite(values, count)) { - throw std::invalid_argument(std::string(what) + " must contain only finite values"); - } const float bound = max_coordinate_magnitude(dim); size_t out_of_range = 0; for (size_t i = 0; i < count; ++i) { out_of_range += static_cast(!(std::fabs(values[i]) <= bound)); } if (out_of_range != 0) { + if (!all_finite(values, count)) { + throw std::invalid_argument( + std::string(what) + " must contain only finite values" + ); + } throw std::invalid_argument( std::string(what) + " exceeds the safe float32 coordinate range" ); diff --git a/tests/unit/rabitqlib/utils/input_checks_test.cpp b/tests/unit/rabitqlib/utils/input_checks_test.cpp index f0abd877..7758473c 100644 --- a/tests/unit/rabitqlib/utils/input_checks_test.cpp +++ b/tests/unit/rabitqlib/utils/input_checks_test.cpp @@ -12,6 +12,14 @@ namespace rabitqlib { namespace { +template +void expect_message(const Call& call, const std::string& message) { + try { + call(); + ADD_FAILURE() << "expected: " << message; + } catch (const std::invalid_argument& error) { EXPECT_EQ(error.what(), message); } +} + TEST(InputChecksTest, DetectsEveryNonFiniteValueForFloatAndDouble) { const std::vector finite{ 0.0F, @@ -59,12 +67,6 @@ TEST(InputChecksTest, CoordinateBoundMatchesClusteringAndAcceptsItself) { EXPECT_NO_THROW(check_coordinates(values.data(), values.size(), kDim, "data")); EXPECT_NO_THROW(check_coordinates(values.data(), 0, kDim, "data")); - const auto expect_message = [](const auto& call, const std::string& message) { - try { - call(); - ADD_FAILURE() << "expected: " << message; - } catch (const std::invalid_argument& error) { EXPECT_EQ(error.what(), message); } - }; values[kDim - 1] = std::nextafter(bound, std::numeric_limits::infinity()); expect_message( [&] { check_coordinates(values.data(), values.size(), kDim, "data"); }, @@ -88,5 +90,50 @@ TEST(InputChecksTest, CoordinateBoundMatchesClusteringAndAcceptsItself) { ); } +TEST(InputChecksTest, NonFiniteWinsOverOversizedFiniteValues) { + constexpr size_t kDim = 128; + const float bound = max_coordinate_magnitude(kDim); + const float oversized = std::nextafter(bound, std::numeric_limits::infinity()); + for (float non_finite : + {std::numeric_limits::quiet_NaN(), + std::numeric_limits::signaling_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}) { + // The oversized finite value precedes the non-finite value. + std::vector values(3, 0.25F); + values[0] = oversized; + values[2] = non_finite; + expect_message( + [&] { check_coordinates(values.data(), values.size(), kDim, "data"); }, + "data must contain only finite values" + ); + // The non-finite value precedes the oversized finite value. + values[0] = non_finite; + values[2] = oversized; + expect_message( + [&] { check_coordinates(values.data(), values.size(), kDim, "data"); }, + "data must contain only finite values" + ); + } +} + +TEST(InputChecksTest, ZeroDimensionRejectsOnlyNonFiniteValues) { + // dim == 0 has no range limit; every finite value is accepted. + EXPECT_EQ(max_coordinate_magnitude(0), std::numeric_limits::max()); + std::vector values{ + std::numeric_limits::max(), -std::numeric_limits::max(), 0.25F}; + EXPECT_NO_THROW(check_coordinates(values.data(), values.size(), 0, "data")); + for (float non_finite : + {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}) { + values[1] = non_finite; + expect_message( + [&] { check_coordinates(values.data(), values.size(), 0, "data"); }, + "data must contain only finite values" + ); + } +} + } // namespace } // namespace rabitqlib