Case 1
Consider the following PDE with constant
Represent the solution
In a spectral method we take
- Note that
so that polar coordinates in the complex plane map to Cartesian components and .

Here,
This gives
This results in
In particular, consider a grid of N points with uniform spacing

The exponent of

The vertical dashed lines connect points with conjugate symmetry (the imaginary parts are the same in magnitude but have opposite signs). If
Conjugate symmetry is illustrated below in Python for six points.
from scipy.fft import fft, ifft, fftshift
import numpy as np
u = np.random.rand(6)
uhat = ifft(u)
for i in uhat:
print(f"{i.real:.4f} + {i.imag:.4f}i")
0.3354 + -0.0000i
0.0999 + -0.1277i
-0.0076 + -0.0542i
0.1084 + -0.0000i
-0.0076 + 0.0542i
0.0999 + 0.1277i
For numerical solution, it is important that the ordering of the indicies
The points are ordered starting at
n = np.arange(N)
n[int(N/2)+1:]-= N
The inverse discrete Fouier transform (IDFT) of
The corresponding discrete Fourier transform (DFT) is given by
These are evaluated using packaged fast Fourier transform (FFT) routines (and the corresponding fast inverse).

Now we will insert the green equation into the PDE. Note that
Case 2
Consider the same PDE, but with
Consider again the PDE. While it remains linear with
Psuedo-spectral method
Here, we solve the problem in the physical space, but evaluate derivatives in the spectral space. On the right-hand-side of the PDE we write
At this point, the pseudo-spectral method (also called the collocation method) evaluates this equation at grid points
Example 1
Solve the PDE with the approach of Case 2, but with constant a=1.
import matplotlib.pyplot as plt
%matplotlib inline
from scipy.integrate import odeint
L = 10.0
nx = 8
a = 1.0
ntflow = 1 # flow-through times; result looks perfect for ntflow integer for any nx
# but requires more points to look good for non-integer ntflow
tend = ntflow*L/a
#---------- solution domain, initial condition
dx = L/nx # not L/(nx-1)
x = np.linspace(0.0, L-dx, nx)
u0 = 1.0/(np.cosh(2.0*(x-5.0)))
#---------- exact solution with more points for plotting
xx = np.linspace(0.0, L, 1000)
uu = 1.0/(np.cosh(2.0*(xx-5.0)))
#---------- solve the problem and plot
def rhsf(u, t):
N = len(u)
n = np.arange(N); n[int(N/2)+1:]-= N
i = 1j # 1j is the imaginary number in python; copy as i for clarity
return -a*ifft(2*np.pi*i*n/L*fft(u)).real
t = np.linspace(0,tend,11)
y = odeint(rhsf, u0, t)
fig,ax=plt.subplots()
ax.plot(xx,uu, '-', color='red', lw=2)
ax.plot(x,y[-1,:], 'o', color='gray', lw=1)
plt.rc('font', size=14)
plt.gca().set_xlim([0,10])
plt.xlabel('x')
plt.ylabel(r'u(x)');
plt.legend(['exact', 'spectral'], frameon=False);

Example 2
Solve the viscous Burgers’ equation:
- Use a periodic initial condition of
. - Solve to
.
Exact solution
The exact solution (from wikipedia ) is given by
This equation is evaluated here using Sympy.
See also this link
import sympy as sp
ν, t, L, x, xp = sp.symbols('ν, t, L, x, xp')
#ex = sp.integrate(sp.cos(2*sp.pi*x/L)+2, (x,0,xp))/2/ν
ex = L/(4*sp.pi*ν)*sp.sin(2*sp.pi*xp/L) + xp/ν
ex = -2*ν*sp.diff(sp.ln(1/sp.sqrt(4*sp.pi*ν*t)*
sp.Integral(sp.exp(-(x-xp)**2/4/ν/t - ex),
(xp,-sp.oo, sp.oo))), x)
ex = ex.subs([(ν,0.1),(t,10.0),(L,10.0)]) # make sure these are the same as below
f = sp.lambdify(x, ex)
xe = np.linspace(0,10,100)
ue = np.empty(len(xe))
for i in range(len(xe)):
ue[i] = f(xe[i])
Spectral solution
def spectral(nx):
L = 10.0
ν = 0.1 # use same value as the exact solution above
tend = 10 # use same value as the exact solution above
#---------- solution domain, initial condition
dx = L/nx # not L/(nx-1)
x = np.linspace(0.0, L-dx, nx)
u0 = np.cos(2*np.pi*x/L) + 2
#---------- solve the problem
def rhsf(u, t):
N = len(u)
n = np.arange(N); n[int(N/2)+1:]-= N # n[int(N/2):]-=N
return -u*ifft(2*np.pi*1j*n/L*fft(u)).real - ν*ifft((2*np.pi*n/L)**2*fft(u)).real
t = np.linspace(0,tend,11)
u = odeint(rhsf, u0, t)
return x, u, u0
Plot results
x,u, u0 = spectral(32)
fig,ax=plt.subplots()
ax.plot(x,u0, ':', color='blue', lw=1)
ax.plot(xe,ue, '-', color='red', lw=2)
ax.plot(x,u[-1,:], 'o', color='gray', lw=1)
plt.rc('font', size=14)
plt.gca().set_xlim([0,10])
plt.xlabel('x')
plt.ylabel(r'u');
plt.legend(['initial', 'exact', 'spectral'], frameon=False);

Finite difference solver
def FD(nx, upwind=False):
L = 10.0
ν = 0.1 # use same value as the exact solution above
tend = 10 # use same value as the exact solution above
#---------- solution domain, initial condition
dx = L/nx # not L/(nx-1)
x = np.linspace(0.0, L-dx, nx)
u0 = np.cos(2*np.pi*x/L) + 2
#---------- solve the problem
i = np.arange(nx)
ip = i+1; ip[-1] = 0
im = i-1; im[0] = nx-1
def rhsf(u, t):
if upwind:
return -u*(u - u[im])/dx + ν/dx/dx*(u[im] - 2*u + u[ip]) # upwind on the advective term: 1st order
else:
return -u*(u[ip] - u[im])/2/dx + ν/dx/dx*(u[im] - 2*u + u[ip]) # central difference: 2nd order
t = np.linspace(0,tend,11)
u = odeint(rhsf, u0, t)
return x, u, u0
Compare spectral and finite difference on multiple grids
- The spectral method bottoms out at
80 points, where the average error is 0.4 times less than the second order finite difference method. - For an average relative error of around 4
, the spectral method requires around 13 times fewer grid points (40 versus 512).
nxs = np.array([2,4,6,8,10,14,20,30,45,60,80,128,256,512])
errSP = np.empty(len(nxs))
errFD = np.empty(len(nxs))
errFDUW = np.empty(len(nxs))
for i,nx in enumerate(nxs):
x,usp,u0 = spectral(nx)
x,ufd,u0 = FD(nx, upwind=False)
x,ufduw,u0 = FD(nx, upwind=True)
ue = np.empty(len(x))
for j in range(len(ue)):
ue[j] = f(x[j])
errSP[i] = np.linalg.norm((usp[-1,:]-ue)/ue)/nx
errFD[i] = np.linalg.norm((ufd[-1,:]-ue)/ue)/nx
errFDUW[i] = np.linalg.norm((ufduw[-1,:]-ue)/ue)/nx
#---------- Spectral, and show exponential convergence: fit err=a*exp(b*nx)
plt.loglog(nxs,errSP, 'bo', label="spectral")
b_loga = np.polyfit(nxs[2:11], np.log(errSP[2:11]), 1)
a,b = np.exp(b_loga[1]), b_loga[0]
xf = np.logspace(0,2,100)
plt.loglog(xf, a*np.exp(b*xf), ':', color='blue', label="");
#---------- FD, and show power law convergence: fit err=a*nx^b
plt.loglog(nxs,errFD, 'gs', label="FD central")
b_loga = np.polyfit(np.log(nxs[-4:]), np.log(errFD[-4:]), 1)
a,b = np.exp(b_loga[1]), b_loga[0]
xf = np.logspace(0,3,100)
plt.loglog(xf, a*xf**b, ':', color='green', label="");
plt.loglog(nxs,errFDUW, 'r^', label="FD upwind")
b_loga = np.polyfit(np.log(nxs[-4:]), np.log(errFDUW[-4:]), 1)
a,b = np.exp(b_loga[1]), b_loga[0]
xf = np.logspace(0,3,100)
plt.loglog(xf, a*xf**b, ':', color='red', label="");
#----------
plt.xlabel('# points')
plt.ylabel('Average Relative Error')
plt.xlim([1,1000])
plt.ylim([1E-10,1])
plt.legend(frameon=False);
