Tests: Preserve subnormal beta reference values on ARMv7 libeigen/eigen!3390 Closes #3164 Co-authored-by: Rasmus Munk Larsen <rmlarsen@gmail.com>
diff --git a/contrib/test/special_functions.cpp b/contrib/test/special_functions.cpp index 1cd0b29..cc044c9 100644 --- a/contrib/test/special_functions.cpp +++ b/contrib/test/special_functions.cpp
@@ -295,10 +295,17 @@ ArrayType b = (m2 * Scalar(4)).exp(); ArrayType x = m3.abs(); - // betainc(a, 1, x) == x**a. One ulp of a subnormal x**a exceeds the relative tolerance, so the smallest normal is - // added to the scale as an absolute floor. + // ARMv7 NEON flushes subnormal float intermediates, unlike the scalar betainc implementation. Form the + // reference in at least double precision so x**a and the recurrence term retain subnormal float results. + using ReferenceScalar = std::conditional_t<(sizeof(Scalar) < sizeof(double)), double, Scalar>; + using ReferenceArray = Array<ReferenceScalar, Dynamic, 1>; + const ReferenceArray& ar = a.template cast<ReferenceScalar>(); + const ReferenceArray& br = b.template cast<ReferenceScalar>(); + const ReferenceArray& xr = x.template cast<ReferenceScalar>(); const Scalar tiny = (std::numeric_limits<Scalar>::min)(); - CALL_SUBTEST(ArrayType test = betainc(a, one, x); ArrayType expected = x.pow(a); + + // betainc(a, 1, x) == x**a. The absolute floor covers rounding of subnormal results. + CALL_SUBTEST(ArrayType test = betainc(a, one, x); ArrayType expected = xr.pow(ar).template cast<Scalar>(); for (Index i = 0; i < test.size(); ++i) VERIFY_IS_APPROX_SCALED(test.segment(i, 1), expected.segment(i, 1), expected(i) + tiny);); @@ -311,15 +318,13 @@ CALL_SUBTEST(ArrayType test = betainc(a, b, x) + betainc(b, a, one - x); ArrayType expected = one; verify_component_wise(test, expected);); - // betainc(a+1, b, x) = betainc(a, b, x) - x**a * (1 - x)**b / (a * beta(a, b)). The difference cancels, so the - // error is bounded relative to the larger of the two terms rather than to the result, plus the smallest normal as - // the floor for subnormal results. The subtracted term is evaluated in log space: in float x**a * (1 - x)**b - // underflows while the term itself, divided by a small beta(a, b), does not. - CALL_SUBTEST(ArrayType log_beta = a.lgamma() + b.lgamma() - (a + b).lgamma(); - ArrayType term = (a * x.log() + b * (-x).log1p() - a.log() - log_beta).exp(); - ArrayType lhs = betainc(a, b, x); ArrayType scale = (lhs.max)(term) + tiny; - ArrayType expected = lhs - term; ArrayType test = betainc(a + one, b, x); - for (Index i = 0; i < test.size(); ++i) + // betainc(a+1, b, x) = betainc(a, b, x) - x**a * (1 - x)**b / (a * beta(a, b)). Bound cancellation relative + // to the larger term. Log space avoids underflow of the numerator before division by a small beta(a, b). + CALL_SUBTEST(ReferenceArray log_beta = ar.lgamma() + br.lgamma() - (ar + br).lgamma(); + ReferenceArray term = (ar * xr.log() + br * (-xr).log1p() - ar.log() - log_beta).exp(); + ArrayType lhs = betainc(a, b, x); ArrayType scale = (lhs.max)(term.template cast<Scalar>()) + tiny; + ArrayType expected = (lhs.template cast<ReferenceScalar>() - term).template cast<Scalar>(); + ArrayType test = betainc(a + one, b, x); for (Index i = 0; i < test.size(); ++i) VERIFY_IS_APPROX_SCALED(test.segment(i, 1), expected.segment(i, 1), scale(i));); // betainc(a, b+1, x) = betainc(a, b, x) + x**a * (1 - x)**b / (b * beta(a, b)) @@ -571,6 +576,16 @@ VERIFY((numext::isnan)(numext::betainc(Scalar(0.5), Scalar(0), nan))); VERIFY((numext::isnan)(numext::betainc(nan, Scalar(0.5), Scalar(0)))); + if (sizeof(Scalar) < sizeof(double)) { + // MPFR-backed Boost.Math ibeta, 100 decimal digits. The log-domain evaluation at a ~= 38 amplifies rounding; + // 32 eps bounds the scalar error here, plus one subnormal ulp for the final conversion. + const Scalar a = Scalar(38.325092315673828), b = Scalar(5.0568113327026367), x = Scalar(0.07500767707824707); + const double ref = 6.17294757793914262e-40; + const double actual = static_cast<double>(numext::betainc(a + Scalar(1), b, x)); + VERIFY(std::abs(actual - ref) <= 32 * static_cast<double>(NumTraits<Scalar>::epsilon()) * ref + + static_cast<double>((std::numeric_limits<Scalar>::denorm_min)())); + } + // A negative q sums until n + q > 9. zeta(2, -20.5) = pi^2/2 + sum_{j=0}^{20} (j + 1/2)^-2. { double ref = EIGEN_PI * EIGEN_PI / 2;