binius_math/ntt/
mod.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3//! Efficient implementations of the binary field additive NTT.
4//!
5//! See [LCH14] and [DP24] Section 2.3 for mathematical background.
6//!
7//! [LCH14]: <https://arxiv.org/abs/1404.3458>
8//! [DP24]: <https://eprint.iacr.org/2024/504>
9
10pub mod domain_context;
11mod neighbors_last;
12mod reference;
13#[cfg(test)]
14mod tests_evaluation;
15#[cfg(test)]
16pub mod tests_reference;
17
18use binius_field::{BinaryField, PackedField};
19pub use neighbors_last::{
20	NeighborsLastBreadthFirst, NeighborsLastMultiThread, NeighborsLastSingleThread,
21};
22pub use reference::NeighborsLastReference;
23
24use crate::{binary_subspace::BinarySubspace, field_buffer::FieldSliceMut};
25
26/// The binary field additive NTT.
27///
28/// A number-theoretic transform (NTT) is a linear transformation on a finite field analogous to
29/// the discrete fourier transform. The version of the additive NTT we use is originally described
30/// in [LCH14]. In [DP24] Section 4.1, the authors present the LCH additive NTT algorithm in a way
31/// that makes apparent its compatibility with the FRI proximity test. Throughout the
32/// documentation, we will refer to the notation used in [DP24].
33///
34/// The additive NTT is parameterized by a binary field $K$ and $\mathbb{F}\_2$-linear subspace. We
35/// write $\beta_0, \ldots, \beta_{\ell-1}$ for the ordered basis elements of the subspace. The
36/// basis determines a novel polynomial basis and an evaluation domain. In the forward direction,
37/// the additive NTT transforms a vector of polynomial coefficients, with respect to the novel
38/// polynomial basis, into a vector of their evaluations over the evaluation domain. The inverse
39/// transformation interpolates polynomial values over the domain into novel polynomial basis
40/// coefficients.
41///
42/// An [`AdditiveNTT`] implementation with a maximum domain dimension of $\ell$ can be applied on
43/// a sequence of $\ell + 1$ evaluation domains of sizes $2^0, \ldots, 2^\ell$. These are the
44/// domains $S^{(\ell)}, S^{(\ell - 1)}, \ldots, S^{(0)}$ defined in [DP24] Section 4.
45///
46/// The methods [`Self::forward_transform`] and [`Self::inverse_transform`] take three parameters:
47/// a `data` buffer with length `2^log_len`, `skip_early`, and `skip_late`. The number of total NTT
48/// layers is considered to be `log_len`. "Early" layers at the beginning of the forward transform
49/// or "late" layers at the end of the forward transform may be skipped. The NTT uses
50/// $S^{(\ell - n)}$ for the evaluation domain for an $n$-layer NTT. (Remember, the novel polynomial
51/// basis is itself parameterized by the evaluation domain.) Counterintuitively, the space
52/// $S^{(n+1)}$ is not necessarily a subset of $S^{(n)}$**. We choose this behavior for the
53/// [`AdditiveNTT`] trait because it facilitates compatibility with FRI when batching proximity
54/// tests for codewords of different dimensions.
55///
56/// [LCH14]: <https://arxiv.org/abs/1404.3458>
57/// [DP24]: <https://eprint.iacr.org/2024/504>
58pub trait AdditiveNTT {
59	type Field: BinaryField;
60
61	/// Forward transformation as defined in [DP24], Section 2.3.
62	///
63	/// Arguments:
64	/// - `data` is the data on which the NTT is performed.
65	/// - `skip_early` is the number of early layers that should be skipped
66	/// - `skip_late` is the number of late layers that should be skipped
67	///
68	/// ## Preconditons
69	///
70	/// - `skip_early + skip_late <= data.log_len()`
71	/// - `data.log_len() - skip_late <= self.log_domain_size()`
72	///
73	/// [DP24]: <https://eprint.iacr.org/2024/504>
74	fn forward_transform<P: PackedField<Scalar = Self::Field>>(
75		&self,
76		data: FieldSliceMut<P>,
77		skip_early: usize,
78		skip_late: usize,
79	);
80
81	/// Inverse transformation of [`Self::forward_transform`].
82	///
83	/// Note that "early" layers here refer to "early" time in the forward transform, i.e. layers
84	/// with low index in the forward transform.
85	///
86	/// ## Preconditions
87	///
88	/// - same as [`Self::forward_transform`]
89	fn inverse_transform<P: PackedField<Scalar = Self::Field>>(
90		&self,
91		data: FieldSliceMut<P>,
92		skip_early: usize,
93		skip_late: usize,
94	);
95
96	/// The associated [`DomainContext`].
97	fn domain_context(&self) -> &impl DomainContext<Field = Self::Field>;
98
99	/// See [`DomainContext::log_domain_size`].
100	fn log_domain_size(&self) -> usize {
101		self.domain_context().log_domain_size()
102	}
103
104	/// See [`DomainContext::subspace`].
105	fn subspace(&self, i: usize) -> BinarySubspace<Self::Field> {
106		self.domain_context().subspace(i)
107	}
108
109	/// See [`DomainContext::twiddle`].
110	fn twiddle(&self, i: usize, j: usize) -> Self::Field {
111		self.domain_context().twiddle(i, j)
112	}
113}
114
115/// Provides information about the domains $S^{(i)}$ and the associated twiddle factors.
116///
117/// Needed by the NTT and by FRI.
118pub trait DomainContext {
119	type Field: BinaryField;
120
121	/// Base 2 logarithm of the size of $S^{(0)}$, i.e., $\ell$.
122	///
123	/// In other words: Index of the first layer that can _not_ be computed anymore.
124	/// I.e., number of the latest layer that _can_ be computed, plus one.
125	/// Layers are indexed starting from 0.
126	///
127	/// If you intend to call the NTT with `skip_late = 0`, then this should be equal to the base 2
128	/// logarithm of the number of scalars in the input.
129	fn log_domain_size(&self) -> usize;
130
131	/// Returns the binary subspace with dimension $i$.
132	///
133	/// In [DP24], this subspace is referred to as $S^{(\ell - i)}$, where $\ell$ is the maximum
134	/// domain size of the NTT, i.e., `self.log_domain_size()`. We choose to reverse the indexing
135	/// order with respect to the paper because it is more natural in code that the $i$th subspace
136	/// has dimension $i$.
137	///
138	/// ## Preconditions
139	///
140	/// - `i` must be less than or equal to `self.log_domain_size()`
141	///
142	/// [DP24]: <https://eprint.iacr.org/2024/504>
143	fn subspace(&self, i: usize) -> BinarySubspace<Self::Field>;
144
145	/// Returns the twiddle of a certain block in a certain layer.
146	///
147	/// The layer numbers start from 0, i.e., the earliest layer is layer 0.
148	///
149	/// Let $i$ be `layer`, and $j$ be `block`. This returns
150	///
151	/// $$
152	/// S^{(\ell - i - 1)}_{2j} = \hat{W}_{\ell - i - 1}\left( \sum_{b = 0}^{i-1} j_b \beta_{\ell -
153	/// i + b} \right) $$
154	///
155	/// The equality above is a consequence of Corollary 4.5 from [DP24].
156	///
157	/// ## Preconditions
158	///
159	/// - `layer < self.log_domain_size()`
160	/// - `block < 2^layer`
161	///
162	/// [DP24]: <https://eprint.iacr.org/2024/504>
163	fn twiddle(&self, layer: usize, block: usize) -> Self::Field;
164
165	/// Returns an iterator over all twiddles in a layer.
166	///
167	/// For layer `i`, this iterates over `twiddle(i, block)` for `block` in `0..2^i`.
168	///
169	/// ## Preconditions
170	///
171	/// - `layer < self.log_domain_size()`
172	fn iter_twiddles(
173		&self,
174		layer: usize,
175		log_step_by: usize,
176	) -> impl Iterator<Item = Self::Field> + '_ {
177		(0..1 << (layer - log_step_by)).map(move |block| self.twiddle(layer, block << log_step_by))
178	}
179}
180
181/// Make it so that references to a [`DomainContext` implement [`DomainContext`] themselves.
182///
183/// This is useful, for example, if you need two objects that each want to _own_ a
184/// [`DomainContext`], but you don't want to clone the [`DomainContext`].
185impl<T: DomainContext> DomainContext for &T {
186	type Field = T::Field;
187
188	fn log_domain_size(&self) -> usize {
189		(*self).log_domain_size()
190	}
191
192	fn subspace(&self, i: usize) -> BinarySubspace<Self::Field> {
193		(*self).subspace(i)
194	}
195
196	fn twiddle(&self, layer: usize, block: usize) -> Self::Field {
197		(*self).twiddle(layer, block)
198	}
199
200	fn iter_twiddles(&self, layer: usize, log_step_by: usize) -> impl Iterator<Item = Self::Field> {
201		(*self).iter_twiddles(layer, log_step_by)
202	}
203}