More on the 1D DFT

More on the 1D DFT#

import numpy as np
import scipy.linalg
import matplotlib.pyplot as plt
def do_plots(f):
    g = np.fft.fft(f)
    
    plt.clf()
    fig = plt.figure(figsize=(10,3))

    plt.subplot(131)
    plt.plot(x, f)

    plt.subplot(132)
    plt.plot(x, np.real(g))
    plt.plot(x, np.imag(g))

    plt.subplot(133)
    h = np.fft.ifft(g)
    plt.plot(x, np.real(h))
    plt.plot(x, np.imag(h))

    plt.show()


# Question 1
# Look at sin(kx) and cos(kx)

n = 32
x = np.linspace(0,n-1,n)
dx = x[1]-x[0]

k = 2*np.pi * (2/n)
do_plots(np.cos(k*x))

do_plots(np.sin(k*x))


# when we add n to the frequency, we get the same answer
# the result is aliased back into our range
k = 2*np.pi * (2/n + n)
do_plots(np.cos(k*x))

# a non-integer frequency gives leakage into other bins
# the discontinuity at the boundaries leads to high freq components
k = 2*np.pi * 3.33/n
do_plots(np.cos(k*x))
<Figure size 640x480 with 0 Axes>
_images/31466cee7bf35aac1a085c10b31190337bb91ba92542472307badf3bbdda149a.png
<Figure size 640x480 with 0 Axes>
_images/1856ea11d787619eec38b71f7513e217eb8a166f0cb15e3e753c8596ef219354.png
<Figure size 640x480 with 0 Axes>
_images/765f5f0de015a13ebfdd8cf2f9a9864861d8a058ebd6ee2b108a26dd434327b6.png
<Figure size 640x480 with 0 Axes>
_images/ea58defe2176c20d49979e07049a9bf3cfb9f524f65f849d2345cf87cb88208d.png
# Gaussian
n = 128
x = np.linspace(-0.5,0.5,n)
dx = x[1]-x[0]

f = np.exp(-(x-0)**2/(0.02**2))
g = np.fft.fft(f)

fig = plt.figure(figsize=(10,3))

plt.subplot(131)
plt.plot(x, f)

plt.subplot(132)
gs = np.fft.fftshift(g)
plt.plot(x, np.real(gs))
plt.plot(x, np.imag(gs))
plt.plot(x, np.abs(gs))

plt.subplot(133)
# Check the inverse transform
h = np.fft.ifft(g)
plt.plot(x, np.real(h))
plt.plot(x, np.imag(h))
plt.show()

# Check Parseval's theorem
sumf = np.sum(np.abs(f)**2)
sumg = np.sum(np.abs(g)**2)/n
print('Parseval check:', sumf, sumg, sumf-sumg)

plt.clf()
# This shows that the real part is symmetric about k=0
# but the imaginary part is anti-symmetric; therefore F(-k) = F^*(k)
fig = plt.figure(figsize=(10,3))
plt.subplot(121)
k = np.fft.fftfreq(n)
plt.plot(k[k>0], np.real(g[k>0]))
plt.plot(-k[k<0], np.real(g[k<0]))

plt.subplot(122)
plt.plot(k[k>0], np.imag(g[k>0]))
plt.plot(-k[k<0], np.imag(g[k<0]))
_images/b84a8aa358c344c58595c865ecb68e388fa9e3a8c62f699d95eebae57e8fd1ec.png
Parseval check: 3.18341790878128 3.18341790878128 0.0
[<matplotlib.lines.Line2D at 0x7de3baa8be00>]
<Figure size 640x480 with 0 Axes>
_images/45e87ed05e48d4c2aa02ad9c790dd5db392cdcdbcd90215e2cfa46d51730722a.png
# Question 2: manipulating f(x) by changing F(k)

n = 128
x = np.linspace(-0.5,0.5,n)
dx = x[1]-x[0]

f = np.exp(-(x-0)**2/(0.02**2)) + x
g = np.fft.fft(f)

fig = plt.figure(figsize=(10,4))

plt.subplot(131)
plt.plot(x, f)

plt.subplot(132)
# complex conjugate is equivalent to x->-x
h = np.fft.ifft(np.conjugate(g))
plt.plot(x, np.real(h))
plt.plot(x, np.imag(h))

plt.subplot(133)
# add a gradient in phase is equivalent to a translation in x
j = complex(0,1)
k = (2*np.pi/dx) * np.fft.fftfreq(n)
g = g * np.exp(-j * k * 0.2)

h = np.fft.ifft(g)
plt.plot(x, np.real(h))
plt.plot(x, np.imag(h))
plt.show()
_images/88de8480d0ba614f177c3f6020b0e61ba1bb1924dcc90639add8c5169a1299d1.png
# Question 3: convolution
import scipy.signal

def convolution(f,g):
    ft = np.fft.fft(f)
    gt = np.fft.fft(g)
    return np.fft.ifft(ft*gt)

n = 1024
x = np.linspace(-0.5,0.5,n)
dx = x[1]-x[0]

f = np.exp(-(x)**2/(0.02**2))
g = np.zeros_like(x)
g[(x>-0.2)&(x<0.2)] = 1

plt.plot(x,f)
plt.plot(x,g)
h = convolution(g,f)
h = np.roll(h, n//2)  # shift the result to centre it
plt.plot(x,np.real(h)/np.max(np.real(h)))

# Compare against the scipy convolution function
h = scipy.signal.convolve(g,f, mode='same')
plt.plot(x,h/np.max(h),':')
[<matplotlib.lines.Line2D at 0x7de3bcea7e00>]
_images/4ade1c8cd067aa04c429f1454cbe0b913f30e76752b2a3c08e684bc44d76a1c7.png
# This wasn't one of the questions, but have a look at the 
# top hat <-> sinc function transform
n = 64
x = np.linspace(0,1,n)
dx = x[1]-x[0]

f = np.zeros_like(x)
f[(x>0.45)&(x<0.55)] = 1

g = np.fft.fft(f)

fig = plt.figure(figsize=(10,4))

plt.subplot(131)
plt.plot(x, f)

plt.subplot(132)
gs = np.fft.fftshift(g)
plt.plot(x, np.real(gs))
plt.plot(x, np.imag(gs))
plt.plot(x, np.abs(gs))

plt.subplot(133)

h = np.fft.ifft(g)
plt.plot(x, np.real(h))
plt.plot(x, np.imag(h))

plt.show()
_images/557f91f5ed75bb6520f1188cebd9c0001c3036cc5548748a0f5053c81bb5bfb6.png
# Question 4: red noise and random walk

n = 1024
x = np.linspace(0,1,n)
dx = x[1]-x[0]

f = np.random.randn(n)

g = np.fft.fft(f)

fig = plt.figure(figsize=(10,4))

plt.subplot(131)
plt.plot(x, f)

plt.subplot(132)
g2 = g.copy()
j = complex(0,1)
k = (2*np.pi/dx) * np.fft.fftfreq(n)
g2[1:] /= j*k[1:]

plt.plot(k[k>0], np.abs(g2[k>0])**2, '.')
plt.yscale('log')
plt.xscale('log')

plt.subplot(133)

h = np.fft.ifft(g2)
plt.plot(x, np.real(h))
#plt.plot(x, np.imag(h))

plt.show()

plt.clf()
plt.plot(np.real(h[:n//2])/dx, np.real(h[n//2:])/dx)
plt.show()
_images/09aed824906db0517dda865905bd296ff6449099024c7e92905bca0da13f5d1b.png _images/a5b9302db0b2ccb6a25387698e6b1403e6ac6ed9e8f85aa1efe72aa3c20880cf.png

In this question, we generate a sample of red noise which has a power spectrum \(1/k^2\) by taking the Fourier transform of white noise (flat power spectrum) and dividing the Fourier transorm by \(ik\). This is equivalent to integrating the white noise, since

\[g(x) = \int^x dx^\prime f(x^\prime) = \int^x dx^\prime \int dk\ F(k) e^{ikx^\prime} = \int dk\ F(k) \int^x dx^\prime e^{ikx^\prime} = \int dk\ {F(k)\over ik} e^{ikx}.\]

When we take the samples of red noise and plot them, we see a random walk (you can see that the distance travelled is \(\sim \sqrt{N}\) as expected). Individual steps in the random are uncorrelated (white noise) but because successive steps add up over time (integrate) we get a red noise spectrum in the end.

This is an interesting way to generate a random walk.