diff --git a/kratos/tests/cpp_tests/utilities/test_comparison.cpp b/kratos/tests/cpp_tests/utilities/test_comparison.cpp new file mode 100644 index 000000000000..0a5a53e34ead --- /dev/null +++ b/kratos/tests/cpp_tests/utilities/test_comparison.cpp @@ -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::Equal equality_comparison; + Comparison::Less ordering; + + for (const auto& [left, right, equality_reference, ordering_reference] : + std::vector> {{-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::Equal equality_comparison(1.0, 0.5); + Comparison::Less ordering(1.0, 0.5); + + for (const auto& [left, right, equality_reference, ordering_reference] : + std::vector> {{-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::min(); + const float relative_tolerance = 1e-4f; + + Comparison::Equal equality_comparison(absolute_tolerance, relative_tolerance); + + const std::array 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 diff --git a/kratos/utilities/comparison.h b/kratos/utilities/comparison.h new file mode 100644 index 000000000000..ddd783d97f2d --- /dev/null +++ b/kratos/utilities/comparison.h @@ -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 +struct KRATOS_API(KRATOS_CORE) Comparison { + static_assert(std::is_same_v, "attempting to instantiate Comparison for unsupported type"); + struct Equal {}; + struct Less {}; +}; + + +template <> +struct KRATOS_API(KRATOS_CORE) Comparison : public Impl::IntegerComparison { + using Impl::IntegerComparison::IntegerComparison; +}; // Comparison + + +template <> +struct KRATOS_API(KRATOS_CORE) Comparison : public Impl::IntegerComparison { + using Impl::IntegerComparison::IntegerComparison; +}; // Comparison + + +template <> +struct KRATOS_API(KRATOS_CORE) Comparison : public Impl::IntegerComparison { + using Impl::IntegerComparison::IntegerComparison; +}; // Comparison + + +/// @copydoc Impl::FloatComparison +template <> +struct KRATOS_API(KRATOS_CORE) Comparison : public Impl::FloatComparison { + using Impl::FloatComparison::FloatComparison; +}; // Comparison + + +/// @copydoc Impl::FloatComparison +template <> +struct KRATOS_API(KRATOS_CORE) Comparison : public Impl::FloatComparison { + using Impl::FloatComparison::FloatComparison; +}; // Comparison + + +} // namespace Kratos diff --git a/kratos/utilities/comparison_impl.hpp b/kratos/utilities/comparison_impl.hpp new file mode 100644 index 000000000000..475031fb6d92 --- /dev/null +++ b/kratos/utilities/comparison_impl.hpp @@ -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 // std::is_same_v +#include // std::min +#include // std::numeric_limits +#include // std::abs + + +namespace Kratos::Impl { + + +template +struct IntegerComparison +{ + static_assert( + std::is_same_v + || std::is_same_v + || std::is_same_v + || std::is_same_v + || std::is_same_v + || std::is_same_v + || std::is_same_v + || std::is_same_v, + "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 +struct FloatComparison { + static_assert( + std::is_same_v + || std::is_same_v + || std::is_same_v, + "attempting to instantiate FloatComparison for unsupported type" + ); + + class Less; + + class Equal { + public: + constexpr Equal() noexcept + : Equal(static_cast(0), static_cast(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::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(0), static_cast(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::Equal mEqualityComparison; + }; // class Less +}; // struct FloatComparison + + +} // namespace Kratos::Impl