Skip to content

Commit 18c3bce

Browse files
committed
Add Devito Introduction chapter, restructure book (Phase 1)
- Create chapters/devito_intro/ with five QMD files covering: - What is Devito (motivation, traditional vs DSL approach) - First PDE (1D wave equation complete example) - Core abstractions (Grid, Function, TimeFunction, Eq, Operator) - Boundary conditions (Dirichlet, Neumann, absorbing, periodic) - Verification (convergence testing, MMS) - Update _quarto.yml to use devito_intro as first main chapter - Update devito-plan.md to mark Phase 1 complete
1 parent 92920f6 commit 18c3bce

8 files changed

Lines changed: 1119 additions & 2 deletions

File tree

_quarto.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ book:
1414
- chapters/preface/index.qmd
1515
- part: "Main Chapters"
1616
chapters:
17-
- chapters/vib/index.qmd
17+
- chapters/devito_intro/index.qmd
1818
- chapters/wave/index.qmd
1919
- chapters/diffu/index.qmd
2020
- chapters/advec/index.qmd
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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

Comments
 (0)