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