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 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:
In [1]:
import numpy as np
import nfft
In [2]:
# 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)
In [3]:
# direct Fourier transform
%time f_x_direct = nfft.ndft(x, f_k)
In [4]:
# fast Fourier transform
%time f_x_fast = nfft.nfft(x, f_k)
In [5]:
# Compare the results
np.allclose(f_x_direct, f_x_fast)
Out[5]:
In [6]:
# define observations
f = np.random.rand(len(x))
In [7]:
# direct adjoint transform
%time f_k_direct = nfft.ndft_adjoint(x, f, N)
In [8]:
# fast adjoint transform
%time f_k_fast = nfft.nfft_adjoint(x, f, N)
In [9]:
# Compare the results
np.allclose(f_k_direct, f_k_fast)
Out[9]: