The nfft package is a lightweight implementation of the non-equispaced
fast Fourier transform (NFFT), based on numpy and scipy and released under
an MIT license.
The NFFT is described in Using NFFT 3 – a software library for various nonequispaced fast Fourier transforms (pdf), which describes a C library that computes the NFFT and several variants and extensions.
The nfft package currently implements only a few of the algorithms described in the above paper, in particular:
The forward transform is given by
$$ f_j = \sum_{k=-N/2}^{N/2-1} \hat{f}_k e^{-2\pi i k x_j} $$for complex amplitudes $\{f_k\}$ specified at the range of integer wavenumbers $k$ in the range $-N/2 \le k < N$, evaluated at points $\{x_j\}$ satisfying $-1/2 \le x_j < 1/2$.
This can be computed via the nfft.ndft() and nfft.nfft() functions, respectively.
The adjoint transform is given by
$$ \hat{f}_k = \sum_{j=0}^{M-1} f_j e^{2\pi i k x_j} $$for complex values $\{f_j\}$ at points $\{x_j\}$ satisfying $-1/2 \le x_j < 1/2$, and for the range of integer wavenumbers $k$ in the range $-N/2 \le k < N$.
This can be computed via the nfft.ndft_adjoint() and nfft.nfft_adjoint() functions, respectively.
The computational complexity of both the forward and adjoint algorithm is approximately
$$ \mathcal{O}[N\log(N) + M\log(1 / \epsilon)] $$where $\epsilon$ is the desired tolerance of the result. In the current implementation, the memory requirements are approximately
$$ \mathcal{O}[N + M\log(1 / \epsilon)] $$Another option for computing the NFFT in Python is to use the pynfft package, which wraps the C library referenced in the above paper.
The advantage of pynfft is that it provides a more complete set of routines, including multi-dimensional NFFTs and various computing strategies.
The disadvantage is that pynfft is GPL-licensed, and has a more complicated set of dependencies.
Performance-wise, nfft and pynfft are comparable, with the nfft package discussed here being up to a factor of 2 faster in most cases of interest (see Benchmarks.ipynb for some benchmarks).
Here are some examples of computing the NFFT and its adjoint, using both a direct method and the fast method:
import numpy as np
import nfft
# define evaluation points
x = -0.5 + np.random.rand(1000)
# define Fourier coefficients
N = 10000
k = N // 2 + np.arange(N)
f_k = np.random.randn(N)
# direct Fourier transform
%time f_x_direct = nfft.ndft(x, f_k)
CPU times: user 401 ms, sys: 120 ms, total: 521 ms Wall time: 505 ms
# fast Fourier transform
%time f_x_fast = nfft.nfft(x, f_k)
CPU times: user 10 ms, sys: 2.11 ms, total: 12.1 ms Wall time: 7.02 ms
# Compare the results
np.allclose(f_x_direct, f_x_fast)
True
# define observations
f = np.random.rand(len(x))
# direct adjoint transform
%time f_k_direct = nfft.ndft_adjoint(x, f, N)
CPU times: user 473 ms, sys: 116 ms, total: 590 ms Wall time: 450 ms
# fast adjoint transform
%time f_k_fast = nfft.nfft_adjoint(x, f, N)
CPU times: user 7.75 ms, sys: 1.56 ms, total: 9.3 ms Wall time: 4.94 ms
# Compare the results
np.allclose(f_k_direct, f_k_fast)
True