Grid¶
Background¶
Grid
Educational script demonstrating grid concepts.
Code¶
```python """ Grid
Educational script demonstrating grid concepts. """
============================================================================¶
heat_equation_2d/grid.py¶
============================================================================¶
import numpy as np from typing import NamedTuple
class Grid2DParams(NamedTuple): """Grid parameters for the 2D heat equation.""" Lx: float; Ly: float; T: float; Nx: int; Ny: int; Nt: int; D: float dx: float; dy: float; dt: float; x: np.ndarray; y: np.ndarray X: np.ndarray; Y: np.ndarray; rx: float; ry: float
def check_2d_stability(rx: float, ry: float, method: str = "forward") -> None: """ Check stability condition for 2D explicit schemes.
Args:
rx: Diffusion coefficient in x direction (D*dt/dx^2)
ry: Diffusion coefficient in y direction (D*dt/dy^2)
method: Numerical method being used
Raises:
ValueError: If stability condition is violated
"""
if method == "forward" and rx + ry > 0.5:
raise ValueError(
f"Forward Euler unstable in 2D: rx + ry = {rx + ry:.4f} > 0.5. "
f"Reduce dt or increase dx/dy."
)
def create_time_array_2d(T: float, Nt: int) -> np.ndarray: """Create time array for the 2D simulation.""" return np.linspace(0, T, Nt + 1)
def get_stability_info(params): """Get stability information for the grid parameters.""" stability_param = params.rx + params.ry return { "stability_parameter": stability_param, "is_stable_forward": stability_param <= 0.5, "rx": params.rx, "ry": params.ry, "dx": params.dx, "dy": params.dy, "dt": params.dt }
def create_2d_grid(Lx=1.0, Ly=1.0, T=0.1, Nx=50, Ny=50, Nt=1000, D=0.01): """Create 2D grid parameters.""" dx, dy, dt = Lx/(Nx-1), Ly/(Ny-1), T/Nt x, y = np.linspace(0, Lx, Nx), np.linspace(0, Ly, Ny) X, Y = np.meshgrid(x, y, indexing="ij") rx, ry = Ddt/dx2, Ddt/dy**2 return Grid2DParams(Lx, Ly, T, Nx, Ny, Nt, D, dx, dy, dt, x, y, X, Y, rx, ry)
if name == "main": pass ```
Exercises¶
Exercise 1. For a 2D grid with \(L_x = L_y = 1\), \(N_x = N_y = 50\), \(N_t = 1000\), \(T = 0.1\), and \(D = 0.01\), compute the stability parameters \(r_x\) and \(r_y\) and determine if Forward Euler is stable.
Solution to Exercise 1
\(\Delta x = 1/49 \approx 0.0204\), \(\Delta y = 1/49 \approx 0.0204\), \(\Delta t = 0.1/1000 = 10^{-4}\).
By symmetry, \(r_y \approx 0.0024\). The stability condition is \(r_x + r_y \le 0.5\), and \(0.0048 \le 0.5\), so Forward Euler is stable.
Exercise 2. Derive the 2D stability condition \(r_x + r_y \le 1/2\) for Forward Euler using von Neumann analysis with a 2D Fourier mode \(u_{j,k}^n = g^n e^{i(j\theta_x + k\theta_y)}\).
Solution to Exercise 2
Substituting into the 2D Forward Euler scheme gives the amplification factor
The worst case is \(\theta_x = \theta_y = \pi\), giving \(g = 1 - 4r_x - 4r_y\). For stability, \(|g| \le 1\), so \(-1 \le 1 - 4(r_x + r_y) \le 1\), which yields \(r_x + r_y \le 1/2\).
Exercise 3. If the 2D grid uses \(N_x = 100\) and \(N_y = 100\), how many unknowns are in the flattened system? Why do 2D implicit methods typically use sparse solvers?
Solution to Exercise 3
The flattened system has \(N_x \times N_y = 10{,}000\) unknowns. The system matrix is \(10{,}000 \times 10{,}000\), which would require 800 MB to store as a dense matrix (assuming 8-byte doubles).
However, the 2D Laplacian matrix is sparse: each row has at most 5 nonzero entries (the stencil \([1, 1, -4, 1, 1]\)). Sparse storage requires only about \(5 \times 10{,}000 = 50{,}000\) entries. Sparse solvers (e.g., scipy.sparse.linalg.spsolve) exploit this structure, reducing memory from \(O(N^2)\) to \(O(N)\) and computation from \(O(N^3)\) to \(O(N^{3/2})\) or better.
Exercise 4.
Explain the difference between indexing="ij" and indexing="xy" in np.meshgrid. Which convention does the 2D heat equation code use and why?
Solution to Exercise 4
With indexing="ij", X[i,j] corresponds to the \(i\)-th \(x\)-value and \(j\)-th \(y\)-value, so the first index varies with \(x\) and the second with \(y\). With indexing="xy", the convention is transposed: X[j,i] uses row for \(y\) and column for \(x\).
The code uses indexing="ij" because it matches the natural matrix convention where the first index is the row (x-direction) and second is the column (y-direction). This simplifies the finite difference stencil implementation and the flattening/reshaping operations needed for implicit solvers.