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}