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}