15  Week 15 – Spectral (Fourier) Methods for Linear Advection

Course: Numerical Analysis
Topic: Fourier/Spectral Methods on Periodic Domains

15.1 Learning Objectives

By the end of this notebook, students should be able to:

  • Represent a function on a periodic domain using a discrete Fourier transform (DFT).
  • Use the FFT to compute Fourier coefficients and reconstruct the function.
  • Implement a spectral time-stepping method for the linear advection equation.
  • Compare spectral and finite-volume/finite-difference solutions in terms of accuracy and dispersion.
  • Observe aliasing and resolution effects for high-frequency initial data.

15.2 1. Linear Advection on a Periodic Domain

We consider again \[u_t + c\,u_x = 0\] on a periodic domain \(x \in [0,L]\).

The exact solution is a translation: \[u(x,t) = u_0(x - ct).\]

Spectral methods take advantage of periodicity by expanding \(u\) in a Fourier series.

15.3 2. Fourier Series and Discrete Fourier Transform

On a periodic domain with period \(L\), a sufficiently smooth function \(u(x,t)\) can be expanded as \[ u(x,t) = \sum_{k=-\infty}^{\infty} \hat u_k(t) e^{i k \kappa x}, \quad \kappa = \frac{2\pi}{L}. \]

In a discrete setting, with \(N\) grid points \(x_j = j\,\Delta x\) and \(\Delta x = L/N\), we approximate this with a finite sum over wavenumbers \(k\): \[u_j \approx \sum_k \hat u_k e^{i k \kappa x_j}.\]

The discrete Fourier transform (DFT) (implemented in NumPy by np.fft.fft) maps the physical values \(u_j\) to complex Fourier coefficients \(\hat u_k\).

import numpy as np
import matplotlib.pyplot as plt

L = 1.0
N = 256  # use a power of 2 for efficient FFT
dx = L / N
x = np.linspace(0, L, N, endpoint=False)

c = 1.0

def initial_condition(x):
    return np.exp(-100.0 * (x - 0.3)**2)

u0 = initial_condition(x)

plt.figure(figsize=(6, 3))
plt.plot(x, u0)
plt.xlabel("x")
plt.ylabel("u")
plt.title("Initial Condition (Gaussian, Periodic)")
plt.grid(True)
plt.show()

15.3.1 2.1. Computing the DFT and Inverse DFT

NumPy provides:

  • np.fft.fft(u) – computes the DFT \(\hat u_k\) from physical values \(u_j\).
  • np.fft.ifft(uhat) – computes the inverse DFT from \(\hat u_k\) back to physical space.

The DFT indices correspond to wavenumbers \(k\), which we can obtain using np.fft.fftfreq.

uhat0 = np.fft.fft(u0)

k_freq = np.fft.fftfreq(N, d=dx)  # cycles per unit length
k = 2 * np.pi * k_freq            # angular wavenumbers

plt.figure(figsize=(6, 3))
markerline, stemlines, baseline = plt.stem(k, np.abs(uhat0), basefmt=" ")
plt.xlim(-60, 60)
plt.xlabel("wavenumber k")
plt.ylabel("|u_hat(k)|")
plt.title("Magnitude of Fourier Coefficients of Initial Condition")
plt.grid(True)
plt.show()

Most of the energy is concentrated at low wavenumbers because the Gaussian is smooth.

15.4 3. Spectral Solution of Linear Advection

Exercise: Substitute the Fourier series into the PDE, and solve the resulting ODE.

Solution:

15.4.1 3.1. Spectral Time Stepping

Given \(u_j^0\):

  1. Compute \(\hat u_k(0) = \mathcal{F}[u_j^0]\) using FFT.
  2. For each time step of size \(\Delta t\), update \[\hat u_k^{n+1} = \hat u_k^{n} \exp(-i c k \Delta t).\]
  3. Transform back to physical space using inverse FFT to visualize \(u_j^n\).

This method is spectrally accurate in space (for smooth periodic solutions) and exact in time for the linear advection PDE, assuming no aliasing issues.

def spectral_advection(u0, c, dx, dt, nt):
    '''
    Spectral solution of u_t + c u_x = 0 on a periodic domain.
    Uses exact evolution in Fourier space.
    '''
    N = ...
    x = ...
    u = ...
    history = ...
    
    # Precompute wavenumbers
    k_freq = np.fft.fftfreq(N, d=dx)
    k = 2 * np.pi * k_freq
    
    # Initial Fourier coefficients
    uhat = ...
    
    # Precompute phase factor for one time step
    phase = ...
    
    for n in range(nt):
        uhat = ...
        u = ...
        history.append(u.copy())
    return np.array(history)

Tfinal = 0.5
lambda_target = 0.8
dt = lambda_target * dx / c
nt = int(Tfinal / dt)

history_spectral = spectral_advection(u0, c, dx, dt, nt)

plt.figure(figsize=(6, 3))
plt.plot(x, u0, label="t=0")
plt.plot(x, history_spectral[nt//2], label=f"t={dt*(nt//2):.3f}")
plt.plot(x, history_spectral[-1], label=f"t={dt*nt:.3f}")
plt.xlabel("x")
plt.ylabel("u")
plt.title("Spectral Solution of Linear Advection")
plt.legend()
plt.grid(True)
plt.show()

15.4.2 Comparison with Exact Solution

Recall the exact solution is a translation: \[u_{\text{exact}}(x,t) = u_0(x - c t).\]

We can construct this and compare it with the spectral solution numerically.

def exact_solution(x, t, c):
    # shift x by ct with periodic wrap-around
    return initial_condition((x - c * t) % L)

u_exact = exact_solution(x, Tfinal, c)
u_spec_final = history_spectral[-1]

plt.figure(figsize=(6, 3))
plt.plot(x, u_exact, label="Exact at T")
plt.plot(x, u_spec_final, '--', label="Spectral at T")
plt.xlabel("x")
plt.ylabel("u")
plt.title("Spectral vs Exact Solution")
plt.legend()
plt.grid(True)
plt.show()

error_l2 = np.sqrt(np.mean((u_spec_final - u_exact)**2))
print(f"L2 error between spectral and exact solution: {error_l2:.2e}")

For this smooth initial condition and reasonable resolution, the spectral method should be extremely accurate.

15.5 4. Try It 1 – Compare Spectral and Finite Volume Solutions

We now compare the spectral method with the finite volume upwind method from Week 14.

15.5.1 Task

  1. Implement or reuse the FV upwind scheme for the same grid and time step.
  2. Compute the FV upwind solution at the same final time.
  3. Plot:
    • Exact solution
    • Spectral solution
    • FV upwind solution
  4. Compare:
    • Phase accuracy (location of the bump)
    • Amplitude and shape (numerical diffusion)
# TRY IT 1: Compare spectral and FV upwind solutions

def fv_upwind(u0, c, dx, dt, nt):
    ...
# TODO:
# 1. Compute history_fv = fv_upwind(u0, c, dx, dt, nt).
# 2. At final time, plot u_exact, spectral solution, and FV solution on the same figure.
# 3. Comment on which method is more accurate and how the FV solution differs.

15.6 5. Try It 2 – High-Frequency Initial Data and Aliasing

Spectral methods rely on resolving the relevant frequencies. If the initial condition contains frequencies near or beyond the Nyquist limit, we see aliasing.

15.6.1 Task

  1. Define a high-frequency initial condition, for example \[u_0(x) = \sin(20\pi x).\]
  2. Repeat the spectral advection experiment to final time \(T\).
  3. Try different values of \(N\) (for example 64, 128, 256) and compare:
    • How well do we resolve the wave?
    • Do you see aliasing or strange patterns?
# TRY IT 2: High-frequency initial condition

def high_freq_ic(x):
    return np.sin(20.0 * np.pi * x)

# TODO:
# 1. Set u0_hf = high_freq_ic(x).
# 2. Run spectral_advection with this initial condition.
# 3. Plot the solution and the exact translated wave.
# 4. Experiment with changing N and observe what happens.

15.7 6. Summary: FD / FV / Spectral Methods for Linear Advection

We have now seen three perspectives on numerically solving the linear advection equation on a periodic domain:

Method Viewpoint Spatial accuracy (for smooth data) Conservation Complexity
FTBS (FD) Local derivative First order Approximate Simple
FV Upwind Flux balance First order Conservative Moderate
Spectral Global expansion Spectral (very high) Conservative (for linear advection) Higher (FFT)

Key takeaways:

  • Finite difference and finite volume methods are local and easy to generalize to complex geometries and non-linear PDEs.
  • Spectral methods exploit global smoothness and periodicity to achieve very high accuracy but require more structure (for example periodic domains) and care with aliasing.
  • For smooth, periodic problems like linear advection, spectral methods can be extremely efficient and accurate.

15.8 7. Reflection Questions

  1. In what settings would you prefer a finite volume method over a spectral method, and why?
  2. How does the concept of conservation appear in each method?
  3. How would you expect these methods to behave for non-smooth initial data (for example discontinuities)?

Response: