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