Abstract
A very wide range of physical processes lead to wave motion, where signals are propagated through a medium in space and time, normally with little or no permanent movement of the 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. Many types of wave motion can be described by the equation \(u_{tt}=\nabla\cdot(c^{2}\nabla u)+f\), which we will solve in the forthcoming text by finite difference methods.
Download chapter PDF
A very wide range of physical processes lead to wave motion, where signals are propagated through a medium in space and time, normally with little or no permanent movement of the 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. Many types of wave motion can be described by the equation \(u_{tt}=\nabla\cdot(c^{2}\nabla u)+f\), which we will solve in the forthcoming text by finite difference methods.
2.1 Simulation of Waves on a String
We begin our study of wave equations by simulating onedimensional waves on a string, say on a guitar or violin. Let the string in the undeformed state coincide with the interval \([0,L]\) on the x 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 constant c and the function I(x) must be prescribed.
Equation (2.1) is known as the onedimensional wave equation. Since this PDE contains a secondorder derivative in time, we need two initial conditions. The condition (2.2) specifies the initial shape of the string, I(x), and (2.3) expresses that the initial velocity of the string is zero. In addition, PDEs need boundary conditions, given here as (2.4) and (2.5). These two conditions specify that the string is fixed at the ends, i.e., that the displacement u is zero.
The solution \(u(x,t)\) varies in space and time and describes waves that move with velocity c to the left and right.
Sometimes we will use a more compact notation for the partial derivatives to save space:
and similar expressions for derivatives with respect to other variables. Then the wave equation can be written compactly as \(u_{tt}=c^{2}u_{xx}\).
The PDE problem (2.1)–(2.5 ) will now be discretized in space and time by a finite difference method.
2.1.1 Discretizing the Domain
The temporal domain \([0,T]\) is represented by a finite number of mesh points
Similarly, the spatial domain \([0,L]\) is replaced by a set of mesh points
One may view the mesh as twodimensional in the x,t plane, consisting of points \((x_{i},t_{n})\), with \(i=0,\ldots,N_{x}\) and \(n=0,\ldots,N_{t}\).
Uniform meshes
For uniformly distributed mesh points we can introduce the constant mesh spacings \(\Delta t\) and \(\Delta x\). We have that
We also have that \(\Delta x=x_{i}x_{i1}\), \(i=1,\ldots,N_{x}\), and \(\Delta t=t_{n}t_{n1}\), \(n=1,\ldots,N_{t}\). Figure 2.1 displays a mesh in the x,t plane with \(N_{t}=5\), \(N_{x}=5\), and constant mesh spacings.
2.1.2 The Discrete Solution
The solution \(u(x,t)\) is sought at the mesh points. We introduce the mesh function \(u_{i}^{n}\), which approximates the exact solution at the mesh point \((x_{i},t_{n})\) for \(i=0,\ldots,N_{x}\) and \(n=0,\ldots,N_{t}\). Using the finite difference method, we shall develop algebraic equations for computing the mesh function.
2.1.3 Fulfilling the Equation at the Mesh Points
In the finite difference method, we relax the condition that (2.1) holds at all points in the spacetime domain \((0,L)\times(0,T]\) to the requirement that the PDE is fulfilled at the interior mesh points only:
for \(i=1,\ldots,N_{x}1\) and \(n=1,\ldots,N_{t}1\). For n = 0 we have the initial conditions \(u=I(x)\) and \(u_{t}=0\), and at the boundaries \(i=0,N_{x}\) we have the boundary condition u = 0.
2.1.4 Replacing Derivatives by Finite Differences
The secondorder derivatives can be replaced by central differences. The most widely used difference approximation of the secondorder derivative is
It is convenient to introduce the finite difference operator notation
A similar approximation of the secondorder derivative in the x direction reads
Algebraic version of the PDE
We can now replace the derivatives in (2.10) and get
or written more compactly using the operator notation:
Interpretation of the equation as a stencil
A characteristic feature of (2.11) is that it involves u values from neighboring points only: \(u_{i}^{n+1}\), \(u^{n}_{i\pm 1}\), \(u^{n}_{i}\), and \(u^{n1}_{i}\). The circles in Fig. 2.1 illustrate such neighboring mesh points that contribute to an algebraic equation. In this particular case, we have sampled the PDE at the point \((2,2)\) and constructed (2.11), which then involves a coupling of \(u_{1}^{2}\), \(u_{2}^{3}\), \(u_{2}^{2}\), \(u_{2}^{1}\), and \(u_{3}^{2}\). The term stencil is often used about the algebraic equation at a mesh point, and the geometry of a typical stencil is illustrated in Fig. 2.1. One also often refers to the algebraic equations as discrete equations, (finite) difference equations or a finite difference scheme.
Algebraic version of the initial conditions
We also need to replace the derivative in the initial condition (2.3) by a finite difference approximation. A centered difference of the type
seems appropriate. Writing out this equation and ordering the terms give
The other initial condition can be computed by
2.1.5 Formulating a Recursive Algorithm
We assume that \(u^{n}_{i}\) and \(u^{n1}_{i}\) are available for \(i=0,\ldots,N_{x}\). The only unknown quantity in (2.11) is therefore \(u^{n+1}_{i}\), which we now can solve for:
We have here introduced the parameter
known as the Courant number.
C is the key parameter in the discrete wave equation
We see that the discrete version of the PDE features only one parameter, C, which is therefore the key parameter, together with N _{ x }, that governs the quality of the numerical solution (see Sect. 2.10 for details). Both the primary physical parameter c and the numerical parameters \(\Delta x\) and \(\Delta t\) are lumped together in C. Note that C is a dimensionless parameter.
Given that \(u^{n1}_{i}\) and \(u^{n}_{i}\) are known for \(i=0,\ldots,N_{x}\), we find new values at the next time level by applying the formula (2.14) for \(i=1,\ldots,N_{x}1\). Figure 2.1 illustrates the points that are used to compute \(u^{3}_{2}\). For the boundary points, i = 0 and \(i=N_{x}\), we apply the boundary conditions \(u_{i}^{n+1}=0\).
Even though sound reasoning leads up to (2.14), there is still a minor challenge with it that needs to be resolved. Think of the very first computational step to be made. The scheme (2.14) is supposed to start at n = 1, which means that we compute u ^{2} from u ^{1} and u ^{0}. Unfortunately, we do not know the value of u ^{1}, so how to proceed? A standard procedure in such cases is to apply (2.14) also for n = 0. This immediately seems strange, since it involves \(u^{1}_{i}\), which is an undefined quantity outside the time mesh (and the time domain). However, we can use the initial condition (2.13) in combination with (2.14) when n = 0 to eliminate \(u^{1}_{i}\) and arrive at a special formula for \(u_{i}^{1}\):
Figure 2.2 illustrates how (2.16) connects four instead of five points: \(u^{1}_{2}\), \(u_{1}^{0}\), \(u_{2}^{0}\), and \(u_{3}^{0}\).
We can now summarize the computational algorithm:

1.
Compute \(u^{0}_{i}=I(x_{i})\) for \(i=0,\ldots,N_{x}\)

2.
Compute \(u^{1}_{i}\) by (2.16) for \(i=1,2,\ldots,N_{x}1\) and set \(u_{i}^{1}=0\) for the boundary points given by i = 0 and \(i=N_{x}\),

3.
For each time level \(n=1,2,\ldots,N_{t}1\)

a)
apply (2.14) to find \(u^{n+1}_{i}\) for \(i=1,\ldots,N_{x}1\)

b)
set \(u^{n+1}_{i}=0\) for the boundary points having i = 0, \(i=N_{x}\).

a)
The algorithm essentially consists of moving a finite difference stencil through all the mesh points, which can be seen as an animation in a web page ^{Footnote 1} or a movie file ^{Footnote 2}.
2.1.6 Sketch of an Implementation
The algorithm only involves the three most recent time levels, so we need only three arrays for \(u_{i}^{n+1}\), \(u_{i}^{n}\), and \(u_{i}^{n1}\), \(i=0,\ldots,N_{x}\). Storing all the solutions in a twodimensional array of size \((N_{x}+1)\times(N_{t}+1)\) would be possible in this simple onedimensional PDE problem, but is normally out of the question in threedimensional (3D) and large twodimensional (2D) problems. We shall therefore, in all our PDE solving programs, have the unknown in memory at as few time levels as possible.
In a Python implementation of this algorithm, we use the array elements u[i] to store \(u^{n+1}_{i}\), u_n[i] to store \(u^{n}_{i}\), and u_nm1[i] to store \(u^{n1}_{i}\).
The following Python snippet realizes the steps in the computational algorithm.
2.2 Verification
Before implementing the algorithm, it is convenient to add a source term to the PDE (2.1), since that gives us more freedom in finding test problems for verification. Physically, a source term acts as a generator for waves in the interior of the domain.
2.2.1 A Slightly Generalized Model Problem
We now address the following extended initialboundary value problem for onedimensional wave phenomena:
Sampling the PDE at \((x_{i},t_{n})\) and using the same finite difference approximations as above, yields
Writing this out and solving for the unknown \(u^{n+1}_{i}\) results in
The equation for the first time step must be rederived. The discretization of the initial condition \(u_{t}=V(x)\) at t = 0 becomes
which, when inserted in (2.23) for n = 0, gives the special formula
2.2.2 Using an Analytical Solution of Physical Significance
Many wave problems feature sinusoidal oscillations in time and space. For example, the original PDE problem (2.1)–(2.5) allows an exact solution
This \(u_{\mbox{\footnotesize e}}\) fulfills the PDE with f = 0, boundary conditions \(u_{\mbox{\footnotesize e}}(0,t)=u_{\mbox{\footnotesize e}}(L,t)=0\), as well as initial conditions \(I(x)=A\sin\left(\frac{\pi}{L}x\right)\) and V = 0.
How to use exact solutions for verification
It is common to use such exact solutions of physical interest to verify implementations. However, the numerical solution \(u^{n}_{i}\) will only be an approximation to \(u_{\mbox{\footnotesize e}}(x_{i},t_{n})\). We have no knowledge of the precise size of the error in this approximation, and therefore we can never know if discrepancies between \(u^{n}_{i}\) and \(u_{\mbox{\footnotesize e}}(x_{i},t_{n})\) are caused by mathematical approximations or programming errors. In particular, if plots of the computed solution \(u^{n}_{i}\) and the exact one (2.25) look similar, many are tempted to claim that the implementation works. However, even if color plots look nice and the accuracy is ‘‘deemed good’’, there can still be serious programming errors present!
The only way to use exact physical solutions like (2.25) for serious and thorough verification is to run a series of simulations on 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 computing of convergence rates is given in Section 3.1.6 in [9]. There is also a detailed example on computing convergence rates in Sect. 1.2.2.
In the present problem, one expects the method to have a convergence rate of 2 (see Sect. 2.10), so if the computed rates are close to 2 on a sufficiently fine mesh, we have good evidence that the implementation is free of programming mistakes.
2.2.3 Manufactured Solution and Estimation of Convergence Rates
Specifying the solution and computing corresponding data
One problem with the exact solution (2.25) is that it requires a simplification (V = 0,f = 0) of the implemented problem (2.17)–(2.21). An advantage of using a manufactured solution is that we can test all terms in the PDE problem. The idea of this approach is to set up some chosen solution and fit the source term, boundary conditions, and initial conditions to be compatible with the chosen 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
Inserted in the PDE \(u_{tt}=c^{2}u_{xx}+f\) we get
The initial conditions become
Defining a single discretization parameter
To verify the code, we compute the convergence rates in a series of simulations, letting each simulation use a finer mesh than the previous one. Such empirical estimation of convergence rates relies on an assumption that some measure E of the numerical error is related to the discretization parameters through
where C _{ t }, C _{ x }, r, and p are constants. The constants r and p are known as the convergence rates in time and space, respectively. From the accuracy in the finite difference approximations, we expect r = p = 2, since the error terms are of order \(\Delta t^{2}\) and \(\Delta x^{2}\). This is confirmed by truncation error analysis and other types of analysis.
By using an exact solution of the PDE problem, we will next compute the error measure E on a sequence of refined meshes and see if the rates r = p = 2 are obtained. We will not be concerned with estimating the constants C _{ t } and C _{ x }, simply because we are not interested in their values.
It is advantageous to introduce a single discretization parameter \(h=\Delta t=\hat{c}\Delta x\) for some constant \(\hat{c}\). Since \(\Delta t\) and \(\Delta x\) are related through the Courant number, \(\Delta t=C\Delta x/c\), we set \(h=\Delta t\), and then \(\Delta x=hc/C\). Now the expression for the error measure is greatly simplified:
Computing errors
We choose an initial discretization parameter h _{0} and run experiments with decreasing h: \(h_{i}=2^{i}h_{0}\), \(i=1,2,\ldots,m\). Halving h in each experiment is not necessary, but it is a common choice. For each experiment we must record E and h. Standard choices of error measure are the \(\ell^{2}\) and \(\ell^{\infty}\) norms of the error mesh function \(e^{n}_{i}\):
In Python, one can compute \(\sum_{i}(e^{n}_{i})^{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 \(\ell^{\infty}\) norm one must compare the maximum error at a time level (e.max()) with the global maximum over the time domain: e_max = max(e_max, e.max()).
An alternative error measure is to use a spatial norm at one time step only, e.g., the end time T (\(n=N_{t}\)):
The important point is that the error measure (E) for the simulation is represented by a single number.
Computing rates
Let E _{ i } be the error measure in experiment (mesh) number i (not to be confused with the spatial index i) and let h _{ i } be the corresponding discretization parameter (h). With the error model \(E_{i}=Dh_{i}^{r}\), we can estimate r by comparing two consecutive experiments:
Dividing the two equations eliminates the (uninteresting) constant D. Thereafter, solving for r yields
Since r depends on i, i.e., which simulations we compare, we add an index to r: r _{ i }, where \(i=0,\ldots,m2\), if we have m experiments: \((h_{0},E_{0}),\ldots,(h_{m1},E_{m1})\).
In our present discretization of the wave equation we expect r = 2, and hence the r _{ i } values should converge to 2 as i increases.
2.2.4 Constructing an Exact Solution of the Discrete Equations
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 significantly better test though, would be to check that the numerical solution is exactly what it should be. This will in general require exact knowledge of the numerical error, which we do not normally have (although we in Sect. 2.10 establish such knowledge in simple cases). However, it is possible to look for solutions where we can show that the numerical error vanishes, i.e., the solution of the original continuous PDE problem is also a solution of the discrete equations. This property often arises if the exact solution of the PDE is a lowerorder polynomial. (Truncation error analysis leads to error measures that involve derivatives of the exact solution. In the present problem, the truncation error involves 4thorder 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. More specifically, we set
which by insertion in the PDE leads to \(f(x,t)=2(1+t)c^{2}\). This \(u_{\mbox{\footnotesize e}}\) fulfills the boundary conditions u = 0 and demands \(I(x)=x(Lx)\) and \(V(x)={\frac{1}{2}}x(Lx)\).
To realize that the chosen \(u_{\mbox{\footnotesize e}}\) is also an exact solution of the discrete equations, we first remind ourselves that \(t_{n}=n\Delta t\) so that
Hence,
Similarly, we get that
Now, \(f^{n}_{i}=2(1+{\frac{1}{2}}t_{n})c^{2}\), which results in
Moreover, \(u_{\mbox{\footnotesize e}}(x_{i},0)=I(x_{i})\), \(\partial u_{\mbox{\footnotesize e}}/\partial t=V(x_{i})\) at t = 0, and \(u_{\mbox{\footnotesize e}}(x_{0},t)=u_{\mbox{\footnotesize e}}(x_{N_{x}},0)=0\). Also the modified scheme for the first time step is fulfilled by \(u_{\mbox{\footnotesize e}}(x_{i},t_{n})\).
Therefore, the exact solution \(u_{\mbox{\footnotesize e}}(x,t)=x(Lx)(1+t/2)\) of the PDE problem is also an exact solution of the discrete problem. This means that we know beforehand what numbers the numerical algorithm should produce. We can use this fact to check that the computed \(u^{n}_{i}\) values from an implementation equals \(u_{\mbox{\footnotesize e}}(x_{i},t_{n})\), within machine precision. This result is valid regardless of the mesh spacings \(\Delta x\) and \(\Delta t\)! Nevertheless, there might be stability restrictions on \(\Delta x\) and \(\Delta t\), so the test 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).
Notice
A product of quadratic or linear expressions in the various independent variables, as shown above, will often fulfill both the PDE problem and the discrete equations, and can therefore be very useful solutions for verifying implementations.
However, for 1D wave equations of the type \(u_{tt}=c^{2}u_{xx}\) we shall see that there is always another much more powerful way of generating exact solutions (which consists in just setting C = 1 (!), as shown in Sect. 2.10).
2.3 Implementation
This section presents the complete computational algorithm, its implementation in Python code, animation of the solution, and verification of the implementation.
A real implementation of the basic computational algorithm from Sect. 2.1.5 and 2.1.6 can be encapsulated in a function, taking all the input data for the problem as arguments. The physical input data consists of c, I(x), V(x), \(f(x,t)\), L, and T. The numerical input is the mesh parameters \(\Delta t\) and \(\Delta x\).
Instead of specifying \(\Delta t\) and \(\Delta x\), we can specify one of them and the Courant number C 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 N _{ x }. The solver function can then compute \(\Delta t=CL/(cN_{x})\). However, for comparing \(u(x,t)\) curves (as functions of x) for various Courant numbers it is more convenient to keep \(\Delta t\) fixed for all C and let \(\Delta x\) vary according to \(\Delta x=c\Delta t/C\). With \(\Delta t\) fixed, all frames correspond to the same time t, and this simplifies animations that compare simulations with different mesh resolutions. Plotting functions of x with different spatial resolution is trivial, so it is easier to let \(\Delta x\) vary in the simulations than \(\Delta t\).
2.3.1 Callback Function for UserSpecific Actions
The solution at all spatial points at a new time level is stored in an array u of length \(N_{x}+1\). We need to decide what to do with this solution, e.g., visualize the curve, analyze the values, or write the array to file for later use. The decision about what to do is left to the user in the form of a usersupplied function
where u is the solution at the spatial points x at time t[n]. The user_action function is called from the solver at each time level n.
If the user wants to plot the solution or store the solution at a time point, she needs to write such a function and take appropriate actions inside it. We will show examples on many such user_action functions.
Since the solver function makes calls back to the user’s code via such a function, this type of function is called a callback function. When writing general software, like our solver function, which also needs to carry out special problem or solutiondependent actions (like visualization), it is a common technique to leave those actions to usersupplied callback functions.
The callback function can be used to terminate the solution process if the user returns True. For example,
is a callback function that will terminate the solver function (given below) of the amplitude of the waves exceed 10, which is here considered as a numerical instability.
2.3.2 The Solver Function
A first attempt at a solver function is listed below.
A couple of remarks about the above code is perhaps necessary:

Although we give dt and compute dx via C and c, the resulting t and x meshes do not necessarily correspond exactly to these values because of rounding errors. To explicitly ensure that dx and dt correspond to the cell sizes in x and t, we recompute the values.

According to the particular choice made in Sect. 2.3.1, a true value returned from user_action should terminate the simulation. This is here implemented by a break statement inside the for loop in the solver.
2.3.3 Verification: Exact Quadratic Solution
We use the test problem derived in Sect. 2.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 nose or pytest.
When this function resides in the file wave1D_u0.py, one can run pytest to check that all test functions with names test_*() in this file work:
2.3.4 Verification: Convergence Rates
A more general method, but not so reliable as a verification method, is to compute the convergence rates and see if they coincide with theoretical estimates. Here we expect a rate of 2 according to the various results in Sect. 2.10. A general function for computing convergence rates can be written like this:
Using the analytical solution from Sect. 2.2.2, we can call convergence_rates to see if we get a convergence rate that approaches 2 and use the final estimate of the rate in an assert statement such that this function becomes a proper test function:
Doing py.test s v wave1D_u0.py will run also this test function and show the rates 2.05, 1.98, 2.00, 2.00, and 2.00 (to two decimals).
2.3.5 Visualization: Animating the Solution
Now that we have verified the implementation it is time to do a real computation where we also display evolution of the waves on the screen. Since the solver function knows nothing about what type of visualizations we may want, it calls the callback function user_action(u, x, t, n). We must therefore write this function and find the proper statements for plotting the solution.
Function for administering the simulation
The following viz function

1.
defines a user_action callback function for plotting the solution at each time level,

2.
calls the solver function, and

3.
combines all the plots (in files) to video in different formats.
Dissection of the code
The viz function can either use SciTools or Matplotlib for visualizing the solution. The user_action function based on SciTools is called plot_u_st, while the 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 Matplotlib and Gnuplot (and many other plotting programs) for doing the graphics, but Gnuplot is a relevant choice for large N _{ x } or in twodimensional problems as Gnuplot is significantly faster than Matplotlib for screen animations.
A function inside another function, like plot_u_st in the above code segment, has access to and remembers all the local variables in the surrounding code inside the viz function (!). This is known in computer science as a closure and is very convenient to program with. For example, the plt and time modules defined outside plot_u are accessible for plot_u_st when the function is called (as user_action) in the solver function. Some may think, however, that a class instead of a closure is a cleaner and easiertounderstand implementation of the user action function, see Sect. 2.8.
The plot_u_st function just makes a standard SciTools plot command for plotting u as a function of x at time t[n]. To achieve a smooth animation, the plot command should take keyword arguments instead of being broken into separate calls to xlabel, ylabel, axis, time, and show. Several plot calls will automatically cause an animation on the screen. In addition, we want to save each frame in the animation to file. We then need a filename where the frame number is padded with zeros, here tmp_0000.png, tmp_0001.png, and so on. The proper printf construction is then tmp_%04d.png. Section 1.3.2 contains more basic information on making animations.
The solver is called with an argument plot_u as user_function. If the user chooses to use SciTools, plot_u is the plot_u_st callback function, but for Matplotlib it is an instance of the class PlotMatplotlib. Also this class makes use of variables defined in the viz function: plt and time. With Matplotlib, one has to make the first plot the standard way, and then update 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 to use a closure for the user_action function when doing the animation with Matplotlib. It is 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 plot_u(u, x, t, n) can be called as a standard callback function from solver.
Making movie files
From the frame_*.png files containing the frames in the animation we can make video files. Section 1.3.2 presents basic information on how to use the ffmpeg (or avconv) program for producing video files in different modern formats: Flash, MP4, Webm, and Ogg.
The viz function creates an ffmpeg or avconv command with the proper arguments for each of the formats Flash, MP4, WebM, and Ogg. The task is greatly simplified by having a codec2ext dictionary for mapping video codec names to filename extensions. As mentioned in Sect. 1.3.2, only two formats are actually needed to ensure that all browsers can successfully play the video: MP4 and WebM.
Some animations having a large number of plot files may not be properly combined into a video using ffmpeg or avconv. A method that always works is to play the PNG files as an animation in a browser using JavaScript code in an HTML file. The SciTools package has a function movie (or a standalone command scitools movie) for creating such an HTML player. The plt.movie call in the viz function shows how the function is used. The file movie.html can be loaded into a browser and features a user interface where the speed of the animation can be controlled. Note that the movie in this case consists of the movie.html file and all the frame files tmp_*.png.
Skipping frames for animation speed
Sometimes the time step is small and T is large, leading to an inconveniently large number of plot files and a slow animation on the screen. The solution to such a problem is to decide on a total number of frames in the animation, num_frames, and plot the solution only for every skip_frame frames. For example, setting skip_frame=5 leads to plots of every 5 frames. The default value skip_frame=1 plots every frame. The total number of time levels (i.e., maximum possible number of frames) is the length of t, t.size (or len(t)), so if we want num_frames frames in the animation, we need to plot every t.size/num_frames frames:
The initial condition (n=0) is included by n % skip_frame == 0, as well as every skip_frameth frame. As n % skip_frame == 0 will very seldom be true for the very final frame, we must also check if n == t.size1 to get the final frame included.
A simple choice of numbers may illustrate the formulas: say we have 801 frames in total (t.size) and we allow only 60 frames to be plotted. As n then runs from 801 to 0, we need to plot every 801/60 frame, which with integer division yields 13 as skip_frame. Using the mod function, n % skip_frame, this operation is zero every time n can be divided by 13 without a remainder. That is, the if test is true when n equals \(0,13,26,39,{\ldots},780,801\). The associated code is included in the plot_u function, inside the viz function, in the file wave1D_u0.py .
2.3.6 Running a Case
The first demo of our 1D wave equation solver concerns vibrations of a string that is initially deformed to a triangular shape, like when picking a guitar string:
We choose L = 75 cm, \(x_{0}=0.8L\), a = 5 mm, and a time frequency ν = 440 Hz. The relation between the wave speed c and ν is \(c=\nu\lambda\), where λ is the wavelength, taken as 2L because the longest wave on the string forms half a wavelength. There is no external force, so f = 0 (meaning we can neglect gravity), and the string is at rest initially, implying V = 0.
Regarding numerical parameters, we need to specify a \(\Delta t\). Sometimes it is more natural to think of a spatial resolution instead of a time step. A natural semicoarse spatial resolution in the present problem is \(N_{x}=50\). We can then choose the associated \(\Delta t\) (as required by the viz and solver functions) as the stability limit: \(\Delta t=L/(N_{x}c)\). This is the \(\Delta t\) to be specified, but notice that if C < 1, the actual \(\Delta x\) computed in solver gets larger than \(L/N_{x}\): \(\Delta x=c\Delta t/C=L/(N_{x}C)\). (The reason is that we fix \(\Delta t\) and adjust \(\Delta x\), so if C gets smaller, the code implements this effect in terms of a larger \(\Delta x\).)
A function for setting the physical and numerical parameters and calling viz in this application goes as follows:
The associated program has the name wave1D_u0.py . Run the program and watch the movie of the vibrating string ^{Footnote 3}. The string should ideally consist of straight segments, but these are somewhat wavy due to numerical approximation. Run the case with the wave1D_u0.py code and C = 1 to see the exact solution.
2.3.7 Working with a Scaled PDE Model
Depending on the model, it may be a substantial job to establish consistent and relevant physical parameter values for a case. The guitar string example illustrates the point. However, by scaling the mathematical problem we can often reduce the need to estimate physical parameters dramatically. The scaling technique consists of introducing new independent and dependent variables, with the aim that the absolute values of these lie in \([0,1]\). We introduce the dimensionless variables (details are found in Section 3.1.1 in [11])
Here, L is a typical length scale, e.g., the length of the domain, and a is a typical size of u, e.g., determined from the initial condition: \(a=\max_{x}I(x)\).
We get by the chain rule that
Similarly,
Inserting the dimensionless variables in the PDE gives, in case f = 0,
Dropping the bars, we arrive at the scaled PDE
which has no parameter c ^{2} anymore. The initial conditions are scaled as
and
resulting in
In the common case V = 0 we see that there are no physical parameters to be estimated in the PDE model!
If we have a program implemented for the physical wave equation with dimensions, we can obtain the dimensionless, scaled version by setting c = 1. The initial condition of a guitar string, given in (2.33), gets its scaled form by choosing a = 1, L = 1, and \(x_{0}\in[0,1]\). This means that we only need to decide on the x _{0} value as a fraction of unity, because the scaled problem corresponds 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 only nontrivial parameter to estimate in the scaled problem is the final end time of the 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 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 that u behaves as \(\cos(\omega t)\) in time in the original problem with dimensions. The corresponding period is then \(P=2\pi/\omega\), but we need to estimate ω. A typical solution of the wave equation is \(u(x,t)=A\cos(kx)\cos(\omega t)\), where A is an amplitude and k is related to the wave length λ in space: \(\lambda=2\pi/k\). Both λ and A will be given by the initial condition I(x). Inserting this \(u(x,t)\) in the PDE yields \(\omega^{2}=c^{2}k^{2}\), i.e., ω = kc. The period is therefore \(P=2\pi/(kc)\). If the boundary conditions are \(u(0,t)=u(L,t)\), we need to have \(kL=n\pi\) for integer n. The period becomes P = 2L ∕ nc. The longest period is P = 2L ∕ c. The dimensionless period \(\tilde{P}\) is obtained by dividing P by the time scale L ∕ c, which results in \(\tilde{P}=2\). Shorter waves in the initial condition will have a dimensionless shorter period \(\tilde{P}=2/n\) (n > 1).
2.4 Vectorization
The computational algorithm for solving the wave equation visits one mesh point at a time and evaluates a formula for the new value \(u_{i}^{n+1}\) at that point. Technically, this is implemented by a loop over array elements in a program. Such loops may run slowly in Python (and similar interpreted languages such as R and MATLAB). One technique for speeding up loops is to perform operations on entire arrays instead of working with one element at a time. This is referred to as vectorization, vector computing, or array computing. Operations on whole arrays are possible if the computations involving each element is independent of each other and therefore can, at least in principle, be performed simultaneously. That is, vectorization not only speeds up the code on serial computers, but also makes it easy to exploit parallel computing. Actually, there are Python tools like Numba ^{Footnote 4} that can automatically turn vectorized code into parallel code.
2.4.1 Operations on Slices of Arrays
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 \(d_{i}=u_{i+1}u_{i}\):
All the differences here are independent of each other. The computation of d can therefore alternatively be done by subtracting the array \((u_{0},u_{1},\ldots,u_{n1})\) from the array where the elements are shifted one index upwards: \((u_{1},u_{2},\ldots,u_{n})\), see Fig. 2.3. The former subset of the array can be expressed by u[0:n1], u[0:1], or just u[:1], meaning from index 0 up to, but not including, the last element (1). The latter subset is obtained by u[1:n] or u[1:], meaning from index 1 and the rest of the array. The computation of d can now be done without an explicit Python loop:
or with explicit limits if desired:
Indices with a colon, going from an index to (but not including) another index are called slices. 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 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.
Test your understanding
Newcomers to vectorization are encouraged to choose a small array u, say with five elements, and simulate with pen and paper both the loop version and the vectorized version above.
Finite difference schemes basically contain differences between array elements with shifted indices. As an example, consider the updating formula
The vectorization consists of replacing the loop by arithmetics on slices of arrays of length n2:
Note that the length of u2 becomes n2. If u2 is already an array of length n and we want to use the formula to update all the ‘‘inner’’ elements of u2, as we will when solving a 1D wave equation, we can write
The first expression’s righthand 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 numpy package performs (behind the scenes) the first line above in four steps:
We need three temporary arrays, but a user does not need to worry about such temporary arrays.
Common mistakes with array slices
Array expressions with slices demand that the slices have the same shape. It easy to make a mistake in, e.g.,
and write
Now u[1:n] has wrong length (n1) compared to the other array slices, causing a ValueError 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 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, when u2 has length n, is to forget the slice in the array on the lefthand side,
This is really crucial: now u2 becomes a new array of length n2, which is the wrong length as we have no entries for the boundary values. We meant to insert the righthand side array into the original u2 array for the entries that correspond to the internal points in the mesh (1:n1 or 1:1).
Vectorization may also work nicely with functions. To illustrate, we may extend the previous example as follows:
Assuming u2, u, and x all have length n, the vectorized version becomes
Obviously, f must be able to take an array as argument for f(x[1:1]) to make sense.
2.4.2 Finite Difference Schemes Expressed as Slices
We now have the necessary tools to vectorize the wave equation algorithm as described mathematically in Sect. 2.1.5 and through code in Sect. 2.3.2. There are three loops: one for the initial condition, one for the first time step, and finally the loop that is repeated for all subsequent time levels. Since only the latter is repeated a potentially large number of times, we limit our vectorization efforts to this loop. Within the time loop, the space loop reads:
The vectorized version becomes
or
The program wave1D_u0v.py 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 vectorized, respectively).
2.4.3 Verification
We may reuse the quadratic solution \(u_{\mbox{\footnotesize e}}(x,t)=x(Lx)(1+{\frac{1}{2}}t)\) for verifying also the vectorized code. A test function can now verify both the scalar and the vectorized version. Moreover, we may use a user_action function that compares the computed and exact solution at each time level and performs a test:
Lambda functions
The code segment above demonstrates how to achieve very compact code, without degraded readability, by use of lambda functions for the various input parameters that require a Python function. In essence,
is equivalent to
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:
2.4.4 Efficiency Measurements
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 Sect. 2.3.5. 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 measurements.
One solution is to copy the viz code from wave1D_u0 into wave1D_u0v.py and add a version argument to the solver_function call. Taking into account how much animation code we then duplicate, this is not a good idea. Alternatively, introducing the version argument in wave1D_u0.viz, so that this function can be imported into wave1D_u0v.py, is not a good solution either, since version has no meaning in that file. We need better ideas!
Solution 1
Calling viz in wave1D_u0 with solver_function as our new solver in wave1D_u0v works fine, since this solver has version=’vectorized’ as default value. The problem arises when we want to test version=’scalar’. The simplest solution is then to use wave1D_u0.solver instead. We make a new viz function in wave1D_u0v.py that has a version argument and that just calls wave1D_u0.viz:
Solution 2
There is a more advanced and fancier solution featuring a very useful trick: we can make a new function that will always call wave1D_u0v.solver with version=’scalar’. The functools.partial function from standard Python takes a function func as argument and a series of positional and keyword arguments and returns a new function that will call func with the supplied arguments, while the user can control all the other arguments in func. Consider a trivial example,
We want to ensure that f is always called with c=3, i.e., f has only two ‘‘free’’ arguments a and b. This functionality is obtained by
Now f2 calls f with whatever the user supplies as a and b, but c is always 3.
Back to our viz code, we can do
The new scalar_solver takes the same arguments as wave1D_u0.scalar and calls wave1D_u0v.scalar, 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 we supply, plus version=’scalar’.
Efficiency experiments
We now have a viz function that can call our solver function both in scalar and vectorized mode. The function run_efficiency_ experiments in wave1D_u0v.py performs a set of experiments and reports the CPU time spent in the scalar and vectorized solver for the previous string vibration example with spatial mesh resolutions \(N_{x}=50,100,200,400,800\). Running this function reveals that the vectorized code runs substantially faster: the vectorized code runs approximately \(N_{x}/10\) times as fast as the scalar code!
2.4.5 Remark on the Updating of Arrays
At the end of each time step we need to update the u_nm1 and u_n arrays such that they have the right content for the next time step:
The order here is important: updating u_n first, makes u_nm1 equal to u, which is wrong!
The assignment u_n[:] = u copies the content of the u array into the elements of the u_n array. 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 more efficient way of making the arrays u_nm1 and u_n ready for the next time step. The idea is based on switching references and explained as follows.
A Python variable is actually a reference to some object (C programmers may think of pointers). Instead of copying data, we can let u_nm1 refer to the u_n object and u_n refer to the u object. This is a very efficient operation (like switching pointers in C). A naive implementation like
will fail, however, because now u_nm1 refers to the u_n object, but then the name u_n refers to u, so that this u object has two references, u_n and u, while our third array, originally referred to by u_nm1, has no more references and is lost. This means that the variables u, u_n, and u_nm1 refer to 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_n too, thereby destroying the solution at the previous time step.
While u_nm1 = u_n is fine, u_n = u is problematic, so the solution to this problem is to ensure that u points to the u_nm1 array. This is mathematically wrong, but new correct values will be filled into u at the next time step and make it right.
The correct switch of references is
We can get rid of the temporary reference tmp by writing
This switching of references for updating our arrays will be used in later implementations.
Caution
The update u_nm1, u_n, u = u_n, u, u_nm1 leaves wrong content in u at the final time step. This means that if we return u, as we do in the example codes here, we actually return u_nm1, which is obviously wrong. It is therefore important to adjust the content of u to u = u_n before returning u. (Note that the user_action function reduces the need to return the solution from the solver.)
2.5 Exercises
Exercise 2.1 (Simulate a standing wave)
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
where m is an integer and A is a freely chosen amplitude. The corresponding exact solution can be computed and reads

a)
Explain that for a function \(\sin kx\cos\omega t\) the wave length in space is \(\lambda=2\pi/k\) and the period in time is \(P=2\pi/\omega\). Use these expressions to find the wave length in space and period in time of \(u_{\mbox{\footnotesize e}}\) above.

b)
Import the solver function from wave1D_u0.py into a new file where the viz function is reimplemented such that it plots either the numerical and the exact solution, or the error.

c)
Make animations where you illustrate how the error \(e^{n}_{i}=u_{\mbox{\footnotesize e}}(x_{i},t_{n})u^{n}_{i}\) develops and increases in time. Also make animations of u and \(u_{\mbox{\footnotesize e}}\) simultaneously.
Hint 1
Quite long time simulations are needed in order to display significant discrepancies between the numerical and exact solution.
Hint 2
A possible set of parameters is L = 12, m = 9, c = 2, A = 1, \(N_{x}=80\), C = 0.8. The error mesh function e ^{n} can be simulated for 10 periods, while 20–30 periods are needed to show significant differences between the curves for the numerical and exact solution.
Filename: wave_standing.
Remarks
The important parameters for numerical quality are C and \(k\Delta x\), where \(C=c\Delta t/\Delta x\) is the Courant number and k is defined above (\(k\Delta x\) is proportional to how many mesh points we have per wave length in space, see Sect. 2.10.4 for explanation).
Exercise 2.2 (Add storage of solution in a user action function)
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 an append operation inside the plot_u function. Note that a function, like plot_u, inside another function, like viz, remembers all local variables in viz function, including all_u, even 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 correctly? Let the viz function return the all_u list converted to a twodimensional numpy array.
Filename: wave1D_u0_s_store.
Exercise 2.3 (Use a class for the user action function)
Redo Exercise 2.2 using a class for the user action function. Let the all_u list be an attribute in this class and implement the user action function as a method (the special method __call__ is a natural choice). The class versions avoid that the user action function depends on parameters defined outside the function (such as all_u in Exercise 2.2).
Filename: wave1D_u0_s2c.
Exercise 2.4 (Compare several Courant numbers in one movie)
The goal of this exercise is to make movies where several curves, corresponding to different Courant numbers, are visualized. Write a program that resembles wave1D_u0_s2c.py in Exercise 2.3, but with a viz function that can take a list of C values 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.2 or 2.3 for details), solver must be computed for each value of the Courant number, and finally one must run through each time step and plot all the spatial solution curves in one figure and store it in a file.
The challenge in such a visualization is to ensure that the curves in one plot correspond to the same time point. The easiest remedy is to keep the time resolution constant and change the space resolution to change the Courant number. Note that each spatial grid is needed for the final plotting, so it is an option to store those grids too.
Filename: wave_numerics_comparison.
Exercise 2.5 (Implementing the solver function as a generator)
The callback function user_action(u, x, t, n) is called from the solver function (in, e.g., wave1D_u0.py) at every time level and lets the user work perform desired actions with the solution, like plotting it on the screen. We have implemented the callback function in the typical way it would have been done in C and Fortran. Specifically, the code looks like
Many Python programmers, however, may claim that solver is an iterative process, and that iterative processes with callbacks to the user code is more elegantly implemented as generators. The rest of the text has little meaning unless you are familiar with Python generators and the yield statement.
Instead of calling user_action, the solver function issues a yield statement, which is a kind of return statement:
The program control is directed back to the calling code:
When the block is done, solver continues with the statement after yield. Note that the functionality of terminating the solution process if user_action returns a True value is not possible to implement in the generator case.
Implement the solver function as a generator, and plot the solution at each time step.
Filename: wave1D_u0_generator.
Project 2.6 (Calculus with 1D mesh functions)
This project explores integration and differentiation of mesh functions, both with scalar and vectorized implementations. We are given a mesh function f _{ i } on a spatial onedimensional mesh \(x_{i}=i\Delta x\), \(i=0,\ldots,N_{x}\), over the interval \([a,b]\).

a)
Define the discrete derivative of f _{ i } by using centered differences at internal mesh points and onesided 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)=4x2.5\) where the discrete derivative should be exact.

b)
Vectorize the implementation of the discrete derivative. Extend the unit test to check the validity of the implementation.

c)
To compute the discrete integral F _{ i } of f _{ i }, we assume that the mesh function f _{ i } varies linearly between the mesh points. Let f(x) be such a linear interpolant of f _{ i }. We then have
$$F_{i}=\int_{x_{0}}^{x_{i}}f(x)dx\thinspace.$$The exact integral of a piecewise linear function f(x) is given by the Trapezoidal rule. Show that if F _{ i } is already computed, we can find \(F_{i+1}\) from
$$F_{i+1}=F_{i}+\frac{1}{2}(f_{i}+f_{i+1})\Delta x\thinspace.$$Make a function for the scalar implementation of the discrete integral as a mesh function. That is, the function should return F _{ i } for \(i=0,\ldots,N_{x}\). For a unit test one can use the fact that the above defined discrete integral of a linear function (say \(f(x)=4x2.5\)) is exact.

d)
Vectorize the implementation of the discrete integral. Extend the unit test to check the validity of the implementation.
Hint
Interpret the recursive formula for \(F_{i+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 can just define some methods that call the previously implemented Python functions. Here is an example on the usage:
Filename: mesh_calculus_1D.
2.6 Generalization: Reflecting Boundaries
The boundary condition u = 0 in a wave equation reflects the wave, but u changes sign at the boundary, while the condition \(u_{x}=0\) reflects the wave as a mirror and preserves the sign, see a web page ^{Footnote 5} or a movie file ^{Footnote 6} for demonstration.
Our next task is to explain how to implement the boundary condition \(u_{x}=0\), which is more complicated to express numerically and also to implement than a given value of u. We shall present two methods for implementing \(u_{x}=0\) in a finite difference scheme, one based on deriving a modified stencil at the boundary, and another one based on extending the mesh with ghost cells and ghost points.
2.6.1 Neumann Boundary Condition
When a wave hits a boundary and is to be reflected back, one applies the condition
The derivative \(\partial/\partial n\) is in the outward normal direction from a general boundary. For a 1D domain \([0,L]\), we have that
Boundary condition terminology
Boundary conditions that specify the value of \(\partial u/\partial n\) (or shorter u _{ n }) are known as Neumann ^{Footnote 7} conditions, while Dirichlet conditions ^{Footnote 8} refer to specifications of u. When the values are zero (\(\partial u/\partial n=0\) or u = 0) we speak about homogeneous Neumann or Dirichlet conditions.
2.6.2 Discretization of Derivatives at the Boundary
How can we incorporate the condition (2.35) in the finite difference scheme? Since we have used central differences in all the other approximations to derivatives in the scheme, it is tempting to implement (2.35) at x = 0 and \(t=t_{n}\) by the difference
The problem is that \(u_{1}^{n}\) is not a u value that is being computed since the point is outside the mesh. However, if we combine (2.36) with the scheme
for i = 0, we can eliminate the fictitious value \(u_{1}^{n}\). We see that \(u_{1}^{n}=u_{1}^{n}\) from (2.36), which can be used in (2.37) to arrive at a modified scheme for the boundary point \(u_{0}^{n+1}\):
Figure 2.4 visualizes this equation for computing \(u^{3}_{0}\) in terms of \(u^{2}_{0}\), \(u^{1}_{0}\), and \(u^{2}_{1}\).
Similarly, (2.35) applied at x = L is discretized by a central difference
Combined with the scheme for \(i=N_{x}\) we get a modified scheme for the boundary value \(u_{N_{x}}^{n+1}\):
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 be illustrated by an animation in a web page ^{Footnote 9} or a movie file ^{Footnote 10}.
2.6.3 Implementation of Neumann Conditions
We have seen in the preceding section that the special formulas for the boundary points arise from replacing \(u_{i1}^{n}\) by \(u_{i+1}^{n}\) when computing \(u_{i}^{n+1}\) from the stencil formula for i = 0. Similarly, we replace \(u_{i+1}^{n}\) by \(u_{i1}^{n}\) in the stencil formula for \(i=N_{x}\). 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[i1] by u[i+1] and vice versa. This is achieved by having the indices i+1 and i1 as variables ip1 (i plus 1) and im1 (i minus 1), respectively. At the boundary we can easily define im1=i+1 while we use im1=i1 in the internal parts of the mesh. Here are the details of the implementation (note that the updating formula for u[i] is the general stencil formula):
We can in fact create one loop over both the internal and boundary points and use only one updating formula:
The program wave1D_n0.py contains a complete implementation of the 1D wave equation with boundary conditions \(u_{x}=0\) at x = 0 and x = L.
It would be nice to modify the test_quadratic test case from the wave1D_u0.py with Dirichlet conditions, described in Sect. 2.4.3. However, the Neumann conditions require the polynomial variation in the x direction to be of third degree, which causes challenging problems when designing a test where the numerical solution is known exactly. Exercise 2.15 outlines ideas and code for this purpose. The only test in wave1D_n0.py is to start with a plug wave at rest and 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 Sect. 2.10.4).
2.6.4 Index Set Notation
To improve our mathematical writing and our implementations, it is wise to introduce a special notation for index sets. This means that we write x _{ i }, followed by \(i\in\mathcal{I}_{x}\), instead of \(i=0,\ldots,N_{x}\). Obviously, \(\mathcal{I}_{x}\) must be the index set \(\mathcal{I}_{x}=\{0,\ldots,N_{x}\}\), 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 new notation saves writing and makes specifications of algorithms and their implementation as computer code simpler.
The first index in the set will be denoted \(\mathcal{I}_{x}^{0}\) and the last \(\mathcal{I}_{x}^{1}\). When we need to skip the first element of the set, we use \(\mathcal{I}_{x}^{+}\) for the remaining subset \(\mathcal{I}_{x}^{+}=\{1,\ldots,N_{x}\}\). Similarly, if the last element is to be dropped, we write \(\mathcal{I}_{x}^{}=\{0,\ldots,N_{x}1\}\) for the remaining indices. All the indices corresponding to inner grid points are specified by \(\mathcal{I}_{x}^{i}=\{1,\ldots,N_{x}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 t _{0} rather than \(n=\mathcal{I}_{t}^{0}\). We also avoid notation like \(x_{\mathcal{I}_{x}^{1}}\) and will instead use x _{ i }, \(i=\mathcal{I}_{x}^{1}\).
The Python code associated with index sets applies the following conventions:
Notation  Python 

\(\mathcal{I}_{x}\)  Ix 
\(\mathcal{I}_{x}^{0}\)  Ix[0] 
\(\mathcal{I}_{x}^{1}\)  Ix[1] 
\(\mathcal{I}_{x}^{}\)  Ix[:1] 
\(\mathcal{I}_{x}^{+}\)  Ix[1:] 
\(\mathcal{I}_{x}^{i}\)  Ix[1:1] 
Why index sets are useful
An important feature of the index set notation is that it keeps our formulas and code independent of how we count mesh points. For example, the notation \(i\in\mathcal{I}_{x}\) or \(i=\mathcal{I}_{x}^{0}\) remains the same whether \(\mathcal{I}_{x}\) is defined as above or as starting at 1, i.e., \(\mathcal{I}_{x}=\{1,\ldots,Q\}\). Similarly, we can in the code define Ix=range(Nx+1) or Ix=range(1,Q), and expressions 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 and C) to languages where the base index is 1 (e.g., MATLAB and Fortran). Another important application is implementation of Neumann conditions via ghost points (see next section).
For the current problem setting in the x,t plane, we work with the index sets
defined in Python as
A finite difference scheme can with the index set notation be specified as
The corresponding implementation becomes
Notice
The program wave1D_dn.py applies the index set notation and solves the 1D wave equation \(u_{tt}=c^{2}u_{xx}+f(x,t)\) with quite general boundary and initial conditions:

x = 0: \(u=U_{0}(t)\) or \(u_{x}=0\)

x = L: \(u=U_{L}(t)\) or \(u_{x}=0\)

t = 0: \(u=I(x)\)

t = 0: \(u_{t}=V(x)\)
The program combines Dirichlet and Neumann conditions, scalar and vectorized implementation of schemes, and the index set notation into one piece of code. A lot of test examples are also included in the program:

A rectangular plugshaped initial condition. (For C = 1 the solution will be a rectangle that jumps one cell per time step, making the case well suited for verification.)

A Gaussian function as initial condition.

A triangular profile as initial condition, which resembles the typical initial shape of a guitar string.

A sinusoidal variation of u at x = 0 and either u = 0 or \(u_{x}=0\) at x = L.

An analytical solution \(u(x,t)=\cos(m\pi t/L)\sin({\frac{1}{2}}m\pi x/L)\), which can be used for convergence rate tests.
2.6.5 Verifying the Implementation of Neumann Conditions
How can we test that the Neumann conditions are correctly implemented? The solver function in the wave1D_dn.py program described in the box above accepts Dirichlet or Neumann conditions at x = 0 and x = L. It is tempting to apply a quadratic solution as described in Sect. 2.2.1 and 2.3.3, but it turns out that this solution is no longer an exact solution of the discrete equations if a Neumann condition is implemented on the boundary. A linear solution does not help since we only have homogeneous Neumann conditions in wave1D_dn.py, and we are consequently left with testing just a constant solution: \(u=\hbox{const}\).
The quadratic solution is very useful for testing, but it requires Dirichlet conditions at both ends.
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 split into two plug waves, one in each direction, and check that the two plug waves come back and form the initial condition again after ‘‘one period’’ of the solution process. Neumann conditions can be applied at both ends. A proper test function reads
Other tests must rely on an unknown approximation error, so effectively we are left with tests on the convergence rate.
2.6.6 Alternative Implementation via Ghost Cells
Idea
Instead of modifying the scheme at the boundary, we can introduce extra points outside the domain such that the fictitious values \(u_{1}^{n}\) and \(u_{N_{x}+1}^{n}\) are defined in the mesh. Adding the intervals \([\Delta x,0]\) and \([L,L+\Delta x]\), known as ghost cells, to the mesh gives us all the needed mesh points, corresponding to \(i=1,0,\ldots,N_{x},N_{x}+1\). The extra points with i = −1 and \(i=N_{x}+1\) are known as ghost points, and values at these points, \(u_{1}^{n}\) and \(u_{N_{x}+1}^{n}\), are called ghost values.
The important idea is to ensure that we always have
because then the application of the standard scheme at a boundary point i = 0 or \(i=N_{x}\) will be correct and guarantee that the solution is compatible with the boundary condition \(u_{x}=0\).
Some readers may find it strange to just extend the domain with ghost cells as a general technique, because in some problems there is a completely different medium with different physics and equations right outside of a boundary. Nevertheless, one should view the ghost cell technique as a purely mathematical technique, which is valid in the limit \(\Delta x\rightarrow 0\) and helps us to implement derivatives.
Implementation
The u array now needs extra elements corresponding to the ghost points. Two new point values are needed:
The arrays u_n and u_nm1 must be defined accordingly.
Unfortunately, a major indexing problem arises with ghost cells. The reason is that Python indices must start at 0 and u[1] will always mean the last element in u. This fact gives, apparently, a mismatch between the mathematical indices \(i=1,0,\ldots,N_{x}+1\) and the Python indices running over u: 0,..,Nx+2. One remedy is to change the mathematical indexing of i in the scheme and write
instead of \(i=0,\ldots,N_{x}\) as we have previously used. The ghost points now correspond to i = 0 and \(i=N_{x}+1\). A better solution is to use the ideas of Sect. 2.6.4: we hide the specific index value in an index set and operate with inner and boundary points using the index set notation.
To this end, we define u with proper length and Ix to be the corresponding indices for the real physical mesh points (\(1,2,\ldots,N_{x}+1\)):
That is, the boundary points have indices Ix[0] and Ix[1] (as before). We first update the solution at all physical mesh points (i.e., interior points in the mesh):
The indexing becomes a bit more complicated when we call functions like V(x) and f(x, t), as we must remember that the appropriate x coordinate is given as x[iIx[0]]:
It remains to update the solution at ghost points, i.e., u[0] and u[1] (or u[Nx+2]). For a boundary condition \(u_{x}=0\), the ghost value must equal the value at the associated inner mesh point. Computer code makes this statement precise:
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 appears in the program wave1D_n0_ghost.py .
Warning
We have to be careful with how the spatial and temporal mesh points are stored. Say we let x be the physical mesh points,
‘‘Standard coding’’ of the initial condition,
becomes wrong, since u_n and x have different lengths and the index i corresponds to two different mesh points. In fact, x[i] corresponds to u[1+i]. A correct implementation is
Similarly, a source term usually coded as f(x[i], t[n]) is incorrect if x is defined to be the physical points, so x[i] must be replaced by x[iIx[0]].
An alternative remedy is to let x also cover the ghost points such that u[i] is the value at x[i].
The ghost cell is only added to the boundary where we have a Neumann condition. Suppose we have a Dirichlet condition at x = L and a homogeneous Neumann condition at x = 0. One ghost cell \([\Delta x,0]\) is added to the mesh, so the index set for the physical points becomes \(\{1,\ldots,N_{x}+1\}\). A relevant implementation is
The physical solution to be plotted is now in u[1:] or (as always) u[Ix[0]: Ix[1]+1].
2.7 Generalization: Variable Wave Velocity
Our next generalization of the 1D wave equation (2.1) or (2.17) is to allow for a variable wave velocity c: \(c=c(x)\), usually motivated by wave motion in a domain composed of different physical media. When the media differ in physical properties like density or porosity, the wave velocity c is affected and will depend on the position in space. Figure 2.5 shows a wave propagating in one medium \([0,0.7]\cup[0.9,1]\) with wave velocity c _{1} (left) before it enters a second medium \((0.7,0.9)\) with wave velocity c _{2} (right). When the wave meets the boundary where c jumps from c _{1} to c _{2}, a part of the wave is reflected back into the first medium (the reflected wave), while one part is transmitted through the second medium (the transmitted wave).
2.7.1 The Model PDE with a Variable Coefficient
Instead of working with the squared quantity \(c^{2}(x)\), we shall for notational convenience introduce \(q(x)=c^{2}(x)\). A 1D wave equation with variable wave velocity often takes the form
This is the most frequent form of a wave equation with variable wave velocity, but other forms also appear, see Sect. 2.14.1 and equation (2.125).
As usual, we sample (2.42) at a mesh point,
where the only new term to discretize is
2.7.2 Discretizing the Variable Coefficient
The principal idea is to first discretize the outer derivative. Define
and use a centered derivative around \(x=x_{i}\) for the derivative of ϕ:
Then discretize
Similarly,
These intermediate results are now combined to
With operator notation we can write the discretization as
Do not use the chain rule on the spatial derivative term!
Many are tempted to use the chain rule on the term \(\frac{\partial}{\partial x}\left(q(x)\frac{\partial u}{\partial x}\right)\), but this is not a good idea when discretizing such a term.
The term with a variable coefficient expresses the net flux qu _{ x } into a small volume (i.e., interval in 1D):
Our discretization reflects this principle directly: qu _{ x } at the right end of the cell minus qu _{ x } at the left end, because this follows from the formula (2.43) or \([D_{x}(qD_{x}u)]^{n}_{i}\).
When using the chain rule, we get two terms \(qu_{xx}+q_{x}u_{x}\). The typical discretization is
Writing this out shows that it is different from \([D_{x}(qD_{x}u)]^{n}_{i}\) and lacks the physical interpretation of net flux into a cell. With a smooth and slowly varying q(x) the differences between the two discretizations are not substantial. However, when q exhibits (potentially large) jumps, \([D_{x}(qD_{x}u)]^{n}_{i}\) with harmonic averaging of q yields a better solution than arithmetic averaging or (2.45). In the literature, the discretization \([D_{x}(qD_{x}u)]^{n}_{i}\) totally dominates and very few mention the alternative in (2.45).
2.7.3 Computing the Coefficient Between Mesh Points
If q is a known function of x, we can easily evaluate \(q_{i+\frac{1}{2}}\) simply as \(q(x_{i+\frac{1}{2}})\) with \(x_{i+\frac{1}{2}}=x_{i}+\frac{1}{2}\Delta x\). However, in many cases c, and hence q, is only known as a discrete function, often at the mesh points x _{ i }. Evaluating q between two mesh points x _{ i } and \(x_{i+1}\) must then be done by interpolation techniques, of which three are of particular interest in this context:
The arithmetic mean in (2.46) is by far the most commonly used averaging technique and is well suited for smooth q(x) functions. The harmonic mean is often preferred when q(x) exhibits large jumps (which is typical for geological media). The geometric mean is less used, but popular in discretizations to linearize quadratic nonlinearities (see Sect. 1.10.2 for an example).
With the operator notation from (2.46) we can specify the discretization of the complete variablecoefficient wave equation in a compact way:
Strictly speaking, \([D_{x}\overline{q}^{x}D_{x}u]^{n}_{i}=[D_{x}(\overline{q}^{x}D_{x}u)]^{n}_{i}\).
From the compact difference notation we immediately see what kind of differences that each term is approximated with. The notation \(\overline{q}^{x}\) also specifies that the variable coefficient is approximated by an arithmetic mean, the definition being \([\overline{q}^{x}]_{i+\frac{1}{2}}=(q_{i}+q_{i+1})/2\).
Before implementing, it remains to solve (2.49) with respect to \(u_{i}^{n+1}\):
2.7.4 How a Variable Coefficient Affects the Stability
The stability criterion derived later (Sect. 2.10.3) reads \(\Delta t\leq\Delta x/c\). If \(c=c(x)\), the criterion will depend on the spatial location. We must therefore choose a \(\Delta t\) that is small enough such that no mesh cell has \(\Delta t> \Delta x/c(x)\). That is, we must use the largest c value in the criterion:
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 allround value).
A different strategy to handle the stability criterion with variable wave velocity is to use a spatially varying \(\Delta t\). While the idea is mathematically attractive at first sight, the implementation quickly becomes very complicated, so we stick to a constant \(\Delta t\) and a worst case value of c(x) (with a safety factor β).
2.7.5 Neumann Condition and a Variable Coefficient
Consider a Neumann condition \(\partial u/\partial x=0\) at \(x=L=N_{x}\Delta x\), discretized as
for \(i=N_{x}\). Using the scheme (2.50) at the end point \(i=N_{x}\) with \(u_{i+1}^{n}=u_{i1}^{n}\) results in
Here we used the approximation
An alternative derivation may apply the arithmetic mean of \(q_{n\frac{1}{2}}\) and \(q_{n+\frac{1}{2}}\) in (2.53), leading to the term
Since \(\frac{1}{2}(q_{i+1}+q_{i1})=q_{i}+{\cal O}(\Delta x^{2})\), we can approximate with \(2q_{i}(u_{i1}^{n}u_{i}^{n})\) for \(i=N_{x}\) and get the same term as we did above.
A common technique when implementing \(\partial u/\partial x=0\) boundary conditions, is to assume dq ∕ dx = 0 as well. This implies \(q_{i+1}=q_{i1}\) and \(q_{i+1/2}=q_{i1/2}\) for \(i=N_{x}\). The implications for the scheme are
2.7.6 Implementation of Variable Coefficients
The implementation of the scheme with a variable wave velocity \(q(x)=c^{2}(x)\) may assume that q is available as an array q[i] at the spatial mesh points. The following loop is a straightforward implementation of the scheme (2.50):
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.
With Neumann conditions \(u_{x}=0\) at the boundary, we need to combine this scheme with the discrete version of the boundary condition, as shown in Sect. 2.7.5. Nevertheless, it would be convenient to reuse the formula for the interior points and just modify the indices ip1=i+1 and im1=i1 as we did in Sect. 2.6.3. Assuming dq ∕ dx = 0 at the boundaries, we can implement the scheme at the boundary with the following code.
With ghost cells we can just reuse the formula for the interior points also at the boundary, provided that the ghost values of both u and q are correctly updated to ensure \(u_{x}=0\) and \(q_{x}=0\).
A vectorized version of the scheme with a variable coefficient at internal mesh points becomes
2.7.7 A More General PDE Model with Variable Coefficients
Sometimes a wave PDE has a variable coefficient in front of the timederivative term:
One example appears when modeling elastic waves in a rod with varying density, cf. (2.14.1) with \(\varrho(x)\).
A natural scheme for (2.58) is
We realize that the \(\varrho\) coefficient poses no particular difficulty, since \(\varrho\) enters the formula just as a simple factor in front of a derivative. There is hence no need for any averaging of \(\varrho\). Often, \(\varrho\) will be moved to the righthand side, also without any difficulty:
2.7.8 Generalization: Damping
Waves die out by two mechanisms. In 2D and 3D the energy of the wave spreads out in space, and energy conservation then requires the amplitude to decrease. This effect is not present in 1D. Damping is another cause of amplitude reduction. For example, the vibrations of a string die out because of damping due to air resistance and nonelastic effects in the string.
The simplest way of including damping is to add a firstorder derivative to the equation (in the same way as friction forces enter a vibrating mechanical system):
where b ≥ 0 is a prescribed damping coefficient.
A typical discretization of (2.61) in terms of centered differences reads
Writing out the equation and solving for the unknown \(u^{n+1}_{i}\) gives the scheme
for \(i\in\mathcal{I}_{x}^{i}\) and n ≥ 1. New equations must be derived for \(u^{1}_{i}\), and for boundary points in case of Neumann conditions.
The damping is very small in many wave phenomena and thus only evident for very long time simulations. This makes the standard wave equation without damping relevant for a lot of applications.
2.8 Building a General 1D Wave Equation Solver
The program wave1D_dn_vc.py is a fairly general code for 1D wave propagation problems that targets the following initialboundary value problem
The only new feature here is the timedependent Dirichlet conditions, but they are trivial to implement:
The solver function is a natural extension of the simplest solver function in the initial wave1D_u0.py program, extended with Neumann boundary conditions (\(u_{x}=0\)), timevarying Dirichlet conditions, as well as a variable wave velocity. The different code segments needed to make these extensions have been shown and commented upon in the preceding text. We refer to the solver function in the wave1D_dn_vc.py file for all the details. Note in that solver function, however, that the technique of ‘‘hashing’’ is used to check whether a certain simulation has been run before, or not. This technique is further explained in Sect. C.2.3.
The vectorization is only applied inside the time loop, not for the initial condition or the first time steps, since this initial work is negligible for long time simulations in 1D problems.
The following sections explain various more advanced programming techniques applied in the general 1D wave equation solver.
2.8.1 User Action Function as a Class
A useful feature in the wave1D_dn_vc.py program is the specification of the user_action function as a class. This part of the program may need some motivation and explanation. Although the plot_u_st function (and the PlotMatplotlib class) in the wave1D_u0.viz function remembers the local variables in the viz function, it is a cleaner solution to store the needed variables together with the function, which is exactly what a class offers.
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:
Dissection
Understanding this class requires quite some familiarity with Python in general and class programming in particular. The class supports plotting with Matplotlib (backend=None) or SciTools (backend=matplotlib or backend= gnuplot) for maximum flexibility.
The constructor shows how we can flexibly import the plotting engine as (typically) scitools.easyviz.gnuplot_ or scitools.easyviz.matplotlib_ (note the trailing underscore  it is required). With the screen_movie parameter we can suppress displaying each movie frame on the screen. Alternatively, for slow movies associated with fine meshes, one can set skip_frame=10, causing every 10 frames to be shown.
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 call to user_action will be a call to p.__call__. The __call__ method plots the solution on the screen, saves the plot to file, and stores the solution in a file for later retrieval.
More details on storing the solution in files appear in Sect. C.2.
2.8.2 Pulse Propagation in Two Media
The function pulse in wave1D_dn_vc.py demonstrates wave motion in heterogeneous media where c varies. One can specify an interval where the wave velocity is decreased by a factor slowness_factor (or increased by making this factor less than one). Figure 2.5 shows a typical simulation scenario.
Four types of initial conditions are available:

1.
a rectangular pulse (plug),

2.
a Gaussian function (gaussian),

3.
a ‘‘cosine hat’’ consisting of one period of the cosine function (cosinehat),

4.
half a period of a ‘‘cosine hat’’ (halfcosinehat)
These peakshaped initial conditions can be placed in the middle (loc=’center’) or at the left end (loc=’left’) of the domain. With the pulse in the middle, it splits in two parts, each with half the initial amplitude, traveling in opposite directions. With the pulse at the left end, centered at x = 0, and using the symmetry condition \(\partial u/\partial x=0\), only a rightgoing pulse is generated. There is also a leftgoing pulse, but it travels from x = 0 in negative x direction and is not visible in the domain \([0,L]\).
The pulse function is a flexible tool for playing around with various wave shapes and jumps in the wave velocity (i.e., discontinuous media). The code is shown to demonstrate how easy it is to reach this flexibility with the building blocks we have already developed:
The PlotMediumAndSolution class used here is a subclass of PlotAndStore Solution where the medium with reduced c value, as specified by the medium interval, is visualized in the plots.
Comment on the choices of discretization parameters
The argument N _{ x } in the pulse function does not correspond to the actual spatial resolution of C < 1, since the solver function takes a fixed \(\Delta t\) and C, and adjusts \(\Delta x\) accordingly. As seen in the pulse function, the specified \(\Delta t\) is chosen according to the limit C = 1, so if C < 1, \(\Delta t\) remains the same, but the solver function operates with a larger \(\Delta x\) and smaller N _{ x } than was specified in the call to pulse. The practical reason is that we always want to keep \(\Delta t\) fixed such that plot frames and movies are synchronized in time regardless of the value of C (i.e., \(\Delta x\) is varied when the Courant number varies).
The reader is encouraged to play around with the pulse function:
To easily kill the graphics by CtrlC and restart a new simulation it might be easier to run the above two statements from the command line with
2.9 Exercises
Exercise 2.7 (Find the analytical solution to a damped wave equation)
Consider the wave equation with damping (2.61). The goal is to find an exact solution to a wave problem with damping and zero source term. A starting point is the standing wave solution from Exercise 2.1. It becomes necessary to include a damping term \(e^{\beta t}\) and also have both a sine and cosine component in time:
Find k from the boundary conditions \(u(0,t)=u(L,t)=0\). Then use the PDE to find constraints on β, ω, A, and B. Set up a complete initialboundary value problem and its solution.
Filename: damped_waves.
Problem 2.8 (Explore symmetry boundary conditions)
Consider the simple ″plug″ wave where \(\Omega=[L,L]\) and
for some number 0 < δ < L. The other initial condition is \(u_{t}(x,0)=0\) and there is no source term f. The boundary conditions can be set to u = 0. The solution to this problem is symmetric around x = 0. This means that we can simulate the wave process in only half of the domain \([0,L]\).

a)
Argue why the symmetry boundary condition is \(u_{x}=0\) at x = 0.
Hint
Symmetry of a function about \(x=x_{0}\) means that \(f(x_{0}+h)=f(x_{0}h)\).

a)
Perform simulations of the complete wave problem on \([L,L]\). Thereafter, utilize the symmetry of the solution and run a simulation in half of the domain \([0,L]\), using a boundary condition at x = 0. Compare plots from the two solutions and confirm that they are the same.

b)
Prove the symmetry property of the solution by setting up the complete initialboundary value problem and showing that if \(u(x,t)\) is a solution, then also \(u(x,t)\) is a solution.

c)
If the code works correctly, the solution \(u(x,t)=x(Lx)(1+\frac{t}{2})\) should be reproduced exactly. Write a test function test_quadratic that checks whether this is the case. Simulate for x in \([0,\frac{L}{2}]\) with a symmetry condition at the end \(x=\frac{L}{2}\).
Filename: wave1D_symmetric.
Exercise 2.9 (Send pulse waves through a layered medium)
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 medium is 1 while it is \(\frac{1}{s_{f}}\) in the right medium. Report what happens with a Gaussian pulse, a ‘‘cosine hat’’ pulse, half a ‘‘cosine hat’’ pulse, and a plug pulse for resolutions \(N_{x}=40,80,160\), and \(s_{f}=2,4\). Simulate until T = 2.
Filename: pulse1D.
Exercise 2.10 (Explain why numerical noise occurs)
The experiments performed in Exercise 2.9 shows considerable numerical noise in the form of nonphysical waves, especially for \(s_{f}=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’’ pulse for \(s_{f}=1\), C = 0.9,0.25, and \(N_{x}=40,80,160\). Use the numerical dispersion relation to explain the observations.
Filename: pulse1D_analysis.
Exercise 2.11 (Investigate harmonic averaging in a 1D model)
Harmonic means are often used if the wave velocity is nonsmooth or discontinuous. Will harmonic averaging of the wave velocity give less numerical noise for the case \(s_{f}=4\) in Exercise 2.9?
Filename: pulse1D_harmonic.
Problem 2.12 (Implement open boundary conditions)
To enable a wave to leave the computational domain and travel undisturbed through the boundary x = L, one can in a onedimensional problem impose the following condition, called a radiation condition or open boundary condition:
The parameter c is the wave velocity.
Show that (2.69) accepts a solution \(u=g_{R}(xct)\) (rightgoing wave), but not \(u=g_{L}(x+ct)\) (leftgoing wave). This means that (2.69) will allow any rightgoing wave \(g_{R}(xct)\) to pass through the boundary undisturbed.
A corresponding open boundary condition for a leftgoing wave through x = 0 is

a)
A natural idea for discretizing the condition (2.69) at the spatial end point \(i=N_{x}\) is to apply centered differences in time and space:
$$[D_{2t}u+cD_{2x}u=0]^{n}_{i},\quad i=N_{x}\thinspace.$$(2.71)Eliminate the fictitious value \(u_{N_{x}+1}^{n}\) by using the discrete equation at the same point.
The equation for the first step, \(u_{i}^{1}\), is in principle also affected, but we can then use the condition \(u_{N_{x}}=0\) since the wave has not yet reached the right boundary.

b)
A much more convenient implementation of the open boundary condition at x = L can be based on an explicit discretization
$$[D^{+}_{t}u+cD_{x}^{}u=0]_{i}^{n},\quad i=N_{x}\thinspace.$$(2.72)From this equation, one can solve for \(u^{n+1}_{N_{x}}\) 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 \(u_{tt}=c^{2}u_{xx}\) in a domain \([0,L]\), where you have \(u_{x}=0\) at x = 0, the condition (2.69) at x = L, and an initial disturbance in the middle of the domain, e.g., a plug profile like
$$u(x,0)=\left\{\begin{array}[]{ll}1,&L/2\ell\leq x\leq L/2+\ell,\\ 0,&\hbox{otherwise}\thinspace.\end{array}\right.$$Observe that the initial wave is split in two, the leftgoing wave is reflected at x = 0, and both waves travel out of x = L, leaving the solution as u = 0 in \([0,L]\). Use a unit Courant number such that the numerical solution is exact. Make a movie to illustrate what happens.
Because this simplified implementation of the open boundary condition works, there is no need to pursue the more complicated discretization in a).
Hint
Modify the solver function in wave1D_dn.py .

a)
Add the possibility to have either \(u_{x}=0\) or an open boundary condition at the left boundary. The latter condition is discretized as
$$[D^{+}_{t}ucD_{x}^{+}u=0]_{i}^{n},\quad i=0,$$(2.73)leading to an explicit update of the boundary value \(u^{n+1}_{0}\).
The implementation can be tested with a Gaussian function as initial condition:
$$g(x;m,s)=\frac{1}{\sqrt{2\pi}s}e^{\frac{(xm)^{2}}{2s^{2}}}\thinspace.$$Run two tests:

a)
Disturbance in the middle of the domain, \(I(x)=g(x;L/2,s)\), and open boundary condition at the left end.

b)
Disturbance at the left end, \(I(x)=g(x;0,s)\), and \(u_{x}=0\) as symmetry boundary condition at this end.
Make test functions for both cases, testing that the solution is zero after the waves have left the domain.

a)

b)
In 2D and 3D it is difficult to compute the correct wave velocity normal to the boundary, which is needed in generalizations of the open boundary conditions in higher dimensions. Test the effect of having a slightly wrong wave velocity in (2.72). Make movies to illustrate what happens.
Filename: wave1D_open_BC.
Remarks
The condition (2.69) works perfectly in 1D when c is known. In 2D and 3D, however, the condition reads \(u_{t}+c_{x}u_{x}+c_{y}u_{y}=0\), where c _{ x } and c _{ y } are the wave speeds in the x and y 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.
Exercise 2.13 (Implement periodic boundary conditions)
It is frequently of interest to follow wave motion over large distances and long times. A straightforward approach is to work with a very large domain, but that might lead to a lot of computations in areas of the domain where the waves cannot be noticed. A more efficient approach is to let a rightgoing wave out of the domain and at the same time let it enter the domain on the left. This is called a periodic boundary condition.
The boundary condition at the right end x = L is an open boundary condition (see Exercise 2.12) to let a rightgoing wave out of the domain. At the left end, x = 0, we apply, in the beginning of the simulation, either a symmetry boundary condition (see Exercise 2.8) \(u_{x}=0\), or an open boundary condition.
This initial wave will split in two and either be reflected or transported out of the domain at x = 0. The purpose of the exercise is to follow the rightgoing wave. We can do that with a periodic boundary condition. This means that when the rightgoing wave hits the boundary x = L, 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 \(u_{x}=0\) or an open boundary condition at the left end to a periodic condition can happen when \(u(L,t)> \epsilon\), where \(\epsilon=10^{4}\) might be an appropriate value for determining when the rightgoing wave hits the boundary x = L.
The open boundary conditions can conveniently be discretized as explained in Exercise 2.12. Implement the described type of boundary conditions and test them on two different initial 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 middle of the domain: \(u(x,0)=\exp{(\frac{1}{2}(x0.5)^{2}/0.05)}\). The domain is the unit interval \([0,1]\). Run these two shapes for Courant numbers 1 and 0.5. Assume constant wave velocity. Make movies of the four cases. Reason why the solutions are correct.
Filename: periodic.
Exercise 2.14 (Compare discretizations of a Neumann condition)
We have a 1D wave equation with variable wave velocity: \(u_{tt}=(qu_{x})_{x}\). A Neumann condition u _{ x } at x = 0,L can be discretized as shown in (2.54) and (2.57).
The aim of this exercise is to examine the rate of the numerical error when using different ways of discretizing the Neumann condition.

a)
As a test problem, \(q=1+(xL/2)^{4}\) can be used, with \(f(x,t)\) adapted such that the solution has a simple form, say \(u(x,t)=\cos(\pi x/L)\cos(\omega t)\) for, e.g., ω = 1. Perform numerical experiments and find the convergence rate of the error using the approximation (2.54).

b)
Switch to \(q(x)=1+\cos(\pi x/L)\), which is symmetric at x = 0,L, and check the convergence rate of the scheme (2.57). Now, \(q_{i1/2}\) is a 2ndorder approximation to q _{ i }, \(q_{i1/2}=q_{i}+0.25q_{i}^{\prime\prime}\Delta x^{2}+\cdots\), because \(q_{i}^{\prime}=0\) for \(i=N_{x}\) (a similar argument can be applied to the case i = 0).

c)
A third discretization can be based on a simple and convenient, but less accurate, onesided difference: \(u_{i}u_{i1}=0\) at \(i=N_{x}\) and \(u_{i+1}u_{i}=0\) at i = 0. Derive the resulting scheme in detail and implement it. Run experiments with q from a) or b) to establish the rate of convergence of the scheme.

d)
A fourth technique is to view the scheme as
$$[D_{t}D_{t}u]^{n}_{i}=\frac{1}{\Delta x}\left([qD_{x}u]_{i+\frac{1}{2}}^{n}[qD_{x}u]_{i\frac{1}{2}}^{n}\right)+[f]_{i}^{n},$$and place the boundary at \(x_{i+\frac{1}{2}}\), \(i=N_{x}\), instead of exactly at the physical boundary. With this idea of approximating (moving) the boundary, we can just set \([qD_{x}u]_{i+\frac{1}{2}}^{n}=0\). Derive the complete scheme using this technique. The implementation of the boundary condition at \(L\Delta x/2\) is \(\mathcal{O}(\Delta x^{2})\) accurate, but the interesting question is what impact the movement of the boundary has on the convergence rate. Compute the errors as usual over the entire mesh and use q from a) or b).
Filename: Neumann_discr.
Exercise 2.15 (Verification by a cubic polynomial in space)
The purpose of this exercise is to verify the implementation of the solver function in the program wave1D_n0.py by using an exact numerical solution for the wave equation \(u_{tt}=c^{2}u_{xx}+f\) with Neumann boundary conditions \(u_{x}(0,t)=u_{x}(L,t)=0\).
A similar verification is used in the file wave1D_u0.py , which solves the same PDE, but with Dirichlet boundary conditions \(u(0,t)=u(L,t)=0\). The idea of the verification test in function test_quadratic in wave1D_u0.py is to produce a solution that is a lowerorder polynomial such that both the PDE problem, the boundary conditions, and all the discrete equations are exactly fulfilled. Then the solver function should reproduce this exact solution to machine precision. More precisely, we seek \(u=X(x)T(t)\), with T(t) as a linear function and X(x) as a parabola that fulfills the boundary conditions. Inserting this u in the PDE determines f. It turns out that u also fulfills the discrete equations, because the truncation error of the discretized PDE has derivatives in x and t of order four and higher. These derivatives all vanish for a quadratic X(x) and linear T(t).
It would be attractive to use a similar approach in the case of Neumann conditions. We set \(u=X(x)T(t)\) and seek lowerorder polynomials X and T. To force u _{ x } to vanish at the boundary, we let X _{ x } be a parabola. Then X is a cubic polynomial. The fourthorder derivative of a cubic polynomial vanishes, so \(u=X(x)T(t)\) will fulfill the discretized PDE also in this case, if f is adjusted such that u fulfills the PDE.
However, the discrete boundary condition is not exactly fulfilled by this choice of u. The reason is that
At the two boundary points, we must demand that the derivative \(X_{x}(x)=0\) such that \(u_{x}=0\). However, u _{ xxx } is a constant and not zero when X(x) is a cubic polynomial. Therefore, our \(u=X(x)T(t)\) fulfills
and not
as it should. (Note that all the higherorder terms \(\mathcal{O}(\Delta x^{4})\) also have higherorder derivatives that vanish for a cubic polynomial.) So to summarize, the fundamental problem is that u as a product of a cubic polynomial and a linear or quadratic polynomial in time is not an exact solution of the discrete boundary conditions.
To make progress, we assume that \(u=X(x)T(t)\), where T for simplicity is taken as a prescribed linear function \(1+\frac{1}{2}t\), and X(x) is taken as an unknown cubic polynomial \(\sum_{j=0}^{3}a_{j}x^{j}\). There are two different ways of determining the coefficients \(a_{0},\ldots,a_{3}\) such that both the discretized PDE and the discretized boundary conditions are fulfilled, under the constraint that we can specify a function \(f(x,t)\) for the PDE to feed to the solver function in wave1D_n0.py. Both approaches are explained in the subexercises.

a)
One can insert u in the discretized PDE and find the corresponding f. Then one can insert u in the discretized boundary conditions. This yields two equations for the four coefficients \(a_{0},\ldots,a_{3}\). To find the coefficients, one can set \(a_{0}=0\) and \(a_{1}=1\) for simplicity and then determine a _{2} and a _{3}. This approach will make a _{2} and a _{3} depend on \(\Delta x\) and f will depend on both \(\Delta x\) and \(\Delta t\).
Use sympy to perform analytical computations. A starting point is to define u as follows:
The symbolic expression for u is reached by calling u(x,t) with x and t as sympy symbols.
Define DxDx(u, i, n), DtDt(u, i, n), and D2x(u, i, n) as Python functions for returning the difference approximations \([D_{x}D_{x}u]^{n}_{i}\), \([D_{t}D_{t}u]^{n}_{i}\), and \([D_{2x}u]^{n}_{i}\). The next step is to set up the residuals for the equations \([D_{2x}u]^{n}_{0}=0\) and \([D_{2x}u]^{n}_{N_{x}}=0\), where \(N_{x}=L/\Delta x\). Call the residuals R_0 and R_L. Substitute a _{0} and a _{1} by 0 and 1, respectively, in R_0, R_L, and a:
Determining a _{2} and a _{3} from the discretized boundary conditions is then about solving two equations with respect to a _{2} and a _{3}, i.e., a[2:]:
Now, a contains computed values and u will automatically use these new values since X accesses a.
Compute the source term f from the discretized PDE: \(f^{n}_{i}=[D_{t}D_{t}uc^{2}D_{x}D_{x}u]^{n}_{i}\). Turn u, the time derivative u _{ t } (needed for the initial condition V(x)), and f into Python functions. Set numerical values for L, N _{ x }, C, and c. Prescribe the time interval as \(\Delta t=CL/(N_{x}c)\), which imply \(\Delta x=c\Delta t/C=L/N_{x}\). Define new functions I(x), V(x), and f(x,t) as wrappers of the ones made above, where fixed values of L, c, \(\Delta x\), and \(\Delta t\) are inserted, such that I, V, and f can 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.
Hint
To turn a sympy expression e, depending on a series of symbols, say x, t, dx, dt, L, and c, into a plain Python function e_exact(x,t,L,dx,dt,c), one can write
The ’numpy’ argument is a good habit as the e_exact function will then work with array arguments if it contains mathematical functions (but here we only do plain arithmetics, which automatically work with arrays).

b)
An alternative way of determining \(a_{0},\ldots,a_{3}\) is to reason as follows. We first construct X(x) such that the boundary conditions are fulfilled: \(X=x(Lx)\). However, to compensate for the fact that this choice of X does not fulfill the discrete boundary condition, we seek u such that
$$u_{x}=\frac{\partial}{\partial x}x(Lx)T(t)\frac{1}{6}u_{xxx}\Delta x^{2},$$since this u will fit the discrete boundary condition. Assuming \(u=T(t)\sum_{j=0}^{3}a_{j}x^{j}\), we can use the above equation to determine the coefficients \(a_{1},a_{2},a_{3}\). A value, e.g., 1 can be used for a _{0}. The following sympy code computes this u:
The next step is to find the source term f_e by inserting u_e in the PDE. Thereafter, turn u, 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 parameters as in a) and check that the solution is exact to machine precision at each time level using an appropriate user_action function.
Filename: wave1D_n0_test_cubic.
2.10 Analysis of the Difference Equations
2.10.1 Properties of the Solution of the Wave Equation
The wave equation
has solutions of the form
for any functions g _{ R } and g _{ L } sufficiently smooth to be differentiated twice. The result follows from inserting (2.75) in the wave equation. A function of the form \(g_{R}(xct)\) represents a signal moving to the right in time with constant velocity c. This feature can be explained as follows. At time t = 0 the signal looks like \(g_{R}(x)\). Introducing a moving horizontal coordinate ξ = x − ct, we see the function \(g_{R}(\xi)\) is ‘‘at rest’’ in the ξ coordinate system, and the shape is always the same. Say the \(g_{R}(\xi)\) function has a peak at ξ = 0. This peak is located at x = ct, which means that it moves with the velocity dx ∕ dt = c in the x coordinate system. Similarly, \(g_{L}(x+ct)\) is a function, initially with shape \(g_{L}(x)\), that moves in the negative x direction with constant velocity c (introduce ξ = x + ct, look at the point ξ = 0, x = −ct, which has velocity dx ∕ dt = −c).
With the particular initial conditions
we get, with u as in (2.75),
The former suggests \(g_{R}=g_{L}\), and the former then leads to \(g_{R}=g_{L}=I/2\). Consequently,
The interpretation of (2.76) is that the initial shape of u is split into two parts, each with the same shape as I but half of the initial amplitude. One part is traveling to the left and the other one to 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 preserve the constant amplitude, but the velocity depends on the mesh parameters \(\Delta t\) and \(\Delta x\).
The solution (2.76) will be influenced by boundary conditions when the parts \(\frac{1}{2}I(xct)\) and \(\frac{1}{2}I(x+ct)\) hit the boundaries and get, e.g., reflected back into the domain. However, when I(x) is nonzero only in a small part in the middle of the spatial domain \([0,L]\), which means that the boundaries are placed far away from the initial disturbance of u, the solution (2.76) is very clearly observed in a simulation.
A useful representation of solutions of wave equations is a linear combination of sine and/or cosine waves. Such a sum of waves is a solution if the governing PDE is linear and each sine or cosine wave fulfills the equation. To ease analytical calculations by hand we shall work with complex exponential functions instead of realvalued sine or cosine functions. The real part of complex expressions will typically be taken as the physical relevant quantity (whenever a physical relevant quantity is strictly needed). The idea now is to build I(x) of complex wave components e ^{ikx}:
Here, k is the frequency of a component, K is some set of all the discrete k values needed to approximate I(x) well, and b _{ k } are constants that must be determined. We will very seldom need to compute the b _{ k } coefficients: most of the insight we look for, and the understanding of the numerical methods we want to establish, come from investigating how the PDE and the scheme treat a single component e ^{ikx} wave.
Letting the number of k values in K tend to infinity, makes the sum (2.77) converge to I(x). This sum is known as a Fourier series representation of I(x). Looking at (2.76), we see that the solution \(u(x,t)\), when I(x) is represented as in (2.77), is also built of basic complex exponential wave components of the form \(e^{ik(x\pm ct)}\) according to
It is common to introduce the frequency in time ω = kc and assume that \(u(x,t)\) is a sum of basic wave components written as \(e^{ikx\omega t}\). (Observe that inserting such a wave component in the governing PDE reveals that \(\omega^{2}=k^{2}c^{2}\), or ω = ±kc, reflecting the two solutions: one (+kc) traveling to the right and the other (−kc) traveling to the left.)
2.10.2 More Precise Definition of Fourier Representations
The above introduction to function representation by sine and cosine waves was quick and intuitive, but will suffice as background knowledge for the following material of single wave component analysis. However, to understand all details of how different wave components sum up to the analytical and numerical solutions, a more precise mathematical treatment is helpful and therefore summarized below.
It is well known that periodic functions can be represented by Fourier series. A generalization of the Fourier series idea to nonperiodic functions defined on the real line is the Fourier transform:
The function A(k) reflects the weight of each wave component e ^{ikx} in an infinite sum of such wave components. That is, A(k) reflects the frequency content in the function I(x). Fourier transforms are particularly fundamental for analyzing and understanding timevarying signals.
The solution of the linear 1D wave PDE can be expressed as
In a finite difference method, we represent u by a mesh function \(u^{n}_{q}\), where n counts temporal mesh points and q counts the spatial ones (the usual counter for spatial points, i, is here already used as imaginary unit). Similarly, I(x) is approximated by the mesh function I _{ q }, \(q=0,\ldots,N_{x}\). On a mesh, it does not make sense to work with wave components e ^{ikx} for very large k, because the shortest possible sine or cosine wave that can be represented uniquely on a mesh with spacing \(\Delta x\) is the wave with wavelength \(2\Delta x\). This wave has its peaks and throughs at every two mesh points. That is, the wave ‘‘jumps up and down’’ between the mesh points.
The corresponding k value for the shortest possible wave in the mesh is \(k=2\pi/(2\Delta x)=\pi/\Delta x\). This maximum frequency is known as the Nyquist frequency. Within the range of relevant frequencies \((0,\pi/\Delta x]\) one defines the discrete Fourier transform ^{Footnote 11}, using \(N_{x}+1\) discrete frequencies:
The A _{ k } values represent the discrete Fourier transform of the I _{ q } values, which themselves are the inverse discrete Fourier transform of the A _{ k } values.
The discrete Fourier transform is efficiently computed by the Fast Fourier transform algorithm. For a real function I(x), the relevant Python code for computing and plotting the discrete Fourier transform appears in the example below.
2.10.3 Stability
The scheme
for the wave equation \(u_{tt}=c^{2}u_{xx}\) allows basic wave components
as solution, but it turns out that the frequency in time, \(\tilde{\omega}\), is not equal to the exact frequency ω = kc. The goal now is to find exactly what \(\tilde{\omega}\) is. We ask two key questions:

How accurate is \(\tilde{\omega}\) compared to ω?

Does the amplitude of such a wave component preserve its (unit) amplitude, as it should, or does it get amplified or damped in time (because of a complex \(\tilde{\omega}\))?
The following analysis will answer these questions. We shall continue using q as an identifier for a certain mesh point in the x direction.
Preliminary results
A key result needed in the investigations is the finite difference approximation of a secondorder derivative acting on a complex wave component:
By just changing symbols (\(\omega\rightarrow k\), \(t\rightarrow x\), \(n\rightarrow q\)) it follows that
Numerical wave propagation
Inserting a basic wave component \(u^{n}_{q}=e^{i(kx_{q}\tilde{\omega}t_{n})}\) in (2.83 ) results in the need to evaluate two expressions:
Then the complete scheme,
leads to the following equation for the unknown numerical frequency \(\tilde{\omega}\) (after dividing by \(e^{ikx}e^{i\tilde{\omega}t}\)):
or
where
is the Courant number. Taking the square root of (2.86) yields
Since the exact ω is real it is reasonable to look for a real solution \(\tilde{\omega}\) of (2.88). The righthand side of (2.88 ) must then be in \([1,1]\) because the sine function on the lefthand side has values in \([1,1]\) for real \(\tilde{\omega}\). The magnitude of the sine function on the righthand side attains the value 1 when
With m = 0 we have \(k\Delta x=\pi\), which means that the wavelength \(\lambda=2\pi/k\) becomes \(2\Delta x\). This 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 to k values whose waves are too short to be represented on a mesh with spacing \(\Delta x\). For the shortest possible wave in the mesh, \(\sin\left(k\Delta x/2\right)=1\), and we must require
Consider a righthand side in (2.88) of magnitude larger than unity. The solution \(\tilde{\omega}\) of (2.88) must then be a complex number \(\tilde{\omega}=\tilde{\omega}_{r}+i\tilde{\omega}_{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 −ω_{ i }. The component with \(\omega_{i}> 0\) gives an amplification factor \(e^{\omega_{i}t}\) that grows exponentially in time. We cannot allow this and must therefore require C ≤ 1 as a stability criterion.
Remark on the stability requirement
For smoother wave components with longer wave lengths per length \(\Delta x\), (2.89) can in theory be relaxed. However, small roundoff errors are always present in a numerical solution and these vary arbitrarily from mesh point to mesh point and can be viewed as unavoidable noise with wavelength \(2\Delta x\). As explained, C > 1 will for this very small noise lead to exponential growth of the shortest possible wave component in the mesh. This noise will therefore grow with time and destroy the whole solution.
2.10.4 Numerical Dispersion Relation
Equation (2.88) can be solved with respect to \(\tilde{\omega}\):
The relation between the numerical frequency \(\tilde{\omega}\) and the other parameters k, c, \(\Delta x\), and \(\Delta 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 \(\lambda=2\pi/k\)) of the wave. Since the wave velocity 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.
The special case C = 1 deserves attention since then the righthand side of (2.90) reduces to
That is, \(\tilde{\omega}=\omega\) and the numerical solution is exact at all mesh points regardless of \(\Delta x\) and \(\Delta t\)! 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 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 \(\tilde{\omega}\omega\), \(\tilde{\omega}/\omega\), or the similar error measures in wave velocity: \(\tilde{c}c\) and \(\tilde{c}/c\), where c = ω ∕ k and \(\tilde{c}=\tilde{\omega}/k\). It appears that the most convenient expression to work with is \(\tilde{c}/c\), since it can be written as a function of just two parameters:
with \(p=k\Delta x/2\) as a nondimensional measure of the spatial frequency. In essence, p tells how many spatial mesh points we have per wave length in space for the wave component with frequency k (recall that the wave length is \(2\pi/k\)). That is, p reflects how well the spatial variation of the wave component is resolved in the mesh. Wave components with wave length less than \(2\Delta x\) (\(2\pi/k<2\Delta x\)) are not visible in the mesh, so it does not make sense to have p > π ∕ 2.
We may introduce the function \(r(C,p)=\tilde{c}/c\) for further investigation of numerical errors in the wave velocity:
This function is very well suited for plotting since it combines several parameters in the problem into a dependence on two dimensionless numbers, C and p.
Defining
we can plot \(r(C,p)\) as a function of p for various values of C, see Fig. 2.6. Note that the shortest waves have the most erroneous velocity, and that short waves move more slowly than they should.
We can also easily make a Taylor series expansion in the discretization parameter p:
Note that without the .removeO() call the series gets an O(x**7) term that makes it impossible 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 the error of this series expansion is
pointing to an error \(\mathcal{O}(\Delta t^{2},\Delta x^{2})\), which is compatible with the errors in the difference approximations (\(D_{t}D_{t}u\) and \(D_{x}D_{x}u\)).
We can do more with a series expansion, e.g., factor it to see how the factor C − 1 plays a significant role. To this end, we make a list of the terms, factor each term, and then sum the terms:
We see from the last expression that C = 1 makes all the terms in rs vanish. Since we already know that the numerical solution is exact for C = 1, the remaining terms in the Taylor series expansion will also contain factors of C − 1 and cancel for C = 1.
2.10.5 Extending the Analysis to 2D and 3D
The typical analytical solution of a 2D wave equation
is a wave traveling in the direction of \(\boldsymbol{k}=k_{x}\boldsymbol{i}+k_{y}\boldsymbol{j}\), where i and j are unit vectors in the x and y directions, respectively (i should not be confused with \(i=\sqrt{1}\) here). Such a wave can be expressed by
for some twice differentiable function g, or with ω = kc, \(k=\boldsymbol{k}\):
We can, in particular, build a solution by adding complex Fourier components of the form
A discrete 2D wave equation can be written as
This equation admits a Fourier component
as solution. Letting the operators \(D_{t}D_{t}\), \(D_{x}D_{x}\), and \(D_{y}D_{y}\) act on \(u^{n}_{q,r}\) from (2.94) transforms (2.93) to
or
where we have eliminated the factor 4 and introduced the symbols
For a realvalued \(\tilde{\omega}\) the righthand side must be less than or equal to unity in absolute value, requiring in general that
This gives the stability criterion, more commonly expressed directly in an inequality for the time step:
A similar, straightforward analysis for the 3D case leads to
In the case of a variable coefficient \(c^{2}=c^{2}(\boldsymbol{x})\), we must use the worstcase value
in the stability criteria. Often, especially in the variable wave velocity case, it is wise to introduce a safety factor \(\beta\in(0,1]\) too:
The exact numerical dispersion relations in 2D and 3D becomes, for constant c,
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 \(\tilde{\omega}\). The direction of the wave is parameterized by the polar angle θ, which means that
A simplification is to set \(\Delta x=\Delta y=h\). Then \(C_{x}=C_{y}=c\Delta t/h\), which we call C. Also,
The numerical frequency \(\tilde{\omega}\) is now a function of three parameters:

C, reflecting the number of cells a wave is displaced during a time step,

\(p=\frac{1}{2}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 \(\Delta t\) as a free parameter in \(\tilde{\omega}\), we work with \(\tilde{c}/c=\tilde{\omega}/(kc)\). The coefficient in front of the \(\sin^{1}\) factor is then
and
We want to visualize this quantity as a function of p and θ for some values of C ≤ 1. It is instructive to make color contour plots of \(1\tilde{c}/c\) in polar coordinates with θ as the angular coordinate and p as the radial coordinate.
The stability criterion (2.97) becomes \(C\leq C_{\max}=1/\sqrt{2}\) in the present 2D case with the C defined above. Let us plot \(1\tilde{c}/c\) in polar coordinates for \(C_{\max},0.9C_{\max},0.5C_{\max},0.2C_{\max}\). The program below does the somewhat tricky work in Matplotlib, and the result appears in Fig. 2.7. From the figure we clearly see that the maximum C value gives the best results, and that waves whose propagation direction makes an angle of 45 degrees with an axis are the most accurate.
2.11 Finite Difference Methods for 2D and 3D Wave Equations
A natural next step is to consider extensions of the methods for various variants of the onedimensional wave equation to twodimensional (2D) and threedimensional (3D) versions of the wave equation.
2.11.1 MultiDimensional Wave Equations
The general wave equation in d space dimensions, with constant wave velocity c, can be written in the compact form
where
in a 2D problem (d = 2) and
in three space dimensions (d = 3).
Many applications involve variable coefficients, and the general wave equation in d dimensions is in this case written as
which in, e.g., 2D becomes
To save some writing and space we may use the index notation, where subscript t, x, or y means differentiation with respect to that coordinate. For example,
These comments extend straightforwardly to 3D, which means that the 3D versions of the two wave PDEs, with and without variable coefficients, can be stated as
At each point of the boundary \(\partial\Omega\) (of Ω) we need one boundary condition involving the 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 an incoming wave),

2.
\(\partial u/\partial n=\boldsymbol{n}\cdot\nabla u\) is prescribed (zero for reflecting boundaries),

3.
an open boundary condition (also called radiation condition) is specified to let waves travel undisturbed out of the domain, see Exercise 2.12 for details.
All the listed wave equations with secondorder derivatives in time need two initial conditions:

1.
u = I,

2.
\(u_{t}=V\).
2.11.2 Mesh
We introduce a mesh in time and in space. The mesh in time consists of time points
normally, for wave equation problems, with a constant spacing \(\Delta t=t_{n+1}t_{n}\), \(n\in\mathcal{I}_{t}^{}\).
Finite difference methods are easy to implement on simple rectangle or boxshaped spatial domains. More complicated shapes of the spatial domain require substantially more advanced techniques and implementational efforts (and a finite element method is usually a more convenient approach). On a rectangle or boxshaped domain, mesh points are introduced separately in the various space directions:
We can write a general mesh point as \((x_{i},y_{j},z_{k},t_{n})\), with \(i\in\mathcal{I}_{x}\), \(j\in\mathcal{I}_{y}\), \(k\in\mathcal{I}_{z}\), and \(n\in\mathcal{I}_{t}\).
It is a very common choice to use constant mesh spacings: \(\Delta x=x_{i+1}x_{i}\), \(i\in\mathcal{I}_{x}^{}\), \(\Delta y=y_{j+1}y_{j}\), \(j\in\mathcal{I}_{y}^{}\), and \(\Delta z=z_{k+1}z_{k}\), \(k\in\mathcal{I}_{z}^{}\). With equal mesh spacings one often introduces \(h=\Delta x=\Delta y=\Delta z\).
The unknown u at mesh point \((x_{i},y_{j},z_{k},t_{n})\) is denoted by \(u^{n}_{i,j,k}\). In 2D problems we just skip the z coordinate (by assuming no variation in that direction: \(\partial/\partial z=0\)) and write \(u^{n}_{i,j}\).
2.11.3 Discretization
Two and threedimensional wave equations are easily discretized by assembling building blocks for discretization of 1D wave equations, because the multidimensional versions just contain terms of the same type as those in 1D.
Discretizing the PDEs
Equation (2.107) can be discretized as
A 2D version might be instructive to write out in detail:
which becomes
Assuming, as usual, that all values at time levels n and n − 1 are known, we can solve for the only unknown \(u^{n+1}_{i,j}\). The result can be compactly written as
As in the 1D case, we need to develop a special formula for \(u^{1}_{i,j}\) where we combine the general scheme for \(u^{n+1}_{i,j}\), when n = 0, with the discretization of the initial condition:
The result becomes, in compact form,
The PDE (2.108) with variable coefficients is discretized term by term using the corresponding elements from the 1D case:
When written out and solved for the unknown \(u^{n+1}_{i,j,k}\), one gets the scheme
Also here we need to develop a special formula for \(u^{1}_{i,j,k}\) by combining the scheme for n = 0 with the discrete initial condition, which is just a matter of inserting \(u^{1}_{i,j,k}=u^{1}_{i,j,k}2\Delta tV_{i,j,k}\) in the scheme and solving for \(u^{1}_{i,j,k}\).
Handling boundary conditions where u is known
The schemes listed above are valid for 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.
Discretizing the Neumann condition
The condition \(\partial u/\partial n=0\) was implemented in 1D by discretizing it with a \(D_{2x}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 introduce ghost cells and update a ghost value for use in the Neumann condition. Exactly the same ideas are reused in multiple dimensions.
Consider the condition \(\partial u/\partial n=0\) at a boundary y = 0 of a rectangular domain \([0,L_{x}]\times[0,L_{y}]\) in 2D. The normal direction is then in −y direction, so
and we set
From this it follows that \(u^{n}_{i,1}=u^{n}_{i,1}\). The discretized PDE at the boundary point \((i,0)\) reads
We can then just insert \(u^{n}_{i,1}\) for \(u^{n}_{i,1}\) in this equation and solve for the boundary value \(u^{n+1}_{i,0}\), just as was done in 1D.
From these calculations, we see a pattern: the general scheme applies at the boundary j = 0 too if we just replace j − 1 by j + 1. Such a pattern is particularly useful for implementations. The details follow from the explained 1D case in Sect. 2.6.3.
The alternative approach to eliminating fictitious values outside the mesh is to have \(u^{n}_{i,1}\) available as a ghost value. The mesh is extended with one extra line (2D) or plane (3D) of ghost cells at a Neumann boundary. In the present example it means that we need a line with ghost cells below the y axis. The ghost values must be updated according to \(u^{n+1}_{i,1}=u^{n+1}_{i,1}\).
2.12 Implementation
We shall now describe in detail various Python implementations for solving a standard 2D, linear wave equation with constant wave velocity and u = 0 on the boundary. The wave equation is to be solved in the spacetime domain \(\Omega\times(0,T]\), where \(\Omega=(0,L_{x})\times(0,L_{y})\) is a rectangular spatial domain. More precisely, the complete initialboundary value problem is defined by
where \(\partial\Omega\) is the boundary of Ω, in this case the four sides of the rectangle \(\Omega=[0,L_{x}]\times[0,L_{y}]\): x = 0, \(x=L_{x}\), y = 0, and \(y=L_{y}\).
The PDE is discretized as
which leads to an explicit updating formula to be implemented in a program:
for all interior mesh points \(i\in\mathcal{I}_{x}^{i}\) and \(j\in\mathcal{I}_{y}^{i}\), for \(n\in\mathcal{I}_{t}^{+}\). The constants C _{ x } and C _{ y } are defined as
At the boundary, we simply set \(u^{n+1}_{i,j}=0\) for i = 0, \(j=0,\ldots,N_{y}\); \(i=N_{x}\), \(j=0,\ldots,N_{y}\); j = 0, \(i=0,\ldots,N_{x}\); and \(j=N_{y}\), \(i=0,\ldots,N_{x}\). For the first step, n = 0, (2.117) is combined with the discretization of the initial condition \(u_{t}=V\), \([D_{2t}u=V]^{0}_{i,j}\) to obtain a special formula for \(u^{1}_{i,j}\) at the interior mesh points:
The algorithm is very similar to the one in 1D:

1.
Set initial condition \(u^{0}_{i,j}=I(x_{i},y_{j})\)

2.
Compute \(u^{1}_{i,j}\) from (2.117)

3.
Set \(u^{1}_{i,j}=0\) for the boundaries \(i=0,N_{x}\), \(j=0,N_{y}\)

4.
For \(n=1,2,\ldots,N_{t}\):

a)
Find \(u^{n+1}_{i,j}\) from (2.117) for all internal mesh points, \(i\in\mathcal{I}_{x}^{i}\), \(j\in\mathcal{I}_{y}^{i}\)

b)
Set \(u^{n+1}_{i,j}=0\) for the boundaries \(i=0,N_{x}\), \(j=0,N_{y}\)

a)
2.12.1 Scalar Computations
The solver function for a 2D case with constant wave velocity and boundary condition u = 0 is analogous to the 1D case with similar parameter values (see wave1D_u0.py), apart from a few necessary extensions. The code is found in the program wave2D_u0.py .
Domain and mesh
The spatial domain is now \([0,L_{x}]\times[0,L_{y}]\), specified by the arguments Lx and Ly. Similarly, the number of mesh points in the x and y directions, N _{ x } and N _{ y }, become the arguments Nx and Ny. In multidimensional problems it makes less sense to specify a Courant number since the wave velocity is a vector and mesh spacings may differ in the various spatial directions. We therefore give \(\Delta t\) explicitly. The signature of the solver function is then
Key parameters used in the calculations are created as
Solution arrays
We store \(u^{n+1}_{i,j}\), \(u^{n}_{i,j}\), and \(u^{n1}_{i,j}\) in three twodimensional arrays,
where \(u^{n+1}_{i,j}\) corresponds to u[i,j], \(u^{n}_{i,j}\) to u_n[i,j], and \(u^{n1}_{i,j}\) to u_nm1[i,j].
Index sets
It is also convenient to introduce the index sets (cf. Sect. 2.6.4)
Computing the solution
Inserting the initial condition I in u_n and making a callback to the user in terms of the user_action function is a straightforward generalization of the 1D code from Sect. 2.1.6:
The user_action function has additional arguments compared to the 1D case. The arguments xv and yv will be commented upon in Sect. 2.12.2.
The key finite difference formula (2.110) for updating the solution at a time level is implemented in a separate function as
The step1 variable has been introduced to allow the formula to be reused for the first step, computing \(u^{1}_{i,j}\):
Below, we will make many alternative implementations of the advance_scalar function to speed up the code since most of the CPU time in simulations is spent in this function.
Remark: How to use the solution
The solver function in the wave2D_u0.py code updates arrays for the next time step by switching references as described in Sect. 2.4.5. Any use of u on the user’s side is assumed to take place in the user action function. However, should the code be changed such that u is returned and used as solution, have in mind that you must return u_n after the time limit, otherwise a return u will actually return u_nm1 (due to the switching of array indices in the loop)!
2.12.2 Vectorized Computations
The scalar code above turns out to be extremely slow for large 2D meshes, and probably useless in 3D beyond debugging of small test cases. Vectorization is therefore a must for multidimensional finite difference computations in Python. For example, with a mesh consisting of 30 × 30 cells, vectorization brings down the CPU time by a factor of 70 (!). Equally important, vectorized code can also easily be parallelized to take (usually) optimal advantage of parallel computer platforms.
In the vectorized case, we must be able to evaluate usergiven functions like \(I(x,y)\) and \(f(x,y,t)\) for the entire mesh in one operation (without loops). These usergiven functions are provided as Python functions I(x,y) and f(x,y,t), respectively. Having the onedimensional coordinate arrays x and y is not sufficient when calling I and f in a vectorized way. We must extend x and y to their vectorized versions xv and yv:
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]) will now return I and f values for the entire set of mesh points.
With the xv and yv arrays for vectorized computing, setting the initial condition is just a matter of
One could also have written u_n = I(xv, yv) and let u_n point to a new object, but vectorized operations often make use of direct insertion in the original array through u_n[:,:], because sometimes not all of the array is to be filled by such a function evaluation. This is the case with the computational scheme for \(u^{n+1}_{i,j}\):
Array slices in 2D are more complicated to understand than those in 1D, but the logic from 1D applies to each dimension separately. For example, when doing \(u^{n}_{i,j}u^{n}_{i1,j}\) for \(i\in\mathcal{I}_{x}^{+}\), we just keep j constant and make a slice in the first index: u_n[1:,j]  u_n[:1,j], exactly as in 1D. The 1: slice specifies all the indices \(i=1,2,\ldots,N_{x}\) (up to the last valid index), while :1 specifies the relevant indices for the second term: \(0,1,\ldots,N_{x}1\) (up to, but not including the last index).
In the above code segment, the situation is slightly more complicated, because each displaced 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.
The boundary conditions along the four sides make use of a slice consisting of all indices along a boundary:
In the vectorized update of u (above), the function f is first computed as an array over all mesh points:
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 available in an array rather than calling our Python function f(x,y,t) for every point.
Also in the advance_vectorized function we have introduced a boolean step1 to reuse the formula for the first time step in the same way as we did with advance_scalar. We refer to the solver function in wave2D_u0.py for the details on how the overall algorithm is implemented.
The callback function now has the arguments u, x, xv, y, yv, t, n. The inclusion of xv and yv makes it easy to, e.g., compute an exact 2D solution in the callback function and compute errors, through an expression like u  u_exact(xv, yv, t[n]).
2.12.3 Verification
Testing a quadratic solution
The 1D solution from Sect. 2.2.4 can be generalized to multidimensions and provides a test case where the exact solution also fulfills the discrete equations, such that we know (to machine precision) what numbers the solver function should produce. In 2D we use the following generalization of (2.30):
This solution fulfills the PDE problem if \(I(x,y)=u_{\mbox{\footnotesize e}}(x,y,0)\), \(V=\frac{1}{2}u_{\mbox{\footnotesize e}}(x,y,0)\), and \(f=2c^{2}(1+{\frac{1}{2}}t)(y(L_{y}y)+x(L_{x}x))\). To show that \(u_{\mbox{\footnotesize e}}\) also solves the discrete equations, we start with the general results \([D_{t}D_{t}1]^{n}=0\), \([D_{t}D_{t}t]^{n}=0\), and \([D_{t}D_{t}t^{2}]=2\), and use these to compute
A similar calculation must be carried out for the \([D_{y}D_{y}u_{\mbox{\footnotesize e}}]^{n}_{i,j}\) and \([D_{t}D_{t}u_{\mbox{\footnotesize e}}]^{n}_{i,j}\) terms. One must also show that the quadratic solution fits the special formula for \(u^{1}_{i,j}\). The details are left as Exercise 2.16. The test_quadratic function in the wave2D_u0.py program implements this verification as a proper test function for the pytest and nose frameworks.
2.12.4 Visualization
Eventually, we are ready for a real application with our code! Look at the wave2D_u0.py and the gaussian function. It starts with a Gaussian function to see how it propagates in a square with u = 0 on the boundaries:
Matplotlib
We want to animate a 3D surface in Matplotlib, but this is a really slow process and not recommended, so we consider Matplotlib not an option as long as onscreen animation is desired. One can use the recipes for single shots of u, where it does produce highquality 3D plots.
Gnuplot
Let us look at different ways for visualization. We import SciTools as st and can access st.mesh and st.surf in Matplotlib or Gnuplot, but this is not supported except for the Gnuplot package, where it works really well (Fig. 2.8). Then we choose plot_method=2 (or less relevant plot_method=1) and force the backend for SciTools to be Gnuplot (if you have the C package Gnuplot and the Gnuplot.py Python interface module installed):
It gives a nice visualization with lifted surface and contours beneath. Figure 2.8 shows four plots of u.
Video files can be made of the PNG frames:
It is wise to use a high frame rate – a low one will just skip many frames. There may also be considerable quality differences between the different formats.
Movie 1
Mayavi
The best option for doing visualization of 2D and 3D scalar and vector fields in Python programs is Mayavi, which is an interface to the highquality package VTK in C++. There is good online documentation and also an introduction in Chapter 5 of [10].
To obtain Mayavi on Ubuntu platforms you can write
For Mac OS X and Windows, we recommend using Anaconda. To obtain Mayavi for Anaconda you can write
Mayavi has a MATLABlike interface called mlab. We can do
and have plt (as usual) or mlab as a kind of MATLAB visualization access inside our program (just more powerful and with higher visual quality).
The official documentation of the mlab module is provided in two places, one for the basic functionality ^{Footnote 12} and one for further functionality ^{Footnote 13}. Basic figure handling ^{Footnote 14} is very similar to the one we know from Matplotlib. Just as for Matplotlib, all plotting commands you do in mlab will go into the same figure, until you manually change to a new figure.
Back to our application, the following code for the user action function with plotting in Mayavi is relevant to add.
This is a point to get started – visualization is as always a very timeconsuming and experimental discipline. With the PNG files we can use ffmpeg to create videos.
Movie 2
2.13 Exercises
Exercise 2.16 (Check that a solution fulfills the discrete model)
Carry out all mathematical details to show that (2.119) is indeed a solution of the discrete model for a 2D wave equation with u = 0 on the boundary. One must check the boundary conditions, the initial conditions, the general discrete equation at a time level and the special version of this equation for the first time level.
Filename: check_quadratic_solution.
Project 2.17 (Calculus with 2D mesh functions)
The goal of this project is to redo Project 2.6 with 2D mesh functions (f _{i,j}).
Differentiation
The differentiation results in a discrete gradient function, which in the 2D case can be represented by a threedimensional 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 onesided forward or backward differences at the boundary points. Construct unit tests and write a corresponding test function.
Integration
The integral of a 2D mesh function f _{i,j} is defined as
where \(f(x,y)\) is a function that takes on the values of the discrete mesh function f _{i,j} at the mesh points, but can also be evaluated in between the mesh points. The particular variation between mesh points can be taken as bilinear, but this is not important as we will use a product Trapezoidal rule to approximate the integral over a cell in the mesh and then we only need to evaluate \(f(x,y)\) at the mesh points.
Suppose F _{i,j} is computed. The calculation of \(F_{i+1,j}\) is then
The integrals in the y direction can be approximated by a Trapezoidal rule. A similar idea can be used to compute \(F_{i,j+1}\). Thereafter, \(F_{i+1,j+1}\) can be computed by adding the integral over the final corner cell to \(F_{i+1,j}+F_{i,j+1}F_{i,j}\). Carry out the details of these computations and implement a function that can return F _{i,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.
Exercise 2.18 (Implement Neumann conditions in 2D)
Modify the wave2D_u0.py program, which solves the 2D wave equation \(u_{tt}=c^{2}(u_{xx}+u_{yy})\) with constant wave velocity c and u = 0 on the boundary, to have Neumann boundary conditions: \(\partial u/\partial n=0\). Include both scalar code (for debugging and reference) and vectorized code (for speed).
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. Another test is to use the plugshaped pulse in the pulse function from Sect. 2.8 and the wave1D_dn_vc.py program. This pulse is exactly propagated in 1D if \(c\Delta t/\Delta x=1\). Check that also the 2D program can propagate this pulse exactly in x direction (\(c\Delta t/\Delta x=1\), \(\Delta y\) arbitrary) and y direction (\(c\Delta t/\Delta y=1\), \(\Delta x\) arbitrary).
Filename: wave2D_dn.
Exercise 2.19 (Test the efficiency of compiled loops in 3D)
Extend the wave2D_u0.py code and the Cython, Fortran, and C versions to 3D. Set up an efficiency experiment to determine the relative efficiency of pure scalar Python code, vectorized code, Cythoncompiled loops, Fortrancompiled loops, and Ccompiled loops. Normalize the CPU time for each mesh by the fastest version.
Filename: wave3D_u0.
2.14 Applications of Wave Equations
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 similar formulation with a system of firstorder equations, there are some exceptions. Perhaps the most important is water waves: these are modeled by the Laplace equation with timedependent boundary conditions at the water surface (long water waves, however, can be approximated by a standard wave equation, see Sect. 2.14.7). Quantum mechanical waves constitute another example where the waves are governed by the Schrödinger equation, i.e., not by a standard wave equation. Many wave phenomena also need to take nonlinear effects into account when the wave amplitude is significant. Shock waves in the air is a primary example.
The derivations in the following are very brief. Those with a firm background in continuum mechanics will probably have enough knowledge to fill in the details, while other readers will hopefully get some impression of the physics and approximations involved when establishing wave equation models.
2.14.1 Waves on a String
Figure 2.10 shows a model we may use to derive the equation for waves on a string. The string 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 x _{ i } be m _{ i }. The displacement of this mass point in the y direction is denoted by \(u_{i}(t)\).
The motion of mass m _{ i } is governed by Newton’s second law of motion. The position of the mass at time t is \(x_{i}\boldsymbol{i}+u_{i}(t)\boldsymbol{j}\), where i and j are unit vectors in the x and y direction, respectively. The acceleration is then \(u_{i}^{\prime\prime}(t)\boldsymbol{j}\). Two forces are acting on the mass as indicated in Fig. 2.10. The force \(\boldsymbol{T}^{}\) acting toward the point \(x_{i1}\) can be decomposed as
where ϕ is the angle between the force and the line \(x=x_{i}\). Let \(\Delta u_{i}=u_{i}u_{i1}\) and let \(\Delta s_{i}=\sqrt{\Delta u_{i}^{2}+(x_{i}x_{i1})^{2}}\) be the distance from mass \(m_{i1}\) to mass m _{ i }. It is seen that \(\cos\phi=\Delta u_{i}/\Delta s_{i}\) and \(\sin\phi=(x_{i}x_{i1})/\Delta s\) or \(\Delta x/\Delta s_{i}\) if we introduce a constant mesh spacing \(\Delta x=x_{i}x_{i1}\). The force can then be written
The force \(\boldsymbol{T}^{+}\) acting toward \(x_{i+1}\) can be calculated in a similar way:
Newton’s second law becomes
which gives the component equations
A basic reasonable assumption for a string is small displacements u _{ i } and small displacement gradients \(\Delta u_{i}/\Delta x\). For small \(g=\Delta u_{i}/\Delta x\) we have that
Equation (2.120) is then simply the identity T = T, while (2.121) can be written as
which upon division by \(\Delta x\) and introducing the density \(\varrho_{i}=m_{i}/\Delta x\) becomes
We can now choose to approximate \(u_{i}^{\prime\prime}\) by a finite difference in time and get the discretized wave equation,
On the other hand, we may go to the continuum limit \(\Delta x\rightarrow 0\) and replace \(u_{i}(t)\) by \(u(x,t)\), \(\varrho_{i}\) by \(\varrho(x)\), and recognize that the righthand side of (2.122) approaches \(\partial^{2}u/\partial x^{2}\) as \(\Delta x\rightarrow 0\). We end up with the continuous model for waves on a string:
Note that the density \(\varrho\) may change along the string, while the tension T is a constant. With variable wave velocity \(c(x)=\sqrt{T/\varrho(x)}\) we can write the wave equation in the more standard form
Because of the way \(\varrho\) enters the equations, the variable wave velocity does not appear inside the derivatives as in many other versions of the wave equation. However, most strings of interest have constant \(\varrho\).
The end points of a string are fixed so that the displacement u is zero. The boundary conditions are therefore u = 0.
Damping
Air resistance and nonelastic effects in the string will contribute to reduce the amplitudes of the waves so that the motion dies out after some time. This damping effect can be modeled by a term bu _{ t } on the lefthand side of the equation
The parameter b ≥ 0 is small for most wave phenomena, but the damping effect may become significant in long time simulations.
External forcing
It is easy to include an external force acting on the string. Say we have a vertical force \(\tilde{f}_{i}\boldsymbol{j}\) acting on mass m _{ i }, modeling the effect of gravity on a string. This force affects the vertical component of Newton’s law and gives rise to an extra term \(\tilde{f}(x,t)\) on the righthand side of (2.124). In the model (2.125) we would add a term \(f(x,t)=\tilde{f}(x,t)/\varrho(x)\).
Modeling the tension via springs
We assumed, in the derivation above, that the tension in the string, T, was constant. It is easy to check this assumption by modeling the string segments between the masses as standard springs, where the force (tension T) is proportional to the elongation of the spring segment. Let k be the spring constant, and set \(T_{i}=k\Delta\ell\) for the tension in the spring segment between \(x_{i1}\) and x _{ i }, where \(\Delta\ell\) is the elongation of this segment from the tensionfree state. A basic feature of a string is that it has high tension in the equilibrium position u = 0. Let the string segment have an elongation \(\Delta\ell_{0}\) in the equilibrium position. After deformation of the string, the elongation is \(\Delta\ell=\Delta\ell_{0}+\Delta s_{i}\): \(T_{i}=k(\Delta\ell_{0}+\Delta s_{i})\approx k(\Delta\ell_{0}+\Delta x)\). This shows that T _{ i } is independent of i. Moreover, the extra approximate elongation \(\Delta x\) is very small compared to \(\Delta\ell_{0}\), so we may well set \(T_{i}=T=k\Delta\ell_{0}\). This means that the tension is completely dominated by the initial tension determined by the tuning of the string. The additional deformations of the spring during the vibrations do not introduce significant changes in the tension.
2.14.2 Elastic Waves in a Rod
Consider an elastic rod subject to a hammer impact at the end. This experiment will give rise to 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 elastic medium,
where \(\varrho\) is the density, u the displacement field, σ the stress tensor, and f body forces. The latter has normally no impact on elastic waves.
For stationary deformation of an elastic rod, aligned with the x axis, one has that \(\sigma_{xx}=Eu_{x}\), with all other stress components being zero. The parameter E is known as Young’s modulus. Moreover, we set \(\boldsymbol{u}=u(x,t)\boldsymbol{i}\) and neglect the radial contraction and expansion (where Poisson’s ratio is the important parameter). Assuming that this simple stress and deformation field is a good approximation, (2.127) simplifies to
The associated boundary conditions are u or \(\sigma_{xx}=Eu_{x}\) known, typically u = 0 for a fixed end and \(\sigma_{xx}=0\) for a free end.
2.14.3 Waves on a Membrane
Think of a thin, elastic membrane with shape as a circle or rectangle. This membrane can be brought into oscillatory motion and will develop elastic waves. We can model this phenomenon somewhat similar to waves in a rod: waves in a membrane are simply the twodimensional counterpart. We assume that the material is deformed in the z direction only and write the elastic displacement field on the form \(\boldsymbol{u}(x,y,t)=w(x,y,t)\boldsymbol{i}\). The z coordinate is omitted since the membrane is thin and all properties are taken as constant throughout the thickness. Inserting this displacement field in Newton’s 2nd law of motion (2.127) results in
This is nothing but a wave equation in \(w(x,y,t)\), which needs the usual initial conditions on w and w _{ t } as well as a boundary condition w = 0. When computing the stress in the membrane, one needs to split σ into a constant highstress component due to the fact that all membranes are normally prestressed, plus a component proportional to the displacement and governed by the wave motion.
2.14.4 The Acoustic Model for Seismic Waves
Seismic waves are used to infer properties of subsurface geological structures. The physical model is a heterogeneous elastic medium where sound is propagated by small elastic vibrations. The general mathematical model for deformations in an elastic medium is based on Newton’s second law,
and a constitutive law relating σ to u, often Hooke’s generalized law,
Here, u is the displacement field, σ is the stress tensor, I is the identity tensor, \(\varrho\) is the medium’s density, f are body forces (such as gravity), K is the medium’s bulk modulus and G is the shear modulus. All these quantities may vary in space, while u and σ will also show significant variation in time during wave motion.
The acoustic approximation to elastic waves arises from a basic assumption that the second term in Hooke’s law, representing the deformations that give rise to shear stresses, can be neglected. This assumption can be interpreted as approximating the geological medium by a fluid. Neglecting also the body forces f, (2.130) becomes
Introducing p as a pressure via
and dividing (2.132) by \(\varrho\), we get
Taking the divergence of this equation, using \(\nabla\cdot\boldsymbol{u}=p/K\) from (2.133), gives the acoustic approximation to elastic waves:
This is a standard, linear wave equation with variable coefficients. It is common to add a source term \(s(x,y,z,t)\) to model the generation of sound waves:
A common additional approximation of (2.136) is based on using the chain rule on the righthand side,
under the assumption that the relative spatial gradient \(\nabla\varrho^{1}=\varrho^{2}\nabla\varrho\) is small. This approximation results in the simplified equation
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 p _{0} such that the boundary condition becomes \(p=p_{0}\).
Anisotropy
Quite often in geological materials, the effective wave velocity \(c=\sqrt{K/\varrho}\) is 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 vertical coordinate, we can introduce a vertical wave velocity c _{ z } and a horizontal wave velocity c _{ h }, and generalize (2.137) to
2.14.5 Sound Waves in Liquids and Gases
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:
These equations are often referred to as the Euler equations for the motion of a fluid. The parameters involved are the density \(\varrho\), the velocity u, and the pressure p. Equation (2.139) reflects mass balance, (2.140) is Newton’s second law for a fluid, with frictional and body forces omitted, and (2.141) is a constitutive law relating density to pressure by thermodynamic considerations. A typical model for (2.141) is the socalled isentropic relation ^{Footnote 15}, valid for adiabatic processes where there is no heat transfer:
Here, p _{0} and \(\varrho_{0}\) are reference values for p and \(\varrho\) when the fluid is at rest, and γ is the ratio of 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 waves are small perturbations to the density, pressure, and velocity. We therefore write
where we have decomposed the fields in a constant equilibrium value, corresponding to u = 0, and a small perturbation marked with a hat symbol. By inserting these decompositions in (2.139) and (2.140), neglecting all product terms of small perturbations and/or their derivatives, and dropping the hat symbols, one gets the following linearized PDE system for the small perturbations in density, pressure, and velocity:
Now we can eliminate \(\varrho_{t}\) by differentiating the relation \(\varrho(p)\),
The product term \(p^{1/\gamma1}p_{t}\) can be linearized as \(p_{0}^{1/\gamma1}p_{t}\), resulting in
We then get
Taking the divergence of (2.146) and differentiating (2.145) with respect to time gives the possibility to easily eliminate \(\nabla\cdot\boldsymbol{u}_{t}\) and arrive at a standard, linear wave equation for p:
where \(c=\sqrt{\gamma p_{0}/\varrho_{0}}\) is the speed of sound in the fluid.
2.14.6 Spherical Waves
Spherically symmetric threedimensional waves propagate in the radial direction r only so that \(u=u(r,t)\). The fully threedimensional wave equation
then reduces to the spherically symmetric wave equation
One can easily show that the function \(v(r,t)=ru(r,t)\) fulfills a standard wave equation in Cartesian coordinates if c is constant. To this end, insert u = v ∕ r in
to obtain
The two terms in the parenthesis can be combined to
which is recognized as the variablecoefficient Laplace operator in one Cartesian coordinate. The spherically symmetric wave equation in terms of \(v(r,t)\) now becomes
In the case of constant wave velocity c, this equation reduces to the wave equation in a single Cartesian coordinate called r:
That is, any program for solving the onedimensional wave equation in a Cartesian coordinate system can be used to solve (2.150), provided the source term is multiplied by the coordinate, and that we divide the Cartesian mesh solution by r to get the spherically symmetric solution. Moreover, if r = 0 is included in the domain, spherical symmetry demands that \(\partial u/\partial r=0\) at r = 0, which means that
For this to hold in the limit \(r\rightarrow 0\), we must have \(v(0,t)=0\) at least as a necessary condition. In most practical applications, we exclude r = 0 from the domain and assume that some boundary condition is assigned at r = ϵ, for some ϵ > 0.
2.14.7 The Linear Shallow Water Equations
The next example considers water waves whose wavelengths are much larger than the depth 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 underwater slides (or a combination, as earthquakes frequently release avalanches of masses). For example, a subsea earthquake will normally have an extension of many kilometers but lift the 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 meters is very small. The water is essentially a thin film, and mathematically we can average the problem in the vertical direction and approximate the 3D wave phenomenon by 2D PDEs. Instead of a moving water domain in three space dimensions, we get a horizontal 2D domain with an unknown function for the surface elevation and the water depth as a variable coefficient in the PDEs.
Let \(\eta(x,y,t)\) be the elevation of the water surface, \(H(x,y)\) the water depth corresponding to a flat surface (η = 0), \(u(x,y,t)\) and \(v(x,y,t)\) the depthaveraged horizontal velocities of the water. Mass and momentum balance of the water volume give rise to the PDEs involving these quantities:
where g is the acceleration of gravity. Equation (2.151) corresponds to mass balance while the other two are derived from momentum balance (Newton’s second law).
The initial conditions associated with (2.151)–(2.153) are η, u, and v prescribed at t = 0. A common condition is to have some water elevation \(\eta=I(x,y)\) and assume that the surface is at rest: u = v = 0. A subsea earthquake usually means a sufficiently rapid motion of the bottom and the water volume to say that the bottom deformation is mirrored at the water surface as an initial lift \(I(x,y)\) and that u = v = 0.
Boundary conditions may be η prescribed for incoming, known waves, or zero normal velocity at reflecting boundaries (steep mountains, for instance): \(un_{x}+vn_{y}=0\), where \((n_{x},n_{y})\) is the outward unit normal to the boundary. More sophisticated boundary conditions are needed when waves run up at the shore, and at open boundaries where we want the waves to leave the computational domain undisturbed.
Equations (2.151), (2.152), and (2.153) can be transformed to a standard, linear wave equation. First, multiply (2.152) and (2.153) by H, differentiate (2.152) with respect to x and (2.153) with respect to y. Second, differentiate (2.151) with respect to t and use that \((Hu)_{xt}=(Hu_{t})_{x}\) and \((Hv)_{yt}=(Hv_{t})_{y}\) when H is independent of t. Third, eliminate \((Hu_{t})_{x}\) and \((Hv_{t})_{y}\) with the aid of the other two differentiated equations. These manipulations result in a standard, linear wave equation for η:
In the case we have an initial nonflat water surface at rest, the initial conditions become \(\eta=I(x,y)\) and \(\eta_{t}=0\). The latter follows from (2.151) 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 (2.151)–(2.153) can be extended to handle a timevarying bottom topography, which is relevant for modeling long waves generated by underwater slides. In such cases the water depth function H is also a function of t, due to the moving slide, and one must add a timederivative term H _{ t } to the lefthand side of (2.151). A moving bottom is best described by introducing \(z=H_{0}\) as the stillwater level, \(z=B(x,y,t)\) as the time and spacevarying bottom topography, so that \(H=H_{0}B(x,y,t)\). In the elimination of u and v one may assume that the dependence of H on t can be neglected in the terms \((Hu)_{xt}\) and \((Hv)_{yt}\). We then end up with a source term in (2.154), because of the moving (accelerating) bottom:
The reduction of (2.155) to 1D, for long waves in a straight channel, or for approximately plane waves in the ocean, is trivial by assuming no change in y direction (\(\partial/\partial y=0\)):
Wind drag on the surface
Surface waves are influenced by the drag of the wind, and if the wind velocity some meters above the surface is \((U,V)\), the wind drag gives contributions \(C_{V}\sqrt{U^{2}+V^{2}}U\) and \(C_{V}\sqrt{U^{2}+V^{2}}V\) to (2.152) and (2.153), respectively, on the righthand sides.
Bottom drag
The waves will experience a drag from the bottom, often roughly modeled by a term similar to the wind drag: \(C_{B}\sqrt{u^{2}+v^{2}}u\) on the righthand side of (2.152) and \(C_{B}\sqrt{u^{2}+v^{2}}v\) on the righthand side of (2.153). Note that in this case the PDEs (2.152) and (2.153) become nonlinear and the elimination of u and v to arrive at a 2ndorder wave equation for η is not possible anymore.
Effect of the Earth’s rotation
Long geophysical waves will often be affected by the rotation of the Earth because of the Coriolis force. This force gives rise to a term fv on the righthand side of (2.152) and −fu on the righthand side of (2.153). Also in this case one cannot eliminate u and v to work with a single equation for η. The Coriolis parameter is \(f=2\Omega\sin\phi\), where Ω is the angular velocity of the earth and ϕ is the latitude.
2.14.8 Waves in Blood Vessels
The flow of blood in our bodies is basically fluid flow in a network of pipes. Unlike rigid pipes, 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 the flow of blood and the deformation of the vessel wall results in waves traveling along our blood vessels.
A model for onedimensional 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 and assume that all cross sections are circular, though with different radii \(R(x,t)\). The main quantities to compute is the cross section area \(A(x,t)\), the averaged pressure \(P(x,t)\), and the total volume flux \(Q(x,t)\). The area of this cross section is
Let \(v_{x}(x,t)\) be the velocity of blood averaged over the cross section at point x. The volume flux, being the total volume of blood passing a cross section per time unit, becomes
Mass balance and Newton’s second law lead to the PDEs
where γ is a parameter related to the velocity profile, \(\varrho\) is the density of blood, and μ is the dynamic viscosity of blood.
We have three unknowns A, Q, and P, and two equations (2.159) and (2.160). A third equation is needed to relate the flow to the deformations of the wall. A common form for this equation is
where C is the compliance of the wall, given by the constitutive relation
which requires a relationship between A and P. One common model is to view the vessel wall, locally, as a thin elastic tube subject to an internal pressure. This gives the relation
where P _{0} and A _{0} are corresponding reference values when the wall is not deformed, h is the thickness of the wall, and E and ν are Young’s modulus and Poisson’s ratio of the elastic material in the wall. The derivative becomes
Another (nonlinear) deformation model of the wall, which has a better fit with experiments, is
where β is some parameter to be estimated. This law leads to
Reduction to the standard wave equation
It is not uncommon to neglect the viscous term on the righthand side of (2.160) and also the quadratic term with Q ^{2} on the lefthand side. The reduced equations (2.160) and (2.161) form a firstorder linear wave equation system:
These can be combined into standard 1D wave PDE by differentiating the first equation with respect to t and the second with respect to x,
which can be approximated by
where the A and C in the expression for c are taken as constant reference values.
2.14.9 Electromagnetic Waves
Light and radio waves are governed by standard wave equations arising from Maxwell’s general equations. When there are no charges and no currents, as in a vacuum, Maxwell’s equations take the form
where \(\epsilon_{0}=8.854187817620\cdot 10^{12}\) (F/m) is the permittivity of free space, also known as the electric constant, and \(\mu_{0}=1.2566370614\cdot 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 identity
gives the wave equation governing the electric and magnetic field:
with \(c=1/\sqrt{\mu_{0}\epsilon_{0}}\) as the velocity of light. Each component of E and B fulfills a wave equation and can hence be solved independently.
2.15 Exercises
Exercise 2.20 (Simulate waves on a nonhomogeneous string)
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.
Hint
According to Sect. 2.14.1, the density enters the mathematical model as \(\varrho\) in \(\varrho u_{tt}=Tu_{xx}\), 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. A value for the tension may be 150 N. Corresponding density values can be computed from the wave velocity estimations in the guitar function in the wave1D_u0v.py file.
Filename: wave1D_u0_sv_discont.
Exercise 2.21 (Simulate damped waves on a string)
Formulate a mathematical model for damped waves on a string. Use data from Sect. 2.3.6, and tune the damping parameter so that the string is very close to the rest state after 15 s. Make a movie of the wave motion.
Filename: wave1D_u0_sv_damping.
Exercise 2.22 (Simulate elastic waves in a rod)
A hammer hits the end of an elastic rod. The exercise is to simulate the resulting wave motion using the model (2.128) from Sect. 2.14.2. Let the rod have length L and let the boundary x = L be stress free so that \(\sigma_{xx}=0\), implying that \(\partial u/\partial x=0\). The left end x = 0 is subject to a strong stress pulse (the hammer), modeled as
The corresponding condition on u becomes \(u_{x}=S/E\) for \(t\leq t_{s}\) and zero afterwards (recall that \(\sigma_{xx}=Eu_{x}\)). This is a nonhomogeneous Neumann condition, and you will need to approximate this condition and combine it with the scheme (the ideas and manipulations follow closely the handling of a nonzero initial condition \(u_{t}=V\) in wave PDEs or the corresponding secondorder ODEs for vibrations).
Filename: wave_rod.
Exercise 2.23 (Simulate spherical waves)
Implement a model for spherically symmetric waves using the method described in Sect. 2.14.6. The boundary condition at r = 0 must be \(\partial u/\partial r=0\), while the condition at r = R can either be u = 0 or a radiation condition as described in Problem 2.12. 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 the case where the source term is located around r = 0 and sends out pulses
Here, Q and ω are constants to be chosen.
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 to compute u[1:] = v[1:]/r[1:] and then set u[0]=u[1]. The latter makes it evident that \(\partial u/\partial r=0\) in a plot.
Filename: wave1D_spherical.
Problem 2.24 (Earthquakegenerated tsunami over a subsea hill)
A subsea earthquake leads to an immediate lift of the water surface, see Fig. 2.11. The lifted water surface splits into two tsunamis, one traveling to the right and one to the left, as depicted in Fig. 2.12. Since tsunamis are normally very long waves, compared to the depth, with a small amplitude, compared to the wave length, a standard wave equation is relevant:
where η is the elevation of the water surface, g is the acceleration of gravity, and H(x) is the still water depth.
To simulate the rightgoing tsunami, we can impose a symmetry boundary at x = 0: \(\partial\eta/\partial x=0\). We then simulate the wave motion in \([0,L]\). Unless the ocean ends at x = L, the waves should travel undisturbed through the boundary x = L. A radiation condition as explained in Problem 2.12 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 \(\eta_{i}^{n}> \epsilon\), \(i=N_{x}1\), is a possibility (ϵ is a small parameter).
The shape of the initial surface can be taken as a Gaussian function,
with \(I_{m}=0\) reflecting the location of the peak of I(x) and I _{ s } being a measure of the width of the function I(x) (I _{ s } is \(\sqrt{2}\) times the standard deviation of the familiar normal distribution curve).
Now we extend the problem with a hill at the sea bottom, see Fig. 2.13. The wave speed \(c=\sqrt{gH(x)}=\sqrt{g(H_{0}B(x))}\) will then be reduced in the shallow water above the hill.
One possible form of the hill is a Gaussian function,
but many other shapes are also possible, e.g., a ″cosine hat″ where
when \(x\in[B_{m}B_{s},B_{m}+B_{s}]\) while \(B=B_{0}\) outside this interval.
Also an abrupt construction may be tried:
for \(x\in[B_{m}B_{s},B_{m}+B_{s}]\) while \(B=B_{0}\) outside this interval.
The wave1D_dn_vc.py program can be used as starting point for the implementation. Visualize both the bottom topography and the water surface elevation in the same plot. Allow for a flexible choice of bottom shape: (2.171), (2.172), (2.173), or \(B(x)=B_{0}\) (flat).
The purpose of this problem is to explore the quality of the numerical solution \(\eta^{n}_{i}\) for different shapes of the bottom obstruction. The ‘‘cosine hat’’ and the boxshaped hills have abrupt changes in the derivative of H(x) and are more likely to generate numerical noise than the smooth Gaussian shape of the hill. Investigate if this is true.
Filename: tsunami1D_hill.
Problem 2.25 (Earthquakegenerated tsunami over a 3D hill)
This problem extends Problem 2.24 to a threedimensional wave phenomenon, governed by the 2D PDE
We assume that the earthquake arises from a fault along the line x = 0 in the xyplane so that the initial lift of the surface can be taken as I(x) in Problem 2.24. That is, a plane wave is propagating to the right, but will experience bending because of the bottom.
The bottom shape is now a function of x and y. An ‘‘elliptic’’ Gaussian function in two dimensions, with its peak at \((B_{mx},B_{my})\), generalizes (2.171):
where b is a scaling parameter: b = 1 gives a circular Gaussian function with circular contour lines, while b ≠ 1 gives an elliptic shape with elliptic contour lines. To indicate the input parameters in the model, we may write
The ‘‘cosine hat’’ (2.172) can also be generalized to
when \(0\leq\sqrt{x^{2}+y^{2}}\leq B_{s}\) and \(B=B_{0}\) outside this circle.
A boxshaped obstacle means that
for x and y inside a rectangle
and \(B=B_{0}\) outside this rectangle. The b parameter controls the rectangular shape of the cross section of the box.
Note that the initial condition and the listed bottom shapes are symmetric around the line \(y=B_{my}\). We therefore expect the surface elevation also to be symmetric with respect to this line. This means that we can halve the computational domain by working with \([0,L_{x}]\times[0,B_{my}]\). Along the upper boundary, \(y=B_{my}\), we must impose the symmetry condition \(\partial\eta/\partial n=0\). Such a symmetry condition (\(\eta_{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 (which becomes \(\eta_{y}=0\)). The wave motion is to be simulated until the wave hits the reflecting boundaries where \(\partial\eta/\partial n=\eta_{x}=0\) (one can also set η = 0  the particular condition does not matter as long as the simulation is stopped before the wave is influenced by the boundary condition).
Visualize the surface elevation. Investigate how different hill shapes, different sizes of the water gap above the hill, and different resolutions \(\Delta x=\Delta y=h\) and \(\Delta t\) influence the numerical quality of the solution.
Filename: tsunami2D_hill.
Problem 2.26 (Investigate Mayavi for visualization)
Play with Mayavi code for visualizing 2D solutions of the wave equation with variable wave velocity. See if there are effective ways to visualize both the solution and the wave velocity scalar field at the same time.
Filename: tsunami2D_hill_mlab.
Problem 2.27 (Investigate visualization packages)
Create some fancy 3D visualization of the water waves and the subsea hill in Problem 2.25. Try to make the hill transparent. Possible visualization tools are Mayavi ^{Footnote 16}, Paraview ^{Footnote 17}, and OpenDX ^{Footnote 18}.
Filename: tsunami2D_hill_viz.
Problem 2.28 (Implement loops in compiled languages)
Extend the program from Problem 2.25 such that the loops over mesh points, inside the time loop, are implemented in compiled languages. Consider implementations in Cython, Fortran via 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 is often advantageous to normalize CPU times by the fastest method on a given mesh.
Filename: tsunami2D_hill_compiled.
Exercise 2.29 (Simulate seismic waves in 2D)
The goal of this exercise is to simulate seismic waves using the PDE model (2.138) in a 2D xz domain with geological layers. Introduce m horizontal layers of thickness h _{ i }, \(i=0,\ldots,m1\). Inside layer number i we have a vertical wave velocity c _{z,i} and a horizontal wave velocity c _{h,i}. Make a program for simulating such 2D waves. Test it on a case with 3 layers where
Let s be a localized point source at the middle of the Earth’s surface (the upper boundary) and 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 from the expanding wave so that the boundary conditions do not disturb the wave. Then the type of boundary condition does not matter, except that we physically need to have \(p=p_{0}\), where p _{0} is the atmospheric pressure, at the upper boundary.
Filename: seismic2D.
Project 2.30 (Model 3D acoustic waves in a room)
The equation for sound waves in air is derived in Sect. 2.14.5 and reads
where \(p(x,y,z,t)\) is the pressure and c is the speed of sound, taken as 340 m/s. However, sound is absorbed in the air due to relaxation of molecules in the gas. A model for simple relaxation, valid for gases consisting only of one type of molecules, is a term \(c^{2}\tau_{s}\nabla^{2}p_{t}\) in the PDE, where τ_{ s } is the relaxation time. If we generate sound from, e.g., a loudspeaker in the room, this sound source must also be added to the governing equation.
The PDE with the mentioned type of damping and source then becomes
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 physical wall) outside the room where c is changed such that some of the wave energy is reflected and some is absorbed in the wall. The absorption of energy can be taken care of by adding a damping term bp _{ t } in the equation:
Typically, b = 0 in the room and b > 0 in the wall. A discontinuity in b or c will give rise to reflections. It can be wise to use a constant c in the wall to control reflections because of the 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 the condition p = 0 or \(\partial p/\partial n=0\) can be imposed. The waves should anyway be approximately dampened to p = 0 this far out in the wall layer.
There are two strategies for discretizing the \(\nabla^{2}p_{t}\) term: using a center difference between times n + 1 and n − 1 (if the equation is sampled at level n), or use a onesided difference based on levels n and n − 1. The latter has the advantage of not leading to any equation system, while the former is secondorder accurate as the scheme for the simple wave equation \(p_{t}t=c^{2}\nabla^{2}p\). To avoid an equation system, go for the onesided difference such that the overall scheme becomes 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 the method of manufactured solutions. Make some demonstrations where the wall reflects and 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.
Project 2.31 (Solve a 1D transport equation)
We shall study the wave equation
with initial condition
and one periodic boundary condition
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 the spatial derivative is of first order only.
Physical interpretation
The parameter c can be constant or variable, \(c=c(x)\). The equation (2.180) arises in transport problems where a quantity u, which could be temperature or concentration of some contaminant, is transported with the velocity c of a fluid. In addition to the transport imposed by ‘‘travelling with the fluid’’, u may also be transported by diffusion (such as heat conduction or Fickian diffusion), but we have in the model \(u_{t}+cu_{x}\) assumed that diffusion effects are negligible, which they often are.

a)
Show that under the assumption of \(a=\hbox{const}\),
$$u(x,t)=I(xct)$$(2.183)fulfills the PDE as well as the initial and boundary condition (provided \(I(0)=I(L)\)).
A widely used numerical scheme for (2.180) applies a forward difference in time and a backward difference in space when c > 0:
$$[D_{t}^{+}u+cD_{x}^{}u=0]_{i}^{n}\thinspace.$$(2.184)For c < 0 we use a forward difference in space: \([cD_{x}^{+}u]_{i}^{n}\).

b)
Set up a computational algorithm and implement it in a function. Assume a is constant and positive.

c)
Test the implementation by using the remarkable property that the numerical solution is exact at the mesh points if \(\Delta t=c^{1}\Delta x\).

d)
Make a movie comparing the numerical and exact solution for the following two choices of initial conditions:
$$I(x)=\left[\sin\left(\pi\frac{x}{L}\right)\right]^{2n}$$(2.185)where n is an integer, typically n = 5, and
$$I(x)=\exp{\left(\frac{(xL/2)^{2}}{2\sigma 2}\right)}\thinspace.$$(2.186)Choose \(\Delta t=c^{1}\Delta x,0.9c^{1}\Delta x,0.5c^{1}\Delta x\).

e)
The performance of the suggested numerical scheme can be investigated by analyzing the numerical dispersion relation. Analytically, we have that the Fourier component
$$u(x,t)=e^{i(kx\omega t)},$$is a solution of the PDE if ω = kc. This is the analytical dispersion relation. A complete solution 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 series.
A similar discrete Fourier component at \((x_{p},t_{n})\) is
$$u_{p}^{q}=e^{i(kp\Delta x\tilde{\omega}n\Delta t)},$$where in general \(\tilde{\omega}\) is a function of k, \(\Delta t\), and \(\Delta x\), and differs from the exact ω = kc.
Insert the discrete Fourier component in the numerical scheme and derive an expression for \(\tilde{\omega}\), i.e., the discrete dispersion relation. Show in particular that if \(\Delta t/(c\Delta x)=1\), the discrete solution coincides with the exact solution at the mesh points, regardless of the mesh resolution (!). Show that if the stability condition
$$\frac{\Delta t}{c\Delta x}\leq 1,$$the discrete Fourier component cannot grow (i.e., \(\tilde{\omega}\) is real).

f)
Write a test for your implementation where you try to use information from the numerical dispersion relation.
We shall hereafter assume that \(c(x)> 0\).

g)
Set up a computational algorithm for the variable coefficient case and implement it in a function. Make a test that the function works for constant a.

h)
It can be shown that for an observer moving with velocity c(x), u is constant. This can be used to derive an exact solution when a varies with x. Show first that
$$u(x,t)=f(C(x)t),$$(2.187)where
$$C^{\prime}(x)=\frac{1}{c(x)},$$is a solution of (2.180) for any differentiable function f.

i)
Use the initial condition to show that an exact solution is
$$u(x,t)=I(C^{1}(C(x)t)),$$with C ^{−1} being the inverse function of \(C=\int c^{1}dx\). Since C(x) is an integral \(\int_{0}^{x}(1/c)dx\), C(x) is monotonically increasing and there exists hence an inverse function C ^{−1} with values in \([0,L]\).
To compute (2.187) we need to integrate 1 ∕ c to obtain C and then compute the inverse of C.
The inverse function computation can be easily done if we first think discretely. Say we have some function \(y=g(x)\) and seek its inverse. Plotting \((x_{i},y_{i})\), where \(y_{i}=g(x_{i})\) for some mesh points x _{ i }, displays g as a function of x. The inverse function is simply x as a function of g, i.e., the curve with points \((y_{i},x_{i})\). 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 linear variation (known as linear interpolation) between the points (which actually means to draw straight lines between the points, exactly as done by a plotting program).
The function wrap2callable in scitools.std can take a set of points and return a continuous 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
To compute C(x) we need to integrate 1 ∕ c, which can be done by a Trapezoidal rule. Suppose we have computed \(C(x_{i})\) and need to compute \(C(x_{i+1})\). Using the Trapezoidal rule with m subintervals over the integration domain \([x_{i},x_{i+1}]\) gives
$$C(x_{i+1})=C(x_{i})+\int_{x_{i}}^{x_{i+1}}\frac{dx}{c}\approx h\left(\frac{1}{2}\frac{1}{c(x_{i})}+\frac{1}{2}\frac{1}{c(x_{i+1})}+\sum_{j=1}^{m1}\frac{1}{c(x_{i}+jh)}\right),$$(2.188)where \(h=(x_{i+1}x_{i})/m\) is the length of the subintervals used for the integral over \([x_{i},x_{i+1}]\). We observe that (2.188) is a difference equation which we can solve by repeatedly applying (2.188) for \(i=0,1,\ldots,N_{x}1\) if a mesh \(x_{0},x_{,}\ldots,x_{N_{x}}\) is prescribed. Note that \(C(0)=0\).

j)
Implement a function for computing \(C(x_{i})\) and one for computing \(C^{1}(x)\) for any x. Use these two functions for computing the exact solution \(I(C^{1}(C(x)t))\). End up with a function u_exact_variable_c(x, n, c, I) that returns the value of \(I(C^{1}(C(x)t_{n}))\).

k)
Make movies showing a comparison of the numerical and exact solutions for the two initial conditions (2.185) and (2.15). Choose \(\Delta t=\Delta x/\max_{0,L}c(x)\) and the velocity of the medium as

a)
\(c(x)=1+\epsilon\sin(k\pi x/L)\), ϵ < 1,
 b)
The PDE \(u_{t}+cu_{x}=0\) expresses that the initial condition I(x) is transported with velocity c(x).

a)
Filename: advec1D.
Problem 2.32 (General analytical solution of a 1D damped wave equation)
We consider an initialboundary value problem for the damped wave equation:
Here, b ≥ 0 and c are given constants. The aim is to derive a general analytical solution of this problem. Familiarity with the method of separation of variables for solving PDEs will be assumed.

a)
Seek a solution on the form \(u(x,t)=X(x)T(t)\). Insert this solution in the PDE and show that it leads to two differential equations for X and T:
$$T^{\prime\prime}+bT^{\prime}+\lambda T=0,\quad c^{2}X^{\prime\prime}+\lambda X=0,$$with \(X(0)=X(L)=0\) as boundary conditions, and λ as a constant to be determined.

b)
Show that X(x) is on the form
$$X_{n}(x)=C_{n}\sin kx,\quad k=\frac{n\pi}{L},\quad n=1,2,\ldots$$where C _{ n } is an arbitrary constant.

c)
Under the assumption that \((b/2)^{2}<k^{2}\), show that T(t) is on the form
$$T_{n}(t)=e^{{\frac{1}{2}}bt}(a_{n}\cos\omega t+b_{n}\sin\omega t),\quad\omega=\sqrt{k^{2}\frac{1}{4}b^{2}},\quad n=1,2,\ldots$$The complete solution is then
$$u(x,t)=\sum_{n=1}^{\infty}\sin kxe^{{\frac{1}{2}}bt}(A_{n}\cos\omega t+B_{n}\sin\omega t),$$where the constants A _{ n } and B _{ n } must be computed from the initial conditions.

d)
Derive a formula for A _{ n } from \(u(x,0)=I(x)\) and developing I(x) as a sine Fourier series on \([0,L]\).

e)
Derive a formula for B _{ n } from \(u_{t}(x,0)=V(x)\) and developing V(x) as a sine Fourier series on \([0,L]\).

f)
Calculate A _{ n } and B _{ n } from vibrations of a string where \(V(x)=0\) and
$$I(x)=\left\{\begin{array}[]{ll}ax/x_{0},&x<x_{0},\\ a(Lx)/(Lx_{0}),&\hbox{otherwise}\thinspace.\end{array}\right.$$(2.189) 
g)
Implement a function u_series(x, t, tol=1E10) for the series for \(u(x,t)\), where tol is a tolerance for truncating the series. Simply sum the terms until \(a_{n}\) and \(b_{b}\) both are less than tol.

h)
What will change in the derivation of the analytical solution if we have \(u_{x}(0,t)=u_{x}(L,t)=0\) as boundary conditions? And how will you solve the problem with \(u(0,t)=0\) and \(u_{x}(L,t)=0\)?
Filename: damped_wave1D.
Problem 2.33 (General analytical solution of a 2D damped wave equation)
Carry out Problem 2.32 in the 2D case: \(u_{tt}+bu_{t}=c^{2}(u_{xx}+u_{yy})\), where \((x,y)\in(0,L_{x})\times(0,L_{y})\). Assume a solution on the form \(u(x,y,t)=X(x)Y(y)T(t)\).
Filename: damped_wave2D.
Notes
 1.
 2.
 3.
 4.
 5.
 6.
 7.
 8.
 9.
 10.
 11.
 12.
 13.
 14.
 15.
 16.
 17.
 18.
Author information
Authors and Affiliations
Rights and permissions
This chapter is published under an open access license. Please check the 'Copyright Information' section either on this page or in the PDF for details of this license and what reuse is permitted. If your intended use exceeds what is permitted by the license or if you are unable to locate the licence and reuse information, please contact the Rights and Permissions team.
Copyright information
© 2017 The Author(s)
About this chapter
Cite this chapter
Linge, S., Langtangen, H.P. (2017). Wave Equations. In: Finite Difference Computing with PDEs. Texts in Computational Science and Engineering, vol 16. Springer, Cham. https://doi.org/10.1007/9783319554563_2
Download citation
DOI: https://doi.org/10.1007/9783319554563_2
Published:
Publisher Name: Springer, Cham
Print ISBN: 9783319554556
Online ISBN: 9783319554563
eBook Packages: Mathematics and StatisticsMathematics and Statistics (R0)