binius_math/ntt/
domain_context.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3//! Different implementations for the [`DomainContext`] trait.
4
5use binius_field::BinaryField;
6
7use super::DomainContext;
8use crate::{BinarySubspace, binary_subspace::BinarySubspaceIterator};
9
10/// Given an $\ell$-dimensional subspace, this computes the evaluations of the normalized subspace
11/// polynomials $hat{W}_i$ on the basis.
12///
13/// The dimension of the subspace is $\ell$. This returns a vector of $\ell$ vectors. The $i$th
14/// vector contains $\ell - i$ entries and contains
15///
16/// $$
17/// \left( \hat{W}_i(\beta_j) \right)_{j = i}^{\ell - 1}
18/// $$
19fn generate_evals_from_subspace<F: BinaryField>(subspace: &BinarySubspace<F>) -> Vec<Vec<F>> {
20	let l = subspace.dim();
21	let mut evals = Vec::with_capacity(l);
22
23	// push $[W_0 (\beta_0), W_0 (\beta_1), ...] = [\beta_0, \beta_1, ...]$
24	evals.push(subspace.basis().to_vec());
25	for i in 1..l {
26		// push $[W_i (\beta_i), W_i (\beta_(i+1)), ...]$
27		evals.push(Vec::with_capacity(l - i));
28		for k in 1..evals[i - 1].len() {
29			// $W_i (X) = W_(i-1) (X) * [ W_(i-1) (X) + W_(i-1) (\beta_(i-1)) ]$
30			// hence: $W_i (\beta_j) = W_(i-1) (\beta_(j)) * [ W_(i-1) (\beta_j) + W_(i-1)
31			// (\beta_(i-1)) ]$
32			let val = evals[i - 1][k] * (evals[i - 1][k] + evals[i - 1][0]);
33			evals[i].push(val);
34		}
35	}
36
37	// normalize so that evaluations of $W_i$ are replaced by evaluations of $hat{W}_i$
38	for evals_i in evals.iter_mut() {
39		let w_i_b_i_inverse = evals_i[0].invert().unwrap();
40		for eval_i_j in evals_i.iter_mut() {
41			*eval_i_j *= w_i_b_i_inverse;
42		}
43	}
44
45	evals
46}
47
48/// Works for any $S^{(0)}$ and computes twiddles on-the-fly.
49///
50/// On-the-fly twiddle computation should not be used for running a big NTT or for a complete FRI
51/// folding. In both cases, all twiddles will be accessed eventually, so they might as well be
52/// precomputed. But it could possibly be used e.g. in the FRI verifier, which only accesses a few
53/// selected twiddles.
54#[derive(Debug)]
55pub struct GenericOnTheFly<F> {
56	/// The subspace polynomial evaluations.
57	///
58	/// Contains $\ell$ vectors. The $i$th vector stores
59	///
60	/// $$
61	/// \left( \hat{W}_i(\beta_j) \right)_{j = i}^{\ell - 1}.
62	/// $$
63	evals: Vec<Vec<F>>,
64}
65
66impl<F: BinaryField> GenericOnTheFly<F> {
67	/// Given a basis of $S^{(0)}$, computes a compatible [`DomainContext`].
68	///
69	/// This will _not_ precompute the twiddles; instead they will be computed on-the-fly.
70	pub fn generate_from_subspace(subspace: &BinarySubspace<F>) -> Self {
71		Self {
72			evals: generate_evals_from_subspace(subspace),
73		}
74	}
75}
76
77impl<F: BinaryField> DomainContext for GenericOnTheFly<F> {
78	type Field = F;
79
80	fn log_domain_size(&self) -> usize {
81		self.evals.len()
82	}
83
84	fn subspace(&self, i: usize) -> BinarySubspace<F> {
85		if i == 0 {
86			return BinarySubspace::with_dim(0).unwrap();
87		}
88		BinarySubspace::new_unchecked(self.evals[self.log_domain_size() - i].clone())
89	}
90
91	fn twiddle(&self, layer: usize, block: usize) -> F {
92		let v = &self.evals[self.log_domain_size() - layer - 1];
93		BinarySubspace::new_unchecked(&v[1..]).get(block)
94	}
95}
96
97/// Works for any $S^{(0)}$ and pre-computes twiddles.
98#[derive(Debug)]
99pub struct GenericPreExpanded<F> {
100	/// The $i$'th vector stores $[hat{W}_i (\beta_i), \hat{W}_i (\beta_(i+1)), ...]$.
101	evals: Vec<Vec<F>>,
102	/// The $n - i - 1$'th vector stores $[0, \hat{W}_i (\beta_(i+1)), \hat{W}_i (\beta_(i+2)),
103	/// \hat{W}_i (\beta_(i+2) + \beta_(i+1)), \hat{W}_i (\beta_(i+3)), ...]$.
104	///
105	/// Notice the absence of $\beta_i$ in this. (Which satisfies $\hat{W}_i (\beta_i) = 1$
106	/// and is absorbed in the butterfly operation itself rather than what we call "twiddles".)
107	expanded: Vec<Vec<F>>,
108}
109
110impl<F: BinaryField> GenericPreExpanded<F> {
111	/// Given a basis of $S^{(0)}$, computes a compatible [`DomainContext`].
112	///
113	/// This will _precompute_ the twiddles.
114	pub fn generate_from_subspace(subspace: &BinarySubspace<F>) -> Self {
115		let evals = generate_evals_from_subspace(subspace);
116
117		let mut expanded = Vec::with_capacity(evals.len());
118		for basis in evals.iter().rev() {
119			let mut expanded_i = Vec::with_capacity(1 << (basis.len() - 1));
120			expanded_i.push(F::ZERO);
121			for i in 1..basis.len() {
122				for j in 0..expanded_i.len() {
123					expanded_i.push(expanded_i[j] + basis[i]);
124				}
125			}
126			assert_eq!(expanded_i.len(), 1 << (basis.len() - 1));
127			expanded.push(expanded_i)
128		}
129		assert_eq!(expanded.len(), evals.len());
130
131		Self { evals, expanded }
132	}
133}
134
135impl<F: BinaryField> DomainContext for GenericPreExpanded<F> {
136	type Field = F;
137
138	fn log_domain_size(&self) -> usize {
139		self.evals.len()
140	}
141
142	fn subspace(&self, i: usize) -> BinarySubspace<F> {
143		if i == 0 {
144			return BinarySubspace::with_dim(0).unwrap();
145		}
146		BinarySubspace::new_unchecked(self.evals[self.log_domain_size() - i].clone())
147	}
148
149	fn twiddle(&self, layer: usize, block: usize) -> F {
150		self.expanded[layer][block]
151	}
152
153	fn iter_twiddles(&self, layer: usize, log_step_by: usize) -> impl Iterator<Item = F> {
154		self.expanded[layer]
155			.iter()
156			.step_by(1 << log_step_by)
157			.copied()
158	}
159}
160
161/// Provides a field element of trace 1.
162pub trait TraceOneElement {
163	/// Returns a field element which has trace 1.
164	fn trace_one_element() -> Self;
165}
166
167impl TraceOneElement for binius_field::BinaryField128bGhash {
168	fn trace_one_element() -> Self {
169		Self::new(1 << 121)
170	}
171}
172
173/// Computes the first `num_basis_elements` Gao-Mateer basis elements of the field.
174///
175/// ## Preconditions
176///
177/// - The degree (over $\mathbb{F}_2$) of the field needs to be a tower of two. For example, it does
178///   **not** work with $\mathbb{F}_{2^3}$, but it works with $\mathbb{F}_{2^4}$.
179/// - `num_basis_elements` must be nonzero
180fn gao_mateer_basis<F: BinaryField + TraceOneElement>(num_basis_elements: usize) -> Vec<F> {
181	assert!(F::N_BITS.is_power_of_two());
182
183	// this is beta_(F::N_BITS - 1)
184	// e.g. for a 128-bit field, this is beta_127
185	let mut beta = F::trace_one_element();
186
187	// we compute beta_126, then beta_125, etc, by repeatedly applying x |-> x^2 + x
188	for _i in 0..(F::N_BITS - num_basis_elements) {
189		beta = beta.square() + beta;
190	}
191
192	// we save beta_0, beta_1, ..., beta_(num_basis_elements - 1)
193	let mut basis = vec![F::ZERO; num_basis_elements];
194	basis[num_basis_elements - 1] = beta;
195	for i in (1..num_basis_elements).rev() {
196		basis[i - 1] = basis[i].square() + basis[i];
197	}
198
199	// check that beta_0 = 1, which must be necessarily true if the trace 1 element above indeed
200	// has trace 1
201	assert_eq!(basis[0], F::ONE);
202
203	basis
204}
205
206/// Produces a specific "Gao-Mateer" $S^{(0)}$ and computes twiddles on-the-fly. Only works for
207/// binary fields whose degree over $\mathbb{F}_2$ is a power of two.
208///
209/// A Gao-Mateer basis of the binary field $\mathbb{F}_{2^{2^k}}$ is any $\mathbb{F}_2$-basis
210/// $(\beta_0, \beta_1,..., \beta_{2^k - 1})$ with the following properties:
211/// - $\beta_{2^k-1}$ has trace 1
212/// - $\beta_i = \beta_{i+1}^2 + \beta_{i+1}$
213///
214/// This implies some nice properties:
215/// - the basis elements with a small index are in a small subfield, in fact
216///   $(\beta_0,...,\beta_{2^l - 1})$ is a basis of $\mathbb{F}_{2^{2^l}}$ for any $l$, and in
217///   particular $\beta_0 = 1$
218/// - The subspace polynomial $W_i$ of $\operatorname{span} {\beta_0, ..., \beta_{i-1}}$ is defined
219///   over $\mathbb{F}_2$, i.e., its coefficients are just $0$ or $1$.
220/// - The subspace polynomial is "auto-normalized", meaning $W_i (\beta_i) = 1$ so that $\hat{W}_i =
221///   W_i$.
222/// - The evaluations of the subspace polynomials are just the basis elements: $W_i (\beta_{i + r})
223///   = \beta_r$ for any $i$ and $r$.
224/// - The previous point together with the first point implies that twiddles in early layers lie in
225///   a small subfield. This could potentially be used to speed up the NTT if one can implement
226///   multiplication with an element from a small subfield more efficiently.
227/// - The folding maps for FRI are all $x \mapsto x^2 + x$, no normalization factors needed.
228///
229/// On-the-fly twiddle computation should not be used for running a big NTT or for a complete FRI
230/// folding. In both cases, all twiddles will be accessed eventually, so they might as well be
231/// precomputed. But it could possibly be used e.g. in the FRI verifier, which only accesses a few
232/// selected twiddles.
233#[derive(Debug)]
234pub struct GaoMateerOnTheFly<F> {
235	/// Stores $[\beta_0, ..., \beta_{\ell-1}]$.
236	basis: Vec<F>,
237}
238
239impl<F: BinaryField + TraceOneElement> GaoMateerOnTheFly<F> {
240	/// Given the intended size of $S^{(0)}$, computes a "nice" Gao-Mateer [`DomainContext`].
241	///
242	/// This will _not_ precompute the twiddles; instead they will be computed on-the-fly.
243	///
244	/// ## Preconditions
245	///
246	/// - The degree (over $\mathbb{F}_2$) of the field needs to be a tower of two. For example, it
247	///   does **not** work with $\mathbb{F}_{2^3}$, but it works with $\mathbb{F}_{2^4}$.
248	/// - `log_domain_size` must be nonzero
249	pub fn generate(log_domain_size: usize) -> Self {
250		Self {
251			basis: gao_mateer_basis(log_domain_size),
252		}
253	}
254}
255
256impl<F: BinaryField> DomainContext for GaoMateerOnTheFly<F> {
257	type Field = F;
258
259	fn log_domain_size(&self) -> usize {
260		self.basis.len()
261	}
262
263	fn subspace(&self, i: usize) -> BinarySubspace<F> {
264		BinarySubspace::new_unchecked(self.basis[..i].to_vec())
265	}
266
267	fn twiddle(&self, layer: usize, block: usize) -> F {
268		BinarySubspace::new_unchecked(&self.basis[1..=layer]).get(block)
269	}
270
271	fn iter_twiddles(&self, layer: usize, log_step_by: usize) -> impl Iterator<Item = F> + '_ {
272		BinarySubspaceIterator::new(&self.basis[1 + log_step_by..=layer])
273	}
274}
275
276/// Produces a specific "Gao-Mateer" $S^{(0)}$ and pre-computes twiddles. Only works for binary
277/// fields whose degree over $\mathbb{F}_2$ is a power of two.
278///
279/// For an explanation of this $S^{(0)}$, see [`GaoMateerOnTheFly`].
280#[derive(Debug)]
281pub struct GaoMateerPreExpanded<F> {
282	/// Stores $[\beta_0, \beta_1, ...]$.
283	basis: Vec<F>,
284	/// Stores $[0, \beta_1, \beta_2, \beta_2 + \beta_1, \beta_3, \beta_3 + \beta_1, ...]$.
285	///
286	/// Notice the absence of $\beta_0$. (Which is $\beta_0 = 1$ and is absorbed in the butterfly
287	/// operation itself rather than what we call "twiddles".)
288	expanded: Vec<F>,
289}
290
291impl<F: BinaryField + TraceOneElement> GaoMateerPreExpanded<F> {
292	/// Given the intended size of $S^{(0)}$, computes a "nice" Gao-Mateer [`DomainContext`].
293	///
294	/// This will _precompute_ the twiddles.
295	///
296	/// ## Preconditions
297	///
298	/// - The degree (over $\mathbb{F}_2$) of the field needs to be a tower of two. For example, it
299	///   does **not** work with $\mathbb{F}_{2^3}$, but it works with $\mathbb{F}_{2^4}$.
300	/// - `log_domain_size` must be nonzero
301	pub fn generate(log_domain_size: usize) -> Self {
302		let basis: Vec<F> = gao_mateer_basis(log_domain_size);
303
304		let mut expanded = Vec::with_capacity(1 << log_domain_size);
305		expanded.push(F::ZERO);
306		for i in 1..log_domain_size {
307			for j in 0..expanded.len() {
308				expanded.push(expanded[j] + basis[i]);
309			}
310		}
311		assert_eq!(expanded.len(), 1usize << (log_domain_size - 1));
312
313		Self { basis, expanded }
314	}
315}
316
317impl<F: BinaryField> DomainContext for GaoMateerPreExpanded<F> {
318	type Field = F;
319
320	fn log_domain_size(&self) -> usize {
321		self.basis.len()
322	}
323
324	fn subspace(&self, i: usize) -> BinarySubspace<F> {
325		BinarySubspace::new_unchecked(self.basis[..i].to_vec())
326	}
327
328	fn twiddle(&self, _layer: usize, block: usize) -> F {
329		self.expanded[block]
330	}
331
332	fn iter_twiddles(&self, layer: usize, log_step_by: usize) -> impl Iterator<Item = F> {
333		self.expanded[..1 << layer]
334			.iter()
335			.step_by(1 << log_step_by)
336			.copied()
337	}
338}
339
340#[cfg(test)]
341mod tests {
342	use super::*;
343	use crate::test_utils::B128;
344
345	fn test_equivalence<F: BinaryField>(
346		dc_1: &impl DomainContext<Field = F>,
347		dc_2: &impl DomainContext<Field = F>,
348		log_domain_size: usize,
349	) {
350		assert_eq!(dc_1.log_domain_size(), log_domain_size);
351		assert_eq!(dc_2.log_domain_size(), log_domain_size);
352
353		for i in 0..log_domain_size {
354			assert_eq!(dc_1.subspace(i), dc_2.subspace(i));
355
356			for block in 0..1 << i {
357				assert_eq!(dc_1.twiddle(i, block), dc_2.twiddle(i, block));
358			}
359		}
360		assert_eq!(dc_1.subspace(log_domain_size), dc_2.subspace(log_domain_size))
361	}
362
363	#[test]
364	fn test_generic() {
365		const LOG_SIZE: usize = 5;
366
367		let subspace = BinarySubspace::with_dim(LOG_SIZE).unwrap();
368
369		let dc_otf = GenericOnTheFly::<B128>::generate_from_subspace(&subspace);
370		let dc_pre = GenericPreExpanded::<B128>::generate_from_subspace(&subspace);
371
372		test_equivalence(&dc_otf, &dc_pre, LOG_SIZE);
373	}
374
375	#[test]
376	fn test_gao_mateer() {
377		const LOG_SIZE: usize = 5;
378
379		let dc_gm_otf = GaoMateerOnTheFly::<B128>::generate(LOG_SIZE);
380		let dc_gm_pre = GaoMateerPreExpanded::<B128>::generate(LOG_SIZE);
381		let dc_generic_otf =
382			GenericOnTheFly::<B128>::generate_from_subspace(&dc_gm_otf.subspace(LOG_SIZE));
383
384		test_equivalence(&dc_gm_otf, &dc_gm_pre, LOG_SIZE);
385		test_equivalence(&dc_gm_otf, &dc_generic_otf, LOG_SIZE);
386	}
387
388	#[test]
389	fn test_iter_layer() {
390		const LOG_SIZE: usize = 7;
391
392		let dc_gm_otf = GaoMateerOnTheFly::<B128>::generate(LOG_SIZE);
393		let dc_gm_pre = GaoMateerPreExpanded::<B128>::generate(LOG_SIZE);
394		let subspace = BinarySubspace::with_dim(LOG_SIZE).unwrap();
395		let dc_generic_otf = GenericOnTheFly::<B128>::generate_from_subspace(&subspace);
396		let dc_generic_pre = GenericPreExpanded::<B128>::generate_from_subspace(&subspace);
397
398		// Test that iter_layer returns the same values as calling twiddle individually
399		for layer in 0..LOG_SIZE {
400			let expected: Vec<_> = (0..1 << layer)
401				.map(|block| dc_gm_pre.twiddle(layer, block))
402				.collect();
403
404			assert_eq!(
405				dc_gm_otf.iter_twiddles(layer, 0).collect::<Vec<_>>(),
406				expected,
407				"GaoMateerOnTheFly iter_layer mismatch at layer {}",
408				layer
409			);
410			assert_eq!(
411				dc_gm_pre.iter_twiddles(layer, 0).collect::<Vec<_>>(),
412				expected,
413				"GaoMateerPreExpanded iter_layer mismatch at layer {}",
414				layer
415			);
416			assert_eq!(
417				dc_generic_otf.iter_twiddles(layer, 0).collect::<Vec<_>>(),
418				dc_generic_pre.iter_twiddles(layer, 0).collect::<Vec<_>>(),
419				"Generic iter_layer mismatch at layer {}",
420				layer
421			);
422		}
423	}
424}