blob: 83452300efc6cf396128c3a902b45e805c618df7 [file]
// Ceres Solver - A fast non-linear least squares minimizer
// Copyright 2026 Google Inc. All rights reserved.
// http://ceres-solver.org/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of Google Inc. nor the names of its contributors may be
// used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Author: sergiu.deitsch@gmail.com (Sergiu Deitsch)
#include "ceres/accurate_norm.h"
#include <cfloat>
#include <cmath>
#include <cstring>
#include <limits>
#include <type_traits>
#include "ceres/constants.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
// Matches if the argument is at most n floating-point values away from the
// expected value.
MATCHER_P2(MaxNumUlp, expected, n, "") {
using Scalar = std::decay_t<decltype(arg)>;
const Scalar target = static_cast<Scalar>(expected);
Scalar value = arg;
for (int distance = 0; value != target && distance < n; ++distance) {
value = std::nextafter(value, target);
}
*result_listener << "actual " << arg << " is not within " << n
<< " ULP of expected " << target;
return value == target;
}
TEST(AccurateNorm, Promote) {
static_assert(std::is_same_v<ceres::internal::Promote_t<int>, double>,
"Promotion of an int must be a double");
static_assert(std::is_same_v<ceres::internal::Promote_t<int, int>, double>,
"Promotion of multuple ints must be a double");
static_assert(std::is_same_v<ceres::internal::Promote_t<unsigned>, double>,
"Promotion of an unsigned int must be double");
static_assert(std::is_same_v<ceres::internal::Promote_t<long>, double>,
"Promotion of a long must be double");
static_assert(
std::is_same_v<ceres::internal::Promote_t<int, long, float>, double>,
"Promotion of arithmetic types must be double");
}
TEST(AccurateNorm, PromotesLvalueArguments) {
int integer = 3;
double floating_point = 4.0;
double zero = 0.0;
EXPECT_EQ(ceres::AccurateNorm(integer, floating_point, zero), 5.0);
}
TEST(AccurateNorm, PromotesIntegralArgumentsOfTheSameType) {
static_assert(std::is_same_v<decltype(ceres::AccurateNorm(3, 4)), double>);
static_assert(std::is_same_v<decltype(ceres::AccurateNorm(3, 4, 0)), double>);
static_assert(
std::is_same_v<decltype(ceres::AccurateNorm(3.0F, 4.0F, 0.0F)), float>);
EXPECT_EQ(ceres::AccurateNorm(3, 4), 5.0);
EXPECT_EQ(ceres::AccurateNorm(3, 4, 0), 5.0);
}
#if GTEST_HAS_TYPED_TEST
template <typename T>
class AccurateNormTest : public testing::Test {
public:
static constexpr auto kTiny = std::numeric_limits<T>::min();
static constexpr auto kHuge = std::numeric_limits<T>::max();
};
using Types = testing::Types<float, double, long double>;
TYPED_TEST_SUITE(AccurateNormTest, Types);
TEST(AccurateNorm, ScaleIsRadixExponent) {
using Traits = ceres::internal::AccurateNormTraits<double>;
// ulp(√F_min) = ulp(2^-511) = 2^-563 for double
static_assert(std::is_same_v<decltype(Traits::ScaleExponent()), int>);
static_assert(Traits::ScaleExponent() == -563);
}
TYPED_TEST(AccurateNormTest, Norm) {
using Scalar = TypeParam;
EXPECT_THAT(ceres::AccurateNorm(this->kTiny, Scalar{0}),
MaxNumUlp(this->kTiny, 0));
EXPECT_THAT(ceres::AccurateNorm(this->kTiny, Scalar{0}, Scalar{0}),
MaxNumUlp(this->kTiny, 0));
EXPECT_THAT(ceres::AccurateNorm(Scalar{0}, this->kTiny),
MaxNumUlp(this->kTiny, 0));
EXPECT_THAT(ceres::AccurateNorm(Scalar{0}, Scalar{0}, this->kTiny),
MaxNumUlp(this->kTiny, 0));
EXPECT_THAT(ceres::AccurateNorm(this->kHuge, Scalar{0}),
MaxNumUlp(this->kHuge, 0));
EXPECT_THAT(ceres::AccurateNorm(this->kHuge, Scalar{0}, Scalar{0}),
MaxNumUlp(this->kHuge, 0));
EXPECT_THAT(ceres::AccurateNorm(Scalar{0}, this->kHuge),
MaxNumUlp(this->kHuge, 0));
EXPECT_THAT(ceres::AccurateNorm(Scalar{0}, Scalar{0}, this->kHuge),
MaxNumUlp(this->kHuge, 0));
EXPECT_THAT(ceres::AccurateNorm(this->kTiny, this->kTiny),
MaxNumUlp(this->kTiny * std::sqrt(Scalar{2}), 1));
EXPECT_THAT(ceres::AccurateNorm(Scalar{0}, Scalar{0}),
MaxNumUlp(Scalar{0}, 0));
EXPECT_TRUE(std::isinf(
ceres::AccurateNorm(+std::numeric_limits<Scalar>::infinity(), 0)));
EXPECT_TRUE(std::isinf(
ceres::AccurateNorm(-std::numeric_limits<Scalar>::infinity(), 0)));
EXPECT_TRUE(std::isinf(
ceres::AccurateNorm(0, +std::numeric_limits<Scalar>::infinity())));
EXPECT_TRUE(std::isinf(
ceres::AccurateNorm(0, -std::numeric_limits<Scalar>::infinity())));
EXPECT_TRUE(std::isnan(
ceres::AccurateNorm(std::numeric_limits<Scalar>::quiet_NaN(), 0)));
EXPECT_TRUE(std::isnan(
ceres::AccurateNorm(0, std::numeric_limits<Scalar>::quiet_NaN())));
}
TEST(AccurateNorm, VariadicNormAccuracy) {
EXPECT_THAT(ceres::AccurateNorm(1.0, 1.0, 1.0),
MaxNumUlp(ceres::constants::sqrt_3, 0));
// Combination exposing a difference of at least two ULPs in inaccurate
// implementations found by random search.
constexpr double kFirst = 0.0;
constexpr double kSecond = -0x1.c4a46e8d5e9f3p-940;
constexpr double kThird = 0x1.2870a0a1f3fa3p-943;
constexpr double kExpected = 0x1.c628110110bf1p-940;
EXPECT_THAT(ceres::AccurateNorm(kFirst, kSecond, kThird),
MaxNumUlp(kExpected, 0));
}
TEST(AccurateNorm, HandlesTableMakerDilemma) {
// Values from Borges, Algorithm 1014, Section 6.
constexpr double kFirst = 0x1.a308e1455f447p+0;
constexpr double kSecond = 0x1.9d931a83ef879p+0;
constexpr double kExpected = 0x1.2660d009d54f9p+1;
EXPECT_THAT(ceres::AccurateNorm(kFirst, kSecond), MaxNumUlp(kExpected, 1));
}
TEST(AccurateNorm, ReturnsLargerArgumentWhenSmallerIsNegligible) {
constexpr double kLarger = 1.0;
// √(ε/2) is the largest ratio of the arguments for which the norm equals the
// larger argument.
constexpr double kSmaller = 0x1.6a09e667f3bcdp-27;
EXPECT_EQ(ceres::AccurateNorm(kLarger, kSmaller), kLarger);
}
TYPED_TEST(AccurateNormTest, AccountsForSmallerArgumentAboveCutoff) {
using Scalar = TypeParam;
// The squared ratio of the arguments equals the machine epsilon which raises
// the norm by three quarters of an ULP above the larger argument.
constexpr Scalar kLarger{1.5};
const Scalar kSmaller =
kLarger * std::sqrt(std::numeric_limits<Scalar>::epsilon());
const Scalar kExpected =
std::nextafter(kLarger, std::numeric_limits<Scalar>::infinity());
EXPECT_EQ(ceres::AccurateNorm(kLarger, kSmaller), kExpected);
}
TEST(AccurateNorm, RecoversSquaringErrorsOfSmallArguments) {
// The arguments are not rescaled but their squares are small enough for the
// rounding errors of the squares to underflow unless the scaling threshold
// accounts for them.
EXPECT_EQ(ceres::AccurateNorm(0x1.23342ep-63f, 0x1.271008p-63f),
0x1.9e8fdcp-63f);
EXPECT_EQ(ceres::AccurateNorm(0x1.4a15545f86ef4p-510, 0x1.3a578a98bcf9dp-510),
0x1.c7d03b992e6a8p-510);
#if LDBL_MANT_DIG == 64 && LDBL_MIN_EXP == -16381
EXPECT_EQ(ceres::AccurateNorm(0x8.b912469c50fab7ap-8194L,
0x8.c54f2ded5ba8268p-8194L),
0xc.5eb4864ce876f86p-8194L);
#endif
}
TEST(AccurateNorm, IsAtLeastAsAccurateAsHypot) {
// The expected values are correctly rounded. Implementations of std::hypot,
// e.g., the one shipped with libstdc++ 16, are off by one ULP for these
// arguments.
constexpr double kFirst = 0x1.424bf2ed916bfp+0;
constexpr double kSecond = 0x1.1435107c5d458p+0;
constexpr double kExpected = 0x1.a8758df39043fp+0;
EXPECT_EQ(ceres::AccurateNorm(kFirst, kSecond), kExpected);
EXPECT_LE(std::fabs(ceres::AccurateNorm(kFirst, kSecond) - kExpected),
std::fabs(std::hypot(kFirst, kSecond) - kExpected));
constexpr double kThreeFirst = 0x1.c11f6531eb66ep+0;
constexpr double kThreeSecond = 0x1.f30567547a34cp+0;
constexpr double kThreeThird = 0x1.1e0edcc120696p+0;
constexpr double kThreeExpected = 0x1.6ce2663da03a7p+1;
EXPECT_EQ(ceres::AccurateNorm(kThreeFirst, kThreeSecond, kThreeThird),
kThreeExpected);
EXPECT_LE(
std::fabs(ceres::AccurateNorm(kThreeFirst, kThreeSecond, kThreeThird) -
kThreeExpected),
std::fabs(std::hypot(kThreeFirst, kThreeSecond, kThreeThird) -
kThreeExpected));
}
TYPED_TEST(AccurateNormTest, PowerOfTwoIsExact) {
using Scalar = TypeParam;
using std::scalbn;
constexpr int kMinExponent = std::numeric_limits<Scalar>::min_exponent -
std::numeric_limits<Scalar>::digits;
constexpr int kMaxExponent = std::numeric_limits<Scalar>::max_exponent - 1;
for (int exponent = kMinExponent; exponent <= kMaxExponent; ++exponent) {
EXPECT_EQ(ceres::internal::PowerOfTwo<Scalar>(exponent),
scalbn(Scalar{1}, exponent))
<< "exponent " << exponent;
}
}
TEST(AccurateNorm, TraitsAreConstantExpressions) {
// Compilers that do not fold std::scalbn would otherwise compute the
// thresholds at runtime on every invocation.
using DoubleTraits = ceres::internal::AccurateNormTraits<double>;
static_assert(DoubleTraits::Tiny() == 0x1p-485);
static_assert(DoubleTraits::UnscaledMinimum() == 0x1p-432);
static_assert(DoubleTraits::UnscaledMaximum(2) == 0x1p+511);
static_assert(DoubleTraits::UnscaledMaximum(3) == 0x1p+510);
using FloatTraits = ceres::internal::AccurateNormTraits<float>;
static_assert(FloatTraits::Tiny() == 0x1p-51f);
static_assert(FloatTraits::UnscaledMinimum() == 0x1p-27f);
static_assert(FloatTraits::UnscaledMaximum(3) == 0x1p+62f);
}
TEST(AccurateNorm, VariadicNormIsAccurateForExtremeMagnitudes) {
// Subnormal arguments
EXPECT_EQ(ceres::AccurateNorm(0x0.123456789abcdp-1022,
0x0.fedcba9876543p-1022,
0x0.0000000000001p-1022),
0x0.ff82f53036b9cp-1022);
// Small arguments including a negligible one
EXPECT_EQ(ceres::AccurateNorm(
0x1.3a578a98bcf9dp-600, -0x1.4a15545f86ef4p-601, 0x1.5p-650),
0x1.6308e012498e5p-600);
// Arguments close to the largest finite value
EXPECT_EQ(ceres::AccurateNorm(0x1.ffffffffffffp+1022,
0x1.4a15545f86ef4p+1021,
-0x1.3a578a98bcf9dp+1000),
0x1.0cf8b69a0aff1p+1023);
// Large arguments including a negligible one
EXPECT_EQ(ceres::AccurateNorm(0x1.8p+1000, 0x1p-1000, 0x1.4p+999),
0x1.ap+1000);
// The norm exceeds the largest finite value
EXPECT_EQ(ceres::AccurateNorm(0x1.ep+1023, 0x1.ep+1023, 0x1.ep+1023),
std::numeric_limits<double>::infinity());
}
TEST(AccurateNorm, VariadicNormIsAccurateAroundUnscaledRange) {
constexpr double kSqrt3Largest = 0x1.bb67ae8584caap+510;
constexpr double kSqrt3Smallest = 0x1.bb67ae8584caap-432;
// Inside the range that requires no rescaling
EXPECT_EQ(ceres::AccurateNorm(0x1p+510, 0x1p+510, 0x1p+510), kSqrt3Largest);
EXPECT_EQ(ceres::AccurateNorm(0x1p-432, 0x1p-432, 0x1p-432), kSqrt3Smallest);
EXPECT_EQ(ceres::AccurateNorm(
0x1.3a578a98bcf9dp+509, -0x1.4a15545f86ef4p+510, 0x1.5p-200),
0x1.6d979f8e15b5ap+510);
// Just outside the range that requires no rescaling
EXPECT_EQ(ceres::AccurateNorm(0x1.fffffffffffffp+510,
0x1.fffffffffffffp+510,
0x1.fffffffffffffp+510),
0x1.bb67ae8584caap+511);
EXPECT_EQ(ceres::AccurateNorm(0x1.fffffffffffffp-433,
0x1.fffffffffffffp-433,
0x1.fffffffffffffp-433),
kSqrt3Smallest);
EXPECT_EQ(ceres::AccurateNorm(
0x1.4a15545f86ef4p-432, -0x1.3a578a98bcf9dp-433, 0x1.5p-500),
0x1.6d979f8e15b5ap-432);
}
TEST(AccurateNorm, RescalesByRadixPowersWithoutRounding) {
constexpr int kLargeExponent = 512;
constexpr int kSmallExponent = -514;
EXPECT_EQ(ceres::AccurateNorm(std::scalbn(3.0, kLargeExponent),
std::scalbn(4.0, kLargeExponent)),
std::scalbn(5.0, kLargeExponent));
EXPECT_EQ(ceres::AccurateNorm(std::scalbn(3.0, kSmallExponent),
std::scalbn(4.0, kSmallExponent)),
std::scalbn(5.0, kSmallExponent));
}
TEST(AccurateNorm, VariadicNormHandlesWideDynamicRange) {
// Combination found by random search that exposes a difference of more than
// two ULPs when normalized values are not stored.
constexpr double kFirst = -0x1.5fdef349a2773p+922;
constexpr double kSecond = -0x1.72ec46b66e1d9p-114;
constexpr double kThird = -0x1.eeb9ef28337eep-462;
constexpr double kFourth = -0x1.137462de2cf44p+205;
constexpr double kFifth = -0x1.3a6cb8edf6622p+264;
constexpr double kSixth = 0x1.0f5b36fe2970ap-326;
constexpr double kSeventh = -0x1.5483d7f40eca8p-537;
constexpr double kEighth = 0x1.4694b77d1bb38p-643;
constexpr double kExpectedNorm = 0x1.5fdef349a2773p+922;
EXPECT_THAT(
ceres::AccurateNorm(
kFirst, kSecond, kThird, kFourth, kFifth, kSixth, kSeventh, kEighth),
MaxNumUlp(kExpectedNorm, 0));
}
TEST(AccurateNorm, VariadicNormRescalesLargeArguments) {
constexpr double kFirst = -0x1.5cf602c1b383ep-360;
constexpr double kSecond = 0x1.e7855aa96a0c7p+850;
constexpr double kThird = -0x1.e3bf678284e4p+876;
constexpr double kFourth = 0x1.7d18d55d5723fp+173;
constexpr double kFifth = -0x1.1a6c3b4a7f1f8p+437;
constexpr double kSixth = -0x1.14b8d80b68457p-561;
constexpr double kExpectedNorm = 0x1.e3bf678284e41p+876;
EXPECT_THAT(
ceres::AccurateNorm(kFirst, kSecond, kThird, kFourth, kFifth, kSixth),
MaxNumUlp(kExpectedNorm, 0));
}
TEST(AccurateNorm, VariadicNormReturnsPositiveInfinity) {
constexpr double kInfinity = std::numeric_limits<double>::infinity();
EXPECT_EQ(ceres::AccurateNorm(-kInfinity, 1.0, 2.0), kInfinity);
}
TEST(AccurateNorm, NonfiniteArgumentHandling) {
constexpr double kInfinity = std::numeric_limits<double>::infinity();
constexpr double kNaN = std::numeric_limits<double>::quiet_NaN();
EXPECT_EQ(ceres::AccurateNorm(kNaN, kInfinity), kInfinity);
EXPECT_EQ(ceres::AccurateNorm(kInfinity, kNaN), kInfinity);
EXPECT_EQ(ceres::AccurateNorm(1.0, kInfinity, kNaN), kInfinity);
EXPECT_EQ(ceres::AccurateNorm(kNaN, 1.0, kInfinity), kInfinity);
EXPECT_EQ(ceres::AccurateNorm(kNaN, kInfinity, 1.0), kInfinity);
EXPECT_EQ(ceres::AccurateNorm(kInfinity, kNaN, 1.0), kInfinity);
EXPECT_TRUE(std::isnan(ceres::AccurateNorm(kNaN, kNaN, 1.0)));
EXPECT_TRUE(std::isnan(ceres::AccurateNorm(1.0, kNaN, kNaN)));
EXPECT_TRUE(std::isnan(ceres::AccurateNorm(kNaN, 1.0, kNaN)));
}
TEST(AccurateNorm, PreservesNaNPayloadAcrossArity) {
const double nan = std::copysign(std::nan("12345"), -1.0);
const double norm = ceres::AccurateNorm(nan, 1.0);
const double variadic_norm = ceres::AccurateNorm(nan, 1.0, 2.0);
EXPECT_EQ(std::memcmp(&variadic_norm, &norm, sizeof(double)), 0);
}
TEST(AccurateNorm, PreservesNaNPayloadOfAnyArgument) {
const double nan = std::copysign(std::nan("12345"), -1.0);
const double expected = std::fabs(nan);
const double first = ceres::AccurateNorm(nan, 1.0, 2.0);
const double last = ceres::AccurateNorm(1.0, 2.0, nan);
EXPECT_EQ(std::memcmp(&first, &expected, sizeof(double)), 0);
EXPECT_EQ(std::memcmp(&last, &expected, sizeof(double)), 0);
}
#endif