minimal-zoom-fft
The Fourier transform of a signal on any frequency band, at any sampling, in PyTorch: a zoomed FFT, computed by the chirp Z-transform. One implementation file, one dependency.
torch.fft.fft evaluates the spectrum of an N-sample signal at N equispaced frequencies covering the whole circle [0, 2π). Often you only care about a narrow band, or you want it sampled more finely than 2π/N, or you want M ≠ N output samples. Bluestein's algorithm does exactly that with three FFTs, at O((N+M) log(N+M)) cost, on any device, with autograd.
Typical uses: pupil-to-PSF computation in microscopy, Fourier ptychography, diffraction onto a rescaled output grid, fine spectral analysis of a narrow band.
Install
pip install minimal-zoom-fft
pip install git+https://github.com/jon-dong/minimal-zoom-fft # the development version
pip install -e ".[test]" && pytest # from a checkout
Requires Python ≥ 3.10 and PyTorch ≥ 2.0.
Quick start
import torch
from minimal_zoom_fft import zoom_fft, zoom_ifft, zoom_freq, czt
x = torch.randn(256, dtype=torch.complex64)
# Spectrum on the band [-0.2, 0.2] rad/sample, sampled at 1024 points.
X = zoom_fft(x, n_out=1024, k_start=-0.2, k_end=0.2)
w = zoom_freq(1024, -0.2, 0.2) # the 1024 frequencies, for plotting
# The adjoint (and, on a full band, the inverse).
x_back = zoom_ifft(zoom_fft(x), n_out=256) # x again, to round-off
# Images: transform the last two axes, batch axes are untouched.
pupil = torch.randn(8, 64, 64, dtype=torch.complex64)
psf = zoom_fft(pupil, n_out=(256, 256), k_start=-0.5, k_end=0.5,
dim=(-2, -1), center=True)
# The raw chirp Z-transform, if you need it.
X = czt(x, n_out=100, w_phase=-0.01, a_phase=0.3)
API
Along one transformed axis with N input and M output samples:
| Function | Computes |
|---|---|
czt(x, n_out, w_phase, a_phase, dim) |
X[k] = Σₙ x[n] e^{-i a n} e^{i w n k}, k = 0..M-1 |
zoom_fft(x, n_out, k_start, k_end, dim, norm, center, include_end) |
X[m] = Σₙ x[n] e^{-i ωₘ (n - c)} |
zoom_ifft(X, n_out, k_start, k_end, dim, norm, center, include_end) |
x[n] = Σₘ X[m] e^{+i ωₘ (n - c)} |
zoom_freq(n, k_start, k_end, include_end) |
the band samples ωₘ = k_start + step · m |
ωₘ samples [k_start, k_end] in radians per sample; c is the origin index set by center. Every function:
- acts on the last axis by default;
dimtakes an int or a tuple (the transform is separable, so a 2-D transform isdim=(-2, -1)); - leaves all other axes alone (batch axes);
- accepts a scalar or one value per transformed axis for
n_out,k_start,k_end,center,w_phase,a_phase; - keeps the input's precision (complex64 in, complex64 out; real input is promoted to complex);
- runs on CPU, CUDA or MPS and is differentiable with respect to
x.
czt_plain is the readable twin of czt, the same transform with Bluestein's algorithm written out one step per line: multiply by the chirp, convolve with the conjugate chirp by a zero-padded FFT, multiply by the chirp again. It comes first in core.py and the tests pin it to czt. What differs is where the cast to the working precision happens: czt_plain evaluates each phasor in float64 and casts the result, while czt reduces the phase modulo 2π and then takes the cosine and the sine in the working precision, on the device, which is cheaper and, thanks to the reduction, as accurate. czt also skips the trivial factors and pads the convolution to a power of two. Read the plain one, call the fast one.
Conventions
The defaults reproduce torch.fft to round-off: zoom_fft(x) is torch.fft.fft(x, norm="ortho"), zoom_ifft the inverse, and czt(x) the unnormalised DFT.
The band is sampled like FFT bins when include_end=False, the default: step = (k_end - k_start) / M, right end excluded, so [0, 2π) with M = N is the DFT. The fftshift-ed spectrum is the full band that starts at k_start = -2π (N // 2) / N: that is [-π, π) for even N, and half a bin above it for odd N. include_end=True puts the last sample on k_end (step = span / (M - 1)).
Normalisation follows torch.fft: "backward" leaves the forward transform unnormalised and divides the inverse by the number of band samples; "forward" does the opposite; "ortho" splits the factor. zoom_ifft is the conjugate transpose of zoom_fft over the same band under mirrored norms ("backward" ↔ "forward", "ortho" ↔ "ortho"), for any band. On a full band, and with the same norm in both directions, it is also its inverse, to round-off.
The origin of the transform is set by center. With False, sample 0 is the origin, as in torch.fft. With True, the origin is at index N // 2, the torch.fft.fftshift convention, so on the full band zoom_fft(x, center=True) is fft(ifftshift(x), norm="ortho") and zoom_ifft(X, center=True) is fftshift(ifft(X, norm="ortho")), to round-off. A float pins the origin at any index, possibly fractional: (N - 1) / 2 is the geometric centre of the grid, which for even N lies between two samples (the convention of psf_generator). The correction is a phase ramp, so it is exact for any band and never wraps indices. In zoom_ifft, center refers to the output grid.
For accuracy, the Bluestein chirps e^{i w k² / 2} are evaluated in float64, reduced modulo 2π, and only then cast to the working precision, so a float32 transform of thousands of samples stays at float32 round-off instead of losing digits to large phases.
Relation to other implementations
scipy.signal.czt/scipy.signal.zoom_fft: same algorithm. SciPy's contour is a general spiral (complexw,a); here it is restricted to the unit circle (phasesw_phase,a_phase), which is the case needed for zooming and is numerically stable. This version is N-D, batched, GPU-capable and differentiable.torch.fft: recovered by the defaults, to round-off, see above.- Origin: extracted from psf_generator (
custom_fft2,custom_ifft2) and theciellinear-operator library. Thecielnames map ascustom_fft2(x, shape_out, k_start, k_end, norm, fftshift_input, include_end)→zoom_fft(x, shape_out, k_start, k_end, dim=(-2, -1), norm, center, include_end), andcustom_ifft2→zoom_ifftin the same way, withfftshift_input=Truebecomingcenter=True.psf_generatorfollows another convention: itscustom_ifft2takes samples on the grid, returns the band and uses the kernele^{+i ωₘ (n - c)}, socustom_ifft2(x, shape_out, k_start, k_end, norm, fftshift_input, include_end)iszoom_fft(x, shape_out, -k_start, -k_end, dim=(-2, -1), norm, center, include_end)with the norm mirrored ("backward"↔"forward") andfftshift_input=Truebecomingcenter=(N - 1) / 2(checked on bands symmetric about zero, the onespsf_generatoruses).
Tutorials
Three notebooks in notebooks/, runnable after pip install -e ".[notebooks]":
- Why a zoomed FFT: the PSF of a disk pupil is an Airy disk that a plain FFT undersamples; zero-padding fixes that at a cost
zoom_fftavoids. - What it computes: one figure and one block of checks per topic: the definition, why zero-padding gives the same samples, Bluestein's algorithm, the band, origin and normalisation conventions, the adjoint, images and autograd.
- Benchmark: the arrays each route allocates, speed against padded FFTs and SciPy, CPU against MPS, precision against float64 summation, memory and edge cases.
Tests
pip install -e ".[test]"
pytest
The tests pin every convention above against explicit float64 summation and against torch.fft.
License
MIT
Manifest
- Purpose: the Fourier transform of a signal on any frequency band, at any sampling, in PyTorch: a zoomed FFT, computed by the chirp Z-transform. One implementation file, one dependency.
- Dependencies:
torch. - Size: about 350 lines of implementation in one module, 5 public functions; about 690 lines of tests; 3 tutorial notebooks.
- Origin: the
psf_generatorlibrary and thecielcomputational-imaging library, EPFL; Bluestein's chirp Z-transform. Used byminimal-linop'sfftextra. - Provenance: written with Claude (Anthropic) from a brief; read and checked in full by Jonathan Dong.
- Version: 0.1.0, MIT.
Metadata
Release files for minimal-zoom-fft 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| minimal_zoom_fft-0.1.0.tar.gz | 20.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| minimal_zoom_fft-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.0 kB
Release files / minimal_zoom_fft-0.1.0.tar.gz
| Download URL | minimal_zoom_fft-0.1.0.tar.gz |
|---|---|
| Size | 20.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
41d31c06b75bb6ba24cd52e28e5b86d1a9dad37bf6b490e2ad54c00ca4e9028d
|
|
BLAKE2b-256 checksum How to use checksums |
ef751195db60cc281fa5fe8e3be8cde0b18e5a2369e733d8cd2256308a8d1dde
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / minimal_zoom_fft-0.1.0-py3-none-any.whl
| Download URL | minimal_zoom_fft-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f033f88252012753a39a045d72747954e7078e8f45ebf15970bd8f0ebface85a
|
|
BLAKE2b-256 checksum How to use checksums |
5114abd75d0e66e5236259cd00fe73fa68dbcbb645effcd403357d6aaa7709dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency log