Skip to main content

hekate_math/fft/
additive.rs

1// SPDX-License-Identifier: Apache-2.0
2// This file is part of the hekate-math project.
3// Copyright (C) 2026 Andrei Kochergin <andrei@oumuamua.dev>
4// Copyright (C) 2026 Oumuamua Labs <info@oumuamua.dev>.
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Gao–Mateer additive FFT (Cantor basis).
19
20use super::{CantorBasis, CantorError};
21use crate::{BinaryFieldExtras, Flat, HardwareField, PackedFlat};
22use alloc::boxed::Box;
23use alloc::vec::Vec;
24use core::ops::{Add, AddAssign, Mul};
25#[cfg(feature = "parallel")]
26use rayon::prelude::*;
27
28const MAX_LEVELS: usize = 64;
29
30#[cfg(feature = "parallel")]
31const TILE_LOG: usize = 10;
32
33#[cfg(feature = "parallel")]
34const TILE: usize = 1 << TILE_LOG;
35
36#[cfg(feature = "parallel")]
37const PARALLEL_THRESHOLD_BYTES: usize = 1 << 20;
38
39/// Error returned by `AdditiveFft`.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum FftError {
43    /// The slice length `got` is not 2^log_n = `expected`.
44    BadLength {
45        /// 2^log_n, the length the transform needs.
46        expected: usize,
47
48        /// The length of the slice passed in.
49        got: usize,
50    },
51
52    /// `log_n` is outside `1..=max`, the sizes `F` supports.
53    BadLogN {
54        /// The `log_n` passed to `new`.
55        log_n: u32,
56
57        /// The largest `log_n` that `F` supports on this target.
58        max: u32,
59    },
60
61    /// Allocating the 2^(log_n-1)-entry twiddle table failed.
62    TwiddleAlloc {
63        /// The `log_n` passed to `new`.
64        log_n: u32,
65    },
66}
67
68impl core::fmt::Display for FftError {
69    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70        match self {
71            FftError::BadLength { expected, got } => {
72                write!(f, "AdditiveFft data length {got}, expected {expected}")
73            }
74            FftError::BadLogN { log_n, max } => {
75                write!(f, "AdditiveFft log_n {log_n}, expected 1..={max}")
76            }
77            FftError::TwiddleAlloc { log_n } => {
78                write!(
79                    f,
80                    "AdditiveFft log_n {log_n}: twiddle table allocation failed"
81                )
82            }
83        }
84    }
85}
86
87impl core::error::Error for FftError {}
88
89/// In-place additive FFT over a 2^log_n
90/// subspace of a binary tower field.
91///
92/// # Examples
93///
94/// ```
95/// use hekate_math::{AdditiveFft, Block16, Flat, HardwareField};
96///
97/// let log_n = 10;
98/// let fft = AdditiveFft::<Block16>::new(log_n)?;
99///
100/// let coeffs: Vec<Flat<Block16>> = (0..1u32 << log_n)
101///     .map(|i| Block16::from(i).to_hardware())
102///     .collect();
103///
104/// let mut data = coeffs.clone();
105/// fft.forward_scalar(&mut data)?;
106/// fft.inverse_scalar(&mut data)?;
107///
108/// assert_eq!(data, coeffs);
109/// # Ok::<(), hekate_math::FftError>(())
110/// ```
111pub struct AdditiveFft<F> {
112    log_n: u32,
113
114    // twiddles[t] = Σ_{bit i of t} β_{i+1},
115    // flat basis.
116    twiddles: Box<[Flat<F>]>,
117}
118
119impl<F: BinaryFieldExtras + HardwareField> AdditiveFft<F> {
120    /// Derives the Cantor basis (via solve_quadratic)
121    /// and the twiddle schedule for transform size 2^log_n.
122    ///
123    /// # Errors
124    /// `BadLogN` unless log_n is in 1..=min(F::BITS, usize::BITS - 1)
125    /// and F admits a Cantor basis of that size;
126    /// `TwiddleAlloc` if the 2^(log_n-1)-entry
127    /// twiddle table cannot be allocated.
128    pub fn new(log_n: u32) -> Result<Self, FftError> {
129        let max = F::BITS.min(usize::BITS as usize - 1) as u32;
130
131        if log_n == 0 || log_n > max {
132            return Err(FftError::BadLogN { log_n, max });
133        }
134
135        let basis = CantorBasis::<F>::new(log_n as usize).map_err(|e| match e {
136            CantorError::ChainEnds { at } => FftError::BadLogN {
137                log_n,
138                max: at as u32,
139            },
140            _ => FftError::BadLogN { log_n, max },
141        })?;
142
143        let half = 1usize << (log_n - 1);
144
145        let mut twiddles = Vec::new();
146        twiddles
147            .try_reserve_exact(half)
148            .map_err(|_| FftError::TwiddleAlloc { log_n })?;
149
150        twiddles.push(Flat::from_raw(F::ZERO));
151
152        for &beta in &basis.betas()[1..] {
153            for t in 0..twiddles.len() {
154                let tw = twiddles[t] + beta;
155                twiddles.push(tw);
156            }
157        }
158
159        Ok(Self {
160            log_n,
161            twiddles: twiddles.into_boxed_slice(),
162        })
163    }
164
165    /// Evaluates in place: novel-basis coefficients of f become
166    /// `data[j] = f(point(j))`, point as in [`CantorBasis::point`].
167    ///
168    /// # Errors
169    /// `BadLength` unless `data.len() == 2^log_n`.
170    pub fn forward_scalar(&self, data: &mut [Flat<F>]) -> Result<(), FftError> {
171        self.forward_coset_scalar(data, Flat::from_raw(F::ZERO))
172    }
173
174    /// Interpolates in place: the inverse of [`Self::forward_scalar`].
175    ///
176    /// # Errors
177    /// `BadLength` unless `data.len() == 2^log_n`.
178    pub fn inverse_scalar(&self, data: &mut [Flat<F>]) -> Result<(), FftError> {
179        self.inverse_coset_scalar(data, Flat::from_raw(F::ZERO))
180    }
181
182    /// Evaluates in place on the coset `offset + W_log_n`:
183    /// `data[j] = f(offset + point(j))`.
184    ///
185    /// # Errors
186    /// `BadLength` unless `data.len() == 2^log_n`.
187    pub fn forward_coset_scalar(
188        &self,
189        data: &mut [Flat<F>],
190        offset: Flat<F>,
191    ) -> Result<(), FftError> {
192        self.check_len(data.len())?;
193        self.fwd_levels(data, offset, fwd_butterflies);
194
195        Ok(())
196    }
197
198    /// Interpolates in place: the inverse of
199    /// [`Self::forward_coset_scalar`] at the same `offset`.
200    ///
201    /// # Errors
202    /// `BadLength` unless `data.len() == 2^log_n`.
203    pub fn inverse_coset_scalar(
204        &self,
205        data: &mut [Flat<F>],
206        offset: Flat<F>,
207    ) -> Result<(), FftError> {
208        self.check_len(data.len())?;
209        self.inv_levels(data, offset, inv_butterflies);
210
211        Ok(())
212    }
213
214    /// Runs [`Self::forward_scalar`] on each of the `F::WIDTH`
215    /// lanes: lane l of `data[j]` is element j of input l.
216    ///
217    /// # Errors
218    /// `BadLength` unless `data.len() == 2^log_n`.
219    pub fn forward(&self, data: &mut [PackedFlat<F>]) -> Result<(), FftError> {
220        self.forward_coset(data, Flat::from_raw(F::ZERO))
221    }
222
223    /// Runs [`Self::inverse_scalar`] on each of the `F::WIDTH` lanes.
224    ///
225    /// # Errors
226    /// `BadLength` unless `data.len() == 2^log_n`.
227    pub fn inverse(&self, data: &mut [PackedFlat<F>]) -> Result<(), FftError> {
228        self.inverse_coset(data, Flat::from_raw(F::ZERO))
229    }
230
231    /// Runs [`Self::forward_coset_scalar`] on each of the `F::WIDTH` lanes.
232    ///
233    /// # Errors
234    /// `BadLength` unless `data.len() == 2^log_n`.
235    pub fn forward_coset(
236        &self,
237        data: &mut [PackedFlat<F>],
238        offset: Flat<F>,
239    ) -> Result<(), FftError> {
240        self.check_len(data.len())?;
241        self.fwd_levels(data, offset, fwd_butterflies);
242
243        Ok(())
244    }
245
246    /// Runs [`Self::inverse_coset_scalar`] on each of the `F::WIDTH` lanes.
247    ///
248    /// # Errors
249    /// `BadLength` unless `data.len() == 2^log_n`.
250    pub fn inverse_coset(
251        &self,
252        data: &mut [PackedFlat<F>],
253        offset: Flat<F>,
254    ) -> Result<(), FftError> {
255        self.check_len(data.len())?;
256        self.inv_levels(data, offset, inv_butterflies);
257
258        Ok(())
259    }
260
261    fn check_len(&self, got: usize) -> Result<(), FftError> {
262        let expected = 1usize << self.log_n;
263        if got != expected {
264            return Err(FftError::BadLength { expected, got });
265        }
266
267        Ok(())
268    }
269
270    /// Every depth-ℓ node shares the coset σ^ℓ(offset),
271    /// σ(x) = x^2 + x; a level's butterflies tile into
272    /// contiguous 2s-blocks (s = 2^ℓ), block b pairing
273    /// (`blk[r]`, `blk[r+s]`) with twiddle `coset + twiddles[b]`.
274    fn fwd_levels<T, K>(&self, data: &mut [T], offset: Flat<F>, kernel: K)
275    where
276        T: Send,
277        K: Fn(&mut [T], &mut [T], Flat<F>) + Sync,
278    {
279        let levels = self.log_n as usize;
280        let chain = coset_chain(offset, levels);
281
282        #[cfg(feature = "parallel")]
283        if parallel_eligible(data) {
284            // One pool entry per transform: from outside the pool,
285            // each parallel pass would inject and block on its own.
286            rayon::scope(|_| fwd_parallel(data, &self.twiddles, &chain[..levels], &kernel));
287            return;
288        }
289
290        for l in (0..levels).rev() {
291            pass(data, &self.twiddles, chain[l], 1usize << l, &kernel);
292        }
293    }
294
295    /// No β^-1 anywhere:
296    /// paired points differ by β_0 = 1.
297    fn inv_levels<T, K>(&self, data: &mut [T], offset: Flat<F>, kernel: K)
298    where
299        T: Send,
300        K: Fn(&mut [T], &mut [T], Flat<F>) + Sync,
301    {
302        #[cfg(feature = "parallel")]
303        if parallel_eligible(data) {
304            let levels = self.log_n as usize;
305            let chain = coset_chain(offset, levels);
306
307            // One pool entry per transform: from outside the pool,
308            // each parallel pass would inject and block on its own.
309            rayon::scope(|_| inv_parallel(data, &self.twiddles, &chain[..levels], &kernel));
310
311            return;
312        }
313
314        let mut c = offset;
315        for l in 0..self.log_n as usize {
316            pass(data, &self.twiddles, c, 1usize << l, &kernel);
317            c = c * c + c;
318        }
319    }
320}
321
322fn coset_chain<F: HardwareField>(offset: Flat<F>, levels: usize) -> [Flat<F>; MAX_LEVELS] {
323    let mut chain = [Flat::from_raw(F::ZERO); MAX_LEVELS];
324    let mut c = offset;
325
326    for slot in chain.iter_mut().take(levels) {
327        *slot = c;
328        c = c * c + c;
329    }
330
331    chain
332}
333
334/// data.len() is 2^log_n (check_len), every level
335/// tiles exactly; kernel gets a block's aligned halves.
336fn pass<F, T, K>(data: &mut [T], twiddles: &[Flat<F>], coset: Flat<F>, s: usize, kernel: &K)
337where
338    F: HardwareField,
339    T: Send,
340    K: Fn(&mut [T], &mut [T], Flat<F>) + Sync,
341{
342    let tws = &twiddles[..data.len() / (2 * s)];
343
344    blocks_serial(data, tws, coset, s, kernel);
345}
346
347#[cfg(feature = "parallel")]
348fn parallel_eligible<T>(data: &[T]) -> bool {
349    size_of_val(data) >= PARALLEL_THRESHOLD_BYTES && data.len() >= TILE
350}
351
352#[cfg(feature = "parallel")]
353fn fwd_parallel<F, T, K>(data: &mut [T], twiddles: &[Flat<F>], chain: &[Flat<F>], kernel: &K)
354where
355    F: HardwareField,
356    T: Send,
357    K: Fn(&mut [T], &mut [T], Flat<F>) + Sync,
358{
359    for l in (TILE_LOG..chain.len()).rev() {
360        wide_pass(data, twiddles, chain[l], 1usize << l, kernel);
361    }
362
363    data.par_chunks_exact_mut(TILE)
364        .enumerate()
365        .for_each(|(i, tile)| {
366            for l in (0..TILE_LOG).rev() {
367                tile_pass(tile, i, twiddles, chain[l], l, kernel);
368            }
369        });
370}
371
372#[cfg(feature = "parallel")]
373fn inv_parallel<F, T, K>(data: &mut [T], twiddles: &[Flat<F>], chain: &[Flat<F>], kernel: &K)
374where
375    F: HardwareField,
376    T: Send,
377    K: Fn(&mut [T], &mut [T], Flat<F>) + Sync,
378{
379    data.par_chunks_exact_mut(TILE)
380        .enumerate()
381        .for_each(|(i, tile)| {
382            for (l, &coset) in chain[..TILE_LOG].iter().enumerate() {
383                tile_pass(tile, i, twiddles, coset, l, kernel);
384            }
385        });
386
387    for (l, &coset) in chain.iter().enumerate().skip(TILE_LOG) {
388        wide_pass(data, twiddles, coset, 1usize << l, kernel);
389    }
390}
391
392#[cfg(feature = "parallel")]
393fn tile_pass<F, T, K>(
394    tile: &mut [T],
395    i: usize,
396    twiddles: &[Flat<F>],
397    coset: Flat<F>,
398    l: usize,
399    kernel: &K,
400) where
401    F: HardwareField,
402    K: Fn(&mut [T], &mut [T], Flat<F>) + Sync,
403{
404    let per = TILE >> (l + 1);
405
406    blocks_serial(
407        tile,
408        &twiddles[i * per..(i + 1) * per],
409        coset,
410        1usize << l,
411        kernel,
412    );
413}
414
415#[cfg(feature = "parallel")]
416fn wide_pass<F, T, K>(data: &mut [T], twiddles: &[Flat<F>], coset: Flat<F>, s: usize, kernel: &K)
417where
418    F: HardwareField,
419    T: Send,
420    K: Fn(&mut [T], &mut [T], Flat<F>) + Sync,
421{
422    let tws = &twiddles[..data.len() / (2 * s)];
423
424    data.par_chunks_exact_mut(2 * s)
425        .zip(tws.par_iter())
426        .for_each(|(blk, &t)| {
427            let tw = coset + t;
428            let (lo, hi) = blk.split_at_mut(s);
429
430            lo.par_chunks_mut(TILE)
431                .zip(hi.par_chunks_mut(TILE))
432                .for_each(|(l, h)| kernel(l, h, tw));
433        });
434}
435
436fn blocks_serial<F, T, K>(data: &mut [T], tws: &[Flat<F>], coset: Flat<F>, s: usize, kernel: &K)
437where
438    F: HardwareField,
439    K: Fn(&mut [T], &mut [T], Flat<F>) + Sync,
440{
441    for (blk, &t) in data.chunks_exact_mut(2 * s).zip(tws) {
442        let (lo, hi) = blk.split_at_mut(s);
443        kernel(lo, hi, coset + t);
444    }
445}
446
447fn fwd_butterflies<F, T>(lo: &mut [T], hi: &mut [T], tw: Flat<F>)
448where
449    F: HardwareField,
450    T: Copy + Add<Output = T> + Mul<Flat<F>, Output = T>,
451{
452    for (p, q) in lo.iter_mut().zip(hi.iter_mut()) {
453        let qv = *q;
454        let v = *p + qv * tw;
455
456        *p = v;
457        *q = v + qv;
458    }
459}
460
461fn inv_butterflies<F, T>(lo: &mut [T], hi: &mut [T], tw: Flat<F>)
462where
463    F: HardwareField,
464    T: Copy + AddAssign + Add<Output = T> + Mul<Flat<F>, Output = T>,
465{
466    for (p, q) in lo.iter_mut().zip(hi.iter_mut()) {
467        let qv = *p + *q;
468        *p += qv * tw;
469        *q = qv;
470    }
471}