Algorithm Search⌘K

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
bluestein_fft.pyRawGitHub
# 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

Read the full page about Bluestein's algorithm