binius_ntt/twiddle.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
// Copyright 2024 Irreducible Inc.
use crate::Error;
use binius_field::{BinaryField, Field};
use std::{marker::PhantomData, ops::Deref};
/// A trait for accessing twiddle factors in a single NTT round.
///
/// Twiddle factors in the additive NTT are subspace polynomial evaluations over linear subspaces,
/// with an implicit NTT round $i$.
/// Setup: let $K \mathbin{/} \mathbb{F}\_2$ be a finite extension of degree $d$, and let $\beta_0,\ldots ,\beta_{d-1}$ be an $\mathbb{F}\_2$-basis.
/// Let $U_i$ be the $\mathbb{F}\_2$-linear span of $\beta_0,\ldots ,\beta_{i-1}$. Let $\hat{W}_i(X)$
/// be the normalized subspace polynomial of degree $2^i$ that vanishes on $U_i$ and is $1$ on $\beta_i$.
/// Evaluating $\hat{W}_i(X)$ turns out to yield an $\mathbb{F}\_2$-linear function $K \rightarrow K$.
///
/// This trait accesses the subspace polynomial evaluations for $\hat{W}\_i(X)$.
/// The evaluations of the vanishing polynomial over all elements in any coset of the subspace
/// are equal. Equivalently, the evaluations of $\hat{W}\_i(X)$ are well-defined on
/// the $d-i$-dimensional vector space $K \mathbin{/} U_i$. Note that $K \mathbin{/} U_i$ has a natural induced basis.
/// Write $\{j\}$ for the $j$th coset of the subspace, where $j$ is in $[0,2^{d-i})$, with respect
/// to this natural basis. This means: write $j$ in binary: $j = j_0 + \cdots + j_{d-i-1} \cdot 2^{d-i-1}$
/// and consider the following element of $K$: $j_0 \cdot \beta_i + \cdots + j_{d-i-1} \cdot \beta_{d-1}$.
/// This element determines an element of $K \mathbin{/} U_i$.
/// The twiddle factor $t_{i,j}$ is then $\hat{W}\_i(\{j\})$, i.e., $\hat{W}\_j$ evaluated at the aforementioned element of
/// the quotient $K \mathbin{/} U_i$.
///
/// As explained, the evaluations of these polynomial yield linear functions, which allows for flexibility in how they are computed.
/// Namely, for an evaluation domain of size $2^{i}$, there is a strategy for computing polynomial
/// evaluations "on-the-fly" with $O(\ell)$ field additions using $O(\ell)$ stored elements or precomputing the $2^\ell$ evaluations and
/// looking them up in constant time (see the [`OnTheFlyTwiddleAccess`] and
/// [`PrecomputedTwiddleAccess`] implementations, respectively).
///
/// See [LCH14] and [DP24] Section 2.3 for more details.
///
/// [LCH14]: <https://arxiv.org/abs/1404.3458>
/// [DP24]: <https://eprint.iacr.org/2024/504>
pub trait TwiddleAccess<F: BinaryField> {
/// Base-2 logarithm of the number of twiddle factors in this round.
fn log_n(&self) -> usize;
/// Get the twiddle factor at the given index.
///
/// Evaluate $\hat{W}\_i(X)$ at the element `index`: write `index` in binary
/// and evaluate at the element $index_0\beta_{i+1} \ldots + index_{d-i-2}\beta_{d-1}$.
///
/// Panics if `index` is not in the range 0 to `1 << self.log_n()`.
fn get(&self, index: usize) -> F;
/// Get the pair of twiddle factors at the indices `index` and `(1 << index_bits) | index`.
///
/// Panics if `index_bits` is not in the range 0 to `self.log_n()` or `index` is not in the
/// range 0 to `1 << index_bits`.
fn get_pair(&self, index_bits: usize, index: usize) -> (F, F);
/// Returns a scoped twiddle access for the coset that fixes the upper `coset_bits` of the
/// index to `coset`.
///
/// Recall that a `TwiddleAccess` has an implicit NTT round $i$. Let $j=d-coset_{bits}$.
/// Then`coset` returns a `TwiddleAccess` object (of NTT round i) for the following affine
/// subspace of $K/U_{i-1}$: the set of all elements of $K/U_{i-1}$
/// whose coordinates in the basis $\beta_i,\ldots ,\beta_{d-1}$ is:
/// $(*, \cdots, *, coset_{0}, \ldots , coset_{bits-1})$, where the first $j$ coordinates are arbitrary.
/// Here $coset = coset_0 + \ldots + coset_{bits-1}2^{bits-1}$. In sum, this amounts to *evaluations* of $\hat{W}\_i$
/// at all such elements.
///
/// Therefore, the `self.log_n` of the new `TwiddleAccess` object is computed as `self.log_n() - coset_bits`.
///
/// Panics if `coset_bits` is not in the range 0 to `self.log_n()` or `coset` is not in the
/// range 0 to `1 << coset_bits`.
fn coset(&self, coset_bits: usize, coset: usize) -> impl TwiddleAccess<F>;
}
/// Twiddle access method that does on-the-fly computation to reduce its memory footprint.
///
/// This implementation uses a small amount of precomputed constants from which the twiddle factors
/// are derived on the fly (OTF). The number of constants is ~$1/2 d^2$ field elements for a domain
/// of size $2^d$.
#[derive(Debug)]
pub struct OnTheFlyTwiddleAccess<F, SEvals = Vec<F>> {
log_n: usize,
/// `offset` is a constant that is added to all twiddle factors.
offset: F,
/// `s_evals` is $<\hat{W}\_i(\beta_{i+1}),\ldots ,\hat{W}\_i(\beta_{d-1})>$ for the implicit round $i$.
s_evals: SEvals,
}
impl<F: BinaryField> OnTheFlyTwiddleAccess<F> {
/// Generate a vector of OnTheFlyTwiddleAccess objects, one for each NTT round.
pub fn generate<DomainField: BinaryField + Into<F>>(
log_domain_size: usize,
) -> Result<Vec<Self>, Error> {
let s_evals = precompute_subspace_evals::<F, DomainField>(log_domain_size)?
.into_iter()
.enumerate()
.map(|(i, s_evals_i)| OnTheFlyTwiddleAccess {
log_n: log_domain_size - 1 - i,
offset: F::ZERO,
s_evals: s_evals_i,
})
.collect();
// The `s_evals` for the $i$th round contains the evaluations of $\hat{W}\_i$ on
// $\beta_{i+1},\ldots ,\beta_{d-1}$.
Ok(s_evals)
}
}
impl<F, SEvals> TwiddleAccess<F> for OnTheFlyTwiddleAccess<F, SEvals>
where
F: BinaryField,
SEvals: Deref<Target = [F]>,
{
#[inline]
fn log_n(&self) -> usize {
self.log_n
}
#[inline]
fn get(&self, i: usize) -> F {
self.offset + subset_sum(&self.s_evals, self.log_n, i)
}
#[inline]
fn get_pair(&self, index_bits: usize, i: usize) -> (F, F) {
let t0 = self.offset + subset_sum(&self.s_evals, index_bits, i);
(t0, t0 + self.s_evals[index_bits])
}
#[inline]
fn coset(&self, coset_bits: usize, coset: usize) -> impl TwiddleAccess<F> {
let log_n = self.log_n - coset_bits;
let offset = subset_sum(&self.s_evals[log_n..], coset_bits, coset);
OnTheFlyTwiddleAccess {
log_n,
offset: self.offset + offset,
s_evals: &self.s_evals[..log_n],
}
}
}
fn subset_sum<F: Field>(values: &[F], n_bits: usize, index: usize) -> F {
(0..n_bits)
.filter(|b| (index >> b) & 1 != 0)
.map(|b| values[b])
.sum()
}
/// Twiddle access method using a larger table of precomputed constants.
///
/// This implementation precomputes all $2^k$ twiddle factors for a domain of size $2^k$.
#[derive(Debug)]
pub struct PrecomputedTwiddleAccess<F, SEvals = Vec<F>> {
log_n: usize,
/// If we are implicitly in NTT round i, then `s_evals` contains the evaluations of $\hat{W}\_i$
/// on the entire space $K/U_{i+1}$, where the order is the usual "binary counting order"
/// in the basis vectors $\beta_{i+1},\ldots ,\beta_{d-1}$.
///
/// While $\hat{W}\_i$ is indeed well-defined on $K/U_i$, we have the
/// normalization $\hat{W}\_{i}(\beta_i)=1$, hence to specify the function we need
/// only specify it on $K/U_{i+1}$, equivalently, the $\mathbb{F}_2$-span of $\beta_{i+1},\ldots ,\beta_{d-1}$.
s_evals: SEvals,
_marker: PhantomData<F>,
}
impl<F: BinaryField> PrecomputedTwiddleAccess<F> {
pub fn generate<DomainField: BinaryField + Into<F>>(
log_domain_size: usize,
) -> Result<Vec<Self>, Error> {
let on_the_fly = OnTheFlyTwiddleAccess::<F, _>::generate::<DomainField>(log_domain_size)?;
Ok(expand_subspace_evals(&on_the_fly))
}
}
impl<F, SEvals> TwiddleAccess<F> for PrecomputedTwiddleAccess<F, SEvals>
where
F: BinaryField,
SEvals: Deref<Target = [F]>,
{
#[inline]
fn log_n(&self) -> usize {
self.log_n
}
#[inline]
fn get(&self, i: usize) -> F {
self.s_evals[i]
}
#[inline]
fn get_pair(&self, index_bits: usize, i: usize) -> (F, F) {
(self.s_evals[i], self.s_evals[1 << index_bits | i])
}
#[inline]
fn coset(&self, coset_bits: usize, coset: usize) -> impl TwiddleAccess<F> {
let log_n = self.log_n - coset_bits;
PrecomputedTwiddleAccess {
log_n,
s_evals: &self.s_evals[coset << log_n..(coset + 1) << log_n],
_marker: PhantomData,
}
}
}
/// Precompute the evaluations of the normalized subspace polynomials $\hat{W}_i$ on a basis.
///
/// Let $K/\mathbb{F}_2$ be a finite extension of degree $d$, and let $\beta_0,\ldots ,\beta_{d-1}$ be a linear basis,
/// with $\beta_0$ = 1. Let $U_i$ be the $\mathbb{F}_2$-linear span of $\beta_0,\ldots ,\beta_{i-1}$, so $U_0$ is the zero subspace.
/// Let $\hat{W}\_i(X)$ be the normalized subspace polynomial of degree $2^i$ that vanishes on $U_i$
/// and is $1$ on $\beta_i$.
/// Return a vector whose $i$th entry is a vector of evaluations of $\hat{W}\_i$ at $\beta_{i+1},\ldots ,\beta_{d-1}$.
fn precompute_subspace_evals<F: BinaryField, DomainField: BinaryField + Into<F>>(
log_domain_size: usize,
) -> Result<Vec<Vec<F>>, Error> {
if DomainField::N_BITS < log_domain_size {
return Err(Error::FieldTooSmall { log_domain_size });
}
let mut s_evals = Vec::with_capacity(log_domain_size);
// `normalization_consts[i]` = $W\_i(2^i):= W\_i(\beta_i)$
let mut normalization_consts = Vec::with_capacity(log_domain_size);
// $\beta_0 = 1$ and $W\_0(X) = X$, so $W\_0(\beta_0) = \beta_0 = 1$
normalization_consts.push(F::ONE);
//`s0_evals` = $(\beta_1,\ldots ,\beta_{d-1}) = (W\_0(\beta_1), \ldots , W\_0(\beta_{d-1}))$
let s0_evals = (1..log_domain_size)
.map(|i| {
DomainField::basis(i)
.expect("basis vector must exist because of FieldTooSmall check above")
.into()
})
.collect::<Vec<F>>();
s_evals.push(s0_evals);
// let $W\_i(X)$ be the *unnormalized* subspace polynomial, i.e., $\prod_{u\in U_{i}}(X-u)$.
// Then $W\_{i+1}(X) = W\_i(X)(W\_i(X)+W\_i(\beta_i))$. This crucially uses the "linearity" of
// $W\_i(X)$. This fundamental relation allows us to iteratively compute `s_evals` layer by layer.
for _ in 1..log_domain_size {
let (norm_const_i, s_i_evals) = {
let norm_prev = *normalization_consts
.last()
.expect("normalization_consts is not empty");
let s_prev_evals = s_evals.last().expect("s_evals is not empty");
// `norm_prev` = $W\_{i-1}(\beta_{i-1})$
// s_prev_evals = $W\_{i-1}(\beta_i),\ldots ,W\_{i-1}(\beta_{d-1})$
let norm_const_i = subspace_map(s_prev_evals[0], norm_prev);
let s_i_evals = s_prev_evals
.iter()
.skip(1)
.map(|&s_ij_prev| subspace_map(s_ij_prev, norm_prev))
.collect::<Vec<_>>();
// the two calls to the function subspace_map yield the following:
// `norm_const_i` = $W\_{i}(\beta_i)$; and
// `s_i_evals` = $W\_{i}(\beta_{i+1}),\ldots ,W\_{i}(\beta_{d-1})$.
(norm_const_i, s_i_evals)
};
normalization_consts.push(norm_const_i);
s_evals.push(s_i_evals);
}
for (norm_const_i, s_evals_i) in normalization_consts.iter().zip(s_evals.iter_mut()) {
let inv_norm_const = norm_const_i
.invert()
.expect("normalization constants are nonzero");
// replace all terms $W\_{i}(\beta_j)$ with $W\_{i}(\beta_j)/W\_{i}(\beta_i)$
// to obtain the evaluations of the *normalized* subspace polynomials.
for s_ij in s_evals_i.iter_mut() {
*s_ij *= inv_norm_const;
}
}
Ok(s_evals)
}
/// Computes the function $(e,c)\mapsto e^2+ce$.
///
/// This is primarily used to compute $W\_{i+1}(X)$ from $W\_i(X)$ in the binary field setting.
fn subspace_map<F: Field>(elem: F, constant: F) -> F {
elem.square() + constant * elem
}
/// Given `OnTheFlyTwiddleAccess` instances for each NTT round, returns a vector of `PrecomputedTwiddleAccess` objects,
/// one for each NTT round.
///
/// For each round $i$, the input contains the value of $\hat{W}\_i$ on the basis $\beta_{i+1},\ldots ,\beta_{d-1}$.
/// The ith element of the output contains the evaluations of $\hat{W}\_i$ on the entire space $K/U_{i+1}$,
/// where the order is the usual "binary counting order" in $\beta_{i+1},\ldots ,\beta_{d-1}$.
/// While $\hat{W}\_i$ is well-defined on $K/U_i$, we have the normalization $\hat{W}\_{i}(\beta_i)=1$,
/// hence to specify the function we need only specify it on $K/U_{i+1}$.
pub fn expand_subspace_evals<F, SEvals>(
on_the_fly: &[OnTheFlyTwiddleAccess<F, SEvals>],
) -> Vec<PrecomputedTwiddleAccess<F>>
where
F: BinaryField,
SEvals: Deref<Target = [F]>,
{
let log_domain_size = on_the_fly.len();
on_the_fly
.iter()
.enumerate()
.map(|(i, on_the_fly_i)| {
let s_evals_i = &on_the_fly_i.s_evals;
let mut expanded = Vec::with_capacity(1 << s_evals_i.len());
expanded.push(F::ZERO);
for &eval in s_evals_i.iter() {
for i in 0..expanded.len() {
expanded.push(expanded[i] + eval);
}
}
PrecomputedTwiddleAccess {
log_n: log_domain_size - 1 - i,
s_evals: expanded,
_marker: PhantomData,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{OnTheFlyTwiddleAccess, PrecomputedTwiddleAccess, TwiddleAccess};
use binius_field::{BinaryField, BinaryField16b, BinaryField32b, BinaryField8b};
use lazy_static::lazy_static;
use proptest::prelude::*;
lazy_static! {
// Precomputed and OnTheFlytwiddle access objects for various binary field sizes.
// We avoided doing the 32B precomputed twiddle access because the tests take too long.
static ref PRECOMPUTED_TWIDDLE_ACCESS_8B: Vec<PrecomputedTwiddleAccess<BinaryField8b>> =
PrecomputedTwiddleAccess::<BinaryField8b>::generate::<BinaryField8b>(8).unwrap();
static ref OTF_TWIDDLE_ACCESS_8B: Vec<OnTheFlyTwiddleAccess<BinaryField8b>> =
OnTheFlyTwiddleAccess::<BinaryField8b>::generate::<BinaryField8b>(8).unwrap();
static ref PRECOMPUTED_TWIDDLE_ACCESS_16B: Vec<PrecomputedTwiddleAccess<BinaryField16b>> =
PrecomputedTwiddleAccess::<BinaryField16b>::generate::<BinaryField16b>(16).unwrap();
static ref OTF_TWIDDLE_ACCESS_16B: Vec<OnTheFlyTwiddleAccess<BinaryField16b>> =
OnTheFlyTwiddleAccess::<BinaryField16b>::generate::<BinaryField16b>(16).unwrap();
static ref OTF_TWIDDLE_ACCESS_32B: Vec<OnTheFlyTwiddleAccess<BinaryField32b>> =
OnTheFlyTwiddleAccess::<BinaryField32b>::generate::<BinaryField32b>(32).unwrap();
}
// Tests that `PrecomputedTwiddleAccess` and `OnTheFlyTwiddleAccess`is linear.
// (This is more or less by design/construction for `PrecomputedTwiddleAccess`.)
// More concretely: picks a `layer`, $\ell$, and two valid indices,
// checks if the claimed equality holds: $\hat{W}_{\ell}(x) + \hat{W}_{\ell}(y) = \hat{W}_{\ell}(x + y).$
proptest! {
#[test]
fn test_linearity_precomputed_8b((x, y, layer) in generate_layer_and_indices(8)) {
let twiddle_access = &PRECOMPUTED_TWIDDLE_ACCESS_8B[layer];
test_linearity::<BinaryField8b, _>(twiddle_access, x, y);
}
#[test]
fn test_linearity_precomputed_16b((x, y, layer) in generate_layer_and_indices(16)) {
let twiddle_access = &PRECOMPUTED_TWIDDLE_ACCESS_16B[layer];
test_linearity::<BinaryField16b, _>(twiddle_access, x, y);
}
#[test]
fn test_linearity_otf_8b((x, y, layer) in generate_layer_and_indices(8)) {
let twiddle_access = &OTF_TWIDDLE_ACCESS_8B[layer];
test_linearity::<BinaryField8b, _>(twiddle_access, x, y);
}
#[test]
fn test_linearity_otf_16b((x, y, layer) in generate_layer_and_indices(16)) {
let twiddle_access = &OTF_TWIDDLE_ACCESS_16B[layer];
test_linearity::<BinaryField16b, _>(twiddle_access, x, y);
}
#[test]
fn test_linearity_otf_32b((x, y, layer) in generate_layer_and_indices(32)) {
let twiddle_access = &OTF_TWIDDLE_ACCESS_32B[layer];
test_linearity::<BinaryField32b, _>(twiddle_access, x, y);
}
}
// Test compatibility between layers for a `TwiddleAccess` object. More precisely,
// this checks that the values of $\hat{W}_{\ell}$ and $\hat{W}_{\ell+1}$ are compatible.
proptest! {
#[test]
fn test_compatibility_otf_8b((x, layer) in generate_layer_and_index(8)){
compatibility_between_layers::<BinaryField8b,_>(layer, &OTF_TWIDDLE_ACCESS_8B, x);
}
#[test]
fn test_compatibility_otf_16b((x, layer) in generate_layer_and_index(16)){
compatibility_between_layers::<BinaryField16b,_>(layer, &OTF_TWIDDLE_ACCESS_16B, x);
}
#[test]
fn test_compatibility_otf_32b((x, layer) in generate_layer_and_index(32)){
compatibility_between_layers::<BinaryField32b,_>(layer, &OTF_TWIDDLE_ACCESS_32B, x);
}
#[test]
fn test_compatibility_precomputed_8b((x, layer) in generate_layer_and_index(8)){
compatibility_between_layers::<BinaryField8b,_>(layer, &PRECOMPUTED_TWIDDLE_ACCESS_8B, x);
}
#[test]
fn test_compatibility_precomputed_16b((x, layer) in generate_layer_and_index(16)){
compatibility_between_layers::<BinaryField16b,_>(layer, &PRECOMPUTED_TWIDDLE_ACCESS_16B, x);
}
}
prop_compose! {
/// Given a `max_layer`, which is implicitly assumed to be the logarithm of the field size,
/// generate a layer (between 0 and `max_layer-2`) of the NTT instance,
/// such that `layer+1` is a valid layer, and furthermore generate
/// a valid index $x$ for `layer+1`.
///
/// Designed to test for compatibility between layers, hence `layer+1` must also be a valid layer.
fn generate_layer_and_index
(max_layer: usize)
(layer in 0usize..max_layer-1)
(x in 0usize..(1 << (max_layer-layer-2)), layer in Just(layer))
-> (usize, usize) {
(x, layer)
}
}
prop_compose! {
/// Given a `max_layer`, which is implicitly assumed to be the logarithm of the field size,
/// generate a layer (between 0 and `max_layer-1`) of the NTT instance a
/// pair of indices $(x, y)$ that are valid for that layer.
///
/// Designed for testing linearity.
fn generate_layer_and_indices
(max_layer: usize)
(layer in 0usize..max_layer)
(x in 0usize..(1 << (max_layer-layer-1)), y in 0usize..1<<(max_layer-layer-1), layer in Just(layer))
-> (usize, usize, usize) {
(x, y, layer)
}
}
/// Given a `TwiddleAccess` object, test linearity, i.e., that:
/// $\hat{W}\_{\ell}(x) + \hat{W}\_{\ell}(y) = \hat{W}\_{\ell}(x + y)$.
///
/// Here, it is important to note that although $x$ and $y$ are `usize`, we consider them
/// elements of the $F$ via the binary expansion encoding the coefficients of a basis
/// expansion. Then the expression $x+y$ in $F$ corresponds to the bitwise XOR of the
/// two `usize` values.
fn test_linearity<F: BinaryField + std::fmt::Display, T: TwiddleAccess<F>>(
twiddle_access: &T,
x: usize,
y: usize,
) {
let first_val = twiddle_access.get(x);
let second_val = twiddle_access.get(y);
assert_eq!(first_val + second_val, twiddle_access.get(x ^ y));
}
/// Test for compatibility between adjacent layers of a `TwiddleAccess` object.
///
/// This checks that the values of $\hat{W}\_{\ell}$ and $\hat{W}\_{\ell+1}$ are compatible.
/// Set $\tilde{W}\_{\ell+1}(X)=\hat{W}\_{\ell}(X)(\hat{W}\_{\ell}(X)+1)$.
/// Then $\hat{W}\_{\ell+1}(X)=\tilde{W}\_{\ell+1}(X)/\tilde{W}\_{\ell+1}(\beta_{\ell+1})$.
/// (The above ensures that $\hat{W}\_{\ell+1}$ has the right properties: vanishes in $U_{\ell}$ and
/// is 1 at $\beta_{\ell+1}$.) This means that knowing $\hat{W}\_{\ell}(x)$ and $\hat{W}\_{\ell}(\beta_{\ell+1}$,
/// we can compute $\hat{W}\_{\ell+1}(x)$.
fn compatibility_between_layers<F: BinaryField + std::fmt::Display, T: TwiddleAccess<F>>(
layer: usize,
twiddle_access: &[T],
// `index_next_layer` is a valid index for the layer `layer+1`,
// i.e., `twiddle_access_next_layer.get(index)` makes sense.
index_next_layer: usize,
) {
let twiddle_access_layer = &twiddle_access[layer];
let twiddle_access_next_layer = &twiddle_access[layer + 1];
// `index` corresponds to an element of $F/U_{i+1}$. This corresponds to elements
// of the space $F/U_i$ whose $\beta_i$ and $\beta_{i+1}$ coordinates are both 0.
let index = index_next_layer << 1;
// If index corresponds to an element $x$ in $F$.
// Claimed value of $\hat{W}_{\ell}(x)$.
let w_hat_layer_x = twiddle_access_layer.get(index);
// Claimed value of $\hat{W}_{\ell+1}(x)$.
let w_hat_next_layer_x = twiddle_access_next_layer.get(index_next_layer);
// In the below, `beta` refers to $\beta_{\ell+1}$.
// $\hat{W}_{\ell}(\beta_{\ell+1})$.
let w_hat_layer_beta = twiddle_access_layer.get(1);
// `normalizing_factor` is $\hat{W}_{\ell}(\beta) * (\hat{W}_{\ell}(\beta) + 1)$,
// i.e. $\tilde{W}_{\ell+1}(\beta)$.
let normalizing_factor = w_hat_layer_beta * w_hat_layer_beta + w_hat_layer_beta;
assert_eq!(
w_hat_next_layer_x * normalizing_factor,
w_hat_layer_x * w_hat_layer_x + w_hat_layer_x
);
}
}