|
| 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