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