Skip to content

Commit 92b5a10

Browse files
committed
Add Devito-focused advection equation chapter content (Phase 4)
Phase 4 implements 1D advection solvers using Devito DSL: Solvers (src/advec/advec1D_devito.py): - Upwind scheme: first-order, uses .subs() for shifted indexing - Lax-Wendroff scheme: second-order accurate - Lax-Friedrichs scheme: first-order, very stable - Helper functions for exact solutions and convergence testing Chapter content: - chapters/advec/advec1D_devito.qmd: scheme comparison and implementation - chapters/advec/advec_devito_exercises.qmd: 10 exercises with solutions - Updated chapters/advec/index.qmd with new includes Testing: - 25 tests for advection solvers (convergence, boundary conditions, accuracy)
1 parent 762d102 commit 92b5a10

7 files changed

Lines changed: 1996 additions & 1 deletion

File tree

chapters/advec/advec1D_devito.qmd

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
## Advection Schemes with Devito {#sec-advec-devito}
2+
3+
Having understood the mathematical properties and challenges of advection
4+
schemes in the previous sections, we now implement these methods using
5+
Devito's symbolic framework. Devito allows us to write the discrete
6+
equations in a form close to the mathematical notation while generating
7+
optimized code automatically.
8+
9+
### The Advection Equation
10+
11+
The 1D linear advection equation is:
12+
13+
$$
14+
\frac{\partial u}{\partial t} + c\frac{\partial u}{\partial x} = 0
15+
$$ {#eq-advec-devito-pde}
16+
17+
where $c$ is the advection velocity (assumed constant and positive).
18+
The exact solution is:
19+
20+
$$
21+
u(x, t) = I(x - ct)
22+
$$
23+
24+
which represents the initial condition $I(x)$ traveling to the right
25+
at velocity $c$ without change in shape.
26+
27+
### Devito Implementation Patterns
28+
29+
Unlike diffusion and wave equations, the advection equation requires
30+
careful treatment of the spatial derivative. Centered differences lead
31+
to instability (as we saw with the FTCS scheme), so we need alternative
32+
approaches:
33+
34+
| Scheme | Spatial Discretization | Order | Key Property |
35+
|--------|----------------------|-------|--------------|
36+
| Upwind | Backward difference | 1st | Stable, diffusive |
37+
| Lax-Wendroff | Centered + diffusion | 2nd | Less diffusion, some dispersion |
38+
| Lax-Friedrichs | Averaged neighbors | 1st | Very diffusive but robust |
39+
40+
All schemes require the CFL condition: $C = c\Delta t/\Delta x \leq 1$.
41+
42+
### Comparison with Wave and Diffusion Equations
43+
44+
The advection equation differs fundamentally from the diffusion and
45+
wave equations we've solved previously:
46+
47+
| Property | Diffusion | Wave | Advection |
48+
|----------|-----------|------|-----------|
49+
| `time_order` | 1 | 2 | 1 |
50+
| Spatial deriv. | 2nd (`.dx2`) | 2nd (`.laplace`) | 1st (`.dx`) |
51+
| Stability | $F \leq 0.5$ | $C \leq 1$ | $C \leq 1$ |
52+
| Centered space | Stable | Stable | **Unstable** |
53+
| Information | Spreads both ways | Spreads both ways | One direction |
54+
55+
The key difference is that advection has directional information flow,
56+
which requires using *upwind* differences rather than centered differences.
57+
58+
### Upwind Scheme Implementation
59+
60+
The upwind scheme uses a backward difference for the spatial derivative
61+
when $c > 0$:
62+
63+
$$
64+
\frac{u^{n+1}_i - u^n_i}{\Delta t} + c\frac{u^n_i - u^n_{i-1}}{\Delta x} = 0
65+
$$
66+
67+
which gives the update formula:
68+
69+
$$
70+
u^{n+1}_i = u^n_i - C(u^n_i - u^n_{i-1})
71+
$$ {#eq-advec-upwind-update}
72+
73+
In Devito, we express this using shifted indexing:
74+
75+
```python
76+
from devito import Grid, TimeFunction, Eq, Operator, Constant
77+
import numpy as np
78+
79+
def solve_advection_upwind(L, c, Nx, T, C, I):
80+
"""Upwind scheme for 1D advection."""
81+
# Grid setup
82+
dx = L / Nx
83+
dt = C * dx / c
84+
85+
grid = Grid(shape=(Nx + 1,), extent=(L,))
86+
x_dim, = grid.dimensions
87+
88+
u = TimeFunction(name='u', grid=grid, time_order=1, space_order=1)
89+
90+
# Set initial condition
91+
x_coords = np.linspace(0, L, Nx + 1)
92+
u.data[0, :] = I(x_coords)
93+
94+
# Courant number as constant
95+
courant = Constant(name='C', value=C)
96+
97+
# Upwind stencil: u^{n+1} = u - C*(u - u[x-dx])
98+
u_minus = u.subs(x_dim, x_dim - x_dim.spacing)
99+
stencil = u - courant * (u - u_minus)
100+
update = Eq(u.forward, stencil)
101+
102+
op = Operator([update])
103+
# ... time stepping loop
104+
```
105+
106+
The key line is:
107+
```python
108+
u_minus = u.subs(x_dim, x_dim - x_dim.spacing)
109+
```
110+
111+
This creates a reference to $u^n_{i-1}$ by substituting `x_dim - x_dim.spacing`
112+
for `x_dim` in the `TimeFunction` `u`.
113+
114+
### Lax-Wendroff Scheme Implementation
115+
116+
The Lax-Wendroff scheme achieves second-order accuracy by including both
117+
a centered advection term and a diffusion-like correction:
118+
119+
$$
120+
u^{n+1}_i = u^n_i - \frac{C}{2}(u^n_{i+1} - u^n_{i-1}) + \frac{C^2}{2}(u^n_{i+1} - 2u^n_i + u^n_{i-1})
121+
$$
122+
123+
This can be written using Devito's derivative operators:
124+
125+
```python
126+
def solve_advection_lax_wendroff(L, c, Nx, T, C, I):
127+
"""Lax-Wendroff scheme for 1D advection."""
128+
dx = L / Nx
129+
dt = C * dx / c
130+
131+
grid = Grid(shape=(Nx + 1,), extent=(L,))
132+
u = TimeFunction(name='u', grid=grid, time_order=1, space_order=2)
133+
134+
x_coords = np.linspace(0, L, Nx + 1)
135+
u.data[0, :] = I(x_coords)
136+
137+
courant = Constant(name='C', value=C)
138+
139+
# Lax-Wendroff: u - (C/2)*dx*u.dx + (C²/2)*dx²*u.dx2
140+
# u.dx = centered first derivative
141+
# u.dx2 = centered second derivative
142+
stencil = u - 0.5*courant*dx*u.dx + 0.5*courant**2*dx**2*u.dx2
143+
update = Eq(u.forward, stencil)
144+
145+
op = Operator([update])
146+
# ... time stepping loop
147+
```
148+
149+
Here we use Devito's built-in derivative operators:
150+
151+
- `u.dx` computes the centered first derivative $(u_{i+1} - u_{i-1})/(2\Delta x)$
152+
- `u.dx2` computes the centered second derivative $(u_{i+1} - 2u_i + u_{i-1})/\Delta x^2$
153+
154+
### Lax-Friedrichs Scheme Implementation
155+
156+
The Lax-Friedrichs scheme is simpler but more diffusive:
157+
158+
$$
159+
u^{n+1}_i = \frac{1}{2}(u^n_{i+1} + u^n_{i-1}) - \frac{C}{2}(u^n_{i+1} - u^n_{i-1})
160+
$$
161+
162+
```python
163+
def solve_advection_lax_friedrichs(L, c, Nx, T, C, I):
164+
"""Lax-Friedrichs scheme for 1D advection."""
165+
dx = L / Nx
166+
dt = C * dx / c
167+
168+
grid = Grid(shape=(Nx + 1,), extent=(L,))
169+
x_dim, = grid.dimensions
170+
171+
u = TimeFunction(name='u', grid=grid, time_order=1, space_order=1)
172+
173+
x_coords = np.linspace(0, L, Nx + 1)
174+
u.data[0, :] = I(x_coords)
175+
176+
courant = Constant(name='C', value=C)
177+
178+
# Neighbor values
179+
u_plus = u.subs(x_dim, x_dim + x_dim.spacing)
180+
u_minus = u.subs(x_dim, x_dim - x_dim.spacing)
181+
182+
# Lax-Friedrichs stencil
183+
stencil = 0.5*(u_plus + u_minus) - 0.5*courant*(u_plus - u_minus)
184+
update = Eq(u.forward, stencil)
185+
186+
op = Operator([update])
187+
# ... time stepping loop
188+
```
189+
190+
### Periodic Boundary Conditions
191+
192+
For advection problems, periodic boundary conditions are often useful
193+
to study wave propagation without boundary effects:
194+
195+
```python
196+
t_dim = grid.stepping_dim
197+
198+
# Periodic BC: u[0] wraps to u[Nx], u[Nx] wraps to u[0]
199+
bc_left = Eq(u[t_dim + 1, 0], u[t_dim, Nx])
200+
bc_right = Eq(u[t_dim + 1, Nx], u[t_dim + 1, 0])
201+
202+
op = Operator([update, bc_left, bc_right])
203+
```
204+
205+
### Using the Solvers
206+
207+
The complete solver implementation in `src/advec/advec1D_devito.py`
208+
provides convenient interfaces:
209+
210+
```python
211+
from src.advec import (
212+
solve_advection_upwind,
213+
solve_advection_lax_wendroff,
214+
solve_advection_lax_friedrichs,
215+
exact_advection_periodic
216+
)
217+
import numpy as np
218+
219+
# Define initial condition
220+
def I(x):
221+
return np.exp(-0.5*((x - 0.25)/0.05)**2)
222+
223+
# Solve with upwind scheme
224+
result = solve_advection_upwind(
225+
L=1.0, c=1.0, Nx=100, T=0.5, C=0.8, I=I,
226+
periodic_bc=True
227+
)
228+
229+
# Compare with exact solution
230+
u_exact = exact_advection_periodic(result.x, result.t, c=1.0, L=1.0, I=I)
231+
error = np.max(np.abs(result.u - u_exact))
232+
print(f"Max error: {error:.6f}")
233+
```
234+
235+
### Scheme Comparison
236+
237+
The three schemes exhibit different numerical behaviors:
238+
239+
```python
240+
import matplotlib.pyplot as plt
241+
from src.advec import (
242+
solve_advection_upwind,
243+
solve_advection_lax_wendroff,
244+
solve_advection_lax_friedrichs,
245+
exact_advection_periodic
246+
)
247+
import numpy as np
248+
249+
def I(x):
250+
return np.exp(-0.5*((x - 0.25)/0.05)**2)
251+
252+
L, c, Nx, T, C = 1.0, 1.0, 50, 0.5, 0.8
253+
254+
# Solve with all three schemes
255+
r_upwind = solve_advection_upwind(L, c, Nx, T, C, I, periodic_bc=True)
256+
r_lw = solve_advection_lax_wendroff(L, c, Nx, T, C, I, periodic_bc=True)
257+
r_lf = solve_advection_lax_friedrichs(L, c, Nx, T, C, I, periodic_bc=True)
258+
259+
# Exact solution
260+
u_exact = exact_advection_periodic(r_upwind.x, r_upwind.t, c, L, I)
261+
262+
plt.figure(figsize=(10, 6))
263+
plt.plot(r_upwind.x, u_exact, 'k-', lw=2, label='Exact')
264+
plt.plot(r_upwind.x, r_upwind.u, 'b--', label='Upwind')
265+
plt.plot(r_lw.x, r_lw.u, 'r-.', label='Lax-Wendroff')
266+
plt.plot(r_lf.x, r_lf.u, 'g:', label='Lax-Friedrichs')
267+
plt.legend()
268+
plt.xlabel('x')
269+
plt.ylabel('u')
270+
plt.title(f'Advection: Nx={Nx}, C={C}, T={T}')
271+
plt.savefig('advec_scheme_comparison.pdf')
272+
```
273+
274+
The Lax-Wendroff scheme typically preserves the wave amplitude better but
275+
may show small oscillations. The upwind and Lax-Friedrichs schemes are
276+
more diffusive, causing the wave to spread and reduce in amplitude.
277+
278+
### Convergence Testing
279+
280+
We can verify the convergence rates of the schemes:
281+
282+
```python
283+
from src.advec import (
284+
solve_advection_upwind,
285+
solve_advection_lax_wendroff,
286+
convergence_test_advection
287+
)
288+
289+
# Test upwind (expect 1st order)
290+
sizes, errors, rate = convergence_test_advection(
291+
solve_advection_upwind,
292+
grid_sizes=[25, 50, 100, 200],
293+
T=0.25, C=0.8
294+
)
295+
print(f"Upwind convergence rate: {rate:.2f}") # ~1.0
296+
297+
# Test Lax-Wendroff (expect 2nd order)
298+
sizes, errors, rate = convergence_test_advection(
299+
solve_advection_lax_wendroff,
300+
grid_sizes=[25, 50, 100, 200],
301+
T=0.25, C=0.8
302+
)
303+
print(f"Lax-Wendroff convergence rate: {rate:.2f}") # ~2.0
304+
```
305+
306+
### Key Takeaways
307+
308+
1. **Upwind differencing** is essential for stable advection schemes—centered
309+
differences in space are unconditionally unstable.
310+
311+
2. **The Courant number** $C = c\Delta t/\Delta x$ controls stability;
312+
all schemes require $C \leq 1$.
313+
314+
3. **Trade-offs exist** between accuracy and numerical diffusion:
315+
- Upwind: Stable, 1st order, diffusive
316+
- Lax-Wendroff: 2nd order, less diffusion, may have small oscillations
317+
- Lax-Friedrichs: Very stable, very diffusive
318+
319+
4. **Devito's shifted indexing** via `u.subs(x_dim, x_dim - x_dim.spacing)`
320+
allows expressing upwind differences naturally.
321+
322+
5. **Periodic BCs** are implemented by explicitly setting boundary equations
323+
that copy values from the opposite end of the domain.

0 commit comments

Comments
 (0)