Fast Fourier Transforms

Fast fourier transforms

Signals: values over time (EX: stock price) Discrete signals: value over time measured at a specific interval Periodic signal: pattern repeats over time

A Fourier transform breaks a signal into the frequency components that make it up. It's used in all time series data, image data, audio data, etc. It allows you to smooth/de-noise signals and inspect data.

Discrete Fourier Transforms

All complex roots of unity are generated as powers of the generator root:

ɷ = omega

ɷn = e^[(2pi/n)j]

ɷn^n = 1 because e^(2pij)

kth root = e^((2pi/n * k)j) or ɷn^j

ɷn is a vector such that it takes n rotations to come back to 1

So using that:

Discrete Fourier Transform of the signal [a0, a1, a2, ... , an-1] <- n real numbers cycle

Think of the polynomial a(x) = your series

The Fourier transform is when you evaluate the polynomial for various roots of unity.

a(ɷn^0), a(ɷn^1), ...

EX:

[+1, -1, +1, -1] <- sequence

[ɷ4^0, ɷ4^1, ɷ4^2, ɷ4^3] <- roots of unity
= [1, j, -1, -j]

A0 = 1+(-1)+1+(-1) = 0
A1 = 1+(-1)j+... = 0
A2 = 4
A3 = 0

[0, 0, 4, 0] <- Discrete Fourier transform

You can invert this with an inverse Fourier transform to get the original sequence.

Computing Fourier Transforms

FT and inv FT are Θ(n^2) if you implement the math naively.

def compute_fourier_coeffs_naive(seq):
    n = len(seq)
    fft_seq = []
    w = 1.0
    # This is the generator of all the nth roots of unity.
    wn = math.cos(2.0 * math.pi /n) + math.sin(2.0 * math.pi/n)*1j
    # Compute each coefficient
    for j in range(n):
        Aj = 0
        wj = 1.0
        # Compute the summation
        for k in range(n):
            Aj = Aj + wj*seq[k]
            wj = wj * w
        fft_seq.append(Aj)
        w = w * wn
    #The overall procedure takes n^2 time.
    return fft_seq

You can do better by splitting the polynomial into even and odd parts by coefficient.

aeven(x^2) + x * aodd(x)

The ith Fourier coefficient is:

aeven((ɷn/2)^i) + ɷn^i aodd((ɷn/2)^i)

So if you compute the even sequences and the odd sequences you can combine them using the equation above to merge and compute the initial sequence.

Base case is when N is 1 - this algo is for if N is even, FFTW(est) can handle N being odd.

Θ(nlogn)

def fft(seq):
    # this code only works when the length of sequence is a power of two.
    # for arbitrary sequence lengths, we need a more carefully designed divide and conquer
    # scheme such as the so called "FFTW" (fast fourier transform in the West) scheme
    # proposed by Cooley and Tukey.
    # base case when seq has single element
    if len(seq) == 1:
        return seq
    else:
        n = len(seq)
        assert n%2 == 0
        # Split the sequence into odd add even elements
        seq_even = [seq[2*j] for j in range(n//2)] # even
        seq_odd = [seq[2*j+1] for j in range(n//2)] # odd
        s1 = fft(seq_even) # recursively call fft
        s2 = fft(seq_odd) # recursively call fft
        # prepare the result array
        fft_ret = [0]*n
        w = 1.0
        wn = (math.cos(2.0 * math.pi /n) + math.sin(2.0 * math.pi/n)*1j)
        # combine the fft for odd and even into
        # fft for the original sequence.
        # See CLRS for explanation.
        for k in range(n//2):
            fft_ret[k] = (s1[k] + w * s2[k])
            fft_ret[k+n//2] = (s1[k] - w * s2[k])
            w = w * wn
        return fft_ret

seq = [1.0, -1.0, -1.0, 1.0, -1.0, -1.0,1.0, -1.0]
fft_seq = fft(seq)