This is an automated email from the ASF dual-hosted git repository.
apitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new 0d8c8d9581 GH-44915: [C++] Add WithinUlp testing functions (#44906)
0d8c8d9581 is described below
commit 0d8c8d9581cc5c2a21881bb8665a634fef07433a
Author: Antoine Pitrou <[email protected]>
AuthorDate: Mon Dec 9 12:08:29 2024 +0100
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 <[email protected]>
Signed-off-by: Antoine Pitrou <[email protected]>
---
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(-)
diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt
index 4e40056839..f1f3b1c30b 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 9b4514197d..daf071c2b3 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 <cmath>
+
+#include <gtest/gtest-spi.h>
#include <gtest/gtest.h>
#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 <typename Float>
+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 <typename Float>
+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 <typename Float>
+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 <typename Float>
+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 0000000000..c3246b1221
--- /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 <cmath>
+#include <limits>
+
+#include <gtest/gtest.h>
+
+#include "arrow/util/logging.h"
+
+namespace arrow {
+namespace {
+
+template <typename Float>
+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<Float>(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 <typename Float>
+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 <typename Float>
+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 0000000000..19001ac177
--- /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 7c05ccd51f..448fb57d7a 100644
--- a/cpp/src/arrow/util/string_builder.h
+++ b/cpp/src/arrow/util/string_builder.h
@@ -20,6 +20,7 @@
#include <memory>
#include <ostream>
#include <string>
+#include <type_traits>
#include <utility>
#include "arrow/util/visibility.h"
@@ -46,7 +47,12 @@ class ARROW_EXPORT StringStreamWrapper {
template <typename Head>
void StringBuilderRecursive(std::ostream& stream, Head&& head) {
- stream << head;
+ if constexpr (std::is_floating_point_v<std::decay_t<Head>>) {
+ // Avoid losing precision when printing floating point numbers
+ stream << std::to_string(head);
+ } else {
+ stream << head;
+ }
}
template <typename Head, typename... Tail>