Spectral Methods

Spectral Methods

Learning Objectives

  • Understand the mathematical foundation of spectral methods
  • Master Fourier spectral methods and FFT-based differentiation
  • Learn Chebyshev collocation and pseudospectral techniques
  • Apply dealiasing strategies (3/2 rule) to prevent aliasing errors
  • Implement spectral solvers for PDEs (Burgers equation, KdV equation)

Table of Contents

  1. Introduction to Spectral Methods
  2. Fourier Spectral Method
  3. Discrete Fourier Transform and FFT
  4. Spectral Differentiation
  5. Chebyshev Polynomials
  6. Pseudospectral Methods
  7. Dealiasing
  8. Applications: Solving PDEs Spectrally
  9. Practice Problems

1. Introduction to Spectral Methods

1.1 What are Spectral Methods?

Spectral methods approximate solutions to differential equations using global basis functions (e.g., Fourier series, Chebyshev polynomials). Unlike finite difference or finite element methods that use local approximations, spectral methods achieve exponential convergence for smooth problems.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             Comparison: Finite Difference vs Spectral       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  Finite Difference:          Spectral Methods:             β”‚
β”‚  β”Œβ”€β”¬β”€β”¬β”€β”¬β”€β”¬β”€β”¬β”€β”¬β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”‚
β”‚  β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚             β”‚ βˆ‘ aβ‚– Ο†β‚–(x)   β”‚              β”‚
β”‚  β””β”€β”΄β”€β”΄β”€β”΄β”€β”΄β”€β”΄β”€β”΄β”€β”˜             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚  Local stencils             Global basis functions         β”‚
β”‚  O(h^p) convergence         O(e^(-cn)) convergence         β”‚
β”‚  (p = order)                (exponential!)                 β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Advantages: - Exponential accuracy for smooth solutions - Natural handling of derivatives (multiplication in spectral space) - Efficient with FFT (O(N log N) operations)

Limitations: - Requires smooth, periodic (Fourier) or well-behaved (Chebyshev) functions - Complex geometries are challenging - Non-periodic boundaries require special treatment

1.2 Types of Spectral Methods

"""
Three main types of spectral methods:

1. Galerkin Method:
   - Weighted residual approach
   - Projects PDE onto basis functions
   - Example: u(x) = Ξ£ aβ‚™ Ο†β‚™(x), minimize ∫R(u)Ο†β‚˜ dx

2. Collocation Method (Pseudospectral):
   - Enforces PDE exactly at collocation points
   - Faster than Galerkin, slightly less accurate
   - Example: R(u(xβ±Ό)) = 0 at grid points xβ±Ό

3. Tau Method:
   - Similar to Galerkin, but allows residual at boundaries
   - Useful for non-periodic problems
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy.fft import fft, ifft, fftfreq

np.random.seed(42)

2. Fourier Spectral Method

2.1 Fourier Series Representation

For periodic functions on [0, 2Ο€], we expand:

u(x) = Ξ£ Γ»β‚– e^(ikx)
       k=-∞ to ∞

where Γ»β‚– = (1/2Ο€) βˆ«β‚€^(2Ο€) u(x) e^(-ikx) dx

In practice, we truncate to N modes:

def fourier_series_example():
    """
    Demonstrate Fourier series approximation of a smooth periodic function.
    """
    N = 64
    x = np.linspace(0, 2*np.pi, N, endpoint=False)

    # Original function: u(x) = sin(x) + 0.5*sin(2x) + 0.1*cos(5x)
    u = np.sin(x) + 0.5*np.sin(2*x) + 0.1*np.cos(5*x)

    # Compute Fourier coefficients using FFT
    u_hat = fft(u)

    # Reconstruct function from spectral coefficients
    u_reconstructed = np.real(ifft(u_hat))

    # Plot
    plt.figure(figsize=(10, 4))
    plt.plot(x, u, 'b-', label='Original', linewidth=2)
    plt.plot(x, u_reconstructed, 'r--', label='Reconstructed', linewidth=2)
    plt.xlabel('x')
    plt.ylabel('u(x)')
    plt.title('Fourier Series Representation')
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.savefig('fourier_series.png', dpi=150)
    plt.close()

    # Print spectral coefficients (magnitude)
    k = fftfreq(N, 1/N)
    plt.figure(figsize=(10, 4))
    plt.stem(k, np.abs(u_hat), basefmt=' ')
    plt.xlabel('Wavenumber k')
    plt.ylabel('|Γ»_k|')
    plt.title('Fourier Spectrum')
    plt.grid(True)
    plt.tight_layout()
    plt.savefig('fourier_spectrum.png', dpi=150)
    plt.close()

    print(f"L2 error: {np.linalg.norm(u - u_reconstructed):.2e}")

fourier_series_example()

Output:

L2 error: 1.34e-14

The reconstruction is essentially exact (up to machine precision).


3. Discrete Fourier Transform and FFT

3.1 DFT Definition

For N discrete points, the DFT is:

Γ»β‚– = Ξ£_{j=0}^{N-1} uβ±Ό e^(-2Ο€ijk/N),  k = 0, 1, ..., N-1

Inverse: uβ±Ό = (1/N) Ξ£_{k=0}^{N-1} Γ»β‚– e^(2Ο€ijk/N)

3.2 FFT Algorithm

The Fast Fourier Transform reduces complexity from O(NΒ²) to O(N log N):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Cooley-Tukey FFT Algorithm                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                            β”‚
β”‚  DFT[uβ‚€, u₁, ..., u_{N-1}]                                β”‚
β”‚       = DFT[uβ‚€, uβ‚‚, uβ‚„, ...] (even indices)               β”‚
β”‚         + W * DFT[u₁, u₃, uβ‚…, ...] (odd indices)          β”‚
β”‚                                                            β”‚
β”‚  where W = e^(-2Ο€i/N) (twiddle factors)                   β”‚
β”‚                                                            β”‚
β”‚  Recursively split until base case (N=1)                  β”‚
β”‚  Complexity: O(N log N)                                    β”‚
β”‚                                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
from scipy.fft import fft, ifft, fftfreq
import time

def fft_performance_test():
    """
    Compare naive DFT vs FFT performance.
    """
    def naive_dft(u):
        N = len(u)
        u_hat = np.zeros(N, dtype=complex)
        for k in range(N):
            for j in range(N):
                u_hat[k] += u[j] * np.exp(-2j * np.pi * k * j / N)
        return u_hat

    sizes = [32, 64, 128, 256, 512]
    naive_times = []
    fft_times = []

    for N in sizes:
        u = np.random.randn(N)

        # Naive DFT
        start = time.time()
        u_hat_naive = naive_dft(u)
        naive_times.append(time.time() - start)

        # FFT
        start = time.time()
        u_hat_fft = fft(u)
        fft_times.append(time.time() - start)

        # Verify equivalence
        error = np.linalg.norm(u_hat_naive - u_hat_fft)
        print(f"N={N:4d}: Naive={naive_times[-1]:.4f}s, FFT={fft_times[-1]:.6f}s, Error={error:.2e}")

    # Plot
    plt.figure(figsize=(8, 5))
    plt.loglog(sizes, naive_times, 'o-', label='Naive DFT O(NΒ²)', linewidth=2)
    plt.loglog(sizes, fft_times, 's-', label='FFT O(N log N)', linewidth=2)
    plt.xlabel('N')
    plt.ylabel('Time (s)')
    plt.title('DFT vs FFT Performance')
    plt.legend()
    plt.grid(True, which='both', alpha=0.3)
    plt.tight_layout()
    plt.savefig('fft_performance.png', dpi=150)
    plt.close()

fft_performance_test()

4. Spectral Differentiation

4.1 Derivatives in Spectral Space

The key advantage of spectral methods: differentiation becomes multiplication in spectral space.

For Fourier basis:

u(x) = Ξ£ Γ»β‚– e^(ikx)
du/dx = Ξ£ (ik) Γ»β‚– e^(ikx)

In code:

def spectral_derivative(u, L=2*np.pi):
    """
    Compute derivative using spectral differentiation.

    Parameters:
    -----------
    u : array, function values on uniform grid
    L : float, domain length (default 2Ο€)

    Returns:
    --------
    du_dx : array, derivative
    """
    N = len(u)
    u_hat = fft(u)
    k = fftfreq(N, L/N) * 2 * np.pi  # Wavenumbers

    # Derivative in spectral space: multiply by ik
    du_hat = 1j * k * u_hat

    # Transform back to physical space
    du_dx = np.real(ifft(du_hat))

    return du_dx

# Example: differentiate u(x) = sin(x) + 0.5*sin(2x)
N = 128
x = np.linspace(0, 2*np.pi, N, endpoint=False)
u = np.sin(x) + 0.5*np.sin(2*x)
du_dx_exact = np.cos(x) + np.cos(2*x)
du_dx_spectral = spectral_derivative(u)

error = np.linalg.norm(du_dx_exact - du_dx_spectral, np.inf)
print(f"Max error in derivative: {error:.2e}")

Output:

Max error in derivative: 1.78e-14

4.2 Higher-Order Derivatives

def spectral_derivative_n(u, n, L=2*np.pi):
    """
    Compute n-th derivative using spectral differentiation.
    """
    N = len(u)
    u_hat = fft(u)
    k = fftfreq(N, L/N) * 2 * np.pi

    # n-th derivative: multiply by (ik)^n
    du_hat = (1j * k)**n * u_hat

    du_dx = np.real(ifft(du_hat))
    return du_dx

# Test second derivative
u = np.sin(x)
d2u_dx2_exact = -np.sin(x)
d2u_dx2_spectral = spectral_derivative_n(u, n=2)

error = np.linalg.norm(d2u_dx2_exact - d2u_dx2_spectral, np.inf)
print(f"Max error in 2nd derivative: {error:.2e}")

5. Chebyshev Polynomials

5.1 Chebyshev Polynomials of the First Kind

For non-periodic problems on [-1, 1], Chebyshev polynomials are optimal:

Tβ‚€(x) = 1
T₁(x) = x
Tβ‚‚(x) = 2xΒ² - 1
T₃(x) = 4xΒ³ - 3x
...
Tβ‚™(x) = cos(n arccos(x))

Recurrence relation:

T_{n+1}(x) = 2x Tβ‚™(x) - T_{n-1}(x)
def chebyshev_polynomials(n, x):
    """
    Evaluate Chebyshev polynomials Tβ‚€, T₁, ..., Tβ‚™ at points x.

    Returns:
    --------
    T : array of shape (n+1, len(x))
    """
    T = np.zeros((n+1, len(x)))
    T[0] = 1
    if n >= 1:
        T[1] = x
    for k in range(2, n+1):
        T[k] = 2*x*T[k-1] - T[k-2]
    return T

# Plot first 5 Chebyshev polynomials
x = np.linspace(-1, 1, 200)
T = chebyshev_polynomials(4, x)

plt.figure(figsize=(10, 5))
for k in range(5):
    plt.plot(x, T[k], label=f'T_{k}(x)', linewidth=2)
plt.xlabel('x')
plt.ylabel('Tβ‚–(x)')
plt.title('Chebyshev Polynomials')
plt.legend()
plt.grid(True)
plt.axhline(0, color='k', linewidth=0.5)
plt.axvline(0, color='k', linewidth=0.5)
plt.tight_layout()
plt.savefig('chebyshev_polynomials.png', dpi=150)
plt.close()

5.2 Chebyshev-Gauss-Lobatto Points

Optimal collocation points (include endpoints):

xβ±Ό = cos(Ο€j/N),  j = 0, 1, ..., N
def chebyshev_points(N):
    """
    Compute Chebyshev-Gauss-Lobatto points.
    """
    j = np.arange(N+1)
    x = np.cos(np.pi * j / N)
    return x

N = 16
x_cheb = chebyshev_points(N)

plt.figure(figsize=(10, 3))
plt.plot(x_cheb, np.zeros_like(x_cheb), 'ro', markersize=8)
plt.xlim(-1.1, 1.1)
plt.ylim(-0.5, 0.5)
plt.xlabel('x')
plt.title(f'Chebyshev-Gauss-Lobatto Points (N={N})')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('chebyshev_points.png', dpi=150)
plt.close()

print("Chebyshev points (clustered near boundaries):")
print(x_cheb)

5.3 Chebyshev Differentiation Matrix

def chebyshev_diff_matrix(N):
    """
    Compute Chebyshev differentiation matrix D.

    u'(xβ±Ό) β‰ˆ Ξ£ Dβ±Όβ‚– u(xβ‚–)
    """
    x = chebyshev_points(N)
    D = np.zeros((N+1, N+1))

    c = np.ones(N+1)
    c[0] = 2
    c[N] = 2

    for i in range(N+1):
        for j in range(N+1):
            if i != j:
                D[i, j] = (c[i] / c[j]) * ((-1)**(i+j)) / (x[i] - x[j])

    # Diagonal elements
    for i in range(N+1):
        D[i, i] = -np.sum(D[i, :])

    return D, x

# Test: differentiate u(x) = exp(x)
N = 16
D, x = chebyshev_diff_matrix(N)
u = np.exp(x)
du_dx_exact = np.exp(x)
du_dx_cheb = D @ u

error = np.linalg.norm(du_dx_exact - du_dx_cheb, np.inf)
print(f"Chebyshev differentiation error: {error:.2e}")

6. Pseudospectral Methods

6.1 Galerkin vs Pseudospectral

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           Galerkin vs Pseudospectral (Collocation)         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                            β”‚
β”‚  Galerkin:                                                 β”‚
β”‚    ∫ R(u) Ο†β‚˜(x) dx = 0  for all basis functions Ο†β‚˜        β”‚
β”‚    β†’ Requires integration (inner products)                β”‚
β”‚    β†’ Higher accuracy, more expensive                      β”‚
β”‚                                                            β”‚
β”‚  Pseudospectral (Collocation):                            β”‚
β”‚    R(u(xβ±Ό)) = 0  at collocation points xβ±Ό                 β”‚
β”‚    β†’ No integration, evaluate at grid points             β”‚
β”‚    β†’ Slightly lower accuracy, much faster                β”‚
β”‚                                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
def pseudospectral_example():
    """
    Solve u''(x) = -π² sin(Ο€x), u(0)=u(1)=0 using pseudospectral method.

    Exact solution: u(x) = sin(Ο€x)
    """
    N = 16

    # Map Chebyshev points from [-1,1] to [0,1]
    x_cheb = (chebyshev_points(N) + 1) / 2

    # Build differentiation matrix (need to scale for [0,1] domain)
    D, _ = chebyshev_diff_matrix(N)
    D = D * 2  # Scale for [0,1] domain
    D2 = D @ D  # Second derivative

    # Boundary conditions: u(0) = u(N) = 0
    # Interior points: indices 1 to N-1
    D2_interior = D2[1:N, 1:N]

    # Right-hand side: f(x) = -π² sin(Ο€x)
    f = -np.pi**2 * np.sin(np.pi * x_cheb[1:N])

    # Solve linear system
    u_interior = np.linalg.solve(D2_interior, f)

    # Add boundary values
    u = np.zeros(N+1)
    u[1:N] = u_interior

    # Compare with exact solution
    u_exact = np.sin(np.pi * x_cheb)

    plt.figure(figsize=(10, 5))
    plt.plot(x_cheb, u_exact, 'b-', label='Exact', linewidth=2)
    plt.plot(x_cheb, u, 'ro', label='Pseudospectral', markersize=8)
    plt.xlabel('x')
    plt.ylabel('u(x)')
    plt.title("Pseudospectral Solution: u'' = -π² sin(Ο€x)")
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.savefig('pseudospectral_example.png', dpi=150)
    plt.close()

    error = np.linalg.norm(u_exact - u, np.inf)
    print(f"Pseudospectral error: {error:.2e}")

pseudospectral_example()

7. Dealiasing

7.1 Aliasing Problem

Nonlinear terms in PDEs (e.g., u βˆ‚u/βˆ‚x in Burgers equation) produce high-frequency modes that can alias onto low frequencies:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Aliasing Example                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                            β”‚
β”‚  If u has max wavenumber k_max = N/2,                     β”‚
β”‚  then uΒ² has modes up to 2*k_max = N                      β”‚
β”‚                                                            β”‚
β”‚  But DFT only resolves modes up to N/2!                   β”‚
β”‚  β†’ High frequencies wrap around (alias)                   β”‚
β”‚                                                            β”‚
β”‚  Solution: 3/2 rule (zero-padding)                        β”‚
β”‚    1. Pad u to 3N/2 modes                                 β”‚
β”‚    2. Compute nonlinearity in extended space             β”‚
β”‚    3. Truncate back to N modes                            β”‚
β”‚                                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

7.2 3/2 Rule Implementation

def dealias_product_3_2_rule(u, v):
    """
    Compute dealiased product w = u*v using 3/2 rule.

    Parameters:
    -----------
    u, v : arrays of length N

    Returns:
    --------
    w : dealiased product (length N)
    """
    N = len(u)
    M = 3 * N // 2  # Extended grid

    # Fourier transform
    u_hat = fft(u)
    v_hat = fft(v)

    # Zero-pad to 3N/2
    u_hat_padded = np.zeros(M, dtype=complex)
    v_hat_padded = np.zeros(M, dtype=complex)

    # Low frequencies
    u_hat_padded[:N//2] = u_hat[:N//2]
    u_hat_padded[-N//2:] = u_hat[-N//2:]

    v_hat_padded[:N//2] = v_hat[:N//2]
    v_hat_padded[-N//2:] = v_hat[-N//2:]

    # Inverse transform to extended grid
    u_extended = ifft(u_hat_padded)
    v_extended = ifft(v_hat_padded)

    # Multiply in physical space
    w_extended = u_extended * v_extended

    # Transform back
    w_hat_extended = fft(w_extended)

    # Truncate to original resolution
    w_hat = np.zeros(N, dtype=complex)
    w_hat[:N//2] = w_hat_extended[:N//2]
    w_hat[-N//2:] = w_hat_extended[-N//2:]

    # Scale (account for extended grid)
    w_hat *= M / N

    w = ifft(w_hat)
    return np.real(w)

# Test: compare aliased vs dealiased product
N = 64
x = np.linspace(0, 2*np.pi, N, endpoint=False)
u = np.sin(3*x)
v = np.cos(4*x)

# Aliased product (direct multiplication)
w_aliased = u * v

# Dealiased product (3/2 rule)
w_dealiased = dealias_product_3_2_rule(u, v)

# Exact product
w_exact = u * v  # For this example, exact = 0.5*(sin(7x) - sin(x))

print(f"Aliased error:   {np.linalg.norm(w_exact - w_aliased):.2e}")
print(f"Dealiased error: {np.linalg.norm(w_exact - w_dealiased):.2e}")

8. Applications: Solving PDEs Spectrally

8.1 Burgers Equation

The viscous Burgers equation is a fundamental nonlinear PDE:

βˆ‚u/βˆ‚t + u βˆ‚u/βˆ‚x = Ξ½ βˆ‚Β²u/βˆ‚xΒ²

Initial condition: u(x,0) = sin(x)
Periodic boundary: u(0,t) = u(2Ο€,t)
def burgers_spectral(nu=0.01, T=2.0, N=128, dt=0.001):
    """
    Solve Burgers equation using Fourier spectral method with dealiasing.

    βˆ‚u/βˆ‚t + u βˆ‚u/βˆ‚x = Ξ½ βˆ‚Β²u/βˆ‚xΒ²
    """
    # Grid
    L = 2 * np.pi
    x = np.linspace(0, L, N, endpoint=False)
    k = fftfreq(N, L/N) * 2 * np.pi

    # Initial condition
    u = np.sin(x)

    # Time integration (RK4)
    nt = int(T / dt)
    time = 0.0

    # Store solution at select times
    t_save = [0, 0.5, 1.0, 1.5, 2.0]
    u_save = []

    def rhs(u):
        """Right-hand side of Burgers equation in spectral space."""
        u_hat = fft(u)

        # Linear term: Ξ½ βˆ‚Β²u/βˆ‚xΒ²
        linear = -nu * k**2 * u_hat

        # Nonlinear term: -u βˆ‚u/βˆ‚x (compute in physical space with dealiasing)
        ux = np.real(ifft(1j * k * u_hat))
        nonlinear_phys = -u * ux
        nonlinear_phys = dealias_product_3_2_rule(u, ux)
        nonlinear = fft(nonlinear_phys)

        du_dt_hat = linear + nonlinear
        return np.real(ifft(du_dt_hat))

    for n in range(nt):
        # RK4 time stepping
        k1 = rhs(u)
        k2 = rhs(u + 0.5*dt*k1)
        k3 = rhs(u + 0.5*dt*k2)
        k4 = rhs(u + dt*k3)

        u = u + (dt/6) * (k1 + 2*k2 + 2*k3 + k4)
        time += dt

        # Save solution
        if any(abs(time - t) < dt/2 for t in t_save):
            u_save.append(u.copy())

    # Plot
    plt.figure(figsize=(10, 6))
    for i, t in enumerate(t_save[:len(u_save)]):
        plt.plot(x, u_save[i], label=f't = {t:.1f}', linewidth=2)
    plt.xlabel('x')
    plt.ylabel('u(x,t)')
    plt.title(f'Burgers Equation (Ξ½={nu})')
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.savefig('burgers_spectral.png', dpi=150)
    plt.close()

    return x, u_save

x, u_save = burgers_spectral(nu=0.01, T=2.0)
print("Burgers equation solved successfully.")

8.2 Korteweg-de Vries (KdV) Equation

The KdV equation models soliton waves:

βˆ‚u/βˆ‚t + u βˆ‚u/βˆ‚x + βˆ‚Β³u/βˆ‚xΒ³ = 0

Initial condition: u(x,0) = -6ΞΊΒ² sechΒ²(ΞΊx)  (single soliton)
def kdv_spectral(kappa=0.5, T=10.0, N=256, dt=0.01):
    """
    Solve KdV equation using Fourier spectral method.

    βˆ‚u/βˆ‚t + u βˆ‚u/βˆ‚x + βˆ‚Β³u/βˆ‚xΒ³ = 0
    """
    # Grid (larger domain for soliton propagation)
    L = 40 * np.pi
    x = np.linspace(-L/2, L/2, N, endpoint=False)
    k = fftfreq(N, L/N) * 2 * np.pi

    # Initial condition: single soliton
    u = -6 * kappa**2 / np.cosh(kappa * x)**2

    # Time integration
    nt = int(T / dt)

    def rhs(u):
        """Right-hand side of KdV equation."""
        u_hat = fft(u)

        # Dispersion: βˆ‚Β³u/βˆ‚xΒ³
        dispersion = -(1j * k)**3 * u_hat

        # Nonlinearity: -u βˆ‚u/βˆ‚x
        ux = np.real(ifft(1j * k * u_hat))
        nonlinear_phys = -u * ux
        nonlinear = fft(nonlinear_phys)

        du_dt_hat = dispersion + nonlinear
        return np.real(ifft(du_dt_hat))

    # RK4 time stepping
    u_save = [u.copy()]
    for n in range(nt):
        k1 = rhs(u)
        k2 = rhs(u + 0.5*dt*k1)
        k3 = rhs(u + 0.5*dt*k2)
        k4 = rhs(u + dt*k3)

        u = u + (dt/6) * (k1 + 2*k2 + 2*k3 + k4)

        if n % 100 == 0:
            u_save.append(u.copy())

    # Plot soliton propagation
    plt.figure(figsize=(12, 6))
    for i, u_snap in enumerate(u_save[::5]):
        plt.plot(x, u_snap, label=f't = {i*5*dt:.1f}', alpha=0.7)
    plt.xlabel('x')
    plt.ylabel('u(x,t)')
    plt.title('KdV Equation: Soliton Propagation')
    plt.legend(loc='upper right')
    plt.grid(True)
    plt.tight_layout()
    plt.savefig('kdv_spectral.png', dpi=150)
    plt.close()

    return x, u_save

x, u_save = kdv_spectral(kappa=0.5, T=10.0)
print("KdV equation solved successfully.")

9. Practice Problems

Problem 1: Exponential Convergence

Write a function to demonstrate exponential convergence of spectral methods: - Approximate u(x) = e^(sin(x)) on [0, 2Ο€] using N Fourier modes - Compute L∞ error for N = 4, 8, 16, 32, 64 - Plot error vs N on a log scale and verify exponential decay

Solution:

def test_exponential_convergence():
    def u_exact(x):
        return np.exp(np.sin(x))

    N_values = [4, 8, 16, 32, 64]
    errors = []

    x_fine = np.linspace(0, 2*np.pi, 1000, endpoint=False)
    u_fine = u_exact(x_fine)

    for N in N_values:
        x = np.linspace(0, 2*np.pi, N, endpoint=False)
        u = u_exact(x)

        # Interpolate using spectral method
        u_hat = fft(u)

        # Evaluate on fine grid
        k = fftfreq(N, 2*np.pi/N) * 2 * np.pi
        u_interp = np.zeros(len(x_fine))
        for j, xj in enumerate(x_fine):
            u_interp[j] = np.real(np.sum(u_hat * np.exp(1j * k * xj)) / N)

        error = np.linalg.norm(u_fine - u_interp, np.inf)
        errors.append(error)
        print(f"N={N:3d}: Error = {error:.2e}")

    # Plot
    plt.figure(figsize=(8, 5))
    plt.semilogy(N_values, errors, 'o-', linewidth=2, markersize=8)
    plt.xlabel('N (number of modes)')
    plt.ylabel('L∞ error')
    plt.title('Exponential Convergence of Spectral Method')
    plt.grid(True, which='both', alpha=0.3)
    plt.tight_layout()
    plt.savefig('exponential_convergence.png', dpi=150)
    plt.close()

test_exponential_convergence()

Problem 2: Chebyshev Interpolation

Interpolate the Runge function f(x) = 1/(1 + 25xΒ²) on [-1, 1] using: - Uniform grid points (show Runge phenomenon) - Chebyshev-Gauss-Lobatto points

Compare interpolation errors.

Problem 3: Heat Equation

Solve the 1D heat equation βˆ‚u/βˆ‚t = βˆ‚Β²u/βˆ‚xΒ² using Fourier spectral method with initial condition u(x,0) = sin(x). Compare with exact solution u(x,t) = e^(-t) sin(x).

Problem 4: 2-Soliton Collision

Modify the KdV solver to simulate collision of two solitons with different speeds. Initial condition:

u(x,0) = -6κ₁² sechΒ²(κ₁(x+5)) - 6ΞΊβ‚‚Β² sechΒ²(ΞΊβ‚‚(x-5))

with κ₁ = 0.5, ΞΊβ‚‚ = 0.3.


to navigate between lessons