def pad_poly(f, n):
'''
Given f as an array of coefficients, pad the polynomial to length n
by adding zeros
'''
if len(f) >= n: return f
return f + [f[0] - f[0] for _ in range(n - len(f))]
def is_power_of_two(n):
if n == 1: return True
if n % 2 == 1: return False
return is_power_of_two(n/2)
def FFT(f, omega, n):
'''
Assuming that:
- f is an array of n coefficients
- omega is an n-th principal root of unity
- n is a power of 2
'''
if n == 1: return [f[0]]
if not is_power_of_two(n): raise ValueError(f"{n} is not a power of two")
f = pad_poly(f, n)
# Precompute all powers of omega
pow_omega = []
omega_i = (omega / omega)
for i in range(n):
pow_omega.append(omega_i)
omega_i = omega_i * omega
f_even = f[::2]
f_odd = f[1::2]
a_even = FFT(f_even, omega*omega, n/2)
a_odd = FFT(f_odd , omega*omega, n/2)
output = []
for i in range(n):
output.append(a_even[i % (n/2)] + pow_omega[i] * a_odd[i % (n/2)])
return output
R = CC
I**2
FFT([1,2,3,4], I, 4)
Let's first write down the function where we are provided a suitable root of unity.
def poly_mult_with_rou(f, g, omega, n):
'''
f and g are polynomials given as an array of n coefficients,
and omega is a 2n-th root of unity.
Assuming that n is a power of 2
'''
if n == 1: return [f[0]*g[0]]
f_prime = pad_poly(f, 2*n)
g_prime = pad_poly(g, 2*n)
FFT_f = FFT(f_prime, omega, 2*n)
FFT_g = FFT(g_prime, omega, 2*n)
FFT_h_times_2n = [FFT_f[i] * FFT_g[i] for i in range(2*n)]
h_times_2n = FFT(FFT_h_times_2n, omega^-1, 2*n)
return [h_times_2n[i] / (2*n) for i in range(2*n) ]
f = [1,2] # 1 + 2x
g = [3,4] # 3 + 4x
poly_mult_with_rou(f, g, omega = I, n = 2)
R.<x> = PolynomialRing(QQ)
f = R(f)
g = R(g)
h = R([3,10,8,0])
h == f * g
We'll just look at an example where we wish to multiply polynomials $f$ and $g$ of degree < $n = 4$. This would need an $8$-th principal root of unity but the field $\mathbb{Q}$ does not have one.
R.<x,y> = PolynomialRing(QQ)
f = 1 + 2*x - x^2 + 4*x^3
g = 7 - 2*x + 0 + 3*x^3
Let's write the above polynomials as a bivariate polynomial ($k = m = 2$)
\begin{align*} f(x) &= 1 + 2x - x^2 + 4x^3 & g(x) &= 7 - 2x + 3x^3\\ \tilde{f}(x,y) & = (1 + 2x) + (-1 + 4x)y & \tilde{g}(x,y) & = (7 - 2x) + (3x)y\\ f(x) & = \tilde{f}(x,x^2) & g(x) & = \tilde{g}(x,x^2) \end{align*}f_tilde = (1 + 2*x) + (-1 + 4*x)*y
g_tilde = (7 - 2*x) + (0 + 3*x)*y
f_tilde_coeff = [1 + 2*x , (-1 + 4*x)]
g_tilde_coeff = [7 - 2*x , (0 + 3*x)]
In order to multiply $\tilde{f}$ and $\tilde{g}$, which are polynomials in $y$ of degree < $2$, we need a $4$-th root of unity. So let's expand the ring to $$R' = \frac{\mathbb{Q}[x]}{(x^4 + 1)}.$$
R_prime = R.quotient(R.ideal(x^4 + 1))
R_prime
xbar, ybar = R_prime.gens()
In the above ring, $\overline{x}$ is actually an $8$-th root of unity but we only need a $4$-th root of unity so we will choose $\omega = \bar{x}^2$ when we need to multiply.
omega = xbar^2
print(f"Powers of omega where we evaluate: {[omega^i for i in range(4)]}")
Let's now interpret the two polynomials $\tilde{f}$ and $\tilde{g}$ modulo $x^4 + 1$.
f_tilde_bar = R_prime(f_tilde)
print(f"f_tilde_bar = {f_tilde_bar}")
g_tilde_bar = R_prime(g_tilde)
print(f"g_tilde_bar = {g_tilde_bar}")
f_tilde_bar_coeff = [R_prime(a) for a in f_tilde_coeff]
g_tilde_bar_coeff = [R_prime(b) for b in g_tilde_coeff]
Since we already have a 4th principal root of unity in our ring ($\omega = \overline{x}^2$) we can use FFT to compute the product.
# If you want to see what FFT does with omega = xbar^2, you can try out the line below:
#
# print(FFT(f_tilde_bar_coeff, omega = xbar^2, n = 4))
# # [
# # 6*xbar,
# # 4*xbar^3 - xbar^2 + 2*xbar + 1,
# # -2*xbar + 2,
# # -4*xbar^3 + xbar^2 + 2*xbar + 1
# # ]
# # which is basically just
# # [f_tilde(xbar, 1), f_tilde(xbar, xbar^2), f_tilde(xbar, -1), f_tilde(xbar, -xbar^2)]
h_tilde_bar_coeff = poly_mult_with_rou(
f_tilde_bar_coeff,
g_tilde_bar_coeff,
omega = xbar^2,
n = 2
)
h_tilde_bar_coeff
h_tilde = (-4*x^2 + 12*x + 7) + (-2*x^2 + 33*x - 7)*y + (12*x^2 - 3*x)*y^2
h_tilde == f_tilde * g_tilde
h = h_tilde.subs(y = x^2)
f*g == h