Algorithm Search⌘K

Rader's algorithm in Python

Computes a prime-length DFT as a cyclic convolution of length p-1 using a primitive root.

File
rader_fft.py, 140 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
rader_fft.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/rader-fft/. The authors are listed in prov/8K6AGB.jsonl.

"""Rader's algorithm: the DFT of prime length n as a cyclic convolution of length n - 1.

X_k = sum_j x_j w**(j*k). Over C, w = exp(-2*pi*i/n). Modulo a prime P, w must
have order n modulo P, and P - 1 must be divisible by a power of two L >= 2n - 3,
which is used for the inner convolution by a number-theoretic transform.
"""

from __future__ import annotations

import cmath
import math
from typing import TypeVar

S = TypeVar("S", int, complex)


def _unit(r: int, n: int) -> complex:
    """Return exp(2*pi*i*r/n), exact when r/n is a multiple of 1/4."""
    r %= n
    if (4 * r) % n == 0:
        return (1 + 0j, 1j, -1 + 0j, -1j)[4 * r // n]
    return cmath.exp(2j * math.pi * r / n)


def _prime_divisors(m: int) -> list[int]:
    out, d = [], 2
    while d * d <= m:
        if m % d == 0:
            out.append(d)
            while m % d == 0:
                m //= d
        d += 1
    if m > 1:
        out.append(m)
    return out


def is_prime(n: int) -> bool:
    return n >= 2 and all(n % d for d in range(2, math.isqrt(n) + 1))


def primitive_root(q: int) -> int:
    """The least primitive root modulo the prime q."""
    if q == 2:
        return 1
    ps = _prime_divisors(q - 1)
    for g in range(2, q):
        if all(pow(g, (q - 1) // r, q) != 1 for r in ps):
            return g
    raise ValueError("no primitive root")


def _fft(a: list[S], table: list[S], P: int | None) -> None:
    """In-place radix-2 DIT FFT; table[k] = (root of order len(a))**k for k < len(a)/2."""
    L = len(a)
    j = 0
    for i in range(1, L):
        bit = L >> 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 < L:
        step = L // (2 * h)
        for s in range(0, L, 2 * h):
            for k in range(h):
                u, v = a[s + k], table[k * step] * a[s + k + h]
                if P is None:
                    a[s + k], a[s + k + h] = u + v, u - v
                else:
                    a[s + k], a[s + k + h] = (u + v) % P, (u - v) % P
        h *= 2


def cyclic_convolution(a: list[S], b: list[S], P: int | None = None) -> list[S]:
    """c_p = sum_q a_q b_{(p - q) mod N} for N = len(a) = len(b), by FFTs of length L >= 2N - 1."""
    N = len(a)
    L = 1
    while L < 2 * N - 1:
        L *= 2
    if P is None:
        fwd = [_unit(-k, L) for k in range(L // 2)]
        inv = [_unit(k, L) for k in range(L // 2)]
    else:
        if (P - 1) % L:
            raise ValueError("P - 1 must be divisible by the convolution length")
        r = pow(primitive_root(P), (P - 1) // L, P)
        fwd = [pow(r, k, P) for k in range(L // 2)]
        inv = [pow(r, -k, P) for k in range(L // 2)]
    A = list(a) + [0] * (L - N)
    B = list(b) + [0] * (L - N)
    _fft(A, fwd, P)
    _fft(B, fwd, P)
    C = [u * v if P is None else u * v % P for u, v in zip(A, B)]
    _fft(C, inv, P)
    scale = 1 / L if P is None else pow(L, -1, P)
    lin = [v * scale if P is None else v * scale % P for v in C]
    out = lin[:N]
    for p in range(N - 1):
        out[p] = out[p] + lin[p + N] if P is None else (out[p] + lin[p + N]) % P
    return out


def rader_fft(x: list[S], w: int | None = None, P: int | None = None, inverse: bool = False) -> list[S]:
    """Return the DFT of x, whose length n must be prime.

    Over C (P is None) the root is exp(-2*pi*i/n), or exp(+2*pi*i/n) with the
    factor 1/n when inverse is True. Modulo P, w must have order n; inverse
    uses w**(-1) and the factor 1/n.
    """
    n = len(x)
    if not is_prime(n):
        raise ValueError("the length must be prime")
    if P is not None and (w is None or w % P == 1 or pow(w, n, P) != 1):
        raise ValueError("w must have order n modulo P")
    sign = 1 if inverse else -1
    if P is None:
        root = lambda r: _unit(sign * r, n)  # noqa: E731  w**r over C
    else:
        wi = pow(w, -1, P) if inverse else w
        root = lambda r: pow(wi, r, P)  # noqa: E731
    g = primitive_root(n)  # @line L1
    N = n - 1
    a = [x[pow(g, q, n)] for q in range(N)]  # @line L2
    b = [root(pow(g, N - m, n)) for m in range(N)]  # @line L3  w**(g**(-m))
    c = cyclic_convolution(a, b, P)  # @line L4
    out = [0] * n
    out[0] = sum(x) if P is None else sum(x) % P  # @line L5
    for p in range(N):  # @line L6
        k = pow(g, N - p, n)
        out[k] = x[0] + c[p] if P is None else (x[0] + c[p]) % P  # @line L7
    if inverse:
        out = [v / n for v in out] if P is None else [v * pow(n, -1, P) % P for v in out]
    return out  # @line L8
test_rader_fft.py 117 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/rader-fft/. The authors are listed in prov/8K6AGB.jsonl.

import cmath
import math
import random

import pytest

from rader_fft import cyclic_convolution, is_prime, primitive_root, rader_fft

U = 2.0**-53
# (prime, primitive root): 3 is a primitive root of 17 and 998244353, 5 of 97.
PRIMES = [(17, 3), (97, 5), (998244353, 3)]


def naive_mod(x, w, P):
    n = len(x)
    return [sum(v * pow(w, j * k, P) for j, v in enumerate(x)) % P for k in range(n)]


def naive_complex(x):
    n = len(x)
    return [sum(v * cmath.exp(-2j * math.pi * ((j * k) % n) / n) for j, v in enumerate(x)) for k in range(n)]


def norm(v):
    return math.sqrt(sum(abs(t) ** 2 for t in v))


def conv_length(n):
    L = 1
    while L < 2 * n - 3:
        L *= 2
    return L


def modulus_for(n):
    """A prime P = 1 mod n with P - 1 divisible by the inner convolution length."""
    step = n * conv_length(n) // math.gcd(n, conv_length(n))
    P = step + 1
    while not is_prime(P):
        P += step
    return P


def test_example_mod_41():
    assert primitive_root(41) == 6 and primitive_root(5) == 2
    w = pow(6, 8, 41)
    assert w == 10
    assert cyclic_convolution([2, 3, 5, 4], [10, 16, 37, 18], 41) == [36, 13, 39, 21]
    assert rader_fft([1, 2, 3, 4, 5], w, 41) == [15, 37, 22, 14, 40]


def test_given_primes():
    rng = random.Random(20261107)
    for P, g in PRIMES:
        for n in [q for q in range(2, 40) if is_prime(q) and (P - 1) % q == 0 and (P - 1) % conv_length(q) == 0]:
            w = pow(g, (P - 1) // n, P)
            for _ in range(5):
                x = [rng.randrange(P) for _ in range(n)]
                y = rader_fft(x, w, P)
                assert y == naive_mod(x, w, P)
                assert rader_fft(y, w, P, inverse=True) == x


def test_many_prime_lengths_exact():
    rng = random.Random(11)
    for n in [q for q in range(2, 140) if is_prime(q)]:
        P = modulus_for(n)
        w = pow(primitive_root(P), (P - 1) // n, P)
        for _ in range(2):
            x = [rng.randrange(P) for _ in range(n)]
            y = rader_fft(x, w, P)
            assert y == naive_mod(x, w, P)
            assert rader_fft(y, w, P, inverse=True) == x


def test_edges():
    assert rader_fft([3, 5], 16, 17) == [8, 15]
    for n in [2, 3, 5, 7, 11, 13]:
        assert max(map(abs, rader_fft([0j] * n))) == 0
        assert all(abs(v - 1) < 1e-12 for v in rader_fft([1.0] + [0.0] * (n - 1)))
        y = rader_fft([2.0] * n)
        assert abs(y[0] - 2 * n) < 1e-12 and all(abs(v) < 1e-12 * n for v in y[1:])


def test_errors():
    with pytest.raises(ValueError):
        rader_fft([1.0, 2.0, 3.0, 4.0])
    with pytest.raises(ValueError):
        rader_fft([1.0])
    with pytest.raises(ValueError):
        rader_fft([1, 2, 3], 1, 97)


def test_random_complex():
    rng = random.Random(77)
    for n in [q for q in range(2, 400) if is_prime(q)]:
        x = [complex(rng.uniform(-1, 1), rng.uniform(-1, 1)) for _ in range(n)]
        ref = naive_complex(x) if n < 200 else None
        y = rader_fft(x)
        tol = 64 * (n.bit_length() + 2) * U
        if ref is not None:
            assert norm([a - b for a, b in zip(y, ref)]) <= tol * norm(ref)
        back = rader_fft(y, inverse=True)
        assert norm([a - b for a, b in zip(back, x)]) <= tol * norm(x)


def test_large_complex():
    rng = random.Random(3)
    n = 8191  # a Mersenne prime; the inner length is 16384
    x = [complex(rng.gauss(0, 1), rng.gauss(0, 1)) for _ in range(n)]
    y = rader_fft(x)
    back = rader_fft(y, inverse=True)
    assert norm([a - b for a, b in zip(back, x)]) <= 64 * (n.bit_length() + 2) * U * norm(x)
    assert abs(norm(y) ** 2 - n * norm(x) ** 2) <= 1e-9 * n * norm(x) ** 2

What the next level needs

  • the tests did not run in this build

Read the full page about Rader's algorithm