0% found this document useful (0 votes)
116 views

Finite Difference Methods For Wave Motion

This document describes finite difference methods for solving the 1D and multi-dimensional wave equation numerically. It introduces discretization of the wave equation on a grid, discusses implementation of boundary conditions like reflecting boundaries, and generalization to problems with variable wave velocity or damping. It also covers analysis of the numerical stability and dispersion of the resulting finite difference schemes. The document outlines exercises and provides Python code for a general 1D wave equation solver.

Uploaded by

daniel
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
116 views

Finite Difference Methods For Wave Motion

This document describes finite difference methods for solving the 1D and multi-dimensional wave equation numerically. It introduces discretization of the wave equation on a grid, discusses implementation of boundary conditions like reflecting boundaries, and generalization to problems with variable wave velocity or damping. It also covers analysis of the numerical stability and dispersion of the resulting finite difference schemes. The document outlines exercises and provides Python code for a general 1D wave equation solver.

Uploaded by

daniel
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 51

Finite difference methods for wave motion 6 Generalization: reflecting boundaries 31

6.1 Neumann boundary condition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31


6.2 Discretization of derivatives at the boundary . . . . . . . . . . . . . . . . . . . . 32
6.3 Implementation of Neumann conditions . . . . . . . . . . . . . . . . . . . . . . . 32
Hans Petter Langtangen1,2 6.4 Index set notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
6.5 Verifying the implementation of Neumann conditions . . . . . . . . . . . . . . . . 36
1
Center for Biomedical Computing, Simula Research Laboratory 6.6 Alternative implementation via ghost cells . . . . . . . . . . . . . . . . . . . . . . 37
2
Department of Informatics, University of Oslo
7 Generalization: variable wave velocity 39
7.1 The model PDE with a variable coefficient . . . . . . . . . . . . . . . . . . . . . . 40
Nov 3, 2016 7.2 Discretizing the variable coefficient . . . . . . . . . . . . . . . . . . . . . . . . . . 40
7.3 Computing the coefficient between mesh points . . . . . . . . . . . . . . . . . . . 41
7.4 How a variable coefficient affects the stability . . . . . . . . . . . . . . . . . . . . 42
7.5 Neumann condition and a variable coefficient . . . . . . . . . . . . . . . . . . . . 42
This is still a preliminary version. 7.6 Implementation of variable coefficients . . . . . . . . . . . . . . . . . . . . . . . . 43
7.7 A more general PDE model with variable coefficients . . . . . . . . . . . . . . . . 44
7.8 Generalization: damping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Contents
8 Building a general 1D wave equation solver 45
1 Simulation of waves on a string 5 8.1 User action function as a class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
1.1 Discretizing the domain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 8.2 Pulse propagation in two media . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.2 The discrete solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3 Fulfilling the equation at the mesh points . . . . . . . . . . . . . . . . . . . . . . 6 9 Exercises 49
1.4 Replacing derivatives by finite differences . . . . . . . . . . . . . . . . . . . . . . 6
1.5 Formulating a recursive algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . 8 10 Analysis of the difference equations 56
1.6 Sketch of an implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 10.1 Properties of the solution of the wave equation . . . . . . . . . . . . . . . . . . . 56
10.2 More precise definition of Fourier representations . . . . . . . . . . . . . . . . . . 57
2 Verification 10 10.3 Stability . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
2.1 A slightly generalized model problem . . . . . . . . . . . . . . . . . . . . . . . . . 10 10.4 Numerical dispersion relation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.2 Using an analytical solution of physical significance . . . . . . . . . . . . . . . . . 11 10.5 Extending the analysis to 2D and 3D . . . . . . . . . . . . . . . . . . . . . . . . . 63
2.3 Manufactured solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.4 Constructing an exact solution of the discrete equations . . . . . . . . . . . . . . 13 11 Finite difference methods for 2D and 3D wave equations 66
11.1 Multi-dimensional wave equations . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
3 Implementation 14 11.2 Mesh . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.1 Callback function for user-specific actions . . . . . . . . . . . . . . . . . . . . . . 15 11.3 Discretization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.2 The solver function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.3 Verification: exact quadratic solution . . . . . . . . . . . . . . . . . . . . . . . . . 16 12 Implementation 70
3.4 Visualization: animating the solution . . . . . . . . . . . . . . . . . . . . . . . . . 17 12.1 Scalar computations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
3.5 Running a case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 12.2 Vectorized computations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.6 Working with a scaled PDE model . . . . . . . . . . . . . . . . . . . . . . . . . . 21 12.3 Verification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

4 Vectorization 22 13 Using classes to implement a simulator 77


4.1 Operations on slices of arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
14 Exercises 77
4.2 Finite difference schemes expressed as slices . . . . . . . . . . . . . . . . . . . . . 25
4.3 Verification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 15 Applications of wave equations 78
4.4 Efficiency measurements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 15.1 Waves on a string . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.5 Remark on the updating of arrays . . . . . . . . . . . . . . . . . . . . . . . . . . 28 15.2 Waves on a membrane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
15.3 Elastic waves in a rod . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5 Exercises 29
15.4 The acoustic model for seismic waves . . . . . . . . . . . . . . . . . . . . . . . . . 82

2
15.5 Sound waves in liquids and gases . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 List of Exercises, Problems, and Projects
15.6 Spherical waves . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Exercise 1 Simulate a standing wave p. 29
15.7 The linear shallow water equations . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Exercise 2 Add storage of solution in a user action function ... p. 29
15.8 Waves in blood vessels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Exercise 3 Use a class for the user action function p. 30
15.9 Electromagnetic waves . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Exercise 4 Compare several Courant numbers in one movie p. 30
16 Exercises 89 Project 5 Calculus with 1D mesh functions p. 30
Exercise 6 Find the analytical solution to a damped wave ... p. 49
References 99 Problem 7 Explore symmetry boundary conditions p. 50
Exercise 8 Send pulse waves through a layered medium p. 50
Index 101 Exercise 9 Explain why numerical noise occurs p. 50
Exercise 10 Investigate harmonic averaging in a 1D model p. 50
Problem 11 Implement open boundary conditions p. 51
Exercise 12 Implement periodic boundary conditions p. 52
Exercise 13 Compare discretizations of a Neumann condition ...
Exercise 14 Verification by a cubic polynomial in space p. 53
Exercise 15 Check that a solution fulfills the discrete ... p. 77
Project 16 Calculus with 2D mesh functions p. 77
Exercise 17 Implement Neumann conditions in 2D p. 78
Exercise 18 Test the efficiency of compiled loops in 3D p. 78
Exercise 19 Simulate waves on a non-homogeneous string p. 89
Exercise 20 Simulate damped waves on a string p. 90
Exercise 21 Simulate elastic waves in a rod p. 90
Exercise 22 Simulate spherical waves p. 90
Problem 23 Earthquake-generated tsunami over a subsea ... p. 91
Problem 24 Earthquake-generated tsunami over a 3D hill p. 93
Problem 25 Investigate Matplotlib for visualization p. 94
Problem 26 Investigate visualization packages p. 94
Problem 27 Implement loops in compiled languages p. 94
Exercise 28 Simulate seismic waves in 2D p. 94
Project 29 Model 3D acoustic waves in a room p. 95
Project 30 Solve a 1D transport equation p. 95
Problem 31 General analytical solution of a 1D damped ... p. 98
Problem 32 General analytical solution of a 2D damped ... p. 99

3 4
A very wide range of physical processes lead to wave motion, where signals are propagated One may view the mesh as two-dimensional in the x, t plane, consisting of points (xi , tn ), with
through a medium in space and time, normally with little or no permanent movement of the i = 0, . . . , Nx and n = 0, . . . , Nt .
medium itself. The shape of the signals may undergo changes as they travel through matter, but
usually not so much that the signals cannot be recognized at some later point in space and time. Uniform meshes. For uniformly distributed mesh points we can introduce the constant mesh
Many types of wave motion can be described by the equation utt = ∇ · (c2 ∇u) + f , which we will spacings ∆t and ∆x. We have that
solve in the forthcoming text by finite difference methods.
xi = i∆x, i = 0, . . . , Nx , tn = n∆t, n = 0, . . . , Nt . (9)
1 Simulation of waves on a string We also have that ∆x = xi − xi−1 , i = 1, . . . , Nx , and ∆t = tn − tn−1 , n = 1, . . . , Nt . Figure 1
displays a mesh in the x, t plane with Nt = 5, Nx = 5, and constant mesh spacings.
We begin our study of wave equations by simulating one-dimensional waves on a string, say on a
guitar or violin. Let the string in the deformed state coincide with the interval [0, L] on the x
1.2 The discrete solution
axis, and let u(x, t) be the displacement at time t in the y direction of a point initially at x. The
displacement function u is governed by the mathematical model The solution u(x, t) is sought at the mesh points. We introduce the mesh function uni , which
approximates the exact solution at the mesh point (xi , tn ) for i = 0, . . . , Nx and n = 0, . . . , Nt .
Using the finite difference method, we shall develop algebraic equations for computing the mesh
∂2u ∂2u function.
= c2 2 , x ∈ (0, L), t ∈ (0, T ] (1)
∂t2 ∂x
u(x, 0) = I(x), x ∈ [0, L] (2)
1.3 Fulfilling the equation at the mesh points

u(x, 0) = 0, x ∈ [0, L] (3) In the finite difference method, we relax the condition that (1) holds at all points in the space-time
∂t
u(0, t) = 0, t ∈ (0, T ] (4) domain (0, L) × (0, T ] to the requirement that the PDE is fulfilled at the interior mesh points
only:
u(L, t) = 0, t ∈ (0, T ] (5)
∂2 ∂2
The constant c and the function I(x) must be prescribed. u(xi , tn ) = c2 2 u(xi , tn ), (10)
∂t2 ∂x
Equation (1) is known as the one-dimensional wave equation. Since this PDE contains a
second-order derivative in time, we need two initial conditions. The condition (2) specifies the for i = 1, . . . , Nx − 1 and n = 1, . . . , Nt − 1. For n = 0 we have the initial conditions u = I(x)
initial shape of the string, I(x), and (3) expresses that the initial velocity of the string is zero. In and ut = 0, and at the boundaries i = 0, Nx we have the boundary condition u = 0.
addition, PDEs need boundary conditions, give here as (4) and (5). These two conditions specify
that the string is fixed at the ends, i.e., that the displacement u is zero. 1.4 Replacing derivatives by finite differences
The solution u(x, t) varies in space and time and describes waves that move with velocity c to
The second-order derivatives can be replaced by central differences. The most widely used
the left and right.
difference approximation of the second-order derivative is
Sometimes we will use a more compact notation for the partial derivatives to save space:

∂2u ∂2 un+1 − 2uni + un−1


ut =
∂u
, utt = 2 , (6) u(xi , tn ) ≈ i i
.
∂t ∂t ∂t2 ∆t2
and similar expressions for derivatives with respect to other variables. Then the wave equation It is convenient to introduce the finite difference operator notation
can be written compactly as utt = c2 uxx .
un+1 − 2uni + un−1
The PDE problem (1)-(5) will now be discretized in space and time by a finite difference [Dt Dt u]ni =
i i
.
∆t2
method.
A similar approximation of the second-order derivative in the x direction reads
1.1 Discretizing the domain ∂2 un − 2uni + uni−1
u(xi , tn ) ≈ i+1 = [Dx Dx u]ni .
The temporal domain [0, T ] is represented by a finite number of mesh points ∂x2 ∆x2

0 = t0 < t1 < t2 < · · · < tNt −1 < tNt = T . (7)


Similarly, the spatial domain [0, L] is replaced by a set of mesh points

0 = x0 < x1 < x2 < · · · < xNx −1 < xNx = L . (8)

5 6
Algebraic version of the PDE. We can now replace the derivatives in (10) and get seems appropriate. In operator notation the initial condition is written as

un+1 − 2uni + uin−1 n


2 ui+1 − 2uni + uni−1 [D2t u]ni = 0, n = 0.
i
=c , (11)
∆t2 ∆x2
Writing out this equation and ordering the terms give
or written more compactly using the operator notation:
un−1 = un+1 , i = 0, . . . , Nx , n = 0 . (13)
[Dt Dt u = c2 Dx Dx ]ni . (12) i i

The other initial condition can be computed by


Interpretation of the equation as a stencil. A typical feature of (11) is that it involves u
values from neighboring points only: un+1 i , uni±1 , uni , and un−1
i . The circles in Figure 1 illustrate u0i = I(xi ), i = 0, . . . , Nx .
such neighboring mesh points that contributes to an algebraic equation. In this particular case,
we have sampled the PDE at the point (2, 2) and constructed (11), which then involves a coupling 1.5 Formulating a recursive algorithm
of u12 , u21 , u22 , u23 , and u32 . The term stencil is often used about the algebraic equation at a mesh
point, and the geometry of a typical stencil is illustrated in Figure 1. One also often refers to We assume that uni and un−1i are already computed for i = 0, . . . , Nx . The only unknown quantity
the algebraic equations as discrete equations, (finite) difference equations or a finite difference in (11) is therefore un+1
i , which we can solve for:
scheme. 
un+1
i = −un−1
i + 2uni + C 2 uni+1 − 2uni + uni−1 , (14)

Stencil at interior point where we have introduced the parameter


5 ∆t
C=c , (15)
∆x
known as the Courant number.
4

C is the key parameter in the discrete wave equation.


3 We see that the discrete version of the PDE features only one parameter, C, which is therefore
index n

the key parameter that governs the quality of the numerical solution (see Section 10 for
details). Both the primary physical parameter c and the numerical parameters ∆x and ∆t
2 are lumped together in C. Note that C is a dimensionless parameter.

Given that un−1


i and uni are computed for i = 0, . . . , Nx , we find new values at the next time
1 level by applying the formula (14) for i = 1, . . . , Nx − 1. Figure 1 illustrates the points that
are used to compute u32 . For the boundary points, i = 0 and i = Nx , we apply the boundary
conditions un+1
i = 0.
0 A problem with (14) arises when n = 0 since the formula for u1i involves u−1 i , which is an
0 1 2 3 4 5 undefined quantity outside the time mesh (and the time domain). However, we can use the initial
index i condition (13) in combination with (14) when n = 0 to eliminate ui and arrive at a special
−1

formula for u1i :


Figure 1: Mesh in space and time. The circles show points connected in a finite difference 1 
equation. u1i = u0i − C 2 u0i+1 − 2u0i + u0i−1 . (16)
2
Figure 2 illustrates how (16) connects four instead of five points: u12 , u01 , u02 , and u03 .
We can now summarize the computational algorithm:
Algebraic version of the initial conditions. We also need to replace the derivative in the
initial condition (3) by a finite difference approximation. A centered difference of the type 1. Compute u0i = I(xi ) for i = 0, . . . , Nx

∂ u1i − u−1 2. Compute u1i by (16) and set u1i = 0 for the boundary points i = 0 and i = Nx , for
u(xi , tn ) ≈ i
= [D2t u]0i , n = 1, 2, . . . , N − 1,
∂t 2∆t

7 8
# Given mesh points as arrays x and t (x[i], t[n])
Stencil at interior point dx = x[1] - x[0]
dt = t[1] - t[0]
5 C = c*dt/dx # Courant number
Nt = len(t)-1
C2 = C**2 # Help variable in the scheme

4 # Set initial condition u(x,0) = I(x)


for i in range(0, Nx+1):
u_1[i] = I(x[i])
# Apply special formula for first step, incorporating du/dt=0
3 for i in range(1, Nx):
index n

u[i] = u_1[i] - 0.5*C**2(u_1[i+1] - 2*u_1[i] + u_1[i-1])


u[0] = 0; u[Nx] = 0 # Enforce boundary conditions

2 # Switch variables before next step


u_2[:], u_1[:] = u_1, u
for n in range(1, Nt):
# Update all inner mesh points at time t[n+1]
1 for i in range(1, Nx):
u[i] = 2u_1[i] - u_2[i] - \
C**2(u_1[i+1] - 2*u_1[i] + u_1[i-1])
# Insert boundary conditions
0 u[0] = 0; u[Nx] = 0
0 1 2 3 4 5
# Switch variables before next step
index i u_2[:], u_1[:] = u_1, u

Figure 2: Modified stencil for the first time step.


2 Verification
3. For each time level n = 1, 2, . . . , Nt − 1
Before implementing the algorithm, it is convenient to add a source term to the PDE (1) since it
(a) apply (14) to find un+1
i for i = 1, . . . , Nx − 1 gives us more freedom in finding test problems for verification. Physically, a source term acts as
(b) set un+1
i = 0 for the boundary points i = 0, i = Nx . a generation of waves in the interior of the domain.

The algorithm essentially consists of moving a finite difference stencil through all the mesh points, 2.1 A slightly generalized model problem
which can be seen as an animation in a web page1 or a movie file2 .
We now address the following extended initial-boundary value problem for one-dimensional wave
phenomena:
1.6 Sketch of an implementation
In a Python implementation of this algorithm, we use the array elements u[i] to store un+1 ,
i utt = c2 uxx + f (x, t), x ∈ (0, L), t ∈ (0, T ] (17)
u_1[i] to store uni , and u_2[i] to store un−1 i . Our naming convention is use u for the unknown
new spatial field to be computed, u_1 as the solution at one time step back in time, u_2 as the u(x, 0) = I(x), x ∈ [0, L] (18)
solution two time steps back in time and so forth. ut (x, 0) = V (x), x ∈ [0, L] (19)
The algorithm only involves the three most recent time levels, so we need only three arrays u(0, t) = 0, t>0 (20)
for un+1 , uni , and un−1 , i = 0, . . . , Nx . Storing all the solutions in a two-dimensional array of
i i u(L, t) = 0, t>0 (21)
size (Nx + 1) × (Nt + 1) would be possible in this simple one-dimensional PDE problem, but is
normally out of the question in three-dimensional (3D) and large two-dimensional (2D) problems. Sampling the PDE at (xi , tn ) and using the same finite difference approximations as above,
We shall therefore, in all our PDE solving programs, have the unknown in memory at as few time yields
levels as possible.
The following Python snippet realizes the steps in the computational algorithm. [Dt Dt u = c2 Dx Dx u + f ]ni . (22)
1 http://tinyurl.com/opdfafk/pub/mov-wave/wave1D_PDE_Dirichlet_stencil_gpl/index.html
2 http://tinyurl.com/opdfafk/pub/mov-wave/wave1D_PDE_Dirichlet_stencil_gpl/movie.ogg Writing this out and solving for the unknown un+1
i results in

9 10
The initial conditions become
un+1
i = −un−1
i + 2uni + C 2 (uni+1 − 2uni + uni−1 ) + ∆t2 fin . (23)
The equation for the first time step must be rederived. The discretization of the initial u(x, 0) =I(x) = 0,
condition ut = V (x) at t = 0 becomes
ut (x, 0) = V (x) = x(L − x) .
[D2t u = V ]0i ⇒ u−1
i = u1i − 2∆tVi , To verify the code, we compute the convergence rates in a series of simulations, letting each
which, when inserted in (23) for n = 0, gives the special formula simulation use a finer mesh than the previous one. Such empirical estimation of convergence
rates tests rely on an assumption that some measure E of the numerical error is related to the
1  1
discretization parameters through
u1i = u0i − ∆tVi + C 2 u0i+1 − 2u0i + u0i−1 + ∆t2 fi0 . (24)
2 2
E = Ct ∆tr + Cx ∆xp ,
2.2 Using an analytical solution of physical significance
where Ct , Cx , r, and p are constants. The constants r and p are known as the convergence rates
Many wave problems feature sinusoidal oscillations in time and space. For example, the original in time and space, respectively. From the accuracy in the finite difference approximations, we
PDE problem (1)-(5) allows an exact solution expect r = p = 2, since the error terms are of order ∆t2 and ∆x2 . This is confirmed by truncation
π  π  error analysis and other types of analysis.
ue (x, t)) = A sin x cos ct . (25) By using an exact solution of the PDE problem, we will next compute the error measure E
L L
on a sequence of refined meshes and see if the rates r = p = 2 are obtained. We will not be
This ue fulfills the PDE with f = 0, boundary conditions ue (0, t) = ue (L, 0) = 0, as well as initial
concerned with estimating the constants Ct and Cx .
conditions I(x) = A sin L π
x and V = 0.
It is advantageous to introduce a single discretization parameter h = ∆t = ĉ∆x for some
It is common to use such exact solutions of physical interest to verify implementations.
constant ĉ. Since ∆t and ∆x are related through the Courant number, ∆t = C∆x/c, we set
However, the numerical solution uni will only be an approximation to ue (xi , tn ). We have no
h = ∆t, and then ∆x = hc/C. Now the expression for the error measure is greatly simplified:
knowledge of the precise size of the error in this approximation, and therefore we can never
know if discrepancies between uni and ue (xi , tn ) are caused by mathematical approximations or  c r  c r
E = Ct ∆tr + Cx ∆xr = Ct hr + Cx hr = Dhr , D = Ct + Cx .
programming errors. In particular, if a plot of the computed solution uni and the exact one (25) C C
looks similar, many are tempted to claim that the implementation works. However, even if color We choose an initial discretization parameter h0 and run experiments with decreasing h:
plots look nice and the accuracy is “deemed good”, there can still be serious programming errors hi = 2−i h0 , i = 1, 2, . . . , m. Halving h in each experiment is not necessary, but it is a common
present! choice. For each experiment we must record E and h. A standard choice of error measure is the
The only way to use exact physical solutions like (25) for serious and thorough verification is `2 or `∞ norm of the error mesh function eni :
to run a series of finer and finer meshes, measure the integrated error in each mesh, and from
this information estimate the empirical convergence rate of the method. An introduction to the
! 12
computing convergence rates is given in Section ?? in [2]. Nt X
X Nx
In the present problem, one expects the method to have a convergence rate of 2 (see Section 10), E= ||eni ||`2 = ∆t∆x (eni )2 , eni = ue (xi , tn ) − uni , (26)
so if the computed rates are close to 2 on a sufficiently mesh, we have good evidence that the n=0 i=0
implementation is free of programming mistakes. E= ||eni ||`∞ = max |ein | . (27)
i,n

P
2.3 Manufactured solution In Python, one can compute i (eni )2 at each time step and accumulate the value in some sum
variable, say e2_sum. At the final time step one can do sqrt(dt*dx*e2_sum). For the `∞ norm
One problem with the exact solution (25) is that it requires a simplification (V = 0, f = 0) of the
one must compare the maximum error at a time level (e.max()) with the global maximum over
implemented problem (17)-(21). An advantage of using a manufactured solution is that we can
the time domain: e_max = max(e_max, e.max()).
test all terms in the PDE problem. The idea of this approach is to set up some chosen solution and
An alternative error measure is to use a spatial norm at one time step only, e.g., the end time
fit the source term, boundary conditions, and initial conditions to be compatible with the chosen
T (n = Nt ):
solution. Given that our boundary conditions in the implementation are u(0, t) = u(L, t) = 0, we
must choose a solution that fulfills these conditions. One example is
Nx
! 12
X
ue (x, t) = x(L − x) sin t . E = ||eni ||`2 = ∆x (eni )2 , eni = ue (xi , tn ) − uni , (28)
Inserted in the PDE utt = c2 uxx + f we get i=0
E = ||eni ||`∞ = max |eni | . (29)
0≤i≤Nx
−x(L − x) sin t = −c2 2 sin t + f ⇒ f = (2c2 − x(L − x)) sin t .

11 12
The important issue is that our error measure E must be one number that represents the error in Hence,
the simulation. 1 1
[Dt Dt ue ]ni = xi (L − xi )[Dt Dt (1 + t)]n = xi (L − xi ) [Dt Dt t]n = 0 .
Let Ei be the error measure in experiment (mesh) number i and let hi be the corresponding 2 2
discretization parameter (h). With the error model Ei = Dhri , we can estimate r by comparing Similarly, we get that
two consecutive experiments:
1 1
[Dx Dx ue ]ni = (1 + tn )[Dx Dx (xL − x2 )]i = (1 + tn )[LDx Dx x − Dx Dx x2 ]i
Ei+1 = Dhri+1 , 2 2
1
Ei = Dhri . = −2(1 + tn ) .
2
Dividing the two equations eliminates the (uninteresting) constant D. Thereafter, solving for r
Now, fin = 2(1 + 12 tn )c2 , which results in
yields
1 1
ln Ei+1 /Ei [Dt Dt ue − c2 Dx Dx ue − f ]ni = 0 − c2 (−1)2(1 + tn + 2(1 + tn )c2 = 0 .
r= . 2 2
ln hi+1 /hi
Moreover, ue (xi , 0) = I(xi ), ∂ue /∂t = V (xi ) at t = 0, and ue (x0 , t) = ue (xNx , 0) = 0. Also
Since r depends on i, i.e., which simulations we compare, we add an index to r: ri , where the modified scheme for the first time step is fulfilled by ue (xi , tn ).
i = 0, . . . , m − 2, if we have m experiments: (h0 , E0 ), . . . , (hm−1 , Em−1 ). Therefore, the exact solution ue (x, t) = x(L − x)(1 + t/2) of the PDE problem is also an
In our present discretization of the wave equation we expect r = 2, and hence the ri values exact solution of the discrete problem. We can use this result to check that the computed uni
should converge to 2 as i increases. values from an implementation equals ue (xi , tn ) within machine precision, regardless of the mesh
spacings ∆x and ∆t! Nevertheless, there might be stability restrictions on ∆x and ∆t, so the test
2.4 Constructing an exact solution of the discrete equations can only be run for a mesh that is compatible with the stability criterion (which in the present
case is C ≤ 1, to be derived later).
With a manufactured or known analytical solution, as outlined above, we can estimate convergence
rates and see if they have the correct asymptotic behavior. Experience shows that this is a quite
good verification technique in that many common bugs will destroy the convergence rates. A Notice.
significantly better test though, would be to check that the numerical solution is exactly what it
A product of quadratic or linear expressions in the various independent variables, as shown
should be. This will in general require exact knowledge of the numerical error, which we do not
above, will often fulfill both the PDE problem and the discrete equations, and can therefore
normally have (although we in Section 10 establish such knowledge in simple cases). However, it
be very useful solutions for verifying implementations.
is possible to look for solutions where we can show that the numerical error vanishes, i.e., the
However, for 1D wave equations of the type utt = c2 uxx we shall see that there is always
solution of the original continuous PDE problem is also a solution of the discrete equations. This
another much more powerful way of generating exact solutions (which consists in just setting
property often arises if the exact solution of the PDE is a lower-order polynomial. (Truncation
C = 1 (!), as shown in Section 10).
error analysis leads to error measures that involve derivatives of the exact solution. In the present
problem, the truncation error involves 4th-order derivatives of u in space and time. Choosing u
as a polynomial of degree three or less will therefore lead to vanishing error.)
We shall now illustrate the construction of an exact solution to both the PDE itself and the
discrete equations. Our chosen manufactured solution is quadratic in space and linear in time. 3 Implementation
More specifically, we set
This section presents the complete computational algorithm, its implementation in Python code,
1 animation of the solution, and verification of the implementation.
ue (x, t) = x(L − x)(1 + t), (30) A real implementation of the basic computational algorithm from Sections 1.5 and 1.6 can
2
2 be encapsulated in a function, taking all the input data for the problem as arguments. The
which by insertion in the PDE leads to f (x, t) = 2(1+t)c . This ue fulfills the boundary conditions
physical input data consists of c, I(x), V (x), f (x, t), L, and T . The numerical input is the mesh
u = 0 and demands I(x) = x(L − x) and V (x) = 21 x(L − x).
parameters ∆t and ∆x.
To realize that the chosen ue is also an exact solution of the discrete equations, we first remind
Instead of specifying ∆t and ∆x, we can specify one of them and the Courant number C
ourselves that tn = n∆t before we establish that
instead, since having explicit control of the Courant number is convenient when investigating the
numerical method. Many find it natural to prescribe the resolution of the spatial grid and set Nx .
t2n+1 − 2t2n + t2n−1 The solver function can then compute ∆t = CL/(cNx ). However, for comparing u(x, t) curves
[Dt Dt t2 ]n = = (n + 1)2 − 2n2 + (n − 1)2 = 2, (31) (as functions of x) for various Courant numbers it is more convenient to keep ∆t fixed for all C
∆t2
tn+1 − 2tn + tn−1 ((n + 1) − 2n + (n − 1))∆t and let ∆x vary according to ∆x = c∆t/C. With ∆t fixed, all frames correspond to the same
[Dt Dt t]n = = = 0. (32) time t, and this simplifies animations that compare simulations with different mesh resolutions.
∆t2 ∆t2

13 14
Plotting functions of x with different spatial resolution is trivial, so it is easier to let ∆x vary in 0.5*dt**2*f(x[i], t[n])
the simulations than ∆t. u[0] = 0; u[Nx] = 0
if user_action is not None:
user_action(u, x, t, 1)
3.1 Callback function for user-specific actions
# Switch variables before next step
The solution at all spatial points at a new time level is stored in an array u of length Nx + 1. We u_2[:] = u_1; u_1[:] = u
need to decide what do to with this solution, e.g., visualize the curve, analyze the values, or write
for n in range(1, Nt):
the array to file for later use. The decision about what to do is left to the user in the form of a # Update all inner points at time t[n+1]
user-suppled supplied function for i in range(1, Nx):
u[i] = - u_2[i] + 2*u_1[i] + \
C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \
user_action(u, x, t, n) dt**2*f(x[i], t[n])

where u is the solution at the spatial points x at time t[n]. The user_action function is call # Insert boundary conditions
u[0] = 0; u[Nx] = 0
from the solver at each time level n. if user_action is not None:
If the user wants to plot the solution or store the solution at a time point, she needs to write if user_action(u, x, t, n+1):
break
such a function and take appropriate actions inside it. We will show examples on many such
user_action functions. # Switch variables before next step
Since the solver function make calls back to the user’s code via such a function, this type of u_2[:] = u_1; u_1[:] = u
function is called a callback function. When writing general software, like our solver function, cpu_time = t0 - time.clock()
which also needs to carry out special problem-dependent actions (like visualization), it is a return u, x, t, cpu_time
common technique to leave those actions to user-supplied callback functions.

3.2 The solver function 3.3 Verification: exact quadratic solution


A first attempt at a solver function is listed below. We use the test problem derived in Section 2.1 for verification. Below is a unit test based on this
test problem and realized as a proper test function (compatible with the unit test frameworks
import numpy as np nose or pytest).
def solver(I, V, f, c, L, dt, C, T, user_action=None):
"""Solve u_tt=c^2*u_xx + f on (0,L)x(0,T].""" def test_quadratic():
Nt = int(round(T/dt)) """Check that u(x,t)=x(L-x)(1+t/2) is exactly reproduced."""
t = np.linspace(0, Nt*dt, Nt+1) # Mesh points in time
dx = dt*c/float(C) def u_exact(x, t):
Nx = int(round(L/dx)) return x*(L-x)*(1 + 0.5*t)
x = np.linspace(0, L, Nx+1) # Mesh points in space
C2 = C**2 # Help variable in the scheme def I(x):
if f is None or f == 0 : return u_exact(x, 0)
f = lambda x, t: 0
if V is None or V == 0: def V(x):
V = lambda x: 0 return 0.5*u_exact(x, 0)

u = np.zeros(Nx+1) # Solution array at new time level def f(x, t):


u_1 = np.zeros(Nx+1) # Solution at 1 time level back return 2*(1 + 0.5*t)*c**2
u_2 = np.zeros(Nx+1) # Solution at 2 time levels back
L = 2.5
import time; t0 = time.clock() # for measuring CPU time c = 1.5
C = 0.75
# Load initial condition into u_1 Nx = 6 # Very coarse mesh for this exact test
for i in range(0,Nx+1): dt = C*(L/Nx)/c
u_1[i] = I(x[i]) T = 18

if user_action is not None: def assert_no_error(u, x, t, n):


user_action(u_1, x, t, 0) u_e = u_exact(x, t[n])
diff = np.abs(u - u_e).max()
# Special formula for first time step tol = 1E-13
n = 0 assert diff < tol
for i in range(1, Nx):
u[i] = u_1[i] + dt*V(x[i]) + \ solver(I, V, f, c, L, dt, C, T,
0.5*C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \ user_action=assert_no_error)

15 16
When this function resides in the file wave1D_u0.py, one can run ether py.test or nosetests, if tool == ’matplotlib’:
import matplotlib.pyplot as plt
Terminal plot_u = PlotMatplotlib()
Terminal> py.test -s -v wave1D_u0.py elif tool == ’scitools’:
Terminal> nosetests -s -v wave1D_u0.py import scitools.std as plt # scitools.easyviz interface
plot_u = plot_u_st
import time, glob, os

to automatically run all test functions with name test_*(). # Clean up old movie frames
for filename in glob.glob(’tmp_*.png’):
os.remove(filename)
3.4 Visualization: animating the solution # Call solver and do the simulaton
user_action = plot_u if animate else None
Now that we have verified the implementation it is time to do a real computation where we also u, x, t, cpu = solver_function(
display the evolution of the waves on the screen. Since the solver function knows nothing about I, V, f, c, L, dt, C, T, user_action)
what type of visualizations we may want, it calls the callback function user_action(u, x, t, n).
# Make video files
We must therefore write this function and find the proper statements for plotting the solution. fps = 4 # frames per second
codec2ext = dict(flv=’flv’, libx264=’mp4’, libvpx=’webm’,
libtheora=’ogg’) # video formats
Function for administering the simulation. The following viz function filespec = ’tmp_%04d.png’
movie_program = ’ffmpeg’ # or ’avconv’
1. defines a user_action callback function for plotting the solution at each time level, for codec in codec2ext:
ext = codec2ext[codec]
cmd = ’%(movie_program)s -r %(fps)d -i %(filespec)s ’\
2. calls the solver function, and ’-vcodec %(codec)s movie.%(ext)s’ % vars()
os.system(cmd)
3. combines all the plots (in files) to video in different formats.
if tool == ’scitools’:
# Make an HTML play for showing the animation in a browser
plt.movie(’tmp_*.png’, encoder=’html’, fps=fps,
output_file=’movie.html’)
def viz( return cpu
I, V, f, c, L, dt, C, T, # PDE paramteres
umin, umax, # Interval for u in plots
animate=True, # Simulation with animation?
tool=’matplotlib’, # ’matplotlib’ or ’scitools’ Dissection of the code. The viz function can either use SciTools or Matplotlib for visualizing
solver_function=solver, # Function with numerical algorithm the solution. The user_action function based on SciTools is called plot_u_st, while the
):
"""Run solver and visualize u at each time level.""" user_action function based on Matplotlib is a bit more complicated as it is realized as a class
and needs statements that differ from those for making static plots. SciTools can utilize both
def plot_u_st(u, x, t, n):
"""user_action function for solver.""" Matplotlib and Gnuplot (and many other plotting programs) for doing the graphics, but Gnuplot
plt.plot(x, u, ’r-’, is a relevant choice for large Nx or in two-dimensional problems as Gnuplot is significantly faster
xlabel=’x’, ylabel=’u’, than Matplotlib for screen animations.
axis=[0, L, umin, umax],
title=’t=%f’ % t[n], show=True) A function inside another function, like plot_u_st in the above code segment, has access to
# Let the initial condition stay on the screen for 2 and remembers all the local variables in the surrounding code inside the viz function (!). This is
# seconds, else insert a pause of 0.2 s between each plot
time.sleep(2) if t[n] == 0 else time.sleep(0.2) known in computer science as a closure and is very convenient to program with. For example, the
plt.savefig(’frame_%04d.png’ % n) # for movie making plt and time modules defined outside plot_u are accessible for plot_u_st when the function is
class PlotMatplotlib: called (as user_action) in the solver function. Some may think, however, that a class instead
def __call__(self, u, x, t, n): of a closure is a cleaner and easier-to-understand implementation of the user action function, see
"""user_action function for solver.""" Section 8.
if n == 0:
plt.ion() The plot_u_st function just makes a standard SciTools plot command for plotting u as a
self.lines = plt.plot(x, u, ’r-’) function of x at time t[n]. To achieve a smooth animation, the plot command should take
plt.xlabel(’x’); plt.ylabel(’u’) keyword arguments instead of being broken into separate calls to xlabel, ylabel, axis, time,
plt.axis([0, L, umin, umax])
plt.legend([’t=%f’ % t[n]], loc=’lower left’) and show. Several plot calls will automatically cause an animation on the screen. In addition,
else: we want to save each frame in the animation to file. We then need a filename where the frame
self.lines[0].set_ydata(u)
plt.legend([’t=%f’ % t[n]], loc=’lower left’) number is padded with zeros, here tmp_0000.png, tmp_0001.png, and so on. The proper printf
plt.draw() construction is then tmp_%04d.png.
time.sleep(2) if t[n] == 0 else time.sleep(0.2)
plt.savefig(’tmp_%04d.png’ % n) # for movie making

17 18
The solver is called with an argument plot_u as user_function. If the user chooses to use to plots of every 5 frames. The default value skip_frame=1 plots every frame. The total number
SciTools, plot_u is the plot_u_st callback function, but for Matplotlib it is an instance of the of time levels (i.e., maximum possible number of frames) is the length of t, t.size (or len(t)),
class PlotMatplotlib. Also this class makes use of variables defined in the viz function: plt so if we want num_frames frames in the animation, we need to plot every t.size/num_frames
and time. With Matplotlib, one has to make the first plot the standard way, and then update frames:
the y data in the plot at every time level. The update requires active use of the returned value
from plt.plot in the first plot. This value would need to be stored in a local variable if we were skip_frame = int(t.size/float(num_frames))
to use a closure for the user_action function when doing the animation with Matplotlib. It is if n % skip_frame == 0 or n == t.size-1:
st.plot(x, u, ’r-’, ...)
much easier to store the variable as a class attribute self.lines. Since the class is essentially
a function, we implement the function as the special method __call__ such that the instance The initial condition (n=0) included by n % skip_frame == 0, as well as every skip_frame-th
plot_u(u, x, t, n) can be called as a standard callback function from solver. frame. As n % skip_frame == 0 will very seldom be true for the very final frame, we must also
check if n == t.size-1 to get the final frame included.
Making movie files. From the frame_*.png files containing the frames in the animation we A simple choice of numbers may illustrate the formulas: say we have 801 frames in total
can make video files. We use the ffmpeg (or avconv) program to combine individual plot files (t.size) and we allow only 60 frames to be plotted. Then we need to plot every 801/60 frame,
to movies in modern formats: Flash, MP4, Webm, and Ogg. A typical ffmpeg (or avconv) which with integer division yields 13 as every. Using the mod function, n % every, this operation
command for creating a movie file in Ogg format with 4 frames per second built from a collection is zero every time n can be divided by 13 without a remainder. That is, the if test is true when
of plot files with names generated by frame_%04d.png, look like n equals 0, 13, 26, 39, ..., 780, 801. The associated code is included in the plot_u function in the
Terminal
file wave1D_u0v.py3 .
Terminal> ffmpeg -r 4 -i frame_%04d.png -c:v libtheora movie.ogg
3.5 Running a case
The different formats require different video encoders (-c:v) to be installed: Flash applies flv, The first demo of our 1D wave equation solver concerns vibrations of a string that is initially
WebM applies libvpx, and MP4 applies libx264: deformed to a triangular shape, like when picking a guitar string:

ax/x0 , x < x0 ,
I(x) = (33)
Terminal

Terminal> ffmpeg -r 4 -i frame_%04d.png -c:v flv movie.flv a(L − x)/(L − x0 ), otherwise


Terminal> ffmpeg -r 4 -i frame_%04d.png -c:v libvpx movie.webm
Terminal> ffmpeg -r 4 -i frame_%04d.png -c:v libx264 movie.mp4 We choose L = 75 cm, x0 = 0.8L, a = 5 mm, and a time frequency ν = 440 Hz. The relation
between the wave speed c and ν is c = νλ, where λ is the wavelength, taken as 2L because the
longest wave on the string form half a wavelength. There is no external force, so f = 0, and the
Players like vlc, mplayer, gxine, and totem can be used to play these movie files. string is at rest initially so that V = 0.
Note that padding the frame counter with zeros in the frame_*.png files, as specified by the Regarding numerical parameters, we need to specify a ∆t. Sometimes it is more natural to
%04d format, is essential so that the wildcard notation frame_*.png expands to the correct set think of a spatial resolution instead of a time step. A natural semi-coarse spatial resolution
of files. in the present problem is Nx = 50. We can then choose the associated ∆t (as required by
The viz function creates a ffmpeg or avconv command with the proper arguments for each of the viz and solver functions) as the stability limit: ∆t = L/(Nx c). This is the ∆t to be
the formats Flash, MP4, WebM, and Ogg. The task is greatly simplified by having a codec2ext specified, but notice that if C < 1, the actual ∆x computed in solver gets larger than L/Nx :
dictionary for mapping video codec names to filename extensions. Only two formats are actually ∆x = c∆t/C = L/(Nx C). (The reason is that we fix ∆t and adjust ∆x, so if C gets smaller, the
needed to ensure that all browsers can successfully play the video: MP4 and WebM. code implements this effect in terms of a larger ∆x.)
Some animations consisting of a large number of plot files may not be properly combined A function for setting the physical and numerical parameters and calling viz in this application
into a video using ffmpeg or avconv. A method that always works is to play the PNG files as goes as follows:
an animation in a browser using JavaScript code in an HTML file. The SciTools package has a
function movie (or a stand-alone command scitools movie) for creating such an HTML player. def guitar(C):
The plt.movie call in the viz function shows how the function is used. The file movie.html can """Triangular wave (pulled guitar string)."""
L = 0.75
be loaded into a browser and features a user interface where the speed of the animation can be x0 = 0.8*L
controlled. Note that the movie in this case consists of the movie.html file and all the frame files a = 0.005
tmp_*.png. freq = 440
wavelength = 2*L
c = freq*wavelength
Skipping frames for animation speed. Sometimes the time step is small and T is large, omega = 2*pi*freq
num_periods = 1
leading to an inconveniently large number of plot files and a slow animation on the screen. The T = 2*pi/omega*num_periods
solution to such a problem is to decide on a total number of frames in the animation, num_frames, 3 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_u0v.py
and plot the solution only for every skip_frame frames. For example, setting skip_frame=5 leads

19 20
# Choose dt the same as the stability limit for Nx=50 In the common case V = 0 we see that there are no physical parameters to be estimated in the
dt = L/50./c PDE model!
def I(x): If we have a program implemented for the physical wave equation with dimensions, we can
return a*x/x0 if x < x0 else a/(L-x0)*(L-x) obtain the dimensionless, scaled version by setting c = 1. The initial condition of a guitar string,
umin = -1.2*a; umax = -umin given in (33), gets its scaled form by choosing a = 1, L = 1, and x0 ∈ [0, 1]. This means that we
cpu = viz(I, 0, 0, c, L, dt, C, T, umin, umax, only need to decide on the x0 value as a fraction of unity, because the scaled problem corresponds
animate=True, tool=’scitools’) to setting all other parameters to unity. In the code we can just set a=c=L=1, x0=0.8, and there
is no need to calculate with wavelengths and frequencies to estimate c!
The associated program has the name wave1D_u0.py4 . Run the program and watch the movie of The only non-trivial parameter to estimate in the scaled problem is the final end time of the
the vibrating string5 . simulation, or more precisely, how it relates to periods in periodic solutions in time, since we often
want to express the end time as a certain number of periods. The period in the dimensionless
3.6 Working with a scaled PDE model problem is 2, so the end time can be set to the desired number of periods times 2.
Why the dimensionless period is 2 can be explained by the following reasoning. Suppose as u
Depending on the model, it may be a substantial job to establish consistent and relevant physical
behaves as cos(ωt) in time in variables with dimension. The corresponding period is then P = 2π/ω,
parameter values for a case. The guitar string example illustrates the point. However, by
but we need to estimate ω. A typical solution of the wave equation is u(x, t) = A cos(kx) cos(ωt),
scaling the mathematical problem we can often reduce the need to estimate physical parameters
where A is an amplitude and k is related to the wave length λ in space: λ = 2π/k. Both λ and A
dramatically. The scaling technique consists of introducing new independent and dependent
will be given by the initial condition I(x). Inserting this u(x, t) in the PDE yields −ω 2 = −c2 k 2 ,
variables, with the aim that the absolute value of these is not very large or small, but preferably
i.e., ω = kc. The period is therefore P = 2π/(kc). If the boundary conditions are u(0, t) = u(0, L),
around unity in size. We introduce the dimensionless variables
we need to have kL = nπ for integer n. The period becomes P = 2L/nc. The longest period is
x c u P = 2L/c. The dimensionless period is P̃ is obtained by dividing P by the time scale L/c, which
x̄ =, t̄ = t, ū = .
L L a results in P̃ = 2. Shorter waves in the initial condition will have a dimensionless shorter period
Here, L is a typical length scale, e.g., the length of the domain, and a is a typical size of u, e.g., P̃ = 2/n (n > 1).
determined from the initial condition: a = maxx |I(x)|.
Inserting these new variables in the PDE and noting that
4 Vectorization
∂u aL ∂ ū
= , The computational algorithm for solving the wave equation visits one mesh point at a time and
∂t c ∂ t̄
evaluates a formula for the new value un+1 at that point. Technically, this is implemented by
by the chain rule, one gets i
a loop over array elements in a program. Such loops may run slowly in Python (and similar
a2 L2 ∂ 2 ū a2 c2 ∂ 2 ū interpreted languages such as R and MATLAB). One technique for speeding up loops is to perform
= 2 , operations on entire arrays instead of working with one element at a time. This is referred to as
c2 ∂ t̄2 L ∂ x̄2
vectorization, vector computing, or array computing. Operations on whole arrays are possible if
in case f = 0. Dropping the bars, we arrive at the scaled PDE the computations involving each element is independent of each other and therefore can, at least
in principle, be performed simultaneously. Vectorization not only speeds up the code on serial
∂2u ∂2u
= , (34) computers, but also makes it easy to exploit parallel computing.
∂t2 ∂x2
2
which has not parameter c anymore. The initial conditions are scaled as
4.1 Operations on slices of arrays
aū(x̄, 0) = I(Lx̄) Efficient computing with numpy arrays demands that we avoid loops and compute with entire
arrays at once (or at least large portions of them). Consider this calculation of differences
and
di = ui+1 − ui :
a ∂ ū
(x̄, 0) = V (Lx̄), n = u.size
L/c ∂ t̄ for i in range(0, n-1):
d[i] = u[i+1] - u[i]
resulting in

I(Lx̄) ∂ ū L All the differences here are independent of each other. The computation of d can therefore
ū(x̄, 0) = , (x̄, 0) = V (Lx̄) . alternatively be done by subtracting the array (u0 , u1 , . . . , un−1 ) from the array where the
maxx |I(x)| ∂ t̄ ac
elements are shifted one index upwards: (u1 , u2 , . . . , un ), see Figure 3. The former subset of the
4 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_u0.py
array can be expressed by u[0:n-1], u[0:-1], or just u[:-1], meaning from index 0 up to, but
5 http://tinyurl.com/opdfafk/pub/mov-wave/guitar_C0.8/index.html
not including, the last element (-1). The latter subset is obtained by u[1:n] or u[1:], meaning

21 22
from index 1 and the rest of the array. The computation of d can now be done without an explicit u2[1:-1] = u[:-2] - 2*u[1:-1] + u[2:]
Python loop: u2[1:n-1] = u[0:n-2] - 2*u[1:n-1] + u[2:n] # alternative

d = u[1:] - u[:-1] The first expression’s right-hand side is realized by the following steps, involving temporary arrays
with intermediate results, since each array operation can only involve one or two arrays. The
or with explicit limits if desired: numpy package performs the first line above in four steps:
d = u[1:n] - u[0:n-1] temp1 = 2*u[1:-1]
temp2 = u[:-2] - temp1
Indices with a colon, going from an index to (but not including) another index are called slices. temp3 = temp2 + u[2:]
u2[1:-1] = temp3
With numpy arrays, the computations are still done by loops, but in efficient, compiled, highly
optimized C or Fortran code. Such loops are sometimes referred to as vectorized loops. Such loops
can also easily be distributed among many processors on parallel computers. We say that the We need three temporary arrays, but a user does not need to worry about such temporary arrays.
scalar code above, working on an element (a scalar) at a time, has been replaced by an equivalent
vectorized code. The process of vectorizing code is called vectorization. Common mistakes with array slices.
0 1 1111
0000
1111 2 1111
0000 3 1111
0000 4
0000 Array expressions with slices demand that the slices have the same shape. It easy to make
00001111
111100001111
00001111
0000 a mistake in, e.g.,
0000
1111
0000
11110000
1111
0000
11110000
1111
0000
11110000
1111
0000
1111
00001111
111100001111
00001111
0000
0000
1111
00001111
11110000
00001111
11110000
00001111
11110000
0000
1111
0000
11110000
11110000
11110000
1111 u2[1:n-1] = u[0:n-2] - 2*u[1:n-1] + u[2:n]
00001111
111100001111
00001111
0000
− − − − and write
1111
00000000
11110000
11110000
1111
0000
1111
00001111
0000
00001111
0000
00001111
0000 u2[1:n-1] = u[0:n-2] - 2*u[1:n-1] + u[1:n]
1111
0000
11111111
0000
11111111
0000
11110000
1111
0000
1111
0000
11110000
11110000
11110000
1111
0000
11110000
11110000
11110000
1111
0000
11110000
1111
00001111
1111
0000
1111
00001111
00001111
11111111
0000
0000
1111 Now u[1:n] has wrong length (n-1) compared to the other array slices, causing a ValueError
0000
1111000000000000
1111
0 1 2 3 4 and the message could not broadcast input array from shape 103 into shape 104
(if n is 105). When such errors occur one must closely examine all the slices. Usually, it is
Figure 3: Illustration of subtracting two slices of two arrays. easier to get upper limits of slices right when they use -1 or -2 or empty limit rather than
expressions involving the length.
Another common mistake is to forget the slice in the array on the left-hand side,

Test your understanding. u2 = u[0:n-2] - 2*u[1:n-1] + u[1:n]


Newcomers to vectorization are encouraged to choose a small array u, say with five elements,
This is really crucial: now u2 becomes a new array of length n-2, which is the wrong length
and simulate with pen and paper both the loop version and the vectorized version above.
as we have no entries for the boundary values. We meant to insert the right-hand side array
into the in the original u2 array for the entries that correspond to the internal points in the
Finite difference schemes basically contain differences between array elements with shifted mesh (1:n-1 or 1:-1).
indices. As an example, consider the updating formula

for i in range(1, n-1): Vectorization may also work nicely with functions. To illustrate, we may extend the previous
u2[i] = u[i-1] - 2*u[i] + u[i+1] example as follows:

The vectorization consists of replacing the loop by arithmetics on slices of arrays of length n-2: def f(x):
return x**2 + 1
u2 = u[:-2] - 2*u[1:-1] + u[2:] for i in range(1, n-1):
u2 = u[0:n-2] - 2*u[1:n-1] + u[2:n] # alternative u2[i] = u[i-1] - 2*u[i] + u[i+1] + f(x[i])
Note that the length of u2 becomes n-2. If u2 is already an array of length n and we want to use
Assuming u2, u, and x all have length n, the vectorized version becomes
the formula to update all the “inner” elements of u2, as we will when solving a 1D wave equation,
we can write

23 24
u2[1:-1] = u[:-2] - 2*u[1:-1] + u[2:] + f(x[1:-1]) T = 18
def assert_no_error(u, x, t, n):
Obviously, f must be able to take an array as argument for f[x[1:-1]) to make sense. u_e = u_exact(x, t[n])
tol = 1E-13
diff = np.abs(u - u_e).max()
4.2 Finite difference schemes expressed as slices assert diff < tol

We now have the necessary tools to vectorize the wave equation algorithm as described math- solver(I, V, f, c, L, dt, C, T,
user_action=assert_no_error, version=’scalar’)
ematically in Section 1.5 and through code in Section 3.2. There are three loops: one for the solver(I, V, f, c, L, dt, C, T,
initial condition, one for the first time step, and finally the loop that is repeated for all subsequent user_action=assert_no_error, version=’vectorized’)
time levels. Since only the latter is repeated a potentially large number of times, we limit our
vectorization efforts to this loop:
Lambda functions.
for i in range(1, Nx):
u[i] = 2*u_1[i] - u_2[i] + \ The code segment above demonstrates how to achieve very compact code, without degraded
C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) readability, by use of lambda functions for the various input parameters that require a
Python function. In essence,
The vectorized version becomes
f = lambda x, t: L*(x-t)**2
u[1:-1] = - u_2[1:-1] + 2*u_1[1:-1] + \
C2*(u_1[:-2] - 2*u_1[1:-1] + u_1[2:])
is equivalent to
or
def f(x, t):
return L(x-t)**2
u[1:Nx] = 2*u_1[1:Nx]- u_2[1:Nx] + \
C2*(u_1[0:Nx-1] - 2*u_1[1:Nx] + u_1[2:Nx+1])
Note that lambda functions can just contain a single expression and no statements.
One advantage with lambda functions is that they can be used directly in calls:
The program wave1D_u0v.py6 contains a new version of the function solver where both
the scalar and the vectorized loops are included (the argument version is set to scalar or solver(I=lambda x: sin(pi*x/L), V=0, f=0, ...)
vectorized, respectively).

4.3 Verification
We may reuse the quadratic solution ue (x, t) = x(L − x)(1 + 12 t) for verifying also the vectorized
code. A test function can now verify both the scalar and the vectorized version. Moreover, we 4.4 Efficiency measurements
may use a user_action function that compares the computed and exact solution at each time
level and performs a test: The wave1D_u0v.py contains our new solver function with both scalar and vectorized code. For
comparing the efficiency of scalar versus vectorized code, we need a viz function as discussed in
def test_quadratic(): Section 3.4. All of this viz function can be reused, except the call to solver_function. This call
""" lacks the parameter version, which we want to set to vectorized and scalar for our efficiency
Check the scalar and vectorized versions work for measurements.
a quadratic u(x,t)=x(L-x)(1+t/2) that is exactly reproduced.
""" One solution is to copy the viz code from wave1D_u0 into wave1D_u0v.py and add a version
# The following function must work for x as array or scalar argument to the solver_function call. Taking into account how much quite complicated
u_exact = lambda x, t: x*(L - x)*(1 + 0.5*t)
I = lambda x: u_exact(x, 0) animation code we then duplicate, this is not a good idea. Introducing the version argument in
V = lambda x: 0.5*u_exact(x, 0) wave1D_u0.viz is not a good solution since version has no meaning in that file.
# f is a scalar (zeros_like(x) works for scalar x too)
f = lambda x, t: np.zeros_like(x) + 2*c**2*(1 + 0.5*t)
Solution 1. Calling viz in wave1D_u0 with solver_function as our new solver in wave1D_u0v
L = 2.5
c = 1.5 works fine, since this solver has version=’vectorized’ as default value. The problem arises when
C = 0.75 we want to test version=’vectorized’. The simplest solution is then to use wave1D_u0.solver
Nx = 3 # Very coarse mesh for this exact test instead. We make a new viz function in wave1D_u0v.py that has a version argument and that
dt = C*(L/Nx)/c
just calls wave1D_u0.viz:
6 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_u0v.py

25 26
def viz( Efficiency experiments. We now have a viz function that can call our solver function both
I, V, f, c, L, dt, C, T, # PDE paramteres in scalar and vectorized mode. The function run_efficiency_experiments in wave1D_u0v.py
umin, umax, # Interval for u in plots performs a set of experiments and reports the CPU time spent in the scalar and vectorized solver
animate=True, # Simulation with animation?
tool=’matplotlib’, # ’matplotlib’ or ’scitools’ for the previous string vibration example with spatial mesh resolutions Nx = 50, 100, 200, 400, 800.
solver_function=solver, # Function with numerical algorithm Running this function reveals that the vectorized code runs substantially faster: the vectorized
version=’vectorized’, # ’scalar’ or ’vectorized’
): code runs approximately Nx /10 times as fast as the scalar code!
import wave1D_u0
if version == ’vectorized’:
# Reuse viz from wave1D_u0, but with the present 4.5 Remark on the updating of arrays
# modules’ new vectorized solver (which has
# version=’vectorized’ as default argument; At the end of each time step we need to update the u_2 and u_1 arrays such that they have the
# wave1D_u0.viz does not feature this argument) right content for the next time step:
cpu = wave1D_u0.viz(
I, V, f, c, L, dt, C, T, umin, umax,
animate, tool, solver_function=solver) u_2[:] = u_1
elif version == ’scalar’: u_1[:] = u
# Call wave1D_u0.viz with a solver with
# scalar code and use wave1D_u0.solver.
cpu = wave1D_u0.viz( The order here is important! (Updating u_1 first, makes u_2 equal to u, which is wrong.)
I, V, f, c, L, dt, C, T, umin, umax, The assignment u_1[:] = u copies the content of the u array into the elements of the u_1 array.
animate, tool,
solver_function=wave1D_u0.solver) Such copying takes time, but that time is negligible compared to the time needed for computing
u from the finite difference formula, even when the formula has a vectorized implementation.
However, efficiency of program code is a key topic when solving PDEs numerically (particularly
when there are two or three space dimensions), so it must be mentioned that there exists a much
Solution 2. There is a more advanced, fancier solution featuring a very useful trick: we can
more efficient way of making the arrays u_2 and u_1 ready for the next time step. The idea is
make a new function that will always call wave1D_u0v.solver with version=’scalar’. The
based on switching references and explained as follows.
functools.partial function from standard Python takes a function func as argument and a
A Python variable is actually a reference to some object (C programmers may think of
series of positional and keyword arguments and returns a new function that will call func with
pointers). Instead of copying data, we can let u_2 refer to the u_1 object and u_1 refer to the u
the supplied arguments, while the user can control all the other arguments in func. Consider a
object. This is a very efficiency operation (like switching pointers in C). A naive implementation
trivial example,
like
def f(a, b, c=2):
return a + b + c u_2 = u_1
u_1 = u
We want to ensure that f is always called with c=3, i.e., f has only two “free” arguments a and b.
will fail, however, because now u_2 refers to the u_1 object, but then the name u_1 refers to u,
This functionality is obtained by
so that this u object has two references, u_1 and u, while our third array, originally referred to by
import functools u_2 has no more references and is lost. This means that the variables u, u_1, and u_2 refer to
f2 = functools.partial(f, c=3) two arrays and not three. Consequently, the computations at the next time level will be messed
up since updating the elements in u will imply updating the elements in u_1 too so the solution
print f2(1, 2) # results in 1+2+3=6
at the previous time step, which is crucial in our formulas, is destroyed.
While u_2 = u_1 is fine, u_1 = u is problematic, so the solution to this problem is to ensure
Now f2 calls f with whatever the user supplies as a and b, but c is always 3.
that u points to the u_2 array. This is mathematically wrong, but new correct values will be
Back to our viz code, we can do
filled into u at the next time step and make it right.
import functools The correct switch of references is
# Call scalar with version fixed to ‘scalar‘
scalar_solver = functools.partial(scalar, version=’scalar’) tmp = u_2
cpu = wave1D_u0.viz( u_2 = u_1
I, V, f, c, L, dt, C, T, umin, umax, u_1 = u
animate, tool, solver_function=scalar_solver) u = tmp

The new scalar_solver takes the same arguments as wave1D_u0.scalar and calls wave1D_u0v.scalar, We can get rid of the temporary reference tmp by writing
but always supplies the extra argument version=’scalar’. When sending this solver_function
to wave1D_u0.viz, the latter will call wave1D_u0v.solver with all the I, V, f, etc., arguments u_2, u_1, u = u_1, u, u_2
we supply, plus version=’scalar’.
This switching of references for updating our arrays will be used in later implementations.

27 28
Caution: when plot_u is called (as user_action) in the solver function. Test both all_u.append(u)
and all_u.append(u.copy()). Why does one of these constructions fail to store the solution
The update u_2, u_1, u = u_1, u, u_2 leaves wrong content in u at the final time step. correctly? Let the viz function return the all_u list converted to a two-dimensional numpy array.
This means that if we return u, as we do in the example codes here, we actually return u_2, Filename: wave1D_u0_s_store.
which is obviously wrong. It is therefore important to adjust the content of u to u = u_1
before returning u.
Exercise 3: Use a class for the user action function
Redo Exercise 2 using a class for the user action function. That is, define a class Action where
the all_u list is an attribute, and implement the user action function as a method (the special
5 Exercises method __call__ is a natural choice). The class versions avoids that the user action function
depends on parameters defined outside the function (such as all_u in Exercise 2). Filename:
Exercise 1: Simulate a standing wave wave1D_u0_s2c.
The purpose of this exercise is to simulate standing waves on [0, L] and illustrate the error in the
simulation. Standing waves arise from an initial condition Exercise 4: Compare several Courant numbers in one movie
π  The goal of this exercise is to make movies where several curves, corresponding to different
u(x, 0) = A sin mx ,
L Courant numbers, are visualized. Import the solver function from the wave1D_u0_s movie in a
where m is an integer and A is a freely chosen amplitude. The corresponding exact solution can new file wave_compare.py. Reimplement the viz function such that it can take a list of C values
be computed and reads as argument and create a movie with solutions corresponding to the given C values. The plot_u
π  π  function must be changed to store the solution in an array (see Exercise 2 or 3 for details), solver
ue (x, t) = A sin mx cos mct . must be computed for each value of the Courant number, and finally one must run through each
L L time step and plot all the spatial solution curves in one figure and store it in a file.
a) Explain that for a function sin kx cos ωt the wave length in space is λ = 2π/k and the period The challenge in such a visualization is to ensure that the curves in one plot corresponds to the
in time is P = 2π/ω. Use these expressions to find the wave length in space and period in time same time point. The easiest remedy is to keep the time and space resolution constant and change
of ue above. the wave velocity c to change the Courant number. Filename: wave_numerics_comparison.

b) Import the solver function wave1D_u0.py into a new file where the viz function is reimple-
mented such that it plots either the numerical and the exact solution, or the error. Project 5: Calculus with 1D mesh functions
c) Make animations where you illustrate how the error eni = ue (xi , tn )−uni develops and increases This project explores integration and differentiation of mesh functions, both with scalar and
in time. Also make animations of u and ue simultaneously. vectorized implementations. We are given a mesh function fi on a spatial one-dimensional mesh
xi = i∆x, i = 0, . . . , Nx , over the interval [a, b].
Hint 1. Quite long time simulations are needed in order to display significant discrepancies a) Define the discrete derivative of fi by using centered differences at internal mesh points and
between the numerical and exact solution. one-sided differences at the end points. Implement a scalar version of the computation in a
Python function and write an associated unit test for the linear case f (x) = 4x − 2.5 where the
Hint 2. A possible set of parameters is L = 12, m = 9, c = 2, A = 1, Nx = 80, C = 0.8. The discrete derivative should be exact.
error mesh function en can be simulated for 10 periods, while 20-30 periods are needed to show b) Vectorize the implementation of the discrete derivative. Extend the unit test to check the
significant differences between the curves for the numerical and exact solution. validity of the implementation.
Filename: wave_standing.
c) To compute the discrete integral Fi of fi , we assume that the mesh function fi varies linearly
Remarks. The important parameters for numerical quality are C and k∆x, where C = c∆t/∆x between the mesh points. Let f (x) be such a linear interpolant of fi . We then have
is the Courant number and k is defined above (k∆x is proportional to how many mesh points we Z xi
have per wave length in space, see Section 10.4 for explanation). Fi = f (x)dx .
x0

Exercise 2: Add storage of solution in a user action function The exact integral of a piecewise linear function f (x) is given by the Trapezoidal rule. S how
that if Fi is already computed, we can find Fi+1 from
Extend the plot_u function in the file wave1D_u0.py to also store the solutions u in a list.
To this end, declare all_u as an empty list in the viz function, outside plot_u, and perform 1
Fi+1 = Fi + (fi + fi+1 )∆x .
an append operation inside the plot_u function. Note that a function, like plot_u, inside 2
another function, like viz, remembers all local variables in viz function, including all_u, even

29 30
Make a function for the scalar implementation of the discrete integral as a mesh function. That Boundary condition terminology.
is, the function should return Fi for i = 0, . . . , Nx . For a unit test one can use the fact that the
above defined discrete integral of a linear function (say f (x) = 4x − 2.5) is exact. Boundary conditions that specify the value of ∂u/∂n, or shorter un , are known as Neumanna
conditions, while Dirichlet conditionsb refer to specifications of u. When the values are zero
d) Vectorize the implementation of the discrete integral. Extend the unit test to check the (∂u/∂n = 0 or u = 0) we speak about homogeneous Neumann or Dirichlet conditions.
validity of the implementation.
a http://en.wikipedia.org/wiki/Neumann_boundary_condition
b http://en.wikipedia.org/wiki/Dirichlet_conditions
Hint. Interpret the recursive formula for Fi+1 as a sum. Make an array with each element of
the sum and use the "cumsum" (numpy.cumsum) operation to compute the accumulative sum:
numpy.cumsum([1,3,5]) is [1,4,9].
e) Create a class MeshCalculus that can integrate and differentiate mesh functions. The class 6.2 Discretization of derivatives at the boundary
can just define some methods that call the previously implemented Python functions. Here is an
How can we incorporate the condition (35) in the finite difference scheme? Since we have used
example on the usage:
central differences in all the other approximations to derivatives in the scheme, it is tempting to
import numpy as np
implement (35) at x = 0 and t = tn by the difference
calc = MeshCalculus(vectorized=True)
x = np.linspace(0, 1, 11) # mesh un−1 − un1
f = np.exp(x) # mesh function [D2x u]n0 = = 0. (36)
2∆x
df = calc.differentiate(f, x) # discrete derivative
F = calc.integrate(f, x) # discrete anti-derivative The problem is that un−1 is not a u value that is being computed since the point is outside the
mesh. However, if we combine (36) with the scheme for i = 0,
Filename: mesh_calculus_1D. 
un+1
i = −un−1
i + 2uni + C 2 uni+1 − 2uni + uni−1 , (37)

6 Generalization: reflecting boundaries we can eliminate the fictitious value un−1 . We see that un−1 = un1 from (36), which can be used in
(37) to arrive at a modified scheme for the boundary point un+1 0 :
The boundary condition u = 0 in a wave equation reflects the wave, but u changes sign at the 
boundary, while the condition ux = 0 reflects the wave as a mirror and preserves the sign, see a un+1
i = −un−1
i + 2uni + 2C 2 uni+1 − uni , i = 0. (38)
web page7 or a movie file8 for demonstration.
Figure 4 visualizes this equation for computing u30
in terms of u20 , u10 ,
and u21 .
Our next task is to explain how to implement the boundary condition ux = 0, which is more
Similarly, (35) applied at x = L is discretized by a central difference
complicated to express numerically and also to implement than a given value of u. We shall
present two methods for implementing ux = 0 in a finite difference scheme, one based on deriving unNx +1 − unNx −1
a modified stencil at the boundary, and another one based on extending the mesh with ghost = 0. (39)
2∆x
cells and ghost points.
Combined with the scheme for i = Nx we get a modified scheme for the boundary value un+1
Nx :

6.1 Neumann boundary condition un+1


i = −un−1
i

+ 2uni + 2C 2 uni−1 − uni , i = Nx . (40)
When a wave hits a boundary and is to be reflected back, one applies the condition The modification of the scheme at the boundary is also required for the special formula for
the first time step. How the stencil moves through the mesh and is modified at the boundary can
∂u
≡ n · ∇u = 0 . (35) be illustrated by an animation in a web page9 or a movie file10 .
∂n
The derivative ∂/∂n is in the outward normal direction from a general boundary. For a 1D
domain [0, L], we have that
6.3 Implementation of Neumann conditions
We have seen in the preceding section that the special formulas for the boundary points arise
∂ ∂ ∂ ∂ from replacing uni−1 by uni+1 when computing un+1 from the stencil formula for i = 0. Similarly,
= , =− . i
∂n x=L ∂x ∂n x=0 ∂x we replace uni+1 by uni−1 in the stencil formula for i = Nx . This observation can conveniently be
used in the coding: we just work with the general stencil formula, but write the code such that it
is easy to replace u[i-1] by u[i+1] and vice versa. This is achieved by having the indices i+1
and i-1 as variables ip1 (i plus 1) and im1 (i minus 1), respectively. At the boundary we can
7 http://tinyurl.com/opdfafk/pub/mov-wave/demo_BC_gaussian/index.html 9 http://tinyurl.com/opdfafk/pub/mov-wave/wave1D_PDE_Neumann_stencil_gpl/index.html
8 http://tinyurl.com/opdfafk/pub/mov-wave/demo_BC_gaussian/movie.flv 10 http://tinyurl.com/opdfafk/pub/mov-wave/wave1D_PDE_Neumann_stencil_gpl/movie.ogg

31 32
designing a test where the numerical solution is known exactly. Exercise 14 outlines ideas and
Stencil at boundary point code for this purpose. The only test in wave1D_n0.py is to start with a plug wave at rest and
5 see that the initial condition is reached again perfectly after one period of motion, but such a
test requires C = 1 (so the numerical solution coincides with the exact solution of the PDE, see
Section 10.4).
4
6.4 Index set notation
To improve our mathematical writing and our implementations, it is wise to introduce a special
3 notation for index sets. This means that we write xi , i ∈ Ix , instead of i = 0, . . . , Nx . Obviously,
index n

Ix must be the index set Ix = {0, . . . , Nx }, but it is often advantageous to have a symbol for
this set rather than specifying all its elements (all the time, as we have done up to now). This
2 new notation saves writing and makes specifications of algorithms and their implementation of
computer code simpler.
The first index in the set will be denoted Ix0 and the last Ix−1 . When we need to skip the
first element of the set, we use Ix+ for the remaining subset Ix+ = {1, . . . , Nx }. Similarly, if the
1
last element is to be dropped, we write Ix− = {0, . . . , Nx − 1} for the remaining indices. All the
indices corresponding to inner grid points are specified by Ixi = {1, . . . , Nx − 1}. For the time
domain we find it natural to explicitly use 0 as the first index, so we will usually write n = 0 and
0 t0 rather than n = It0 . We also avoid notation like xIx−1 and will instead use xi , i = Ix−1 .
0 1 2 3 4 5 The Python code associated with index sets applies the following conventions:
index i
Notation Python
Figure 4: Modified stencil at a boundary with a Neumann condition. Ix Ix
Ix0 Ix[0]
Ix−1 Ix[-1]
easily define im1=i+1 while we use im1=i-1 in the internal parts of the mesh. Here are the details Ix− Ix[:-1]
of the implementation (note that the updating formula for u[i] is the general stencil formula): Ix+ Ix[1:]
Ixi Ix[1:-1]
i = 0
ip1 = i+1
im1 = ip1 # i-1 -> i+1
u[i] = u_1[i] + C2*(u_1[im1] - 2*u_1[i] + u_1[ip1])
Why index sets are useful.
i = Nx
im1 = i-1 An important feature of the index set notation is that it keeps our formulas and code
ip1 = im1 # i+1 -> i-1
u[i] = u_1[i] + C2*(u_1[im1] - 2*u_1[i] + u_1[ip1]) independent of how we count mesh points. For example, the notation i ∈ Ix or i = Ix0
remains the same whether Ix is defined as above or as starting at 1, i.e., Ix = {1, . . . , Q}.
We can in fact create one loop over both the internal and boundary points and use only one Similarly, we can in the code define Ix=range(Nx+1) or Ix=range(1,Q), and expressions
updating formula: like Ix[0] and Ix[1:-1] remain correct. One application where the index set notation is
convenient is conversion of code from a language where arrays has base index 0 (e.g., Python
for i in range(0, Nx+1): and C) to languages where the base index is 1 (e.g., MATLAB and Fortran). Another
ip1 = i+1 if i < Nx else i-1 important application is implementation of Neumann conditions via ghost points (see next
im1 = i-1 if i > 0 else i+1
u[i] = u_1[i] + C2*(u_1[im1] - 2*u_1[i] + u_1[ip1]) section).

The program wave1D_n0.py11 contains a complete implementation of the 1D wave equation


For the current problem setting in the x, t plane, we work with the index sets
with boundary conditions ux = 0 at x = 0 and x = L.
It would be nice to modify the test_quadratic test case from the wave1D_u0.py with
Ix = {0, . . . , Nx }, It = {0, . . . , Nt }, (41)
Dirichlet conditions, described in Section 4.3. However, the Neumann conditions requires the
polynomial variation in x direction to be of third degree, which causes challenging problems when defined in Python as
11 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_n0.py

33 34
Ix = range(0, Nx+1) • An exact analytical solution u(x, t) = cos(mπt/L) sin( 12 mπx/L), which can be used
It = range(0, Nt+1) for convergence rate tests.

A finite difference scheme can with the index set notation be specified as
a http://tinyurl.com/nm5587k/wave/wave1D/wave1D_dn.py

1 
un+1 = uni − C 2 uni+1 − 2uni + uni−1 , , i ∈ Ixi , n = 0,
i
2 
un+1 = −un−1 + 2uni + C 2 uni+1 − 2uni + uni−1 , i ∈ Ixi , n ∈ Iti ,
i i
6.5 Verifying the implementation of Neumann conditions
un+1
i = 0, i = Ix0 , n ∈ It− ,
How can we test that the Neumann conditions are correctly implemented? The solver function in
un+1
i = 0, i = Ix−1 , n ∈ It− . the wave1D_dn.py program described in the box above accepts Dirichlet and Neumann conditions
The corresponding implementation becomes at x = 0 and x = L. It is tempting to apply a quadratic solution as described in Sections 2.1
and 3.3, but it turns out that this solution is no longer an exact solution of the discrete equations
# Initial condition if a Neumann condition is implemented on the boundary. A linear solution does not help since
for i in Ix[1:-1]: we only have homogeneous Neumann conditions in wave1D_dn.py, and we are consequently left
u[i] = u_1[i] - 0.5*C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1])
with testing just a constant solution: u = const.
# Time loop
for n in It[1:-1]: def test_constant():
# Compute internal points """
for i in Ix[1:-1]: Check the scalar and vectorized versions work for
u[i] = - u_2[i] + 2*u_1[i] + \ a constant u(x,t). We simulate in [0, L] and apply
C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) Neumann and Dirichlet conditions at both ends.
# Compute boundary conditions """
i = Ix[0]; u[i] = 0 u_const = 0.45
i = Ix[-1]; u[i] = 0 u_exact = lambda x, t: u_const
I = lambda x: u_exact(x, 0)
V = lambda x: 0
f = lambda x, t: 0
Notice.
def assert_no_error(u, x, t, n):
The program wave1D_dn.pya applies the index set notation and solves the 1D wave equation u_e = u_exact(x, t[n])
diff = np.abs(u - u_e).max()
utt = c2 uxx + f (x, t) with quite general boundary and initial conditions: msg = ’diff=%E, t_%d=%g’ % (diff, n, t[n])
tol = 1E-13
• x = 0: u = U0 (t) or ux = 0 assert diff < tol, msg
for U_0 in (None, lambda t: u_const):
• x = L: u = UL (t) or ux = 0 for U_L in (None, lambda t: u_const):
L = 2.5
• t = 0: u = I(x) c = 1.5
C = 0.75
• t = 0: ut = I(x) Nx = 3 # Very coarse mesh for this exact test
dt = C*(L/Nx)/c
T = 18 # long time integration
The program combines Dirichlet and Neumann conditions, scalar and vectorized implemen-
tation of schemes, and the index notation into one piece of code. A lot of test examples are solver(I, V, f, c, U_0, U_L, L, dt, C, T,
user_action=assert_no_error,
also included in the program: version=’scalar’)
solver(I, V, f, c, U_0, U_L, L, dt, C, T,
• A rectangular plug-shaped initial condition. (For C = 1 the solution will be a rectangle user_action=assert_no_error,
version=’vectorized’)
that jumps one cell per time step, making the case well suited for verification.) print U_0, U_L
• A Gaussian function as initial condition.
The quadratic solution is very useful for testing though, but it requires Dirichlet conditions at
• A triangular profile as initial condition, which resembles the typical initial shape of a both ends.
guitar string. Another test may utilize the fact that the approximation error vanishes when the Courant
number is unity. We can, for example, start with a plug profile as initial condition, let this wave
• A sinusoidal variation of u at x = 0 and either u = 0 or ux = 0 at x = L. split into two plug waves, one in each direction, and check that the two plug waves come back and

35 36
form the initial condition again after “one period” of the solution process. Neumann conditions instead of i = 0, . . . , Nx as we have previously used. The ghost points now correspond to i = 0
can be applied at both ends. A proper test function reads and i = Nx + 1. A better solution is to use the ideas of Section 6.4: we hide the specific index
value in an index set and operate with inner and boundary points using the index set notation.
def test_plug(): To this end, we define u with proper length and Ix to be the corresponding indices for the
"""Check that an initial plug is correct back after one period.""" real physical mesh points (1, 2, . . . , Nx + 1):
L = 1.0
c = 0.5
dt = (L/10)/c # Nx=10 u = zeros(Nx+3)
I = lambda x: 0 if abs(x-L/2.0) > 0.1 else 1 Ix = range(1, u.shape[0]-1)
u_s, x, t, cpu = solver(
I=I, That is, the boundary points have indices Ix[0] and Ix[-1] (as before). We first update the
V=None, f=None, c=0.5, U_0=None, U_L=None, L=L, solution at all physical mesh points (i.e., interior points in the mesh):
dt=dt, C=1, T=4, user_action=None, version=’scalar’)
u_v, x, t, cpu = solver(
I=I, for i in Ix:
V=None, f=None, c=0.5, U_0=None, U_L=None, L=L, u[i] = - u_2[i] + 2*u_1[i] + \
dt=dt, C=1, T=4, user_action=None, version=’vectorized’) C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1])
tol = 1E-13
diff = abs(u_s - u_v).max()
assert diff < tol The indexing becomes a bit more complicated when we call functions like V(x) and f(x, t), as
u_0 = np.array([I(x_) for x_ in x]) we must remember that the appropriate x coordinate is given as x[i-Ix[0]]:
diff = np.abs(u_s - u_0).max()
assert diff < tol
for i in Ix:
u[i] = u_1[i] + dt*V(x[i-Ix[0]]) + \
Other tests must rely on an unknown approximation error, so effectively we are left with tests 0.5*C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \
on the convergence rate. 0.5*dt2*f(x[i-Ix[0]], t[0])

It remains to update the solution at ghost points, i.e, u[0] and u[-1] (or u[Nx+2]). For a
6.6 Alternative implementation via ghost cells boundary condition ux = 0, the ghost value must equal the value at the associated inner mesh
Idea. Instead of modifying the scheme at the boundary, we can introduce extra points outside point. Computer code makes this statement precise:
the domain such that the fictitious values un−1 and unNx +1 are defined in the mesh. Adding the
intervals [−∆x, 0] and [L, L + ∆x], often referred to as ghost cells, to the mesh gives us all the i = Ix[0] # x=0 boundary
u[i-1] = u[i+1]
needed mesh points, corresponding to i = −1, 0, . . . , Nx , Nx + 1. The extra points i = −1 and i = Ix[-1] # x=L boundary
i = Nx + 1 are known as ghost points, and values at these points, un−1 and unNx +1 , are called ghost u[i+1] = u[i-1]
values.
The important idea is to ensure that we always have The physical solution to be plotted is now in u[1:-1], or equivalently u[Ix[0]:Ix[-1]+1],
so this slice is the quantity to be returned from a solver function. A complete implementation
un−1 = un1 and unNx +1 = unNx −1 , appears in the program wave1D_n0_ghost.py12 .

because then the application of the standard scheme at a boundary point i = 0 or i = Nx will be
correct and guarantee that the solution is compatible with the boundary condition ux = 0. Warning.
We have to be careful with how the spatial and temporal mesh points are stored. Say we
Implementation. The u array now needs extra elements corresponding to the ghost points. let x be the physical mesh points,
Two new point values are needed:
x = linspace(0, L, Nx+1)
u = zeros(Nx+3)
"Standard coding" of the initial condition,
The arrays u_1 and u_2 must be defined accordingly.
Unfortunately, a major indexing problem arises with ghost cells. The reason is that Python for i in Ix:
indices must start at 0 and u[-1] will always mean the last element in u. This fact gives, u_1[i] = I(x[i])
apparently, a mismatch between the mathematical indices i = −1, 0, . . . , Nx + 1 and the Python
indices running over u: 0,..,Nx+2. One remedy is to change the mathematical indexing of i in becomes wrong, since u_1 and x have different lengths and the index i corresponds to two
the scheme and write different mesh points. In fact, x[i] corresponds to u[1+i]. A correct implementation is

un+1
i = ··· , i = 1, . . . , Nx + 1, 12 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_n0_ghost.py

37 38
for i in Ix: 7.1 The model PDE with a variable coefficient
u_1[i] = I(x[i-Ix[0]])
Instead of working with the squared quantity c2 (x), we shall for notational convenience introduce
Similarly, a source term usually coded as f(x[i], t[n]) is incorrect if x is defined to be q(x) = c2 (x). A 1D wave equation with variable wave velocity often takes the form
the physical points, so x[i] must be replaced by x[i-Ix[0]].  
∂2u ∂ ∂u
An alternative remedy is to let x also cover the ghost points such that u[i] is the value 2
= q(x) + f (x, t) . (42)
at x[i]. ∂t ∂x ∂x
This is the most frequent form of a wave equation with variable wave velocity, but other forms
also appear, see Section 15.1 and equation (125).
The ghost cell is only added to the boundary where we have a Neumann condition. Suppose we As usual, we sample (42) at a mesh point,
have a Dirichlet condition at x = L and a homogeneous Neumann condition at x = 0. One ghost
 
cell [−∆x, 0] is added to the mesh, so the index set for the physical points becomes {1, . . . , Nx + 1}. ∂2 ∂ ∂
A relevant implementation is 2
u(xi , tn ) = q(xi ) u(xi , tn ) + f (xi , tn ),
∂t ∂x ∂x
u = zeros(Nx+2) where the only new term to discretize is
Ix = range(1, u.shape[0])     n
... ∂ ∂ ∂ ∂u
for i in Ix[:-1]: q(xi ) u(xi , tn ) = q(x) .
u[i] = - u_2[i] + 2*u_1[i] + \ ∂x ∂x ∂x ∂x i
C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \
dt2*f(x[i-Ix[0]], t[n])
i = Ix[-1] 7.2 Discretizing the variable coefficient
u[i] = U_0 # set Dirichlet value
i = Ix[0] The principal idea is to first discretize the outer derivative. Define
u[i-1] = u[i+1] # update ghost value
∂u
The physical solution to be plotted is now in u[1:] or (as always) u[Ix[0]:Ix[-1]+1]. φ = q(x) ,
∂x
and use a centered derivative around x = xi for the derivative of φ:
7 Generalization: variable wave velocity  n
∂φ φi+ 12 − φi− 21
≈ = [Dx φ]ni .
Our next generalization of the 1D wave equation (1) or (17) is to allow for a variable wave velocity ∂x i ∆x
c: c = c(x), usually motivated by wave motion in a domain composed of different physical media. Then discretize  n
When the media differ in physical properties like density or porosity, the wave velocity c is affected ∂u uni+1 − uni
and will depend on the position in space. Figure 5 shows a wave propagating in one medium φi+ 12 = qi+ 12 ≈ qi+ 12 = [qDx u]ni+ 1 .
∂x i+ 12 ∆x 2
[0, 0.7] ∪ [0.9, 1] with wave velocity c1 (left) before it enters a second medium (0.7, 0.9) with wave
velocity c2 (right). When the wave passes the boundary where c jumps from c1 to c2 , a part of Similarly,  n
the wave is reflected back into the first medium (the reflected wave), while one part is transmitted ∂u uni − uni−1
φi− 12 = qi− 21 ≈ qi− 12 = [qDx u]ni− 1 .
through the second medium (the transmitted wave). ∂x i− 21 ∆x 2

These intermediate results are now combined to


  n
∂ ∂u 1   
q(x) ≈ qi+ 12 uni+1 − uni − qi− 12 uni − uni−1 . (43)
∂x ∂x i ∆x 2

With operator notation we can write the discretization as


  n
∂ ∂u
q(x) ≈ [Dx qDx u]ni . (44)
∂x ∂x i

Do not use the chain rule on the spatial derivative term.



Many are tempted to use the chain rule on the term ∂
∂x ∂x , but this is not a good
q(x) ∂u
Figure 5: Left: wave entering another medium; right: transmitted and reflected wave. idea when discretizing such a term.

39 40
The term with a variable coefficient expresses the net flux qux into a small volume (i.e.,
interval in 1D):
  un+1
i = −un−1
i + 2uni +
∂ ∂u 1  2  
q(x) ≈ (q(x + ∆x)ux (x + ∆x) − q(x)ux (x)) . ∆t 1 1
∂x ∂x ∆x (qi + qi+1 )(uni+1 − uni ) − (qi + qi−1 )(uni − uni−1 ) +
∆x 2 2
Our discretization reflects this principle directly: qux at the right end of the cell minus qux
∆t2 fin . (50)
at the left end, because this follows from the formula (43) or [Dx (qDx u)]ni .
When using the chain rule, we get two terms quxx + qx ux . The typical discretization is
7.4 How a variable coefficient affects the stability
qDx Dx u + D2x qD2x u]ni , (45) The stability criterion derived in Section 10.3 reads ∆t ≤ ∆x/c. If c = c(x), the criterion will
Writing this out shows that it is different from [Dx (qDx u)]ni and lacks the physical interpre- depend on the spatial location. We must therefore choose a ∆t that is small enough such that no
tation of net flux into a cell. With a smooth and slowly varying q(x) the differences between mesh cell has ∆x/c(x) > ∆t. That is, we must use the largest c value in the criterion:
the two discretizations are not substantial. However, when q exhibits (potentially large)
∆x
jumps, [Dx (qDx u)]ni with harmonic averaging of q yields a better solution than arithmetic ∆t ≤ β . (51)
averaging or (45). In the literature, the discretization [Dx (qDx u)]ni totally dominant and maxx∈[0,L] c(x)
very few mention the possibility of (45). The parameter β is included as a safety factor: in some problems with a significantly varying c it
turns out that one must choose β < 1 to have stable solutions (β = 0.9 may act as an all-round
value).
A different strategy to handle the stability criterion with variable wave velocity is to use a
7.3 Computing the coefficient between mesh points spatially varying ∆t. While the idea is mathematically attractive at first sight, the implementation
If q is a known function of x, we can easily evaluate qi+ 21 simply as q(xi+ 12 ) with xi+ 12 = xi + 12 ∆x. quickly becomes very complicated, so we stick to using a constant ∆t and a worst case value of
However, in many cases c, and hence q, is only known as a discrete function, often at the mesh c(x) (with a safety factor β).
points xi . Evaluating q between two mesh points xi and xi+1 can then be done by averaging in
three ways: 7.5 Neumann condition and a variable coefficient
Consider a Neumann condition ∂u/∂x = 0 at x = L = Nx ∆x, discretized as
1
qi+ 12 ≈ (qi + qi+1 ) = [q x ]i (arithmetic mean) (46) uni+1 − uni−1
2 [D2x u]ni = = 0 uni+1 = uni−1 ,

1 1
−1 2∆x
qi+ 12 ≈2 + (harmonic mean) (47) for i = Nx . Using the scheme (50) at the end point i = Nx with uni+1 = uni−1 results in
qi qi+1
1/2
qi+ 12 ≈ (qi qi+1 ) (geometric mean) (48)
un+1
i = −un−1
i + 2uni +
The arithmetic mean in (46) is by far the most commonly used averaging technique and is well  2  
suited for smooth q(x) functions. The harmonic mean is often preferred when q(x) exhibits large ∆t
qi+ 12 (uni−1 − uni ) − qi− 21 (uni − uni−1 ) + ∆t2 fin (52)
jumps (which is typical for geological media). The geometric mean is less used, but popular in ∆x
 2
discretizations to linearize quadratic nonlinearities. ∆t
With the operator notation from (46) we can specify the discretization of the complete = −un−1 + 2uni + (qi+ 21 + qi− 12 )(uni−1 − uni ) + ∆t2 fin (53)
i
∆x
variable-coefficient wave equation in a compact way:  2
∆t
≈ −un−1 + 2uni + 2qi (uni−1 − uni ) + ∆t2 fin . (54)
[Dt Dt u = Dx q x Dx u + f ]ni . (49) i
∆x

From this notation we immediately see what kind of differences that each term is approximated Here we used the approximation
with. The notation q x also specifies that the variable coefficient is approximated by an arithmetic
mean, the definition being [q x ]i+ 12 = (qi + qi+1 )/2. With the notation [Dx qDx u]ni , we specify
that q is evaluated directly, as a function, between the mesh points: q(xi− 12 ) and q(xi+ 21 ).
Before any implementation, it remains to solve (49) with respect to un+1 i :

41 42
i = 0
  2  ip1 = i+1
dq d q im1 = ip1
qi+ 12 + qi− 12 = qi + ∆x + ∆x2 + · · · + u[i] = - u_2[i] + 2*u_1[i] + \
dx dx2 C2*(0.5*(q[i] + q[ip1])*(u_1[ip1] - u_1[i]) - \
 i  2 i 0.5*(q[i] + q[im1])*(u_1[i] - u_1[im1])) + \
dq d q
qi − ∆x + ∆x2 + · · · dt2*f(x[i], t[n])
dx i dx2 i
 2 
d q With ghost cells we can just reuse the formula for the interior points also at the boundary,
= 2qi + 2 ∆x2 + O(∆x4 ) provided that the ghost values of both u and q are correctly updated to ensure ux = 0 and qx = 0.
dx2 i
A vectorized version of the scheme with a variable coefficient at internal mesh points becomes
≈ 2qi . (55)
u[1:-1] = - u_2[1:-1] + 2*u_1[1:-1] + \
An alternative derivation may apply the arithmetic mean of q in (52), leading to the term C2*(0.5*(q[1:-1] + q[2:])*(u_1[2:] - u_1[1:-1]) -
0.5*(q[1:-1] + q[:-2])*(u_1[1:-1] - u_1[:-2])) + \
1 dt2*f(x[1:-1], t[n])
(qi + (qi+1 + qi−1 ))(uni−1 − uni ) .
2
Since 12 (qi+1 + qi−1 ) = qi + O(∆x2 ), we can approximate with 2qi (uni−1 − uni ) for i = Nx and get
the same term as we did above. 7.7 A more general PDE model with variable coefficients
A common technique when implementing ∂u/∂x = 0 boundary conditions, is to assume Sometimes a wave PDE has a variable coefficient in front of the time-derivative term:
dq/dx = 0 as well. This implies qi+1 = qi−1 and qi+1/2 = qi−1/2 for i = Nx . The implications for
 
the scheme are ∂2u ∂ ∂u
%(x) 2 = q(x) + f (x, t) . (58)
∂t ∂x ∂x
un+1
i = −un−1
i + 2uni + One example appears when modeling elastic waves in a rod with varying density, cf. (15.1) with
 2   %(x).
∆t A natural scheme for (58) is
qi+ 12 (uni−1 − uni ) − qi− 12 (uni − uni−1 ) +
∆x
∆t2 fin (56) [%Dt Dt u = Dx q x Dx u + f ]ni . (59)
 2
∆t We realize that the % coefficient poses no particular difficulty, since % enters the formula just a
= −un−1 + 2uni + 2qi− 12 (uni−1 − uni ) + ∆t2 fin . (57)
i
∆x simple factor in front of a derivative. There is hence no need for any averaging of %. Often, % will
be moved to the right-hand side, also without any difficulty:
7.6 Implementation of variable coefficients
[Dt Dt u = %−1 Dx q x Dx u + f ]ni . (60)
The implementation of the scheme with a variable wave velocity q(x) = c2 (x) may assume that q
is available as an array q[i] at the spatial mesh points. The following loop is a straightforward
7.8 Generalization: damping
implementation of the scheme (50):
Waves die out by two mechanisms. In 2D and 3D the energy of the wave spreads out in space,
for i in range(1, Nx): and energy conservation then requires the amplitude to decrease. This effect is not present in 1D.
u[i] = - u_2[i] + 2*u_1[i] + \
C2*(0.5*(q[i] + q[i+1])*(u_1[i+1] - u_1[i]) - \ Damping is another cause of amplitude reduction. For example, the vibrations of a string die out
0.5*(q[i] + q[i-1])*(u_1[i] - u_1[i-1])) + \ because of damping due to air resistance and non-elastic effects in the string.
dt2*f(x[i], t[n]) The simplest way of including damping is to add a first-order derivative to the equation (in
the same way as friction forces enter a vibrating mechanical system):
The coefficient C2 is now defined as (dt/dx)**2, i.e., not as the squared Courant number, since
the wave velocity is variable and appears inside the parenthesis. ∂2u ∂u ∂2u
With Neumann conditions ux = 0 at the boundary, we need to combine this scheme with the +b = c2 2 + f (x, t), (61)
∂t2 ∂t ∂x
discrete version of the boundary condition, as shown in Section 7.5. Nevertheless, it would be
convenient to reuse the formula for the interior points and just modify the indices ip1=i+1 and where b ≥ 0 is a prescribed damping coefficient.
im1=i-1 as we did in Section 6.3. Assuming dq/dx = 0 at the boundaries, we can implement the A typical discretization of (61) in terms of centered differences reads
scheme at the boundary with the following code.
[Dt Dt u + bD2t u = c2 Dx Dx u + f ]ni . (62)
Writing out the equation and solving for the unknown un+1
i gives the scheme

43 44
The code. A class for flexible plotting, cleaning up files, making movie files, like the function
wave1D_u0.viz did, can be coded as follows:
1 1 
un+1 = (1 + b∆t)−1 (( b∆t − 1)un−1 + 2uni + C 2 uni+1 − 2uni + uni−1 + ∆t2 fin ), (63)
i
2 2 i
class PlotAndStoreSolution:
"""
for i ∈ Ixi and n ≥ 1. New equations must be derived for u1i , and for boundary points in case of Class for the user_action function in solver.
Neumann conditions. Visualizes the solution only.
"""
The damping is very small in many wave phenomena and thus only evident for very long def __init__(
time simulations. This makes the standard wave equation without damping relevant for a lot of self,
applications. casename=’tmp’, # Prefix in filenames
umin=-1, umax=1, # Fixed range of y axis
pause_between_frames=None, # Movie speed
backend=’matplotlib’, # or ’gnuplot’ or None
8 Building a general 1D wave equation solver screen_movie=True, # Show movie on screen?
title=’’, # Extra message in title
skip_frame=1, # Skip every skip_frame frame
The program wave1D_dn_vc.py13 is a fairly general code for 1D wave propagation problems that filename=None): # Name of file with solutions
targets the following initial-boundary value problem self.casename = casename
self.yaxis = [umin, umax]
self.pause = pause_between_frames
self.backend = backend
utt = (c2 (x)ux )x + f (x, t), x ∈ (0, L), t ∈ (0, T ] (64) if backend is None:
# Use native matplotlib
u(x, 0) = I(x), x ∈ [0, L] (65) import matplotlib.pyplot as plt
elif backend in (’matplotlib’, ’gnuplot’):
ut (x, 0) = V (t), x ∈ [0, L] (66) module = ’scitools.easyviz.’ + backend + ’_’
exec(’import %s as plt’ % module)
u(0, t) = U0 (t) or ux (0, t) = 0, t ∈ (0, T ] (67) self.plt = plt
u(L, t) = UL (t) or ux (L, t) = 0, t ∈ (0, T ] (68) self.screen_movie = screen_movie
self.title = title
self.skip_frame = skip_frame
The only new feature here is the time-dependent Dirichlet conditions. These are trivial to self.filename = filename
implement: if filename is not None:
# Store time points when u is written to file
self.t = []
i = Ix[0] # x=0 filenames = glob.glob(’.’ + self.filename + ’*.dat.npz’)
u[i] = U_0(t[n+1]) for filename in filenames:
os.remove(filename)
i = Ix[-1] # x=L
u[i] = U_L(t[n+1]) # Clean up old movie frames
for filename in glob.glob(’frame_*.png’):
os.remove(filename)
The solver function is a natural extension of the simplest solver function in the initial
wave1D_u0.py program, extended with Neumann boundary conditions (ux = 0), a the time- def __call__(self, u, x, t, n):
varying Dirichlet conditions, as well as a variable wave velocity. The different code segments """
Callback function user_action, call by solver:
needed to make these extensions have been shown and commented upon in the preceding text. Store solution, plot on screen and save to file.
We refer to the solver function in the wave1D_dn_vc.py file for all the details. """
# Save solution u to a file using numpy.savez
The vectorization is only applied inside the time loop, not for the initial condition or the first if self.filename is not None:
time steps, since this initial work is negligible for long time simulations in 1D problems. name = ’u%04d’ % n # array name
The following sections explain various more advanced programming techniques applied in the kwargs = {name: u}
fname = ’.’ + self.filename + ’_’ + name + ’.dat’
general 1D wave equation solver. np.savez(fname, **kwargs)
self.t.append(t[n]) # store corresponding time value
if n == 0: # save x once
8.1 User action function as a class np.savez(’.’ + self.filename + ’_x.dat’, x=x)

A useful feature in the wave1D_dn_vc.py program is the specification of the user_action function # Animate
if n % self.skip_frame != 0:
as a class. This part of the program may need some motivation and explanation. Although the return
plot_u_st function (and the PlotMatplotlib class) in the wave1D_u0.viz function remembers title = ’t=%.3f’ % t[n]
if self.title:
the local variables in the viz function, it is a cleaner solution to store the needed variables title = self.title + ’ ’ + title
together with the function, which is exactly what a class offers. if self.backend is None:
# native matplotlib animation
13 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_dn_vc.py if n == 0:

45 46
self.plt.ion() 4. half a period of a “cosine hat” (half-cosinehat)
self.lines = self.plt.plot(x, u, ’r-’)
self.plt.axis([x[0], x[-1], These peak-shaped initial conditions can be placed in the middle (loc=’center’) or at the left
self.yaxis[0], self.yaxis[1]])
self.plt.xlabel(’x’) end (loc=’left’) of the domain. With the pulse in the middle, it splits in two parts, each with
self.plt.ylabel(’u’) half the initial amplitude, traveling in opposite directions. With the pulse at the left end, centered
self.plt.title(title)
self.plt.legend([’t=%.3f’ % t[n]]) at x = 0, and using the symmetry condition ∂u/∂x = 0, only a right-going pulse is generated.
else: There is also a left-going pulse, but it travels from x = 0 in negative x direction and is not visible
# Update new solution in the domain [0, L].
self.lines[0].set_ydata(u)
self.plt.legend([’t=%.3f’ % t[n]]) The pulse function is a flexible tool for playing around with various wave shapes and location
self.plt.draw() of a medium with a different wave velocity.
else:
# scitools.easyviz animation The code is shown to demonstrate how easy it is to reach this flexibility with the building
self.plt.plot(x, u, ’r-’, blocks we have already developed:
xlabel=’x’, ylabel=’u’,
axis=[x[0], x[-1],
self.yaxis[0], self.yaxis[1]], def pulse(C=1, # aximum Courant number
title=title, Nx=200, # spatial resolution
show=self.screen_movie) animate=True,
# pause version=’vectorized’,
if t[n] == 0: T=2, # end time
time.sleep(2) # let initial condition stay 2 s loc=’left’, # location of initial condition
else: pulse_tp=’gaussian’, # pulse/init.cond. type
if self.pause is None: slowness_factor=2, # wave vel. in right medium
pause = 0.2 if u.size < 100 else 0 medium=[0.7, 0.9], # interval for right medium
time.sleep(pause) skip_frame=1, # skip frames in animations
sigma=0.05, # width measure of the pulse
self.plt.savefig(’frame_%04d.png’ % (n)) ):
"""
Various peaked-shaped initial conditions on [0,1].
Wave velocity is decreased by the slowness_factor inside
Dissection. Understanding this class requires quite some familiarity with Python in general medium. The loc parameter can be ’center’ or ’left’,
depending on where the initial pulse is to be located.
and class programming in particular. The class supports plotting with Matplotlib (backend=None) The sigma parameter governs the width of the pulse.
or SciTools (backend=matplotlib or backend=gnuplot) for maximum flexibility. """
# Use scaled parameters: L=1 for domain length, c_0=1
The constructor shows how we can flexibly import the plotting engine as (typically) scitools.easyviz.gnuplot_ # for wave velocity outside the domain.
or scitools.easyviz.matplotlib_ (note the trailing underscore - it is required). With the L = 1.0
screen_movie parameter we can suppress displaying each movie frame on the screen. Alterna- c_0 = 1.0
if loc == ’center’:
tively, for slow movies associated with fine meshes, one can set skip_frame=10, causing every 10 xc = L/2
frames to be shown. elif loc == ’left’:
xc = 0
The __call__ method makes PlotAndStoreSolution instances behave like functions, so we
can just pass an instance, say p, as the user_action argument in the solver function, and any if pulse_tp in (’gaussian’,’Gaussian’):
def I(x):
call to user_action will be a call to p.__call__. The __call__ method plots the solution on return np.exp(-0.5*((x-xc)/sigma)**2)
the screen, saves the plot to file, and stores the solution in a file for later retrieval. elif pulse_tp == ’plug’:
More details on storing the solution in files appear in Section ?? in [1]. def I(x):
return 0 if abs(x-xc) > sigma else 1
elif pulse_tp == ’cosinehat’:
def I(x):
8.2 Pulse propagation in two media # One period of a cosine
w = 2
The function pulse in wave1D_dn_vc.py demonstrates wave motion in heterogeneous media a = w*sigma
where c varies. One can specify an interval where the wave velocity is decreased by a factor return 0.5*(1 + np.cos(np.pi*(x-xc)/a)) \
if xc - a <= x <= xc + a else 0
slowness_factor (or increased by making this factor less than one). Figure 5 shows a typical
simulation scenario. elif pulse_tp == ’half-cosinehat’:
Four types of initial conditions are available: def I(x):
# Half a period of a cosine
w = 4
1. a rectangular pulse (plug), a = w*sigma
return np.cos(np.pi*(x-xc)/a) \
2. a Gaussian function (gaussian), if xc - 0.5*a <= x <= xc + 0.5*a else 0
else:
3. a “cosine hat” consisting of one period of the cosine function (cosinehat), raise ValueError(’Wrong pulse_tp="%s"’ % pulse_tp)

47 48
def c(x):
return c_0/slowness_factor \ ue (x, t) = e−βt sin kx (A cos ωt + B sin ωt) .
if medium[0] <= x <= medium[1] else c_0
Find k from the boundary conditions u(0, t) = u(L, t) = 0. Then use the PDE to find constraints
umin=-0.5; umax=1.5*I(xc)
casename = ’%s_Nx%s_sf%s’ % \ on β, ω, A, and B. Set up a complete initial-boundary value problem and its solution. Filename:
(pulse_tp, Nx, slowness_factor) damped_waves.
action = PlotMediumAndSolution(
medium, casename=casename, umin=umin, umax=umax,
skip_frame=skip_frame, screen_movie=animate, Problem 7: Explore symmetry boundary conditions
backend=None, filename=’tmpdata’)
# Choose the stability limit with given Nx, worst case c
Consider the simple "plug" wave where Ω = [−L, L] and
# (lower C will then use this dt, but smaller Nx) 
dt = (L/Nx)/c_0 1, x ∈ [−δ, δ],
solver(I=I, V=None, f=None, c=c, U_0=None, U_L=None, I(x) =
0, otherwise
L=L, dt=dt, C=C, T=T,
user_action=action, version=version, for some number 0 < δ < L. The other initial condition is ut (x, 0) = 0 and there is no source
stability_safety_factor=1)
action.make_movie_file() term f . The boundary conditions can be set to u = 0. The solution to this problem is symmetric
action.file_close() around x = 0. This means that we can simulate the wave process in only the half of the domain
[0, L].
The PlotMediumAndSolution class used here is a subclass of PlotAndStoreSolution where the
medium with reduced c value, as specified by the medium interval, is visualized in the plots. a) Argue why the symmetry boundary condition is ux = 0 at x = 0.

Hint. Symmetry of a function about x = x0 means that f (x0 + h) = f (x0 − h).


Comment on the choices of discretization parameters.
b) Perform simulations of the complete wave problem from on [−L, L]. Thereafter, utilize the
The argument Nx in the pulse function does not correspond to the actual spatial resolution symmetry of the solution and run a simulation in half of the domain [0, L], using a boundary
of C < 1, since the solver function takes a fixed ∆t and C, and adjusts ∆x accordingly. condition at x = 0. Compare the two solutions and make sure that they are the same.
As seen in the pulse function, the specified ∆t is chosen according to the limit C = 1, so
if C < 1, ∆t remains the same, but the solver function operates with a larger ∆x and c) Prove the symmetry property of the solution by setting up the complete initial-boundary
smaller Nx than was specified in the call to pulse. The practical reason is that we always value problem and showing that if u(x, t) is a solution, then also u(−x, t) is a solution.
want to keep ∆t fixed such that plot frames and movies are synchronized in time regardless Filename: wave1D_symmetric.
of the value of C (i.e., ∆x is varies when the Courant number varies).
Exercise 8: Send pulse waves through a layered medium
The reader is encouraged to play around with the pulse function: Use the pulse function in wave1D_dn_vc.py to investigate sending a pulse, located with its peak
at x = 0, through two media with different wave velocities. The (scaled) velocity in the left
>>> import wave1D_dn_vc as w medium is 1 while it is sf in the right medium. Report what happens with a Gaussian pulse, a
>>> w.pulse(loc=’left’, pulse_tp=’cosinehat’, Nx=50, every_frame=10)
“cosine hat” pulse, half a “cosine hat” pulse, and a plug pulse for resolutions Nx = 40, 80, 160,
To easily kill the graphics by Ctrl-C and restart a new simulation it might be easier to run the and sf = 2, 4. Simulate until T = 2. Filename: pulse1D.
above two statements from the command line with
Exercise 9: Explain why numerical noise occurs
Terminal

Terminal> python -c ’import wave1D_dn_vc as w; w.pulse(...)’ The experiments performed in Exercise 8 shows considerable numerical noise in the form of
non-physical waves, especially for sf = 4 and the plug pulse or the half a “cosinehat” pulse. The
noise is much less visible for a Gaussian pulse. Run the case with the plug and half a “cosinehat”
pulses for sf = 1, C = 0.9, 0.25, and Nx = 40, 80, 160. Use the numerical dispersion relation to
9 Exercises explain the observations. Filename: pulse1D_analysis.

Exercise 6: Find the analytical solution to a damped wave equation


Exercise 10: Investigate harmonic averaging in a 1D model
Consider the wave equation with damping (61). The goal is to find an exact solution to a wave
problem with damping. A starting point is the standing wave solution from Exercise 1. It becomes Harmonic means are often used if the wave velocity is non-smooth or discontinuous. Will harmonic
necessary to include a damping term e−ct and also have both a sine and cosine component in averaging of the wave velocity give less numerical noise for the case sf = 4 in Exercise 8? Filename:
time: pulse1D_harmonic.

49 50
Problem 11: Implement open boundary conditions c) Add the possibility to have either ux = 0 or an open boundary condition at the left boundary.
The latter condition is discretized as
To enable a wave to leave the computational domain and travel undisturbed through the boundary
x = L, one can in a one-dimensional problem impose the following condition, called a radiation
[Dt+ u − cDx+ u = 0]ni , i = 0, (73)
condition or open boundary condition:
leading to an explicit update of the boundary value un+1
0 .
∂u
+c
∂u
= 0. (69) The implementation can be tested with a Gaussian function as initial condition:
∂t ∂x
The parameter c is the wave velocity. 1 (x−m)2
g(x; m, s) = √ e− 2s2 .
Show that (69) accepts a solution u = gR (x − ct) (right-going wave), but not u = gL (x + ct) 2πs
(left-going wave). This means that (69) will allow any right-going wave gR (x − ct) to pass through Run two tests:
the boundary undisturbed.
A corresponding open boundary condition for a left-going wave through x = 0 is 1. Disturbance in the middle of the domain, I(x) = g(x; L/2, s), and open boundary condition
at the left end.
∂u ∂u
−c = 0. (70) 2. Disturbance at the left end, I(x) = g(x; 0, s), and ux = 0 as symmetry boundary condition
∂t ∂x
at this end.
a) A natural idea for discretizing the condition (69) at the spatial end point i = Nx is to apply
centered differences in time and space: Make nose tests for both cases, testing that the solution is zero after the waves have left the
domain.
[D2t u + cD2x u = 0]ni , i = Nx . (71)
d) In 2D and 3D it is difficult to compute the correct wave velocity normal to the boundary,
Eliminate the fictitious value unNx +1 by using the discrete equation at the same point. which is needed in generalizations of the open boundary conditions in higher dimensions. Test the
The equation for the first step, u1i , is in principle also affected, but we can then use the effect of having a slightly wrong wave velocity in (72). Make a movies to illustrate what happens.
condition uNx = 0 since the wave has not yet reached the right boundary. Filename: wave1D_open_BC.
b) A much more convenient implementation of the open boundary condition at x = L can be
based on an explicit discretization Remarks. The condition (69) works perfectly in 1D when c is known. In 2D and 3D, however,
the condition reads ut + cx ux + cy uy = 0, where cx and cy are the wave speeds in the x and y
[Dt+ u + cDx− u = 0]ni , i = Nx . (72) directions. Estimating these components (i.e., the direction of the wave) is often challenging.
Other methods are normally used in 2D and 3D to let waves move out of a computational domain.
From this equation, one can solve for un+1
Nx and apply the formula as a Dirichlet condition at the
boundary point. However, the finite difference approximations involved are of first order.
Implement this scheme for a wave equation utt = c2 uxx in a domain [0, L], where you have Exercise 12: Implement periodic boundary conditions
ux = 0 at x = 0, the condition (69) at x = L, and an initial disturbance in the middle of the It is frequently of interest to follow wave motion over large distances and long times. A straight-
domain, e.g., a plug profile like forward approach is to work with a very large domain, but might lead to a lot of computations in

1, L/2 − ` ≤ x ≤ L/2 + `, areas of the domain where the waves cannot be noticed. A more efficient approach is to let a
u(x, 0) = right-going wave out of the domain and at the same time let it enter the domain on the left. This
0, otherwise
is called a periodic boundary condition.
Observe that the initial wave is split in two, the left-going wave is reflected at x = 0, and both The boundary condition at the right end x = L is an open boundary condition (see Exercise 11)
waves travel out of x = L, leaving the solution as u = 0 in [0, L]. Use a unit Courant number to let a right-going wave out of the domain. At the left end, x = 0, we apply, in the beginning
such that the numerical solution is exact. Make a movie to illustrate what happens. of the simulation, either a symmetry boundary condition (see Exercise 7) ux = 0, or an open
Because this simplified implementation of the open boundary condition works, there is no boundary condition.
need to pursue the more complicated discretization in a). This initial wave will split in two and either reflected or transported out of the domain at
x = 0. The purpose of the exercise is to follow the right-going wave. We can do that with a
Hint. Modify the solver function in wave1D_dn.py14 . periodic boundary condition. This means that when the right-going wave hits the boundary x = L,
14 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_dn.py
the open boundary condition lets the wave out of the domain, but at the same time we use a
boundary condition on the left end x = 0 that feeds the outgoing wave into the domain again.
This periodic condition is simply u(0) = u(L). The switch from ux = 0 or an open boundary
condition at the left end to a periodic condition can happen when u(L, t) > , where  = 10−4
might be an appropriate value for determining when the right-going wave hits the boundary
x = L.

51 52
The open boundary conditions can conveniently be discretized as explained in Exercise 11. More precisely, we seek u = X(x)T (t), with T (t) as a linear function and X(x) as a parabola
Implement the described type of boundary conditions and test them on two different initial that fulfills the boundary conditions. Inserting this u in the PDE determines f . It turns out that
shapes: a plug u(x, 0) = 1 for x ≤ 0.1, u(x, 0) = 0 for x > 0.1, and a Gaussian function in the u also fulfills the discrete equations, because the truncation error of the discretized PDE has
middle of the domain: u(x, 0) = exp (− 12 (x − 0.5)2 /0.05). The domain is the unit interval [0, 1]. derivatives in x and t of order four and higher. These derivatives all vanish for a quadratic X(x)
Run these two shapes for Courant numbers 1 and 0.5. Assume constant wave velocity. Make and linear T (t).
movies of the four cases. Reason why the solutions are correct. Filename: periodic. It would be attractive to use a similar approach in the case of Neumann conditions. We set
u = X(x)T (t) and seek lower-order polynomials X and T . To force ux to vanish at the boundary,
Exercise 13: Compare discretizations of a Neumann condition we let Xx be a parabola. Then X is a cubic polynomial. The fourth-order derivative of a cubic
polynomial vanishes, so u = X(x)T (t) will fulfill the discretized PDE also in this case, if f is
We have a 1D wave equation with variable wave velocity: utt = (qux )x . A Neumann condition ux adjusted such that u fulfills the PDE.
at x = 0, L can be discretized as shown in (54) and (57). However, the discrete boundary condition is not exactly fulfilled by this choice of u. The
The aim of this exercise is to examine the rate of the numerical error when using different reason is that
ways of discretizing the Neumann condition.
1
a) As a test problem, q = 1 + (x − L/2)4 can be used, with f (x, t) adapted such that the [D2x u]ni = ux (xi , tn ) + uxxx (xi , tn )∆x2 + O(∆x4 ) . (74)
6
solution has a simple form, say u(x, t) = cos(πx/L) cos(ωt) for, e.g., ω = 1. Perform numerical
At the boundary two boundary points, Xx (x) = 0 such that ux = 0. However, uxxx is a constant
experiments and find the convergence rate of the error using the approximation (54).
and not zero when X(x) is a cubic polynomial. Therefore, our u = X(x)T (t) fulfills
b) Switch to q(x) = 1+cos(πx/L), which is symmetric at x = 0, L, and check the convergence rate
of the scheme (57). Now, qi−1/2 is a 2nd-order approximation to qi , qi−1/2 = qi + 0.25qi00 ∆x2 + · · · , 1
[D2x u]ni = uxxx (xi , tn )∆x2 ,
because qi0 = 0 for i = Nx (a similar argument can be applied to the case i = 0). 6
and not
c) A third discretization can be based on a simple and convenient, but less accurate, one-sided
difference: ui − ui−1 = 0 at i = Nx and ui+1 − ui = 0 at i = 0. Derive the resulting scheme in [D2x u]ni = 0, i = 0, Nx ,
detail and implement it. Run experiments with q from a) or b) to establish the rate of convergence
of the scheme. as it should. (Note that all the higher-order terms O(∆x4 ) also have higher-order derivatives that
vanish for a cubic polynomial.) So to summarize, the fundamental problem is that u as a product
d) A fourth technique is to view the scheme as of a cubic polynomial and a linear or quadratic polynomial in time is not an exact solution of the
1   discrete boundary conditions.
[Dt Dt u]ni = [qDx u]ni+ 1 − [qDx u]ni− 1 + [f ]ni , To make progress, we assume that u = X(x)T (t), where T for simplicity is taken as a prescribed
∆x 2 2 P3
linear function 1 + 12 t, and X(x) is taken as an unknown cubic polynomial j=0 aj xj . There are
and place the boundary at xi+ 12 , i = Nx , instead of exactly at the physical boundary. With this
two different ways of determining the coefficients a0 , . . . , a3 such that both the discretized PDE
idea of approximating (moving) the boundary, we can just set [qDx u]ni+ 1 = 0. Derive the complete and the discretized boundary conditions are fulfilled, under the constraint that we can specify a
2
scheme using this technique. The implementation of the boundary condition at L − ∆x/2 is function f (x, t) for the PDE to feed to the solver function in wave1D_n0.py. Both approaches
O(∆x2 ) accurate, but the interesting question is what impact the movement of the boundary has are explained in the subexercises.
on the convergence rate. Compute the errors as usual over the entire mesh and use q from a) or
b). a) One can insert u in the discretized PDE and find the corresponding f . Then one can insert u in
Filename: Neumann_discr. the discretized boundary conditions. This yields two equations for the four coefficients a0 , . . . , a3 .
To find the coefficients, one can set a0 = 0 and a1 = 1 for simplicity and then determine a2 and
a3 . This approach will make a2 and a3 depend on ∆x and f will depend on both ∆x and ∆t.
Exercise 14: Verification by a cubic polynomial in space Use sympy to perform analytical computations. A starting point is to define u as follows:
The purpose of this exercise is to verify the implementation of the solver function in the program
wave1D_n0.py15 by using an exact numerical solution for the wave equation utt = c2 uxx + f with def test_cubic1():
import sympy as sm
Neumann boundary conditions ux (0, t) = ux (L, t) = 0. x, t, c, L, dx, dt = sm.symbols(’x t c L dx dt’)
A similar verification is used in the file wave1D_u0.py16 , which solves the same PDE, but with i, n = sm.symbols(’i n’, integer=True)
Dirichlet boundary conditions u(0, t) = u(L, t) = 0. The idea of the verification test in function # Assume discrete solution is a polynomial of degree 3 in x
test_quadratic in wave1D_u0.py is to produce a solution that is a lower-order polynomial such T = lambda t: 1 + sm.Rational(1,2)*t # Temporal term
that both the PDE problem, the boundary conditions, and all the discrete equations are exactly a = sm.symbols(’a_0 a_1 a_2 a_3’)
X = lambda x: sum(a[q]*x**q for q in range(4)) # Spatial term
fulfilled. Then the solver function should reproduce this exact solution to machine precision. u = lambda x, t: X(x)*T(t)
15 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_n0.py
16 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_u0.py The symbolic expression for u is reached by calling u(x,t) with x and t as sympy symbols.

53 54
Define DxDx(u, i, n), DtDt(u, i, n), and D2x(u, i, n) as Python functions for returning # a correction term the analytical suggestion x*(L-x)*T
the difference approximations [Dx Dx u]ni , [Dt Dt u]ni , and [D2x u]ni . The next step is to set up the # u_x = x*(L-x)*T(t) - 1/6*u_xxx*dx**2
R = sm.diff(u(x,t), x) - (
residuals for the equations [D2x u]n0 = 0 and [D2x u]nNx = 0, where Nx = L/∆x. Call the residuals x*(L-x) - sm.Rational(1,6)*sm.diff(u(x,t), x, x, x)*dx**2)
R_0 and R_L. Substitute a0 and a1 by 0 and 1, respectively, in R_0, R_L, and a: # R is a polynomial: force all coefficients to vanish.
# Turn R to Poly to extract coefficients:
R = sm.poly(R, x)
R_0 = R_0.subs(a[0], 0).subs(a[1], 1) coeff = R.all_coeffs()
R_L = R_L.subs(a[0], 0).subs(a[1], 1) s = sm.solve(coeff, a[1:]) # a[0] is not present in R
a = list(a) # enable in-place assignment # s is dictionary with a[i] as keys
a[0:2] = 0, 1 # Fix a[0] as 1
s[a[0]] = 1
Determining a2 and a3 from the discretized boundary conditions is then about solving two X = lambda x: sm.simplify(sum(s[a[i]]*x**i for i in range(4)))
u = lambda x, t: X(x)*T(t)
equations with respect to a2 and a3 , i.e., a[2:]: print ’u:’, u(x,t)

s = sm.solve([R_0, R_L], a[2:]) The next step is to find the source term f_e by inserting u_e in the PDE. Thereafter, turn u,
# s is dictionary with the unknowns a[2] and a[3] as keys
a[2:] = s[a[2]], s[a[3]] f, and the time derivative of u into plain Python functions as in a), and then wrap these functions
in new functions I, V, and f, with the right signature as required by the solver function. Set
Now, a contains computed values and u will automatically use these new values since X accesses a. parameters as in a) and check that the solution is exact to machine precision at each time level
Compute the source term f from the discretized PDE: fin = [Dt Dt u − c2 Dx Dx u]ni . Turn u, using an appropriate user_action function.
the time derivative ut (needed for the initial condition V (x)), and f into Python functions. Set Filename: wave1D_n0_test_cubic.
numerical values for L, Nx , C, and c. Prescribe the time interval as ∆t = CL/(Nx c), which
imply ∆x = c∆t/C = L/Nx . Define new functions I(x), V(x), and f(x,t) as wrappers of the
ones made above, where fixed values of L, c, ∆x, and ∆t are inserted, such that I, V, and f can 10 Analysis of the difference equations
be passed on to the solver function. Finally, call solver with a user_action function that
compares the numerical solution to this exact solution u of the discrete PDE problem. 10.1 Properties of the solution of the wave equation
The wave equation
Hint. To turn a sympy expression e, depending on a series of symbols, say x, t, dx, dt, L, and
c, into plain Python function e_exact(x,t,L,dx,dt,c), one can write ∂2u ∂2u
= c2 2
∂t2 ∂x
e_exact = sm.lambdify([x,t,L,dx,dt,c], e, ’numpy’) has solutions of the form

The ’numpy’ argument is a good habit as the e_exact function will then work with array u(x, t) = gR (x − ct) + gL (x + ct), (75)
arguments if it contains mathematical functions (but here we only do plain arithmetics, which
automatically work with arrays). for any functions gR and gL sufficiently smooth to be differentiated twice. The result follows
from inserting (75) in the wave equation. A function of the form gR (x − ct) represents a signal
b) An alternative way of determining a0 , . . . , a3 is to reason as follows. We first construct X(x) moving to the right in time with constant velocity c. This feature can be explained as follows. At
such that the boundary conditions are fulfilled: X = x(L − x). However, to compensate for the time t = 0 the signal looks like gR (x). Introducing a moving x axis with coordinates ξ = x − ct,
fact that this choice of X does not fulfill the discrete boundary condition, we seek u such that we see the function gR (ξ) is "at rest" in the ξ coordinate system, and the shape is always the
∂ 1 same. Say the gR (ξ) function has a peak at ξ = 0. This peak is located at x = ct, which means
ux = x(L − x)T (t) − uxxx ∆x2 , that it moves with the velocity dx/dt = c in the x coordinate system. Similarly, gL (x + ct) is a
∂x 6
P3 function initially with shape gL (x) that moves in the negative x direction with constant velocity
since this u will fit the discrete boundary condition. Assuming u = T (t) j=0 aj xj , we can use c (introduce ξ = x + ct, look at the point ξ = 0, x = −ct, which has velocity dx/dt = −c).
the above equation to determine the coefficients a1 , a2 , a3 . A value, e.g., 1 can be used for a0 . With the particular initial conditions
The following sumpy code computes this u:

u(x, 0) = I(x), u(x, 0) = 0,
def test_cubic2(): ∂t
import sympy as sm
x, t, c, L, dx = sm.symbols(’x t c L dx’) we get, with u as in (75),
T = lambda t: 1 + sm.Rational(1,2)*t # Temporal term
# Set u as a 3rd-degree polynomial in space
X = lambda x: sum(a[i]*x**i for i in range(4)) gR (x) + gL (x) = I(x), 0
−cgR (x) + cgL
0
(x) = 0,
a = sm.symbols(’a_0 a_1 a_2 a_3’)
u = lambda x, t: X(x)*T(t) which have the solution gR = gL = I/2, and consequently
# Force discrete boundary condition to be zero by adding

55 56
1 1
u(x, t) = I(x − ct) + I(x + ct) . (76) Z ∞
2 2 I(x) = A(k)eikx dk, (79)
The interpretation of (76) is that the initial shape of u is split into two parts, each with the same −∞
Z ∞
shape as I but half of the initial amplitude. One part is traveling to the left and the other one to
A(k) = I(x)e−ikx dx . (80)
the right. −∞
The solution has two important physical features: constant amplitude of the left and right
wave, and constant velocity of these two waves. It turns out that the numerical solution will also The function A(k) reflects the weight of each wave component eikx in an infinite sum of such
preserve the constant amplitude, but the velocity depends on the mesh parameters ∆t and ∆x. wave components. That is, A(k) reflects the frequency content in the function I(x). Fourier
The solution (76) will be influenced by boundary conditions when the parts 12 I(x − ct) and transforms are particularly fundamental for analyzing and understanding time-varying signals.
1
2 I(x + ct) hit the boundaries and get, e.g., reflected back into the domain. However, when I(x) The solution of the linear 1D wave PDE can be expressed as
is nonzero only in a small part in the middle of the spatial domain [0, L], which means that the Z ∞
boundaries are placed far away from the initial disturbance of u, the solution (76) is very clearly u(x, t) = A(k)ei(kx−ω(k)t) dx .
observed in a simulation. −∞
A useful representation of solutions of wave equations is a linear combination of sine and/or In a finite difference method, we represent u by a mesh function unq , where n counts temporal
cosine waves. Such a sum of waves is a solution if the governing PDE is linear and each sine mesh points and q counts the spatial ones (the usual counter for spatial points, i, is here already
or cosine wave fulfills the equation. To ease analytical calculations by hand we shall work with used as imaginary unit). Similarly, I(x) is approximated by the mesh function Iq , q = 0, . . . , Nx .
complex exponential functions instead of real-valued sine or cosine functions. The real part of On a mesh, it does not make sense to work with wave components eikx for very large k, because
complex expressions will typically be taken as the physical relevant quantity (whenever a physical the shortest possible sine or cosine wave that can be represented uniquely on a mesh with spacing
relevant quantity is strictly needed). The idea now is to build I(x) of complex wave components ∆x is the wave with wavelength 2∆x. This wave has its peaks and throughs at every two mesh
eikx : X points. That is, the “jumps up and down” between the mesh points.
I(x) ≈ bk eikx . (77) The corresponding k value for the shortest possible wave in the mesh is k = 2π/(2∆x) = π/∆x.
k∈K This maximum frequency is known as the Nyquist frequency. Within the range of relevant
Here, k is the frequency of a component, K is some set of all the discrete k values needed to frequencies (0, π/∆x] one defines the discrete Fourier transform17 , using Nx +1 discrete frequencies:
approximate I(x) well, and bk are constants that must be determined. We will very seldom need
to compute the bk coefficients: most of the insight we look for, and the understanding of the
1 X N
numerical methods we want to establish, come from investigating how the PDE and the scheme
x

Iq = Ak ei2πkj/(Nx +1) , i = 0, . . . , Nx , (81)


treat a single component eikx wave. Nx + 1
k=0
Letting the number of k values in K tend to infinity, makes the sum (77) converge to I(x). Nx
This sum is known as a Fourier series representation of I(x). Looking at (76), we see that the X
Ak = Iq e−i2πkq/(Nx +1) , k = 0, . . . , Nx + 1 . (82)
solution u(x, t), when I(x) is represented as in (77), is also built of basic complex exponential q=0
wave components of the form eik(x±ct) according to
The Ak values represent the discrete Fourier transform of the Iq values, which themselves are the
1 X 1 X
u(x, t) = bk eik(x−ct) + bk eik(x+ct) . (78) inverse discrete Fourier transform of the Ak values.
2 2 The discrete Fourier transform is efficiently computed by the Fast Fourier transform algorithm.
k∈K k∈K

It is common to introduce the frequency in time ω = kc and assume that u(x, t) is a sum of For a real function I(x), the relevant Python code for computing and plotting the discrete Fourier
basic wave components written as eikx−ωt . (Observe that inserting such a wave component in transform appears in the example below.
the governing PDE reveals that ω 2 = k 2 c2 , or ω = ±kc, reflecting the two solutions: one (+kc)
import numpy as np
traveling to the right and the other (−kc) traveling to the left.) from numpy import sin, pi
def I(x):
10.2 More precise definition of Fourier representations return sin(2*pi*x) + 0.5*sin(4*pi*x) + 0.1*sin(6*pi*x)

The above introduction to function representation by sine and cosine waves was quick and intuitive, # Mesh
L = 10; Nx = 100
but will suffice as background knowledge for the following material of single wave component x = np.linspace(0, L, Nx+1)
analysis. However, to understand all details of how different wave components sum up to the dx = L/float(Nx)
analytical and numerical solutions, a more precise mathematical treatment is helpful and therefore
# Discrete Fourier transform
summarized below. A = np.fft.rfft(I(x))
It is well known that periodic functions can be represented by Fourier series. A generalization 17 http://en.wikipedia.org/wiki/Discrete_Fourier_transform
of the Fourier series idea to non-periodic functions defined on the real line is the Fourier transform:

57 58
A_amplitude = np.abs(A) Then the complete scheme,
# Compute the corresponding frequencies
freqs = np.linspace(0, pi/dx, A_amplitude.size) [Dt Dt eikx e−iω̃t = c2 Dx Dx eikx e−iω̃t ]nq
import matplotlib.pyplot as plt leads to the following equation for the unknown numerical frequency ω̃ (after dividing by
plt.plot(freqs, A_amplitude) −eikx e−iω̃t ):
plt.show()
   
4 ω̃∆t 4 k∆x
sin2 = c2 sin2 ,
∆t 2 2 ∆x 2 2
10.3 Stability or
The scheme 
ω̃∆t
 
k∆x

sin2 = C 2 sin2 , (86)
2 2
[Dt Dt u = c2 Dx Dx u]nq (83)
where
for the wave equation ut = c2 uxx allows basic wave components
c∆t
unq = ei(kxq −ω̃tn ) C= (87)
∆x
as solution, but it turns out that the frequency in time, ω̃, is not equal to the exact frequency is the Courant number. Taking the square root of (86) yields
ω = kc. The goal now is to find exactly what ω̃ is. We ask two key questions:    
ω̃∆t k∆x
sin = C sin , (88)
• How accurate is ω̃ compared to ω? 2 2

• Does the amplitude of such a wave component preserve its (unit) amplitude, as it should, Since the exact ω is real it is reasonable to look for a real solution ω̃ of (88). The right-hand
or does it get amplified or damped in time (because of a complex ω̃)? side of (88) must then be in [−1, 1] because the sine function on the left-hand side has values in
[−1, 1] for real ω̃. The sine function on the right-hand side can attain the value 1 when
The following analysis will answer these questions. We shall continue using q as counter for the
k∆x π
mesh point in x direction. = m , m ∈ Z.
2 2
Preliminary results. A key result needed in the investigations is the finite difference approxi- With m = 1 we have k∆x = π, which means that the wavelength λ = 2π/k becomes 2∆x. This
mation of a second-order derivative acting on a complex wave component: is the absolutely shortest wavelength that can be represented on the mesh: the wave jumps up
  and down between each mesh point. Larger values of |m| are irrelevant since these correspond
4 ω∆t iωn∆t to k values whose waves are too short to be represented on a mesh with spacing ∆x. For the
[Dt Dt eiωt ]n = − 2 sin2 e .
∆t 2 shortest possible wave in the mesh, sin (k∆x/2) = 1, and we must require
By just changing symbols (ω → k, t → x, n → q) it follows that
C ≤ 1. (89)
 
4 2 k∆x ikq∆x Consider a right-hand side in (88) of magnitude larger than unity. The solution ω̃ of (88)
[Dx Dx eikx ]q = − sin e .
∆x2 2 must then be a complex number ω̃ = ω̃r + iω̃i because the sine function is larger than unity for a
complex argument. One can show that for any ωi there will also be a corresponding solution with
Numerical wave propagation. Inserting a basic wave component unq = ei(kxq −ω̃tn ) in (83) −ωi . The component with ωi > 0 gives an amplification factor eωi t that grows exponentially in
results in the need to evaluate two expressions: time. We cannot allow this and must therefore require C ≤ 1 as a stability criterion.

[Dt Dt eikx e−iω̃t ]nq = [Dt Dt e−iω̃t ]n eikq∆x Remark on the stability requirement.
 
4 ω̃∆t −iω̃n∆t ikq∆x For smoother wave components with longer wave lengths per length ∆x, (89) can in theory
= − 2 sin2 e e (84) be relaxed. However, small round-off errors are always present in a numerical solution and
∆t 2
these vary arbitrarily from mesh point to mesh point and can be viewed as unavoidable
[Dx Dx eikx e−iω̃t ]nq = [Dx Dx eikx ]q e−iω̃n∆t
  noise with wavelength 2∆x. As explained, C > 1 will for this very small noise leads to
4 k∆x ikq∆x −iω̃n∆t
=− sin2 e e . (85)
∆x 2 2

59 60
exponential growth of the shortest possible wave component in the mesh. This noise will
therefore grow with time and destroy the whole solution. Numerical divided by exact wave velocity
1.1

10.4 Numerical dispersion relation


1.0
Equation (88) can be solved with respect to ω̃:
  
2 k∆x
ω̃ = sin−1 C sin . (90)
∆t 2 0.9

velocity ratio
The relation between the numerical frequency ω̃ and the other parameters k, c, ∆x, and ∆t
is called a numerical dispersion relation. Correspondingly, ω = kc is the analytical dispersion
relation. In general, dispersion refers to the phenomenon where the wave velocity depends on
the spatial frequency (k, or the wave length λ = 2π/k) of the wave. Since the wave velocity
0.8
is ω/k = c, we realize that the analytical dispersion relation reflects the fact that there is no
dispersion. However, in a numerical scheme we have dispersive waves where the wave velocity
depends on k. 0.7 C=1
The special case C = 1 deserves attention since then the right-hand side of (90) reduces to C=0.95
C=0.8
2 k∆x 1 ω∆x
= =
ω
= ω. C=0.3
∆t 2 ∆t c C 0.6
That is, ω̃ = ω and the numerical solution is exact at all mesh points regardless of ∆x and ∆t!
0.2 0.4 0.6 0.8 1.0 1.2 1.4
p
This implies that the numerical solution method is also an analytical solution method, at least
for computing u at discrete points (the numerical method says nothing about the variation of
u between the mesh points, and employing the common linear interpolation for extending the Figure 6: The fractional error in the wave velocity for different Courant numbers.
discrete solution gives a curve that in general deviates from the exact one).
For a closer examination of the error in the numerical dispersion relation when C < 1, we can
study ω̃ − ω, ω̃/ω, or the similar error measures in wave velocity: c̃ − c and c̃/c, where c = ω/k def r(C, p):
and c̃ = ω̃/k. It appears that the most convenient expression to work with is c̃/c, since it can be return 2/(C*p)*asin(C*sin(p))
written as a function of just two parameters:
we can plot r(C, p) as a function of p for various values of C, see Figure 6. Note that the shortest
c̃ 1 waves have the most erroneous velocity, and that short waves move more slowly than they should.
= sin−1 (C sin p) ,
c Cp We can also easily make a Taylor series expansion in the discretization parameter p:
with p = k∆x/2 as a non-dimensional measure of the spatial frequency. In essence, p tells how >>> import sympy as sym
many spatial mesh points we have per wave length in space for the wave component with frequency >>> C, p = sym.symbols(’C p’)
k (recall that the wave length is 2π/k). That is, p reflects how well the spatial variation of the >>> # Compute the 7 first terms around p=0 with no O() term
>>> rs = r(C, p).series(p, 0, 7).removeO()
wave component is resolved in the mesh. Wave components with wave length less than 2∆x >>> rs
(2π/k < 2∆x) are not visible in the mesh, so it does not make sense to have p > π/2. p**6*(5*C**6/112 - C**4/16 + 13*C**2/720 - 1/5040) +
We may introduce the function r(C, p) = c̃/c for further investigation of numerical errors in p**4*(3*C**4/40 - C**2/12 + 1/120) +
p**2*(C**2/6 - 1/6) + 1
the wave velocity:
>>> # Pick out the leading order term, but drop the constant 1
1 >>> rs_error_leading_order = (rs - 1).extract_leading_order(p)
r(C, p) = sin−1 (C sin p) , C ∈ (0, 1], p ∈ (0, π/2] . (91) >>> rs_error_leading_order
Cp p**2*(C**2/6 - 1/6)
This function is very well suited for plotting since it combines several parameters in the problem >>> # Turn the series expansion into a Python function
into a dependence on two dimensionless numbers, C and p. >>> rs_pyfunc = lambdify([C, p], rs, modules=’numpy’)
Defining
>>> # Check: rs_pyfunc is exact (=1) for C=1
>>> rs_pyfunc(1, 0.1)
1.0

61 62
Note that without the .removeO() call the series get an O(x**7) term that makes it impossible This equation admits a Fourier component
to convert the series to a Python function (for, e.g., plotting).
From the rs_error_leading_order expression above, we see that the leading order term in unq,r = exp (i(kx q∆x + ky r∆y − ω̃n∆t)), (94)
the error of this series expansion is
as solution. Letting the operators Dt Dt , Dx Dx , and Dy Dy act on from (94) transforms (93)
unq,r
 2 to
1 k∆x k2 2 2       
2
(C − 1) = c ∆t − ∆x2 , (92) 4 ω̃∆t 4 kx ∆x 4 ky ∆y
6 2 24 sin2 = c2 sin2 + c2 sin2 . (95)
∆t 2 2 ∆x 2 2 ∆y 2 2
2 2
pointing to an error O(∆t , ∆x ), which is compatible with the errors in the difference approxi- or  
mations (Dt Dt u and Dx Dx u). ω̃∆t
sin2 = Cx2 sin2 px + Cy2 sin2 py , (96)
We can do more with a series expansion, e.g., factor it to see how the factor C − 1 plays a 2
significant role. To this end, we make a list of the terms, factor each term, and then sum the
where we have eliminated the factor 4 and introduced the symbols
terms:
c∆t c∆t kx ∆x ky ∆y
>>> rs = r(C, p).series(p, 0, 4).removeO().as_ordered_terms() Cx = , Cy = , px = , py = .
>>> rs
∆x ∆y 2 2
[1, C**2*p**2/6 - p**2/6, For a real-valued ω̃ the right-hand side must be less than or equal to unity in absolute value,
3*C**4*p**4/40 - C**2*p**4/12 + p**4/120,
5*C**6*p**6/112 - C**4*p**6/16 + 13*C**2*p**6/720 - p**6/5040] requiring in general that
>>> rs = [factor(t) for t in rs]
>>> rs Cx2 + Cy2 ≤ 1 . (97)
[1, p**2*(C - 1)*(C + 1)/6,
p**4*(C - 1)*(C + 1)*(3*C - 1)*(3*C + 1)/120,
p**6*(C - 1)*(C + 1)*(225*C**4 - 90*C**2 + 1)/5040] This gives the stability criterion, more commonly expressed directly in an inequality for the time
>>> rs = sum(rs) # Python’s sum function sums the list step:
>>> rs
p**6*(C - 1)*(C + 1)*(225*C**4 - 90*C**2 + 1)/5040 +  −1/2
p**4*(C - 1)*(C + 1)*(3*C - 1)*(3*C + 1)/120 + 1 1 1
p**2*(C - 1)*(C + 1)/6 + 1 ∆t ≤ + (98)
c ∆x2 ∆y 2

We see from the last expression that C = 1 makes all the terms in rs vanish. Since we already A similar, straightforward analysis for the 3D case leads to
know that the numerical solution is exact for C = 1, the remaining terms in the Taylor series  −1/2
expansion will also contain factors of C − 1 and cancel for C = 1. 1 1 1 1
∆t ≤ + + (99)
c ∆x2 ∆y 2 ∆z 2
10.5 Extending the analysis to 2D and 3D In the case of a variable coefficient c2 = c2 (x), we must use the worst-case value
q
The typical analytical solution of a 2D wave equation c̄ = max c2 (x) (100)
x∈Ω
utt = c2 (uxx + uyy ), in the stability criteria. Often, especially in the variable wave velocity case, it is wise to introduce
is a wave traveling in the direction of k = kx i + ky j, where i and j are unit vectors in the x and a safety factor β ∈ (0, 1] too:
y directions, respectively. Such a wave can be expressed by  −1/2
1 1 1 1
∆t ≤ β + + (101)
u(x, y, t) = g(kx x + ky y − kct) c̄ ∆x2 ∆y 2 ∆z 2

for some twice differentiable function g, or with ω = kc, k = |k|: The exact numerical dispersion relations in 2D and 3D becomes, for constant c,

u(x, y, t) = g(kx x + ky y − ωt) . 2  1 


ω̃ = sin−1 Cx2 sin2 px + Cy2 sin2 py 2 , (102)
We can, in particular, build a solution by adding complex Fourier components of the form ∆t
2  1 
ω̃ = sin−1 Cx2 sin2 px + Cy2 sin2 py + Cz2 sin2 pz 2 . (103)
exp (i(kx x + ky y − ωt)) . ∆t

A discrete 2D wave equation can be written as We can visualize the numerical dispersion error in 2D much like we did in 1D. To this end, we
need to reduce the number of parameters in ω̃. The direction of the wave is parameterized by the
[Dt Dt u = c2 (Dx Dx u + Dy Dy u)]nq,r . (93) polar angle θ, which means that

63 64
# use vmin=error.min(), vmax=error.max()
kx = k sin θ, ky = k cos θ . cax = axes[row][column].contourf(
theta, r, error, 50, vmin=-1, vmax=-0.28)
A simplification is to set ∆x = ∆y = h. Then Cx = Cy = c∆t/h, which we call C. Also, axes[row][column].set_xticks([])
axes[row][column].set_yticks([])
1 1 # Add colorbar to the last plot
px = kh cos θ, py = kh sin θ . cbar = plt.colorbar(cax)
2 2
cbar.ax.set_ylabel(’error in wave velocity’)
The numerical frequency ω̃ is now a function of three parameters: plt.savefig(’disprel2D.png’); plt.savefig(’disprel2D.pdf’)
plt.show()
• C, reflecting the number cells a wave is displaced during a time step,
• p = 21 kh, reflecting the number of cells per wave length in space,
• θ, expressing the direction of the wave.

We want to visualize the error in the numerical frequency. To avoid having ∆t as a free parameter
in ω̃, we work with c̃/c = ω̃/(kc). The coefficient in front of the sin−1 factor is then
2 2 1 2
= = = ,
kc∆t 2kc∆th/h Ckh Cp
and
c̃ 2  1 
= sin−1 C sin2 (p cos θ) + sin2 (p sin θ) 2 .
c Cp
We want to visualize this quantity as a function of p and θ for some values of C ≤ 1. It is
0.270
0.345

error in wave velocity


instructive to make color contour plots of 1 − c̃/c in polar coordinates with θ as the angular 0.420
coordinate and p as the radial coordinate. √ 0.495
The stability criterion (97) becomes C ≤ Cmax = 1/ 2 in the present 2D case with the C 0.570
defined above. Let us plot 1 − c̃/c in polar coordinates for Cmax , 0.9Cmax , 0.5Cmax , 0.2Cmax . The 0.645
program below does the somewhat tricky work in Matplotlib, and the result appears in Figure 7. 0.720
From the figure we clearly see that the maximum C value gives the best results, and that waves 0.795
whose propagation direction makes an angle of 45 degrees with an axis are the most accurate.
0.870
0.945
def dispersion_relation_2D(p, theta, C):
arg = C*sqrt(sin(p*cos(theta))**2 +
sin(p*sin(theta))**2)
c_frac = 2./(C*p)*arcsin(arg)
return c_frac Figure 7: Error in numerical dispersion in 2D.

import numpy as np
from numpy import \
cos, sin, arcsin, sqrt, pi # for nicer math formulas
11 Finite difference methods for 2D and 3D wave equa-
r = p = np.linspace(0.001, pi/2, 101)
theta = np.linspace(0, 2*pi, 51) tions
r, theta = np.meshgrid(r, theta)
# Make 2x2 filled contour plots for 4 values of C A natural next step is to consider extensions of the methods for various variants of the one-
import matplotlib.pyplot as plt dimensional wave equation to two-dimensional (2D) and three-dimensional (3D) versions of the
C_max = 1/sqrt(2) wave equation.
C = [[C_max, 0.9*C_max], [0.5*C_max, 0.2*C_max]]
fix, axes = plt.subplots(2, 2, subplot_kw=dict(polar=True))
for row in range(2):
for column in range(2): 11.1 Multi-dimensional wave equations
error = 1 - dispersion_relation_2D(
p, theta, C[row][column]) The general wave equation in d space dimensions, with constant wave velocity c, can be written
print error.min(), error.max() in the compact form

65 66
11.2 Mesh
∂2u
= c2 ∇2 u for x ∈ Ω ⊂ Rd , t ∈ (0, T ], (104) We introduce a mesh in time and in space. The mesh in time consists of time points
∂t2
where
t0 = 0 < t1 < · · · < tNt ,
∂2u ∂2u
∇2 u = + 2, often with a constant spacing ∆t = tn+1 − tn , n ∈ It− .
∂x2 ∂y Finite difference methods are easy to implement on simple rectangle- or box-shaped domains.
in a 2D problem (d = 2) and More complicated shapes of the domain require substantially more advanced techniques and
implementational efforts. On a rectangle- or box-shaped domain, mesh points are introduced
∂2u ∂2u ∂2u separately in the various space directions:
∇2 u = + 2 + 2,
∂x2 ∂y ∂z
in three space dimensions d = 3).
Many applications involve variable coefficients, and the general wave equation in d dimensions x0 < x1 < · · · < xNx in the x direction,
is in this case written as y0 < y1 < · · · < yNy in the y direction,
∂2u z0 < z1 < · · · < zNz in the z direction .
% 2 = ∇ · (q∇u) + f for x ∈ Ω ⊂ Rd , t ∈ (0, T ], (105)
∂t We can write a general mesh point as (xi , yj , zk , tn ), with i ∈ Ix , j ∈ Iy , k ∈ Iz , and n ∈ It .
which in, e.g., 2D becomes
It is a very common choice to use constant mesh spacings: ∆x = xi+1 − xi , i ∈ Ix− ,
∂2u ∂

∂u



∂u
 ∆y = yj+1 − yj , j ∈ Iy− , and ∆z = zk+1 − zk , k ∈ Iz− . With equal mesh spacings one often
%(x, y) 2 = q(x, y) + q(x, y) + f (x, y, t) . (106) introduces h = ∆x = ∆y = ∆z.
∂t ∂x ∂x ∂y ∂y
The unknown u at mesh point (xi , yj , zk , tn ) is denoted by uni,j,k . In 2D problems we just skip
To save some writing and space we may use the index notation, where subscript t, x, or y means the z coordinate (by assuming no variation in that direction: ∂/∂z = 0) and write uni,j .
differentiation with respect to that coordinate. For example,

11.3 Discretization
∂2u
= utt ,
2
∂t Two- and three-dimensional wave equations are easily discretized by assembling building blocks

∂ ∂u for discretization of 1D wave equations, because the multi-dimensional versions just contain terms
q(x, y) = (quy )y . of the same type as those in 1D.
∂y ∂y
These comments extend straightforwardly to 3D, which means that the 3D versions of the two
Discretizing the PDEs. Equation (107) can be discretized as
wave PDEs, with and without variable coefficients, can with be stated as
[Dt Dt u = c2 (Dx Dx u + Dy Dy u + Dz Dz u) + f ]ni,j,k . (109)
utt = c2 (uxx + uyy + uzz ) + f, (107)
A 2D version might be instructive to write out in detail:
%utt = (qux )x + (quz )z + (quz )z + f, (108)
where the index notation for differentiation has been used. [Dt Dt u = c2 (Dx Dx u + Dy Dy u) + f ]ni,j,k ,
At each point of the boundary ∂Ω (of Ω) we need one boundary condition involving the which becomes
unknown u. The boundary conditions are of three principal types:
1. u is prescribed (u = 0 or a known time variation of u at the boundary points, e.g., modeling
i,j − 2ui,j + ui,j
un+1 uni+1,j − 2uni,j + uni−1,j uni,j+1 − 2uni,j + uni,j−1
n n−1
an incoming wave), = c2 + c2 + fi,j
n
,
∆t2 ∆x2 ∆y 2
2. ∂u/∂n = n · ∇u is prescribed (zero for reflecting boundaries),
Assuming, as usual, that all values at time levels n and n − 1 are known, we can solve for the
3. an open boundary condition (also called radiation condition) is specified to let waves travel only unknown un+1
i,j . The result can be compactly written as
undisturbed out of the domain, see Exercise 11 for details.
2 2
All the listed wave equations with second-order derivatives in time need two initial conditions: i,j = 2ui,j + ui,j + c ∆t [Dx Dx u + Dy Dy u]i,j .
un+1 (110)
n n−1 n

1. u = I, As in the 1D case, we need to develop a special formula for u1i,j where we combine the general
2. ut = V . scheme for un+1
i,j , when n = 0, with the discretization of the initial condition:

67 68
[D2t u = V ]0i,j 1 uni,1 − uni,−1
⇒ i,j = ui,j − 2∆tVi,j .
u−1 [−D2y u = 0]ni,0 ⇒ = 0.
2∆y
The result becomes, in compact form,
From this it follows that uni,−1 = uni,1 . The discretized PDE at the boundary point (i, 0) reads
1
=
un+1 − 2∆Vi,j + c2 ∆t2 [Dx Dx u + Dy Dy u]ni,j .
uni,j (111)
i,0 − 2ui,0 + ui,0
un+1 uni+1,0 − 2uni,0 + uni−1,0 uni,1 − 2uni,0 + uni,−1
n−1
2
i,j n
= c2 + c2 + fi,j
n
,
The PDE (108) with variable coefficients is discretized term by term using the corresponding ∆t2 ∆x2 ∆y 2
elements from the 1D case: We can then just insert uni,1 for uni,−1 in this equation and solve for the boundary value un+1
i,0 , just
as was done in 1D.
[%Dt Dt u = (Dx q Dx u + Dy q Dy u + Dz q Dz u) +
x y z
f ]ni,j,k . (112) From these calculations, we see a pattern: the general scheme applies at the boundary j = 0
When written out and solved for the unknown un+1 too if we just replace j − 1 by j + 1. Such a pattern is particularly useful for implementations.
i,j,k , one gets the scheme
The details follow from the explained 1D case in Section 6.3.
The alternative approach to eliminating fictitious values outside the mesh is to have uni,−1
i,j,k = −ui,j,k + 2ui,j,k +
un+1 available as a ghost value. The mesh is extended with one extra line (2D) or plane (3D) of ghost
n−1 n

1 1 1 cells at a Neumann boundary. In the present example it means that we need a line with ghost
( (qi,j,k + qi+1,j,k )(uni+1,j,k − uni,j,k )− cells below the y axis. The ghost values must be updated according to un+1 i,−1 = ui,1 .
n+1
%i,j,k ∆x2 2
1
(qi−1,j,k + qi,j,k )(uni,j,k − uni−1,j,k ))+
2 12 Implementation
1 1 1
( (qi,j,k + qi,j+1,k )(uni,j+1,k − uni,j,k )−
%i,j,k ∆x2 2 We shall now describe in detail various Python implementations for solving a standard 2D, linear
1 wave equation with constant wave velocity and u = 0 on the boundary. The wave equation is
(qi,j−1,k + qi,j,k )(uni,j,k − uni,j−1,k ))+ to be solved in the space-time domain Ω × (0, T ], where Ω = (0, Lx ) × (0, Ly ) is a rectangular
2
1 1 1 spatial domain. More precisely, the complete initial-boundary value problem is defined by
( (qi,j,k + qi,j,k+1 )(uni,j,k+1 − uni,j,k )−
%i,j,k ∆x2 2
1
(qi,j,k−1 + qi,j,k )(uni,j,k − uni,j,k−1 ))+ utt = c2 (uxx + uyy ) + f (x, y, t), (x, y) ∈ Ω, t ∈ (0, T ], (113)
2
2 n
∆t fi,j,k . u(x, y, 0) = I(x, y), (x, y) ∈ Ω, (114)
ut (x, y, 0) = V (x, y), (x, y) ∈ Ω, (115)
Also here we need to develop a special formula for u1i,j,k by combining the scheme for n = 0 u = 0, (x, y) ∈ ∂Ω, t ∈ (0, T ], (116)
1
with the discrete initial condition, which is just a matter of inserting u−1
i,j,k = ui,j,k − 2∆tVi,j,k in
the scheme and solving for u1i,j,k . where ∂Ω is the boundary of Ω, in this case the four sides of the rectangle Ω = [0, Lx ] × [0, Ly ]:
x = 0, x = Lx , y = 0, and y = Ly .
Handling boundary conditions where u is known. The schemes listed above are valid for The PDE is discretized as
the internal points in the mesh. After updating these, we need to visit all the mesh points at the
boundaries and set the prescribed u value. [Dt Dt u = c2 (Dx Dx u + Dy Dy u) + f ]ni,j ,

which leads to an explicit updating formula to be implemented in a program:


Discretizing the Neumann condition. The condition ∂u/∂n = 0 was implemented in 1D
by discretizing it with a D2x u centered difference, followed by eliminating the fictitious u point
outside the mesh by using the general scheme at the boundary point. Alternatively, one can un+1 = −un−1
i,j + 2ui,j +
n

introduce ghost cells and update a ghost value for use in the Neumann condition. Exactly the
Cx2 (uni+1,j − 2uni,j + uni−1,j ) + Cy2 (uni,j+1 − 2uni,j + uni,j−1 ) + ∆t2 fi,j
n
, (117)
same ideas are reused in multiple dimensions.
Consider the condition ∂u/∂n = 0 at a boundary y = 0 of a rectangular domain [0, Lx ]×[0, Ly ]
for all interior mesh points i ∈ Ixi and j ∈ Iyi , and for n ∈ It+ . The constants Cx and Cy are
in 2D. The normal direction is then in −y direction, so
defined as
∂u ∂u
=− , ∆t ∆t
∂n ∂y Cx = c , Cx = c .
∆x ∆y
and we set

69 70
At the boundary, we simply set un+1 i,j = 0 for i = 0, j = 0, . . . , Ny ; i = Nx , j = 0, . . . , Ny ; u = zeros((Nx+1,Ny+1)) # solution array
j = 0, i = 0, . . . , Nx ; and j = Ny , i = 0, . . . , Nx . For the first step, n = 0, (117) is combined with u_1 = zeros((Nx+1,Ny+1)) # solution at t-dt
the discretization of the initial condition ut = V , [D2t u = V ]0i,j to obtain a special formula for u_2 = zeros((Nx+1,Ny+1)) # solution at t-2*dt
u1i,j at the interior mesh points:
where un+1
i,j corresponds to u[i,j], ui,j to u_1[i,j], and ui,j to u_2[i,j]
n n−1

u1 = u0i,j + ∆tVi,j + Index sets. It is also convenient to introduce the index sets (cf. Section 6.4)
1 2 0 1 1
C (u − 2u0i,j + u0i−1,j ) + Cy2 (u0i,j+1 − 2u0i,j + u0i,j−1 ) + ∆t2 fi,j
n
, (118) Ix = range(0, u.shape[0])
2 x i+1,j 2 2 Iy = range(0, u.shape[1])
The algorithm is very similar to the one in 1D: It = range(0, t.shape[0])

1. Set initial condition u0i,j = I(xi , yj )


Computing the solution. Inserting the initial condition I in u_1 and making a callback to
2. Compute u1i,j from (117)
the user in terms of the user_action function is a straightforward generalization of the 1D code
3. Set u1i,j = 0 for the boundaries i = 0, Nx , j = 0, Ny from Section 1.6:

4. For n = 1, 2, . . . , Nt : for i in Ix:


for j in Iy:
(a) i,j from (117) for all internal mesh
Find un+1 points, i ∈ Ixi , j∈ Iyi u_1[i,j] = I(x[i], y[j])

(b) Set un+1


i,j = 0 for the boundaries i = 0, Nx , j = 0, Ny if user_action is not None:
user_action(u_1, x, xv, y, yv, t, 0)

The user_action function has additional arguments compared to the 1D case. The arguments
12.1 Scalar computations xv and yv will be commented upon in Section 12.2.
The solver function for a 2D case with constant wave velocity and boundary condition u = 0 is The key finite difference formula (110) for updating the solution at a time level is implemented
analogous to the 1D case with similar parameter values (see wave1D_u0.py), apart from a few in a separate function as
necessary extensions. The code is found in the program wave2D_u0.py18 .
def advance_scalar(u, u_1, u_2, f, x, y, t, n, Cx2, Cy2, dt2,
V=None, step1=False):
Domain and mesh. The spatial domain is now [0, Lx ] × [0, Ly ], specified by the arguments Lx Ix = range(0, u.shape[0]); Iy = range(0, u.shape[1])
and Ly. Similarly, the number of mesh points in the x and y directions, Nx and Ny , become the if step1:
dt = sqrt(dt2) # save
arguments Nx and Ny. In multi-dimensional problems it makes less sense to specify a Courant Cx2 = 0.5*Cx2; Cy2 = 0.5*Cy2; dt2 = 0.5*dt2 # redefine
number since the wave velocity is a vector and mesh spacings may differ in the various spatial D1 = 1; D2 = 0
else:
directions. We therefore give ∆t explicitly. The signature of the solver function is then D1 = 2; D2 = 1
for i in Ix[1:-1]:
def solver(I, V, f, c, Lx, Ly, Nx, Ny, dt, T, for j in Iy[1:-1]:
user_action=None, version=’scalar’): u_xx = u_1[i-1,j] - 2*u_1[i,j] + u_1[i+1,j]
u_yy = u_1[i,j-1] - 2*u_1[i,j] + u_1[i,j+1]
u[i,j] = D1*u_1[i,j] - D2*u_2[i,j] + \
Key parameters used in the calculations are created as Cx2*u_xx + Cy2*u_yy + dt2*f(x[i], y[j], t[n])
if step1:
x = linspace(0, Lx, Nx+1) # mesh points in x dir u[i,j] += dt*V(x[i], y[j])
y = linspace(0, Ly, Ny+1) # mesh points in y dir # Boundary condition u=0
dx = x[1] - x[0] j = Iy[0]
dy = y[1] - y[0] for i in Ix: u[i,j] = 0
Nt = int(round(T/float(dt))) j = Iy[-1]
t = linspace(0, N*dt, N+1) # mesh points in time for i in Ix: u[i,j] = 0
Cx2 = (c*dt/dx)**2; Cy2 = (c*dt/dy)**2 # help variables i = Ix[0]
dt2 = dt**2 for j in Iy: u[i,j] = 0
i = Ix[-1]
for j in Iy: u[i,j] = 0
return u
Solution arrays. We store un+1
i,j , ui,j , and ui,j in three two-dimensional arrays,
n n−1

The step1 variable has been introduced to allow the formula to be reused for first step u1i,j :
18 http://tinyurl.com/nm5587k/wave/wave2D_u0/wave2D_u0.py

71 72
u = advance_scalar(u, u_1, u_2, f, x, y, t, u[1:-1,1:-1] += dt*V[1:-1, 1:-1]
n, Cx2, Cy2, dt, V, step1=True) # Boundary condition u=0
j = 0
u[:,j] = 0
Below, we will make many alternative implementations of the advance_scalar function to speed j = u.shape[1]-1
up the code since most of the CPU time in simulations is spent in this function. u[:,j] = 0
i = 0
Finally, we remark that the solver function in the wave2D_u0.py code updates arrays for the u[i,:] = 0
next time step by switching references as described in Section 4.5. If the solution u is returned i = u.shape[0]-1
u[i,:] = 0
from solver, which it is not, it is important to set u = u_1 after the time loop, otherwise u return u
actually contains u_2.
def quadratic(Nx, Ny, version):
"""Exact discrete solution of the scheme."""
12.2 Vectorized computations def exact_solution(x, y, t):
The scalar code above turns out to be extremely slow for large 2D meshes, and probably useless in return x*(Lx - x)*y*(Ly - y)*(1 + 0.5*t)
3D beyond debugging of small test cases. Vectorization is therefore a must for multi-dimensional def I(x, y):
finite difference computations in Python. For example, with a mesh consisting of 30 × 30 cells, return exact_solution(x, y, 0)
vectorization brings down the CPU time by a factor of 70 (!). def V(x, y):
In the vectorized case, we must be able to evaluate user-given functions like I(x, y) and return 0.5*exact_solution(x, y, 0)
f (x, y, t) for the entire mesh in one operation (without loops). These user-given functions are def f(x, y, t):
provided as Python functions I(x,y) and f(x,y,t), respectively. Having the one-dimensional return 2*c**2*(1 + 0.5*t)*(y*(Ly - y) + x*(Lx - x))
coordinate arrays x and y is not sufficient when calling I and f in a vectorized way. We must
Lx = 5; Ly = 2
extend x and y to their vectorized versions xv and yv: c = 1.5
dt = -1 # use longest possible steps
from numpy import newaxis T = 18
xv = x[:,newaxis]
yv = y[newaxis,:] def assert_no_error(u, x, xv, y, yv, t, n):
# or u_e = exact_solution(xv, yv, t[n])
xv = x.reshape((x.size, 1)) diff = abs(u - u_e).max()
yv = y.reshape((1, y.size)) tol = 1E-12
msg = ’diff=%g, step %d, time=%g’ % (diff, n, t[n])
assert diff < tol, msg
This is a standard required technique when evaluating functions over a 2D mesh, say sin(xv)*cos(xv),
which then gives a result with shape (Nx+1,Ny+1). Calling I(xv, yv) and f(xv, yv, t[n]) new_dt, cpu = solver(
I, V, f, c, Lx, Ly, Nx, Ny, dt, T,
will now return I and f values for the entire set of mesh points. user_action=assert_no_error, version=version)
With the xv and yv arrays for vectorized computing, setting the initial condition is just a return new_dt, cpu
matter of
def test_quadratic():
u_1[:,:] = I(xv, yv) # Test a series of meshes where Nx > Ny and Nx < Ny
versions = ’scalar’, ’vectorized’, ’cython’, ’f77’, ’c_cy’, ’c_f2py’
for Nx in range(2, 6, 2):
One could also have written u_1 = I(xv, yv) and let u_1 point to a new object, but vectorized for Ny in range(2, 6, 2):
operations often make use of direct insertion in the original array through u_1[:,:], because for version in versions:
print ’testing’, version, ’for %dx%d mesh’ % (Nx, Ny)
sometimes not all of the array is to be filled by such a function evaluation. This is the case with quadratic(Nx, Ny, version)
the computational scheme for un+1i,j :
def run_efficiency(nrefinements=4):
def I(x, y):
def advance_vectorized(u, u_1, u_2, f_a, Cx2, Cy2, dt2, return sin(pi*x/Lx)*sin(pi*y/Ly)
V=None, step1=False):
if step1: Lx = 10; Ly = 10
dt = sqrt(dt2) # save c = 1.5
Cx2 = 0.5*Cx2; Cy2 = 0.5*Cy2; dt2 = 0.5*dt2 # redefine T = 100
D1 = 1; D2 = 0 versions = [’scalar’, ’vectorized’, ’cython’, ’f77’,
else: ’c_f2py’, ’c_cy’]
D1 = 2; D2 = 1 print ’ ’*15, ’’.join([’%-13s’ % v for v in versions])
u_xx = u_1[:-2,1:-1] - 2*u_1[1:-1,1:-1] + u_1[2:,1:-1] for Nx in 15, 30, 60, 120:
u_yy = u_1[1:-1,:-2] - 2*u_1[1:-1,1:-1] + u_1[1:-1,2:] cpu = {}
u[1:-1,1:-1] = D1*u_1[1:-1,1:-1] - D2*u_2[1:-1,1:-1] + \ for version in versions:
Cx2*u_xx + Cy2*u_yy + dt2*f_a[1:-1,1:-1] dt, cpu_ = solver(I, None, None, c, Lx, Ly, Nx, Nx,
if step1:

73 74
-1, T, user_action=None,
version=version) if __name__ == ’__main__’:
cpu[version] = cpu_ test_quadratic()
cpu_min = min(list(cpu.values()))
if cpu_min < 1E-6:
print ’Ignored %dx%d grid (too small execution time)’ \ Array slices in 2D are more complicated to understand than those in 1D, but the logic from
% (Nx, Nx)
else: 1D applies to each dimension separately. For example, when doing uni,j − uni−1,j for i ∈ Ix+ , we
cpu = {version: cpu[version]/cpu_min for version in cpu} just keep j constant and make a slice in the first index: u_1[1:,j] - u_1[:-1,j], exactly as in
print ’%-15s’ % ’%dx%d’ % (Nx, Nx), 1D. The 1: slice specifies all the indices i = 1, 2, . . . , Nx (up to the last valid index), while :-1
print ’’.join([’%13.1f’ % cpu[version] for version in versions])
specifies the relevant indices for the second term: 0, 1, . . . , Nx − 1 (up to, but not including the
def gaussian(plot_method=2, version=’vectorized’, save_plot=True): last index).
"""
Initial Gaussian bell in the middle of the domain. In the above code segment, the situation is slightly more complicated, because each displaced
plot_method=1 applies mesh function, =2 means surf, =0 means no plot. slice in one direction is accompanied by a 1:-1 slice in the other direction. The reason is that we
""" only work with the internal points for the index that is kept constant in a difference.
# Clean up plot files
for name in glob(’tmp_*.png’): The boundary conditions along the four sides makes use of a slice consisting of all indices
os.remove(name) along a boundary:
Lx = 10
Ly = 10 u[: ,0] = 0
c = 1.0 u[:,Ny] = 0
u[0 ,:] = 0
def I(x, y): u[Nx,:] = 0
"""Gaussian peak at (Lx/2, Ly/2)."""
return exp(-0.5*(x-Lx/2.0)**2 - 0.5*(y-Ly/2.0)**2)
In the vectorized update of u (above), the function f is first computed as an array over all
if plot_method == 3: mesh points:
from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt
from matplotlib import cm f_a = f(xv, yv, t[n])
plt.ion()
fig = plt.figure()
u_surf = None We could, alternatively, have used the call f(xv, yv, t[n])[1:-1,1:-1] in the last term of
the update statement, but other implementations in compiled languages benefit from having f
def plot_u(u, x, xv, y, yv, t, n): available in an array rather than calling our Python function f(x,y,t) for every point.
if t[n] == 0:
time.sleep(2) Also in the advance_vectorized function we have introduced a boolean step1 to reuse the
if plot_method == 1: formula for the first time step in the same way as we did with advance_scalar. We refer to the
mesh(x, y, u, title=’t=%g’ % t[n], zlim=[-1,1],
caxis=[-1,1]) solver function in wave2D_u0.py for the details on how the overall algorithm is implemented.
elif plot_method == 2: The callback function now has the arguments u, x, xv, y, yv, t, n. The inclusion of xv
surfc(xv, yv, u, title=’t=%g’ % t[n], zlim=[-1, 1], and yv makes it easy to, e.g., compute an exact 2D solution in the callback function and compute
colorbar=True, colormap=hot(), caxis=[-1,1],
shading=’flat’) errors, through an expression like u - u_exact(xv, yv, t[n]).
elif plot_method == 3:
print ’Experimental 3D matplotlib...under development...’
#plt.clf() 12.3 Verification
ax = fig.add_subplot(111, projection=’3d’)
u_surf = ax.plot_surface(xv, yv, u, alpha=0.3) Testing a quadratic solution. The 1D solution from Section 2.4 can be generalized to multi-
#ax.contourf(xv, yv, u, zdir=’z’, offset=-100, cmap=cm.coolwarm)
#ax.set_zlim(-1, 1) dimensions and provides a test case where the exact solution also fulfills the discrete equations,
# Remove old surface before drawing such that we know (to machine precision) what numbers the solver function should produce. In
if u_surf is not None: 2D we use the following generalization of (30):
ax.collections.remove(u_surf)
plt.draw()
time.sleep(1) 1
if plot_method > 0:
ue (x, y, t) = x(Lx − x)y(Ly − y)(1 + t) . (119)
2
time.sleep(0) # pause between frames
1
if save_plot: This solution fulfills the PDE problem if I(x, y) = ue (x, y, 0), V = 2 ue (x, y, 0),and f =
filename = ’tmp_%04d.png’ % n 2c2 (1 + 12 t)(y(Ly − y) + x(Lx − x)). To show that ue also solves the discrete equations, we start
savefig(filename) # time consuming!
with the general results [Dt Dt 1]n = 0, [Dt Dt t]n = 0, and [Dt Dt t2 ] = 2, and use these to compute
Nx = 40; Ny = 40; T = 20
dt, cpu = solver(I, None, None, c, Lx, Ly, Nx, Ny, -1, T,
user_action=plot_u, version=version)

75 76
Z xi+1 Z yj
1 Fi+1,j = Fi,j + f (x, y)dydx
[Dx Dx ue ]ni,j = [y(Ly − y)(1 + t)Dx Dx x(Lx − x)]ni,j
2 x y0
Z iyj Z yj 
1 1
= yj (Ly − yj )(1 + tn )(−2) . ≈ ∆x f (xi , y)dy + f (xi+1 , y)dy
2 2 y0 y0

A similar calculation must be carried out for the [Dy Dy ue ]ni,j and [Dt Dt ue ]ni,j terms. One must The integrals in the y direction can be approximated by a Trapezoidal rule. A similar idea can be
also show that the quadratic solution fits the special formula for u1i,j . The details are left as used to compute Fi,j+1 . Thereafter, Fi+1,j+1 can be computed by adding the integral over the final
Exercise 15. The test_quadratic function in the wave2D_u0.py19 program implements this corner cell to Fi+1,j + Fi,j+1 − Fi,j . Carry out the details of these computations and implement
verification as a nose test. a function that can return Fi,j for all mesh indices i and j. Use the fact that the Trapezoidal
rule is exact for linear functions and write a test function. Filename: mesh_calculus_2D.

13 Using classes to implement a simulator Exercise 17: Implement Neumann conditions in 2D


• Introduce classes Mesh, Function, Problem, Solver, Visualizer, File Modify the wave2D_u0.py20 program, which solves the 2D wave equation utt = c2 (uxx + uyy )
with constant wave velocity c and u = 0 on the boundary, to have Neumann boundary conditions:
∂u/∂n = 0. Include both scalar code (for debugging and reference) and vectorized code (for
speed).
14 Exercises To test the code, use u = 1.2 as solution (I(x, y) = 1.2, V = f = 0, and c arbitrary),
which should be exactly reproduced with any mesh as long as the stability criterion is satisfied.
Exercise 15: Check that a solution fulfills the discrete model
Another test is to use the plug-shaped pulse in the pulse function from Section 8 and the
Carry out all mathematical details to show that (119) is indeed a solution of the discrete model wave1D_dn_vc.py21 program. This pulse is exactly propagated in 1D if c∆t/∆x = 1. Check that
for a 2D wave equation with u = 0 on the boundary. One must check the boundary conditions, also the 2D program can propagate this pulse exactly in x direction (c∆t/∆x = 1, ∆y arbitrary)
the initial conditions, the general discrete equation at a time level and the special version of this and y direction (c∆t/∆y = 1, ∆x arbitrary). Filename: wave2D_dn.
equation for the first time level. Filename: check_quadratic_solution.
Exercise 18: Test the efficiency of compiled loops in 3D
Project 16: Calculus with 2D mesh functions Extend the wave2D_u0.py code and the Cython, Fortran, and C versions to 3D. Set up an
The goal of this project is to redo Project 5 with 2D mesh functions (fi,j ). efficiency experiment to determine the relative efficiency of pure scalar Python code, vectorized
code, Cython-compiled loops, Fortran-compiled loops, and C-compiled loops. Normalize the CPU
Differentiation. The differentiation results in a discrete gradient function, which in the 2D time for each mesh by the fastest version. Filename: wave3D_u0.
case can be represented by a three-dimensional array df[d,i,j] where d represents the direction
of the derivative, and i,j is a mesh point in 2D. Use centered differences for the derivative at
inner points and one-sided forward or backward differences at the boundary points. Construct
15 Applications of wave equations
unit tests and write a corresponding test function. This section presents a range of wave equation models for different physical phenomena. Although
many wave motion problems in physics can be modeled by the standard linear wave equation, or a
Integration. The integral of a 2D mesh function fi,j is defined as similar formulation with a system of first-order equations, there are some exceptions. Perhaps the
Z yj Z xi most important is water waves: these are modeled by the Laplace equation with time-dependent
Fi,j = f (x, y)dxdy, boundary conditions at the water surface (long water waves, however, can be approximated by
y0 x0 a standard wave equation, see Section 15.7). Quantum mechanical waves constitute another
where f (x, y) is a function that takes on the values of the discrete mesh function fi,j at the example where the waves are governed by the Schrödinger equation, i.e., not by a standard wave
mesh points, but can also be evaluated in between the mesh points. The particular variation equation. Many wave phenomena also need to take nonlinear effects into account when the wave
between mesh points can be taken as bilinear, but this is not important as we will use a product amplitude is significant. Shock waves in the air is a primary example.
Trapezoidal rule to approximate the integral over a cell in the mesh and then we only need to The derivations in the following are very brief. Those with a firm background in continuum
evaluate f (x, y) at the mesh points. mechanics will probably have enough information to fill in the details, while other readers will
Suppose Fi,j is computed. The calculation of Fi+1,j is then hopefully get some impression of the physics and approximations involved when establishing wave
equation models.
19 http://tinyurl.com/nm5587k/wave/wave2D_u0/wave2D_u0.py
20 http://tinyurl.com/nm5587k/wave/wave2D_u0/wave2D_u0.py
21 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_dn_vc.py

77 78
15.1 Waves on a string
T − = −T sin φi − T cos φj,
where φpis the angle between the force and the line x = xi . Let ∆ui = ui − ui−1 and let
∆si = ∆u2i + (xi − xi−1 )2 be the distance from mass mi−1 to mass mi . It is seen that
cos φ = ∆ui /∆si and sin φ = (xi − xi−1 )/∆s or ∆x/∆si if we introduce a constant mesh spacing
∆x = xi − xi−1 . The force can then be written

∆x ∆ui
T − = −T i−T j.
∆si ∆si
The force T + acting toward xi+1 can be calculated in a similar way:

ui ∆x ∆ui+1
T+ = T i+T j.
∆si+1 ∆si+1
T ui +1 Newton’s second law becomes
ui−1 T
mi u00i (t)j = T + + T − ,
which gives the component equations

∆x ∆x
T =T , (120)
∆si ∆si+1
∆ui+1 ∆ui
mi u00i (t) = T −T . (121)
∆si+1 ∆si

A basic reasonable assumption for a string is small displacements ui and small displacement
gradients ∆ui /∆x. For small g = ∆ui /∆x we have that
q p 1
∆si = ∆u2i + ∆x2 = ∆x 1 + g 2 + ∆x(1 + g 2 + O(g 4 ) ≈ ∆x .
xi−1 xi xi +1 2
Equation (120) is then simply the identity T = T , while (121) can be written as

∆ui+1 ∆ui
mi u00i (t) = T −T ,
∆x ∆x
which upon division by ∆x and introducing the density %i = mi /∆x becomes
1
%i u00i (t) = T (ui+1 − 2ui + ui−1 ) . (122)
∆x2
We can now choose to approximate ui by a finite difference in time and get the discretized wave
00

Figure 8: Discrete string model with point masses connected by elastic strings. equation,
1  1
Figure 8 shows a model we may use to derive the equation for waves on a string. The string %i un+1 − 2uni − un−1 =T (ui+1 − 2ui + ui−1 ) . (123)
∆t2 i i
∆x2
is modeled as a set of discrete point masses (at mesh points) with elastic strings in between. The
string has a large constant tension T . We let the mass at mesh point xi be mi . The displacement On the other hand, we may go to the continuum limit ∆x → 0 and replace ui (t) by u(x, t), %i by
of this mass point in y direction is denoted by ui (t). %(x), and recognize that the right-hand side of (122) approaches ∂ 2 u/∂x2 as ∆x → 0. We end up
The motion of mass mi is governed by Newton’s second law of motion. The position of the with the continuous model for waves on a string:
mass at time t is xi i + ui (t)j, where i and j are unit vectors in the x and y direction, respectively.
∂2u ∂2u
The acceleration is then u00i (t)j. Two forces are acting on the mass as indicated in Figure 8. The % =T 2. (124)
force T − acting toward the point xi−1 can be decomposed as ∂t2 ∂x

79 80
Note that the density % may change
p along the string, while the tension T is a constant. With
variable wave velocity c(x) = T /%(x) we can write the wave equation in the more standard %utt = ∇ · σ + %f , (127)
form
where % is the density, u the displacement field, σ the stress tensor, and f body forces. The
∂2u ∂2u latter has normally no impact on elastic waves.
2
= c2 (x) 2 . (125) For stationary deformation of an elastic rod, one has that σxx = Eux , with all other stress
∂t ∂x
components being zero. The parameter E is known as Young’s modulus. Moreover, we set u =
Because of the way % enters the equations, the variable wave velocity does not appear inside the
u(x, t)i and neglect the radial contraction and expansion (where Poisson’s ratio is the important
derivatives as in many other versions of the wave equation. However, most strings of interest
parameter). Assuming that this simple stress and deformation field is a good approximation,
have constant %.
(127) simplifies to
The end points of a string are fixed so that the displacement u is zero. The boundary
conditions are therefore u = 0.  
∂2u ∂ ∂u
% 2 = E . (128)
∂t ∂x ∂x
Damping. Air resistance and non-elastic effects in the string will contribute to reduce the
The associated boundary conditions are u or σxx = Eux known, typically u = 0 for a fixed
amplitudes of the waves so that the motion dies out after some time. This damping effect can be
end and σxx = 0 for a free end.
modeled by a term but on the left-hand side of the equation

%
∂2u
+b
∂u ∂2u
=T 2. (126)
15.4 The acoustic model for seismic waves
∂t2 ∂t ∂x
Seismic waves are used to infer properties of subsurface geological structures. The physical model
The parameter b ≥ 0 is small for most wave phenomena, but the damping effect may become is a heterogeneous elastic medium where sound is propagated by small elastic vibrations. The
significant in long time simulations. general mathematical model for deformations in an elastic medium is based on Newton’s second
law,
External forcing. It is easy to include an external force acting on the string. Say we have a
vertical force f˜i j acting on mass mi . This force affects the vertical component of Newton’s law %utt = ∇ · σ + %f , (129)
and gives rise to an extra term f˜(x, t) on the right-hand side of (124). In the model (125) we
would add a term f (x, t) = f˜(x, y)/%(x). and a constitutive law relating σ to u, often Hooke’s generalized law,
2
Modeling the tension via springs. We assumed, in the derivation above, that the tension in σ = K∇ · u I + G(∇u + (∇u)T − ∇ · u I) . (130)
3
the string, T , was constant. It is easy to check this assumption by modeling the string segments
Here, u is the displacement field, σ is the stress tensor, I is the identity tensor, % is the medium’s
between the masses as standard springs, where the force (tension T ) is proportional to the
density, f are body forces (such as gravity), K is the medium’s bulk modulus and G is the
elongation of the spring segment. Let k be the spring constant, and set Ti = k∆` for the tension
shear modulus. All these quantities may vary in space, while u and σ will also show significant
in the spring segment between xi−1 and xi , where ∆` is the elongation of this segment from
variation in time during wave motion.
the tension-free state. A basic feature of a string is that it has high tension in the equilibrium
The acoustic approximation to elastic waves arises from a basic assumption that the second
position u = 0. Let the string segment have an elongation ∆`0 in the equilibrium position. After
term in Hooke’s law, representing the deformations that give rise to shear stresses, can be
deformation of the string, the elongation is ∆` = ∆`0 + ∆si : Ti = k(∆`0 + ∆si ) ≈ k(∆`0 + ∆x).
neglected. This assumption can be interpreted as approximating the geological medium by a
This shows that Ti is independent of i. Moreover, the extra approximate elongation ∆x is very
fluid. Neglecting also the body forces f , (129) becomes
small compared to ∆`0 , so we may well set Ti = T = k∆`0 . This means that the tension is
completely dominated by the initial tension determined by the tuning of the string. The additional
%utt = ∇(K∇ · u) (131)
deformations of the spring during the vibrations do not introduce significant changes in the
tension. Introducing p as a pressure via

15.2 Waves on a membrane p = −K∇ · u, (132)


and dividing (131) by %, we get
15.3 Elastic waves in a rod
1
Consider an elastic rod subject to a hammer impact at the end. This experiment will give rise to utt = − ∇p . (133)
an elastic deformation pulse that travels through the rod. A mathematical model for longitudinal %
waves along an elastic rod starts with the general equation for deformations and stresses in an Taking the divergence of this equation, using ∇ · u = −p/K from (132), gives the acoustic
elastic medium, approximation to elastic waves:

81 82
   1/γ
1 p
ptt = K∇ · ∇p . (134) % = %0 . (141)
% p0
This is a standard, linear wave equation with variable coefficients. It is common to add a source Here, p0 and %0 are references values for p and % when the fluid is at rest, and γ is the ratio of
term s(x, y, z, t) to model the generation of sound waves: specific heat at constant pressure and constant volume (γ = 5/3 for air).
  The key approximation in a mathematical model for sound waves is to assume that these
1
ptt = K∇ · ∇p + s . (135) waves are small perturbations to the density, pressure, and velocity. We therefore write
%
A common additional approximation of (135) is based on using the chain rule on the right-hand
side, p = p0 + p̂,
    % = %0 + %̂,
1 K 1 K
K∇ · ∇p = ∇2 p + K∇ · ∇p ≈ ∇2 p, u = û,
% % % %
under the assumption that the relative spatial gradient ∇%−1 = −%−2 ∇% is small. This approxi- where we have decomposed the fields in a constant equilibrium value, corresponding to u = 0, and
mation results in the simplified equation a small perturbation marked with a hat symbol. By inserting these decompositions in (138) and
(139), neglecting all product terms of small perturbations and/or their derivatives, and dropping
K 2 the hat symbols, one gets the following linearized PDE system for the small perturbations in
ptt = ∇ p + s. (136)
% density, pressure, and velocity:
The acoustic approximations to seismic waves are used for sound waves in the ground, and
the Earth’s surface is then a boundary where p equals the atmospheric pressure p0 such that the
%t + %0 ∇ · u = 0, (142)
boundary condition becomes p = p0 .
%0 ut = −∇p . (143)
p
Anisotropy. Quite often in geological materials, the effective wave velocity c = K/% is
Now we can eliminate %t by differentiating the relation %(p),
different in different spatial directions because geological layers are compacted, and often twisted,
in such a way that the properties in the horizontal and vertical direction differ. With z as the 1

p
1/γ−1
1 %0

p
1/γ−1
vertical coordinate, we can introduce a vertical wave velocity cz and a horizontal wave velocity %t = %0 pt = pt .
γ p0 p0 γp0 p0
ch , and generalize (136) to
1/γ−1
The product term p1/γ−1 pt can be linearized as p0 pt , resulting in
ptt = c2z pzz + c2h (pxx + pyy ) + s . (137)
%0
%t ≈ pt .
γp0
15.5 Sound waves in liquids and gases
We then get
Sound waves arise from pressure and density variations in fluids. The starting point of modeling
sound waves is the basic equations for a compressible fluid where we omit viscous (frictional)
forces, body forces (gravity, for instance), and temperature effects: pt + γp0 ∇ · u = 0, (144)
1
ut = − ∇p, . (145)
%0
%t + ∇ · (%u) = 0, (138)
%ut + %u · ∇u = −∇p, (139) Taking the divergence of (145) and differentiating (144) with respect to time gives the possibility
% = %(p) . (140) to easily eliminate ∇ · ut and arrive at a standard, linear wave equation for p:

These equations are often referred to as the Euler equations for the motion of a fluid. The ptt = c2 ∇2 p, (146)
parameters involved are the density %, the velocity u, and the pressure p. Equation (139) reflects p
where c = γp0 /%0 is the speed of sound in the fluid.
mass balance, (138) is Newton’s second law for a fluid, with frictional and body forces omitted,
and (140) is a constitutive law relating density to pressure by thermodynamic considerations. A
typical model for (140) is the so-called isentropic relation22 , valid for adiabatic processes where
there is no heat transfer:
22 http://en.wikipedia.org/wiki/Isentropic_process

83 84
15.6 Spherical waves 15.7 The linear shallow water equations
Spherically symmetric three-dimensional waves propagate in the radial direction r only so that The next example considers water waves whose wavelengths are much lager than the depth
u = u(r, t). The fully three-dimensional wave equation and whose wave amplitudes are small. This class of waves may be generated by catastrophic
geophysical events, such as earthquakes at the sea bottom, landslides moving into water, or
∂2u underwater slides (or a combination, as earthquakes frequently release avalanches of masses). For
= ∇ · (c2 ∇u) + f
∂t2 example, a subsea earthquake will normally have an extension of many kilometers but lift the
then reduces to the spherically symmetric wave equation water only a few meters. The wave length will have a size dictated by the earthquake area, which
  is much lager than the water depth, and compared to this wave length, an amplitude of a few
∂2u 1 ∂ ∂u meters is very small. The water is essentially a thin film, and mathematically we can average
= 2 c2 (r)r2 + f (r, t), r ∈ (0, R), t > 0 . (147)
∂t2 r ∂r ∂t the problem in the vertical direction and approximate the 3D wave phenomenon by 2D PDEs.
One can easily show that the function v(r, t) = ru(r, t) fulfills a standard wave equation in Instead of a moving water domain in three space dimensions, we get a horizontal 2D domain with
Cartesian coordinates if c is constant. To this end, insert u = v/r in an unknown function for the surface elevation and the water depth as a variable coefficient in the
PDEs.
 
1 ∂ ∂u Let η(x, y, t) be the elevation of the water surface, H(x, y) the water depth corresponding
c2 (r)r2 to a flat surface (η = 0), u(x, y, t) and v(x, y, t) the depth-averaged horizontal velocities of the
r2 ∂r ∂t
water. Mass and momentum balance of the water volume give rise to the PDEs involving these
to obtain quantities:
 
dc2 ∂v ∂2v dc2
r + c2 2 − v.
dr ∂r ∂r dr ηt = −(Hu)x − (Hv)x (150)
The two terms in the parenthesis can be combined to ut = −gηx , (151)
  vt = −gηy , (152)
∂ ∂v
r c2 ,
∂r ∂r where g is the acceleration of gravity. Equation (150) corresponds to mass balance while the
which is recognized as the variable-coefficient Laplace operator in one Cartesian coordinate. The other two are derived from momentum balance (Newton’s second law).
spherically symmetric wave equation in terms of v(r, t) now becomes The initial conditions associated with (150)-(152) are η, u, and v prescribed at t = 0. A
common condition is to have some water elevation η = I(x, y) and assume that the surface is at
 
∂2v ∂ 2 ∂v 1 dc2 rest: u = v = 0. A subsea earthquake usually means a sufficiently rapid motion of the bottom
= c (r) − v + rf (r, t), r ∈ (0, R), t > 0 . (148) and the water volume to say that the bottom deformation is mirrored at the water surface as an
∂t2 ∂r ∂r r dr
initial lift I(x, y) and that u = v = 0.
In the case of constant wave velocity c, this equation reduces to the wave equation in a single
Boundary conditions may be η prescribed for incoming, known waves, or zero normal velocity
Cartesian coordinate called r:
at reflecting boundaries (steep mountains, for instance): unx + vny = 0, where (nx , ny ) is the
∂2v ∂2v outward unit normal to the boundary. More sophisticated boundary conditions are needed
= c2 2 + rf (r, t), r ∈ (0, R), t > 0 . (149) when waves run up at the shore, and at open boundaries where we want the waves to leave the
∂t2 ∂r
computational domain undisturbed.
That is, any program for solving the one-dimensional wave equation in a Cartesian coordinate Equations (150), (151), and (152) can be transformed to a standard, linear wave equation.
system can be used to solve (149), provided the source term is multiplied by the coordinate, First, multiply (151) and (152) by H, differentiate (151)) with respect to x and (152) with
and that we divide the Cartesian mesh solution by r to get the spherically symmetric solution. respect to y. Second, differentiate (150) with respect to t and use that (Hu)xt = (Hut )x and
Moreover, if r = 0 is included in the domain, spherical symmetry demands that ∂u/∂r = 0 at (Hv)yt = (Hvt )y when H is independent of t. Third, eliminate (Hut )x and (Hvt )y with the aid
r = 0, which means that of the other two differentiated equations. These manipulations results in a standard, linear wave
∂u 1

∂v
 equation for η:
= 2 r − v = 0, r = 0,
∂r r ∂r
ηtt = (gHηx )x + (gHηy )y = ∇ · (gH∇η) . (153)
implying v(0, t) = 0 as a necessary condition. For practical applications, we exclude r = 0 from
In the case we have an initial non-flat water surface at rest, the initial conditions become
the domain and assume that some boundary condition is assigned at r = , for some  > 0.
η = I(x, y) and ηt = 0. The latter follows from (150) if u = v = 0, or simply from the fact that
the vertical velocity of the surface is ηt , which is zero for a surface at rest.
The system (150)-(152) can be extended to handle a time-varying bottom topography, which
is relevant for modeling long waves generated by underwater slides. In such cases the water depth

85 86
function H is also a function of t, due to the moving slide, and one must add a time-derivative Mass balance and Newton’s second law lead to the PDEs
term Ht to the left-hand side of (150). A moving bottom is best described by introducing z = H0
as the still-water level, z = B(x, y, t) as the time- and space-varying bottom topography, so that
∂A ∂Q
H = H0 − B(x, y, t). In the elimination of u and v one may assume that the dependence of H on + = 0, (158)
t can be neglected in the terms (Hu)xt and (Hv)yt . We then end up with a source term in (153),  ∂t ∂x
because of the moving (accelerating) bottom: ∂Q γ + 2 ∂ Q2 A ∂P µQ
+ + = −2π(γ + 2) , (159)
∂t γ + 1 ∂x A % ∂x %A
ηtt = ∇ · (gH∇η) + Btt . (154)
where γ is a parameter related to the velocity profile, % is the density of blood, and µ is the
The reduction of (154) to 1D, for long waves in a straight channel, or for approximately plane dynamic viscosity of blood.
waves in the ocean, is trivial by assuming no change in y direction (∂/∂y = 0): We have three unknowns A, Q, and P , and two equations (158) and (159). A third equation
is needed to relate the flow to the deformations of the wall. A common form for this equation is
ηtt = (gHηx )x + Btt . (155)
∂P 1 ∂Q
+ = 0, (160)
Wind drag on the surface. Surface waves are influenced by the drag of the wind, and if ∂t C ∂x
the√wind velocity some √
meters above the surface is (U, V ), the wind drag gives contributions where C is the compliance of the wall, given by the constitutive relation
CV U 2 + V 2 U and CV U 2 + V 2 V to (151) and (152), respectively, on the right-hand sides.
∂A ∂A
C= + , (161)
∂P ∂t
Bottom drag. The waves will experience a drag from the bottom, often roughly modeled by a
√ √ which require a relationship between A and P . One common model is to view the vessel wall,
term similar to the wind drag: CB u2 + v 2 u on the right-hand side of (151) and CB u2 + v 2 v
locally, as a thin elastic tube subject to an internal pressure. This gives the relation
on the right-hand side of (152). Note that in this case the PDEs (151) and (152) become nonlinear
and the elimination of u and v to arrive at a 2nd-order wave equation for η is not possible πhE √ p
anymore. P = P0 + ( A − A0 ),
(1 − ν 2 )A0
where P0 and A0 are corresponding reference values when the wall is not deformed, h is the
Effect of the Earth’s rotation. Long geophysical waves will often be affected by the rotation
thickness of the wall, and E and ν are Young’s modulus and Poisson’s ratio of the elastic material
of the Earth because of the Coriolis force. This force gives rise to a term f v on the right-hand
in the wall. The derivative becomes
side of (151) and −f u on the right-hand side of (152). Also in this case one cannot eliminate u
and v to work with a single equation for η. The Coriolis parameter is f = 2Ω sin φ, where Ω is  2
∂A 2(1 − ν 2 )A0 p (1 − ν 2 )A0
the angular velocity of the earth and φ is the latitude. C= = A0 + 2 (P − P0 ) . (162)
∂P πhE πhE

15.8 Waves in blood vessels Another (nonlinear) deformation model of the wall, which has a better fit with experiments, is

The flow of blood in our bodies is basically fluid flow in a network of pipes. Unlike rigid pipes, P = P0 exp (β(A/A0 − 1)),
the walls in the blood vessels are elastic and will increase their diameter when the pressure rises.
The elastic forces will then push the wall back and accelerate the fluid. This interaction between where β is some parameter to be estimated. This law leads to
the flow of blood and the deformation of the vessel wall results in waves traveling along our blood ∂A A0
vessels. C= = . (163)
∂P βP
A model for one-dimensional waves along blood vessels can be derived from averaging the
fluid flow over the cross section of the blood vessels. Let x be a coordinate along the blood vessel
Reduction to the standard wave equation. It is not uncommon to neglect the viscous
and assume that all cross sections are circular, though with different radii R(x, t). The main
term on the right-hand side of (159) and also the quadratic term with Q2 on the left-hand side.
quantities to compute is the cross section area A(x, t), the averaged pressure P (x, t), and the
The reduced equations (159) and (160) form a first-order linear wave equation system:
total volume flux Q(x, t). The area of this cross section is
Z R(x,t)
∂P ∂Q
A(x, t) = 2π rdr, (156) C =− , (164)
0 ∂t ∂x
Let vx (x, t) be the velocity of blood averaged over the cross section at point x. The volume flux, ∂Q A ∂P
=− . (165)
being the total volume of blood passing a cross section per time unit, becomes ∂t % ∂x

Q(x, t) = A(x, t)vx (x, t) (157)

87 88
These can be combined into standard 1D wave equation PDE by differentiating the first equation Hint. According to Section 15.1, the density enters the mathematical model as % in %utt = T uxx ,
with respect t and the second with respect to x, where T is the string tension. Modify, e.g., the wave1D_u0v.py code to incorporate the tension
    and two density values. Make a mesh function rho with density values at each spatial mesh point.
∂ ∂P ∂ A ∂P A value for the tension may be 150 N. Corresponding density values can be computed from the
C = ,
∂t ∂t ∂x % ∂x wave velocity estimations in the guitar function in the wave1D_u0v.py file.
Filename: wave1D_u0_sv_discont.
which can be approximated by
s
∂2Q ∂2Q A Exercise 20: Simulate damped waves on a string
= c2 2 , c= , (166)
∂t2 ∂x %C Formulate a mathematical model for damped waves on a string. Use data from Section 3.5, and
tune the damping parameter so that the string is very close to the rest state after 15 s. Make a
where the A and C in the expression for c are taken as constant reference values.
movie of the wave motion. Filename: wave1D_u0_sv_damping.

15.9 Electromagnetic waves Exercise 21: Simulate elastic waves in a rod


Light and radio waves are governed by standard wave equations arising from Maxwell’s general
A hammer hits the end of an elastic rod. The exercise is to simulate the resulting wave motion
equations. When there are no charges and no currents, as in a vacuum, Maxwell’s equations take
using the model (128) from Section 15.3. Let the rod have length L and let the boundary x = L
the form
be stress free so that σxx = 0, implying that ∂u/∂x = 0. The left end x = 0 is subject to a strong
stress pulse (the hammer), modeled as
∇ · E = 0, 
S, 0 < t ≤ ts ,
∇ · B = 0, σxx (t) =
0, t > ts
∂BB
∇×E = − , The corresponding condition on u becomes ux = S/E for t ≤ ts and zero afterwards (recall that
∂t σxx = Eux ). This is a non-homogeneous Neumann condition, and you will need to approximate
∂EE
∇ × B = µ0 0 , this condition and combine it with the scheme (the ideas and manipulations follow closely the
∂t handling of a non-zero initial condition ut = V in wave PDEs or the corresponding second-order
where 0 = 8.854187817620 · 10−12 (F/m) is the permittivity of free space, also known as the ODEs for vibrations). Filename: wave_rod.
electric constant, and µ0 = 1.2566370614·10−6 (H/m) is the permeability of free space, also known
as the magnetic constant. Taking the curl of the two last equations and using the mathematical Exercise 22: Simulate spherical waves
identity
∇ × (∇ × E ) = ∇(∇ · E ) − ∇2E = −∇2E when ∇ · E = 0, Implement a model for spherically symmetric waves using the method described in Section 15.6.
The boundary condition at r = 0 must be ∂u/∂r = 0, while the condition at r = R can either be
gives the wave equation governing the electric and magnetic field: u = 0 or a radiation condition as described in Problem 11. The u = 0 condition is sufficient if R
is so large that the amplitude of the spherical wave has become insignificant. Make movie(s) of
∂ 2E
= c2 ∇2E , (167) the case where the source term is located around r = 0 and sends out pulses
∂t2

∂ 2B r2
Q exp (− 2∆r 2 ) sin ωt, sin ωt ≥ 0
= c2 ∇2B , (168) f (r, t) =
∂t2 0, sin ωt < 0

with c = 1/ µ0 0 as the velocity of light. Each component of E and B fulfills a wave equation Here, Q and ω are constants to be chosen.
and can hence be solved independently.
Hint. Use the program wave1D_u0v.py as a starting point. Let solver compute the v function
and then set u = v/r. However, u = v/r for r = 0 requires special treatment. One possibility is
16 Exercises to compute u[1:] = v[1:]/r[1:] and then set u[0]=u[1]. The latter makes it evident that
∂u/∂r = 0 in a plot.
Exercise 19: Simulate waves on a non-homogeneous string
Filename: wave1D_spherical.
Simulate waves on a string that consists of two materials with different density. The tension in
the string is constant, but the density has a jump at the middle of the string. Experiment with
different sizes of the jump and produce animations that visualize the effect of the jump on the
wave motion.

89 90
Problem 23: Earthquake-generated tsunami over a subsea hill
A subsea earthquake leads to an immediate lift of the water surface, see Figure 9. The lifted
water surface splits into two tsunamis, one traveling to the right and one to the left, as depicted
in Figure 10. Since tsunamis are normally very long waves, compared to the depth, with a small
amplitude, compared to the wave length, the wave equation model described in Section 15.7 is
relevant:

ηtt = (gH(x)ηx )x ,
H0
where g is the acceleration of gravity, and H(x) is the still water depth.
x =0

I(x)

Figure 10: An initial surface elevation is split into two waves.


H0

x =0
I(x)

H0
Figure 9: Sketch of initial water surface due to a subsea earthquake. Ba
x =0 B(x)
To simulate the right-going tsunami, we can impose a symmetry boundary at x = 0: ∂η ∂x = 0.
We then simulate the wave motion in [0, L]. Unless the ocean ends at x = L, the waves should Bm 4mBs
travel undisturbed through the boundary x = L. A radiation condition as explained in Problem 11
can be used for this purpose. Alternatively, one can just stop the simulations before the wave
hits the boundary at x = L. In that case it does not matter what kind of boundary condition
we use at x = L. Imposing η = 0 and stopping the simulations when |ηin | > , i = Nx − 1, is a
possibility ( is a small parameter).
The shape of the initial surface can be taken as a Gaussian function, Figure 11: Sketch of an earthquake-generated tsunami passing over a subsea hill.
 2 !
x − Im
I(x; I0 , Ia , Im , Is ) = I0 + Ia exp − , (169)  2 !
Is x − Bm
B(x; B0 , Ba , Bm , Bs ) = B0 + Ba exp − , (170)
with Im = 0 reflecting
√ the location of the peak of I(x) and Is being a measure of the width of the
Bs
function I(x) (Is is 2 times the standard deviation of the familiar normal distribution curve). but many other shapes are also possible, e.g., a "cosine hat" where
Now
p we extend p the problem with a hill at the sea bottom, see Figure 11. The wave speed  
c = gH(x) = g(H0 − B(x)) will then be reduced in the shallow water above the hill. x − Bm
One possible form of the hill is a Gaussian function, B(x; B0 , Ba , Bm , Bs ) = B0 + Ba cos π , (171)
2Bs
when x ∈ [Bm − Bs , Bm + Bs ] while B = B0 outside this interval.

91 92
Also an abrupt construction may be tried: (which becomes −ηy = 0). The wave motion is to be simulated until the wave hits the reflecting
boundaries where ∂η/∂n = ηx = 0 (one can also set η = 0 - the particular condition does
B(x; B0 , Ba , Bm , Bs ) = B0 + Ba , (172) not matter as long as the simulation is stopped before the wave is influenced by the boundary
condition).
for x ∈ [Bm − Bs , Bm + Bs ] while B = B0 outside this interval. Visualize the surface elevation. Investigate how different hill shapes, different sizes of the
The wave1D_dn_vc.py23 program can be used as starting point for the implementation. water gap above the hill, and different resolutions ∆x = ∆y = h and ∆t influence the numerical
Visualize both the bottom topography and the water surface elevation in the same plot. Allow quality of the solution. Filename: tsunami2D_hill.
for a flexible choice of bottom shape: (170), (171), (172), or B(x) = B0 (flat).
The purpose of this problem is to explore the quality of the numerical solution ηin for different
shapes of the bottom obstruction. The "cosine hat" and the box-shaped hills have abrupt changes Problem 25: Investigate Matplotlib for visualization
in the derivative of H(x) and are more likely to generate numerical noise than the smooth Play with native Matplotlib code for visualizing 2D solutions of the wave equation with variable
Gaussian shape of the hill. Investigate if this is true. Filename: tsunami1D_hill. wave velocity. See if there are effective ways to visualize both the solution and the wave velocity.
Filename: tsunami2D_hill_mpl.
Problem 24: Earthquake-generated tsunami over a 3D hill
This problem extends Problem 23 to a three-dimensional wave phenomenon, governed by the Problem 26: Investigate visualization packages
2D PDE (153). We assume that the earthquake arise from a fault along the line x = 0 in the Create some fancy 3D visualization of the water waves and the subsea hill in Problem 24. Try to
xy-plane so that the initial lift of the surface can be taken as I(x) in Problem 23. That is, a make the hill transparent. Possible visualization tools are Mayavi24 , Paraview25 , and OpenDX26 .
plane wave is propagating to the right, but will experience bending because of the bottom. Filename: tsunami2D_hill_viz.
The bottom shape is now a function of x and y. An "elliptic" Gaussian function in two
dimensions, with its peak at (Bmx , Bmy ), generalizes (170):
Problem 27: Implement loops in compiled languages
 2  2 ! Extend the program from Problem 24 such that the loops over mesh points, inside the time
x − Bmx y − Bmy loop, are implemented in compiled languages. Consider implementations in Cython, Fortran via
B(x; B0 , Ba , Bmx , Bmy , Bs , b) = B0 + Ba exp − − , (173)
Bs bBs f2py, C via Cython, C via f2py, C/C++ via Instant, and C/C++ via scipy.weave. Perform
efficiency experiments to investigate the relative performance of the various implementations. It
where b is a scaling parameter: b = 1 gives a circular Gaussian function with circular contour is often advantageous to normalize CPU times by the fastest method on a given mesh. Filename:
lines, while b 6= 1 gives an elliptic shape with elliptic contour lines. tsunami2D_hill_compiled.
The "cosine hat" (171) can also be generalized to
   
x − Bmx y − Bmy Exercise 28: Simulate seismic waves in 2D
B(x; B0 , Ba , Bmx , Bmy , Bs ) = B0 + Ba cos π cos π , (174)
2Bs 2Bs The goal of this exercise is to simulate seismic waves using the PDE model (137) in a 2D xz
p
when 0 ≤ x2 + y 2 ≤ Bs and B = B0 outside this circle. domain with geological layers. Introduce m horizontal layers of thickness hi , i = 0, . . . , m − 1.
A box-shaped obstacle means that Inside layer number i we have a vertical wave velocity cz,i and a horizontal wave velocity ch,i .
Make a program for simulating such 2D waves. Test it on a case with 3 layers where
B(x; B0 , Ba , Bm , Bs , b) = B0 + Ba (175)
cz,0 = cz,1 = cz,2 , ch,0 = ch,2 , ch,1  ch,0 .
for x and y inside a rectangle
Let s be a localized point source at the middle of the Earth’s surface (the upper boundary) and
Bmx − Bs ≤ x ≤ Bmx + Bs , Bmy − bBs ≤ y ≤ Bmy + bBs , investigate how the resulting wave travels through the medium. The source can be a localized
Gaussian peak that oscillates in time for some time interval. Place the boundaries far enough
and B = B0 outside this rectangle. The b parameter controls the rectangular shape of the cross from the expanding wave so that the boundary conditions do not disturb the wave. Then the type
section of the box. of boundary condition does not matter, except that we physically need to have p = p0 , where p0
Note that the initial condition and the listed bottom shapes are symmetric around the line is the atmospheric pressure, at the upper boundary. Filename: seismic2D.
y = Bmy . We therefore expect the surface elevation also to be symmetric with respect to this 24 http://code.enthought.com/projects/mayavi/
line. This means that we can halve the computational domain by working with [0, Lx ] × [0, Bmy ]. 25 http://www.paraview.org/
Along the upper boundary, y = Bmy , we must impose the symmetry condition ∂η/∂n = 0. 26 http://www.opendx.org/

Such a symmetry condition (−ηx = 0) is also needed at the x = 0 boundary because the initial
condition has a symmetry here. At the lower boundary y = 0 we also set a Neumann condition
23 http://tinyurl.com/nm5587k/wave/wave1D/wave1D_dn_vc.py

93 94
Project 29: Model 3D acoustic waves in a room This boundary condition means that what goes out of the domain at x = L comes in at x = 0.
Roughly speaking, we need only one boundary condition because of the spatial derivative is of
The equation for sound waves in air is derived in Section 15.5 and reads
first order only.
ptt = c2 ∇2 p,
Physical interpretation. The parameter c can be constant or variable, c = c(x). The equation
where p(x, y, z, t) is the pressure and c is the speed of sound, taken as 340 m/s. However, sound (178) arises in transport problems where a quantity u, which could be temperature or concentration
is absorbed in the air due to relaxation of molecules in the gas. A model for simple relaxation, of some contaminant, is transported with the velocity c of a fluid. In addition to the transport
valid for gases consisting only of one type of molecules, is a term c2 τs ∇2 pt in the PDE, where τs imposed by "travelling with the fluid", u may also be transported by diffusion (such as heat
is the relaxation time. If we generate sound from, e.g., a loudspeaker in the room, this sound conduction or Fickian diffusion), but we have in the model ut + cux assumed that diffusion effects
source must also be added to the governing equation. are negligible, which they often are.
The PDE with the mentioned type of damping and source then becomes
a) Show that under the assumption of a = const,
pt t = c2 ∇p + c2 τs ∇2 pt + f, (176)
u(x, t) = I(x − ct) (181)
where f (x, y, z, t) is the source term.
The walls can absorb some sound. A possible model is to have a "wall layer" (thicker than the fulfills the PDE as well as the initial and boundary condition (provided I(0) = I(L)).
physical wall) outside the room where c is changed such that some of the wave energy is reflected A widely used numerical scheme for (178) applies a forward difference in time and a backward
and some is absorbed in the wall. The absorption of energy can be taken care of by adding a difference in space when c > 0:
damping term bpt in the equation:
[Dt+ u + cDx− u = 0]ni . (182)
pt t + bpt = c2 ∇p + c2 τs ∇2 pt + f . (177) For c < 0 we use a forward difference in space: [cDx+ u]ni .
Typically, b = 0 in the room and b > 0 in the wall. A discontinuity in b or c will give rise to
b) Set up a computational algorithm and implement it in a function. Assume a is constant and
reflections. It can be wise to use a constant c in the wall to control reflections because of the
positive.
discontinuity between c in the air and in the wall, while b is gradually increased as we go into
the wall to avoid reflections because of rapid changes in b. At the outer boundary of the wall c) Test implementation by using the remarkable property that the numerical solution is exact at
the condition p = 0 or ∂p/∂n = 0 can be imposed. The waves should anyway be approximately the mesh points if ∆t = c−1 ∆x.
dampened to p = 0 this far out in the wall layer.
d) Make a movie comparing the numerical and exact solution for the following two choices of
There are two strategies for discretizing the ∇2 pt term: using a center difference between
initial conditions:
times n + 1 and n − 1 (if the equation is sampled at level n), or use a one-sided difference based
on levels n and n − 1. The latter has the advantage of not leading to any equation system, while h  x i2n
the former is second-order accurate as the scheme for the simple wave equation pt t = c2 ∇2 p. To I(x) = sin π (183)
L
avoid an equation system, go for the one-sided difference such that the overall scheme becomes
where n is an integer, typically n = 5, and
explicit and only of first order in time.
Develop a 3D solver for the specified PDE and introduce a wall layer. Test the solver with  
(x − L/2)2
the method of manufactured solutions. Make some demonstrations where the wall reflects and I(x) = exp − . (184)
2σ2
absorbs the waves (reflection because of discontinuity in b and absorption because of growing b).
Experiment with the impact of the τs parameter. Filename: acoustics. Choose ∆t = c−1 ∆x, 0.9c−1 ∆x, 0.5c−1 ∆x.
e) The performance of the suggested numerical scheme can be investigated by analyzing the
Project 30: Solve a 1D transport equation numerical dispersion relation. Analytically, we have that the Fourier component
We shall study the wave equation
u(x, t) = ei(kx−ωt) ,
ut + cux = 0, x ∈ (0, L], t ∈ (0, T ], (178) is a solution of the PDE if ω = kc. This is the analytical dispersion relation. A complete solution
with initial condition of the PDE can be built by adding up such Fourier components with different amplitudes, where
the initial condition I determines the amplitudes. The solution u is then represented by a Fourier
u(x, 0) = I(x), x ∈ [0, L], (179) series.
A similar discrete Fourier component at (xp , tn ) is
and one periodic boundary condition
uqp = ei(kp∆x−ω̃n∆t) ,
u(0, t) = u(L, t) . (180)

95 96
where in general ω̃ is a function of k, ∆t, and ∆x, and differs from the exact ω = kc. To compute C(x) we need to integrate 1/c, which can be done by a Trapezoidal rule. Suppose
Insert the discrete Fourier component in the numerical scheme and derive an expression for ω̃, we have computed C(xi ) and need to compute C(xi+1 ). Using the Trapezoidal rule with m
i.e., the discrete dispersion relation. Show in particular that if the ∆t/(c∆x) = 1, the discrete subintervals over the integration domain [xi , xi+1 ] gives
solution coincides with the exact solution at the mesh points, regardless of the mesh resolution
(!). Show that if the stability condition  
Z
1 1 1 1 1
xi+1 m−1
X
dx
∆t C(xi+1 ) = C(xi ) + ≈ h + + , (186)
≤ 1, xi c 2 c(xi ) 2 c(xi+1 ) j=1
c(xi + jh)
c∆x
the discrete Fourier component cannot grow (i.e., ω̃ is real). where h = (xi+1 − xi )/m is the length of the subintervals used for the integral over [xi , xi+1 ]. We
f) Write a test for your implementation where you try to use information from the numerical observe that (186) is a difference equation which we can solve by repeatedly applying (186) for
dispersion relation. i = 0, 1, . . . , Nx − 1 if a mesh x0 , x, . . . , xNx is prescribed. Note that C(0) = 0.
We shall hereafter assume that = c(x) > 0. j) Implement a function for computing C(xi ) and one for computing C −1 (x) for any x. Use
g) Set up a computational algorithm for the variable coefficient case and implement it in a these two functions for computing the exact solution I(C −1 (C(x) − t)). End up with a function
function. Make a test that the function works for constant a. u_exact_variable_c(x, n, c, I) that returns the value of I(C −1 (C(x) − tn )).

h) It can be shown that for an observer moving with velocity c(x), u is constant. This can be k) Make movies showing a comparison of the numerical and exact solutions for the two initial
used to derive an exact solution when a varies with x. Show first that conditions (183) and (30). Choose ∆t = ∆x/ max0,L c(x) and the velocity of the medium as

u(x, t) = f (C(x) − t), (185) 1. c(x) = 1 +  sin(kπx/L),  < 1,

where 2. c(x) = 1 + I(x), where I is given by (183) or (30).

1 The PDE ut + cux = 0 expresses that the initial condition I(x) is transported with velocity c(x).
C 0 (x) = ,
c(x) Filename: advec1D.
is a solution of (178) for any differentiable function f .
Problem 31: General analytical solution of a 1D damped wave equation
i) Use the initial condition to show that an exact solution is
We consider an initial-boundary value problem for the damped wave equation:
u(x, t) = I(C −1 (C(x) − t)),
R Rx
with C being the inverse function of C = c1 dx. Since C(x) is an integral 0 (1/c)dx, C(x) is
−1
utt + but = c2 uxx , x ∈ (0, L), t ∈ (0, T ]
monotonically increasing and there exists hence an inverse function C −1 with values in [0, L]. u(0, t) = 0,
To compute (185) we need to integrate 1/c to obtain C and then compute the inverse of C.
u(L, t) = 0,
The inverse function computation can be easily done if we first think discretely. Say we have
some function y = g(x) and seeks its inverse. Plotting (xi , yi ), where yi = g(xi ) for some mesh u(x, 0) = I(x),
points xi , displays g as a function of x. The inverse function is simply x as a function of g, ut (x, 0) = V (x) .
i.e., the curve with points (yi , xi ). We can therefore quickly compute points at the curve of the
inverse function. One way of extending these points to a continuous function is to assume a Here, b ≥ 0 and c are given constants. The aim is to derive a general analytical solution of this
linear variation (known as linear interpolation) between the points (which actually means to draw problem. Familiarity with the method of separation of variables for solving PDEs will be assumed.
straight lines between the points, exactly as done by a plotting program). a) Seek a solution on the form u(x, t) = X(x)T (t). Insert this solution in the PDE and show
The function wrap2callable in scitools.std can take a set of points and return a continuous that it leads to two differential equations for X and T :
function that corresponds to linear variation between the points. The computation of the inverse
of a function g on [0, L] can then be done by T 00 + bT 0 + λT = 0, c2 X 00 + λX = 0,
def inverse(g, domain, resolution=101): with X(0) = X(L) = 0 as boundary conditions, and λ as a constant to be determined.
x = linspace(domain[0], domain[L], resolution)
y = g(x) b) Show that X(x) is on the form
from scitools.std import wrap2callable
g_inverse = wrap2callable((y, x)) nπ
return g_inverse Xn (x) = Cn sin kx, k= , n = 1, 2, . . .
L
where Cn is an arbitrary constant.

97 98
c) Under the assumption that (b/2)2 < k 2 , show that T (t) is on the form
r
1 1
Tn (t) = e− 2 bt (an cos ωt + bn sin ωt), ω = k 2 − b2 , n = 1, 2, . . .
4
The complete solution is then

X 1
u(x, t) = sin kxe− 2 bt (An cos ωt + Bn sin ωt),
n=1

where the constants An and Bn must be computed from the initial conditions.
d) Derive a formula for An from u(x, 0) = I(x) and developing I(x) as a sine Fourier series on
[0, L].
e) Derive a formula for Bn from ut (x, 0) = V (x) and developing V (x) as a sine Fourier series on
[0, L].
f) Calculate An and Bn from vibrations of a string where V (x) = 0 and

ax/x0 , x < x0 ,
I(x) = (187)
a(L − x)/(L − x0 ), otherwise
g) Implement the series for u(x, t) in a function u_series(x, t, tol=1E-10), where tol is a
tolerance for truncating the series. Simply sum the terms until |an | and |bb | both are less than
tol.
h) What will change in the derivation of the analytical solution if we have ux (0, t) = ux (L, t) = 0
as boundary conditions? And how will you solve the problem with u(0, t) = 0 and ux (L, t) = 0?
Filename: damped_wave1D.

Problem 32: General analytical solution of a 2D damped wave equation


Carry out Problem 31 in the 2D case: utt + but = c2 (uxx + uyy ), where (x, y) ∈ (0, Lx ) × (0, Ly ).
Assume a solution on the form u(x, y, t) = X(x)Y (y)T (t). Filename: damped_wave2D.

References
[1] H. P. Langtangen. Scientific software engineering; wave equation case. http://tinyurl.com/
k3sdbuv/pub/softeng2.
[2] H. P. Langtangen. Finite Difference Computing with Exponential Decay Models. 2015.
http://tinyurl.com/nclmcng/web.

99 100
Index
arithmetic mean, 41 slice, 22
array computing, 22 software testing
array slices, 22 nose, 16
averaging pytest, 16
arithmetic, 41 stability criterion, 60
geometric, 41 stencil
harmonic, 41 1D wave equation, 6
Neumann boundary, 32
boundary condition
open (radiation), 51 unit testing, 16
boundary conditions
Dirichlet, 31 vectorization, 22
Neumann, 31
periodic, 52 wave equation
1D, 5
callback function, 15 1D, analytical properties, 56
closure, 18 1D, exact numerical solution, 59
Courant number, 60 1D, finite difference method, 5
1D, implementation, 14
Dirichlet conditions, 31 1D, stability, 60
discrete Fourier transform, 57 2D, implementation, 70
waves
Fourier series, 57 on a string, 5
Fourier transform, 57

geometric mean, 41

harmonic average, 41
homogeneous Dirichlet conditions, 31
homogeneous Neumann conditions, 31

index set notation, 34, 72

lambda function (Python), 25

mesh
finite differences, 5
mesh function, 6

Neumann conditions, 31
nose test, 16

open boundary condition, 51

periodic boundary conditions, 52


pytest test, 16

radiation condition, 51

scalar code, 22

101

You might also like