libdpf/test/tests/offset_horner_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

1402 lines
49 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 T wrapped = offset_horner_group_add(center, eta);
const auto & a = coeff[static_cast<std::size_t>(circular_piece(wrapped, knots))];
return power_sum<Degree>(a, lift(wrapped));
}
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);
uint64_t summed = 0;
for (uint64_t term : got)
summed += term;
EXPECT_EQ(summed, 740u);
const auto q = grotto::offset_horner_clear_coefficients<D>(center, knots, coeff, eta);
EXPECT_EQ(q, binomial_shift<D>(coeff[0], lift(eta)));
// 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, CarrySplitEvaluatesTheWrappedRepresentative)
{
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, 88u);
EXPECT_NE(got, 600u);
const std::vector<int8_t> sknots{-128, 0};
const auto scoeff = take_degree<D>(pad3({
{0, 3, 0, 0},
{5, 0, 0, 0},
}));
const int8_t sc = 100;
const int8_t se = 100;
const int8_t sw = offset_horner_group_add(sc, se);
EXPECT_EQ(sw, int8_t{-56});
auto smat = grotto::make_offset_horner_keys<int8_t, D>(sc);
EXPECT_EQ(open_eval<D>(smat, sknots, scoeff, se), gold<D>(sc, se, sknots, scoeff));
EXPECT_EQ(open_eval<D>(smat, sknots, scoeff, se),
power_sum<D>(scoeff[0], lift(sw)));
}
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));
const auto parts = open_coeffs<D>(mat, knots, coeff, eta);
uint64_t summed = 0;
for (uint64_t term : parts)
summed += term;
EXPECT_EQ(summed, open_eval<D>(mat, 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));
const auto parts = open_coeffs<D>(mat, knots, coeff, eta);
uint64_t summed = 0;
for (uint64_t term : parts)
summed += term;
EXPECT_EQ(summed, open_eval<D>(mat, knots, coeff, eta));
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;
const auto parts = open_coeffs<D>(mat, knots, coeff, eta);
uint64_t summed = 0;
for (uint64_t term : parts)
summed += term;
EXPECT_EQ(summed, gold<D>(center, eta, knots, coeff)) << trial;
}
}
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);
const uint64_t cleared = grotto::offset_horner_clear<D>(center, knots, coeff, eta);
if (got != want || cleared != want)
{
ADD_FAILURE() << "center=" << c << " eta=" << e
<< " got=" << got << " clear=" << cleared
<< " 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);
const uint64_t cleared = grotto::offset_horner_clear<D>(center, knots, coeff, eta);
if (got != want || cleared != want)
{
ADD_FAILURE() << "center=" << c << " eta=" << e
<< " got=" << got << " clear=" << cleared
<< " 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, GenevalXorSharesMatchTheDealerPoint)
{
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 share = 0x3c;
const uint8_t other = static_cast<uint8_t>(center ^ share);
const uint8_t eta = 3;
EXPECT_EQ(grotto::geneval_offset_horner_center(share, other), center);
EXPECT_EQ(grotto::geneval_offset_horner_center(center, uint8_t{0}), center);
const auto got = grotto::geneval_offset_horner<D>(share, other, eta, knots, coeff);
EXPECT_EQ(got.center, center);
EXPECT_EQ(got.eta, eta);
EXPECT_EQ(got.value0 + got.value1, gold<D>(center, eta, knots, coeff));
EXPECT_EQ(got.value0 + got.value1, 30u);
}
TEST(OffsetHorner, GenevalFromAdditiveSharesOfXAndR)
{
constexpr std::size_t D = 2;
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 x = 100;
const uint8_t r = 200;
const uint8_t x0 = 7;
const uint8_t r0 = 11;
const uint8_t x1 = offset_horner_group_sub(x, x0);
const uint8_t r1 = offset_horner_group_sub(r, r0);
const auto got = grotto::geneval_offset_horner<D>(x0, x1, r0, r1, knots, coeff);
const uint8_t eta = offset_horner_group_sub(x, r);
const uint8_t center = offset_horner_group_add(r, r);
EXPECT_EQ(got.eta, eta);
EXPECT_EQ(got.center, center);
EXPECT_EQ(got.value0 + got.value1, gold<D>(center, eta, knots, coeff));
EXPECT_EQ(got.value0 + got.value1, 88u);
const uint8_t wrapped = offset_horner_group_add(center, eta);
EXPECT_EQ(wrapped, 44);
EXPECT_EQ(got.value0 + got.value1,
power_sum<D>(coeff[static_cast<std::size_t>(circular_piece(wrapped, knots))],
lift(wrapped)));
}
TEST(OffsetHorner, GenevalSignedSharesUseGenevalConvention)
{
constexpr std::size_t D = 3;
const std::vector<int8_t> knots{-128, -40, 0, 20, 100};
const auto coeff = take_degree<D>(pad3({
{uint64_t(-3), 4, 0, 1},
{0, uint64_t(-1), 2, 0},
{1, 1, 1, 1},
{uint64_t(-5), uint64_t(-5), 0, 0},
{2, 0, uint64_t(-1), 0},
}));
const int8_t center = -20;
const int8_t share = 3;
const int8_t other = static_cast<int8_t>(center ^ share);
const int8_t eta = -3;
EXPECT_EQ(grotto::geneval_offset_horner_center(share, other), center);
const auto got = grotto::geneval_offset_horner<D>(share, other, eta, knots, coeff);
EXPECT_EQ(got.center, center);
EXPECT_EQ(got.value0 + got.value1, gold<D>(center, eta, knots, coeff));
const int8_t x = 40;
const int8_t r = -15;
const int8_t x0 = -100;
const int8_t r0 = 50;
const auto from_mask = grotto::geneval_offset_horner<D>(
x0, offset_horner_group_sub(x, x0),
r0, offset_horner_group_sub(r, r0),
knots, coeff);
const int8_t expect_center = offset_horner_group_add(r, r);
const int8_t expect_eta = offset_horner_group_sub(x, r);
EXPECT_EQ(from_mask.center, expect_center);
EXPECT_EQ(from_mask.eta, expect_eta);
EXPECT_EQ(from_mask.value0 + from_mask.value1,
gold<D>(expect_center, expect_eta, knots, coeff));
}
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;
for (uint64_t ck : c)
y += ck;
EXPECT_EQ(y, open_eval<D>(mat, knots, coeff, eta));
EXPECT_EQ(y, gold<D>(center, eta, knots, coeff));
}
template <std::size_t Degree, typename T>
void expect_wrapped(const grotto::offset_horner_keys<T, Degree> & mat,
const std::vector<T> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
T center, T eta, const char * where)
{
const uint64_t want = gold<Degree>(center, eta, knots, coeff);
const uint64_t got = open_eval<Degree>(mat, knots, coeff, eta);
const uint64_t cleared = grotto::offset_horner_clear<Degree>(center, knots, coeff, eta);
const auto q = grotto::offset_horner_clear_coefficients<Degree>(center, knots, coeff, eta);
uint64_t horner = q[Degree];
const uint64_t limb = lift(center);
for (std::size_t k = Degree; k-- > 0; )
horner = horner * limb + q[k];
EXPECT_EQ(got, want) << where;
EXPECT_EQ(cleared, want) << where;
EXPECT_EQ(horner, want) << where;
if (got != want || cleared != want || horner != want)
return;
}
template <std::size_t Degree, typename T>
void expect_geneval(T center, T eta, const std::vector<T> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff, const char * where)
{
const T share = static_cast<T>(0x3c);
const T other = static_cast<T>(center ^ share);
const auto g = grotto::geneval_offset_horner<Degree>(share, other, eta, knots, coeff);
const uint64_t want = gold<Degree>(center, eta, knots, coeff);
EXPECT_EQ(g.center, center) << where;
EXPECT_EQ(g.value0 + g.value1, want) << where;
uint64_t summed = 0;
for (std::size_t k = 0; k <= Degree; ++k)
summed += g.coeff0[k] + g.coeff1[k];
EXPECT_EQ(summed, want) << where;
}
TEST(OffsetHorner, KnotsThatOmitZeroStillSplitTheCarry)
{
constexpr std::size_t D = 3;
const std::vector<uint8_t> knots{40, 90, 150, 220};
const auto coeff = take_degree<D>(pad3({
{1, 0, 0, 1},
{0, uint64_t(-3), 1, 0},
{4, 2, 0, uint64_t(-1)},
{9, 0, 2, 1},
}));
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 += 1)
{
const auto eta = static_cast<uint8_t>(e);
expect_wrapped<D>(mat, knots, coeff, center, eta, "omit-zero");
if (HasFailure())
{
ADD_FAILURE() << "center=" << c << " eta=" << e;
return;
}
}
}
}
TEST(OffsetHorner, SignedKnotsThatOmitTheMinimum)
{
constexpr std::size_t D = 3;
const std::vector<int8_t> knots{-40, 10, 70};
const auto coeff = take_degree<D>(pad3({
{uint64_t(-2), 1, 0, 1},
{3, 0, uint64_t(-1), 0},
{0, 4, 2, uint64_t(-3)},
}));
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);
expect_wrapped<D>(mat, knots, coeff, center, eta, "omit-min");
if (HasFailure())
{
ADD_FAILURE() << "center=" << c << " eta=" << e;
return;
}
}
}
}
TEST(OffsetHorner, CarryThresholdLandsOnEveryKnotAndOnTheDomainEnds)
{
constexpr std::size_t D = 2;
const std::vector<uint8_t> knots{1, 16, 64, 128, 200, 255};
const auto coeff = take_degree<D>(pad3({
{1, 1, 0, 0},
{2, 0, 1, 0},
{3, uint64_t(-1), 0, 0},
{4, 2, 2, 0},
{5, 0, 0, 0},
{6, 3, 1, 0},
}));
for (uint8_t knot : knots)
{
if (knot == 0)
continue;
const uint8_t eta = static_cast<uint8_t>(256u - knot);
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);
expect_wrapped<D>(mat, knots, coeff, center, eta, "threshold-on-knot");
if (HasFailure())
{
ADD_FAILURE() << "knot=" << int(knot) << " center=" << c;
return;
}
}
}
for (uint8_t eta : {uint8_t{0}, uint8_t{1}, uint8_t{255}})
{
for (uint8_t center : {uint8_t{0}, uint8_t{1}, uint8_t{254}, uint8_t{255}})
{
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
expect_wrapped<D>(mat, knots, coeff, center, eta, "domain-end");
if (HasFailure())
return;
}
}
}
TEST(OffsetHorner, DegreeZeroIsThePieceConstantOnBothSidesOfTheCarry)
{
constexpr std::size_t D = 0;
const std::vector<uint8_t> knots{10, 80, 200};
const auto coeff = take_degree<D>(pad3({
{4, 0, 0, 0},
{11, 0, 0, 0},
{uint64_t(-2), 0, 0, 0},
}));
for (int c = 0; c < 256; c += 3)
{
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 += 5)
{
const auto eta = static_cast<uint8_t>(e);
const uint64_t want = gold<D>(center, eta, knots, coeff);
EXPECT_EQ(open_eval<D>(mat, knots, coeff, eta), want);
const auto wrapped = offset_horner_group_add(center, eta);
const auto piece = static_cast<std::size_t>(circular_piece(wrapped, knots));
EXPECT_EQ(want, coeff[piece][0]);
if (HasFailure())
return;
}
}
}
TEST(OffsetHorner, CubicAcrossUnsignedAndSignedCarryHasANegativeKappa)
{
constexpr std::size_t D = 3;
const std::vector<uint8_t> uknots{1, 70};
const auto ucoeff = take_degree<D>(pad3({
{1, 0, 0, 1},
{2, 3, uint64_t(-1), 1},
}));
const uint8_t uc = 200;
const uint8_t ue = 100;
const uint8_t uw = offset_horner_group_add(uc, ue);
EXPECT_EQ(uw, 44);
EXPECT_NE(lift(uc) + lift(ue), lift(uw));
auto umat = grotto::make_offset_horner_keys<uint8_t, D>(uc);
expect_wrapped<D>(umat, uknots, ucoeff, uc, ue, "cubic-unsigned");
EXPECT_NE(open_eval<D>(umat, uknots, ucoeff, ue),
power_sum<D>(ucoeff[static_cast<std::size_t>(circular_piece(uw, uknots))],
lift(uc) + lift(ue)));
const std::vector<int8_t> sknots{-20, 30};
const auto scoeff = take_degree<D>(pad3({
{0, 0, 0, 1},
{7, 1, 0, 0},
}));
const int8_t sc = -100;
const int8_t se = -80;
const int8_t sw = offset_horner_group_add(sc, se);
EXPECT_LT(int(sc) + int(se), -128);
auto smat = grotto::make_offset_horner_keys<int8_t, D>(sc);
expect_wrapped<D>(smat, sknots, scoeff, sc, se, "cubic-signed-low");
EXPECT_EQ(open_eval<D>(smat, sknots, scoeff, se),
power_sum<D>(scoeff[static_cast<std::size_t>(circular_piece(sw, sknots))], lift(sw)));
const int8_t hc = 90;
const int8_t he = 80;
auto hmat = grotto::make_offset_horner_keys<int8_t, D>(hc);
expect_wrapped<D>(hmat, sknots, scoeff, hc, he, "cubic-signed-high");
const int8_t hw = offset_horner_group_add(hc, he);
EXPECT_GT(int(hc) + int(he), 127);
EXPECT_EQ(open_eval<D>(hmat, sknots, scoeff, he),
power_sum<D>(scoeff[static_cast<std::size_t>(circular_piece(hw, sknots))], lift(hw)));
}
TEST(OffsetHorner, ZeroPolynomialAndProperShares)
{
constexpr std::size_t D = 3;
const std::vector<uint8_t> knots{5, 40, 90};
const auto coeff = take_degree<D>(pad3({
{0, 0, 0, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},
}));
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(uint8_t{200});
EXPECT_EQ(open_eval<D>(mat, knots, coeff, uint8_t{200}), 0u);
EXPECT_EQ(grotto::offset_horner_clear<D>(uint8_t{200}, knots, coeff, uint8_t{200}), 0u);
const auto live = take_degree<D>(pad3({
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 8, 7, 6},
}));
const uint64_t p0 = party_eval<0, D>(mat, knots, live, uint8_t{180});
const uint64_t p1 = party_eval<1, D>(mat, knots, live, uint8_t{180});
const uint64_t want = gold<D>(uint8_t{200}, uint8_t{180}, knots, live);
EXPECT_EQ(p0 + p1, want);
EXPECT_NE(p0, want);
EXPECT_NE(p1, want);
}
TEST(OffsetHorner, XPlusRMatchesTheWrappedSumOnAStride)
{
constexpr std::size_t D = 3;
const std::vector<uint8_t> knots{7, 60, 130, 210};
const auto coeff = take_degree<D>(pad3({
{1, 1, 0, 1},
{0, uint64_t(-4), 2, 0},
{3, 0, 0, uint64_t(-1)},
{8, 2, 1, 0},
}));
for (int rv = 0; rv < 256; rv += 5)
{
const auto r = static_cast<uint8_t>(rv);
const auto center = offset_horner_group_add(r, r);
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
for (int xv = 0; xv < 256; xv += 5)
{
const auto x = static_cast<uint8_t>(xv);
const auto eta = offset_horner_group_sub(x, r);
const uint64_t got = open_eval<D>(mat, knots, coeff, eta);
const auto sum = offset_horner_group_add(x, r);
EXPECT_EQ(offset_horner_group_add(center, eta), sum);
EXPECT_EQ(got, gold<D>(center, eta, knots, coeff));
EXPECT_EQ(got, power_sum<D>(
coeff[static_cast<std::size_t>(circular_piece(sum, knots))], lift(sum)));
if (HasFailure())
{
ADD_FAILURE() << "x=" << xv << " r=" << rv;
return;
}
}
}
}
TEST(OffsetHorner, GenevalAgreesWithDealerAcrossCarryAndEdges)
{
constexpr std::size_t D = 3;
const std::vector<uint8_t> knots{25, 80, 140, 200};
const auto coeff = take_degree<D>(pad3({
{1, 0, 2, 1},
{uint64_t(-5), 3, 0, 1},
{4, 0, uint64_t(-2), 0},
{0, 1, 1, uint64_t(-1)},
}));
auto check = [&](uint8_t center, uint8_t eta) {
auto mat = grotto::make_offset_horner_keys<uint8_t, D>(center);
expect_wrapped<D>(mat, knots, coeff, center, eta, "dealer");
expect_geneval<D>(center, eta, knots, coeff, "geneval");
};
for (int c = 0; c < 256; c += 8)
{
for (int e = 0; e < 256; e += 8)
{
check(static_cast<uint8_t>(c), static_cast<uint8_t>(e));
if (HasFailure())
{
ADD_FAILURE() << "center=" << c << " eta=" << e;
return;
}
}
}
for (uint8_t end : {uint8_t{0}, uint8_t{1}, uint8_t{127}, uint8_t{128}, uint8_t{254}, uint8_t{255}})
{
check(end, uint8_t{1});
check(end, uint8_t{255});
check(uint8_t{200}, end);
check(uint8_t{3}, end);
if (HasFailure())
return;
}
const std::vector<int8_t> sknots{-100, -5, 20, 90};
const auto scoeff = take_degree<D>(pad3({
{1, 0, 0, 1},
{0, uint64_t(-1), 2, 0},
{4, 3, 0, uint64_t(-2)},
{9, 0, 1, 1},
}));
for (int c = -128; c <= 127; c += 9)
{
for (int e = -128; e <= 127; e += 9)
{
const auto center = static_cast<int8_t>(c);
const auto eta = static_cast<int8_t>(e);
auto mat = grotto::make_offset_horner_keys<int8_t, D>(center);
expect_wrapped<D>(mat, sknots, scoeff, center, eta, "signed-dealer");
expect_geneval<D>(center, eta, sknots, scoeff, "signed-geneval");
if (HasFailure())
{
ADD_FAILURE() << "center=" << c << " eta=" << e;
return;
}
}
}
}
TEST(OffsetHorner, WiderRandomDomainsMatchWrappedGold)
{
constexpr std::size_t D = 3;
std::mt19937 rng(0x0c0ffe);
std::uniform_int_distribution<int> u16(0, 65535);
std::vector<uint16_t> uknots{0, 1000, 8000, 20000, 40000, 60000};
std::vector<std::array<uint64_t, D + 1>> ucoeff(uknots.size());
for (auto & row : ucoeff)
for (uint64_t & a : row)
a = rng();
for (int trial = 0; trial < 40; ++trial)
{
const auto center = static_cast<uint16_t>(u16(rng));
const auto eta = static_cast<uint16_t>(u16(rng));
auto mat = grotto::make_offset_horner_keys<uint16_t, D>(center);
expect_wrapped<D>(mat, uknots, ucoeff, center, eta, "u16");
expect_geneval<D>(center, eta, uknots, ucoeff, "u16-geneval");
if (HasFailure())
return;
}
std::uniform_int_distribution<int> s16(-32768, 32767);
std::vector<int16_t> sknots{-32768, -20000, -100, 0, 5000, 30000};
std::vector<std::array<uint64_t, D + 1>> scoeff(sknots.size());
for (auto & row : scoeff)
for (uint64_t & a : row)
a = rng();
for (int trial = 0; trial < 40; ++trial)
{
const auto center = static_cast<int16_t>(s16(rng));
const auto eta = static_cast<int16_t>(s16(rng));
auto mat = grotto::make_offset_horner_keys<int16_t, D>(center);
expect_wrapped<D>(mat, sknots, scoeff, center, eta, "i16");
expect_geneval<D>(center, eta, sknots, scoeff, "i16-geneval");
if (HasFailure())
return;
}
}
template <typename T>
int64_t math_of(T value)
{
if constexpr (std::is_signed_v<T>)
return static_cast<int64_t>(value);
else
return static_cast<int64_t>(static_cast<std::make_unsigned_t<T>>(value));
}
template <typename T>
bool fits_in_domain(int64_t value)
{
return value >= math_of(std::numeric_limits<T>::min())
&& value <= math_of(std::numeric_limits<T>::max());
}
template <typename T>
bool addition_leaves_domain(T center, T eta)
{
constexpr unsigned bits = dpf::utils::bitlength_of_v<T>;
if (bits > 62)
return false;
const int64_t sum = math_of(center) + math_of(eta);
const int64_t mod = int64_t{1} << bits;
if constexpr (std::is_signed_v<T>)
return sum >= (mod >> 1) || sum < -(mod >> 1);
else
return sum >= mod;
}
template <typename T>
void exercise_big_domain()
{
constexpr std::size_t D = 3;
constexpr unsigned bits = dpf::utils::bitlength_of_v<T>;
using lim = std::numeric_limits<T>;
const T minv = lim::min();
const T maxv = lim::max();
std::vector<T> knots;
if constexpr (std::is_signed_v<T>)
{
knots.push_back(static_cast<T>(minv / 2));
knots.push_back(T{-2});
knots.push_back(T{-1});
knots.push_back(T{1});
knots.push_back(T{2});
knots.push_back(static_cast<T>(maxv / 2));
}
else
{
using u = std::make_unsigned_t<T>;
knots.push_back(T{1});
knots.push_back(T{2});
knots.push_back(static_cast<T>(u{1} << (bits / 2)));
if (bits > 1 && bits <= 63)
knots.push_back(static_cast<T>(u{1} << (bits - 1)));
knots.push_back(static_cast<T>(maxv - 2));
knots.push_back(static_cast<T>(maxv - 1));
}
std::sort(knots.begin(), knots.end());
knots.erase(std::unique(knots.begin(), knots.end()), knots.end());
ASSERT_GE(knots.size(), 4u);
ASSERT_NE(knots.front(), minv);
std::vector<std::array<uint64_t, D + 1>> coeff(knots.size());
for (std::size_t i = 0; i < knots.size(); ++i)
{
coeff[i] = {
static_cast<uint64_t>(i + 1),
static_cast<uint64_t>(-static_cast<int>(i) - 3),
static_cast<uint64_t>(i * 5 + 1),
uint64_t{1} << (8 + (i % 4)),
};
}
std::vector<T> points;
auto add_point = [&](T value) { points.push_back(value); };
add_point(minv);
add_point(static_cast<T>(minv + T{1}));
if constexpr (std::is_signed_v<T>)
{
add_point(T{-1});
add_point(T{0});
add_point(T{1});
}
else
{
add_point(T{0});
}
add_point(static_cast<T>(maxv - T{1}));
add_point(maxv);
for (T knot : knots)
{
add_point(knot);
if (knot != minv)
add_point(static_cast<T>(knot - T{1}));
if (knot != maxv)
add_point(static_cast<T>(knot + T{1}));
}
std::mt19937 rng(0xB16Du ^ bits ^ (std::is_signed_v<T> ? 0x51u : 0u));
std::uniform_int_distribution<uint64_t> dist(
0, std::numeric_limits<std::make_unsigned_t<T>>::max());
for (int n = 0; n < 24; ++n)
add_point(static_cast<T>(dist(rng)));
std::vector<T> etas = points;
if (bits <= 62)
{
const int64_t mod = int64_t{1} << bits;
const int64_t half = mod >> 1;
for (T knot : knots)
{
const int64_t k = math_of(knot);
if constexpr (std::is_signed_v<T>)
{
if (fits_in_domain<T>(half - k))
etas.push_back(static_cast<T>(half - k));
if (fits_in_domain<T>(-half - k))
etas.push_back(static_cast<T>(-half - k));
}
else if (k != 0 && fits_in_domain<T>(mod - k))
{
etas.push_back(static_cast<T>(mod - k));
}
}
}
std::sort(etas.begin(), etas.end());
etas.erase(std::unique(etas.begin(), etas.end()), etas.end());
std::sort(points.begin(), points.end());
points.erase(std::unique(points.begin(), points.end()), points.end());
int wraps = 0;
for (T center : points)
{
auto mat = grotto::make_offset_horner_keys<T, D>(center);
for (T eta : etas)
{
if (addition_leaves_domain(center, eta))
++wraps;
expect_wrapped<D>(mat, knots, coeff, center, eta, "big-dealer");
expect_geneval<D>(center, eta, knots, coeff, "big-geneval");
if (::testing::Test::HasFailure())
{
if constexpr (std::is_signed_v<T>)
ADD_FAILURE() << "signed " << bits << " center=" << static_cast<long long>(center)
<< " eta=" << static_cast<long long>(eta);
else
ADD_FAILURE() << "unsigned " << bits << " center=" << static_cast<unsigned long long>(center)
<< " eta=" << static_cast<unsigned long long>(eta);
return;
}
}
}
if (bits <= 62)
EXPECT_GT(wraps, 0) << (std::is_signed_v<T> ? "signed " : "unsigned ") << bits;
const std::vector<std::array<uint64_t, 1>> constants(knots.size(), {uint64_t{42}});
const T const_center = points.back();
const T const_eta = etas.front();
auto const_keys = grotto::make_offset_horner_keys<T, 0>(const_center);
EXPECT_EQ(open_eval<0>(const_keys, knots, constants, const_eta),
gold<0>(const_center, const_eta, knots, constants));
for (int n = 0; n < 8; ++n)
{
const T x = static_cast<T>(dist(rng));
const T r = static_cast<T>(dist(rng));
const T x0 = static_cast<T>(dist(rng));
const T r0 = static_cast<T>(dist(rng));
const T x1 = offset_horner_group_sub(x, x0);
const T r1 = offset_horner_group_sub(r, r0);
const auto got = grotto::geneval_offset_horner<D>(x0, x1, r0, r1, knots, coeff);
const T sum = offset_horner_group_add(x, r);
EXPECT_EQ(got.center, offset_horner_group_add(r, r));
EXPECT_EQ(got.eta, offset_horner_group_sub(x, r));
EXPECT_EQ(offset_horner_group_add(got.center, got.eta), sum);
EXPECT_EQ(got.value0 + got.value1, gold<D>(got.center, got.eta, knots, coeff));
if (::testing::Test::HasFailure())
return;
}
}
TEST(OffsetHorner, BiggerDomainsExerciseCarrySplitAndGeneval)
{
exercise_big_domain<uint16_t>();
if (HasFailure())
return;
exercise_big_domain<int16_t>();
if (HasFailure())
return;
exercise_big_domain<uint32_t>();
if (HasFailure())
return;
exercise_big_domain<int32_t>();
if (HasFailure())
return;
exercise_big_domain<uint64_t>();
if (HasFailure())
return;
exercise_big_domain<int64_t>();
}