--- title: "Migrating from fftwtools" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Migrating from fftwtools} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") has_fftwtools <- requireNamespace("fftwtools", quietly = TRUE) ``` zufft computes the same transforms as fftwtools without the FFTW system library, and under a permissive licence. This vignette maps every fftwtools call to its zufft equivalent. Where fftwtools is installed, each example also checks that both give the same values. ```{r} library(zufft) ``` ## Two conventions to know **Scaling.** fftwtools, like `stats::fft()`, never scales: an inverse after a forward transform multiplies by the length. zufft follows `scipy.fft` and divides the inverse by the length by default (`norm = "backward"`). Use `norm = "none"` to get fftwtools' numbers exactly. ```{r} x <- c(1, 4, 2, 8, 5, 7) fft_c2c(fft_c2c(x), inverse = TRUE) # x again fft_c2c(fft_c2c(x), inverse = TRUE, norm = "none") # 6 * x, as fftwtools ``` **Half spectra.** fftwtools' `HermConj = 1` (its default) returns the full spectrum of real input, and `HermConj = 0` the non-redundant half. In zufft that is the choice between `fft_c2c()` and `fft_r2c()`. ```{r} fft_c2c(x) # full spectrum, as fftw(x) and fftw_r2c(x) fft_r2c(x) # first n %/% 2 + 1 values, as fftw_r2c(x, HermConj = 0) ``` To go back from a half spectrum you need the original length, because an even and an odd length give the same half size. Both packages take it as `n`: ```{r} X <- fft_r2c(1:7) fft_c2r(X, n = 7, norm = "none") # as fftw_c2r(X, HermConj = 0, n = 7) ``` ## One-dimensional transforms | fftwtools | zufft | | --- | --- | | `fftw(x)` | `fft_c2c(x)` | | `fftw(x, inverse = 1)` | `fft_c2c(x, inverse = TRUE, norm = "none")` | | `fftw(x, HermConj = 0)` (real `x`) | `fft_r2c(x)` | | `fftw(X, inverse = 1, HermConj = 0, n = n)` | `fft_c2r(X, n = n, norm = "none")` | | `fftw_r2c(x)` | `fft_c2c(x)` | | `fftw_r2c(x, HermConj = 0)` | `fft_r2c(x)` | | `fftw_c2r(X, HermConj = 0, n = n)` | `fft_c2r(X, n = n, norm = "none")` | | `fftw_c2r(X)` (full spectrum) | `Re(fft_c2c(X, inverse = TRUE, norm = "none"))` | | `fftw_c2c(x, inverse = i)` | `fft_c2c(x, inverse = i, norm = "none")` | `inverse = 0` and `inverse = 1` work in zufft too. ```{r, eval = has_fftwtools} z <- complex(real = rnorm(8), imaginary = rnorm(8)) all.equal(fft_c2c(z, inverse = 1, norm = "none"), fftwtools::fftw_c2c(z, inverse = 1)) all.equal(fft_r2c(x), fftwtools::fftw_r2c(x, HermConj = 0)) all.equal(fft_c2r(X, n = 7, norm = "none"), fftwtools::fftw_c2r(X, HermConj = 0, n = 7)) ``` ## Columns of a matrix fftwtools' `mvfftw*` functions transform each column. In zufft that is `axes = 1`. | fftwtools | zufft | | --- | --- | | `mvfftw(m)`, `mvfftw_c2c(m)` | `fft_c2c(m, axes = 1)` | | `mvfftw_r2c(m, HermConj = 0)` | `fft_r2c(m, axes = 1)` | | `mvfftw_c2r(M, HermConj = 0, n = n)` | `fft_c2r(M, n = n, axes = 1, norm = "none")` | | `mvfftw(..., fftplanopt = 1)` | nothing to do: zufft caches its plans | ```{r, eval = has_fftwtools} m <- matrix(rnorm(24), 6) all.equal(fft_c2c(m, axes = 1), fftwtools::mvfftw(m)) M <- fft_r2c(m, axes = 1) all.equal(fft_c2r(M, n = 6, axes = 1, norm = "none"), fftwtools::mvfftw_c2r(M, HermConj = 0, n = 6)) ``` ## Two, three and more dimensions | fftwtools | zufft | | --- | --- | | `fftw2d(x)`, `fftw_c2c_2d(x)`, `fftw3d(x)`, `fftw_c2c_3d(x)`, `fftw_c2c_xd(x)` | `fft_c2c(x)` | | `fftw_r2c_2d(x, HermConj = 0)` | `fft_r2c(x)` | | `fftw_r2c_3d(x, HermConj = 0)` | `fft_r2c(x)` | FFTW stores arrays row by row, so fftwtools' real 2-D and 3-D transforms halve R's *first* dimension. zufft's real transforms do the same by default: in R the first dimension is the contiguous one, and halving it is the fast choice. ```{r, eval = has_fftwtools} x2 <- matrix(rnorm(35), 5, 7) all.equal(fft_r2c(x2), fftwtools::fftw_r2c_2d(x2, HermConj = 0)) all.equal(fft_c2c(x2), fftwtools::fftw2d(x2)) ``` ## What you gain - **No system library.** zufft compiles its bundled ducc0 code, so `install.packages("zufft")` is enough, everywhere. - **No GPL.** zufft is MIT-licensed and its bundled code BSD-3, so a package that imports it keeps its own licence. - **Speed.** On the benchmark grid in the package's `bench/RESULTS.md`, zufft is faster than fftwtools on most cases and within 1.5 times everywhere, because it caches its plans while fftwtools plans on every call. Multi-dimensional complex transforms gain the most. - **Threads.** `workers = 2` (or `fft_workers(2)`) splits a transform over threads; fftwtools has none. - **More transforms.** `fft_dct()` and `fft_dst()` (types 1 to 4), any subset of axes, cropping and zero-padding with `n`, and `fft_next_len()` for fast lengths. - **Untouched inputs.** `fft_c2r()` never modifies its input, while FFTW's complex-to-real transform destroys it.