FFT¶
Each FFT object owns one plan and is reused across calls. Multiple
instances of different sizes or signs coexist with no global state.
1-D FFT¶
from doppler.spectral import FFT
import numpy as np
rng = np.random.default_rng(0)
x32 = (rng.standard_normal(1024) + 1j * rng.standard_normal(1024)).astype(np.complex64)
f = FFT(1024)
X32 = f.execute_cf32(x32) # complex64 in → complex64 out
assert X32.dtype == np.complex64
# Parseval: Σ|x|² == Σ|X|² / N. The error is float32 rounding (~5e-4 on
# this input), so bound it rather than comparing exactly.
err = abs(np.sum(np.abs(x32)**2) - np.sum(np.abs(X32)**2) / 1024)
assert err < 1e-2
CF64 input auto-selects the double-precision path:
x64 = rng.standard_normal(1024) + 1j * rng.standard_normal(1024)
X64 = f.execute_cf64(x64) # complex128 in → complex128 out
assert X64.dtype == np.complex128
Reusing a plan¶
Plan once, transform many times — no reallocation on repeated calls:
f = FFT(1024)
for _ in range(1000):
out = f.execute_cf32(x32) # out-of-place, returns new array
out = f.execute_inplace_cf32(x32) # uses internal buffer, same result
2-D FFT¶
from doppler.spectral import FFT2D
import numpy as np
rng = np.random.default_rng(0)
x = (rng.standard_normal((64, 64)) + 1j * rng.standard_normal((64, 64))).astype(np.complex64)
f2 = FFT2D(64, 64)
out = f2.execute_cf32(x.ravel()) # flat row-major input and output
assert len(out) == 64 * 64 and out.dtype == np.complex64