|
| 1 | +## Boundary Conditions in Devito {#sec-devito-intro-bcs} |
| 2 | + |
| 3 | +Properly implementing boundary conditions is crucial for accurate PDE |
| 4 | +solutions. Devito provides several approaches, each suited to different |
| 5 | +situations. |
| 6 | + |
| 7 | +### Dirichlet Boundary Conditions |
| 8 | + |
| 9 | +Dirichlet conditions specify the solution value at the boundary: |
| 10 | +$$ |
| 11 | +u(0, t) = g_0(t), \quad u(L, t) = g_L(t) |
| 12 | +$$ |
| 13 | + |
| 14 | +**Method 1: Explicit equations** |
| 15 | + |
| 16 | +The most direct approach adds equations that set boundary values: |
| 17 | + |
| 18 | +```python |
| 19 | +from devito import Grid, TimeFunction, Eq, Operator |
| 20 | + |
| 21 | +grid = Grid(shape=(101,), extent=(1.0,)) |
| 22 | +u = TimeFunction(name='u', grid=grid, time_order=2, space_order=2) |
| 23 | + |
| 24 | +# Get the time dimension for indexing |
| 25 | +t = grid.stepping_dim |
| 26 | + |
| 27 | +# Interior update (wave equation) |
| 28 | +update = Eq(u.forward, 2*u - u.backward + dt**2 * c**2 * u.dx2) |
| 29 | + |
| 30 | +# Boundary conditions: u = 0 at both ends |
| 31 | +bc_left = Eq(u[t+1, 0], 0) |
| 32 | +bc_right = Eq(u[t+1, 100], 0) |
| 33 | + |
| 34 | +# Include all equations in the operator |
| 35 | +op = Operator([update, bc_left, bc_right]) |
| 36 | +``` |
| 37 | + |
| 38 | +**Method 2: Using subdomain** |
| 39 | + |
| 40 | +For interior-only updates, use `subdomain=grid.interior`: |
| 41 | + |
| 42 | +```python |
| 43 | +# Update only interior points (automatically excludes boundaries) |
| 44 | +update = Eq(u.forward, 2*u - u.backward + dt**2 * c**2 * u.dx2, |
| 45 | + subdomain=grid.interior) |
| 46 | + |
| 47 | +# Set boundaries explicitly |
| 48 | +bc_left = Eq(u[t+1, 0], 0) |
| 49 | +bc_right = Eq(u[t+1, 100], 0) |
| 50 | + |
| 51 | +op = Operator([update, bc_left, bc_right]) |
| 52 | +``` |
| 53 | + |
| 54 | +The `subdomain=grid.interior` approach is often cleaner because it |
| 55 | +explicitly separates the physics (interior PDE) from the boundary treatment. |
| 56 | + |
| 57 | +### Neumann Boundary Conditions |
| 58 | + |
| 59 | +Neumann conditions specify the derivative at the boundary: |
| 60 | +$$ |
| 61 | +\frac{\partial u}{\partial x}(0, t) = h_0(t), \quad |
| 62 | +\frac{\partial u}{\partial x}(L, t) = h_L(t) |
| 63 | +$$ |
| 64 | + |
| 65 | +For a zero-flux condition ($\partial u/\partial x = 0$), we use the |
| 66 | +ghost point method. The central difference at the boundary requires |
| 67 | +a point outside the domain: |
| 68 | +$$ |
| 69 | +\frac{\partial u}{\partial x}\bigg|_{i=0} \approx \frac{u_1 - u_{-1}}{2\Delta x} = 0 |
| 70 | +$$ |
| 71 | + |
| 72 | +This gives $u_{-1} = u_1$, which we substitute into the interior equation: |
| 73 | + |
| 74 | +```python |
| 75 | +grid = Grid(shape=(101,), extent=(1.0,)) |
| 76 | +u = TimeFunction(name='u', grid=grid, time_order=1, space_order=2) |
| 77 | +x = grid.dimensions[0] |
| 78 | +t = grid.stepping_dim |
| 79 | + |
| 80 | +# Interior update (diffusion equation) |
| 81 | +update = Eq(u.forward, u + alpha * dt * u.dx2, subdomain=grid.interior) |
| 82 | + |
| 83 | +# Neumann BC at left (du/dx = 0): use one-sided update |
| 84 | +# u_new[0] = u[0] + alpha*dt * 2*(u[1] - u[0])/dx^2 |
| 85 | +dx = grid.spacing[0] |
| 86 | +bc_left = Eq(u[t+1, 0], u[t, 0] + alpha * dt * 2 * (u[t, 1] - u[t, 0]) / dx**2) |
| 87 | + |
| 88 | +# Neumann BC at right (du/dx = 0) |
| 89 | +bc_right = Eq(u[t+1, 100], u[t, 100] + alpha * dt * 2 * (u[t, 99] - u[t, 100]) / dx**2) |
| 90 | + |
| 91 | +op = Operator([update, bc_left, bc_right]) |
| 92 | +``` |
| 93 | + |
| 94 | +### Mixed Boundary Conditions |
| 95 | + |
| 96 | +Often we have different conditions on different boundaries: |
| 97 | + |
| 98 | +```python |
| 99 | +# Dirichlet on left, Neumann on right |
| 100 | +bc_left = Eq(u[t+1, 0], 0) # u(0,t) = 0 |
| 101 | +bc_right = Eq(u[t+1, 100], u[t+1, 99]) # du/dx(L,t) = 0 (copy from interior) |
| 102 | + |
| 103 | +op = Operator([update, bc_left, bc_right]) |
| 104 | +``` |
| 105 | + |
| 106 | +### 2D Boundary Conditions |
| 107 | + |
| 108 | +For 2D problems, boundary conditions apply to all four edges: |
| 109 | + |
| 110 | +```python |
| 111 | +grid = Grid(shape=(101, 101), extent=(1.0, 1.0)) |
| 112 | +u = TimeFunction(name='u', grid=grid, time_order=2, space_order=2) |
| 113 | + |
| 114 | +x, y = grid.dimensions |
| 115 | +t = grid.stepping_dim |
| 116 | +Nx, Ny = 100, 100 |
| 117 | + |
| 118 | +# Interior update |
| 119 | +update = Eq(u.forward, 2*u - u.backward + dt**2 * c**2 * u.laplace, |
| 120 | + subdomain=grid.interior) |
| 121 | + |
| 122 | +# Dirichlet BCs on all four edges |
| 123 | +bc_left = Eq(u[t+1, 0, y], 0) |
| 124 | +bc_right = Eq(u[t+1, Nx, y], 0) |
| 125 | +bc_bottom = Eq(u[t+1, x, 0], 0) |
| 126 | +bc_top = Eq(u[t+1, x, Ny], 0) |
| 127 | + |
| 128 | +op = Operator([update, bc_left, bc_right, bc_bottom, bc_top]) |
| 129 | +``` |
| 130 | + |
| 131 | +### Time-Dependent Boundary Conditions |
| 132 | + |
| 133 | +For boundaries that vary in time, use the time index: |
| 134 | + |
| 135 | +```python |
| 136 | +from devito import Constant |
| 137 | + |
| 138 | +# Time-varying amplitude |
| 139 | +A = Constant(name='A') |
| 140 | + |
| 141 | +# Sinusoidal forcing at left boundary |
| 142 | +# u(0, t) = A * sin(omega * t) |
| 143 | +import sympy as sp |
| 144 | +omega = 2 * sp.pi # Angular frequency |
| 145 | + |
| 146 | +# The time value at step n |
| 147 | +t_val = t * dt # Symbolic time value |
| 148 | + |
| 149 | +bc_left = Eq(u[t+1, 0], A * sp.sin(omega * t_val)) |
| 150 | + |
| 151 | +# Set the amplitude before running |
| 152 | +op = Operator([update, bc_left, bc_right]) |
| 153 | +op(time=Nt, dt=dt, A=1.0) # Pass A as keyword argument |
| 154 | +``` |
| 155 | + |
| 156 | +### Absorbing Boundary Conditions |
| 157 | + |
| 158 | +For wave equations, we often want waves to exit the domain without |
| 159 | +reflection. A simple first-order absorbing condition is: |
| 160 | +$$ |
| 161 | +\frac{\partial u}{\partial t} + c\frac{\partial u}{\partial x} = 0 \quad \text{at } x = L |
| 162 | +$$ |
| 163 | + |
| 164 | +This can be discretized as: |
| 165 | + |
| 166 | +```python |
| 167 | +# Absorbing BC at right boundary (waves traveling right) |
| 168 | +dx = grid.spacing[0] |
| 169 | +bc_right_absorbing = Eq( |
| 170 | + u[t+1, Nx], |
| 171 | + u[t, Nx] - c * dt / dx * (u[t, Nx] - u[t, Nx-1]) |
| 172 | +) |
| 173 | +``` |
| 174 | + |
| 175 | +More sophisticated absorbing conditions use damping layers (sponges) |
| 176 | +near the boundaries. This is covered in detail in @sec-wave-1d-absorbing. |
| 177 | + |
| 178 | +### Periodic Boundary Conditions |
| 179 | + |
| 180 | +For periodic domains, the solution wraps around: |
| 181 | +$$ |
| 182 | +u(0, t) = u(L, t) |
| 183 | +$$ |
| 184 | + |
| 185 | +Devito doesn't directly support periodic BCs, but they can be implemented |
| 186 | +by copying values: |
| 187 | + |
| 188 | +```python |
| 189 | +# Periodic BCs: u[0] = u[Nx-1], u[Nx] = u[1] |
| 190 | +bc_periodic_left = Eq(u[t+1, 0], u[t+1, Nx-1]) |
| 191 | +bc_periodic_right = Eq(u[t+1, Nx], u[t+1, 1]) |
| 192 | +``` |
| 193 | + |
| 194 | +Note: The order of equations matters. Update the interior first, then |
| 195 | +copy for periodicity. |
| 196 | + |
| 197 | +### Best Practices |
| 198 | + |
| 199 | +1. **Use `subdomain=grid.interior`** for interior updates to clearly |
| 200 | + separate physics from boundary treatment |
| 201 | + |
| 202 | +2. **Check boundary equation order**: Boundary equations should typically |
| 203 | + come after interior updates in the operator |
| 204 | + |
| 205 | +3. **Verify boundary values**: After running, check that boundaries have |
| 206 | + the expected values |
| 207 | + |
| 208 | +4. **Test with known solutions**: Use problems with analytical solutions |
| 209 | + to verify boundary condition implementation |
| 210 | + |
| 211 | +### Example: Complete Wave Equation Solver |
| 212 | + |
| 213 | +Here's a complete example combining interior updates with boundary conditions: |
| 214 | + |
| 215 | +```python |
| 216 | +from devito import Grid, TimeFunction, Eq, Operator |
| 217 | +import numpy as np |
| 218 | + |
| 219 | +# Setup |
| 220 | +L, c, T = 1.0, 1.0, 2.0 |
| 221 | +Nx = 100 |
| 222 | +C = 0.9 # Courant number |
| 223 | +dx = L / Nx |
| 224 | +dt = C * dx / c |
| 225 | +Nt = int(T / dt) |
| 226 | + |
| 227 | +# Grid and field |
| 228 | +grid = Grid(shape=(Nx + 1,), extent=(L,)) |
| 229 | +u = TimeFunction(name='u', grid=grid, time_order=2, space_order=2) |
| 230 | +t = grid.stepping_dim |
| 231 | + |
| 232 | +# Initial condition: plucked string |
| 233 | +x_vals = np.linspace(0, L, Nx + 1) |
| 234 | +u.data[0, :] = np.sin(np.pi * x_vals) |
| 235 | +u.data[1, :] = u.data[0, :] # Zero initial velocity |
| 236 | + |
| 237 | +# Equations |
| 238 | +update = Eq(u.forward, 2*u - u.backward + (c*dt)**2 * u.dx2, |
| 239 | + subdomain=grid.interior) |
| 240 | +bc_left = Eq(u[t+1, 0], 0) |
| 241 | +bc_right = Eq(u[t+1, Nx], 0) |
| 242 | + |
| 243 | +# Solve |
| 244 | +op = Operator([update, bc_left, bc_right]) |
| 245 | +op(time=Nt, dt=dt) |
| 246 | + |
| 247 | +# Verify: solution should return to initial shape at t = 2L/c |
| 248 | +print(f"Initial max: {np.max(u.data[1, :]):.6f}") |
| 249 | +print(f"Final max: {np.max(u.data[0, :]):.6f}") |
| 250 | +``` |
| 251 | + |
| 252 | +For a string with fixed ends and initial shape $\sin(\pi x)$, the solution |
| 253 | +oscillates with period $2L/c$. After one period, it should return to the |
| 254 | +initial configuration. |
0 commit comments