Skip to main content

p3_dft/
radix_2_dit_parallel.rs

1use alloc::slice;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::mem::{MaybeUninit, transmute};
5
6use itertools::{Itertools, izip};
7use p3_field::integers::QuotientMap;
8use p3_field::{Field, Powers, PrimeCharacteristicRing, TwoAdicField};
9use p3_matrix::Matrix;
10use p3_matrix::bitrev::{BitReversalPerm, BitReversedMatrixView, BitReversibleMatrix};
11use p3_matrix::dense::{RowMajorMatrix, RowMajorMatrixView, RowMajorMatrixViewMut};
12use p3_matrix::util::reverse_matrix_index_bits;
13use p3_maybe_rayon::prelude::*;
14use p3_util::{log2_strict_usize, reverse_bits_len, reverse_slice_index_bits};
15use tracing::{debug_span, instrument};
16
17use crate::butterflies::{Butterfly, DitButterfly, ScaledDitButterfly, TwiddleFreeButterfly};
18use crate::twiddle_cache::TwiddleCache;
19use crate::{Layout, TwoAdicSubgroupDft};
20
21/// A parallel FFT algorithm which divides a butterfly network's layers into two halves.
22///
23/// For the first half, we apply a butterfly network with smaller blocks in earlier layers,
24/// i.e. either DIT or Bowers G. Then we bit-reverse, and for the second half, we continue executing
25/// the same network but in bit-reversed order. This way we're always working with small blocks,
26/// so within each half, we can have a certain amount of parallelism with no cross-thread
27/// communication.
28#[derive(Default, Clone, Debug)]
29pub struct Radix2DitParallel<F> {
30    /// Twiddles based on roots of unity, used in the forward DFT.
31    twiddles: Arc<TwiddleCache<usize, VectorPair<F>>>,
32
33    /// A map from `(log_h, shift)` to forward DFT twiddles with that coset shift baked in.
34    #[allow(clippy::type_complexity)]
35    coset_twiddles: Arc<TwiddleCache<(usize, F), [Vec<F>]>>,
36
37    /// Twiddles based on inverse roots of unity, used in the inverse DFT.
38    inverse_twiddles: Arc<TwiddleCache<usize, VectorPair<F>>>,
39}
40
41/// A pair of vectors, one with twiddle factors in their natural order, the other bit-reversed.
42#[derive(Default, Clone, Debug)]
43struct VectorPair<F> {
44    twiddles: Vec<F>,
45    bitrev_twiddles: Vec<F>,
46}
47
48impl<F> Radix2DitParallel<F>
49where
50    F: TwoAdicField + Ord,
51{
52    fn get_or_compute_twiddles(&self, log_h: usize) -> Arc<VectorPair<F>> {
53        self.twiddles.get_or_compute(log_h, || {
54            let half_h = (1 << log_h) >> 1;
55            let root = F::two_adic_generator(log_h);
56            let twiddles = root.powers().collect_n(half_h);
57            let mut bitrev_twiddles = twiddles.clone();
58            reverse_slice_index_bits(&mut bitrev_twiddles);
59
60            Arc::new(VectorPair {
61                twiddles,
62                bitrev_twiddles,
63            })
64        })
65    }
66
67    fn get_or_compute_coset_twiddles(&self, (log_h, shift): (usize, F)) -> Arc<[Vec<F>]> {
68        self.coset_twiddles.get_or_compute((log_h, shift), || {
69            let mid = log_h.div_ceil(2);
70            let h = 1 << log_h;
71            let root = F::two_adic_generator(log_h);
72            (0..log_h)
73                .map(|layer| {
74                    let shift_power = shift.exp_power_of_2(layer);
75                    let powers = Powers {
76                        base: root.exp_power_of_2(layer),
77                        current: shift_power,
78                    };
79                    let mut twiddles = powers.collect_n(h >> (layer + 1));
80                    let layer_rev = log_h - 1 - layer;
81                    if layer_rev >= mid {
82                        reverse_slice_index_bits(&mut twiddles);
83                    }
84                    twiddles
85                })
86                .collect::<Vec<_>>()
87                .into()
88        })
89    }
90
91    fn get_or_compute_inverse_twiddles(&self, log_h: usize) -> Arc<VectorPair<F>> {
92        self.inverse_twiddles.get_or_compute(log_h, || {
93            let half_h = (1 << log_h) >> 1;
94            let root_inv = F::two_adic_generator(log_h).inverse();
95            let twiddles = root_inv.powers().collect_n(half_h);
96            let mut bitrev_twiddles = twiddles.clone();
97            reverse_slice_index_bits(&mut bitrev_twiddles);
98
99            Arc::new(VectorPair {
100                twiddles,
101                bitrev_twiddles,
102            })
103        })
104    }
105}
106
107impl<F: TwoAdicField + Ord> TwoAdicSubgroupDft<F> for Radix2DitParallel<F> {
108    type Evaluations = BitReversedMatrixView<RowMajorMatrix<F>>;
109
110    fn dft_batch(&self, mut mat: RowMajorMatrix<F>) -> Self::Evaluations {
111        let h = mat.height();
112        let log_h = log2_strict_usize(h);
113
114        // Compute twiddle factors, or take memoized ones if already available.
115        let twiddles = self.get_or_compute_twiddles(log_h);
116
117        let mid = log_h.div_ceil(2);
118
119        // The first half looks like a normal DIT.
120        reverse_matrix_index_bits(&mut mat);
121        first_half(&mut mat, mid, &twiddles.twiddles);
122
123        // For the second half, we flip the DIT, working in bit-reversed order.
124        reverse_matrix_index_bits(&mut mat);
125        second_half(&mut mat, mid, &twiddles.bitrev_twiddles, None);
126
127        mat.bit_reverse_rows()
128    }
129
130    fn coset_dft_batch(&self, mut mat: RowMajorMatrix<F>, shift: F) -> Self::Evaluations {
131        reverse_matrix_index_bits(&mut mat);
132        coset_dft(self, &mut mat.as_view_mut(), shift, 0, &|_, _| {});
133        BitReversalPerm::new_view(mat)
134    }
135
136    fn idft_batch(&self, mut mat: RowMajorMatrix<F>) -> RowMajorMatrix<F> {
137        let h = mat.height();
138        if h == 1 {
139            return mat;
140        }
141        let log_h = log2_strict_usize(h);
142        let mid = log_h.div_ceil(2);
143        let inverse_twiddles = self.get_or_compute_inverse_twiddles(log_h);
144
145        reverse_matrix_index_bits(&mut mat);
146        first_half(&mut mat, mid, &inverse_twiddles.twiddles);
147        reverse_matrix_index_bits(&mut mat);
148        let h_inv_subfield = F::PrimeSubfield::ONE.div_2exp_u64(log_h as u64);
149        let scale = Some(F::from_prime_subfield(h_inv_subfield));
150        second_half(&mut mat, mid, &inverse_twiddles.bitrev_twiddles, scale);
151        reverse_matrix_index_bits(&mut mat);
152        mat
153    }
154
155    fn coset_idft_batch(&self, mat: RowMajorMatrix<F>, shift: F) -> RowMajorMatrix<F> {
156        let mut coeffs = self.idft_batch(mat);
157        crate::util::coset_shift_cols(&mut coeffs, shift.inverse());
158        coeffs
159    }
160
161    #[instrument(skip_all, level = "debug", fields(dims = %mat.dimensions(), added_bits = added_bits))]
162    fn coset_lde_batch_with_transform<T>(
163        &self,
164        mat: RowMajorMatrix<F>,
165        added_bits: usize,
166        shift: F,
167        transform: T,
168    ) -> Self::Evaluations
169    where
170        T: FnOnce(&mut RowMajorMatrixViewMut<'_, F>, Layout),
171    {
172        self.lde_with_consumer(mat, added_bits, shift, transform, &|_, _| {})
173    }
174
175    #[instrument(skip_all, level = "debug", fields(dims = %mat.dimensions(), added_bits = added_bits))]
176    fn coset_lde_batch_with_blocks<T, K, C>(
177        &self,
178        mat: RowMajorMatrix<F>,
179        added_bits: usize,
180        shift: F,
181        transform: T,
182        make_consumer: K,
183    ) -> Self::Evaluations
184    where
185        T: FnOnce(&mut RowMajorMatrixViewMut<'_, F>, Layout),
186        K: FnOnce(usize) -> C,
187        C: Fn(usize, RowMajorMatrixView<'_, F>) + Sync,
188    {
189        // Blocks are the second butterfly half's sub-FFTs; see `second_half_general`.
190        let log_h = log2_strict_usize(mat.height());
191        let mid = log_h.div_ceil(2);
192        let consume = make_consumer(1 << (log_h - mid));
193        self.lde_with_consumer(mat, added_bits, shift, transform, &consume)
194    }
195}
196
197impl<F: TwoAdicField + Ord> Radix2DitParallel<F> {
198    fn lde_with_consumer<T>(
199        &self,
200        mut mat: RowMajorMatrix<F>,
201        added_bits: usize,
202        shift: F,
203        transform: T,
204        consume: &(impl Fn(usize, RowMajorMatrixView<'_, F>) + Sync),
205    ) -> BitReversedMatrixView<RowMajorMatrix<F>>
206    where
207        T: FnOnce(&mut RowMajorMatrixViewMut<'_, F>, Layout),
208    {
209        let w = mat.width;
210        let h = mat.height();
211        let log_h = log2_strict_usize(h);
212        let mid = log_h.div_ceil(2);
213
214        let inverse_twiddles = self.get_or_compute_inverse_twiddles(log_h);
215
216        // The first half looks like a normal DIT.
217        reverse_matrix_index_bits(&mut mat);
218        first_half(&mut mat, mid, &inverse_twiddles.twiddles);
219
220        // For the second half, we flip the DIT, working in bit-reversed order.
221        reverse_matrix_index_bits(&mut mat);
222        // We'll also scale by 1/h, as per the usual inverse DFT algorithm.
223        // If F isn't a PrimeField, (and is thus an extension field) it's much cheaper to
224        // invert in F::PrimeSubfield.
225        let h_inv_subfield = F::PrimeSubfield::from_int(h).try_inverse();
226        let scale = h_inv_subfield.map(F::from_prime_subfield);
227        second_half(&mut mat, mid, &inverse_twiddles.bitrev_twiddles, scale);
228        // We skip the final bit-reversal, since the next FFT expects bit-reversed input.
229
230        transform(&mut mat.as_view_mut(), Layout::BitReversed);
231
232        let lde_elems = w * (h << added_bits);
233        let elems_to_add = lde_elems - w * h;
234        debug_span!("reserve_exact").in_scope(|| mat.values.reserve_exact(elems_to_add));
235
236        let g_big = F::two_adic_generator(log_h + added_bits);
237
238        // Resolve tables before the forward transforms, indexed by the exponent of g_big.
239        let coset_twiddles: Vec<_> = (0..(1usize << added_bits))
240            .into_par_iter()
241            .map(|coset_idx| {
242                let total_shift = g_big.exp_u64(coset_idx as u64) * shift;
243                self.get_or_compute_coset_twiddles((log_h, total_shift))
244            })
245            .collect();
246
247        let mat_ptr = mat.values.as_mut_ptr();
248        let rest_ptr = unsafe { (mat_ptr as *mut MaybeUninit<F>).add(w * h) };
249        let first_slice: &mut [F] = unsafe { slice::from_raw_parts_mut(mat_ptr, w * h) };
250        let rest_slice: &mut [MaybeUninit<F>] =
251            unsafe { slice::from_raw_parts_mut(rest_ptr, lde_elems - w * h) };
252        let mut first_coset_mat = RowMajorMatrixViewMut::new(first_slice, w);
253        let mut rest_cosets_mat = rest_slice
254            .chunks_exact_mut(w * h)
255            .map(|slice| RowMajorMatrixViewMut::new(slice, w))
256            .collect_vec();
257
258        // Each task writes a disjoint destination while sharing the coefficient matrix.
259        // Physical slot k + 1 holds coset reverse_bits_len(k + 1, added_bits).
260        let src = first_coset_mat.as_view();
261        rest_cosets_mat
262            .par_iter_mut()
263            .enumerate()
264            .for_each(|(k, dest)| {
265                let coset_idx = reverse_bits_len(k + 1, added_bits);
266                coset_dft_oop(&src, dest, &coset_twiddles[coset_idx], (k + 1) * h, consume);
267            });
268
269        // Join all coefficient readers and consumers before coset zero overwrites the coefficients.
270        coset_dft(self, &mut first_coset_mat.as_view_mut(), shift, 0, consume);
271
272        // SAFETY: We wrote all values above.
273        unsafe {
274            mat.values.set_len(lde_elems);
275        }
276        BitReversalPerm::new_view(mat)
277    }
278}
279
280#[instrument(level = "debug", skip_all)]
281fn coset_dft<F: TwoAdicField + Ord>(
282    dft: &Radix2DitParallel<F>,
283    mat: &mut RowMajorMatrixViewMut<'_, F>,
284    shift: F,
285    row_base: usize,
286    consume: &(impl Fn(usize, RowMajorMatrixView<'_, F>) + Sync),
287) {
288    let log_h = log2_strict_usize(mat.height());
289    let mid = log_h.div_ceil(2);
290
291    let twiddles = dft.get_or_compute_coset_twiddles((log_h, shift));
292
293    // The first half looks like a normal DIT.
294    first_half_general(mat, mid, &twiddles);
295
296    // For the second half, we flip the DIT, working in bit-reversed order.
297    reverse_matrix_index_bits(mat);
298
299    second_half_general(mat, mid, &twiddles, row_base, consume);
300}
301
302/// Like `coset_dft`, except out-of-place and using precomputed twiddles.
303///
304/// Tables must use the layer layout of `get_or_compute_coset_twiddles` for the
305/// source height and intended shift. Each layer contains `height >> (layer + 1)` entries.
306///
307/// # Panics
308/// Panics if the matrix dimensions or twiddle table dimensions do not match,
309/// or if the height is not a positive power of two.
310#[instrument(level = "debug", skip_all)]
311fn coset_dft_oop<F: Field>(
312    src: &RowMajorMatrixView<'_, F>,
313    dst_maybe: &mut RowMajorMatrixViewMut<'_, MaybeUninit<F>>,
314    twiddles: &[Vec<F>],
315    row_base: usize,
316    consume: &(impl Fn(usize, RowMajorMatrixView<'_, F>) + Sync),
317) {
318    assert_eq!(src.dimensions(), dst_maybe.dimensions());
319
320    let log_h = log2_strict_usize(dst_maybe.height());
321    // Short tables can leave destination rows uninitialized before the casts below.
322    assert_eq!(twiddles.len(), log_h, "incorrect number of twiddle layers");
323    for (layer, table) in twiddles.iter().enumerate() {
324        assert_eq!(
325            table.len(),
326            src.height() >> (layer + 1),
327            "incorrect twiddle count for layer {layer}"
328        );
329    }
330
331    let mid = log_h.div_ceil(2);
332    if log_h == 0 {
333        // This is an edge case where first_half_general_oop doesn't work, as it expects there to be
334        // at least one layer in the network, so we just copy instead.
335        let src_maybe = unsafe {
336            transmute::<&RowMajorMatrixView<'_, F>, &RowMajorMatrixView<'_, MaybeUninit<F>>>(src)
337        };
338        dst_maybe.copy_from(src_maybe);
339    } else {
340        // The first half looks like a normal DIT.
341        first_half_general_oop(src, dst_maybe, mid, twiddles);
342    }
343
344    // SAFETY: The copy or first FFT half initialized every destination element.
345    let dst = unsafe {
346        transmute::<&mut RowMajorMatrixViewMut<'_, MaybeUninit<F>>, &mut RowMajorMatrixViewMut<'_, F>>(
347            dst_maybe,
348        )
349    };
350
351    if log_h == 0 {
352        consume(row_base, dst.as_view());
353        return;
354    }
355
356    // For the second half, we flip the DIT, working in bit-reversed order.
357    reverse_matrix_index_bits(dst);
358
359    second_half_general(dst, mid, twiddles, row_base, consume);
360}
361
362/// This can be used as the first half of a DIT butterfly network.
363///
364/// For layer 0, all twiddle factors are 1 (root^0 = 1), so we use `TwiddleFreeButterfly`
365/// to avoid a Montgomery multiply by 1 across the entire matrix.
366///
367/// For layers 1 to mid-1 included, the first twiddle in each block is also always 1 (`twiddles[0] = 1`),
368/// so we special-case the first row-pair of each block to use `TwiddleFreeButterfly` as well.
369#[instrument(level = "debug", skip_all)]
370fn first_half<F: Field>(mat: &mut RowMajorMatrix<F>, mid: usize, twiddles: &[F]) {
371    let log_h = log2_strict_usize(mat.height());
372
373    // max block size: 2^mid
374    mat.par_row_chunks_exact_mut(1 << mid)
375        .for_each(|mut submat| {
376            let mut backwards = false;
377            for layer in 0..mid {
378                if layer == 0 {
379                    // For layer 0, half_block_size=1 and each block clones the twiddle
380                    // iterator from the start, consuming only twiddles[0] = root^0 = 1.
381                    // Use TwiddleFreeButterfly to skip the multiply entirely.
382                    dit_layer_twiddle_free(&mut submat, backwards);
383                } else {
384                    let layer_rev = log_h - 1 - layer;
385                    let layer_pow = 1 << layer_rev;
386                    // For layers 1..mid-1, twiddles[0] = root^0 = 1 is always the first
387                    // twiddle consumed per block. Use the optimized version that applies
388                    // TwiddleFreeButterfly for the first row-pair of each block.
389                    dit_layer_first_one(
390                        &mut submat,
391                        layer,
392                        twiddles.iter().step_by(layer_pow),
393                        backwards,
394                    );
395                }
396                backwards = !backwards;
397            }
398        });
399}
400
401/// Like `first_half`, except supporting different twiddle factors per layer, enabling coset shifts
402/// to be baked into them.
403#[instrument(level = "debug", skip_all)]
404fn first_half_general<F: Field>(
405    mat: &mut RowMajorMatrixViewMut<'_, F>,
406    mid: usize,
407    twiddles: &[Vec<F>],
408) {
409    let log_h = log2_strict_usize(mat.height());
410    mat.par_row_chunks_exact_mut(1 << mid)
411        .for_each(|mut submat| {
412            let mut backwards = false;
413            for layer in 0..mid {
414                let layer_rev = log_h - 1 - layer;
415                dit_layer(&mut submat, layer, twiddles[layer_rev].iter(), backwards);
416                backwards = !backwards;
417            }
418        });
419}
420
421/// Like `first_half_general`, except out-of-place.
422///
423/// Assumes there's at least one layer in the network, i.e. `src.height() > 1`.
424///
425/// # Panics
426/// Panics (via `log2_strict_usize` and arithmetic underflow) if `src.height() < 2`.
427#[instrument(level = "debug", skip_all)]
428fn first_half_general_oop<F: Field>(
429    src: &RowMajorMatrixView<'_, F>,
430    dst_maybe: &mut RowMajorMatrixViewMut<'_, MaybeUninit<F>>,
431    mid: usize,
432    twiddles: &[Vec<F>],
433) {
434    let log_h = log2_strict_usize(src.height());
435    src.par_row_chunks_exact(1 << mid)
436        .zip(dst_maybe.par_row_chunks_exact_mut(1 << mid))
437        .for_each(|(src_submat, mut dst_submat_maybe)| {
438            debug_assert_eq!(src_submat.dimensions(), dst_submat_maybe.dimensions());
439
440            // The first layer is special, done out-of-place.
441            // (Recall from the mid definition that there must be at least one layer here.)
442            let layer_rev = log_h - 1;
443            dit_layer_oop(
444                &src_submat,
445                &mut dst_submat_maybe,
446                0,
447                twiddles[layer_rev].iter(),
448            );
449
450            // submat is now initialized.
451            let mut dst_submat = unsafe {
452                transmute::<RowMajorMatrixViewMut<'_, MaybeUninit<F>>, RowMajorMatrixViewMut<'_, F>>(
453                    dst_submat_maybe,
454                )
455            };
456
457            // Subsequent layers.
458            let mut backwards = true;
459            for layer in 1..mid {
460                let layer_rev = log_h - 1 - layer;
461                dit_layer(
462                    &mut dst_submat,
463                    layer,
464                    twiddles[layer_rev].iter(),
465                    backwards,
466                );
467                backwards = !backwards;
468            }
469        });
470}
471
472/// This can be used as the second half of a DIT butterfly network. It works in bit-reversed order.
473///
474/// The optional `scale` parameter is used to scale the matrix by a constant factor. Rather than
475/// doing a separate pass over memory, we fold the scaling into the first butterfly layer to
476/// eliminate an extra memory pass.
477#[instrument(level = "debug", skip_all)]
478#[inline(always)] // To avoid branch on scale
479fn second_half<F: Field>(
480    mat: &mut RowMajorMatrix<F>,
481    mid: usize,
482    twiddles_rev: &[F],
483    scale: Option<F>,
484) {
485    let log_h = log2_strict_usize(mat.height());
486
487    // max block size: 2^(log_h - mid)
488    mat.par_row_chunks_exact_mut(1 << (log_h - mid))
489        .enumerate()
490        .for_each(|(thread, mut submat)| {
491            let mut backwards = false;
492            if let Some(scale) = scale {
493                // Fold the scale into the first butterfly layer to avoid a separate
494                // memory pass. This merges the O(N) scaling step into the first O(N)
495                // butterfly pass.
496                let mut scale_applied = false;
497                for layer in mid..log_h {
498                    let first_block = thread << (layer - mid);
499                    if !scale_applied {
500                        scale_applied = true;
501                        dit_layer_rev_scaled(
502                            &mut submat,
503                            log_h,
504                            layer,
505                            twiddles_rev[first_block..].iter().copied(),
506                            backwards,
507                            Some(scale),
508                        );
509                    } else {
510                        dit_layer_rev(
511                            &mut submat,
512                            log_h,
513                            layer,
514                            twiddles_rev[first_block..].iter().copied(),
515                            backwards,
516                        );
517                    }
518                    backwards = !backwards;
519                }
520                // Handle case where there are no layers in the second half (mid == log_h).
521                if !scale_applied {
522                    submat.scale(scale);
523                }
524            } else {
525                for layer in mid..log_h {
526                    let first_block = thread << (layer - mid);
527                    dit_layer_rev(
528                        &mut submat,
529                        log_h,
530                        layer,
531                        twiddles_rev[first_block..].iter().copied(),
532                        backwards,
533                    );
534                    backwards = !backwards;
535                }
536            }
537        });
538}
539
540/// Like `second_half`, except supporting different twiddle factors per layer, enabling coset shifts
541/// to be baked into them.
542#[instrument(level = "debug", skip_all)]
543fn second_half_general<F: Field>(
544    mat: &mut RowMajorMatrixViewMut<'_, F>,
545    mid: usize,
546    twiddles_rev: &[Vec<F>],
547    row_base: usize,
548    consume: &(impl Fn(usize, RowMajorMatrixView<'_, F>) + Sync),
549) {
550    let log_h = log2_strict_usize(mat.height());
551    mat.par_row_chunks_exact_mut(1 << (log_h - mid))
552        .enumerate()
553        .for_each(|(thread, mut submat)| {
554            let mut backwards = false;
555            for layer in mid..log_h {
556                let layer_rev = log_h - 1 - layer;
557                let first_block = thread << (layer - mid);
558                dit_layer_rev(
559                    &mut submat,
560                    log_h,
561                    layer,
562                    twiddles_rev[layer_rev][first_block..].iter().copied(),
563                    backwards,
564                );
565                backwards = !backwards;
566            }
567            consume(row_base + thread * submat.height(), submat.as_view());
568        });
569}
570
571/// One layer of a DIT butterfly network where all twiddle factors are 1 (i.e., layer 0).
572///
573/// This is equivalent to `dit_layer` with `layer=0` and `twiddles[0]=1`, but uses
574/// `TwiddleFreeButterfly` to avoid a Montgomery multiplication by 1 in the hot loop.
575///
576/// Correctness: For layer=0, `half_block_size=1` and each block clones the twiddle
577/// iterator from position 0, consuming only `twiddles[0] = generator^0 = 1`.
578/// Since multiplying by 1 is a no-op, `TwiddleFreeButterfly` gives identical results.
579fn dit_layer_twiddle_free<F: Field>(submat: &mut RowMajorMatrixViewMut<'_, F>, backwards: bool) {
580    // layer=0 means half_block_size=1, block_size=2.
581    let width = submat.width();
582    debug_assert!(submat.height() >= 2);
583
584    let process_block = move |block: &mut [F]| {
585        // Each block is exactly 2 rows: lo = block[0..width], hi = block[width..2*width]
586        let (lo, hi) = block.split_at_mut(width);
587        TwiddleFreeButterfly.apply_to_rows(lo, hi);
588    };
589
590    let blocks = submat.values.chunks_mut(2 * width);
591    if backwards {
592        for block in blocks.rev() {
593            process_block(block);
594        }
595    } else {
596        for block in blocks {
597            process_block(block);
598        }
599    }
600}
601
602/// One layer of a DIT butterfly network where the first twiddle factor per block is always 1.
603///
604/// This is used in `first_half` for layers 1..mid-1 of the standard (non-coset) DFT/inverse DFT,
605/// where `twiddles[0] = root^0 = 1`. The first row-pair of each block uses `TwiddleFreeButterfly`
606/// to avoid one Montgomery multiplication per block, while subsequent row-pairs use `DitButterfly`.
607///
608/// Correctness: The twiddle iterator yields `twiddles[0], twiddles[step], twiddles[2*step], ...`
609/// where `twiddles[0] = root^0 = 1`. Only used when this property holds.
610fn dit_layer_first_one<'a, F: Field>(
611    submat: &mut RowMajorMatrixViewMut<'_, F>,
612    layer: usize,
613    twiddles: impl Iterator<Item = &'a F> + Clone,
614    backwards: bool,
615) {
616    let half_block_size = 1 << layer;
617    let block_size = half_block_size * 2;
618    let width = submat.width();
619    debug_assert!(submat.height() >= block_size);
620    debug_assert!(
621        half_block_size >= 2,
622        "layer must be >= 1 for dit_layer_first_one"
623    );
624
625    let process_block = move |block: &mut [F]| {
626        let (lows, highs) = block.split_at_mut(half_block_size * width);
627        let mut tw_iter = twiddles.clone();
628        // First row-pair: twiddle is always 1, use TwiddleFreeButterfly to skip the multiply.
629        let _ = tw_iter.next(); // consume twiddles[0] = 1
630        let (lo0, lo_rest) = lows.split_at_mut(width);
631        let (hi0, hi_rest) = highs.split_at_mut(width);
632        TwiddleFreeButterfly.apply_to_rows(lo0, hi0);
633        // Remaining row-pairs use DitButterfly with their respective twiddle factors.
634        for (lo, hi, twiddle) in izip!(
635            lo_rest.chunks_mut(width),
636            hi_rest.chunks_mut(width),
637            tw_iter
638        ) {
639            DitButterfly(*twiddle).apply_to_rows(lo, hi);
640        }
641    };
642
643    let blocks = submat.values.chunks_mut(block_size * width);
644    if backwards {
645        for block in blocks.rev() {
646            process_block(block);
647        }
648    } else {
649        for block in blocks {
650            process_block(block);
651        }
652    }
653}
654
655/// One layer of a DIT butterfly network.
656fn dit_layer<'a, F: Field>(
657    submat: &mut RowMajorMatrixViewMut<'_, F>,
658    layer: usize,
659    twiddles: impl Iterator<Item = &'a F> + Clone,
660    backwards: bool,
661) {
662    let half_block_size = 1 << layer;
663    let block_size = half_block_size * 2;
664    let width = submat.width();
665    debug_assert!(submat.height() >= block_size);
666
667    let process_block = move |block: &mut [F]| {
668        let (lows, highs) = block.split_at_mut(half_block_size * width);
669        for (lo, hi, twiddle) in izip!(
670            lows.chunks_mut(width),
671            highs.chunks_mut(width),
672            twiddles.clone()
673        ) {
674            DitButterfly(*twiddle).apply_to_rows(lo, hi);
675        }
676    };
677
678    let blocks = submat.values.chunks_mut(block_size * width);
679    if backwards {
680        for block in blocks.rev() {
681            process_block(block);
682        }
683    } else {
684        for block in blocks {
685            process_block(block);
686        }
687    }
688}
689
690/// One layer of a DIT butterfly network, out-of-place.
691fn dit_layer_oop<'a, F: Field>(
692    src: &RowMajorMatrixView<'_, F>,
693    dst: &mut RowMajorMatrixViewMut<'_, MaybeUninit<F>>,
694    layer: usize,
695    twiddles: impl Iterator<Item = &'a F> + Clone,
696) {
697    debug_assert_eq!(src.dimensions(), dst.dimensions());
698    let half_block_size = 1 << layer;
699    let block_size = half_block_size * 2;
700    let width = dst.width();
701    debug_assert!(dst.height() >= block_size);
702
703    let process_blocks = move |src_block: &[F], dst_block: &mut [MaybeUninit<F>]| {
704        let (src_lows, src_highs) = src_block.split_at(half_block_size * width);
705        let (dst_lows, dst_highs) = dst_block.split_at_mut(half_block_size * width);
706
707        for (src_lo, dst_lo, src_hi, dst_hi, twiddle) in izip!(
708            src_lows.chunks(width),
709            dst_lows.chunks_mut(width),
710            src_highs.chunks(width),
711            dst_highs.chunks_mut(width),
712            twiddles.clone()
713        ) {
714            DitButterfly(*twiddle).apply_to_rows_oop(src_lo, dst_lo, src_hi, dst_hi);
715        }
716    };
717
718    let src_chunks = src.values.chunks(block_size * width);
719    let dst_chunks = dst.values.chunks_mut(block_size * width);
720
721    for (src_block, dst_block) in src_chunks.zip(dst_chunks) {
722        process_blocks(src_block, dst_block);
723    }
724}
725
726/// Like `dit_layer_rev`, except with an optional scale factor folded into the butterfly.
727///
728/// This avoids an extra memory pass when scaling is required (e.g., 1/N in inverse DFT).
729/// When `scale` is `None`, this is identical to `dit_layer_rev`.
730///
731/// When `scale` is `Some(s)`, uses `ScaledDitButterfly::new(twiddle, s)` which precomputes
732/// `twiddle * scale` once per block, reducing multiplications in the hot loop from 3 to 2.
733fn dit_layer_rev_scaled<F: Field>(
734    submat: &mut RowMajorMatrixViewMut<'_, F>,
735    log_h: usize,
736    layer: usize,
737    twiddles_rev: impl DoubleEndedIterator<Item = F> + ExactSizeIterator,
738    backwards: bool,
739    scale: Option<F>,
740) {
741    let layer_rev = log_h - 1 - layer;
742
743    let half_block_size = 1 << layer_rev;
744    let block_size = half_block_size * 2;
745    let width = submat.width();
746    debug_assert!(submat.height() >= block_size);
747
748    match scale {
749        None => {
750            // No scaling: same as regular dit_layer_rev
751            let blocks_and_twiddles = submat
752                .values
753                .chunks_mut(block_size * width)
754                .zip(twiddles_rev);
755            if backwards {
756                for (block, twiddle) in blocks_and_twiddles.rev() {
757                    let (lo, hi) = block.split_at_mut(half_block_size * width);
758                    DitButterfly(twiddle).apply_to_rows(lo, hi);
759                }
760            } else {
761                for (block, twiddle) in blocks_and_twiddles {
762                    let (lo, hi) = block.split_at_mut(half_block_size * width);
763                    DitButterfly(twiddle).apply_to_rows(lo, hi);
764                }
765            }
766        }
767        Some(s) => {
768            // Fold scaling into the butterfly to avoid a separate memory pass.
769            // ScaledDitButterfly::new precomputes twiddle * scale once per block,
770            // so the hot loop only needs 2 multiplications instead of 3.
771            let blocks_and_twiddles = submat
772                .values
773                .chunks_mut(block_size * width)
774                .zip(twiddles_rev);
775            if backwards {
776                for (block, twiddle) in blocks_and_twiddles.rev() {
777                    let (lo, hi) = block.split_at_mut(half_block_size * width);
778                    ScaledDitButterfly::new(twiddle, s).apply_to_rows(lo, hi);
779                }
780            } else {
781                for (block, twiddle) in blocks_and_twiddles {
782                    let (lo, hi) = block.split_at_mut(half_block_size * width);
783                    ScaledDitButterfly::new(twiddle, s).apply_to_rows(lo, hi);
784                }
785            }
786        }
787    }
788}
789
790/// Like `dit_layer`, except the matrix and twiddles are encoded in bit-reversed order.
791/// This can also be viewed as a layer of the Bowers G^T network.
792fn dit_layer_rev<F: Field>(
793    submat: &mut RowMajorMatrixViewMut<'_, F>,
794    log_h: usize,
795    layer: usize,
796    twiddles_rev: impl DoubleEndedIterator<Item = F> + ExactSizeIterator,
797    backwards: bool,
798) {
799    let layer_rev = log_h - 1 - layer;
800
801    let half_block_size = 1 << layer_rev;
802    let block_size = half_block_size * 2;
803    let width = submat.width();
804    debug_assert!(submat.height() >= block_size);
805
806    let blocks_and_twiddles = submat
807        .values
808        .chunks_mut(block_size * width)
809        .zip(twiddles_rev);
810    if backwards {
811        for (block, twiddle) in blocks_and_twiddles.rev() {
812            let (lo, hi) = block.split_at_mut(half_block_size * width);
813            DitButterfly(twiddle).apply_to_rows(lo, hi);
814        }
815    } else {
816        for (block, twiddle) in blocks_and_twiddles {
817            let (lo, hi) = block.split_at_mut(half_block_size * width);
818            DitButterfly(twiddle).apply_to_rows(lo, hi);
819        }
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use p3_baby_bear::BabyBear;
826    use p3_field::TwoAdicField;
827    use p3_matrix::Matrix;
828    use p3_matrix::dense::RowMajorMatrix;
829    use rand::SeedableRng;
830    use rand::rngs::SmallRng;
831
832    use super::*;
833
834    type F = BabyBear;
835
836    #[test]
837    #[should_panic(expected = "incorrect number of twiddle layers")]
838    fn coset_dft_oop_rejects_missing_twiddle_layer() {
839        let dft = Radix2DitParallel::<F>::default();
840        let mut twiddles = dft
841            .get_or_compute_coset_twiddles((3, F::GENERATOR))
842            .to_vec();
843        twiddles.pop();
844        let src = RowMajorMatrix::new(alloc::vec![F::ONE; 8], 1);
845        let mut dst = RowMajorMatrix::new(alloc::vec![MaybeUninit::uninit(); 8], 1);
846
847        coset_dft_oop(
848            &src.as_view(),
849            &mut dst.as_view_mut(),
850            &twiddles,
851            0,
852            &|_, _| {},
853        );
854    }
855
856    #[test]
857    #[should_panic(expected = "incorrect twiddle count for layer 2")]
858    fn coset_dft_oop_rejects_short_twiddle_layer() {
859        let dft = Radix2DitParallel::<F>::default();
860        let mut twiddles = dft
861            .get_or_compute_coset_twiddles((3, F::GENERATOR))
862            .to_vec();
863        twiddles[2].clear();
864        let src = RowMajorMatrix::new(alloc::vec![F::ONE; 8], 1);
865        let mut dst = RowMajorMatrix::new(alloc::vec![MaybeUninit::uninit(); 8], 1);
866
867        coset_dft_oop(
868            &src.as_view(),
869            &mut dst.as_view_mut(),
870            &twiddles,
871            0,
872            &|_, _| {},
873        );
874    }
875
876    #[test]
877    fn coset_dft_idft_roundtrip() {
878        let dft = Radix2DitParallel::<F>::default();
879        let shift = F::GENERATOR;
880        let mut rng = SmallRng::seed_from_u64(42);
881        let original = RowMajorMatrix::<F>::rand(&mut rng, 16, 3);
882
883        let evals = dft.coset_dft_batch(original.clone(), shift);
884        let recovered = dft.coset_idft_batch(evals.to_row_major_matrix(), shift);
885
886        assert_eq!(original, recovered);
887    }
888
889    #[test]
890    fn coset_dft_matches_default_trait() {
891        let dft = Radix2DitParallel::<F>::default();
892        let shift = F::two_adic_generator(4) * F::GENERATOR;
893        let mut rng = SmallRng::seed_from_u64(7);
894        let mat = RowMajorMatrix::<F>::rand(&mut rng, 16, 4);
895
896        let override_result = dft
897            .coset_dft_batch(mat.clone(), shift)
898            .to_row_major_matrix();
899
900        let mut shifted = mat;
901        crate::util::coset_shift_cols(&mut shifted, shift);
902        let default_result = dft.dft_batch(shifted).to_row_major_matrix();
903
904        assert_eq!(override_result, default_result);
905    }
906}