#include #include #include "grotto/range_lut.hpp" #include #include namespace { long double truth_of(grotto::reduced which, long double x) { switch (which) { case grotto::reduced::ln: return std::log(x); case grotto::reduced::lg: return std::log2(x); case grotto::reduced::log10: return std::log10(x); case grotto::reduced::exp: return std::exp(x); case grotto::reduced::exp2: return std::exp2(x); case grotto::reduced::exp10: return std::exp(x * std::log(10.0L)); case grotto::reduced::sin: return std::sin(x); case grotto::reduced::cos: return std::cos(x); case grotto::reduced::tan: return std::tan(x); case grotto::reduced::cot: return 1.0L / std::tan(x); case grotto::reduced::sec: return 1.0L / std::cos(x); case grotto::reduced::csc: return 1.0L / std::sin(x); case grotto::reduced::sinh: return std::sinh(x); case grotto::reduced::cosh: return std::cosh(x); case grotto::reduced::tanh: return std::tanh(x); case grotto::reduced::coth: return 1.0L / std::tanh(x); case grotto::reduced::sech: return 1.0L / std::cosh(x); case grotto::reduced::csch: return 1.0L / std::sinh(x); case grotto::reduced::sqrt: return std::sqrt(x); case grotto::reduced::inv: return 1.0L / x; case grotto::reduced::rsqrt: return 1.0L / std::sqrt(x); case grotto::reduced::invsq: return 1.0L / (x * x); case grotto::reduced::expm1: return expm1l(x); case grotto::reduced::log1p: return log1pl(x); } return 0; } const char * name_of(grotto::reduced which) { switch (which) { case grotto::reduced::ln: return "ln"; case grotto::reduced::lg: return "lg"; case grotto::reduced::log10: return "log10"; case grotto::reduced::exp: return "exp"; case grotto::reduced::exp2: return "exp2"; case grotto::reduced::exp10: return "exp10"; case grotto::reduced::sin: return "sin"; case grotto::reduced::cos: return "cos"; case grotto::reduced::tan: return "tan"; case grotto::reduced::cot: return "cot"; case grotto::reduced::sec: return "sec"; case grotto::reduced::csc: return "csc"; case grotto::reduced::sinh: return "sinh"; case grotto::reduced::cosh: return "cosh"; case grotto::reduced::tanh: return "tanh"; case grotto::reduced::coth: return "coth"; case grotto::reduced::sech: return "sech"; case grotto::reduced::csch: return "csch"; case grotto::reduced::sqrt: return "sqrt"; case grotto::reduced::inv: return "inv"; case grotto::reduced::rsqrt: return "rsqrt"; case grotto::reduced::invsq: return "invsq"; case grotto::reduced::expm1: return "expm1"; case grotto::reduced::log1p: return "log1p"; } return "?"; } std::int64_t raw_of(long double x, unsigned k) { const long double scaled = std::ldexp(x, static_cast(k)); return std::llround(scaled); } void expect_ulps(grotto::reduced which, unsigned k, long double x, long double ulps) { const std::int64_t raw = raw_of(x, k); std::int64_t got = 0; ASSERT_NO_THROW(got = grotto::eval_reduced(which, k, raw)) << name_of(which) << " k=" << k << " x=" << static_cast(x); const long double truth = truth_of(which, std::ldexp(static_cast(raw), -static_cast(k))); const long double want = truth * std::ldexp(1.0L, static_cast(k)); EXPECT_LE(std::fabsl(static_cast(got) - want), ulps) << name_of(which) << " k=" << k << " x=" << static_cast(x) << " got=" << got << " want=" << static_cast(want); } bool near_odd_multiple_of_half_pi(long double x) { const long double turn = std::fmod(std::fabsl(x), 3.14159265358979323846L); const long double dist = std::fmod(turn + 1.5707963267948966L, 3.14159265358979323846L); const long double folded = dist > 1.5707963267948966L ? 3.14159265358979323846L - dist : dist; return folded < 0.15L; } } // namespace TEST(RangeLut, LogarithmsTrackLibmOnEveryPrecision) { const long double samples[] = { 0.125L, 0.3L, 0.5L, 0.75L, 1.0L, 1.5L, 2.0L, 3.0L, 7.5L, 16.0L, 24.0L, 100.0L, }; for (unsigned k : grotto::principal_precisions) { for (long double x : samples) { expect_ulps(grotto::reduced::ln, k, x, 1.0L); expect_ulps(grotto::reduced::log10, k, x, 1.0L); expect_ulps(grotto::reduced::lg, k, x, 2.0L); } for (int exp : {-20, 20}) { const long double x = std::ldexp(1.0L, exp); if (raw_of(x, k) == 0) continue; expect_ulps(grotto::reduced::ln, k, x, 1.0L); expect_ulps(grotto::reduced::log10, k, x, 1.0L); } } } TEST(RangeLut, ExponentialsTrackLibmOnEveryPrecision) { const long double samples[] = { -2.0L, -1.5L, -0.5L, -0.1L, 0.0L, 0.1L, 0.5L, 1.0L, 1.5L, 2.0L, }; for (unsigned k : grotto::principal_precisions) for (long double x : samples) { expect_ulps(grotto::reduced::exp, k, x, 1.0L); expect_ulps(grotto::reduced::exp2, k, x, 2.0L); } for (unsigned k : grotto::principal_precisions) { for (long double x : {-0.9L, -0.25L, 0.0L, 0.25L, 0.9L}) expect_ulps(grotto::reduced::exp10, k, x, 24.0L); // 10^q scales the absolute error of exp(f ln 10) by the integer power. for (long double x : {-1.5L, 1.5L}) expect_ulps(grotto::reduced::exp10, k, x, 80.0L); } } TEST(RangeLut, QuarterTurnTrigTracksLibm) { const long double samples[] = { -3.5L, -2.2L, -1.2L, -0.7L, -0.3L, 0.0L, 0.2L, 0.4L, 0.7L, 1.0L, 1.2L, 2.5L, 3.5L, }; for (auto which : {grotto::reduced::sin, grotto::reduced::cos}) for (unsigned k : grotto::principal_precisions) for (long double x : samples) expect_ulps(which, k, x, 3.0L); for (auto which : {grotto::reduced::tan, grotto::reduced::sec}) for (unsigned k : grotto::principal_precisions) for (long double x : samples) { if (near_odd_multiple_of_half_pi(x)) continue; expect_ulps(which, k, x, 8.0L); } for (auto which : {grotto::reduced::cot, grotto::reduced::csc}) for (unsigned k : grotto::principal_precisions) for (long double x : samples) { if (x == 0.0L || near_odd_multiple_of_half_pi(x)) continue; expect_ulps(which, k, x, 8.0L); } } TEST(RangeLut, HyperbolicReductionsTrackLibm) { const long double samples[] = { -2.0L, -1.2L, -0.4L, -0.05L, 0.05L, 0.4L, 0.7L, 1.2L, 2.0L, }; for (auto which : {grotto::reduced::sinh, grotto::reduced::cosh}) for (unsigned k : grotto::principal_precisions) for (long double x : samples) expect_ulps(which, k, x, 12.0L); for (auto which : { grotto::reduced::tanh, grotto::reduced::sech, grotto::reduced::coth, grotto::reduced::csch, }) { for (unsigned k : grotto::principal_precisions) for (long double x : samples) { if ((which == grotto::reduced::coth || which == grotto::reduced::csch) && std::fabsl(x) < 0.2L) continue; expect_ulps(which, k, x, 6.0L); } } } TEST(RangeLut, DyadicLiftsCoverOddAndEvenExponents) { const grotto::reduced maps[] = { grotto::reduced::sqrt, grotto::reduced::inv, grotto::reduced::rsqrt, grotto::reduced::invsq, }; const long double samples[] = { 0.125L, 0.3L, 0.5L, 0.75L, 1.0L, 1.5L, 2.0L, 3.0L, 6.0L, 7.5L, 16.0L, 24.0L, 48.0L, 100.0L, }; for (auto which : maps) for (unsigned k : grotto::principal_precisions) for (long double x : samples) { const long double budget = which == grotto::reduced::sqrt ? 12.0L : 4.0L; expect_ulps(which, k, x, budget); } } TEST(RangeLut, TinyHyperbolicArgumentsUseThePrincipalTables) { for (unsigned k : grotto::principal_precisions) { if (k < 13) continue; const long double tiny = std::ldexp(1.0L, -16); expect_ulps(grotto::reduced::sinh, k, tiny, 2.0L); expect_ulps(grotto::reduced::cosh, k, tiny, 2.0L); expect_ulps(grotto::reduced::sinh, k, -tiny, 2.0L); expect_ulps(grotto::reduced::cosh, k, -tiny, 2.0L); } } TEST(RangeLut, TanhAndCothSaturatePastBeta) { for (unsigned k : grotto::principal_precisions) { const std::int64_t one = std::int64_t{1} << k; const std::int64_t raw = raw_of(20.0L, k); EXPECT_EQ(grotto::eval_reduced(grotto::reduced::tanh, k, raw), one); EXPECT_EQ(grotto::eval_reduced(grotto::reduced::tanh, k, -raw), -one); EXPECT_EQ(grotto::eval_reduced(grotto::reduced::coth, k, raw), one); EXPECT_EQ(grotto::eval_reduced(grotto::reduced::coth, k, -raw), -one); } } TEST(RangeLut, SquareRootOfZeroIsZero) { for (unsigned k : grotto::principal_precisions) EXPECT_EQ(grotto::eval_reduced(grotto::reduced::sqrt, k, 0), 0); } TEST(RangeLut, RejectsPolesAndNonPositiveLogarithms) { EXPECT_THROW(grotto::eval_reduced(grotto::reduced::ln, 16, 0), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::ln, 16, -4), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::lg, 16, -1), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::sqrt, 16, -4), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::inv, 16, 0), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::rsqrt, 16, -8), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::cot, 16, 0), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::csc, 16, 0), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::coth, 16, 0), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::csch, 16, 0), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::ln, 7, 32), std::invalid_argument); } TEST(RangeLut, TanAndSecPolesThrow) { const unsigned k = 16; auto pole_of = [&](grotto::reduced which) { const std::int64_t lo = raw_of(-8.0L, k); const std::int64_t hi = raw_of(8.0L, k); for (std::int64_t raw = lo; raw <= hi; ++raw) { try { grotto::eval_reduced(which, k, raw); } catch (const std::domain_error &) { return raw; } } return std::int64_t{0}; }; for (auto which : {grotto::reduced::tan, grotto::reduced::sec}) { const std::int64_t raw = pole_of(which); ASSERT_NE(raw, 0) << name_of(which); EXPECT_THROW(grotto::eval_reduced(which, k, raw), std::domain_error) << name_of(which) << " raw=" << raw; if (raw > raw_of(-8.0L, k)) { EXPECT_NO_THROW(grotto::eval_reduced(which, k, raw - 1)) << name_of(which); } } } TEST(RangeLut, Exp10RejectsAnIntegerPowerPast18) { const unsigned k = 16; int first_overflow = -1; for (int n = 0; n <= 20; ++n) { try { grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(static_cast(n), k)); } catch (const std::overflow_error &) { first_overflow = n; break; } } ASSERT_GT(first_overflow, 0); ASSERT_LE(first_overflow, 19); EXPECT_NO_THROW(grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(static_cast(first_overflow - 1), k))); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(19.0L, k)), std::overflow_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(-19.0L, k)), std::overflow_error); } TEST(RangeLut, Expm1AndLog1pTrackLibm) { const long double expm1_samples[] = { -4.0L, -2.0L, -1.0L, -0.7L, -0.5L, -0.25L, -0.125L, -0.015625L, 0.0L, 0.015625L, 0.125L, 0.25L, 0.5L, 0.7L, 1.0L, 1.5L, 2.0L, }; const long double log1p_samples[] = { -0.75L, -0.5L, -0.25L, -0.125L, -0.015625L, 0.0L, 0.015625L, 0.125L, 0.25L, 0.5L, 1.0L, 1.5L, 3.0L, 7.5L, 16.0L, 100.0L, }; for (unsigned k : grotto::principal_precisions) { expect_ulps(grotto::reduced::expm1, k, std::ldexp(1.0L, -static_cast(k)), 2.0L); expect_ulps(grotto::reduced::expm1, k, -std::ldexp(1.0L, -static_cast(k)), 2.0L); expect_ulps(grotto::reduced::log1p, k, std::ldexp(1.0L, -static_cast(k)), 2.0L); expect_ulps(grotto::reduced::log1p, k, -std::ldexp(1.0L, -static_cast(k)), 2.0L); for (long double x : expm1_samples) expect_ulps(grotto::reduced::expm1, k, x, 1.0L); for (long double x : log1p_samples) expect_ulps(grotto::reduced::log1p, k, x, 1.0L); } } TEST(RangeLut, ReciprocalFamilyStaysInsideOneUlpAfterTheLift) { const long double samples[] = { std::ldexp(1.0L, -12), 0.015625L, 0.125L, 0.5L, 0.75L, 1.0L, 1.5L, 3.0L, 8.0L, 100.0L, }; for (auto which : {grotto::reduced::inv, grotto::reduced::rsqrt, grotto::reduced::invsq}) { for (unsigned k : {16u, 32u}) { for (long double x : samples) { if (k == 16 && x < std::ldexp(1.0L, -10)) continue; expect_ulps(which, k, x, 1.0L); } } } } TEST(RangeLut, Expm1AndLog1pFixpoints) { for (unsigned k : grotto::principal_precisions) { EXPECT_EQ(grotto::eval_reduced(grotto::reduced::expm1, k, 0), 0); EXPECT_EQ(grotto::eval_reduced(grotto::reduced::log1p, k, 0), 0); const std::int64_t neg_one = -raw_of(1.0L, k); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::log1p, k, neg_one), std::domain_error); EXPECT_THROW(grotto::eval_reduced(grotto::reduced::log1p, k, neg_one - 1), std::domain_error); const std::int64_t far = raw_of(-40.0L, k); EXPECT_EQ(grotto::eval_reduced(grotto::reduced::expm1, k, far), neg_one); } }