Skip to content

Commit 762d102

Browse files
committed
Add Devito-focused diffusion equation chapter content (Phase 3)
Python Solvers: - src/diffu/diffu1D_devito.py: 1D diffusion solver (Forward Euler explicit) with DiffusionResult dataclass, exact solution, convergence testing - src/diffu/diffu2D_devito.py: 2D diffusion solver using .laplace - src/diffu/__init__.py: Module exports for new Devito solvers Quarto Chapters: - chapters/diffu/diffu1D_devito.qmd: Forward Euler with Devito covering time_order=1, .dt derivative, Fourier number F <= 0.5 stability - chapters/diffu/diffu2D_devito.qmd: 2D extension with .laplace and F <= 0.25 stability for equal spacing - chapters/diffu/diffu_devito_exercises.qmd: 10 exercises with solutions covering stability, convergence, Gaussian/plug initial conditions, 2D heat diffusion, variable coefficients, manufactured solutions Tests: - tests/test_diffu_devito.py: 23 tests covering 1D/2D solvers, exact solutions, convergence rates, boundary conditions, initial conditions All 111 tests pass.
1 parent 16c942b commit 762d102

9 files changed

Lines changed: 2174 additions & 1 deletion

File tree

chapters/diffu/diffu1D_devito.qmd

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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

Comments
 (0)