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