Search code examples
c++catch2

Catch2: Checking if result is 0.0 with a relative error fails


I'm using Catch2 to check the result of a floating point calculation. It fails with the message:

Failure:
  REQUIRE_THAT(quat.R_component_1(), Catch::WithinRel(0.0f, ALLOWED_RELATIVE_ERROR))
with expansion:
  0.0 and 0 are within 0.01% of each other

It seems that Catch2 is comparing a floating point value with an integer value, but if have no idea where the integer value is coming from. Or maybe the failure message is not accurate and I'm doing somethin else wrong?

This is the unit test which produces the failure:

constexpr double ALLOWED_RELATIVE_ERROR = 0.0001; // 0.01%

TEST_CASE("Euler angles [-135.0, 45.0, -90.0] are converted correctly")
    {
        constexpr double PHI_DEG = -135.0; // rotation around x-axis
        constexpr double THETA_DEG = 45.0; // rotation around y-axis
        constexpr double PSI_DEG = -90.0; // rotation around z-axis

        auto quat = eulerYZXToQuaternion(PHI_DEG, THETA_DEG, PSI_DEG);
        REQUIRE_THAT(quat.R_component_1(), Catch::WithinRel(0.0, ALLOWED_RELATIVE_ERROR)); // This line fails with the message posted above.
        REQUIRE_THAT(quat.R_component_2(), Catch::WithinRel(-0.7071068, ALLOWED_RELATIVE_ERROR));
        REQUIRE_THAT(quat.R_component_3(), Catch::WithinRel(0.7071068, ALLOWED_RELATIVE_ERROR));
        REQUIRE_THAT(quat.R_component_4(), Catch::WithinRel(0.0, ALLOWED_RELATIVE_ERROR));
    }

quat is of type boost::math::quaternion<double>

These are the functions to be tested:

double degToRad(double degrees) { return degrees * M_PI / 180.0; }

boost::math::quaternion<double> eulerYZXToQuaternion(double phi, double theta, double psi)
{
    // Convert angles from degrees to radians
    phi = degToRad(phi); // rotation around x-axis
    theta = degToRad(theta); // rotation around y-axis
    psi = degToRad(psi); // rotation around z-axis

    double half_phi = 0.5 * phi;
    double half_theta = 0.5 * theta;
    double half_psi = 0.5 * psi;

    double cos_half_phi = cos(half_phi);
    double sin_half_phi = sin(half_phi);
    double cos_half_theta = cos(half_theta);
    double sin_half_theta = sin(half_theta);
    double cos_half_psi = cos(half_psi);
    double sin_half_psi = sin(half_psi);

    boost::math::quaternion<double> q(1, 0, 0, 0);

    // Compute the quaternion components. Rotation in sequence Y, Z, X
    q *= boost::math::quaternion<double>(cos_half_theta, 0.0, sin_half_theta, 0.0); // y
    q *= boost::math::quaternion<double>(cos_half_psi, 0.0, 0.0, sin_half_psi); // z
    q *= boost::math::quaternion<double>(cos_half_phi, sin_half_phi, 0.0, 0.0); // x
    return q;
}

When I'm doing the check in the following way it works as expected: REQUIRE(quat.R_component_1() == Approx(0.0).margin(ALLOWED_RELATIVE_ERROR));

Edit: When I'm using Catch::WithinAbs() instead of Catch::WithinRel() the test passes. But why does the check on quat.R_component_4() pass with the relative error check?

constexpr double ALLOWED_RELATIVE_ERROR = 0.0001; // 0.01%
constexpr double ALLOWED_ABSOLUT_ERROR = 1e-9;

TEST_CASE("Euler angles [-135.0, 45.0, -90.0] are converted correctly")
    {
        constexpr double PHI_DEG = -135.0; // rotation around x-axis
        constexpr double THETA_DEG = 45.0; // rotation around y-axis
        constexpr double PSI_DEG = -90.0; // rotation around z-axis

        auto quat = eulerYZXToQuaternion(PHI_DEG, THETA_DEG, PSI_DEG);
        REQUIRE_THAT(quat.R_component_1(), Catch::WithinAbs(0.0, ALLOWED_ABSOLUT_ERROR)); // Changed to Catch::WithinAbs()
        REQUIRE_THAT(quat.R_component_2(), Catch::WithinRel(-0.7071068, ALLOWED_RELATIVE_ERROR));
        REQUIRE_THAT(quat.R_component_3(), Catch::WithinRel(0.7071068, ALLOWED_RELATIVE_ERROR));
        REQUIRE_THAT(quat.R_component_4(), Catch::WithinRel(0.0, ALLOWED_RELATIVE_ERROR));
    }

Solution

  • The answer can be found here: https://github.com/catchorg/Catch2/blob/devel/docs/comparing-floating-point-numbers.md

    Specifically in the WithinRel section, where they tell you that it matches:

    |arg - target| <= eps * max(|arg|, |target|)

    Note that if target is 0 this reduces to: |arg| <= eps * |arg| which is only true if arg == 0 (as long as eps < 1). Essentially by using WithinRel with a target of 0 you disable the relative part.

    As for your failure message. If I do this:

    double bla = 0.00000000001;
    REQUIRE_THAT(bla, Catch::Matchers::WithinRel(0.0, 0.0001));
    

    Catch2 prints:

    FAILED:
      REQUIRE_THAT( bla, Catch::Matchers::WithinRel(0.0, 0.0001) )
    with expansion:
      0.0 and 0 are within 0.01% of each other
    

    Which is because doubles typically are not printed with full decimal precision, so in this case 0.00000000001 prints as 0.0.

    So to sum up:

    quat.R_component_1() is less than 1e-9 but not actually 0, so WithinRel fails while WithinAbs succeeds.

    quat.R_component_4() is actually equal to 0 so WithinRel succeeds.