Skip to content

Complex-to-complex FFT implementation

This page examines in detail how the code performs the 2D complex-to-complex FFT, including padding, the 1D FFT core, and the inverse with trimming.

Padding to power-of-two dimensions

Before applying the FFT, the input matrix is padded so that both dimensions are powers of two. In FFT.cpp, the helper next_power_of_two and padding loop are used:

FFT.cpp
std::size_t next_power_of_two(std::size_t n) {
    std::size_t p = 1;
    while (p < n) p <<= 1;
    return p;
}
inside FFT::fft2d(...)
std::size_t R = input.size(); // number of rows
std::size_t C = R ? input[0].size() : 0; // number of columns
std::size_t M = next_power_of_two(R);
std::size_t N = next_power_of_two(C);

// pad to M×N
std::vector<std::vector<std::complex<double>>> a(
    M, std::vector<std::complex<double>>(N, {0,0}));
for (std::size_t i = 0; i < R; ++i)
    for (std::size_t j = 0; j < C; ++j)
        a[i][j] = input[i][j];

This ensures optimal FFT speed and simplifies the Cooley–Tukey recursion.

1D FFT core

The 1D FFT uses an in-place Cooley–Tukey algorithm with bit reversal and iterative butterflies:

FFT.cpp
void fft(std::vector<std::complex<double>>& a, bool invert) {
    const std::size_t n = a.size();
    if (n < 2) return;

    // bit‑reversal permute
    for (std::size_t i = 1, j = 0; i < n; ++i) {
        std::size_t bit = n >> 1;
        for (; j & bit; bit >>= 1) j ^= bit;
        j |= bit;
        if (i < j) std::swap(a[i], a[j]);
    }

    // Cooley–Tukey butterflies
    for (std::size_t len = 2; len <= n; len <<= 1) {
        double ang = 2 * M_PI / double(len) * (invert ? 1 : -1);
        std::complex<double> wlen(std::cos(ang), std::sin(ang));
        for (std::size_t i = 0; i < n; i += len) {
            std::complex<double> w{1,0};
            for (std::size_t k = 0; k < len/2; ++k) {
                auto u = a[i + k];
                auto v = a[i + k + len/2] * w;
                a[i + k]         = u + v;
                a[i + k + len/2] = u - v;
                w *= wlen;
            }
        }
    }

    // scale for inverse
    if (invert) {
        for (auto& x : a) x /= double(n);
    }
}

This routine underlies both forward and inverse transforms.

2D FFT implementation

The 2D FFT applies fft row-wise, transposes, and applies fft again row-wise (on columns):

FFT.cpp
std::vector<std::vector<std::complex<double>>>
fft2d(const std::vector<std::vector<std::complex<double>>>& input,
      bool invert)
{
    std::size_t R = input.size();
    std::size_t C = R ? input[0].size() : 0;
    std::size_t M = next_power_of_two(R);
    std::size_t N = next_power_of_two(C);

    // pad to M×N
    std::vector<std::vector<std::complex<double>>> a(
        M, std::vector<std::complex<double>>(N, {0,0}));
    for (std::size_t i = 0; i < R; ++i)
        for (std::size_t j = 0; j < C; ++j)
            a[i][j] = input[i][j];

    // FFT rows
    for (std::size_t i = 0; i < M; ++i)
        a[i] = fft1d(a[i], invert);

    // FFT cols
    std::vector<std::complex<double>> tmp(M);
    for (std::size_t j = 0; j < N; ++j) {
        for (std::size_t i = 0; i < M; ++i) tmp[i] = a[i][j];
        tmp = fft1d(tmp, invert);
        for (std::size_t i = 0; i < M; ++i) a[i][j] = tmp[i];
    }

    return a;
}

This decomposes the 2D problem into calls to the 1D FFT.

Complex-to-complex FFT

Rather than returning a bare matrix, fft2d_c2c_trim returns a struct that encapsulates both the frequency-domain data and the original dimensions. In FFT.hpp:

FFT.hpp
struct FFT2dC2CTrimmed {
    std::vector<std::vector<std::complex<double>>> freq;
    std::size_t orig_rows, orig_cols;
    std::size_t pad_rows, pad_cols;
};

FFT2dC2CTrimmed
fft2d_c2c_trim(const std::vector<std::vector<std::complex<double>>>& input);

In FFT.cpp, the implementation captures the full padded transform and records the original size:

FFT.cpp
FFT2dC2CTrimmed
fft2d_c2c_trim(const std::vector<std::vector<std::complex<double>>>& input)
{
    const std::size_t R = input.size();
    const std::size_t C = R ? input[0].size() : 0;
    const std::size_t M = next_power_of_two(R);
    const std::size_t N = next_power_of_two(C);

    // 1) compute the full M×N forward transform
    auto full = fft2d(input, /*invert=*/false);

    // 2) package it all up
    return FFT2dC2CTrimmed{ std::move(full), R, C, M, N };
}

The padded transform full has dimensions that are powers of two, which may exceed the original. By retaining orig_rows and orig_cols, the inverse transform can know exactly how to crop the result back to the user’s intended size.

Inverse complex-to-complex FFT

To return to original dimensions, ifft2d_c2c_trim accepts the FFT2dC2CTrimmed struct and uses its metadata:

FFT.cpp
std::vector<std::vector<std::complex<double>>>
ifft2d_c2c_trim(const FFT2dC2CTrimmed& t)
{
    // 1) full padded inverse
    auto fullRec = fft2d(t.freq, /*invert=*/true);

    // 2) crop back to original R×C
    std::vector<std::vector<std::complex<double>>> out(
        t.orig_rows,
        std::vector<std::complex<double>>(t.orig_cols)
    );
    for (std::size_t i = 0; i < t.orig_rows; ++i)
        for (std::size_t j = 0; j < t.orig_cols; ++j)
            out[i][j] = fullRec[i][j];

    return out;
}

By combining the padded spectrum and the stored original sizes, this function ensures the round-trip FFT returns exactly to the user’s matrix dimensions.