1
Files
algorithms-python/fft.py

152 lines
8.6 KiB
Python
Executable File

#!/usr/bin/env python3
import numpy as np
from rich.traceback import install
install()
# We want to solve the FFT and inverse FFT in an easily accessible way, not necessarily efficient.
#
# The coefficient representation:
# - Every polynomial of degree d is of the form P_d(x) = c_0 * x**0 + c_1 * x**1 + ... + c_d * x**d
# - There exists a bijective mapping between the set of (d + 1)-tuples of coefficients and the set of polynomials
# - If we store a list of coefficients where the index corresponds to the degree we have a very simple representation of a polynomial
# - Example: P_2(x) = x**2 + 5 has the coefficient representation [5, 0, 1]
#
# The value representation:
# - There exists a surjective mapping between the set of (d + 1)-tuples of points from the polynomial's image to the set of polynomials
# - Example: P_2(x) = x**2 could have the value representation [(-1, 1), (1, 1)] or [(-2, 4), (2, 4)]
#
# Roots of unity:
# - The n-th roots of unity are all complex numbers satisfying the equation z**n = 1.
# - They can be visualized as n equally spaced points on the complex unit circle
# - In the real domain the unit circle is defined by the constraint x**2 + y**2 = 1, meaning every point on the real unit circle has the distance 1 from the origin
# Different formulation: We have a point (x, y) on the real unit circle with the angle p: x = cos(p), y = sin(p) and we know that cos(p)**2 + sin(p)**2 = 1, which is the distance to the origin (pythagorean theorem)
# - In the complex domain the pythagorean theorem works the same: We have a complex number c = (c_real, c_img) on the complex unit circle with the angle p: c_real = cos(p), c_img = sin(p)
# If we take Eulers formula: e**(i * p) = cos(p) + i * sin(p), we can easily define the r-th root of the n-th roots of unity:
# - The angle between the n-th roots is (2pi)/n, because all n roots are equally spaced
# - The angle of the r-th root can be written as (2pi * r)/n
# - The r-th root can thus be written as cos((2pi * r)/n) + i * sin((2pi * r)/n), which is equal to e**((i * 2pi * r)/n)
# - The "distance from the origin = 1" constraint holds (obviously): e**((i * 2pi * r)/n) = (e**((i * pi * r)/n))**2 = cos((2pi * r)/n)**2 + i * sin((2pi * r)/n)**2 = 1
# - With this visualization it is also easy to define them in a closed form term: w**r = e**((i * 2pi * r)/n) (w**r is the r-th of the n-th roots of unity)
# - It is also clear that the roots always come in +/- pairs, because if they are equally spaced on the complex unit circle every root is connected to another root by a line through the origin
#
# Calculating the value representation (FFT):
# - The naive approach is to evaluate polynomial of degree d in (d + 1) distinct points,
# but this would have a runtime of O(d**2), as every evaluation has a runtime of O(d)
# - To lower the runtime we can make use of a polynomial's symmetry: Even polynomials are symmetric regarding the imaginary axis, odd polynomials are symmetric regarding the origin
# - Every polynomial can be split into a sum of an even and an odd polynomial, so if we do this a single time, we have a runtime of O((d/2) * d), which is still quadratic
# - We split e.g. P_3(x) = 3 * x**3 + x**2 + 2 * x + 5 into P_3_even(x) = x + 5 and P_3_odd(x) = 3 * x + 2,
# so we get the alternative representation P_3(x) = P_3_even(x**2) + x * P_3_odd(x**2) = x**2 + 5 + x * (3 * x**2 + 2) = 3 * x**3 + x**2 + 2 * x + 5.
# This way we can always make use of the symmetry of even polynomials
# - We only need to evaluate the polynomial in d/2 points, because by using the symmetry every point can be transformed into its counterpart "on the other side":
# If we take the polynomial P_2(x) = x**2, the value representation could be [(-1, 1), (1, 1)], where (-1, 1) was inferred from one evaluation at x = 1
# - In general, we can write the value representation for a polynomial of 2nd degree as [(-x, P_2(x)), (x, P_2(x))]
# - To lower the runtime further one idea is obvious: Recursively split the polynomials to reach a runtime of O(d * log(d))
# - This raises a problem though: To infer all needed points from the polynomials image we always have to evaluate the polynomial in +/- pairs, e.g. x = 1 and x = -1.
# If we try to evaluate the split polynomial (P_3_even(x**2) and P_3_odd(x**2)) we can no longer choose arbitrary values, as x**2 is always positive in the real domain
# - The solution is to use the roots of unity as evaluation points :
# - If we want to calculate the FFT of P_2(x) = x**2 with P_2_even(x**2) = x and P_2_odd(x**2) = 0 we choose the 4th roots of unity as evaluation points for P_2(x)
# (4th because we need at least 3 points for the value representation of a degree 2 polynomial), and the 2nd roots of unity as evaluation points for P_2_even(x**2) and P_2_odd(x**2)
# - The last step is to combine the split value representations back into one:
#
# Calculating the coefficient representation (IFFT):
# Main Algorithms
def FFT_recursive(n: int, coefficient_repr: list):
if n == 1:
return coefficient_repr # In this case we already have the value representation as the function is constant
# Split the function in even and odd parts of degree n/2
even_coefficients = coefficient_repr[0::2]
odd_coefficients = coefficient_repr[1::2]
# This step is the important part: We recursively apply FFT to reach the runtime of O(n log n). The result are two value representations evaluated in the (n/2)ths roots of unity.
even_values = FFT_recursive(int(n/2), even_coefficients)
odd_values = FFT_recursive(int(n/2), odd_coefficients)
w = np.exp(1j * 2 * np.pi / n) # w**r is our r-th root of the n-th roots of unity
value_repr = [0] * n
for i in range(int(n/2)):
# Note that the odd values have to be multiplied by the evaluation point because we extracted one "x" from the term to get an even polynomial again
value_repr[i] = even_values[i] + w**i * odd_values[i] # Evaluate the even part of our polynomial (symmetry regarding x-axis => +, only the evaluation point is negative)
value_repr[i + int(n/2)] = even_values[i] - w**i * odd_values[i] # Evaluate the odd part of our polynomial (symmetry regarding origin => -, the evaluation point and evaluation value are negative)
return value_repr
def IFFT_recursive(n: int, value_repr: list):
if n == 1:
return value_repr
even_values = value_repr[0::2]
odd_values = value_repr[1::2]
even_coefficients = IFFT_recursive(int(n/2), even_values)
odd_coefficients = IFFT_recursive(int(n/2), odd_values)
w = np.exp(-1j * 2 * np.pi / n) # The DFT can be inverted by just conjugating the exponants and normalizing everything later by 1/n
coefficient_repr = [0] * n
for i in range(int(n/2)):
coefficient_repr[i] = even_coefficients[i] + w**i * odd_coefficients[i]
coefficient_repr[i + int(n/2)] = even_coefficients[i] - w**i * odd_coefficients[i]
return coefficient_repr
# Helper functions
def power_of_two(n):
"Find the smallest power of two that is larger than n."
power = 0
while 2**power < n:
power += 1
return power
def pad_coefficients(coefficient_repr):
"Transform a coefficient representation to a representation of higher degree (FFT handles polynomials with degrees of powers of two)."
power = power_of_two(len(coefficient_repr))
for _ in range(2**power - len(coefficient_repr)):
coefficient_repr += [(0+0j)]
return coefficient_repr
# FFT basically converts from the frequency domain to the time/signal domain
# The coefficient representation
def FFT(coefficient_repr):
"Convert a polynomial from its coefficient representation to its value representation."
n = len(coefficient_repr)
return FFT_recursive(n, coefficient_repr)
# FFT basically converts from the time/signal domain to the frequency domain
# The value representation is the signal: A lot of values at different points in time
def IFFT(value_repr):
"Convert a polynomial from its coefficient representation to its value representation."
n = len(value_repr)
unnormalized = IFFT_recursive(n, value_repr)
# The second part of the DFT matrix inversion: Normalize by 1/n
normalized = [(0+0j)] * n
for i in range(n):
normalized[i] = unnormalized[i] / n
return normalized
# FFT:
print("FFT:")
coefficient_repr = pad_coefficients([(0+0j), (0+0j), (1+0j), (0+0j)]) # P_2(x) = x**2
value_repr = FFT(coefficient_repr)
print("Coefficient Representation:", coefficient_repr)
print("Value Representation:", value_repr)
# IFFT:
print("IFFT:")
coefficient_repr = IFFT(value_repr)
print("Value Representation:", value_repr)
print("Coefficient Representation:", coefficient_repr)