janmr blog

The Wave Equation on the Unit Square

Let us consider the wave equation

2vt2=Δv\frac{\partial^2 v}{\partial t^2} = \Delta v

on the unit square Ω=[0,1]×[0,1]\Omega = [0, 1] \times [0, 1] with homogeneous Dirichlet boundary conditions, vΩ=0v|_{\partial \Omega} = 0.

We would like to solve this partial differential equation numerically. First, we discretize in space using a finite difference discretization as described in a previous post. More specifically, we use nn interior points in each direction and a grid spacing of h=1/(n+1)h = 1/(n+1). We introduce the matrix URn×nU \in \mathbb{R}^{n \times n} with entries Uij=v(ih,jh)U_{ij} = v(ih,jh) for i,j=1,2,,ni,j = 1,2,\ldots,n. Finally, we set u=vec(U)u = \operatorname{vec}(U), where the vec\operatorname{vec} operator stacks the columns of UU on top of each other. Thus, uRn2u \in \mathbb{R}^{n^2}.

We now have

2ut2=Du,\frac{\partial^2 u}{\partial t^2} = D u,

where D=LLD = L \oplus L and LRn×nL \in \mathbb{R}^{n \times n} is the finite difference discretization of the Laplace operator:

L=1h2[2100121012010012]L = \frac{1}{h^2} \begin{bmatrix} -2 & 1 & 0 & \cdots & 0 \\ 1 & -2 & 1 & \ddots & \vdots \\ 0 & 1 & -2 & \ddots & 0 \\ \vdots & \ddots & \ddots & \ddots & 1 \\ 0 & \cdots & 0 & 1 & -2 \end{bmatrix}

(see the previous post for the definition of \oplus and other details).

So now we have a system of ordinary differential equations. It is of second order, however, but it is easily transformed into a system of first order ODEs:

t[uut]=[utDu]=[0ID0][uut].\frac{\partial}{\partial t} \begin{bmatrix} u \\ u_t \end{bmatrix} = \begin{bmatrix} u_t \\ D u \end{bmatrix} = \begin{bmatrix} 0 & I \\ D & 0 \end{bmatrix} \begin{bmatrix} u \\ u_t \end{bmatrix}.

Now, given initial values for uu and utu_t at time t=0t=0, we have a system that can be solved using a numerical ODE solver.

Let us now describe how to solve this system numerically using Python's numpy and scipy libraries.

First, the grid:

n = 199
h = 1 / (n + 1)
x = y = np.linspace(0, 1, n + 2)

Next, the initial conditions:

X, Y = np.meshgrid(x, y)
R = np.sqrt((X - 0.45)**2 + (Y - 0.4)**2)
U = 20 * np.exp(-R**2) * np.cos(4 * R) * X * (1 - X) * Y * (1 - Y)

This expression for UU is chosen to have a nice, non-trivial shape and to satisfy the boundary conditions:

Initial condition for U at t=0
Figure 1. Initial condition for U at t=0.

We now set up the Laplace matrix LL and its 2D version DD:

L = (scipy.sparse.eye(n, k=-1) - 2 * scipy.sparse.eye(n)
     + scipy.sparse.eye(n, k=1)) / h**2
D = scipy.sparse.kronsum(L, L)

We set the initial values for u(0)u(0) (a vector version of the inner of UU), use ut(0)=0u_t(0)=0 and set up the time range for the solver:

u0 = U[1:-1, 1:-1].reshape(-1)
ut0 = np.zeros_like(u0)
timespan = [0, 5]
sample_times = np.linspace(0, 5, 200)

The variable sample_times is used to instruct the solver to record the solution at these times.

Finally, we solve the system using scipy's initial value ODE solver, which uses the explicit Runge-Kutta method of order 5(4) by default:

R = scipy.integrate.solve_ivp(
    lambda _, u: np.concatenate((u[n*n:], D @ u[:n*n])),
    timespan,
    np.concatenate((u0, ut0)),
    t_eval=sample_times
)

By plotting each of the sampled solutions, we can view an animation of the solution:

(The plotting was done using matplotlib and the movie was created using ffmpeg.)

Feel free to leave any question, correction or comment in this Mastodon thread.