Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Nonanti/mathcore/llms.txt

Use this file to discover all available pages before exploring further.

MathCore’s transforms module brings signal processing directly into your symbolic math pipeline. At its core is the FFT struct, which provides a fast Fourier transform, a reference-quality DFT, an inverse FFT, a 2D FFT for image processing, and FFT-accelerated convolution. When the fft feature is enabled, the forward and inverse transforms delegate to rustfft — one of the fastest FFT libraries available in any language. Without the feature, a manual Cooley-Tukey implementation handles power-of-two lengths and falls back to the O(n²) DFT for other sizes.

Import

use mathcore::transforms::FFT;
use num_complex::Complex64;

Feature Flag

The fft feature gates the rustfft backend. Add it to your Cargo.toml to get the best performance:
[dependencies]
mathcore = { version = "0.3.1", features = ["fft"] }
Without the fft feature, FFT::fft uses a built-in Cooley-Tukey recursive implementation for power-of-two lengths, and falls back to FFT::dft (O(n²)) for other lengths. Both paths produce identical numeric results — the feature only affects speed.

FFT::fft

FFT::fft(input: &[Complex64]) -> Vec<Complex64>

Computes the forward Discrete Fourier Transform of a complex-valued signal. With the fft feature enabled, this uses rustfft’s highly optimised mixed-radix Cooley-Tukey algorithm. Without the feature, it falls back to the manual recursive implementation.
use mathcore::transforms::FFT;
use num_complex::Complex64;

let signal: Vec<Complex64> = vec![
    Complex64::new(1.0, 0.0),
    Complex64::new(1.0, 0.0),
    Complex64::new(0.0, 0.0),
    Complex64::new(0.0, 0.0),
];

let spectrum = FFT::fft(&signal);
println!("Frequency components: {:?}", spectrum);
// spectrum[0] is the DC component (sum of all samples = 2)
The output has the same length as the input. The k-th element is:
X[k] = Σ_{j=0}^{N-1} x[j] · e^{-2πi·jk/N}
For purely real signals, construct a Vec<Complex64> with imaginary parts set to 0.0. The power spectrum helper FFT::power_spectrum does this automatically from &[f64].

FFT::dft

FFT::dft(input: &[Complex64]) -> Vec<Complex64>

The reference O(n²) Discrete Fourier Transform. Always available regardless of feature flags. Computes the same transform as fft but without any algorithmic speedup — suitable for small inputs, verification, or inputs whose length is not a power of two and rustfft is not enabled.
use mathcore::transforms::FFT;
use num_complex::Complex64;

let signal: Vec<Complex64> = vec![
    Complex64::new(1.0, 0.0),
    Complex64::new(0.0, 0.0),
    Complex64::new(-1.0, 0.0),
    Complex64::new(0.0, 0.0),
];

let spectrum = FFT::dft(&signal);
// Verify result matches FFT
let fft_spectrum = FFT::fft(&signal);
for (d, f) in spectrum.iter().zip(fft_spectrum.iter()) {
    assert!((d - f).norm() < 1e-10);
}

FFT::ifft

FFT::ifft(input: &[Complex64]) -> Vec<Complex64>

Computes the inverse FFT using the conjugate trick: conjugate the input, apply the forward FFT, conjugate and scale the output by 1/N. The result satisfies ifft(fft(x)) ≈ x up to floating-point rounding.
use mathcore::transforms::FFT;
use num_complex::Complex64;

let signal = vec![
    Complex64::new(1.0, 0.0),
    Complex64::new(2.0, 0.0),
    Complex64::new(3.0, 0.0),
    Complex64::new(4.0, 0.0),
];

let spectrum = FFT::fft(&signal);
let recovered = FFT::ifft(&spectrum);

for (original, recovered) in signal.iter().zip(recovered.iter()) {
    println!(
        "original={:.2}, recovered={:.2}",
        original.re, recovered.re
    );
}

FFT::power_spectrum

FFT::power_spectrum(signal: &[f64]) -> Vec<f64>

Convenience wrapper: converts a real-valued &[f64] to complex, runs the FFT, and returns |X[k]|² for each frequency bin. Useful for spectral analysis of real signals without manual complex conversion.
use mathcore::transforms::FFT;

let real_signal: Vec<f64> = (0..64)
    .map(|i| (2.0 * std::f64::consts::PI * 5.0 * i as f64 / 64.0).sin())
    .collect();

let power = FFT::power_spectrum(&real_signal);

let peak_bin = power
    .iter()
    .enumerate()
    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
    .map(|(i, _)| i)
    .unwrap();

println!("Peak frequency bin: {}", peak_bin); // 5 (or its mirror at 59)

FFT::fft2d

FFT::fft2d(input: &[Vec<Complex64>]) -> Vec<Vec<Complex64>>

2D FFT for image and matrix analysis: applies the 1D FFT across all rows first, then across all columns. The output has the same dimensions as the input.
use mathcore::transforms::FFT;
use num_complex::Complex64;

// 4×4 test image
let image: Vec<Vec<Complex64>> = vec![
    vec![Complex64::new(1.0, 0.0); 4],
    vec![Complex64::new(0.0, 0.0); 4],
    vec![Complex64::new(1.0, 0.0); 4],
    vec![Complex64::new(0.0, 0.0); 4],
];

let spectrum_2d = FFT::fft2d(&image);
println!("DC component: {:?}", spectrum_2d[0][0]);

FFT::convolve

FFT::convolve(a: &[f64], b: &[f64]) -> Vec<f64>

Computes the linear convolution of two real-valued signals using the FFT overlap-add approach: both signals are zero-padded to the next power of two, transformed, pointwise-multiplied in the frequency domain, then inverse-transformed. This is O(n log n) compared to O(n²) for direct convolution.
use mathcore::transforms::FFT;

let signal  = vec![1.0, 2.0, 3.0, 4.0];
let kernel  = vec![0.5, 0.5]; // simple moving average kernel

let result = FFT::convolve(&signal, &kernel);
// result has length signal.len() + kernel.len() - 1 = 5
println!("Convolution: {:?}", result);
FFT::convolve is faster than a direct O(n²) loop for inputs longer than ~64 elements. For very short sequences the overhead of the FFT may dominate — benchmark both if performance is critical.

Input Length and Performance

Power-of-two lengths (2, 4, 8, 16, 32, 64, …) are the most efficient for the Cooley-Tukey algorithm. What happens for other lengths depends on which backend is active:
ConditionBehaviour
fft feature enabled, any lengthrustfft’s mixed-radix algorithm handles all lengths efficiently
fft feature disabled, length is a power of twoBuilt-in Cooley-Tukey recursive FFT — O(n log n)
fft feature disabled, length is not a power of twoFalls back to FFT::dft — O(n²)
Without the fft feature, passing a non-power-of-two input to FFT::fft silently falls back to the O(n²) DFT. This is correct but can be slow for large inputs. Either enable the fft feature or zero-pad your input to the next power of two using Vec::resize.
Zero-padding example:
use mathcore::transforms::FFT;
use num_complex::Complex64;

let mut signal: Vec<Complex64> = vec![
    Complex64::new(1.0, 0.0),
    Complex64::new(2.0, 0.0),
    Complex64::new(3.0, 0.0),
];

// Pad to next power of two (4)
signal.resize(4, Complex64::new(0.0, 0.0));

let spectrum = FFT::fft(&signal);

Full Example

use mathcore::transforms::FFT;
use num_complex::Complex64;
use std::f64::consts::PI;

fn main() {
    // Synthesise a signal: sum of two sine waves at 3 Hz and 7 Hz
    let n = 64;
    let signal: Vec<Complex64> = (0..n)
        .map(|i| {
            let t = i as f64 / n as f64;
            let v = (2.0 * PI * 3.0 * t).sin() + 0.5 * (2.0 * PI * 7.0 * t).sin();
            Complex64::new(v, 0.0)
        })
        .collect();

    // Forward FFT
    let spectrum = FFT::fft(&signal);

    // Power spectrum
    let power: Vec<f64> = spectrum.iter().map(|c| c.norm_sqr()).collect();

    // Find dominant frequencies
    println!("Top frequency bins by power:");
    let mut indexed: Vec<(usize, f64)> = power.iter().cloned().enumerate().collect();
    indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
    for (bin, p) in indexed.iter().take(4) {
        println!("  bin {:2} — power {:.2}", bin, p);
    }
    // Expected peaks near bins 3, 7 and their mirrors at 57, 61

    // Round-trip check
    let recovered = FFT::ifft(&spectrum);
    let max_error = signal
        .iter()
        .zip(recovered.iter())
        .map(|(a, b)| (a - b).norm())
        .fold(0.0_f64, f64::max);
    println!("Max round-trip error: {:.2e}", max_error); // ~1e-14
}

Enabling the fft Feature

Add the following to your Cargo.toml:
[dependencies]
mathcore = { version = "0.3.1", features = ["fft"] }
num-complex = "0.4"
With this configuration, FFT::fft and FFT::ifft use rustfft’s planner internally. The planner caches twiddle factors and FFT plans, so repeated transforms of the same length are especially fast.

Build docs developers (and LLMs) love