libdpf/test/tests/principal_lut_test.cpp
Ryan Henry 875f09fec1 Record Grotto half-ulp tables and comparison geneval, and factor shared beaver terms before the quotient.
Horner and window evaluation need those tables in the tree. Comparison geneval opens the same value words as a Doerner–Shelat key. A factor common to every polynomial term is multiplied first so that preprocessing stays smaller.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 15:16:21 -06:00

252 lines
8.4 KiB
C++

#include <gtest/gtest.h>
#include "grotto/principal_lut.hpp"
#include <cmath>
#include <cstdint>
#include <vector>
namespace
{
struct sample
{
int which;
unsigned k;
std::int64_t raw;
std::int64_t y;
};
const sample kSamples[] = {
#include "principal_samples.inc"
};
long double series_coth_minus_inv(long double u)
{
const long double u2 = u * u;
return u * (1.0L / 3.0L - u2 / 45.0L + 2.0L * u2 * u2 / 945.0L);
}
long double reference(grotto::principal which, unsigned k, long double x)
{
constexpr long double pi = 3.141592653589793238462643383279502884L;
switch (which)
{
case grotto::principal::ln:
return logl(x);
case grotto::principal::exp:
return expl(ldexpl(x, -13));
case grotto::principal::sin:
return sinl(pi * x / 2);
case grotto::principal::tanf:
if (x == 0)
return 1;
{
const long double z = pi * x / 4;
return tanl(z) / z;
}
case grotto::principal::tang:
if (x < ldexpl(1, -12))
{
const long double z = pi * x / 4;
const long double z2 = z * z;
return -z / 3 - z2 * z / 45;
}
{
const long double z = pi * x / 4;
return 1 / tanl(z) - 1 / z;
}
case grotto::principal::sinh:
return sinhl(ldexpl(x, -13));
case grotto::principal::cosh:
return coshl(ldexpl(x, -13));
case grotto::principal::sqrt:
return sqrtl(x);
case grotto::principal::coth:
if (x == 0)
return 0;
{
const long double beta = 0.5L * logl(ldexpl(1, static_cast<int>(k) + 1) + 1);
const long double u = beta * x;
if (u < 0.05L)
return series_coth_minus_inv(u);
return 1 / tanhl(u) - 1 / u;
}
case grotto::principal::sec:
return 1 / cosl(pi * x / 4);
case grotto::principal::gsec:
if (x < ldexpl(1, -12))
{
const long double z = pi * x / 4;
const long double z2 = z * z;
return z / 6 + 7 * z2 * z / 360;
}
{
const long double z = pi * x / 4;
return 1 / sinl(z) - 1 / z;
}
case grotto::principal::csch:
if (x < ldexpl(1, -12))
{
const long double x2 = x * x;
return -x / 6 + 7 * x2 * x / 360;
}
return 1 / sinhl(x) - 1 / x;
}
return 0;
}
std::int64_t domain_left(grotto::principal which, unsigned k)
{
if (which == grotto::principal::ln || which == grotto::principal::sqrt)
return std::int64_t{1} << (k - 1);
return 0;
}
std::int64_t domain_right(unsigned k)
{
return std::int64_t{1} << k;
}
void expect_close(grotto::principal which, unsigned k, std::int64_t raw)
{
const auto y = grotto::eval_principal(which, k, raw);
const long double x = ldexpl(static_cast<long double>(raw), -static_cast<int>(k));
const long double truth = reference(which, k, x) * ldexpl(1, static_cast<int>(k));
EXPECT_LE(fabsl(static_cast<long double>(y) - truth), 1.5L) << static_cast<int>(which) << " k=" << k << " raw=" << raw;
}
} // namespace
TEST(PrincipalLut, CoarsenedPieceCountsStayPut)
{
EXPECT_EQ(grotto::principal_parts(grotto::principal::ln, 8), grotto::principal_parts(grotto::principal::ln, 32));
EXPECT_EQ(grotto::principal_parts(grotto::principal::sqrt, 8), 393);
EXPECT_EQ(grotto::principal_parts(grotto::principal::exp, 32), 2u);
EXPECT_EQ(grotto::principal_parts(grotto::principal::sinh, 16), 1u);
EXPECT_LT(grotto::principal_parts(grotto::principal::coth, 8), grotto::principal_parts(grotto::principal::coth, 32));
}
TEST(PrincipalLut, EmbeddedHornerMatches)
{
for (const sample & point : kSamples)
{
const auto which = static_cast<grotto::principal>(point.which);
EXPECT_EQ(grotto::eval_principal(which, point.k, point.raw), point.y)
<< point.which << " k=" << point.k << " raw=" << point.raw;
}
}
TEST(PrincipalLut, WithinOneUlpOnTheDomain)
{
for (unsigned which_i = 0; which_i < 12; ++which_i)
{
const auto which = static_cast<grotto::principal>(which_i);
for (unsigned k : {8u, 12u})
{
const auto left = domain_left(which, k);
const auto right = domain_right(k);
for (std::int64_t raw = left; raw <= right; ++raw)
expect_close(which, k, raw);
}
for (unsigned k : {16u, 20u, 24u, 28u, 32u})
{
const auto left = domain_left(which, k);
const auto right = domain_right(k);
const std::int64_t step = std::max<std::int64_t>(1, (right - left) / 256);
expect_close(which, k, left);
expect_close(which, k, right);
for (std::int64_t raw = left; raw < right; raw += step)
expect_close(which, k, raw);
}
}
}
TEST(PrincipalLut, RejectsBadPrecisionAndDomain)
{
EXPECT_THROW(grotto::eval_principal(grotto::principal::ln, 7, 64), std::invalid_argument);
EXPECT_THROW(grotto::eval_principal(grotto::principal::ln, 8, 0), std::out_of_range);
EXPECT_THROW(grotto::eval_principal(grotto::principal::exp, 8, -1), std::out_of_range);
EXPECT_THROW(grotto::eval_principal(grotto::principal::sin, 8, 257), std::out_of_range);
}
long double recip_reference(grotto::principal which, long double x)
{
switch (which)
{
case grotto::principal::inv:
return 1.0L / x;
case grotto::principal::rsqrt:
return 1.0L / sqrtl(x);
case grotto::principal::invsq:
return 1.0L / (x * x);
default:
return 0;
}
}
void expect_recip(grotto::principal which, unsigned k, std::int64_t raw)
{
const auto y = grotto::eval_principal(which, k, raw);
const long double x = ldexpl(static_cast<long double>(raw), -static_cast<int>(k));
const long double truth = recip_reference(which, x) * ldexpl(1.0L, static_cast<int>(k));
EXPECT_LE(fabsl(static_cast<long double>(y) - truth), 1.5L)
<< static_cast<int>(which) << " k=" << k << " raw=" << raw << " y=" << y;
}
TEST(PrincipalLut, ReciprocalPieceCounts)
{
const unsigned inv[] = {2u, 3u, 5u, 9u, 18u, 35u, 69u};
const unsigned rsqrt[] = {1u, 2u, 3u, 6u, 12u, 24u, 48u};
const unsigned invsq[] = {2u, 4u, 8u, 15u, 29u, 57u, 113u};
unsigned slot = 0;
for (unsigned k : grotto::principal_precisions)
{
EXPECT_EQ(grotto::principal_parts(grotto::principal::inv, k), inv[slot]);
EXPECT_EQ(grotto::principal_parts(grotto::principal::rsqrt, k), rsqrt[slot]);
EXPECT_EQ(grotto::principal_parts(grotto::principal::invsq, k), invsq[slot]);
++slot;
}
}
TEST(PrincipalLut, ReciprocalWithinOneAndAHalfUlp)
{
const grotto::principal maps[] = {
grotto::principal::inv,
grotto::principal::rsqrt,
grotto::principal::invsq,
};
for (const auto which : maps)
{
for (unsigned k : {8u, 12u, 16u})
{
const auto left = std::int64_t{1} << (k - 1);
const auto right = std::int64_t{1} << k;
for (std::int64_t raw = left; raw <= right; ++raw)
expect_recip(which, k, raw);
}
for (unsigned k : {20u, 24u, 28u, 32u})
{
const auto left = std::int64_t{1} << (k - 1);
const auto right = std::int64_t{1} << k;
const std::int64_t step = std::max<std::int64_t>(1, (right - left) / 4096);
expect_recip(which, k, left);
expect_recip(which, k, right);
for (std::int64_t raw = left; raw < right; raw += step)
expect_recip(which, k, raw);
}
}
}
TEST(PrincipalLut, ReciprocalRejectsOutsidePrincipalInterval)
{
EXPECT_THROW(grotto::eval_principal(grotto::principal::inv, 7, 128), std::invalid_argument);
EXPECT_THROW(grotto::eval_principal(grotto::principal::inv, 8, 127), std::out_of_range);
EXPECT_THROW(grotto::eval_principal(grotto::principal::inv, 8, 257), std::out_of_range);
EXPECT_THROW(grotto::eval_principal(grotto::principal::rsqrt, 12, 2047), std::out_of_range);
EXPECT_THROW(grotto::eval_principal(grotto::principal::invsq, 16, (std::int64_t{1} << 16) + 1), std::out_of_range);
EXPECT_THROW(grotto::eval_principal(grotto::principal::invsq, 8, -1), std::out_of_range);
EXPECT_NO_THROW(grotto::eval_principal(grotto::principal::inv, 8, 128));
EXPECT_NO_THROW(grotto::eval_principal(grotto::principal::rsqrt, 8, 256));
EXPECT_NO_THROW(grotto::eval_principal(grotto::principal::invsq, 12, 4096));
}