Skip to content

Commit ea8d892

Browse files
committed
Refactor book code to use Quarto includes with tested source files
- Add callout notes to chapters pointing to tested source implementations - Extract inline Python code to src/book_snippets/ with RESULT variables for testing - Create snippet wrapper .qmd files in chapters/*/snippets/ directories - Add comprehensive test suite in tests/test_book_snippets.py (21 tests) - Create src/nonlin/split_logistic.py and split_diffu_react.py modules Chapters updated with callout notes: - elliptic/elliptic.qmd -> src/elliptic/ - vib/vib_undamped.qmd -> src/vib/ - wave/wave1D_fd1.qmd, wave1D_fd2.qmd -> src/wave/ - diffu/diffu_fd1.qmd, diffu_fd3.qmd, diffu_rw.qmd -> src/diffu/ - appendices/softeng2/softeng2.qmd -> src/softeng2/ Chapters refactored to use includes: - devito_intro/ (what_is_devito, first_pde, boundary_conditions, verification) - nonlin/ (burgers, nonlin_ode, nonlin_split) - advec/advec1D_devito.qmd
1 parent 687f534 commit ea8d892

58 files changed

Lines changed: 2161 additions & 720 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

chapters/advec/advec1D_devito.qmd

Lines changed: 8 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -70,46 +70,11 @@ $$
7070
u^{n+1}_i = u^n_i - C(u^n_i - u^n_{i-1})
7171
$$ {#eq-advec-upwind-update}
7272
73-
In Devito, we express this using shifted indexing:
73+
In Devito, we express this using shifted indexing. The key technique is
74+
using `u.subs(x_dim, x_dim - x_dim.spacing)` to create a reference to
75+
$u^n_{i-1}$:
7476
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`.
77+
{{< include snippets/advec_upwind.qmd >}}
11378
11479
### Lax-Wendroff Scheme Implementation
11580
@@ -120,36 +85,11 @@ $$
12085
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})
12186
$$
12287
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:
88+
This can be written using Devito's built-in derivative operators where
89+
`u.dx` computes the centered first derivative and `u.dx2` computes the
90+
centered second derivative:
15091
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$
92+
{{< include snippets/advec_lax_wendroff.qmd >}}
15393
15494
### Lax-Friedrichs Scheme Implementation
15595
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
::: {.callout-note title="Snippet (tested)"}
2+
This code is included from `src/book_snippets/advec_lax_wendroff.py` and exercised by `pytest` (see `tests/test_book_snippets.py`).
3+
:::
4+
5+
```python
6+
{{< include ../../src/book_snippets/advec_lax_wendroff.py >}}
7+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
::: {.callout-note title="Snippet (tested)"}
2+
This code is included from `src/book_snippets/advec_upwind.py` and exercised by `pytest` (see `tests/test_book_snippets.py`).
3+
:::
4+
5+
```python
6+
{{< include ../../src/book_snippets/advec_upwind.py >}}
7+
```

chapters/appendices/softeng2/softeng2.qmd

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ and @sec-wave-pde2-var-c.
4141
4242
## A solver function
4343
44+
::: {.callout-note}
45+
## Source Files
46+
47+
The software engineering patterns presented in this appendix (classes like
48+
`Storage`, `Parameters`, `Problem`, `Mesh`, `Function`, `Solver`) are
49+
implemented in `src/softeng2/`. Key files include:
50+
51+
- `src/softeng2/Storage.py` - Data persistence with joblib
52+
- `src/softeng2/wave1D_oo.py` - Object-oriented wave solver with `Parameters` class
53+
- `src/softeng2/wave2D_u0.py` - 2D wave implementations
54+
55+
These serve as reference implementations for the patterns discussed.
56+
:::
57+
4458
The general initial-boundary value problem
4559
solved by finite difference methods. For modern implementations using
4660
Devito, see @sec-wave-devito. This section covers software engineering

chapters/devito_intro/boundary_conditions.qmd

Lines changed: 9 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,11 @@ $$
1515

1616
The most direct approach adds equations that set boundary values:
1717

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-
```
18+
{{< include snippets/boundary_dirichlet_wave.qmd >}}
3719

3820
**Method 2: Using subdomain**
3921

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-
```
22+
The snippet above already uses `subdomain=grid.interior` to keep the interior PDE update separate from boundary treatment.
5323

5424
The `subdomain=grid.interior` approach is often cleaner because it
5525
explicitly separates the physics (interior PDE) from the boundary treatment.
@@ -71,87 +41,25 @@ $$
7141

7242
This gives $u_{-1} = u_1$, which we substitute into the interior equation:
7343

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-
```
44+
{{< include snippets/neumann_bc_diffusion_1d.qmd >}}
9345

9446
### Mixed Boundary Conditions
9547

9648
Often we have different conditions on different boundaries:
9749

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-
```
50+
{{< include snippets/mixed_bc_diffusion_1d.qmd >}}
10551

10652
### 2D Boundary Conditions
10753

10854
For 2D problems, boundary conditions apply to all four edges:
10955

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-
```
56+
{{< include snippets/bc_2d_dirichlet_wave.qmd >}}
13057

13158
### Time-Dependent Boundary Conditions
13259

13360
For boundaries that vary in time, use the time index:
13461

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-
```
62+
{{< include snippets/time_dependent_bc_sine.qmd >}}
15563

15664
### Absorbing Boundary Conditions
15765

@@ -163,14 +71,7 @@ $$
16371

16472
This can be discretized as:
16573

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-
```
74+
{{< include snippets/absorbing_bc_right_wave.qmd >}}
17475

17576
More sophisticated absorbing conditions use damping layers (sponges)
17677
near the boundaries. This is covered in detail in @sec-wave-1d-absorbing.
@@ -185,11 +86,7 @@ $$
18586
Devito doesn't directly support periodic BCs, but they can be implemented
18687
by copying values:
18788

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-
```
89+
{{< include snippets/periodic_bc_advection_1d.qmd >}}
19390

19491
Note: The order of equations matters. Update the interior first, then
19592
copy for periodicity.
@@ -212,42 +109,7 @@ copy for periodicity.
212109

213110
Here's a complete example combining interior updates with boundary conditions:
214111

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-
```
112+
{{< include snippets/boundary_dirichlet_wave.qmd >}}
251113

252114
For a string with fixed ends and initial shape $\sin(\pi x)$, the solution
253115
oscillates with period $2L/c$. After one period, it should return to the

chapters/devito_intro/first_pde.qmd

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -43,52 +43,7 @@ for $C \le 1$.
4343
4444
Let's implement this step by step:
4545
46-
```python
47-
from devito import Grid, TimeFunction, Eq, Operator
48-
import numpy as np
49-
50-
# Problem parameters
51-
L = 1.0 # Domain length
52-
c = 1.0 # Wave speed
53-
T = 1.0 # Final time
54-
Nx = 100 # Number of grid points
55-
C = 0.5 # Courant number (for stability)
56-
57-
# Derived parameters
58-
dx = L / Nx
59-
dt = C * dx / c
60-
Nt = int(T / dt)
61-
62-
# Create the computational grid
63-
grid = Grid(shape=(Nx + 1,), extent=(L,))
64-
65-
# Create a time-varying field
66-
# time_order=2 because we have second derivative in time
67-
# space_order=2 for standard second-order accuracy
68-
u = TimeFunction(name='u', grid=grid, time_order=2, space_order=2)
69-
70-
# Set initial condition: a Gaussian pulse
71-
x = grid.dimensions[0]
72-
x_coord = 0.5 * L # Center of domain
73-
sigma = 0.1 # Width of pulse
74-
u.data[0, :] = np.exp(-((np.linspace(0, L, Nx+1) - x_coord)**2) / (2*sigma**2))
75-
u.data[1, :] = u.data[0, :] # Zero initial velocity
76-
77-
# Define the update equation
78-
# u.forward is u at time n+1, u is at time n, u.backward is at time n-1
79-
# u.dx2 is the second spatial derivative
80-
eq = Eq(u.forward, 2*u - u.backward + (c*dt)**2 * u.dx2)
81-
82-
# Create the operator
83-
op = Operator([eq])
84-
85-
# Run the simulation
86-
op(time=Nt, dt=dt)
87-
88-
# The solution is now in u.data
89-
print(f"Simulation complete: {Nt} time steps")
90-
print(f"Max amplitude at t={T}: {np.max(np.abs(u.data[0, :])):.6f}")
91-
```
46+
{{< include snippets/first_pde_wave1d.qmd >}}
9247
9348
### Understanding the Code
9449
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
::: {.callout-note title="Snippet (tested)"}
2+
This code is included from `src/book_snippets/absorbing_bc_right_wave.py` and exercised by `pytest` (see `tests/test_book_snippets.py`).
3+
:::
4+
5+
```python
6+
{{< include ../../src/book_snippets/absorbing_bc_right_wave.py >}}
7+
```

0 commit comments

Comments
 (0)