libdpf/doc/pages/jet_and_ring.md
Ryan Henry 0d22946a0e Checkpoint the party/runtime stack before share-program and malicious-mode work.
Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 05:59:19 -06:00

26 KiB
Raw Blame History

Jet and exact ring switch

One opened offset eta = x - r drives two cheap corrections. The binomial jet returns shares of \f$\binom{x}{0},\ldots,\binom{x}{d}\f$ after a public Chu–Vandermonde shift. The ring switch returns shares of x in any residue group whose comparison payload is the destination modulus.

The same offset also drives [offset Horner](@ref offset_horner), [offset polynomials](@ref offset_poly), and [carry](@ref carry). [Prefix parity](@ref prefix_parity) reads a key's path. [Cleartext maps](@ref grotto_luts) evaluate fixed-point functions with no tree.

Binomial jet

\htmlonly

ELI5. The dealer keys the binomial coefficients of (center + eta) up to a chosen degree. After eta is public, a dot with those shares is the monomial or the polynomial, with no further tree walk.
\endhtmlonly

make_offset_jet_keys(center, degree) keys one incremental gt whose payload is the vector of \f$\binom{\mathrm{center}}{k}\f$ in \f$\mathbb{Z}/2^{64}\f$. After eta opens, the same knot shift and carry cut as offset poly refine the pieces. On the piece with carry kappa,

\f[ \binom{c+\kappa}{k} =\sum_j\binom{c}{j}\binom{\kappa}{k-j}. \f]

make_offset_jet_keys writes one incremental comparison for degree d (d ≤ 16). The seed spine is Θ(n λ) bits, with n the center's bit length and λ the seed width. Value words grow with the d+1 binomial lanes. After η is public, offset_jet_shares evaluates that one key on the K knots, the same order as one eval_sequence on the knots. The Chu–Vandermonde update after those walks is Θ(P · d²) arithmetic, where P is the number of refined pieces (the knots, plus the domain minimum, plus the carry cut when the input width is at most 62). offset_jet_dot is Θ(d). No further round when the coefficients are public.

offset_jet_shares returns that shifted jet. Public dots are free:

  • value of \f$\sum a_k\binom{x}{k}\f$ via offset_jet_dot;
  • forward difference via offset_jet_difference_coeff (Pascal);
  • hockey-stick prefix via offset_jet_prefix_coeff.

The prefix needs \f$\binom{x}{k+1}\f$, so the key degree must be one larger than the polynomial degree. Degree 16 therefore prefix-sums polynomials through degree 15.

Binomials modulo \f$2^{64}\f$ use a falling factorial modulo \f$2^{64+v_2(k!)}\f$ (\f$v_2(16!)=15\f$), then multiply by the inverse of the odd part of \f$k!\f$. Dividing by \f$k!\f$ inside \f$\mathbb{Z}/2^{64}\f$ alone is not exact.

A Padé pair or one Newton correction is two dots against the same jet and one reciprocal after the shares are opened. Those are not separate APIs.

Code samples\n

  • jet_and_ring.cpp \include{cpp} grotto/jet_and_ring.cpp

Exact ring switch

\htmlonly

ELI5. An n-bit limb is rewritten into another modulus, a field, or a P-256 scalar by an exact map on the opened residue. The value does not go through floating point, and the map does not expand another key.
\endhtmlonly

For an unsigned \f$n\f$-bit limb (\f$n\le 64\f$) with representatives in \f$[0,2^n)\f$,

\f[ \eta + r = x + w\cdot 2^n,\qquad w=\mathbf{1}[r+\eta\ge 2^n]. \f]

In any modulus \f$M\f$,

\f[ x \equiv \eta + (r\bmod M) - w\cdot(2^n\bmod M)\pmod M. \f]

A uint64 comparison share is not a share mod \f$M\f$. The payload of the wrap comparison is the destination element \f$2^n\bmod M\f$. The dealer keys lt(2^n \bmod M) at the secret r and stores an additive split of r in the residue group. After eta opens, each party evaluates at the public query \f$2^n-1-\eta\f$. That indicator is hot exactly on wrap, including the eta = 0 case. Party 0 adds public eta.

Destination groups:

  • grotto::zn64<Mod> and grotto::zn128<Lo,Hi> ([residue.hpp](@ref grotto/residue.hpp));
  • dpf::field128;
  • dpf::p256_scalar (NIST P-256 order, not the point group).

ring_switch_factor<Factor> reduces a share when Factor divides the modulus. One switch into an lcm yields every factor by local reduction.

The dealer material is one lt key on that limb, Θ(n λ) bits for limb width n ≤ 64 and seed width λ, plus two residue shares of r. After η is public, each party does one point evaluation (Θ(n) expands) and a constant amount of arithmetic in the destination group. ring_switch_factor is local.

This is the exact neighbour of truncated Barrett nmod. grotto::nmod(x_raw, x_bits, recip_raw, recip_bits, residue_bits) splits x / M when recip_raw / 2^recip_bits is a positive approximation of 1/M. The result is an nmod_result: quotient is floor(x/M), and residue is the fractional part truncated onto residue_bits. nmod_pow2(x_raw, x_bits, exp, residue_bits) is the same split when the modulus is a power of two. Both are one product by a reciprocal of at most 128 bits, so time and extra memory are constant in the word size.

\code{cpp} auto split = grotto::nmod_pow2(raw, 16, 0, 16); \endcode

Defined in\n @ref grotto/nmod.hpp

See also [representation shift and twisted jets](@ref repr_and_twist).

Offset Horner

\htmlonly

ELI5. Powers of a public center are already shared. Shifting them by the opened eta, the binomial way, evaluates the polynomial at the secret. The degree here is fixed in the template.
\endhtmlonly

make_offset_horner_keys<Input, Degree>(center) keys one gt whose payload is center^m for m = 0 .. Degree. Degree is at most 3 (offset_horner_max_degree). Pass dpf::verifiable{} for proof tokens. After eta opens, offset_horner_eval<Party, Degree> returns that party's share of the cubic at the wrapped point. Coefficients are one std::array<uint64_t, Degree + 1> per knot, low degree first.

\code{cpp} const std::uint8_t center = 12; auto mat = grotto::make_offset_horner_keys<std::uint8_t, 2>(center); std::vectorstd::uint8_t knots{0}; std::vector<std::array<std::uint64_t, 3>> coeff{{4, 2, 1}}; auto s0 = grotto::offset_horner_eval<0, 2>(mat, knots, coeff, eta); \endcode

geneval_offset_horner runs the same cubic from Jack Doerner and abhi shelat shares of x and of the center, on a dpf::ds_randomness tape.

Degree is at most 3. The seed spine is one comparison, Θ(n λ) bits, and the value words hold the four powers. Evaluation after η opens is one sequence-shaped walk on the knots plus O(1) arithmetic. The geneval form generates that same comparison once, opens one correction word per level, as in [geneval](@ref tour_ds), and does not store a reusable key.

Defined in\n @ref grotto/offset_horner.hpp

Offset polynomial

\htmlonly

ELI5. Same shift as offset Horner, but the degree is an argument, so the number of powered shares is chosen when the keys are built.
\endhtmlonly

make_offset_poly_keys(center, degree) is offset Horner at a runtime degree, at most 16 (offset_poly_max_degree). One incremental gt whose payload is the vector of powers. offset_poly_eval<Party> dots the shifted powers. offset_poly_clear is the same polynomial in the clear. offset_poly_kappas is the public carry of each piece. Shared coefficients use offset_poly_shift_share (the binomial map is linear) and offset_poly_beaver_share for the dot.

Degree d is at most 16: one key, Θ(n λ) bits of seed spine plus value words that grow with d. The clear and public-coefficient evals are one sequence-shaped walk on the K knots, then O(d^2) arithmetic. A shared-coefficient dot is one Beaver inner product: one opening round of the masked vectors, communication linear in the flattened length (pieces times d+1 coefficients), and one product share per coefficient in preprocessing. The shift of each party's coefficient share is local.

\code{cpp} auto mat = grotto::make_offset_poly_keys(std::uint8_t{12}, 4); std::vectorstd::uint8_t knots{0}; std::vector<std::vectorstd::uint64_t> coeff{{4, 2, 1, 0, 0}}; auto s0 = grotto::offset_poly_eval<0>(mat, knots, coeff, eta); auto opened = s0 + grotto::offset_poly_eval<1>(mat, knots, coeff, eta); \endcode

Defined in\n @ref grotto/offset_poly.hpp

Carry

\htmlonly

ELI5. A carry across a shift is a short list of comparisons and bit corrections, not a generic circuit. The request names the source width, the shift, and the width of what comes out. Truncate, arithmetic shift, and sign-extend are the same plan with different output widths.
\endhtmlonly

A carry_request names the source width n, the shift s, the output width out_n, a carry_mode (truncate_reduce, same_ring, extend, window), and a sign_knowledge (unknown, nonnegative, negative). plan_carry returns a carry_recipe whose flags are the steps that are still live. plan_carry_in(n, s), plan_carry_out(n, s, sign), and plan_carry_fused(n, s, out_n, sign) fill the common requests.

make_carry_keys(recipe) (and make_carry_in_keys, make_carry_out_keys, make_carry_fused_keys) builds the dealer keys. finalize_carry_in_blinds adjusts a truncate-reduce split. Online, eval_carry_in(keys, party, opened) returns a carry_eval_share whose value is that party's share. opened is (x0 + x1 + rin) mod 2^n. The other online entry points are eval_carry_out_known, eval_carry_out_unknown, eval_carry_extend, eval_carry_window, and eval_carry_fused.

Cleartext twins, for tests and for a public limb, are eval_carry_clear, carry_in_clear, carry_out_clear, carry_asr, and carry_mask. plan_carry is a constant-time inspection of the request. Each live comparison flag becomes one DPF key whose domain is the limb width w of that comparison, Θ(w λ) bits, and the online step is one point walk of that key. A share-MSB AND adds one Beaver bit triple in preprocessing and one opening round of a bit.

\code{cpp} auto keys = grotto::make_carry_in_keys(32, 8); auto share = grotto::eval_carry_in(keys, /party/ 0, opened); auto clear = grotto::eval_carry_clear(keys.recipe, x0, x1); \endcode

Defined in\n @ref grotto/carry_plan.hpp, @ref grotto/carry.hpp

Prefix parity

\htmlonly

ELI5. One walk of an existing key stops at the public endpoints and folds XOR or addition along that prefix. The fold is O(number of endpoints), not a new key per prefix.
\endhtmlonly

prefix_parities(key, endpoints) walks a key to the sorted endpoints and returns XOR shares of the prefix parities, plus the index of the first endpoint on the wrap. segment_parities turns those into one share per segment. all_segment_parities_from_prefix_parities is the same conversion when you already hold the prefix array.

signed_prefix_parities(key, endpoints) needs a comparison channel (assigned, if the payload was a wildcard). It returns one additive uint64_t share per endpoint: for dpf::gt(1) that share opens to 1 when the secret point is below the endpoint. signed_prefix_parities_into writes a runtime-length buffer.

The prefix walk follows Storrier, Vadapalli, Lyons, and Henry, ePrint 2023/108: one key's prefix parity in place of a comparison per piece. On m endpoints the walk resumes one path memoizer (Θ(n) nodes, n the key depth). Expands are the nodes on those paths, O(m n) in the worst case, and less when endpoints share a prefix or the zero-suffix stop hits. signed_prefix_parities adds an O(n) sum of value-correction words on each endpoint. Both calls are local. segment_parities is the prefix walk plus an O(m) XOR of those bits.

\code{cpp} std::array<std::uint8_t, 2> ends{10, 40}; auto [bits, first] = grotto::prefix_parities(k0, ends); auto segs = grotto::segment_parities(k0, ends); auto signs = grotto::signed_prefix_parities(cmp0, ends); \endcode

Defined in\n @ref grotto/prefix_parity.hpp

Several LUTs, one comparison

\htmlonly

ELI5. Stack the breakpoints of every table into one sorted list. One comparison and one prefix walk label the pieces of that list. Each table then sums the labels that fall inside its own intervals.
\endhtmlonly

make_lut_union_plan(luts, eta) shifts every piecewise LUT by the public eta, inserts the same domain-minimum and carry cuts as [offset polynomial](@ref offset_poly), and sorts the union. A piece of one LUT is a span of those union knots: [begin, end), or [begin, end-of-union) ∪ [0, end) when the piece wraps. The span stores that piece's binomial shift by its public kappa. Spans of one LUT partition the union.

The interactive plan is one comparison, whatever the number of LUTs and whatever the number of union knots:

  • plan.comparisons and plan.prefix_walks are 1.
  • plan.depth and plan.geneval_rounds() are the bitlength of the input.
  • plan.degree is the widest polynomial. The payload is 1, center, …, center^degree.
  • schedule_lut_union records one fss_cmp of that depth. The slot is lut_union_slot_bytes: one AES block, or lanes * 8 when the power vector is wider. Prefix parity of the union is local after that comparison.

lut_union_eval<Party> reads one make_offset_poly_keys key of degree at least plan.degree and returns a share per LUT. geneval_lut_union opens that comparison from XOR shares of the center, as in geneval_offset_horner. Pass dpf::arith_input when the shares add to the center in the input group.

piecewise_from_easy and piecewise_from_constant adapt the cleartext tables. An easy_lut denominator other than 1 is a rounding division, so piecewise_from_easy rejects it. Powers that are zero on every piece are dropped, and the shared payload stays only as wide as the widest remaining degree.

\code{cpp} grotto::piecewise_lutstd::uint8_t low{{0, 10}, {{1, 0}, {0, 2}}}; grotto::piecewise_lutstd::uint8_t high{{0, 4, 12}, {{3, 0}, {1, 1}, {9, 4}}}; const std::uint8_t center = 12; const std::uint8_t eta = 3; auto plan = grotto::make_lut_union_plan({low, high}, eta); auto mat = grotto::make_offset_poly_keysstd::uint8_t(center, plan.degree); auto s0 = grotto::lut_union_eval<0>(mat, plan); auto s1 = grotto::lut_union_eval<1>(mat, plan); dpf::protocol::composer composer(0); grotto::schedule_lut_union(composer, plan); // rounds == plan.depth \endcode

Code samples\n

  • lut_union.cpp \include{cpp} grotto/lut_union.cpp

Defined in\n @ref grotto/lut_union.hpp

Cleartext maps

These functions take a raw fixed-point word (n << fractional_bits) and return a raw word. They do not build a DPF. The type grotto::fixedpoint itself is a domain and an output; see [Input types](@ref input_types) and [Output types](@ref output_types).

Fixed-point product

\htmlonly

ELI5. The product lives in a ring wide enough for both fixed-point operands. One Beaver triple in that ring is the product; the binary point is placed by a public shift afterward.
\endhtmlonly

fixed_mul<IntegerBits, FractionalBits>(lhs, rhs) multiplies two fixedpoint values and keeps that many integer bits (including the sign) and fraction bits. Bits below the fraction are floored. The product type is the result_type of fixed_mul_plan. The plan uses at most 8 limbs and refuses a wider window, so the product is a constant amount of 64-bit arithmetic and O(1) extra memory.

\code{cpp} using q16 = grotto::fixedpoint<16, std::int32_t>; auto prod = grotto::fixed_mul<16, 16>(q16{1.5}, q16{2.0}); \endcode

Defined in\n @ref grotto/fixedpoint_mul.hpp

Lookup tables

\htmlonly

ELI5. A cleartext approximation is replaced by a table addressed with the secret. Constant, easy, range, window, and principal tables differ in how many bits of the input they consume and how the correction is added.
\endhtmlonly

Constant, easy, principal, range, and window tables are included from grotto.hpp. The dyadic table comes in through exact_steps.hpp, which grotto.hpp also includes.

  • Constant. make_exact_constant_lut<Raw>(exact_constant::signum, fractional_bits) and the other exact_constant names (positive, negative, nonneg, nonpos, zero, nonzero, ilogb, ceil_ilogb, ilog10, clz, clrsb). make_threshold_lut, make_interval_lut, and make_clipped_quotient_lut build a constant_lut<Raw> you call as table(raw).
  • Easy. Few-piece polynomials with integer knots: make_abs_lut, make_relu_lut, make_clip_lut, make_hardsigmoid_lut, make_hardswish_lut, make_leaky_relu_hundredth_lut, and the other make_*_lut factories in [easy_lut.hpp](@ref grotto/easy_lut.hpp). The result is an easy_lut<Raw>. make_leaky_relu_lut(shift) is the dyadic slope 1/2^shift. The Appendix D leaky ReLU is slope 1/100.
  • Dyadic. Exact steps on powers of two: make_signum_lut, make_msb_lut(index), make_ilogb_lut, make_ilog10_lut, make_clz_lut, make_clrsb_lut, and the sign predicates make_positive_lut through make_nonzero_lut. ilog_of_zero is the sentinel for a zero argument. msb_bit_limit is 8.
  • Range. eval_reduced(reduced::ln, fractional_bits, raw) and the other reduced names (lg, log10, exp, exp2, exp10, sin, cos, tan, cot, sec, csc, the hyperbolics, sqrt, inv, rsqrt, invsq, expm1, log1p). split_positive is the dyadic mantissa split those reductions use.
  • Window. eval_window(window::gelu, fractional_bits, raw). The window names cover smoothstep, sigmoid, tanh, erf, erfc, softplus, gelu, silu, asin, acos, probit, hardelish, lecun_tanh, one_minus_sigmoid, and the rest of the enum in [window_lut.hpp](@ref grotto/window_lut.hpp).
  • Principal. eval_principal(principal::sin, fractional_bits, raw) on the closed principal interval. Precisions are 8, 12, …, 32 (principal_precision). Names: ln, exp, sin, tanf, tang, sinh, cosh, sqrt, coth, sec, gsec, csch, inv, rsqrt, invsq.
  • Wavelet. Haar and bior(5,3) compressed tables: [Wavelet lookup tables](@ref dwt_luts).

\code{cpp} auto sign = grotto::make_exact_constant_lutstd::int32_t( grotto::exact_constant::signum, 0); auto s = sign(std::int32_t{-3}); auto relu = grotto::make_relu_lutstd::int32_t(8); auto ln = grotto::eval_reduced(grotto::reduced::ln, 16, raw); auto gelu = grotto::eval_window(grotto:🪟:gelu, 16, raw); auto sine = grotto::eval_principal(grotto::principal::sin, 16, raw); \endcode

The degree-0 exact tables follow Storrier, Vadapalli, Lyons, and Henry, [ePrint 2023/108](@ref bib_grotto), Appendix D. Sign predicates are a constant number of cuts, Θ(1). clz and ilogb cut once per bit of the raw width, Θ(w). ilog10 binary-searches the raw domain once per decimal exponent, Θ(w²) probes. make_msb_lut emits Θ(2^index) cuts and rejects index at or above 8. make_clipped_quotient_lut is linear in (high-low)/modulus, capped at 2^16 pieces. Calling a constant or easy table binary-searches its P pieces, O(log P). eval_principal and eval_window binary-search the static knots and then run one cubic. eval_reduced adds a short series on the small interval (expm1 24 terms, log1p 80). No DPF and no communication.

Appendix D maps that were still cleartext

Appendix D of [ePrint 2023/108](@ref bib_grotto) lists HardELiSH, LeCun tanh, and leaky ReLU with slope 1/100. Those three now have fixed-point evaluators. one_minus_sigmoid is the sigmoid table complemented, which rounds out the logistic pair.

The cubics were built on mocha2. Sollya chose the longest pieces whose absolute error stays within half an ulp. Mathematica (Remez), Maple (numapprox[minimax]), and MATLAB/Chebfun (minimax) fitted each piece, and the shipped polynomial is the one with the lowest error after the coefficients are rounded to k+16 fraction bits. Of the 443 cubics, Sollya won 229, Maple 83, MATLAB 74, and Mathematica 57.

Half an ulp at 16 fraction bits is 2^{-17} ≈ 7.63e-6. Appendix D's own columns are tighter (4.2e-8) and therefore use more pieces (HardELiSH 38, LeCun tanh 89). The counts below are this library's half-ulp partitions.

map degree pieces at k = 8, 12, 16, 20, 24, 28, 32 evaluation
window::hardelish 3 on (-1, 0); exact quadratic on [0, 1] 1, 2, 3, 6, 12, 23, 45 Θ(log P) knot search and one cubic on (-1, 0). Elsewhere Θ(1): 0, round(x(x+1)/2), or x
window::lecun_tanh 3 3, 6, 11, 22, 44, 88, 177 Θ(log P) on the positive knots of the absolute value, then a sign. Past the last knot the value is the constant ±round(1.7159 · 2^k)
window::one_minus_sigmoid 3 same as sigmoid: 8, 16, 64, 128, 256, 1024, 2048 one sigmoid evaluation and one subtraction. Θ(log P)
make_leaky_relu_hundredth_lut 1 2 Θ(1). Identity on the right, round(x/100) on the left. Error at most half a unit in the last place

make_leaky_relu_lut(shift) is still the dyadic slope 1/2^shift. The hundredth factory is the Appendix D slope and does not depend on the fractional width.

grotto::polynomials::eval_horner evaluates a poly_constant, poly_linear, poly_quadratic, or poly_cubic (a std::array of coefficients, constant term first). piecewise_eval(polys, bounds, x) picks the piece and calls that Horner step.

Defined in\n @ref grotto/constant_lut.hpp, @ref grotto/easy_lut.hpp, @ref grotto/dyadic_lut.hpp, @ref grotto/range_lut.hpp, @ref grotto/window_lut.hpp, @ref grotto/principal_lut.hpp, @ref grotto/piecewise.hpp

Wavelet lookup tables

\htmlonly

ELI5. A wavelet step is a fixed linear combination. The LUT stores that combination so the signal stays in shares and never enters a floating-point routine. Haar and biorthogonal 5/3 are the two filters.
\endhtmlonly

make_haar_dwt_lut and make_bior53_dwt_lut compress a real signal of length \f$2^n\f$ and evaluate it as a fixed-point word. The construction is the cleartext Haar and bior(5,3) lookup of Reis, Ugurbil, Wagh, Henry, and de Vega, [ePrint 2025/013](@ref bib_wave), Equations (7) and (8). sample_dwt_signal(domain_bits, fractional_bits, f) writes the grid \f$i \cdot 2^{-f}\f$ for \f$i \in [0, 2^n)\f$.

Both builders run the depth-\f$j\f$ low-pass with the smooth edge extension used for that paper's accuracy tables. PyWavelets calls these filters haar and bior2.2; bior(5,3) is the same pair, named there by vanishing moments. Building either table is \f$\Theta(N)\f$ arithmetic and extra memory, \f$N = 2^n\f$.

Haar then multiplies the approximation coefficients by \f$2^{-j/2}\f$ and rounds down to \f$f\f$ fraction bits. On this grid that coefficient is the mean of each block of \f$2^j\f$ samples. Evaluation reads coeff[raw >> j], one indexing step, \f$\Theta(1)\f$.

bior(5,3) multiplies by \f$2^{j/2}\f$ and rounds down the same way. Smooth extension prepends two coefficients, so the bin msb = raw >> j lives at index msb + 2, and the next tap at msb + 3, wrapping in the stored vector. With lsb = raw mod 2^j,

\f[ y = \bigl\lfloor\bigl(c_{\mathrm{msb}+2},(2^j - \mathrm{lsb})

  • c_{\mathrm{msb}+3},\mathrm{lsb}\bigr) / 2^{2j}\bigr\rfloor. \f]

That is Equation (8): the Lemma 6 weights \f$(2^j - \mathrm{lsb}_j)\f$ and \f$\mathrm{lsb}_j\f$, in integer arithmetic. Two multiplications and a shift, \f$\Theta(1)\f$.

The paper's online protocols look these tables up under a DPF. Haar is paired there with a deterministic Pika truncation; bior(5,3) is paired with segment parity. Those protocols are not a key type here. The value they open is table(raw).

\code{cpp} auto samples = grotto::sample_dwt_signal(6, 4, [](double x) { return 1.0 / (1.0 + std::exp(-(x - 2.0))); }); auto haar = grotto::make_haar_dwt_lut(samples, 4, 2); auto bior = grotto::make_bior53_dwt_lut(samples, 4, 2); auto h = haar(std::uint64_t{32}); auto b = bior(std::uint64_t{33}); \endcode

Code samples\n

  • dwt_lut.cpp \include{cpp} grotto/dwt_lut.cpp

Defined in\n @ref grotto/dwt_lut.hpp

Closed form

eval_closed(closed::atanh, fractional_bits, raw) composes eval_reduced and eval_window. The closed names are the inverse hyperbolics and inverse trig functions, selu, elu, celu, softsign, tanhshrink, the logistic / exponential / laplace / cauchy quantiles, sinc, and the extra powers cbrt, qtrt, icbrt, iqtrt, pow_m01, pow_p15, pow_m3. Precision is one of 8, 12, …, 32. Each call runs a constant number of eval_reduced or eval_window evaluations.

\code{cpp} auto y = grotto::eval_closed(grotto::closed::atan, 16, raw); \endcode

Defined in\n @ref grotto/closed_form.hpp

Exact steps

Counts and booleans come back as fixed-point integers, n << fractional_bits.

\code{cpp} auto floor_x = grotto::eval_dec_floor(raw, 16); auto digits = grotto::eval_dec_width(raw, 16); auto bits = grotto::eval_bit_widthstd::int32_t(raw, 16); \endcode

Decimal digit counts divide in a loop, so the time follows the number of digits. eval_bit_width, eval_bit_floor, eval_bit_ceil, eval_countl_one, and eval_has_single_bit scan the raw width, Θ(width) bit operations and at most 64 shifts. eval_logstar is five magnitude comparisons, Θ(1). eval_deg2rad and eval_rad2deg are one scale each.

Also eval_dec_ceil, eval_oct_width, eval_b64_width, eval_value_length (base 8, 10, or 64), eval_has_single_digit, eval_bit_floor, eval_bit_ceil, eval_countl_one, eval_has_single_bit, eval_deg2rad, and eval_rad2deg. make_exact_step_lut builds the matching easy_lut.

Defined in\n @ref grotto/exact_steps.hpp

Gadget functors

grotto/gadgets.hpp still includes the decimal and exponential reference headers. The functors that those headers used to provide are deprecated: call eval_reduced, eval_window, make_*_lut, or exact_constant instead. gadget_hints<T> holds the old domain, degree, and pole notes for a functor type.

Defined in\n @ref grotto/gadgets.hpp, @ref grotto/gadget_hints.hpp

\htmlonly

TL;DR. Open eta = x − r once. Jets and Horner turn that public distance into polynomial powers. Ring switch and carry move the integer. Prefix parity folds a key you already hold. Lookup tables, including the wavelet tables, replace cleartext math. A fixed-point product is one Beaver triple.
\endhtmlonly