Wave Equation using Finite Differences and the Method of Lines
Authors: Oscar Reula, Joaquin Pelle, Pablo Montes
Click here to download this notebook
We will use the DifferentialEquations.jl
package. This notebook contains modifications of examples from the following two pages:
https://tutorials.sciml.ai/html/introduction/03-optimizingdiffeqcode.html
http://juliamatrices.github.io/BandedMatrices.jl/latest/#Creating-banded-matrices-1
Method of Lines
We will solve hyperbolic equations using the method of lines and finite difference approximations. This means that starting from a scalar system of the form
\[ u_t = u_x \]
we will initially approximate it using a finite difference operator $D_x$ in the spatial direction, yielding
\[ v_t = D_x\; v. \]
Here, $v$ is a version of $u$ discretized only in space. That is, if we take a grid of $N$ points, $v$ will be a vector of $N$ elements that depends continuously on time.
At this stage, we effectively have a system of $N$ ordinary differential equations, which we can approximate using a suitable ODE integrator. In this way, we arrive at a discretization in both space and time.
Wave Equation
We will solve the wave equation
\[ \phi_{tt} = \phi_{xx} \]
To express it in standard form, we define two new variables, $u := \phi_x$ and $v := \phi_t$. This system then becomes
\[ \begin{array}{rl} \phi_t & = & v \\ v_t & = & u_x \\ u_t & = & v_x, \end{array} \]
where we have used that $u_t := \phi_{xt} = \phi_{tx} := v_x$, and $v_t := \phi_{tt} = \phi_{xx} = u_x$.
Since the equation for $\phi$ can be integrated once $(u, v)$ are known, and $\phi$ is not necessary to solve the rest of the system, we will ignore it for now.
Diagonalization of the system
If we define the variables $u^{+} = u+v$ and $u^{-} = u-v$ we can obtain a diagonalized system,
\[ \begin{array}{rl} u^+_t & = & u^+_x \\ u^-_t & = & -u^-_x, \end{array} \]
where the solution consists of two independent waves, $u^{+}$ to the left and $u^{-}$ to the right:
\[ \begin{array}{rl} u^{+}(x,t) = u^{+}_0(x+t)\\ u^{-}(x,t) = u^{-}_0(x-t) \end{array} \]
Therefore, the solutions of the original system will be linear combinations of these two solutions, which depend on the initial data.
For example, if we take as initial data $\phi_0(x) = e^{-x^{2}}$ and $\phi_t(x, t=0) = 0$ with periodic boundary conditions on the interval $(-4, 4)$, the exact solution will be
using Plots N = 201 dx = 8.0/(N-1) x = range(-4.0, 4.0, length = N) # spatial values times = range(0, 8, length = 200) # temporal values function return_to_interval(xini::Number, xfin::Number, x) # This function takes a value x and returns it to the interval [xini, xfin) length = xfin - xini offset = x + xini factor = offset / length x = xini + (factor - floor(factor)) * length end function up_0(x) #(u^+)_0 x = return_to_interval(-4, 4, x) return -2 * x * exp(-x^2) end function um_0(x) #(u^-)_0 x = return_to_interval(-4, 4, x) return -2 * x * exp(-x^2) end anim = @animate for t in times up = up_0.(x .+ t) um = um_0.(x .- t) u = 0.5 * (up + um) v = 0.5 * (up - um) ϕ = [dx * sum(u[1:i]) for i in 1:N] ϕ = ϕ .- minimum(ϕ) lfs = 10 p1 = plot(x, ϕ, label = "\$ \\phi \$", ylim = (-0.1, 1.1), xlim = (-4, 4), xlabel = "\$x\$", legendfontsize = lfs) title!(p1, "Original System") p2 = plot(x, u, label = "\$u\$", legendfontsize = lfs) plot!(p2, x, v, label = "\$v\$", ylim = (-1, 1), xlim = (-4, 4), xlabel = "\$x\$", legendfontsize = lfs) title!(p2, "New System") p3 = plot(x, up, label = "\$u^{+}\$", legendfontsize = lfs) plot!(p3, x, um, label = "\$u^{-}\$", ylim = (-1, 1), xlim = (-4, 4), xlabel = "\$x\$", legendfontsize = lfs) title!(p3, "Diagonalized New System") plot(p1, p2, p3, layout = (3, 1), size = (400, 600)) end gif(anim, "wave_example.gif", fps = 30)
Estamos usando Julia, por lo que cargaremos algunos paquetes para manejar matrices, resolver ODEs y graficar.
using OrdinaryDiffEq using Plots using LinearAlgebra using BandedMatrices using SparseArrays
Now we add some parameters for the simulation. Some values are arbitrary, and you may try playing with them. $N$ is the number of points in the spatial discretization. We will solve the periodic problem, so if the grid starts at $1$ and ends at $N$, we identify the points $(N+1, N+2, \ldots)$ with $(1, 2, \ldots)$, and the points $(0, -1, \ldots)$ with $(N, N-1, \ldots)$.
N = 500 # Number of points in the spatial discretization L = 1.0 # Spatial interval dx = L/N # dx T = 10.0 # Final time dt = 1.0 * dx # We take dt ≈ dx/speed_max to satisfy the CFL condition and # ensure stability of the algorithm r0 = zeros(N, 2) # Discretization of the fields u and v x = [dx * i for i in 0:N-1]; # x coordinates, needed to define the initial data
dt
0.002
We now define the finite difference schemes. They are implemented as matrices that multiply the solution vectors. The matrices are defined as sparse for greater computational efficiency. In general, defining numerical derivatives using matrices is not efficient, but the cases we will work with are simple enough to be illustrative.
function create_D_2_per(N) D_2_per = sparse(Tridiagonal([-0.5 for i in 1:N-1],[0.0 for i in 1:N],[0.5 for i in 1:N-1])) D_2_per[1,end] = -0.5 D_2_per[end,1] = 0.5 dropzeros!(D_2_per) return D_2_per end function create_D2_2_per(N) D2_2_per = BandedMatrix{Float64}(Zeros(N,N), (N-1,N-1)) D2_2_per[band(0)] .= -2.0 D2_2_per[band(1)] .= 1.0 D2_2_per[band(-1)] .= 1.0 D2_2_per[band(N-1)] .= 1.0 D2_2_per[band(-N+1)] .= 1.0 D2_2_per = sparse(D2_2_per) dropzeros!(D2_2_per) return D2_2_per end function create_D_4_per(N) D_4_per = BandedMatrix{Float64}(Zeros(N,N), (N-1,N-1)) D_4_per[band(0)] .= 0.0 D_4_per[band(1)] .= 2.0/3.0 D_4_per[band(-1)] .= -2.0/3.0 D_4_per[band(2)] .= -1.0/12.0 D_4_per[band(-2)] .= 1.0/12.0 D_4_per[band(N-1)] .= -2.0/3.0 D_4_per[band(N-2)] .= 1.0/12.0 D_4_per[band(-N+1)] .= 2.0/3.0 D_4_per[band(-N+2)] .= -1.0/12.0 D_4_per = sparse(D_4_per) dropzeros!(D_4_per) return D_4_per end;
println("Second-order periodic approximation of the first derivative:") println(create_D_2_per(8)) println() println("Fourth-order periodic approximation of the first derivative:") println(round.(create_D_4_per(8), digits = 4))
Second-order periodic approximation of the first derivative: sparse([2, 8, 1, 3, 2, 4, 3, 5, 4, 6, 5, 7, 6, 8, 1, 7], [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8], [-0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, -0.5 , 0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5], 8, 8) Fourth-order periodic approximation of the first derivative: sparse([2, 3, 7, 8, 1, 3, 4, 8, 1, 2, 4, 5, 2, 3, 5, 6, 3, 4, 6, 7, 4, 5, 7 , 8, 1, 5, 6, 8, 1, 2, 6, 7], [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8], [-0.6667, 0.0833, -0.0 833, 0.6667, 0.6667, -0.6667, 0.0833, -0.0833, -0.0833, 0.6667, -0.6667, 0. 0833, -0.0833, 0.6667, -0.6667, 0.0833, -0.0833, 0.6667, -0.6667, 0.0833, - 0.0833, 0.6667, -0.6667, 0.0833, 0.0833, -0.0833, 0.6667, -0.6667, -0.6667, 0.0833, -0.0833, 0.6667], 8, 8)
We now define the right-hand side of the equations in the method of lines, that is, the spatial discretization.
function F!(dr, r, p, t) # second order version dx, D = p h = 1.0 / dx u = @view r[:,1] v = @view r[:,2] du = @view dr[:,1] dv = @view dr[:,2] mul!(du, D, v, h, 0) # du/dt = h * D * v mul!(dv, D, u, h, 0) # dv/dt = h * D * u # Note: mul!(C, A, B, α, β) performs the operation α*A*B + β*C and stores it in C end;
We now specify the initial data. In this particular case, we choose one for which $u^{+}$ vanishes. Therefore, the only wave we should observe is $u^{-}$ propagating to the right. You may experiment by changing the initial data to something else.
x0 = 0.4; x1 = 0.6 function create_r0!(r0,x0,x1,N,p) u = @view r0[:,1] v = @view r0[:,2] for i in 1:N x[i] = dx*(i-1) if x[i] > x0 && x[i] < x1 u[i] = (x[i] - x0)^p * (x[i] - x1)^p / (x1-x0)^(2p) * 2^(2p) v[i] = -u[i] end end end create_r0!(r0,x0,x1,N,8) plot(x, r0, label = ["\$u\$" "\$v\$"], xlabel = "\$x\$", legendfontsize = 15)
We define two problems with different precision
D_2_per = create_D_2_per(N) D_4_per = create_D_4_per(N) p2 = (dx, D_2_per) p4 = (dx, D_4_per) @time prob2 = ODEProblem(F!,r0,(0.0,T),p2); @time prob4 = ODEProblem(F!,r0,(0.0,T),p4);
0.027907 seconds (49.63 k allocations: 2.719 MiB, 97.60% compilation time ) 0.000121 seconds (64 allocations: 2.438 KiB)
Now we solve
@time sol2 = solve(prob2,RK4(),dt=dt, adaptive = false);
2.503869 seconds (8.84 M allocations: 598.220 MiB, 5.11% gc time, 95.00% compilation time)
@time sol4 = solve(prob4,RK4(),dt=dt, adaptive = false);
0.190474 seconds (55.08 k allocations: 116.291 MiB, 22.32% gc time)
Finally, we plot the solution at different times. Note that since the propagation speed is $1$ and the system is periodic with spatial length $1$, we expect to recover the initial data at times $t = i \cdot 1.0$.
plt = plot() for i in 0:4 plot!(plt,x, sol2(T*0.1*i)[:,1], label = "t = $(T*0.1*i)") end title!("Second-order derivative") xlabel!("\$x\$") ylabel!("\$u\$") display(plt)
plt = plot() for i in 0:4 plot!(plt,x, sol4(T*0.2*i)[:,1], label = "t = $(T*0.2*i)") end title!("Fourth-order derivative") xlabel!("\$x\$") ylabel!("\$u\$") display(plt)
anim = @animate for i in 0:100 plot(x, sol2(i*T/100)[:,1], ylim = (-0.5, 1.5), label = "t = $(i*T/100)") end gif(anim, "wave_anim_fps10.gif", fps = 10)
plot([r0[:,1],sol2(T)[:,1],sol4(T)[:,1]]) #plot(x,sol.u)
function Q(sol1, sol2, sol4, t, i) return norm(sol1(t)[1:end,i] - sol2(t)[1:2:end,i])/norm(sol2(t)[1:2:end,i] - sol4(t)[1:4:end,i]) end;
p = 8 N = 160 # Number of points in the spatial discretization L = 1.0 # Spatial interval dx = L/N # dx T = 1.0 # Final time # We take dt ≈ dx/speed_max to satisfy the CFL condition and # ensure the stability of the algorithm. # dt = 1.0 * dx dt = 1e-6 r0 = zeros(N, 2) # Discretization of the fields u and v x = [dx * i for i in 0:N-1] # x coordinates, needed to define the initial data D_4_per = create_D_4_per(N) p4 = (dx, D_4_per) create_r0!(r0, x0, x1, N, p) prob4 = ODEProblem(F!, r0, (0.0, T), p4) sol1 = solve(prob4, RK4(), dt = dt, saveat = 0.001, adaptive = false);
N = 2 * N # Number of points in the spatial discretization dx = L / N # dx # dt = 1.0 * dx # We take dt ≈ dx/speed_max to satisfy the CFL condition and # ensure the stability of the algorithm. dt = 1e-6 r0 = zeros(N, 2) # Discretization of the fields u and v x = [dx * i for i in 0:N-1] # x coordinates, needed to define the initial data D_4_per = create_D_4_per(N) p4 = (dx, D_4_per) create_r0!(r0, x0, x1, N, p) prob4 = ODEProblem(F!, r0, (0.0, T), p4) sol2 = solve(prob4, RK4(), dt = dt, saveat = 0.001, adaptive = false);
N = 2 * N # Number of points in the spatial discretization L = 1.0 # Spatial interval dx = L / N # dx T = 1.0 # Final time # dt = 1.0 * dx # We take dt ≈ dx/speed_max to satisfy the CFL condition and # ensure the stability of the algorithm. dt = 1e-6 r0 = zeros(N, 2) # Discretization of the fields u and v x = [dx * i for i in 0:N-1] # x coordinates, needed to define the initial data D_4_per = create_D_4_per(N) p4 = (dx, D_4_per) create_r0!(r0, x0, x1, N, p) prob4 = ODEProblem(F!, r0, (0.0, T), p4) sol4 = solve(prob4, RK4(), dt = dt, saveat = 0.001, adaptive = false);
Q(sol1,sol2,sol4,dt,1)
15.564542843879961
plot(sol1.t,map(x -> Q(sol1,sol2,sol4,x,1), sol1.t))
function create_r0_noise!(r0,x0,x1,N,p) u = @view r0[:,1] v = @view r0[:,2] for i in 1:N x[i] = dx*(i-1) if x[i] > x0 && x[i] < x1 u[i] = (x[i] - x0)^p * (x[i] - x1)^p / (x1-x0)^(2p) * 2^(2p) * (-1)^i v[i] = -u[i] end end end;
p = 8 N = 160 # Number of points in the spatial discretization L = 1.0 # Spatial interval T = 1.0 # Final time # We take dt ≈ dx/speed_max to satisfy the CFL condition and # ensure the stability of the algorithm. dx = L / N dt = 1.0 * dx # dt = 1e-6 r0_noise = zeros(N, 2) # Discretization of the fields u and v (noisy initial data) r0_smooth = zeros(N, 2) # Discretization of the fields u and v (smooth initial data) x = [dx * i for i in 0:N-1] # x coordinates, needed to define the initial data D_4_per = create_D_4_per(N) p4 = (dx, D_4_per) create_r0_noise!(r0_noise, x0, x1, N, p) create_r0!(r0_smooth, x0, x1, N, p) prob4 = ODEProblem(F!, r0_noise, (0.0, T), p4) sol_noise = solve(prob4, RK4(), dt = dt, adaptive = false) prob4 = ODEProblem(F!, r0_smooth, (0.0, T), p4) sol_smooth = solve(prob4, RK4(), dt = dt, adaptive = false);
anim = @animate for i in 0:100 plot(x, sol_noise(i*T/100)[:,1], ylim = (-0.5, 1.5), label = "noise t = $(i*T/100)") plot!(x, sol_smooth(i*T/100)[:,1], ylim = (-0.5, 1.5), label = "smooth") end gif(anim, "noise_anim_fps10.gif", fps = 10)