17. Spectral and Advanced Methods

17. Spectral and Advanced Methods

Learning Objectives

By the end of this lesson, you will be able to:

  • Understand the principles of pseudo-spectral methods for MHD
  • Implement Fourier-based spectral solvers for periodic MHD problems
  • Apply dealiasing techniques to handle nonlinear terms correctly
  • Use Chebyshev spectral methods for non-periodic boundary value problems
  • Understand Adaptive Mesh Refinement (AMR) for MHD
  • Describe hybrid MHD-PIC methods for kinetic effects
  • Compare SPH-MHD with grid-based methods
  • Survey production MHD codes: Athena++, PLUTO, FLASH, Pencil Code, Dedalus
  • Implement a simplified 2D pseudo-spectral MHD solver in Python

1. Introduction to Spectral Methods

1.1 Why Spectral Methods?

Finite difference and finite volume methods offer: - Accuracy: $O(\Delta x^2)$ to $O(\Delta x^5)$ (WENO) - Flexibility: Complex geometries, AMR, shocks

Spectral methods offer: - Accuracy: Exponential convergence for smooth solutions - Efficiency: Exact derivatives via FFT ($O(N \log N)$) - Simplicity: Natural for incompressible turbulence, dynamos

Trade-offs: | Method | Convergence | Shocks | BC | Geometries | |--------|-------------|--------|-----|------------| | Finite Volume | Algebraic | Excellent | Flexible | Any | | Spectral (Fourier) | Exponential | Poor (Gibbs) | Periodic only | Simple | | Spectral (Chebyshev) | Exponential | Poor | Flexible | 1D/2D slab |

Best use cases: - MHD turbulence (periodic box) - Dynamo simulations (high Reynolds numbers) - Linear stability analysis (eigenmodes)

1.2 Fourier vs Chebyshev Basis

Fourier: $$ f(x) = \sum_{k=-N/2}^{N/2-1} \hat{f}_k e^{i k x} $$ - Periodic BC: $f(0) = f(2\pi)$ - Derivatives: $\partial_x f \leftrightarrow i k \hat{f}_k$ - Orthogonality: $\int_0^{2\pi} e^{i k x} e^{-i k' x} dx = 2\pi \delta_{kk'}$

Chebyshev: $$ f(x) = \sum_{n=0}^{N} a_n T_n(x), \quad T_n(x) = \cos(n \arccos x), \quad x \in [-1, 1] $$ - Non-periodic BC: Dirichlet, Neumann, mixed - Derivatives: recurrence relations (more complex than Fourier) - Clustering: Chebyshev points cluster near boundaries (resolves boundary layers)


2. Pseudo-Spectral Methods for MHD

2.1 Basic Algorithm

Consider incompressible MHD in a periodic box: $$ \frac{\partial \mathbf{v}}{\partial t} = -(\mathbf{v} \cdot \nabla)\mathbf{v} + (\mathbf{B} \cdot \nabla)\mathbf{B} - \nabla p + \nu \nabla^2 \mathbf{v} $$ $$ \frac{\partial \mathbf{B}}{\partial t} = \nabla \times (\mathbf{v} \times \mathbf{B}) + \eta \nabla^2 \mathbf{B} $$ $$ \nabla \cdot \mathbf{v} = 0, \quad \nabla \cdot \mathbf{B} = 0 $$

Pseudo-spectral scheme:

  1. Initialization: FFT of $\mathbf{v}(\mathbf{x}, 0)$ and $\mathbf{B}(\mathbf{x}, 0)$ β†’ $\hat{\mathbf{v}}(\mathbf{k}, 0)$, $\hat{\mathbf{B}}(\mathbf{k}, 0)$

  2. Time loop:

  3. Nonlinear terms: Compute $(\mathbf{v} \cdot \nabla)\mathbf{v}$ and $\mathbf{v} \times \mathbf{B}$ in physical space

    • IFFT: $\hat{\mathbf{v}}(\mathbf{k}) \to \mathbf{v}(\mathbf{x})$, $\hat{\mathbf{B}}(\mathbf{k}) \to \mathbf{B}(\mathbf{x})$
    • Compute: $\mathbf{NL}_v = -(\mathbf{v} \cdot \nabla)\mathbf{v} + (\mathbf{B} \cdot \nabla)\mathbf{B}$, $\mathbf{NL}_B = \nabla \times (\mathbf{v} \times \mathbf{B})$
    • FFT: $\mathbf{NL}_v(\mathbf{x}) \to \widehat{\mathbf{NL}}_v(\mathbf{k})$, $\mathbf{NL}_B(\mathbf{x}) \to \widehat{\mathbf{NL}}_B(\mathbf{k})$
  4. Linear terms: Compute in spectral space

    • Diffusion: $\widehat{\nabla^2 \mathbf{v}} = -k^2 \hat{\mathbf{v}}$
    • Pressure: Project $\widehat{\mathbf{NL}}_v$ to divergence-free subspace $$ \hat{\mathbf{v}}_{\perp}(\mathbf{k}) = \hat{\mathbf{v}}(\mathbf{k}) - \frac{\mathbf{k} \cdot \hat{\mathbf{v}}(\mathbf{k})}{k^2} \mathbf{k} $$
  5. Time integration: RK4 or exponential integrator $$ \frac{d \hat{\mathbf{v}}}{dt} = \widehat{\mathbf{NL}}_v - \nu k^2 \hat{\mathbf{v}} $$

  6. Divergence cleaning: Enforce $\nabla \cdot \mathbf{B} = 0$ at each step: $$ \hat{\mathbf{B}}_{\perp}(\mathbf{k}) = \hat{\mathbf{B}}(\mathbf{k}) - \frac{\mathbf{k} \cdot \hat{\mathbf{B}}(\mathbf{k})}{k^2} \mathbf{k} $$

2.2 Dealiasing (2/3 Rule)

Problem: Aliasing errors from nonlinear terms.

Product of two functions: $$ f(x) g(x) \quad \text{with max wavenumber } k_{\max} $$ has Fourier modes up to $2 k_{\max}$ (convolution theorem).

If grid has $N$ points, Nyquist wavenumber $k_N = N/2$. Modes $k > k_N$ alias to lower $k$.

Solution: 2/3 dealiasing (Orszag 1971) - Keep only modes $|k| \leq k_{\max} = 2N/3$ - Zero out modes $|k| > 2N/3$ before IFFT - After computing nonlinear term, zero high-$k$ modes again

Cost: Lose 1/3 of modes, but exact representation of quadratic nonlinearity.

2.3 Time Integration

Explicit RK4: $$ \frac{d \hat{u}}{dt} = L \hat{u} + N(\hat{u}) $$ where $L = -\nu k^2$ (linear), $N$ (nonlinear).

Standard RK4: $$ \begin{aligned} k_1 &= L \hat{u}^n + N(\hat{u}^n) \\ k_2 &= L(\hat{u}^n + \frac{\Delta t}{2} k_1) + N(\hat{u}^n + \frac{\Delta t}{2} k_1) \\ k_3 &= L(\hat{u}^n + \frac{\Delta t}{2} k_2) + N(\hat{u}^n + \frac{\Delta t}{2} k_2) \\ k_4 &= L(\hat{u}^n + \Delta t k_3) + N(\hat{u}^n + \Delta t k_3) \\ \hat{u}^{n+1} &= \hat{u}^n + \frac{\Delta t}{6}(k_1 + 2k_2 + 2k_3 + k_4) \end{aligned} $$

Exponential integrator (ETDRK4):

For stiff linear terms ($\nu k^2$ large at high $k$): $$ \hat{u}^{n+1} = e^{L \Delta t} \hat{u}^n + \Delta t \phi_1(L \Delta t) N(\hat{u}^n) + \ldots $$ where $\phi_1(z) = (e^z - 1)/z$.

Advantage: Unconditionally stable for diffusion (allows larger $\Delta t$).


3. Incompressible MHD Turbulence

3.1 Governing Equations (Dimensionless)

$$ \frac{\partial \mathbf{v}}{\partial t} + (\mathbf{v} \cdot \nabla)\mathbf{v} = -\nabla p + (\mathbf{B} \cdot \nabla)\mathbf{B} + \frac{1}{Re} \nabla^2 \mathbf{v} + \mathbf{f} $$ $$ \frac{\partial \mathbf{B}}{\partial t} = \nabla \times (\mathbf{v} \times \mathbf{B}) + \frac{1}{Rm} \nabla^2 \mathbf{B} $$ $$ \nabla \cdot \mathbf{v} = 0, \quad \nabla \cdot \mathbf{B} = 0 $$

Parameters: - $Re = U L / \nu$ (Reynolds number) - $Rm = U L / \eta$ (magnetic Reynolds number) - $\mathbf{f}$ = large-scale forcing (maintain turbulence)

3.2 Energy Spectra

Kinetic energy spectrum: $$ E_K(k) = \frac{1}{2} \sum_{|\mathbf{k}'| \in [k, k+dk]} |\hat{\mathbf{v}}(\mathbf{k}')|^2 $$

Magnetic energy spectrum: $$ E_M(k) = \frac{1}{2} \sum_{|\mathbf{k}'| \in [k, k+dk]} |\hat{\mathbf{B}}(\mathbf{k}')|^2 $$

Total energy: $$ E_{\text{total}} = \int E_K(k) dk + \int E_M(k) dk $$

Kolmogorov spectrum (hydrodynamic turbulence): $$ E(k) \propto k^{-5/3} $$

Iroshnikov-Kraichnan spectrum (MHD turbulence): $$ E(k) \propto k^{-3/2} $$ (Debated; modern DNS shows $k^{-5/3}$ for strong $B_0$, $k^{-3/2}$ for weak $B_0$)

3.3 Invariants

Ideal MHD conserves: - Total energy: $E = \int (\mathbf{v}^2 + \mathbf{B}^2)/2 \, d^3x$ - Magnetic helicity: $H_M = \int \mathbf{A} \cdot \mathbf{B} \, d^3x$ (for $\eta = 0$) - Cross-helicity: $H_C = \int \mathbf{v} \cdot \mathbf{B} \, d^3x$

Spectral method: Monitor these to verify energy conservation (check for numerical dissipation).


4. Chebyshev Spectral Methods

4.1 Chebyshev Polynomials

Definition: $$ T_n(x) = \cos(n \arccos x), \quad x \in [-1, 1] $$

Recurrence: $$ T_0(x) = 1, \quad T_1(x) = x, \quad T_{n+1}(x) = 2x T_n(x) - T_{n-1}(x) $$

Orthogonality: $$ \int_{-1}^{1} \frac{T_m(x) T_n(x)}{\sqrt{1-x^2}} dx = \begin{cases} 0 & m \neq n \\ \pi/2 & m = n \neq 0 \\ \pi & n = 0 \end{cases} $$

Collocation points (Gauss-Lobatto): $$ x_j = \cos\left(\frac{\pi j}{N}\right), \quad j = 0, 1, \ldots, N $$

4.2 Differentiation Matrices

Approximate: $$ f(x) \approx \sum_{n=0}^N a_n T_n(x) $$

Derivative: $$ \frac{df}{dx}(x_i) = \sum_{j=0}^N D_{ij} f(x_j) $$

where $D_{ij}$ is the Chebyshev differentiation matrix (computed once, dense matrix).

Second derivative: $$ \frac{d^2 f}{dx^2}(x_i) = \sum_{j=0}^N D^{(2)}_{ij} f(x_j) $$

4.3 Application: Grad-Shafranov Solver

Axisymmetric MHD equilibrium: $$ \nabla^2 \psi = -\mu_0 R^2 \frac{dp}{d\psi} - \frac{1}{2} \frac{dF^2}{d\psi} $$ where $\psi(R, Z)$ is the poloidal flux function.

Chebyshev discretization: - Map $(R, Z) \in [R_{\min}, R_{\max}] \times [Z_{\min}, Z_{\max}]$ to $[-1, 1]^2$ - Expand $\psi$ on Chebyshev grid - Compute $\nabla^2 \psi$ via differentiation matrices - Solve nonlinear BVP (Newton iteration)

Boundary conditions: - Dirichlet: $\psi = 0$ on wall - Modify first/last rows of $D^{(2)}$


5. Adaptive Mesh Refinement (AMR)

5.1 Block-Structured AMR

Idea: Refine grid where gradients large (shocks, current sheets), coarsen elsewhere.

Hierarchy:

Level 0: Coarse grid (base)
Level 1: Refined patches (2x resolution)
Level 2: Doubly refined (4x resolution)
...

Data structure: - Tree of nested grids - Each level covers subset of domain - Ghost cells for inter-level communication

5.2 Refinement Criteria

Gradient-based: $$ \text{Refine if } \frac{|\nabla \rho|}{\rho} > \epsilon_{\text{refine}} $$

Current density: $$ \text{Refine if } |\mathbf{J}| > J_{\text{thresh}} $$

LΓΆhner estimator: $$ \mathcal{E} = \frac{\sum_i |\Delta u_i|}{\sum_i |u_i| + \delta} $$ where $\Delta u_i = u_{i+1} - 2u_i + u_{i-1}$ (2nd difference).

5.3 Prolongation and Restriction

Prolongation (coarse β†’ fine): - Interpolate coarse cell values to fine cells - Piecewise linear or cubic interpolation

Restriction (fine β†’ coarse): - Average fine cells: $$ U_{\text{coarse}} = \frac{1}{4} \sum_{\text{fine cells}} U_{\text{fine}} $$ (2D; factor 8 in 3D)

Conservative restriction: $$ U_{\text{coarse}} A_{\text{coarse}} = \sum_{\text{fine}} U_{\text{fine}} A_{\text{fine}} $$ ensures mass/energy conservation.

5.4 Flux Correction

Problem: Fine/coarse interface sees flux mismatch.

Flux-Correction Algorithm: 1. Compute fluxes at fine level 2. Sum fine fluxes at coarse boundary 3. Replace coarse flux with summed fine flux 4. Correct conserved quantities

Software: Paramesh, CHOMBO, AMReX.


6. Hybrid MHD-PIC Methods

6.1 Motivation

Pure MHD: Fluid description, misses kinetic effects (Landau damping, wave-particle interaction).

Pure PIC: Fully kinetic, prohibitively expensive for large-scale astrophysical systems.

Hybrid approach: - Bulk plasma: MHD (ions + electrons as single fluid) - Energetic particles: PIC (cosmic rays, SEPs, suprathermal populations)

6.2 Coupling

MHD β†’ Particles: - Particles experience Lorentz force: $$ \frac{d\mathbf{p}_i}{dt} = q_i (\mathbf{E} + \mathbf{v}_i \times \mathbf{B}) $$ where $\mathbf{E}$, $\mathbf{B}$ from MHD solution.

Particles β†’ MHD: - Particles contribute pressure/momentum: $$ p_{\text{CR}} = \sum_i \frac{p_i^2}{3m_i}, \quad \mathbf{f}_{\text{CR}} = -\nabla p_{\text{CR}} $$ - Add $\mathbf{f}_{\text{CR}}$ to momentum equation: $$ \frac{\partial (\rho \mathbf{v})}{\partial t} + \ldots = \ldots + \mathbf{f}_{\text{CR}} $$

6.3 Applications

  • Cosmic ray transport: Diffusion, streaming instability
  • Solar energetic particles: Acceleration at shocks
  • Planetary magnetospheres: Ring current particles

7. SPH-MHD

7.1 Smoothed Particle Hydrodynamics (SPH)

Lagrangian method: - Fluid represented by particles (mass elements) - Properties smoothed over kernel: $$ f(\mathbf{x}) = \sum_j m_j \frac{f_j}{\rho_j} W(|\mathbf{x} - \mathbf{x}_j|, h) $$ where $W$ is smoothing kernel (e.g., cubic spline), $h$ = smoothing length.

Equations of motion: $$ \frac{d\mathbf{v}_i}{dt} = -\sum_j m_j \left( \frac{p_i}{\rho_i^2} + \frac{p_j}{\rho_j^2} \right) \nabla_i W_{ij} $$

7.2 MHD in SPH

Challenge: Magnetic field is Eulerian (grid-based), but SPH is Lagrangian.

Approaches: 1. Conservative formulation: Evolve $\mathbf{B}$ on particles, use $\nabla \cdot \mathbf{B} = 0$ cleaning 2. Vector potential: Evolve $\mathbf{A}$, compute $\mathbf{B} = \nabla \times \mathbf{A}$ (guarantees $\nabla \cdot \mathbf{B} = 0$)

Tensile instability: - Particle clumping in magnetic pressure gradient regions - Mitigation: artificial viscosity, improved kernels

7.3 Applications

  • Star formation: Collapse of magnetized molecular clouds
  • Protoplanetary disks: MRI, disk-planet interaction
  • Galaxy evolution: Cosmological simulations with B-fields

Pros: - Naturally Lagrangian (follows fluid) - No advection errors - Good for large density contrasts

Cons: - Low-order accuracy (SPH kernels $\sim O(h^2)$) - $\nabla \cdot \mathbf{B}$ errors accumulate - Expensive neighbor search


8. Production MHD Codes

8.1 Athena++

Type: Grid-based, finite volume

Features: - MHD, SRMHD, radiation MHD - Static mesh refinement (SMR) and AMR - Curvilinear coordinates (spherical, cylindrical) - Constrained transport (CT) for $\nabla \cdot \mathbf{B} = 0$ - Multi-physics: dust, cosmic rays, chemistry

Riemann solvers: HLLC, HLLD, Roe

Use cases: - Accretion disks (MRI turbulence) - Protostellar jets - Galaxy cluster simulations

Website: https://www.athena-astro.app/

8.2 PLUTO

Type: Grid-based, finite volume

Features: - MHD, RMHD, RHD, HD - Modular physics: cooling, thermal conduction, particles - Multiple Riemann solvers - Adaptive grids (CHOMBO AMR) - User-friendly setup (pluto.ini config file)

Coordinates: Cartesian, cylindrical, spherical, curvilinear

Use cases: - Astrophysical jets - Stellar winds - Planet-disk interaction

Website: http://plutocode.ph.unito.it/

8.3 FLASH

Type: Grid-based, AMR (Paramesh/CHOMBO)

Features: - Compressible MHD, radiation hydro - Nuclear burning (stellar evolution) - Gravity (Poisson solver, tree code) - Laser energy deposition (ICF)

Best for: Multi-physics simulations (SNe, ICF, stellar interiors)

Website: https://flash.rochester.edu/

8.4 Pencil Code

Type: Finite difference, high-order (6th order spatial)

Features: - Spectral-like accuracy on Cartesian grids - MHD turbulence, dynamos - Non-ideal effects: ambipolar diffusion, Hall effect - Dust, chemistry, radiation

Best for: - Direct numerical simulation (DNS) of turbulence - Large $Re$, $Rm$ studies - Dynamo theory

Website: https://github.com/pencil-code/

8.5 Dedalus

Type: Spectral (Fourier + Chebyshev)

Features: - Flexible PDE solver (user specifies equations symbolically) - Parallelized (MPI + efficient eigensolvers) - Time-stepping: SBDF, RK443 - Linear stability analysis (eigenvalue problems)

Best for: - Convection, dynamos in spherical shells - Stability analysis (MRI, MHD instabilities) - Custom PDEs

Website: https://dedalus-project.org/


9. Python Implementation: 2D Pseudo-Spectral MHD

9.1 Problem Setup

Simulate 2D incompressible MHD turbulence with forcing: $$ \frac{\partial \mathbf{v}}{\partial t} = -(\mathbf{v} \cdot \nabla)\mathbf{v} + (\mathbf{B} \cdot \nabla)\mathbf{B} - \nabla p + \nu \nabla^2 \mathbf{v} + \mathbf{f} $$ $$ \frac{\partial \mathbf{B}}{\partial t} = \nabla \times (\mathbf{v} \times \mathbf{B}) + \eta \nabla^2 \mathbf{B} $$

Domain: $[0, 2\pi]^2$, periodic BC, $N_x = N_y = 128$.

9.2 Code

import numpy as np
import matplotlib.pyplot as plt
from numpy.fft import fftn, ifftn, fftfreq

# Parameters
N = 128          # Grid points
L = 2.0 * np.pi  # Domain size
nu = 1e-3        # Viscosity
eta = 1e-3       # Resistivity
dt = 0.001       # Timestep
T_end = 1.0      # End time
forcing_amp = 0.1

# Grid
x = np.linspace(0, L, N, endpoint=False)
y = np.linspace(0, L, N, endpoint=False)
X, Y = np.meshgrid(x, y)

# Wavenumbers
kx = fftfreq(N, L/(2*np.pi*N)) * 2*np.pi
ky = fftfreq(N, L/(2*np.pi*N)) * 2*np.pi
KX, KY = np.meshgrid(kx, ky)
K2 = KX**2 + KY**2
K2[0, 0] = 1.0  # Avoid division by zero

# Dealiasing mask (2/3 rule)
kmax = N // 3
dealias = (np.abs(KX) <= kmax) & (np.abs(KY) <= kmax)

# Initial conditions (random vorticity + magnetic field)
np.random.seed(42)
vx = np.random.randn(N, N) * 0.1
vy = np.random.randn(N, N) * 0.1
Bx = np.sin(2*X) * np.cos(Y)
By = -np.cos(X) * np.sin(2*Y)

# Enforce incompressibility
vx_hat = fftn(vx)
vy_hat = fftn(vy)
div_v = 1j*KX*vx_hat + 1j*KY*vy_hat
vx_hat -= 1j*KX*div_v/K2
vy_hat -= 1j*KY*div_v/K2
vx = np.real(ifftn(vx_hat))
vy = np.real(ifftn(vy_hat))

Bx_hat = fftn(Bx)
By_hat = fftn(By)
div_B = 1j*KX*Bx_hat + 1j*KY*By_hat
Bx_hat -= 1j*KX*div_B/K2
By_hat -= 1j*KY*div_B/K2
Bx = np.real(ifftn(Bx_hat))
By = np.real(ifftn(By_hat))

def compute_nonlinear(vx, vy, Bx, By):
    """Compute nonlinear terms in physical space."""
    # Velocity advection
    vx_hat = fftn(vx)
    vy_hat = fftn(vy)
    dvx_dx = np.real(ifftn(1j*KX*vx_hat))
    dvx_dy = np.real(ifftn(1j*KY*vx_hat))
    dvy_dx = np.real(ifftn(1j*KX*vy_hat))
    dvy_dy = np.real(ifftn(1j*KY*vy_hat))

    NL_vx = -(vx*dvx_dx + vy*dvx_dy)
    NL_vy = -(vx*dvy_dx + vy*dvy_dy)

    # Magnetic tension
    Bx_hat = fftn(Bx)
    By_hat = fftn(By)
    dBx_dx = np.real(ifftn(1j*KX*Bx_hat))
    dBx_dy = np.real(ifftn(1j*KY*Bx_hat))
    dBy_dx = np.real(ifftn(1j*KX*By_hat))
    dBy_dy = np.real(ifftn(1j*KY*By_hat))

    NL_vx += Bx*dBx_dx + By*dBx_dy
    NL_vy += Bx*dBy_dx + By*dBy_dy

    # Induction equation
    NL_Bx = vx*dBx_dx + vy*dBx_dy - Bx*dvx_dx - By*dvx_dy
    NL_By = vx*dBy_dx + vy*dBy_dy - Bx*dvy_dx - By*dvy_dy

    return NL_vx, NL_vy, NL_Bx, NL_By

def project_divergence_free(fx_hat, fy_hat):
    """Project to divergence-free subspace."""
    div_f = 1j*KX*fx_hat + 1j*KY*fy_hat
    fx_hat -= 1j*KX*div_f/K2
    fy_hat -= 1j*KY*div_f/K2
    return fx_hat, fy_hat

def rhs(vx, vy, Bx, By, t):
    """Compute RHS of equations."""
    NL_vx, NL_vy, NL_Bx, NL_By = compute_nonlinear(vx, vy, Bx, By)

    # Add forcing (low wavenumber)
    forcing_vx = forcing_amp * np.sin(2*X + t) * np.cos(Y)
    forcing_vy = forcing_amp * np.cos(X) * np.sin(2*Y + t)
    NL_vx += forcing_vx
    NL_vy += forcing_vy

    # FFT
    NL_vx_hat = fftn(NL_vx) * dealias
    NL_vy_hat = fftn(NL_vy) * dealias
    NL_Bx_hat = fftn(NL_Bx) * dealias
    NL_By_hat = fftn(NL_By) * dealias

    # Project to divergence-free
    NL_vx_hat, NL_vy_hat = project_divergence_free(NL_vx_hat, NL_vy_hat)
    NL_Bx_hat, NL_By_hat = project_divergence_free(NL_Bx_hat, NL_By_hat)

    # Add diffusion
    vx_hat = fftn(vx)
    vy_hat = fftn(vy)
    Bx_hat = fftn(Bx)
    By_hat = fftn(By)

    dvx_dt_hat = NL_vx_hat - nu*K2*vx_hat
    dvy_dt_hat = NL_vy_hat - nu*K2*vy_hat
    dBx_dt_hat = NL_Bx_hat - eta*K2*Bx_hat
    dBy_dt_hat = NL_By_hat - eta*K2*By_hat

    return (np.real(ifftn(dvx_dt_hat)), np.real(ifftn(dvy_dt_hat)),
            np.real(ifftn(dBx_dt_hat)), np.real(ifftn(dBy_dt_hat)))

# Time integration (RK4)
def rk4_step(vx, vy, Bx, By, dt, t):
    k1 = rhs(vx, vy, Bx, By, t)
    k2 = rhs(vx + 0.5*dt*k1[0], vy + 0.5*dt*k1[1],
             Bx + 0.5*dt*k1[2], By + 0.5*dt*k1[3], t + 0.5*dt)
    k3 = rhs(vx + 0.5*dt*k2[0], vy + 0.5*dt*k2[1],
             Bx + 0.5*dt*k2[2], By + 0.5*dt*k2[3], t + 0.5*dt)
    k4 = rhs(vx + dt*k3[0], vy + dt*k3[1],
             Bx + dt*k3[2], By + dt*k3[3], t + dt)

    vx_new = vx + (dt/6)*(k1[0] + 2*k2[0] + 2*k3[0] + k4[0])
    vy_new = vy + (dt/6)*(k1[1] + 2*k2[1] + 2*k3[1] + k4[1])
    Bx_new = Bx + (dt/6)*(k1[2] + 2*k2[2] + 2*k3[2] + k4[2])
    By_new = By + (dt/6)*(k1[3] + 2*k2[3] + 2*k3[3] + k4[3])

    return vx_new, vy_new, Bx_new, By_new

# Energy spectrum
def compute_spectrum(vx, vy, Bx, By):
    vx_hat = fftn(vx)
    vy_hat = fftn(vy)
    Bx_hat = fftn(Bx)
    By_hat = fftn(By)

    E_kin = 0.5*(np.abs(vx_hat)**2 + np.abs(vy_hat)**2)
    E_mag = 0.5*(np.abs(Bx_hat)**2 + np.abs(By_hat)**2)

    k_bins = np.arange(1, N//2)
    E_kin_spec = np.zeros(len(k_bins))
    E_mag_spec = np.zeros(len(k_bins))

    K_mag = np.sqrt(KX**2 + KY**2)
    for i, k in enumerate(k_bins):
        mask = (K_mag >= k) & (K_mag < k+1)
        E_kin_spec[i] = np.sum(E_kin[mask])
        E_mag_spec[i] = np.sum(E_mag[mask])

    return k_bins, E_kin_spec, E_mag_spec

# Main loop
t = 0.0
step = 0
snapshots = []

print("Running 2D spectral MHD simulation...")
while t < T_end:
    if step % 100 == 0:
        E_kin_total = 0.5*np.sum(vx**2 + vy**2)
        E_mag_total = 0.5*np.sum(Bx**2 + By**2)
        print(f"Step {step:4d}, t={t:.3f}, E_kin={E_kin_total:.4f}, E_mag={E_mag_total:.4f}")

        if len(snapshots) < 4:
            snapshots.append((t, vx.copy(), vy.copy(), Bx.copy(), By.copy()))

    vx, vy, Bx, By = rk4_step(vx, vy, Bx, By, dt, t)
    t += dt
    step += 1

# Final spectrum
k_bins, E_kin_spec, E_mag_spec = compute_spectrum(vx, vy, Bx, By)

# Plot spectra
fig, ax = plt.subplots(figsize=(10, 6))
ax.loglog(k_bins, E_kin_spec, 'b-', label='Kinetic', linewidth=2)
ax.loglog(k_bins, E_mag_spec, 'r--', label='Magnetic', linewidth=2)

# Reference slopes
k_ref = np.array([2, 20])
ax.loglog(k_ref, 1e2*k_ref**(-5.0/3.0), 'k:', label=r'$k^{-5/3}$', alpha=0.5)
ax.loglog(k_ref, 1e2*k_ref**(-3.0/2.0), 'g:', label=r'$k^{-3/2}$', alpha=0.5)

ax.set_xlabel('Wavenumber k')
ax.set_ylabel('Energy E(k)')
ax.set_title('MHD Turbulence Energy Spectrum')
ax.legend()
ax.grid(True, alpha=0.3)
plt.savefig('mhd_spectrum.png', dpi=150, bbox_inches='tight')
print("Spectrum saved: mhd_spectrum.png")
plt.close()

# Plot snapshots
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for i, (t_snap, vx_snap, vy_snap, Bx_snap, By_snap) in enumerate(snapshots):
    ax = axes[i // 2, i % 2]
    vorticity = np.real(ifftn(-K2 * fftn(vx_snap)))  # Simplified vorticity
    im = ax.contourf(X, Y, vorticity, levels=20, cmap='RdBu_r')
    ax.streamplot(X, Y, Bx_snap, By_snap, color='k', density=0.8, linewidth=0.5)
    ax.set_title(f't = {t_snap:.2f}')
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    plt.colorbar(im, ax=ax, label='Vorticity')

plt.tight_layout()
plt.savefig('mhd_snapshots.png', dpi=150, bbox_inches='tight')
print("Snapshots saved: mhd_snapshots.png")
plt.close()

9.3 Expected Output

  • Energy spectrum: Power-law decay (check if $k^{-5/3}$ or $k^{-3/2}$)
  • Snapshots: Vorticity contours + magnetic field lines
  • Energy conservation: Total energy should stay approximately constant (with slow dissipation from $\nu$, $\eta$)

10. Chebyshev Equilibrium Solver

10.1 1D Force Balance

Solve: $$ \frac{d}{dx}\left(p + \frac{B^2}{2}\right) = 0 $$ for $p(x)$, $B(x)$ in $x \in [-1, 1]$ with BC: $p(-1) = 1$, $B(-1) = 0.5$.

import numpy as np
import matplotlib.pyplot as plt

# Chebyshev differentiation matrix
def cheb_diff_matrix(N):
    """Compute Chebyshev differentiation matrix."""
    x = np.cos(np.pi * np.arange(N+1) / N)
    c = np.ones(N+1)
    c[0] = c[-1] = 2.0
    c[1:-1] = 1.0
    c *= (-1)**np.arange(N+1)

    X = np.tile(x, (N+1, 1)).T
    dX = X - X.T
    D = np.outer(c, 1.0/c) / (dX + np.eye(N+1))
    D -= np.diag(np.sum(D, axis=1))

    return D, x

N = 32
D, x = cheb_diff_matrix(N)

# Initial guess
p = 1.0 - 0.5*(x + 1.0)
B = 0.5 + 0.3*(x + 1.0)

# Solve force balance: d/dx(p + B^2/2) = 0
# Integrate: p + B^2/2 = const
# BC: p(-1) = 1, B(-1) = 0.5 => const = 1 + 0.5^2/2 = 1.125

const = 1.0 + 0.5**2 / 2.0
p_sol = const - B**2 / 2.0

# Verify
ptot = p_sol + B**2 / 2.0
dptot_dx = D @ ptot

# Plot
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

axes[0].plot(x, p_sol, 'b-', linewidth=2, label='Pressure p')
axes[0].set_xlabel('x')
axes[0].set_ylabel('p')
axes[0].set_title('Pressure Profile')
axes[0].grid(True, alpha=0.3)
axes[0].legend()

axes[1].plot(x, B, 'r-', linewidth=2, label='Magnetic Field B')
axes[1].set_xlabel('x')
axes[1].set_ylabel('B')
axes[1].set_title('Magnetic Field Profile')
axes[1].grid(True, alpha=0.3)
axes[1].legend()

axes[2].plot(x, dptot_dx, 'g-', linewidth=2, label=r'$d(p+B^2/2)/dx$')
axes[2].axhline(0, color='k', linestyle=':', alpha=0.5)
axes[2].set_xlabel('x')
axes[2].set_ylabel('Residual')
axes[2].set_title('Force Balance Residual')
axes[2].grid(True, alpha=0.3)
axes[2].legend()

plt.tight_layout()
plt.savefig('chebyshev_equilibrium.png', dpi=150, bbox_inches='tight')
print("Plot saved: chebyshev_equilibrium.png")
plt.close()

11. AMR Concept Demonstration

11.1 1D Adaptive Grid

import numpy as np
import matplotlib.pyplot as plt

# Function with sharp gradient
def f(x):
    return np.tanh(20*(x - 0.5))

# Initial coarse grid
x_coarse = np.linspace(0, 1, 11)
f_coarse = f(x_coarse)

# Refinement criterion: large gradient
df_dx = np.gradient(f_coarse, x_coarse)
refine_indices = np.where(np.abs(df_dx) > 5.0)[0]

# Refined grid (insert midpoints)
x_fine = []
f_fine = []
for i in range(len(x_coarse)):
    x_fine.append(x_coarse[i])
    f_fine.append(f_coarse[i])
    if i in refine_indices and i < len(x_coarse) - 1:
        x_mid = 0.5*(x_coarse[i] + x_coarse[i+1])
        x_fine.append(x_mid)
        f_fine.append(f(x_mid))

x_fine = np.array(x_fine)
f_fine = np.array(f_fine)

# Plot
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

x_exact = np.linspace(0, 1, 500)
f_exact = f(x_exact)

axes[0].plot(x_exact, f_exact, 'k-', linewidth=1, label='Exact', alpha=0.5)
axes[0].plot(x_coarse, f_coarse, 'bo-', linewidth=2, label='Coarse Grid', markersize=8)
axes[0].set_xlabel('x')
axes[0].set_ylabel('f(x)')
axes[0].set_title('Coarse Grid (11 points)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(x_exact, f_exact, 'k-', linewidth=1, label='Exact', alpha=0.5)
axes[1].plot(x_fine, f_fine, 'ro-', linewidth=2, label='AMR Grid', markersize=6)
axes[1].set_xlabel('x')
axes[1].set_ylabel('f(x)')
axes[1].set_title(f'AMR Grid ({len(x_fine)} points)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('amr_concept.png', dpi=150, bbox_inches='tight')
print("Plot saved: amr_concept.png")
print(f"Coarse grid: {len(x_coarse)} points")
print(f"AMR grid: {len(x_fine)} points")
plt.close()

Output: AMR concentrates points in high-gradient region (around $x = 0.5$).


Summary

Advanced MHD methods extend basic finite-volume approaches:

  1. Pseudo-spectral: Exponential accuracy for smooth, periodic problems (MHD turbulence, dynamos)
  2. Dealiasing: Essential for correct nonlinear term evaluation (2/3 rule)
  3. Chebyshev: Non-periodic BC, ideal for boundary value problems (equilibria, stability)
  4. AMR: Adaptive resolution for multi-scale problems (shocks, current sheets)
  5. Hybrid MHD-PIC: Combine fluid bulk + kinetic particles (cosmic rays)
  6. SPH-MHD: Lagrangian method for star formation, astrophysical flows
  7. Production codes: Athena++, PLUTO, FLASH (grid), Pencil Code (high-order FD), Dedalus (spectral)

When to use: - Spectral: Turbulence DNS, dynamos (high $Re$, $Rm$) - AMR: Shocks, jets, accretion (multi-scale) - Chebyshev: Equilibria, linear stability - Hybrid: Cosmic ray acceleration, energetic particles

Each method has trade-offs; choose based on physics and computational resources.


Practice Problems

  1. Fourier Derivative: For $f(x) = \sin(3x)$ on $[0, 2\pi]$ with $N = 16$ points, compute $f'(x)$ using FFT. Compare to exact $f'(x) = 3\cos(3x)$. What is the maximum error?

  2. 2/3 Dealiasing: Consider $f(x) = \sin(5x)$ and $g(x) = \sin(7x)$. Their product has max wavenumber 12. If $N = 16$ (Nyquist $k_N = 8$), will aliasing occur without dealiasing? What is the aliased wavenumber?

  3. Chebyshev Points: Compute the $N = 8$ Gauss-Lobatto Chebyshev points $x_j = \cos(\pi j / N)$. Plot their distribution. Why do they cluster at endpoints?

  4. Energy Spectrum Slope: From the 2D spectral MHD code, vary $\nu$ and $\eta$ by factors of 2. Does the inertial range slope change? At what $Re$ do you observe a clear $k^{-5/3}$ range?

  5. AMR Efficiency: For a 1D shock (Sod test), estimate the number of grid points needed for uniform grid vs 2-level AMR (refinement factor 4) to resolve the shock to 1% accuracy. Assume shock width $\sim 10$ cells.

  6. Hybrid MHD-PIC: In a hybrid code, cosmic ray particles diffuse with $D = 10^{28}$ cmΒ²/s. If the MHD grid has $\Delta x = 10^{10}$ cm, estimate the timestep from the diffusion CFL condition $\Delta t < (\Delta x)^2 / D$.

  7. SPH Kernel: For the cubic spline kernel $W(r, h) \propto (1 - r/h)^3$ for $r < h$, verify that $\int W(\mathbf{r}, h) d^3r = 1$. Compute the normalization constant in 2D.

  8. Athena++ vs PLUTO: Research: What Riemann solver does Athena++ use by default for MHD? Compare to PLUTO's default. Which is more diffusive?


Previous: Relativistic MHD | Next: Projects

to navigate between lessons