Skip to content

Commit

Permalink
GH-44915: [C++] Add WithinUlp testing functions (#44906)
Browse files Browse the repository at this point in the history
### 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]>
  • Loading branch information
pitrou authored Dec 9, 2024
1 parent dfb6d0e commit 0d8c8d9
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 1 deletion.
1 change: 1 addition & 0 deletions cpp/src/arrow/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
110 changes: 110 additions & 0 deletions cpp/src/arrow/testing/gtest_util_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
88 changes: 88 additions & 0 deletions cpp/src/arrow/testing/math.cc
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions cpp/src/arrow/testing/math.h
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion cpp/src/arrow/util/string_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <memory>
#include <ostream>
#include <string>
#include <type_traits>
#include <utility>

#include "arrow/util/visibility.h"
Expand All @@ -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>
Expand Down

0 comments on commit 0d8c8d9

Please sign in to comment.