zufft wraps ducc0, the
C++ library behind scipy.fft, to provide fast Fourier
transforms (FFTs) and discrete cosine and sine transforms (DCTs and
DSTs):
| zufft | stats | fftwtools | Computes |
|---|---|---|---|
fft_c2c() |
fft(), mvfft() |
fftw(), fftw_c2c(),
mvfftw_c2c(), fftw2d(),
fftw3d() |
the discrete Fourier transform (DFT) |
fft_r2c() |
— | fftw_r2c(), mvfftw_r2c(),
fftw_r2c_2d(), fftw_r2c_3d(), with
HermConj = 0 |
the DFT of real input, non-negative frequency terms only |
fft_c2r() |
— | fftw_c2r(), mvfftw_c2r(), with
HermConj = 0 |
the inverse of fft_r2c() |
fft_dct(), fft_dst() |
— | — | DCTs and DSTs of types 1 to 4 |
fft_stats(), mvfft_stats() |
fft(), mvfft() |
— | the same as fft() and mvfft(), as drop-in
replacements |
One difference matters when you switch: zufft scales inverse
transforms by 1/n, as SciPy does, while stats and fftwtools
don’t. Use norm = "none" to get their numbers.
vignette("migrating-from-fftwtools") gives the exact
arguments for each fftwtools function.
The conventions for axes, normalisation and the DCT and DST
definitions follow scipy.fft. Helpers such as
fft_next_len(), fft_freq() and
fft_shift() mirror SciPy’s next_fast_len(),
fftfreq() and fftshift().
# Install zufft from CRAN:
install.packages("zufft")
# Or the development version from GitHub:
# install.packages("pak")
pak::pak("pedrobtz/zufft")library(zufft)fft_c2c() computes the full spectrum, just like
stats::fft():
x <- c(1, 4, 2, 8, 5, 7)
fft_c2c(x)
#> [1] 27+0.000000e+00i -5+5.196152e+00i 0+4.440892e-16i -11+0.000000e+00i
#> [5] 0-4.440892e-16i -5-5.196152e+00iFor real input, the spectrum is Hermitian-symmetric: the
negative-frequency terms are the complex conjugates of the positive
ones. fft_r2c() returns only the n %/% 2 + 1
non-negative frequency terms, and fft_c2r() takes you back.
Give n to recover an odd length: by default
fft_c2r() assumes an even one, as SciPy’s
irfft() does:
X <- fft_r2c(x)
X
#> [1] 27+0.000000e+00i -5+5.196152e+00i 0+4.440892e-16i -11+0.000000e+00i
fft_c2r(X, n = length(x))
#> [1] 1 4 2 8 5 7Use axes to choose the dimensions to transform, and
workers to spread the work over several threads
(fft_workers() sets a default for the session). Here we
transform each column of a matrix on two threads:
m <- matrix(rnorm(4096 * 64), 4096)
spec <- fft_r2c(m, axes = 1, workers = 2)
dim(spec)
#> [1] 2049 64R stores arrays in column-major order, so the first dimension is
contiguous in memory. That’s why, with axes = NULL, the
real transforms halve the first dimension (to n %/% 2 + 1):
it’s fast, and it matches fftwtools. If you give axes
explicitly, the last axis listed is halved instead, as in SciPy. With
CRAN’s builds, the results are bit-identical whatever the number of
threads.
fft_dct() and fft_dst() compute DCTs and
DSTs with SciPy’s definitions. The default is type 2, and the inverse
takes you back to x:
fft_dct(x, norm = "ortho")
#> [1] 11.0227038 -4.6508890 -1.0000000 0.4082483 0.0000000 -3.8343924
fft_dct(fft_dct(x), inverse = TRUE)
#> [1] 1 4 2 8 5 7Lengths with only small prime factors transform fastest.
fft_next_len() returns the smallest fast length at or above
n, so you can zero-pad the input to it:
fft_next_len(1009)
#> [1] 1024You don’t need a native build for zufft to keep up with fftwtools. Here are a few cases on a Linux x86-64 runner, using one thread, against Ubuntu’s FFTW (built with SSE2 and AVX SIMD code):
| Case | fftwtools | zufft, CRAN build | ratio | zufft native, ratio |
|---|---|---|---|---|
| 1-D complex 1 048 576 | 27.02 ms | 20.64 ms | 0.76 | 0.59 |
| 1-D real 1 000 000 | 16.50 ms | 11.77 ms | 0.71 | 0.57 |
| columns real 1024 × 1024 | 5.97 ms | 2.55 ms | 0.43 | 0.34 |
| 2-D complex 2048² | 279.85 ms | 67.76 ms | 0.24 | 0.19 |
| 3-D complex 128³ | 44.80 ms | 34.10 ms | 0.76 | 0.83 |
| 1-D complex 100 000 | 1.44 ms | 1.81 ms | 1.26 | 0.83 |
A ratio below 1 means zufft is faster. On one thread, CRAN’s portable build is faster on 30 of the 33 benchmark cases. The three exceptions are single 1-D complex transforms of about 10⁵ to 10⁶ points, at worst 1.26 times slower. A native build is faster on all 33.
Most of the speed comes from planning: zufft caches its plans, while
fftwtools creates and destroys an FFTW plan on every call. Threads
(workers) add more, which fftwtools doesn’t offer. And
every length runs in O(n log n), even a prime one, unlike in
stats::fft().
bench/RESULTS.md
has all 33 cases, how they were measured, and the numbers for macOS on
Apple silicon.
A native build is only worth it on Linux x86-64. CRAN’s binaries are compiled to run on any CPU of their architecture, and on x86-64 that means SSE2, whose SIMD registers hold two doubles. If your CPU has AVX2 or AVX-512, a build for your own machine runs faster. It takes two steps in R:
Set the environment variable ZUFFT_CXXFLAGS with
Sys.setenv(). zufft adds it to its compiler flags, and no
other package sees it.
Install from source with
install.packages("zufft", type = "source"). A prebuilt
binary is already compiled and ignores the variable.
Sys.setenv(ZUFFT_CXXFLAGS = "-march=native")
install.packages("zufft", type = "source")You can check the result with zufft_info(). The SIMD
width, in doubles, is 4 with AVX2, 8 with AVX-512, and 2 for a portable
build:
zufft::zufft_info()$simd_widthHow much does it help? On the Linux x86-64 runners of
bench/RESULTS.md, where this gives AVX2, the 16384 × 64
complex column case goes from 0.96 to 0.60 of fftwtools’ time. A complex
transform of prime length 10 007 goes from 0.81 to 0.49.
Other platforms gain little or nothing:
On Apple silicon the SIMD width stays at two doubles (NEON), so a native build gains little.
Windows builds ignore ZUFFT_CXXFLAGS and stay
portable, because code built for AVX by the GCC in Rtools crashed in our
tests.
Keep in mind that a native build with GCC, on a CPU with fused
multiply-add (FMA), can give results that differ in the last bit between
thread counts. zufft::zufft_info()$fp_contract reports
"fast" when that applies. CRAN’s builds report
"off".
zufft is MIT-licensed. Its bundled ducc0 files are the “BSD-3-Clause
OR GPL-2.0-or-later” subset that SciPy vendors, used under BSD-3-Clause.
inst/COPYRIGHTS lists every copyright holder. No GPL-only
ducc0 code is included.
If you encounter a clear bug, please file an issue with a minimal reproducible example on GitHub.