p3_dft/radix_2_dit.rs
1use alloc::collections::BTreeMap;
2use alloc::sync::Arc;
3
4use p3_field::{Field, TwoAdicField};
5use p3_matrix::Matrix;
6use p3_matrix::dense::{RowMajorMatrix, RowMajorMatrixViewMut};
7use p3_matrix::util::reverse_matrix_index_bits;
8use p3_maybe_rayon::prelude::*;
9use p3_util::log2_strict_usize;
10use spin::RwLock;
11
12use crate::butterflies::{Butterfly, DitButterfly, TwiddleFreeButterfly};
13use crate::util::coset_shift_cols;
14use crate::{Layout, TwoAdicSubgroupDft};
15
16/// Radix-2 Decimation-in-Time FFT over a two-adic subgroup.
17///
18/// This struct implements a fast Fourier transform (FFT) using the Radix-2
19/// Decimation-in-Time (DIT) algorithm over a two-adic multiplicative subgroup of a finite field.
20/// It is optimized for a batch setting where multiple FFT's are being computed simultaneously.
21///
22/// Internally, the implementation memoizes twiddle factors (powers of the root of unity)
23/// for reuse across multiple transforms. This avoids redundant computation
24/// when performing FFTs of the same size.
25#[derive(Default, Clone, Debug)]
26pub struct Radix2Dit<F: TwoAdicField> {
27 /// Memoized twiddle factors indexed by `log2(n)`, where `n` is the DFT length.
28 ///
29 /// This allows fast lookup and reuse of previously computed twiddle values
30 /// (powers of a two-adic generator), which are expensive to recompute.
31 ///
32 /// `RwLock` is used to enable interior mutability for caching purposes along with thread
33 /// safety.
34 twiddles: Arc<RwLock<BTreeMap<usize, Arc<[F]>>>>,
35}
36
37impl<F: TwoAdicField> Radix2Dit<F> {
38 /// Returns the twiddle factors for a DFT of size `2^log_h`.
39 /// If they haven't been computed yet, this function computes and caches them.
40 fn get_or_compute_twiddles(&self, log_h: usize) -> Arc<[F]> {
41 // Fast path: Check if the twiddles already exist with a read lock.
42 if let Some(twiddles) = self.twiddles.read().get(&log_h) {
43 return twiddles.clone();
44 }
45 // Slow path: The twiddles were not found. We need to compute them.
46 // Acquire a write lock to ensure only one thread computes and inserts the values.
47 let mut w_lock = self.twiddles.write();
48 // Double-check: Another thread might have computed and inserted the twiddles
49 // while we were waiting for the write lock. The `entry` API handles this
50 // check and insertion atomically.
51 w_lock
52 .entry(log_h)
53 .or_insert_with(|| {
54 let n = 1 << log_h;
55 let root = F::two_adic_generator(log_h);
56 Arc::from(root.powers().collect_n(n / 2))
57 })
58 .clone()
59 }
60}
61
62impl<F: TwoAdicField> TwoAdicSubgroupDft<F> for Radix2Dit<F> {
63 type Evaluations = RowMajorMatrix<F>;
64
65 fn dft_batch(&self, mut mat: RowMajorMatrix<F>) -> RowMajorMatrix<F> {
66 let h = mat.height();
67 let log_h = log2_strict_usize(h);
68
69 // Compute twiddle factors, or take memoized ones if already available.
70 let twiddles = self.get_or_compute_twiddles(log_h);
71
72 // DIT butterfly
73 reverse_matrix_index_bits(&mut mat);
74 for layer in 0..log_h {
75 dit_layer(&mut mat.as_view_mut(), layer, &twiddles);
76 }
77 mat
78 }
79
80 fn coset_lde_batch_with_transform<T>(
81 &self,
82 mat: RowMajorMatrix<F>,
83 added_bits: usize,
84 shift: F,
85 transform: T,
86 ) -> Self::Evaluations
87 where
88 T: FnOnce(&mut RowMajorMatrixViewMut<'_, F>, Layout),
89 {
90 let w = mat.width();
91 let h = mat.height();
92 let padded_h = h << added_bits;
93
94 // Recover polynomial coefficients.
95 let mut coeffs = self.idft_batch(mat);
96
97 // Let the caller inspect or modify the coefficient buffer.
98 transform(&mut coeffs.as_view_mut(), Layout::Natural);
99
100 // Apply the coset shift to only the live coefficient rows; the zero-padded
101 // tail does not need shifting because 0 * s^j = 0 for any j.
102 coset_shift_cols(&mut coeffs, shift);
103
104 // Grow the buffer to full padded capacity without touching existing elements.
105 // The tail must hold zeros; we zero-initialize only the new slots.
106 let live_len = w * h;
107 let total_len = w * padded_h;
108 coeffs.values.reserve_exact(total_len - live_len);
109 coeffs.values.resize(total_len, F::ZERO);
110 coeffs.width = w;
111
112 // Forward DFT over the full padded height.
113 self.dft_batch(coeffs)
114 }
115}
116
117/// Applies one layer of the Radix-2 DIT FFT butterfly network.
118///
119/// Splits the matrix into blocks of rows and performs in-place butterfly operations
120/// on each block. Uses a `TwiddleFreeButterfly` for the first pair and `DitButterfly`
121/// with precomputed twiddles for the rest.
122///
123/// # Arguments
124/// - `mat`: Mutable matrix view with height as a power of two.
125/// - `layer`: Index of the current FFT layer (starting at 0).
126/// - `twiddles`: Precomputed twiddle factors for this layer.
127fn dit_layer<F: Field>(mat: &mut RowMajorMatrixViewMut<'_, F>, layer: usize, twiddles: &[F]) {
128 // Get the number of rows in the matrix (must be a power of two)
129 let h = mat.height();
130 // Compute reversed layer index to access twiddle indices correctly
131 let log_h = log2_strict_usize(h);
132 let layer_rev = log_h - 1 - layer;
133
134 // Each butterfly operates on 2 rows; this is the number of rows in half a block
135 let half_block_size = 1 << layer;
136 // Each block contains 2^layer * 2 rows; full size of the butterfly block
137 let block_size = half_block_size * 2;
138
139 // Process the matrix in blocks of rows of size `block_size`
140 mat.par_row_chunks_exact_mut(block_size)
141 .for_each(|mut block_chunks| {
142 // Split each block vertically into top (hi) and bottom (lo) halves
143 let (mut hi_chunks, mut lo_chunks) = block_chunks.split_rows_mut(half_block_size);
144 // For each pair of rows (hi, lo), apply a butterfly
145 hi_chunks
146 .par_rows_mut()
147 .zip(lo_chunks.par_rows_mut())
148 .enumerate()
149 .for_each(|(ind, (hi_chunk, lo_chunk))| {
150 if ind == 0 {
151 // The first pair doesn't require a twiddle factor
152 TwiddleFreeButterfly.apply_to_rows(hi_chunk, lo_chunk);
153 } else {
154 // Apply DIT butterfly using the twiddle factor at index `ind << layer_rev`
155 DitButterfly(twiddles[ind << layer_rev]).apply_to_rows(hi_chunk, lo_chunk);
156 }
157 });
158 });
159}