2.1 Simulation of Waves on a String
2.1.1 Discretizing the Domain
2.1.2 The Discrete Solution
2.1.3 Fulfilling the Equation at the Mesh Points
2.1.4 Replacing Derivatives by Finite Differences
2.1.5 Formulating a Recursive Algorithm
2.1.6 Sketch of an Implementation
u[i]
to store \(u^{n+1}_{i}\), u_n[i]
to store \(u^{n}_{i}\), and u_nm1[i]
to store \(u^{n-1}_{i}\).2.2 Verification
2.2.1 A Slightly Generalized Model Problem
2.2.2 Using an Analytical Solution of Physical Significance
2.2.3 Manufactured Solution and Estimation of Convergence Rates
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())
.2.2.4 Constructing an Exact Solution of the Discrete Equations
2.3 Implementation
2.3.1 Callback Function for User-Specific Actions
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 user-supplied functionu
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
.user_action
functions.True
. For example,2.3.2 The Solver Function
-
Although we give
dt
and computedx
viaC
andc
, the resultingt
andx
meshes do not necessarily correspond exactly to these values because of rounding errors. To explicitly ensure thatdx
anddt
correspond to the cell sizes inx
andt
, 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 abreak
statement inside the for loop in the solver.
2.3.3 Verification: Exact Quadratic Solution
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
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: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
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.viz
functionuser_action
callback function for plotting the solution at each time level,solver
function, andviz
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 two-dimensional problems as Gnuplot is significantly faster than Matplotlib for screen animations.
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 easier-to-understand implementation of the user action function, see Sect. 2.8.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.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
.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.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.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 stand-alone 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
.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:n=0
) is included by n % skip_frame == 0
, as well as every skip_frame
-th frame. As n % skip_frame == 0
will very seldom be true for the very final frame, we must also check if n == t.size-1
to get the final frame included.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
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\).)viz
in this application goes as follows:wave1D_u0.py
. Run the program and watch the movie of the vibrating string
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
a=c=L=1
, x0=0.8
, and there is no need to calculate with wavelengths and frequencies to estimate c!2.4 Vectorization
2.4.1 Operations on Slices of Arrays
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}\):d
can therefore alternatively be done by subtracting the array \((u_{0},u_{1},\ldots,u_{n-1})\) 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:n-1]
, 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:
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.u
, say with five elements, and simulate with pen and paper both the loop version and the vectorized version above.n-2
:u2
becomes n-2
. 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 writenumpy
package performs (behind the scenes) the first line above in four steps:u[1:n]
has wrong length (n-1
) 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.u2
has length n
, is to forget the slice in the array on the left-hand side,u2
becomes a new array of length n-2
, which is the wrong length as we have no entries for the boundary values. We meant to insert the right-hand side array into the original u2
array for the entries that correspond to the internal points in the mesh (1:n-1
or 1:-1
).u2
, u
, and x
all have length n
, the vectorized version becomesf
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
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
user_action
function that compares the computed and exact solution at each time level and performs a test:2.4.4 Efficiency Measurements
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.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!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
: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,f
is always called with c=3
, i.e., f
has only two ‘‘free’’ arguments a
and b
. This functionality is obtained byf2
calls f
with whatever the user supplies as a
and b
, but c
is always 3
.viz
code, we can doscalar_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’
.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
u_nm1
and u_n
arrays such that they have the right content for the next time step:u_n
first, makes u_nm1
equal to u
, which is wrong!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.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 likeu_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.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.tmp
by writingu_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
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.wave_standing
.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 two-dimensional numpy
array.wave1D_u0_s_store
.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).wave1D_u0_s2c
.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.wave_numerics_comparison
.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 likesolver
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.user_action
, the solver
function issues a yield
statement, which is a kind of return
statement: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.solver
function as a generator, and plot the solution at each time step.wave1D_u0_generator
.numpy.cumsum
) operation to compute the accumulative sum: numpy.cumsum([1,3,5])
is [1,4,9]
.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:mesh_calculus_1D
.2.6 Generalization: Reflecting Boundaries
2.6.1 Neumann Boundary Condition
2.6.2 Discretization of Derivatives at the Boundary
2.6.3 Implementation of Neumann Conditions
u[i-1]
by u[i+1]
and vice versa. This is achieved by having the indices i+1
and i-1
as variables ip1
(i
plus 1) and im1
(i
minus 1), respectively. At the boundary we can easily define im1=i+1
while we use im1=i-1
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):wave1D_n0.py
contains a complete implementation of the 1D wave equation with boundary conditions \(u_{x}=0\) at x = 0 and x = L.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
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]
|
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).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)\)
-
A rectangular plug-shaped 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
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}\).2.6.6 Alternative Implementation via Ghost Cells
u
array now needs extra elements corresponding to the ghost points. Two new point values are needed:u_n
and u_nm1
must be defined accordingly.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 u
with proper length and Ix
to be the corresponding indices for the real physical mesh points (\(1,2,\ldots,N_{x}+1\)):Ix[0]
and Ix[-1]
(as before). We first update the solution at all physical mesh points (i.e., interior points in the mesh):V(x)
and f(x, t)
, as we must remember that the appropriate x coordinate is given as x[i-Ix[0]]
: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: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
.x
be the physical mesh points,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 isf(x[i], t[n])
is incorrect if x
is defined to be the physical points, so x[i]
must be replaced by x[i-Ix[0]]
.x
also cover the ghost points such that u[i]
is the value at x[i]
.u[1:]
or (as always) u[Ix[0]:
Ix[-1]+1]
.2.7 Generalization: Variable Wave Velocity
2.7.1 The Model PDE with a Variable Coefficient
2.7.2 Discretizing the Variable Coefficient
2.7.3 Computing the Coefficient Between Mesh Points
2.7.4 How a Variable Coefficient Affects the Stability
2.7.5 Neumann Condition and a Variable Coefficient
2.7.6 Implementation of Variable Coefficients
q[i]
at the spatial mesh points. The following loop is a straightforward implementation of the scheme (2.50):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.ip1=i+1
and im1=i-1
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.2.7.7 A More General PDE Model with Variable Coefficients
2.7.8 Generalization: Damping
2.8 Building a General 1D Wave Equation Solver
wave1D_dn_vc.py
is a fairly general code for 1D wave propagation problems that targets the following initial-boundary value problem 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\)), time-varying 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.2.8.1 User Action Function as a Class
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.wave1D_u0.viz
did, can be coded as follows:backend=None
) or SciTools (backend=matplotlib
or backend=
gnuplot
) for maximum flexibility.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.__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.2.8.2 Pulse Propagation in Two Media
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.plug
),gaussian
),cosinehat
),half-cosinehat
)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 right-going pulse is generated. There is also a left-going pulse, but it travels from x = 0 in negative x direction and is not visible in the domain \([0,L]\).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: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.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).pulse
function:2.9 Exercises
damped_waves
.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}\).wave1D_symmetric
.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.pulse1D
.pulse1D_analysis
.pulse1D_harmonic
.wave1D_dn.py
.wave1D_open_BC
.periodic
.Neumann_discr
.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\).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 lower-order 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).solver
function in wave1D_n0.py
. Both approaches are explained in the subexercises.sympy
to perform analytical computations. A starting point is to define u as follows:u(x,t)
with x
and t
as sympy
symbols.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
:a[2:]
:a
contains computed values and u
will automatically use these new values since X
accesses a
.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.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’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).sympy
code computes this u: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.wave1D_n0_test_cubic
.2.10 Analysis of the Difference Equations
2.10.1 Properties of the Solution of the Wave Equation
2.10.2 More Precise Definition of Fourier Representations
2.10.3 Stability
-
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}\))?
2.10.4 Numerical Dispersion Relation
.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).rs_error_leading_order
expression above, we see that the leading order term in the error of this series expansion is 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
-
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.
2.11 Finite Difference Methods for 2D and 3D Wave Equations
2.11.1 Multi-Dimensional Wave Equations
2.11.2 Mesh
2.11.3 Discretization
2.12 Implementation
2.12.1 Scalar Computations
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
.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 multi-dimensional 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 thenu[i,j]
, \(u^{n}_{i,j}\) to u_n[i,j]
, and \(u^{n-1}_{i,j}\) to u_nm1[i,j]
.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: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.step1
variable has been introduced to allow the formula to be reused for the first step, computing \(u^{1}_{i,j}\):advance_scalar
function to speed up the code since most of the CPU time in simulations is spent in this function.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
I(x,y)
and f(x,y,t)
, respectively. Having the one-dimensional 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
: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.xv
and yv
arrays for vectorized computing, setting the initial condition is just a matter ofu_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}\):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).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.u
(above), the function f
is first computed as an array over all mesh points: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.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.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
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
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: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):
mlab
. We can doplt
(as usual) or mlab
as a kind of MATLAB visualization access inside our program (just more powerful and with higher visual quality).mlab
module is provided in two places, one for the basic functionality
12 and one for further functionality
13. Basic figure handling
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.ffmpeg
to create videos.2.13 Exercises
check_quadratic_solution
.df[d,i,j]
where d
represents the direction of the derivative, and i,j
is a mesh point in 2D. Use centered differences for the derivative at inner points and one-sided forward or backward differences at the boundary points. Construct unit tests and write a corresponding test function.mesh_calculus_2D
.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).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).wave2D_dn
.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, Cython-compiled loops, Fortran-compiled loops, and C-compiled loops. Normalize the CPU time for each mesh by the fastest version.wave3D_u0
.2.14 Applications of Wave Equations
2.14.1 Waves on a String
2.14.2 Elastic Waves in a Rod
2.14.3 Waves on a Membrane
2.14.4 The Acoustic Model for Seismic Waves
2.14.5 Sound Waves in Liquids and Gases
2.14.6 Spherical Waves
2.14.7 The Linear Shallow Water Equations
2.14.8 Waves in Blood Vessels
2.14.9 Electromagnetic Waves
2.15 Exercises
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.wave1D_u0_sv_discont
.wave1D_u0_sv_damping
.wave_rod
.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.wave1D_spherical
.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).tsunami1D_hill
.tsunami2D_hill
.tsunami2D_hill_mlab
.tsunami2D_hill_viz
.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.tsunami2D_hill_compiled
.seismic2D
.acoustics
.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 byu_exact_variable_c(x, n, c, I)
that returns the value of \(I(C^{-1}(C(x)-t_{n}))\).advec1D
.u_series(x, t, tol=1E-10)
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
.damped_wave1D
.damped_wave2D
.