From 0d8c8d9581cc5c2a21881bb8665a634fef07433a Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Mon, 9 Dec 2024 12:08:29 +0100 Subject: [PATCH] GH-44915: [C++] Add WithinUlp testing functions (#44906) ### Rationale for this change When testing math-related functions, we might want to check that some results are very close to an expected value, but not necessarily exactly equal. ### What changes are included in this PR? Add functions that test whether two floating-point values are within N ulps. ("ulp" stands for "unit in the last place": https://en.wikipedia.org/wiki/Unit_in_the_last_place) ### Are these changes tested? Yes. ### Are there any user-facing changes? Potentially more useful error messages. * GitHub Issue: #44915 Authored-by: Antoine Pitrou Signed-off-by: Antoine Pitrou --- cpp/src/arrow/CMakeLists.txt | 1 + cpp/src/arrow/testing/gtest_util_test.cc | 110 +++++++++++++++++++++++ cpp/src/arrow/testing/math.cc | 88 ++++++++++++++++++ cpp/src/arrow/testing/math.h | 34 +++++++ cpp/src/arrow/util/string_builder.h | 8 +- 5 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 cpp/src/arrow/testing/math.cc create mode 100644 cpp/src/arrow/testing/math.h diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt index 4e40056839ce2..f1f3b1c30b701 100644 --- a/cpp/src/arrow/CMakeLists.txt +++ b/cpp/src/arrow/CMakeLists.txt @@ -674,6 +674,7 @@ set(ARROW_TESTING_SRCS testing/fixed_width_test_util.cc testing/generator.cc testing/gtest_util.cc + testing/math.cc testing/process.cc testing/random.cc testing/util.cc) diff --git a/cpp/src/arrow/testing/gtest_util_test.cc b/cpp/src/arrow/testing/gtest_util_test.cc index 9b4514197d776..daf071c2b36f1 100644 --- a/cpp/src/arrow/testing/gtest_util_test.cc +++ b/cpp/src/arrow/testing/gtest_util_test.cc @@ -15,6 +15,9 @@ // specific language governing permissions and limitations // under the License. +#include + +#include #include #include "arrow/array.h" @@ -23,6 +26,7 @@ #include "arrow/record_batch.h" #include "arrow/tensor.h" #include "arrow/testing/gtest_util.h" +#include "arrow/testing/math.h" #include "arrow/testing/random.h" #include "arrow/type.h" #include "arrow/type_traits.h" @@ -171,4 +175,110 @@ TEST_F(TestTensorFromJSON, FromJSON) { EXPECT_TRUE(tensor_expected->Equals(*result)); } +template +void CheckWithinUlpSingle(Float x, Float y, int n_ulp) { + ARROW_SCOPED_TRACE("x = ", x, ", y = ", y, ", n_ulp = ", n_ulp); + ASSERT_TRUE(WithinUlp(x, y, n_ulp)); +} + +template +void CheckNotWithinUlpSingle(Float x, Float y, int n_ulp) { + ARROW_SCOPED_TRACE("x = ", x, ", y = ", y, ", n_ulp = ", n_ulp); + ASSERT_FALSE(WithinUlp(x, y, n_ulp)); +} + +template +void CheckWithinUlp(Float x, Float y, int n_ulp) { + CheckWithinUlpSingle(x, y, n_ulp); + CheckWithinUlpSingle(y, x, n_ulp); + CheckWithinUlpSingle(x, y, n_ulp + 1); + CheckWithinUlpSingle(y, x, n_ulp + 1); + CheckWithinUlpSingle(-x, -y, n_ulp); + CheckWithinUlpSingle(-y, -x, n_ulp); + + for (int exp : {1, -1, 10, -10}) { + Float x_scaled = std::ldexp(x, exp); + Float y_scaled = std::ldexp(y, exp); + CheckWithinUlpSingle(x_scaled, y_scaled, n_ulp); + CheckWithinUlpSingle(y_scaled, x_scaled, n_ulp); + } +} + +template +void CheckNotWithinUlp(Float x, Float y, int n_ulp) { + CheckNotWithinUlpSingle(x, y, n_ulp); + CheckNotWithinUlpSingle(y, x, n_ulp); + CheckNotWithinUlpSingle(-x, -y, n_ulp); + CheckNotWithinUlpSingle(-y, -x, n_ulp); + if (n_ulp > 1) { + CheckNotWithinUlpSingle(x, y, n_ulp - 1); + CheckNotWithinUlpSingle(y, x, n_ulp - 1); + CheckNotWithinUlpSingle(-x, -y, n_ulp - 1); + CheckNotWithinUlpSingle(-y, -x, n_ulp - 1); + } + + for (int exp : {1, -1, 10, -10}) { + Float x_scaled = std::ldexp(x, exp); + Float y_scaled = std::ldexp(y, exp); + CheckNotWithinUlpSingle(x_scaled, y_scaled, n_ulp); + CheckNotWithinUlpSingle(y_scaled, x_scaled, n_ulp); + } +} + +TEST(TestWithinUlp, Double) { + for (double f : {0.0, 1e-20, 1.0, 2345678.9}) { + CheckWithinUlp(f, f, 1); + CheckWithinUlp(f, f, 42); + } + CheckWithinUlp(-0.0, 0.0, 1); + CheckWithinUlp(1.0, 1.0000000000000002, 1); + CheckWithinUlp(1.0, 1.0000000000000007, 3); + CheckNotWithinUlp(1.0, 1.0000000000000007, 2); + CheckNotWithinUlp(1.0, 1.0000000000000007, 1); + // left and right have a different exponent but are still very close + CheckWithinUlp(1.0, 0.9999999999999999, 1); + CheckWithinUlp(1.0, 0.9999999999999988, 11); + CheckNotWithinUlp(1.0, 0.9999999999999988, 10); + + CheckWithinUlp(123.4567, 123.45670000000015, 11); + CheckNotWithinUlp(123.4567, 123.45670000000015, 10); + + CheckNotWithinUlp(HUGE_VAL, -HUGE_VAL, 10); + CheckNotWithinUlp(12.34, -HUGE_VAL, 10); + CheckNotWithinUlp(12.34, std::nan(""), 10); + CheckNotWithinUlp(12.34, -12.34, 10); + CheckNotWithinUlp(0.0, 1e-20, 10); +} + +TEST(TestWithinUlp, Float) { + for (float f : {0.0f, 1e-8f, 1.0f, 123.456f}) { + CheckWithinUlp(f, f, 1); + CheckWithinUlp(f, f, 42); + } + CheckWithinUlp(-0.0f, 0.0f, 1); + CheckWithinUlp(1.0f, 1.0000001f, 1); + CheckWithinUlp(1.0f, 1.0000013f, 11); + CheckNotWithinUlp(1.0f, 1.0000013f, 10); + // left and right have a different exponent but are still very close + CheckWithinUlp(1.0f, 0.99999994f, 1); + CheckWithinUlp(1.0f, 0.99999934f, 11); + CheckNotWithinUlp(1.0f, 0.99999934f, 10); + + CheckWithinUlp(123.456f, 123.456085f, 11); + CheckNotWithinUlp(123.456f, 123.456085f, 10); + + CheckNotWithinUlp(HUGE_VALF, -HUGE_VALF, 10); + CheckNotWithinUlp(12.34f, -HUGE_VALF, 10); + CheckNotWithinUlp(12.34f, std::nanf(""), 10); + CheckNotWithinUlp(12.34f, -12.34f, 10); +} + +TEST(AssertTestWithinUlp, Basics) { + AssertWithinUlp(123.4567, 123.45670000000015, 11); + AssertWithinUlp(123.456f, 123.456085f, 11); + EXPECT_FATAL_FAILURE(AssertWithinUlp(123.4567, 123.45670000000015, 10), + "not within 10 ulps"); + EXPECT_FATAL_FAILURE(AssertWithinUlp(123.456f, 123.456085f, 10), "not within 10 ulps"); +} + } // namespace arrow diff --git a/cpp/src/arrow/testing/math.cc b/cpp/src/arrow/testing/math.cc new file mode 100644 index 0000000000000..c3246b1221a36 --- /dev/null +++ b/cpp/src/arrow/testing/math.cc @@ -0,0 +1,88 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "arrow/testing/math.h" + +#include +#include + +#include + +#include "arrow/util/logging.h" + +namespace arrow { +namespace { + +template +bool WithinUlpOneWay(Float left, Float right, int n_ulp) { + // The delta between 1.0 and the FP value immediately before it. + // We're using this value because `frexp` returns a mantissa between 0.5 and 1.0. + static const Float kOneUlp = Float(1.0) - std::nextafter(Float(1.0), Float(0.0)); + + DCHECK_GE(n_ulp, 1); + + if (left == 0) { + return left == right; + } + if (left < 0) { + left = -left; + right = -right; + } + + int left_exp; + Float left_mant = std::frexp(left, &left_exp); + Float delta = static_cast(n_ulp) * kOneUlp; + Float lower_bound = std::ldexp(left_mant - delta, left_exp); + Float upper_bound = std::ldexp(left_mant + delta, left_exp); + return right >= lower_bound && right <= upper_bound; +} + +template +bool WithinUlpGeneric(Float left, Float right, int n_ulp) { + if (!std::isfinite(left) || !std::isfinite(right)) { + return left == right; + } + return (std::abs(left) <= std::abs(right)) ? WithinUlpOneWay(left, right, n_ulp) + : WithinUlpOneWay(right, left, n_ulp); +} + +template +void AssertWithinUlpGeneric(Float left, Float right, int n_ulp) { + if (!WithinUlpGeneric(left, right, n_ulp)) { + FAIL() << left << " and " << right << " are not within " << n_ulp << " ulps"; + } +} + +} // namespace + +bool WithinUlp(float left, float right, int n_ulp) { + return WithinUlpGeneric(left, right, n_ulp); +} + +bool WithinUlp(double left, double right, int n_ulp) { + return WithinUlpGeneric(left, right, n_ulp); +} + +void AssertWithinUlp(float left, float right, int n_ulps) { + AssertWithinUlpGeneric(left, right, n_ulps); +} + +void AssertWithinUlp(double left, double right, int n_ulps) { + AssertWithinUlpGeneric(left, right, n_ulps); +} + +} // namespace arrow diff --git a/cpp/src/arrow/testing/math.h b/cpp/src/arrow/testing/math.h new file mode 100644 index 0000000000000..19001ac177b7f --- /dev/null +++ b/cpp/src/arrow/testing/math.h @@ -0,0 +1,34 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include "arrow/testing/visibility.h" + +namespace arrow { + +ARROW_TESTING_EXPORT +bool WithinUlp(float left, float right, int n_ulp); +ARROW_TESTING_EXPORT +bool WithinUlp(double left, double right, int n_ulp); + +ARROW_TESTING_EXPORT +void AssertWithinUlp(float left, float right, int n_ulps); +ARROW_TESTING_EXPORT +void AssertWithinUlp(double left, double right, int n_ulps); + +} // namespace arrow diff --git a/cpp/src/arrow/util/string_builder.h b/cpp/src/arrow/util/string_builder.h index 7c05ccd51f7fd..448fb57d7a79a 100644 --- a/cpp/src/arrow/util/string_builder.h +++ b/cpp/src/arrow/util/string_builder.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "arrow/util/visibility.h" @@ -46,7 +47,12 @@ class ARROW_EXPORT StringStreamWrapper { template void StringBuilderRecursive(std::ostream& stream, Head&& head) { - stream << head; + if constexpr (std::is_floating_point_v>) { + // Avoid losing precision when printing floating point numbers + stream << std::to_string(head); + } else { + stream << head; + } } template