Wave equation using finite difference
1. Problem statement
We aim to solve the acoustic wave equation in a homogeneous rectangular medium using FDTD. Mathematically, the problem is stated as follows:
- Equation’s parameters are
-
-
\(u\) is the function to be determined (that describes the wave)
-
\(c\) is the wave velocity (assumed given constant)
-
\(\Delta_{xy}\) is the Laplacian operator in space
-
\(s\) is the source term (assumed given function)
-
\(L\) is the length of the domain (assumed given constant)
-
\(H\) is the height of the domain (assumed given constant)
-
\(T\) is the final time (assumed given constant)
-
\(f\) is the initial condition for $u$ (assumed given function)
-
\(g\) is the initial condition for \(\partial_t u\) (assumed given function)
-
\(k\) is the boundary condition for \(\partial_n u\) (Neumann condition: assumed given function)
-
\(\partial \Omega\) denotes the boundary of the domain.
-
1.1. Semi-discretization in space
We use a finite difference scheme to discretize the Laplacian operator in space. Let’s introduce a uniform grid defined by its vertices of coordinates :
where \(\Delta x = \frac{L}{I}\) and \(\Delta y = \frac{H}{J}\) are the space steps, and \(\Delta t\) is the time step. We denote by \(u_{i,j}(t)\) the approximation of \(u(x_i,y_j,t)\) and by \(u_{i,j}^n\) the approximation of \(u(x_i,y_j,t^n)\).
Inside the domain, we approximate the second-order partial derivatives using three-point central differences. At the edges, except at the corners, we approximate the first-order partial derivatives using two-point central differences of second order. For simplicity, at the corners, we approximate the first-order partial derivatives using two-point off-centered differences of first order.
To get \(u_{i,j}\) as vector, we define \(U_n = u_{i,j}\) with the bijection \(n = n(i,j) = i+j(I+1)\). So after discretization, we write all the discretized equations in the form of a linear system of the form \(A \tilde U = F\), where
-
\(A\) is a matrix with size $[(I+1)(J+1)]^2$,
-
\(\tilde U = (U_1, U_2, \ldots )\) is the vector of unknowns with size \((I+1)(J+1)\) and
-
\(F\) is the right hand side vector with size \((I+1)(J+1)\).
1.1.1. Inside the domain
Inside the domain, indices \(i\) and \(j\) vary in $[\![1;I-1]\!]$ and $[\![1;J-1]\!]$ respectively.
By expanding and summing, the discretization can be written as :
which is equivalent to :
where \(S_n(t) = s_{i,j}(t)\).
We deduce the matrix \(A\) and the vector \(F\) as follows:
1.1.2. At the edges (except corners)
In order to approximate first-order partial derivatives using two-point central differences of second order, we use the virtual points located outside the domain. After that, we eliminate them, so that they do not appear in the linear system.
At bottom edge
At the bottom edge, indices \(i\) vary in $[\![1;I-1]\!]$ and \(j=0\).
By expanding and summing, the discretization can be written as :
which is equivalent to :
We deduce the matrix \(A\) and the vector \(F\) as follows:
At left edge
At the left edge, indices \(i=0\) and \(j\) vary in $[\![1;J-1]\!]$.
By expanding and summing, the discretization can be written as :
which is equivalent to :
We deduce the matrix \(A\) and the vector \(F\) as follows:
At top edge
At the top edge, indices \(i\) vary in $[\![1;I-1]\!]$ and \(j=J\).
By expanding and summing, the discretization can be written as :
which is equivalent to :
We deduce the matrix \(A\) and the vector \(F\) as follows:
At right edge
At the right edge, indices \(i=I\) and \(j\) vary in $[\![1;J-1]\!]$.
By expanding and summing, the discretization can be written as :
which is equivalent to :
We deduce the matrix \(A\) and the vector \(F\) as follows:
1.1.3. At corners
At the corners, we do not use virtual points. We approximate the first-order partial derivatives using two-point off-centered differences.
At bottom-left corner
At the bottom-left corner, indices \(i=0\) and \(j=0\).
By expanding and summing, the discretization can be written as :
which is equivalent to :
We deduce the matrix \(A\) and the vector \(F\) as follows:
At top-left corner
At the top-left corner, indices \(i=0\) and \(j=J\).
By expanding and summing, the discretization can be written as :
which is equivalent to :
We deduce the matrix \(A\) and the vector \(F\) as follows:
At top-right corner
At the top-right corner, indices \(i=I\) and \(j=J\).
By expanding and summing, the discretization can be written as :
which is equivalent to :
We deduce the matrix \(A\) and the vector \(F\) as follows:
At bottom-right corner
At the bottom-right corner, indices \(i=I\) and \(j=0\).
By expanding and summing, the discretization can be written as :
which is equivalent to :
We deduce the matrix \(A\) and the vector \(F\) as follows:
1.2. Time discretization
After discretization and elimination, the semi-discretized wave equation in space can be written in the form :
By approximating the second-order time derivative with a three-point central difference, we obtain a relation between \(\tilde U^{n+1}\), \(\tilde U^n\) and \(\tilde U^{n-1}\) as follows:
Consequently, we obtain the following explicit scheme :
Initializing this scheme requires the values of $\tilde U^0 = \tilde U(0)$ and $\tilde U^1 \approx \tilde U(\Delta t)$.
Using $\tilde U(\Delta t) \approx \tilde U(0) + \Delta t \tilde U'(0) + \frac{\Delta t^2}{2} \tilde U''(0)$, we rewrite the scheme in the following form :
2. Implementation
from feelpp_sonorhc.fd import wave_2d
import numpy as np
L, nx = 2., 40
H, ny = 2., 40
T = 4.
c = 1.0 # m/s
# source term
def s(x: np.ndarray, y: np.ndarray, t: float) -> np.ndarray:
return np.zeros_like(x)
# neumann condition
def k(x: np.ndarray, y: np.ndarray, t: float) -> np.ndarray:
return np.zeros_like(x)
# gaussian function
def gaussian(x: np.ndarray, y: np.ndarray, A: float, sigma: float, x0: float, y0: float) -> np.ndarray:
return A * np.exp(-((x - x0)**2 + (y - y0)**2) / (2 * sigma**2))
# initial condition 1
def f(x: np.ndarray, y: np.ndarray) -> np.ndarray:
return gaussian(x, y, A=0.3, sigma=0.05, x0=1., y0=1.)
# initial condition 2
def g(x: np.ndarray, y: np.ndarray) -> np.ndarray:
return np.zeros_like(x)
# solve the problem
U = wave_2d.solve_wave2D_fdtd(c, s, f, g, k, L, nx, H, ny, T)
print(U.shape)
Results
(1681, 114)
3. Visualization
We use the library plotly to visualize the solution.
import numpy as np
import plotly.graph_objs as go
x = np.linspace(0, L, nx + 1)
y = np.linspace(0, H, ny + 1)
x, y = np.meshgrid(x, y)
# create figure
fig = go.Figure()
# create frames
frames = []
for i in range(U.shape[1]):
z = U[:, i].reshape(nx + 1, ny + 1) # reshape to 2D array
frame = go.Frame(data=[go.Surface(z=z, x=x, y=y, colorscale='Viridis', cmin=np.min(U), cmax=np.max(U))],
name=f'frame{i}')
frames.append(frame)
# add data to be displayed before animation starts
fig.add_trace(go.Surface(z=frames[0].data[0].z, x=x, y=y, colorscale='Viridis', cmin=np.min(U), cmax=np.max(U)))
# setup frames for animation
fig.frames = frames
# setup animation
fig.update_layout(
updatemenus=[dict(type='buttons',
showactive=False,
y=1,
x=0.8,
xanchor='left',
yanchor='bottom',
pad=dict(t=45, r=10),
buttons=[dict(label='Play',
method='animate',
args=[None, dict(frame=dict(duration=100, redraw=True),
fromcurrent=True,
transition=dict(duration=0))])])],
scene=dict(zaxis=dict(range=[np.min(U), np.max(U)], autorange=False),
xaxis_title='X Axis',
yaxis_title='Y Axis',
zaxis_title='Amplitude'),
)
# add slider
sliders = [dict(steps=[dict(method='animate',
args=[[f'frame{k}'],
dict(mode='immediate',
frame=dict(duration=100, redraw=True),
transition=dict(duration=0))],
label=f'{(T * k) / U.shape[1]:.2f} s') for k in range(U.shape[1])],
active=0,
transition=dict(duration=0),
x=0, # Slider starting position
y=0,
currentvalue=dict(font=dict(size=12),
prefix='Time: ',
visible=True,
xanchor='center'),
len=1.0)]
fig.update_layout(sliders=sliders)
# show figure
fig.show()