Bluestein's algorithm in Python
Computes a DFT of arbitrary length n as a convolution with a chirp, using power-of-two FFTs.
- File
bluestein_fft.py, 129 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bluestein-fft/. The authors are listed in prov/GDR6AH.jsonl.
import cmath
import math
def _fft(a: list, w, p: int | None) -> None:
"""Replace a by its DFT with the principal len(a)-th root w, in place.
Iterative radix-2 Cooley-Tukey; len(a) must be a power of two.
Modulo p, w is the root itself. Over the complex numbers, w is the sign
-1 or +1 of the exponent, w = exp(sign 2 pi i / len(a)), and each twiddle
factor is computed directly so that its error does not grow with its index.
"""
n = len(a)
j = 0
for i in range(1, n):
bit = n >> 1
while j & bit:
j ^= bit
bit >>= 1
j |= bit
if i < j:
a[i], a[j] = a[j], a[i]
h = 1
while h < n:
if p is None:
table = [cmath.exp(w * 1j * math.pi * k / h) for k in range(h)]
else:
step = pow(w, n // (2 * h), p)
table = [1] * h
for k in range(1, h):
table[k] = table[k - 1] * step % p
for s in range(0, n, 2 * h):
for k in range(h):
u = a[s + k]
v = a[s + h + k] * table[k]
if p is None:
a[s + k], a[s + h + k] = u + v, u - v
else:
a[s + k], a[s + h + k] = (u + v) % p, (u - v) % p
h *= 2
def _chirp_dft(x: list, zeta, zeta_inv, root_m, p: int | None) -> list:
"""Return X[k] = sum_j x[j] zeta^(2jk) by Bluestein's chirp convolution.
Modulo p, zeta must satisfy zeta^(2n) = 1 with zeta^2 a principal n-th root of
unity, zeta_inv is its inverse, and root_m is a principal M-th root of unity for
the power of two M chosen below. Over the complex numbers, zeta is the sign -1
or +1 of zeta = exp(sign pi i / n), and zeta_inv and root_m are unused; each
chirp value is computed directly from m^2 mod 2n.
"""
n = len(x) # @line L1
big_m = 1
while big_m < 2 * n - 1:
big_m *= 2
if p is None: # @line L2
c = [cmath.exp(zeta * 1j * math.pi * (m * m % (2 * n)) / n) for m in range(n)]
cb = [c[m].conjugate() for m in range(n)]
root_m = -1
else:
c = [pow(zeta, m * m % (2 * n), p) for m in range(n)]
cb = [pow(zeta_inv, m * m % (2 * n), p) for m in range(n)]
a = [0] * big_m # @line L3
for j in range(n):
a[j] = x[j] * c[j] if p is None else x[j] * c[j] % p
b = [0] * big_m # @line L4
for m in range(n):
b[m] = cb[m]
if m:
b[big_m - m] = cb[m]
_fft(a, root_m, p) # @line L5
_fft(b, root_m, p)
for r in range(big_m): # @line L6
a[r] = a[r] * b[r] if p is None else a[r] * b[r] % p
if p is None: # @line L7
_fft(a, 1, None)
z = [v / big_m for v in a]
else:
_fft(a, pow(root_m, -1, p), p)
inv_m = pow(big_m, -1, p)
z = [v * inv_m % p for v in a]
if p is None: # @line L8
return [c[k] * z[k] for k in range(n)]
return [c[k] * z[k] % p for k in range(n)] # @line L9
def _modular_roots(n: int, p: int, g: int) -> tuple[int, int, int]:
"""Return (zeta, zeta_inv, root_m) modulo the prime p from a primitive root g."""
big_m = 1
while big_m < 2 * n - 1:
big_m *= 2
if (p - 1) % (2 * n) or (p - 1) % big_m:
raise ValueError("p - 1 must be divisible by 2n and by the convolution length")
zeta = pow(g, (p - 1) // (2 * n), p)
return zeta, pow(zeta, -1, p), pow(g, (p - 1) // big_m, p)
def bluestein(x: list, p: int | None = None, g: int | None = None) -> list:
"""Return the DFT X[k] = sum_j x[j] w^(jk) of x, for any length n >= 1.
Over the complex numbers (p is None), w = exp(-2 pi i / n). Modulo a prime p
with primitive root g, w = g^((p - 1) / n); then p - 1 must be divisible by 2n
and by the power of two M >= 2n - 1 used for the convolution.
Cost: three FFTs of length M plus 2n + 2M multiplications, Theta(n log n).
"""
n = len(x)
if n < 1:
raise ValueError("the length must be positive")
if p is None:
return _chirp_dft(x, -1, None, None, None)
zeta, zeta_inv, root_m = _modular_roots(n, p, g)
return _chirp_dft([v % p for v in x], zeta, zeta_inv, root_m, p)
def inverse_bluestein(big_x: list, p: int | None = None, g: int | None = None) -> list:
"""Return the x with bluestein(x, p, g) == big_x, by the conjugate chirp and 1/n."""
n = len(big_x)
if n < 1:
raise ValueError("the length must be positive")
if p is None:
y = _chirp_dft(big_x, 1, None, None, None) # @line L10
return [v / n for v in y] # @line L11
zeta, zeta_inv, root_m = _modular_roots(n, p, g)
y = _chirp_dft([v % p for v in big_x], zeta_inv, zeta, root_m, p)
inv_n = pow(n, -1, p)
return [v * inv_n % p for v in y]
test_bluestein_fft.py 129 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bluestein-fft/. The authors are listed in prov/GDR6AH.jsonl.
import cmath
import math
import random
import pytest
from bluestein_fft import bluestein, inverse_bluestein
P = 998244353
ROOTS = {17: 3, 97: 5, P: 3}
PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101]
def naive_dft(x: list, p: int | None = None) -> list:
n = len(x)
if p is None:
table = [cmath.exp(-2j * math.pi * m / n) for m in range(n)]
return [sum(x[j] * table[j * k % n] for j in range(n)) for k in range(n)]
w = pow(ROOTS[p], (p - 1) // n, p)
table = [pow(w, m, p) for m in range(n)]
return [sum(x[j] * table[j * k % n] for j in range(n)) % p for k in range(n)]
def close(a: list, b: list, scale: float) -> bool:
n = max(len(a), 1)
tol = 1e-13 * n * max(scale, 1.0)
return len(a) == len(b) and all(abs(u - v) <= tol for u, v in zip(a, b))
def valid_lengths(p: int, limit: int) -> list[int]:
out = []
for n in range(1, limit + 1):
m = 1
while m < 2 * n - 1:
m *= 2
if (p - 1) % (2 * n) == 0 and (p - 1) % m == 0:
out.append(n)
return out
def test_worked_example() -> None:
assert bluestein([1, 2, 3], 97, 5) == [6, 60, 34] == naive_dft([1, 2, 3], 97)
assert inverse_bluestein([6, 60, 34], 97, 5) == [1, 2, 3]
h = math.sqrt(3) / 2
assert close(bluestein([1, 2, 3]), [6, complex(-1.5, h), complex(-1.5, -h)], 6)
def test_valid_lengths() -> None:
assert valid_lengths(17, 200) == [1, 2, 4, 8]
assert valid_lengths(97, 200) == [1, 2, 3, 4, 6, 8, 12, 16]
assert valid_lengths(P, 200) == [1, 2, 4, 7, 8, 14, 16, 17, 28, 32, 34, 56, 64, 68, 112, 119, 128, 136]
def test_modular_against_oracle() -> None:
rng = random.Random(1970)
cases = 0
for p in (17, 97, P):
for n in valid_lengths(p, 140):
for _ in range(25 if n <= 40 else 3):
x = [rng.randrange(p) for _ in range(n)]
y = bluestein(x, p, ROOTS[p])
assert y == naive_dft(x, p)
assert inverse_bluestein(y, p, ROOTS[p]) == x
cases += 1
assert cases >= 500
def test_complex_primes() -> None:
rng = random.Random(1968)
for n in PRIMES:
for _ in range(3):
x = [complex(rng.uniform(-5, 5), rng.uniform(-5, 5)) for _ in range(n)]
y = bluestein(x)
scale = 5 * math.sqrt(n)
assert close(y, naive_dft(x), scale)
assert close(inverse_bluestein(y), x, 5)
def test_complex_random_lengths() -> None:
rng = random.Random(1969)
for _ in range(500):
n = rng.randint(1, 40)
x = [complex(rng.uniform(-10, 10), rng.uniform(-10, 10)) for _ in range(n)]
y = bluestein(x)
scale = 10 * math.sqrt(n)
assert close(y, naive_dft(x), scale)
assert close(inverse_bluestein(y), x, 10)
def test_edge_cases() -> None:
assert bluestein([5], 97, 5) == [5]
assert bluestein([3, 4], 17, 3) == [7, 16]
assert close(bluestein([2.5]), [2.5], 1)
assert close(bluestein([3, 4]), [7, -1], 4)
for n in (1, 2, 3, 5, 6, 7, 10, 12, 31, 64, 100):
assert close(bluestein([0.0] * n), [0.0] * n, 1)
assert close(bluestein([1.0] + [0.0] * (n - 1)), [1.0] * n, 1)
assert close(bluestein([2.0] * n), [2.0 * n] + [0.0] * (n - 1), 2 * n)
for s in (0, n // 2, n - 1):
imp = [0.0] * n
imp[s] = 1.0
assert close(bluestein(imp), naive_dft(imp), 1)
for p in (17, 97, P):
for n in valid_lengths(p, 20):
assert bluestein([0] * n, p, ROOTS[p]) == [0] * n
assert bluestein([1] + [0] * (n - 1), p, ROOTS[p]) == [1] * n
assert bluestein([2] * n, p, ROOTS[p]) == [2 * n % p] + [0] * (n - 1)
def test_larger_lengths() -> None:
rng = random.Random(2)
for n in (127, 251, 509, 1000):
x = [complex(rng.uniform(-1, 1), rng.uniform(-1, 1)) for _ in range(n)]
y = bluestein(x)
energy = sum(abs(v) ** 2 for v in y)
assert abs(energy - n * sum(abs(v) ** 2 for v in x)) < 1e-9 * n * n
assert close(inverse_bluestein(y), x, 1)
def test_bad_input() -> None:
with pytest.raises(ValueError):
bluestein([])
with pytest.raises(ValueError):
bluestein([1, 2, 3], 17, 3)
with pytest.raises(ValueError):
bluestein([1] * 5, 97, 5)
What the next level needs
- the tests did not run in this build