|
| 1 | +## Solving the Diffusion Equation with Devito {#sec-diffu-devito} |
| 2 | + |
| 3 | +Having established the finite difference discretization of the diffusion |
| 4 | +equation, we now implement the Forward Euler scheme using Devito. The |
| 5 | +symbolic approach allows us to express the PDE directly and let Devito |
| 6 | +generate optimized code. |
| 7 | + |
| 8 | +### From Discretization to Devito |
| 9 | + |
| 10 | +Recall the Forward Euler scheme for the diffusion equation: |
| 11 | +$$ |
| 12 | +u^{n+1}_i = u^n_i + F\left(u^{n}_{i+1} - 2u^n_i + u^n_{i-1}\right) |
| 13 | +$$ |
| 14 | + |
| 15 | +where the Fourier number $F = \dfc \Delta t / \Delta x^2$ must satisfy |
| 16 | +$F \le 0.5$ for stability. |
| 17 | + |
| 18 | +In Devito, we express this as the PDE $u_t = \dfc u_{xx}$ and let |
| 19 | +the framework derive the update formula automatically. |
| 20 | + |
| 21 | +### The Devito Implementation |
| 22 | + |
| 23 | +```python |
| 24 | +from devito import Grid, TimeFunction, Eq, solve, Operator, Constant |
| 25 | +import numpy as np |
| 26 | + |
| 27 | +# Domain and discretization |
| 28 | +L = 1.0 # Domain length |
| 29 | +Nx = 100 # Grid points |
| 30 | +a = 1.0 # Diffusion coefficient |
| 31 | +F = 0.5 # Fourier number |
| 32 | + |
| 33 | +dx = L / Nx |
| 34 | +dt = F * dx**2 / a # Time step from stability condition |
| 35 | + |
| 36 | +# Create Devito grid |
| 37 | +grid = Grid(shape=(Nx + 1,), extent=(L,)) |
| 38 | + |
| 39 | +# Time-varying temperature field |
| 40 | +# time_order=1 for first-order time derivative |
| 41 | +u = TimeFunction(name='u', grid=grid, time_order=1, space_order=2) |
| 42 | +``` |
| 43 | + |
| 44 | +### Key Differences from the Wave Equation |
| 45 | + |
| 46 | +Compare this to the wave equation setup: |
| 47 | + |
| 48 | +| Parameter | Wave Equation | Diffusion Equation | |
| 49 | +|-----------|---------------|-------------------| |
| 50 | +| `time_order` | 2 (for $u_{tt}$) | 1 (for $u_t$) | |
| 51 | +| Time derivative | `.dt2` | `.dt` | |
| 52 | +| Time levels | 3 ($u^{n-1}, u^n, u^{n+1}$) | 2 ($u^n, u^{n+1}$) | |
| 53 | +| Stability number | Courant: $C = c\Delta t/\Delta x \le 1$ | Fourier: $F = \dfc\Delta t/\Delta x^2 \le 0.5$ | |
| 54 | + |
| 55 | +### Symbolic PDE Definition |
| 56 | + |
| 57 | +With `time_order=1`, Devito provides the `.dt` derivative: |
| 58 | + |
| 59 | +```python |
| 60 | +# Diffusion coefficient as a Devito constant |
| 61 | +a_const = Constant(name='a_const') |
| 62 | + |
| 63 | +# PDE: u_t = a * u_xx => u_t - a * u_xx = 0 |
| 64 | +pde = u.dt - a_const * u.dx2 |
| 65 | + |
| 66 | +# Solve for u at the forward time level |
| 67 | +stencil = Eq(u.forward, solve(pde, u.forward)) |
| 68 | +``` |
| 69 | + |
| 70 | +When we print the stencil, we see: |
| 71 | +```python |
| 72 | +print(stencil) |
| 73 | +# Eq(u(t + dt, x), dt*a_const*u(t, x).dx2 + u(t, x)) |
| 74 | +``` |
| 75 | + |
| 76 | +This is exactly the Forward Euler update: $u^{n+1} = u^n + \Delta t \cdot \dfc \cdot u_{xx}^n$. |
| 77 | + |
| 78 | +### Boundary Conditions |
| 79 | + |
| 80 | +For homogeneous Dirichlet conditions $u(0,t) = u(L,t) = 0$: |
| 81 | + |
| 82 | +```python |
| 83 | +t_dim = grid.stepping_dim |
| 84 | +bc_left = Eq(u[t_dim + 1, 0], 0) |
| 85 | +bc_right = Eq(u[t_dim + 1, Nx], 0) |
| 86 | +``` |
| 87 | + |
| 88 | +### Complete Solver |
| 89 | + |
| 90 | +The `src.diffu` module provides `solve_diffusion_1d`: |
| 91 | + |
| 92 | +```python |
| 93 | +from src.diffu import solve_diffusion_1d |
| 94 | +import numpy as np |
| 95 | + |
| 96 | +# Initial condition: sinusoidal temperature profile |
| 97 | +def I(x): |
| 98 | + return np.sin(np.pi * x) |
| 99 | + |
| 100 | +result = solve_diffusion_1d( |
| 101 | + L=1.0, # Domain length |
| 102 | + a=1.0, # Diffusion coefficient |
| 103 | + Nx=100, # Grid points |
| 104 | + T=0.1, # Final time |
| 105 | + F=0.5, # Fourier number (at stability limit) |
| 106 | + I=I, # Initial condition |
| 107 | +) |
| 108 | + |
| 109 | +print(f"Final time: {result.t:.4f}") |
| 110 | +print(f"Max temperature: {result.u.max():.6f}") |
| 111 | +``` |
| 112 | + |
| 113 | +### Verification with Exact Solution |
| 114 | + |
| 115 | +For the initial condition $I(x) = \sin(\pi x/L)$, the exact solution is: |
| 116 | +$$ |
| 117 | +u(x,t) = e^{-\dfc (\pi/L)^2 t} \sin(\pi x/L) |
| 118 | +$$ |
| 119 | + |
| 120 | +This exponentially decaying sinusoid can verify our implementation: |
| 121 | + |
| 122 | +```python |
| 123 | +from src.diffu import exact_diffusion_sine |
| 124 | + |
| 125 | +# Compare numerical and exact solutions |
| 126 | +u_exact = exact_diffusion_sine(result.x, result.t, L=1.0, a=1.0) |
| 127 | +error = np.max(np.abs(result.u - u_exact)) |
| 128 | +print(f"Maximum error: {error:.2e}") |
| 129 | +``` |
| 130 | + |
| 131 | +### Convergence Testing |
| 132 | + |
| 133 | +We verify second-order spatial accuracy: |
| 134 | + |
| 135 | +```python |
| 136 | +from src.diffu import convergence_test_diffusion_1d |
| 137 | + |
| 138 | +grid_sizes, errors, rate = convergence_test_diffusion_1d( |
| 139 | + grid_sizes=[10, 20, 40, 80], |
| 140 | + T=0.1, |
| 141 | + F=0.5, |
| 142 | +) |
| 143 | + |
| 144 | +print(f"Observed convergence rate: {rate:.2f}") # Should approach 2.0 |
| 145 | +``` |
| 146 | + |
| 147 | +With $F$ fixed, refining the grid means $\Delta x \to \Delta x/2$ and |
| 148 | +$\Delta t \to \Delta t/4$ (since $F = \dfc\Delta t/\Delta x^2$). The |
| 149 | +spatial error $O(\Delta x^2)$ dominates, giving second-order convergence. |
| 150 | + |
| 151 | +### Visualizing the Solution Evolution |
| 152 | + |
| 153 | +```python |
| 154 | +import matplotlib.pyplot as plt |
| 155 | + |
| 156 | +result = solve_diffusion_1d( |
| 157 | + L=1.0, a=1.0, Nx=100, T=0.5, F=0.5, |
| 158 | + save_history=True, |
| 159 | +) |
| 160 | + |
| 161 | +# Plot at several times |
| 162 | +times_to_plot = [0, 0.1, 0.2, 0.3, 0.5] |
| 163 | +plt.figure(figsize=(10, 6)) |
| 164 | + |
| 165 | +for t in times_to_plot: |
| 166 | + idx = int(t / result.dt) |
| 167 | + if idx < len(result.t_history): |
| 168 | + plt.plot(result.x, result.u_history[idx], |
| 169 | + label=f't = {result.t_history[idx]:.2f}') |
| 170 | + |
| 171 | +plt.xlabel('x') |
| 172 | +plt.ylabel('u(x, t)') |
| 173 | +plt.title('Diffusion of a Sinusoidal Profile') |
| 174 | +plt.legend() |
| 175 | +plt.grid(True) |
| 176 | +``` |
| 177 | + |
| 178 | +The solution shows the characteristic behavior of the heat equation: |
| 179 | +the sinusoidal profile decays exponentially in time while maintaining |
| 180 | +its shape. |
| 181 | + |
| 182 | +### The Fourier Number and Physical Interpretation |
| 183 | + |
| 184 | +The Fourier number $F = \dfc \Delta t / \Delta x^2$ has a physical |
| 185 | +interpretation. It represents the ratio of the diffusion time scale |
| 186 | +to the computational time step: |
| 187 | + |
| 188 | +- **Large $F$**: Heat diffuses quickly relative to the time step |
| 189 | +- **Small $F$**: Slow diffusion, finer time resolution |
| 190 | + |
| 191 | +The stability limit $F \le 0.5$ means we cannot take time steps larger |
| 192 | +than half the time for heat to diffuse across one grid cell. |
| 193 | + |
| 194 | +### Handling Different Initial Conditions |
| 195 | + |
| 196 | +The diffusion equation smooths out discontinuities over time. Let's |
| 197 | +compare a smooth Gaussian and a discontinuous "plug": |
| 198 | + |
| 199 | +```python |
| 200 | +from src.diffu import gaussian_initial_condition, plug_initial_condition |
| 201 | + |
| 202 | +# Gaussian: smooth initial condition |
| 203 | +result_gaussian = solve_diffusion_1d( |
| 204 | + L=1.0, Nx=100, T=0.1, F=0.5, |
| 205 | + I=lambda x: gaussian_initial_condition(x, L=1.0, sigma=0.05), |
| 206 | +) |
| 207 | + |
| 208 | +# Plug: discontinuous initial condition |
| 209 | +result_plug = solve_diffusion_1d( |
| 210 | + L=1.0, Nx=100, T=0.1, F=0.5, |
| 211 | + I=lambda x: plug_initial_condition(x, L=1.0, width=0.1), |
| 212 | +) |
| 213 | +``` |
| 214 | + |
| 215 | +For smooth initial conditions, the Forward Euler scheme with $F = 0.5$ |
| 216 | +works well. For discontinuous initial conditions, a smaller Fourier |
| 217 | +number ($F \le 0.25$) may be needed to avoid oscillations. |
| 218 | + |
| 219 | +### Summary |
| 220 | + |
| 221 | +Key points for the diffusion equation with Devito: |
| 222 | + |
| 223 | +1. Use `time_order=1` for the first-order time derivative |
| 224 | +2. The `.dt` attribute provides the time derivative |
| 225 | +3. The Fourier number $F = \dfc\Delta t/\Delta x^2$ must satisfy $F \le 0.5$ |
| 226 | +4. The exact sinusoidal solution provides excellent verification |
| 227 | +5. Smooth initial conditions work well at $F = 0.5$; discontinuous |
| 228 | + conditions may need smaller $F$ |
| 229 | + |
| 230 | +The Forward Euler scheme is simple and explicit, but the time step |
| 231 | +restriction can be severe for accuracy. In the next section, we |
| 232 | +discuss implicit methods that remove this restriction. |
0 commit comments