libdpf/test/tests/offset_horner_test.cpp
Ryan Henry e4e666f459 Initial import of libdpf.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 14:08:32 -06:00

724 lines
25 KiB
C++

#include <gtest/gtest.h>
#include "dpf.hpp"
#include "grotto/offset_horner.hpp"
#include "grotto/prefix_parity.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <limits>
#include <random>
#include <string>
#include <utility>
#include <vector>
namespace
{
using grotto::offset_horner_group_add;
using grotto::offset_horner_group_sub;
template <typename T>
uint64_t lift(T v)
{
if constexpr (std::is_signed_v<T>)
return static_cast<uint64_t>(static_cast<std::int64_t>(v));
else
return static_cast<uint64_t>(v);
}
template <typename T>
int circular_piece(T point, const std::vector<T> & knots)
{
if (knots.size() <= 1)
return 0;
for (std::size_t i = 0; i + 1 < knots.size(); ++i)
{
if (point >= knots[i] && point < knots[i + 1])
return static_cast<int>(i);
}
return static_cast<int>(knots.size() - 1);
}
template <std::size_t Degree>
uint64_t power_sum(const std::array<uint64_t, Degree + 1> & a, uint64_t point)
{
uint64_t acc = 0;
uint64_t p = 1;
for (std::size_t m = 0; m <= Degree; ++m)
{
acc += a[m] * p;
p *= point;
}
return acc;
}
template <std::size_t Degree>
std::array<uint64_t, Degree + 1> binomial_shift(
const std::array<uint64_t, Degree + 1> & a, uint64_t center)
{
static constexpr uint64_t binom[4][4] = {
{1, 0, 0, 0},
{1, 1, 0, 0},
{1, 2, 1, 0},
{1, 3, 3, 1},
};
std::array<uint64_t, Degree + 1> c{};
for (std::size_t m = 0; m <= Degree; ++m)
{
for (std::size_t k = 0; k <= m; ++k)
{
uint64_t cmk = 1;
for (std::size_t t = 0; t < m - k; ++t)
cmk *= center;
c[k] += a[m] * binom[m][k] * cmk;
}
}
return c;
}
template <std::size_t Degree, typename T>
const std::array<uint64_t, Degree + 1> & coeff_of_wrapped(
T center, T eta, const std::vector<T> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff)
{
struct row
{
T knot;
std::size_t id;
};
std::vector<row> rows(knots.size());
for (std::size_t i = 0; i < knots.size(); ++i)
rows[i] = row{offset_horner_group_sub(knots[i], eta), i};
std::sort(rows.begin(), rows.end(),
[](const row & a, const row & b) { return a.knot < b.knot; });
std::vector<T> shifted(rows.size());
for (std::size_t i = 0; i < rows.size(); ++i)
shifted[i] = rows[i].knot;
const int hot = circular_piece(center, shifted);
return coeff[rows[static_cast<std::size_t>(hot)].id];
}
template <std::size_t Degree, typename T>
uint64_t gold(T center, T eta, const std::vector<T> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff)
{
const auto & a = coeff_of_wrapped<Degree>(center, eta, knots, coeff);
return power_sum<Degree>(a, lift(center) + lift(eta));
}
template <std::size_t Party, std::size_t Degree, typename T>
uint64_t party_eval(const grotto::offset_horner_keys<T, Degree> & mat,
const std::vector<T> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff, T eta)
{
return grotto::offset_horner_eval<Party, Degree>(mat, knots, coeff, eta);
}
template <std::size_t Degree, typename T>
uint64_t open_eval(const grotto::offset_horner_keys<T, Degree> & mat,
const std::vector<T> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff, T eta)
{
return party_eval<0, Degree>(mat, knots, coeff, eta)
+ party_eval<1, Degree>(mat, knots, coeff, eta);
}
template <std::size_t Degree, typename T>
std::array<uint64_t, Degree + 1> open_coeffs(
const grotto::offset_horner_keys<T, Degree> & mat,
const std::vector<T> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff, T eta)
{
auto a = grotto::offset_horner_coefficient_share<0, Degree>(mat, knots, coeff, eta);
auto b = grotto::offset_horner_coefficient_share<1, Degree>(mat, knots, coeff, eta);
for (std::size_t k = 0; k <= Degree; ++k)
a[k] += b[k];
return a;
}
inline std::vector<std::array<uint64_t, 4>> pad3(
std::initializer_list<std::initializer_list<uint64_t>> rows)
{
std::vector<std::array<uint64_t, 4>> out;
out.reserve(rows.size());
for (const auto & row : rows)
{
std::array<uint64_t, 4> a{};
std::size_t k = 0;
for (uint64_t v : row)
{
if (k >= 4)
break;
a[k++] = v;
}
out.push_back(a);
}
return out;
}
template <std::size_t Degree>
std::vector<std::array<uint64_t, Degree + 1>> take_degree(
const std::vector<std::array<uint64_t, 4>> & rows)
{
std::vector<std::array<uint64_t, Degree + 1>> out(rows.size());
for (std::size_t i = 0; i < rows.size(); ++i)
for (std::size_t k = 0; k <= Degree; ++k)
out[i][k] = rows[i][k];
return out;
}
} // namespace
TEST(OffsetHorner, BinomialAgreesWithPowerSum)
{
const std::array<uint64_t, 4> a{5, 0, 1, 2};
const auto c = binomial_shift<3>(a, 3);
EXPECT_EQ(c[0], 68u);
EXPECT_EQ(c[1], 60u);
EXPECT_EQ(c[2], 19u);
EXPECT_EQ(c[3], 2u);
uint64_t y = 0;
uint64_t p = 1;
for (uint64_t ck : c)
{
y += ck * p;
p *= 4;
}
EXPECT_EQ(y, 740u);
EXPECT_EQ(power_sum<3>(a, 7), 740u);
}
TEST(OffsetHorner, HandCubicAtCenterPlusEta)
{
constexpr std::size_t D = 3;
const std::vector<uint8_t> knots{0};
const auto coeff = take_degree<D>(pad3({{5, 0, 1, 2}}));
const uint8_t center = 3;
const uint8_t eta = 4;
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), 740u);
EXPECT_EQ(grotto::offset_horner_clear<D>(center, knots, coeff, eta), 740u);
const auto got = open_coeffs<D>(mat, knots, coeff, eta);
const auto want = binomial_shift<D>(coeff[0], lift(center));
EXPECT_EQ(got, want);
// Each party evaluates from its own shares and the public eta.
const uint64_t p0 = party_eval<0, D>(mat, knots, coeff, eta);
const uint64_t p1 = party_eval<1, D>(mat, knots, coeff, eta);
EXPECT_EQ(p0 + p1, 740u);
EXPECT_NE(p0, 740u);
}
TEST(OffsetHorner, MultiPieceSelectsWrappedInput)
{
constexpr std::size_t D = 3;
const std::vector<uint8_t> knots{0, 10, 50};
const auto coeff = take_degree<D>(pad3({
{1, 0, 0, 0},
{0, 2, 0, 0},
{7, 0, 0, 1},
}));
const uint8_t center = 12;
const uint8_t eta = 3;
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
const uint64_t want = gold<D>(center, eta, knots, coeff);
EXPECT_EQ(want, 30u);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), want);
}
TEST(OffsetHorner, UnreducedSumIsNotTheWrappedRepresentative)
{
constexpr std::size_t D = 1;
const std::vector<uint8_t> knots{0, 30, 80};
const auto coeff = take_degree<D>(pad3({
{0, 1, 0, 0},
{0, 2, 0, 0},
{9, 0, 0, 0},
}));
const uint8_t center = 144;
const uint8_t eta = 156;
const uint8_t wrapped = offset_horner_group_add(center, eta);
EXPECT_EQ(wrapped, 44);
EXPECT_EQ(lift(center) + lift(eta), 300u);
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
const uint64_t got = open_eval<D>(mat, knots, coeff, eta);
EXPECT_EQ(got, gold<D>(center, eta, knots, coeff));
EXPECT_EQ(got, 600u);
const int piece = circular_piece(wrapped, knots);
const uint64_t at_wrapped = power_sum<D>(coeff[static_cast<std::size_t>(piece)], lift(wrapped));
EXPECT_EQ(at_wrapped, 88u);
EXPECT_NE(got, at_wrapped);
}
TEST(OffsetHorner, XPlusRWiring)
{
constexpr std::size_t D = 2;
const std::vector<uint8_t> knots{0, 40, 100};
const auto coeff = take_degree<D>(pad3({
{3, 1, 0, 0},
{0, 0, 1, 0},
{4, 0, 0, 0},
}));
const uint8_t x = 20;
const uint8_t r = 6;
const auto q = grotto::offset_horner_at_x_plus_r(x, r);
EXPECT_EQ(q.eta, offset_horner_group_sub(x, r));
EXPECT_EQ(q.center, offset_horner_group_add(r, r));
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(q.center);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, q.eta), gold<D>(q.center, q.eta, knots, coeff));
}
TEST(OffsetHorner, DegreesZeroOneAndTwo)
{
const std::vector<uint8_t> knots{0, 20, 40};
const auto full = pad3({
{4, 0, 0, 0},
{1, 3, 0, 0},
{2, 0, 5, 0},
});
const uint8_t center = 25;
const uint8_t eta = 7;
{
constexpr std::size_t D = 0;
const auto coeff = take_degree<D>(full);
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), gold<D>(center, eta, knots, coeff));
}
{
constexpr std::size_t D = 1;
const auto coeff = take_degree<D>(full);
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), gold<D>(center, eta, knots, coeff));
}
{
constexpr std::size_t D = 2;
const auto coeff = take_degree<D>(full);
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), gold<D>(center, eta, knots, coeff));
EXPECT_EQ(open_coeffs<D>(mat, knots, coeff, eta),
grotto::offset_horner_clear_coefficients<D>(center, knots, coeff, eta));
}
}
TEST(OffsetHorner, WholeDomainAndWrapPiece)
{
constexpr std::size_t D = 3;
const auto only = take_degree<D>(pad3({{8, 1, 0, 1}}));
auto one = grotto::make_offset_horner_keys<uint8_t, D>(uint8_t{200});
EXPECT_EQ(open_eval<D>(one, std::vector<uint8_t>{0}, only, uint8_t{9}),
gold<D>(uint8_t{200}, uint8_t{9}, std::vector<uint8_t>{0}, only));
const std::vector<uint8_t> knots{10, 20};
const auto coeff = take_degree<D>(pad3({
{1, 0, 0, 0},
{6, 2, 0, 0},
}));
for (uint8_t center : {uint8_t{0}, uint8_t{5}, uint8_t{10}, uint8_t{19}, uint8_t{20}, uint8_t{255}})
{
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
for (uint8_t eta : {uint8_t{0}, uint8_t{1}, uint8_t{15}, uint8_t{200}})
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), gold<D>(center, eta, knots, coeff))
<< "center=" << int(center) << " eta=" << int(eta);
}
}
TEST(OffsetHorner, CenterOrEtaZero)
{
constexpr std::size_t D = 3;
const std::vector<uint8_t> knots{0, 8, 16, 64, 200};
const auto coeff = take_degree<D>(pad3({
{1, 2, 3, 4},
{5, 0, 1, 0},
{0, 0, 0, 1},
{9, 9, 0, 0},
{2, 0, 0, 0},
}));
auto at0 = grotto::make_offset_horner_keys<uint8_t, D>(uint8_t{0});
EXPECT_EQ(open_eval<D>(at0, knots, coeff, uint8_t{3}), gold<D>(uint8_t{0}, uint8_t{3}, knots, coeff));
auto at = grotto::make_offset_horner_keys<uint8_t, D>(uint8_t{70});
EXPECT_EQ(open_eval<D>(at, knots, coeff, uint8_t{0}), gold<D>(uint8_t{70}, uint8_t{0}, knots, coeff));
}
TEST(OffsetHorner, NegativeCoefficientsAndSignedDomain)
{
constexpr std::size_t D = 3;
const std::vector<int8_t> knots{-128, -40, -1, 0, 20, 100};
const auto coeff = take_degree<D>(pad3({
{uint64_t(-3), 4, 0, 1},
{0, uint64_t(-1), 2, 0},
{8, 0, 0, 0},
{1, 1, 1, 1},
{uint64_t(-5), uint64_t(-5), 0, 0},
{2, 0, uint64_t(-1), 0},
}));
const int8_t center = -2;
const int8_t eta = -3;
auto mat = grotto::make_offset_horner_keys<int8_t, D>(center);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), gold<D>(center, eta, knots, coeff));
EXPECT_EQ(open_coeffs<D>(mat, knots, coeff, eta),
binomial_shift<D>(coeff_of_wrapped<D>(center, eta, knots, coeff), lift(center)));
auto at_max = grotto::make_offset_horner_keys<int8_t, D>(int8_t{127});
EXPECT_EQ(open_eval<D>(at_max, knots, coeff, int8_t{-4}),
gold<D>(int8_t{127}, int8_t{-4}, knots, coeff));
auto at_min = grotto::make_offset_horner_keys<int8_t, D>(std::numeric_limits<int8_t>::min());
EXPECT_EQ(open_eval<D>(at_min, knots, coeff, int8_t{1}),
gold<D>(std::numeric_limits<int8_t>::min(), int8_t{1}, knots, coeff));
}
TEST(OffsetHorner, ShiftedKnotsThatAreNotSortedStillSelect)
{
constexpr std::size_t D = 2;
const std::vector<uint8_t> knots{0, 10, 20};
const auto coeff = take_degree<D>(pad3({
{1, 0, 0, 0},
{0, 3, 0, 0},
{0, 0, 2, 0},
}));
// eta = 5 rotates 0,10,20 to 251,5,15. The walk sees them sorted.
const uint8_t eta = 5;
for (uint8_t center : {uint8_t{3}, uint8_t{6}, uint8_t{16}, uint8_t{252}})
{
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), gold<D>(center, eta, knots, coeff))
<< int(center);
const uint8_t wrapped = offset_horner_group_add(center, eta);
const auto & selected = coeff_of_wrapped<D>(center, eta, knots, coeff);
EXPECT_EQ(selected, coeff[static_cast<std::size_t>(circular_piece(wrapped, knots))]);
}
}
TEST(OffsetHorner, RingOverflowDiffersFromWideInteger)
{
constexpr std::size_t D = 3;
const std::vector<uint32_t> knots{0};
const std::array<uint64_t, 4> a{0, 0, 0, 1};
const auto coeff = std::vector<std::array<uint64_t, 4>>{a};
const uint32_t center = 3u << 20;
const uint32_t eta = 1u << 20;
using u128 = unsigned __int128;
const u128 wide = u128(center) + eta;
const u128 wide_p = wide * wide * wide;
auto mat = grotto::make_offset_horner_keys<uint32_t, D>(center);
const uint64_t got = open_eval<D>(mat, knots, coeff, eta);
EXPECT_EQ(got, gold<D>(center, eta, knots, coeff));
EXPECT_EQ(got, static_cast<uint64_t>(wide_p));
EXPECT_NE(wide_p, u128(got));
}
TEST(OffsetHorner, WrapSharesHideThePayload)
{
constexpr std::size_t D = 3;
const uint8_t center = 9;
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
uint64_t pow = 1;
for (std::size_t m = 0; m <= D; ++m)
{
EXPECT_EQ(mat.wrap_share[m][0] + mat.wrap_share[m][1], pow);
if (pow != 0)
EXPECT_NE(mat.wrap_share[m][0], pow);
pow *= lift(center);
}
}
TEST(OffsetHorner, PowerKeysAreIndependentComparisons)
{
constexpr std::size_t D = 3;
const uint16_t center = 1000;
auto mat = grotto::make_offset_horner_keys<uint16_t, D>(center);
const auto & k0 = std::get<0>(mat.keys[0]);
const auto & k1 = std::get<0>(mat.keys[1]);
EXPECT_NE(std::memcmp(&k0.root(), &k1.root(), sizeof(k0.root())), 0);
bool cw_differs = false;
const std::size_t depth = std::remove_reference_t<decltype(k0)>::depth;
for (std::size_t level = 0; level < depth; ++level)
{
if (k0.value_cw(level) != k1.value_cw(level))
cw_differs = true;
}
EXPECT_TRUE(cw_differs);
EXPECT_EQ(mat.keys.size(), D + 1);
}
TEST(OffsetHorner, DegreeZeroMatchesSignRespectingDot)
{
constexpr std::size_t D = 0;
const std::vector<uint8_t> knots{0, 15, 80, 200};
const auto coeff = take_degree<D>(pad3({
{4, 0, 0, 0},
{11, 0, 0, 0},
{uint64_t(-2), 0, 0, 0},
{9, 0, 0, 0},
}));
const uint8_t center = 90;
const uint8_t eta = 30;
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
struct row { uint8_t knot; uint64_t a; };
std::vector<row> rows;
for (std::size_t i = 0; i < knots.size(); ++i)
rows.push_back(row{offset_horner_group_sub(knots[i], eta), coeff[i][0]});
std::sort(rows.begin(), rows.end(),
[](const row & a, const row & b) { return a.knot < b.knot; });
std::array<uint8_t, 4> shifted{};
for (std::size_t i = 0; i < rows.size(); ++i)
shifted[i] = rows[i].knot;
auto unit = dpf::make_dpf(center, dpf::gt(uint64_t{1}));
const auto s0 = grotto::signed_segment_parities(std::get<0>(unit), shifted);
const auto s1 = grotto::signed_segment_parities(std::get<1>(unit), shifted);
uint64_t dot = 0;
for (std::size_t i = 0; i < rows.size(); ++i)
dot += (s0[i] + s1[i]) * rows[i].a;
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), dot);
EXPECT_EQ(dot, gold<D>(center, eta, knots, coeff));
}
TEST(OffsetHorner, AdviceBitSignIsNotACoefficientShare)
{
const std::vector<uint8_t> knots{0, 10, 40, 90, 140, 200};
std::array<uint8_t, 6> ends{};
for (std::size_t i = 0; i < knots.size(); ++i)
ends[i] = knots[i];
const std::array<uint64_t, 6> constants{3, 5, 7, 11, 13, 17};
bool saw_negative = false;
bool saw_positive = false;
for (int alpha = 0; alpha < 256; ++alpha)
{
const auto a = static_cast<uint8_t>(alpha);
auto [k0, k1] = dpf::make_dpf(a, dpf::bit::one);
const auto s0 = grotto::segment_parities(k0, ends);
const auto s1 = grotto::segment_parities(k1, ends);
int sign = 0;
int64_t acc = 0;
for (std::size_t i = 0; i < ends.size(); ++i)
{
const int bit = int(s0[i]) - int(s1[i]);
sign += bit;
acc += bit * static_cast<int64_t>(constants[i]);
}
ASSERT_EQ(sign * sign, 1) << alpha;
const int64_t corrected = sign * acc;
const int hot = circular_piece(a, knots);
ASSERT_EQ(corrected, static_cast<int64_t>(constants[static_cast<std::size_t>(hot)])) << alpha;
if (sign < 0)
{
saw_negative = true;
EXPECT_EQ(acc, -corrected);
}
else
{
saw_positive = true;
}
}
EXPECT_TRUE(saw_negative);
EXPECT_TRUE(saw_positive);
// The sign-respecting unit payload does not flip.
const uint8_t probe = 40;
auto cmp = dpf::make_dpf(probe, dpf::gt(uint64_t{1}));
const auto p0 = grotto::signed_segment_parities(std::get<0>(cmp), ends);
const auto p1 = grotto::signed_segment_parities(std::get<1>(cmp), ends);
uint64_t opened = 0;
for (std::size_t i = 0; i < ends.size(); ++i)
opened += (p0[i] + p1[i]) * constants[i];
EXPECT_EQ(opened, constants[static_cast<std::size_t>(circular_piece(probe, knots))]);
}
TEST(OffsetHorner, SignRespectingOneHotIsNotTheShiftedCubic)
{
constexpr std::size_t D = 2;
const std::vector<uint8_t> knots{0, 50, 150};
const auto coeff = take_degree<D>(pad3({
{1, 0, 0, 0},
{0, 4, 1, 0},
{8, 0, 0, 0},
}));
const uint8_t center = 10;
const uint8_t eta = 60;
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
const uint64_t ours = open_eval<D>(mat, knots, coeff, eta);
auto unit = dpf::make_dpf(center, dpf::gt(uint64_t{1}));
std::array<uint8_t, 3> ends{0, 50, 150};
const auto s0 = grotto::signed_segment_parities(std::get<0>(unit), ends);
const auto s1 = grotto::signed_segment_parities(std::get<1>(unit), ends);
uint64_t const_term = 0;
for (std::size_t i = 0; i < ends.size(); ++i)
const_term += (s0[i] + s1[i]) * coeff[i][0];
// Unit payload, unshifted knots: the piece of `center`, and only its constant.
EXPECT_EQ(const_term, 1u);
EXPECT_NE(ours, const_term);
EXPECT_EQ(ours, gold<D>(center, eta, knots, coeff));
}
TEST(OffsetHorner, PrefixIntoMatchesFixedArray)
{
const uint8_t alpha = 40;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::gt(uint64_t{7}));
const std::array<uint8_t, 5> ends{0, 1, 10, 40, 200};
const auto fixed = grotto::signed_prefix_parities(k0, ends);
uint64_t into[5] = {};
grotto::signed_prefix_parities_into(k0, ends.data(), ends.size(), into);
for (std::size_t i = 0; i < ends.size(); ++i)
EXPECT_EQ(into[i], fixed[i]);
const uint64_t mask = k0.cmp().mask;
for (std::size_t i = 0; i < ends.size(); ++i)
{
const auto one = std::array<uint8_t, 1>{ends[i]};
const auto alone0 = grotto::signed_prefix_parities(k0, one);
const auto alone1 = grotto::signed_prefix_parities(k1, one);
EXPECT_EQ((alone0[0] + alone1[0]) & mask, ends[i] > alpha ? 7u : 0u);
}
}
TEST(OffsetHorner, RejectsBadKnots)
{
constexpr std::size_t D = 1;
const auto coeff = take_degree<D>(pad3({{1, 1, 0, 0}, {2, 0, 0, 0}}));
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(uint8_t{1});
EXPECT_THROW(open_eval<D>(mat, std::vector<uint8_t>{}, coeff, uint8_t{0}), std::invalid_argument);
EXPECT_THROW(open_eval<D>(mat, std::vector<uint8_t>{1, 1}, coeff, uint8_t{0}), std::invalid_argument);
EXPECT_THROW(open_eval<D>(mat, std::vector<uint8_t>{3, 2}, coeff, uint8_t{0}), std::invalid_argument);
EXPECT_THROW(open_eval<D>(mat, std::vector<uint8_t>{0}, coeff, uint8_t{0}), std::invalid_argument);
}
TEST(OffsetHorner, ManyPiecesAndRandomUint16)
{
constexpr std::size_t D = 3;
std::mt19937 rng(0x0ff5e7u);
std::vector<uint16_t> knots;
for (uint16_t k = 0; knots.size() < 20; k = static_cast<uint16_t>(k + 1000))
knots.push_back(k);
std::vector<std::array<uint64_t, D + 1>> coeff(knots.size());
for (auto & row : coeff)
for (uint64_t & a : row)
a = rng();
std::uniform_int_distribution<int> dist(0, 65535);
for (int trial = 0; trial < 30; ++trial)
{
const auto center = static_cast<uint16_t>(dist(rng));
const auto eta = static_cast<uint16_t>(dist(rng));
auto mat = grotto::make_offset_horner_keys<uint16_t, D>(center);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), gold<D>(center, eta, knots, coeff))
<< trial;
EXPECT_EQ(open_coeffs<D>(mat, knots, coeff, eta),
binomial_shift<D>(coeff_of_wrapped<D>(center, eta, knots, coeff), lift(center)));
}
}
TEST(OffsetHorner, ExhaustiveUint8AgreesWithGoldAndCountsWraps)
{
constexpr std::size_t D = 2;
const std::vector<uint8_t> knots{0, 30, 80, 140, 200};
const auto coeff = take_degree<D>(pad3({
{1, 2, 0, 0},
{0, 0, 1, 0},
{4, 1, 0, 0},
{9, 0, 2, 0},
{3, 5, 0, 0},
}));
int point_mismatch = 0;
int value_mismatch = 0;
int piece_mismatch = 0;
for (int c = 0; c < 256; ++c)
{
const auto center = static_cast<uint8_t>(c);
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
for (int e = 0; e < 256; ++e)
{
const auto eta = static_cast<uint8_t>(e);
const uint64_t got = open_eval<D>(mat, knots, coeff, eta);
const uint64_t want = gold<D>(center, eta, knots, coeff);
if (got != want)
{
ADD_FAILURE() << "center=" << c << " eta=" << e
<< " got=" << got << " want=" << want;
return;
}
const uint8_t wrapped = offset_horner_group_add(center, eta);
const uint64_t unreduced = lift(center) + lift(eta);
if (unreduced != lift(wrapped))
++point_mismatch;
const auto & selected = coeff_of_wrapped<D>(center, eta, knots, coeff);
const auto & wrapped_row = coeff[static_cast<std::size_t>(circular_piece(wrapped, knots))];
if (selected != wrapped_row)
++piece_mismatch;
if (power_sum<D>(selected, unreduced) != power_sum<D>(selected, lift(wrapped)))
++value_mismatch;
}
}
EXPECT_EQ(point_mismatch, 32640);
EXPECT_EQ(piece_mismatch, 0);
EXPECT_GT(value_mismatch, 0);
EXPECT_LT(value_mismatch, 65536);
}
TEST(OffsetHorner, ExhaustiveInt8AgreesWithGoldAndCountsOverflows)
{
constexpr std::size_t D = 3;
const std::vector<int8_t> knots{-128, -50, -3, 0, 40, 90};
const auto coeff = take_degree<D>(pad3({
{1, 0, 0, 1},
{0, uint64_t(-2), 0, 0},
{5, 1, 1, 0},
{0, 0, 3, 0},
{2, 0, 0, 0},
{uint64_t(-4), 1, 0, 1},
}));
int point_mismatch = 0;
int piece_mismatch = 0;
for (int c = -128; c <= 127; ++c)
{
const auto center = static_cast<int8_t>(c);
auto mat = grotto::make_offset_horner_keys<int8_t, D>(center);
for (int e = -128; e <= 127; ++e)
{
const auto eta = static_cast<int8_t>(e);
const uint64_t got = open_eval<D>(mat, knots, coeff, eta);
const uint64_t want = gold<D>(center, eta, knots, coeff);
if (got != want)
{
ADD_FAILURE() << "center=" << c << " eta=" << e
<< " got=" << got << " want=" << want;
return;
}
const int8_t wrapped = offset_horner_group_add(center, eta);
const uint64_t unreduced = lift(center) + lift(eta);
if (unreduced != lift(wrapped))
++point_mismatch;
const auto & selected = coeff_of_wrapped<D>(center, eta, knots, coeff);
const auto & wrapped_row = coeff[static_cast<std::size_t>(circular_piece(wrapped, knots))];
if (selected != wrapped_row)
++piece_mismatch;
}
}
EXPECT_EQ(point_mismatch, 16384);
EXPECT_EQ(piece_mismatch, 0);
}
TEST(OffsetHorner, HornerOfOpenedCoefficientsMatchesValue)
{
constexpr std::size_t D = 3;
std::mt19937 rng(1);
const std::vector<uint8_t> knots{0, 25, 100, 180};
std::vector<std::array<uint64_t, D + 1>> coeff(knots.size());
for (auto & row : coeff)
for (uint64_t & a : row)
a = rng();
const uint8_t center = 77;
const uint8_t eta = 19;
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
const auto c = open_coeffs<D>(mat, knots, coeff, eta);
uint64_t y = 0;
uint64_t p = 1;
const uint64_t e = lift(eta);
for (uint64_t ck : c)
{
y += ck * p;
p *= e;
}
EXPECT_EQ(y, open_eval<D>(mat, knots, coeff, eta));
EXPECT_EQ(y, gold<D>(center, eta, knots, coeff));
}