libdpf/test/tests/offset_horner_test.cpp

1430 lines
50 KiB
C++
Raw Normal View History

#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));
}
TEST(OffsetHorner, OpenedSharesAreNotHornerInputs)
{
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 auto c = open_coeffs<D>(mat, knots, coeff, eta);
uint64_t sum = 0;
for (uint64_t ck : c)
sum += ck;
const uint64_t value = gold<D>(center, eta, knots, coeff);
EXPECT_EQ(sum, value);
EXPECT_EQ(value, 5180u);
uint64_t horner = c[D];
const uint64_t limb = lift(center);
for (std::size_t k = D; k-- > 0; )
horner = horner * limb + c[k];
EXPECT_NE(horner, value);
}
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>();
}