Skip to content

[Core] Add Utility for Floating Point Comparison #13433

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions kratos/tests/cpp_tests/utilities/test_comparison.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// | / |
// ' / __| _` | __| _ \ __|
// . \ | ( | | ( |\__ `
// _|\_\_| \__,_|\__|\___/ ____/
// Multi-Physics
//
// License: BSD License
// Kratos default license: kratos/license.txt
//
// Main authors: Máté Kelemen
//

// Project includes
#include "testing/testing.h"
#include "utilities/comparison.h" // Comparison


namespace Kratos::Testing {


KRATOS_TEST_CASE_IN_SUITE(IntegerComparison, KratosCoreFastSuite)
{
Comparison<int>::Equal equality_comparison;
Comparison<int>::Less ordering;

for (const auto& [left, right, equality_reference, ordering_reference] :
std::vector<std::tuple<int,int,bool,bool>> {{-4, -4, true, false},
{-2, -4, false, false},
{-1, -4, false, false},
{ 0, -4, false, false},
{ 1, -4, false, false},
{ 2, -4, false, false},
{ 4, -4, false, false},

{-4, -2, false, true},
{-2, -2, true, false},
{-1, -2, false, false},
{ 0, -2, false, false},
{ 1, -2, false, false},
{ 2, -2, false, false},
{ 4, -2, false, false},

{-4, -1, false, true},
{-2, -1, false, true},
{-1, -1, true, false},
{ 0, -1, false, false},
{ 1, -1, false, false},
{ 2, -1, false, false},
{ 4, -1, false, false},

{-4, 0, false, true},
{-2, 0, false, true},
{-1, 0, false, true},
{ 0, 0, true, false},
{ 1, 0, false, false},
{ 2, 0, false, false},
{ 4, 0, false, false},

{-4, 1, false, true},
{-2, 1, false, true},
{-1, 1, false, true},
{ 0, 1, false, true},
{ 1, 1, true, false},
{ 2, 1, false, false},
{ 4, 1, false, false},

{-4, 2, false, true},
{-2, 2, false, true},
{-1, 2, false, true},
{ 0, 2, false, true},
{ 1, 2, false, true},
{ 2, 2, true, false},
{ 4, 2, false, false},

{-4, 4, false, true},
{-2, 4, false, true},
{-1, 4, false, true},
{ 0, 4, false, true},
{ 1, 4, false, true},
{ 2, 4, false, true},
{ 4, 4, true, false}}) {
KRATOS_EXPECT_TRUE(equality_comparison(left, right) == equality_reference);
KRATOS_EXPECT_TRUE(ordering(left, right) == ordering_reference);
}
}


KRATOS_TEST_CASE_IN_SUITE(FloatComparison, KratosCoreFastSuite)
{
Comparison<double>::Equal equality_comparison(1.0, 0.5);
Comparison<double>::Less ordering(1.0, 0.5);

for (const auto& [left, right, equality_reference, ordering_reference] :
std::vector<std::tuple<double,double,bool,bool>> {{-4.0, -4.0, true, false},
{-2.0, -4.0, true, false},
{-1.0, -4.0, false, false},
{ 0.0, -4.0, false, false},
{ 1.0, -4.0, false, false},
{ 2.0, -4.0, false, false},
{ 4.0, -4.0, false, false},

{-4.0, -2.0, true, false},
{-2.0, -2.0, true, false},
{-1.0, -2.0, true, false},
{ 0.0, -2.0, false, false},
{ 1.0, -2.0, false, false},
{ 2.0, -2.0, false, false},
{ 4.0, -2.0, false, false},

{-4.0, -1.0, false, true},
{-2.0, -1.0, true, false},
{-1.0, -1.0, true, false},
{ 0.0, -1.0, false, false},
{ 1.0, -1.0, false, false},
{ 2.0, -1.0, false, false},
{ 4.0, -1.0, false, false},

{-4.0, 0.0, false, true},
{-2.0, 0.0, false, true},
{-1.0, 0.0, false, true},
{ 0.0, 0.0, true, false},
{ 1.0, 0.0, false, false},
{ 2.0, 0.0, false, false},
{ 4.0, 0.0, false, false},

{-4.0, 1.0, false, true},
{-2.0, 1.0, false, true},
{-1.0, 1.0, false, true},
{ 0.0, 1.0, false, true},
{ 1.0, 1.0, true, false},
{ 2.0, 1.0, true, false},
{ 4.0, 1.0, false, false},

{-4.0, 2.0, false, true},
{-2.0, 2.0, false, true},
{-1.0, 2.0, false, true},
{ 0.0, 2.0, false, true},
{ 1.0, 2.0, true, false},
{ 2.0, 2.0, true, false},
{ 4.0, 2.0, true, false},

{-4.0, 4.0, false, true},
{-2.0, 4.0, false, true},
{-1.0, 4.0, false, true},
{ 0.0, 4.0, false, true},
{ 1.0, 4.0, false, true},
{ 2.0, 4.0, true, false},
{ 4.0, 4.0, true, false}}) {
KRATOS_EXPECT_TRUE(equality_comparison(left, right) == equality_reference);
KRATOS_EXPECT_TRUE(ordering(left, right) == ordering_reference);
}
}


KRATOS_TEST_CASE_IN_SUITE(FloatComparisonConsistency, KratosCoreFastSuite)
{
const float absolute_tolerance = std::numeric_limits<float>::min();
const float relative_tolerance = 1e-4f;

Comparison<float>::Equal equality_comparison(absolute_tolerance, relative_tolerance);

const std::array<float,3> test_values {4.9303807e-32f, //< reference value
4.9303810e-32f,
4.9309825e-32f};

// Example of an inconsistent comparison.
{
const auto inconsistent_comparison = [absolute_tolerance, relative_tolerance](float left, float right) {
const float diff = std::abs(left - right);
if (left == 0.0f || right == 0.0f || diff < absolute_tolerance) {
return diff < relative_tolerance * absolute_tolerance;
} else {
return diff < relative_tolerance * (std::abs(left) + std::abs(right));
}
};

// Even though these values are very close to each other, they are already
// too far apart for the absolute tolerance, but not far enough for
// the relative comparison to kick in yet.
KRATOS_EXPECT_FALSE(inconsistent_comparison(test_values[0], test_values[1]));

// Even though these values are farther apart than the previous two, they're
// picked up by the relative comparison.
KRATOS_EXPECT_TRUE(inconsistent_comparison(test_values[0], test_values[2]));
}

// Make sure the implemented comparison deals with the case
// the inconsistent one fails at.
KRATOS_EXPECT_TRUE(equality_comparison(test_values[0], test_values[1]));
KRATOS_EXPECT_TRUE(equality_comparison(test_values[0], test_values[2]));

// Classic example of finite precision inaccuracies.
KRATOS_EXPECT_TRUE(3.0f * 0.3f != 1.0f - 0.1f
&& equality_comparison(3.0f * 0.3f, 1.0f - 0.1f));
}


} // namespace Kratos::Testing
64 changes: 64 additions & 0 deletions kratos/utilities/comparison.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// | / |
// ' / __| _` | __| _ \ __|
// . \ | ( | | ( |\__ `
// _|\_\_| \__,_|\__|\___/ ____/
// Multi-Physics
//
// License: BSD License
// Kratos default license: kratos/license.txt
//
// Main authors: Máté Kelemen
//

#pragma once

// Project includes
#include "utilities/comparison_impl.hpp" // IntegerComparison, FloatComparison
#include "includes/kratos_export_api.h" // KRATOS_API


namespace Kratos {


/// @brief Unspecialized base class for comparison operators to be specialized for specific types.
template <class T>
struct KRATOS_API(KRATOS_CORE) Comparison {
static_assert(std::is_same_v<T,void>, "attempting to instantiate Comparison for unsupported type");
struct Equal {};
struct Less {};
};


template <>
struct KRATOS_API(KRATOS_CORE) Comparison<int> : public Impl::IntegerComparison<int> {
using Impl::IntegerComparison<int>::IntegerComparison;
}; // Comparison<int>


template <>
struct KRATOS_API(KRATOS_CORE) Comparison<unsigned> : public Impl::IntegerComparison<unsigned> {
using Impl::IntegerComparison<unsigned>::IntegerComparison;
}; // Comparison<unsigned>


template <>
struct KRATOS_API(KRATOS_CORE) Comparison<std::size_t> : public Impl::IntegerComparison<std::size_t> {
using Impl::IntegerComparison<std::size_t>::IntegerComparison;
}; // Comparison<std::size_t>


/// @copydoc Impl::FloatComparison
template <>
struct KRATOS_API(KRATOS_CORE) Comparison<float> : public Impl::FloatComparison<float> {
using Impl::FloatComparison<float>::FloatComparison;
}; // Comparison<float>


/// @copydoc Impl::FloatComparison
template <>
struct KRATOS_API(KRATOS_CORE) Comparison<double> : public Impl::FloatComparison<double> {
using Impl::FloatComparison<double>::FloatComparison;
}; // Comparison<double>


} // namespace Kratos
113 changes: 113 additions & 0 deletions kratos/utilities/comparison_impl.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// | / |
// ' / __| _` | __| _ \ __|
// . \ | ( | | ( |\__ `
// _|\_\_| \__,_|\__|\___/ ____/
// Multi-Physics
//
// License: BSD License
// Kratos default license: kratos/license.txt
//
// Main authors: Máté Kelemen
//

#pragma once

// System includes
#include <type_traits> // std::is_same_v
#include <algorithm> // std::min
#include <limits> // std::numeric_limits
#include <cmath> // std::abs


namespace Kratos::Impl {


template <class T>
struct IntegerComparison
{
static_assert(
std::is_same_v<T,short>
|| std::is_same_v<T,int>
|| std::is_same_v<T,long>
|| std::is_same_v<T,long long>
|| std::is_same_v<T,unsigned short>
|| std::is_same_v<T,unsigned>
|| std::is_same_v<T,unsigned long>
|| std::is_same_v<T,unsigned long long>,
"attempting to instantiate IntegerComparison for unsupported type"
);

struct Equal {
constexpr bool operator()(T Left, T Right) const noexcept {
return Left == Right;
}
}; // struct Equal

struct Less {
constexpr bool operator()(T Left, T Right) const noexcept {
return Left < Right;
}
}; // struct Less
}; // struct IntegerComparison


/// @brief Relaxed floating point equality comparison and ordering.
/// @details This class implements a relaxed equality comparison for floating point
/// numbers that switches between relative and absolute tolerances depending
/// on the magnitude of numbers to be compared. This provides greater
/// robustness on a wider range.
/// The ordering builds on top of the equality operator.
/// @see https://stackoverflow.com/a/32334103/12350793
template <class T>
struct FloatComparison {
static_assert(
std::is_same_v<T,float>
|| std::is_same_v<T,double>
|| std::is_same_v<T,long double>,
"attempting to instantiate FloatComparison for unsupported type"
);

class Less;

class Equal {
public:
constexpr Equal() noexcept
: Equal(static_cast<T>(0), static_cast<T>(0))
{}

constexpr Equal(T AbsoluteTolerance, T RelativeTolerance) noexcept
: mAbsoluteTolerance(AbsoluteTolerance),
mRelativeTolerance(RelativeTolerance)
{}

constexpr bool operator()(T Left, T Right) const noexcept {
const T norm = std::min(std::abs(Left) + std::abs(Right),
std::numeric_limits<T>::max());
return Left == Right || std::abs(Left - Right) < std::max(mAbsoluteTolerance, mRelativeTolerance * norm);
}

private:
T mAbsoluteTolerance, mRelativeTolerance;
}; // struct Equal

class Less {
public:
constexpr Less() noexcept
: Less(static_cast<T>(0), static_cast<T>(0))
{}

constexpr Less(T AbsoluteTolerance, T RelativeTolerance) noexcept
: mEqualityComparison(AbsoluteTolerance, RelativeTolerance)
{}

constexpr bool operator()(T Left, T Right) const noexcept {
return Left < Right && !mEqualityComparison(Left, Right);
}

private:
FloatComparison<T>::Equal mEqualityComparison;
}; // class Less
}; // struct FloatComparison


} // namespace Kratos::Impl
Loading