Skip to main content

p3_binary_dft/
naive.rs

1//! Reference additive NTT, evaluated straight from the definition.
2
3use core::marker::PhantomData;
4
5use p3_binary_field::TowerLevel;
6use p3_matrix::Matrix;
7use p3_matrix::dense::RowMajorMatrix;
8use p3_util::log2_strict_usize;
9
10use crate::domain::domain_point;
11use crate::traits::AdditiveNtt;
12
13/// Reference additive NTT.
14///
15/// The forward transform evaluates `Σ_i d_i · X_i(x)` directly, with the normalised subspace
16/// polynomials taken from the product definition `Ŵ_j(x) = ∏_{s ∈ S_j}(x − s) / ∏_{s ∈ S_j}(v_j − s)`.
17/// It therefore depends on none of D8's identities and is a genuine oracle for the fast [`crate::LchNtt`].
18/// The cost is `O(n² log n)`; keep test sizes at or below `2^10`.
19#[derive(Clone, Debug, Default)]
20pub struct NaiveAdditiveNtt<F> {
21    _marker: PhantomData<F>,
22}
23
24/// `Ŵ_j` from the product definition, normalised at `v_j`.
25fn normalised_subspace_poly<F: TowerLevel>(j: usize, x: F) -> F {
26    let vanishing = |y: F| (0..1 << j).map(|m| y + domain_point::<F>(m)).product::<F>();
27    vanishing(x)
28        * vanishing(F::cantor_basis(j))
29            .try_inverse()
30            .expect("W_j does not vanish at v_j, which lies outside S_j")
31}
32
33impl<F: TowerLevel> AdditiveNtt<F> for NaiveAdditiveNtt<F> {
34    fn shifted_ntt_batch(&self, mat: RowMajorMatrix<F>, shift: F) -> RowMajorMatrix<F> {
35        let width = mat.width();
36        let height = mat.height();
37        let log_n = log2_strict_usize(height);
38
39        let mut out = F::zero_vec(width * height);
40        for row in 0..height {
41            let x = shift + domain_point::<F>(row);
42            // X_i(x) = ∏_j Ŵ_j(x)^{bit_j(i)}; build the 2^log_n products by doubling.
43            let mut basis = F::zero_vec(height);
44            basis[0] = F::ONE;
45            for j in 0..log_n {
46                let w = normalised_subspace_poly::<F>(j, x);
47                for i in 0..1 << j {
48                    basis[i + (1 << j)] = basis[i] * w;
49                }
50            }
51            for col in 0..width {
52                out[row * width + col] = (0..height)
53                    .map(|i| basis[i] * mat.values[i * width + col])
54                    .sum();
55            }
56        }
57        RowMajorMatrix::new(out, width)
58    }
59
60    fn shifted_intt_batch(&self, mut mat: RowMajorMatrix<F>, shift: F) -> RowMajorMatrix<F> {
61        let width = mat.width();
62        let log_n = log2_strict_usize(mat.height());
63
64        // Inverse LCH butterflies, with every twiddle taken from the product definition.
65        for j in 0..log_n {
66            let half = 1 << j;
67            for (block, base) in (0..mat.height()).step_by(half << 1).enumerate() {
68                let t =
69                    normalised_subspace_poly::<F>(j, shift + domain_point::<F>(block << (j + 1)));
70                for k in 0..half {
71                    for col in 0..width {
72                        let u = mat.values[(base + k) * width + col];
73                        let v = mat.values[(base + k + half) * width + col];
74                        let hi = u + v;
75                        mat.values[(base + k) * width + col] = u + t * hi;
76                        mat.values[(base + k + half) * width + col] = hi;
77                    }
78                }
79            }
80        }
81        mat
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use p3_binary_field::{BinaryField8, BinaryField16, TowerLevel};
88    use p3_field::PrimeCharacteristicRing;
89    use p3_matrix::Matrix;
90    use p3_matrix::dense::RowMajorMatrix;
91    use rand::SeedableRng;
92    use rand::rngs::SmallRng;
93
94    use super::NaiveAdditiveNtt;
95    use crate::traits::AdditiveNtt;
96    use crate::{domain_point, subspace_polynomial};
97
98    /// Transforming the `k`-th novel-basis unit vector gives `X_k` evaluated on the domain.
99    #[test]
100    fn unit_vectors_evaluate_the_novel_basis() {
101        const LOG_N: usize = 3;
102        let ntt = NaiveAdditiveNtt::<BinaryField8>::default();
103
104        for k in 0..1 << LOG_N {
105            let mut coeffs = BinaryField8::zero_vec(1 << LOG_N);
106            coeffs[k] = BinaryField8::ONE;
107            let evals = ntt.ntt_batch(RowMajorMatrix::new_col(coeffs));
108
109            for m in 0..1 << LOG_N {
110                // X_k(x) = ∏_j W_j(x)^{bit_j(k)}, with W_j normalised to W_j(v_j) = 1.
111                let x = domain_point::<BinaryField8>(m);
112                let expected = (0..LOG_N)
113                    .filter(|j| k >> j & 1 == 1)
114                    .map(|j| subspace_polynomial::<BinaryField8>(j, x))
115                    .product::<BinaryField8>();
116                assert_eq!(evals.values[m], expected, "k={k} m={m}");
117            }
118        }
119    }
120
121    /// The inverse transform undoes the forward transform, on a coset too.
122    #[test]
123    fn naive_round_trips() {
124        let mut rng = SmallRng::seed_from_u64(1);
125        let ntt = NaiveAdditiveNtt::<BinaryField16>::default();
126        let coeffs = RowMajorMatrix::<BinaryField16>::rand(&mut rng, 1 << 6, 3);
127        let shift = BinaryField16::from_repr(0x1234);
128
129        let evals = ntt.shifted_ntt_batch(coeffs.clone(), shift);
130        assert_eq!(ntt.shifted_intt_batch(evals, shift), coeffs);
131    }
132
133    /// The low-degree extension reproduces the input rows as its prefix.
134    #[test]
135    fn lde_extends_the_input() {
136        let mut rng = SmallRng::seed_from_u64(2);
137        let ntt = NaiveAdditiveNtt::<BinaryField16>::default();
138        let evals = RowMajorMatrix::<BinaryField16>::rand(&mut rng, 1 << 5, 2);
139
140        let extended = ntt.lde_batch(evals.clone(), 2);
141        assert_eq!(extended.height(), evals.height() * 4);
142        assert_eq!(&extended.values[..evals.values.len()], &evals.values[..]);
143    }
144
145    /// The Cantor basis makes the normalisation trivial. Assert that rather than believing it:
146    /// `normalised_subspace_poly` divides by `∏_{m ∈ S_j}(v_j − m)` and this pins that divisor
147    /// to `1`, independently of the `x² + x` recurrence the fast transform relies on instead.
148    #[test]
149    fn the_normalisation_divisor_is_one() {
150        for j in 0..8 {
151            let divisor = (0..1usize << j)
152                .map(|m| BinaryField8::cantor_basis(j) + domain_point::<BinaryField8>(m))
153                .product::<BinaryField8>();
154            assert_eq!(divisor, BinaryField8::ONE, "j={j}");
155        }
156    }
157}