Helmholtz equation (FE)

This tutorial shows how to solve the Helmholtz eigen problem with a finite-element approach using Bcube.

Theory

We consider the following Helmholtz equation, representing for instance the acoustic wave propagation with Neuman boundary condition(s):

\[\begin{cases} \Delta u + \omega^2 u = 0 \\ \dfrac{\partial u}{\partial n} = 0 \textrm{ on } \Gamma \end{cases}\]

An analytic solution of this equation can be obtained: for a rectangular domain $\Omega = [0,L_x] \times [0,L_y]$,

\[u(x,y) = \cos \left( \frac{k_x \pi}{L_x} x \right) \cos \left( \frac{k_y \pi}{L_y} y \right) \mathrm{~~with~~} k_x,~k_y \in \mathbb{N}\]

with $\omega^2 = \pi^2 \left( \dfrac{k_x^2}{L_x^2} + \dfrac{k_y^2}{L_y^2} \right)$

Now, both the finite-element method and the discontinuous Galerkin method requires to write the weak form of the problem:

\[\int_\Omega (\Delta u \Delta v + \omega^2u)v \mathrm{\,d}\Omega = 0\]

\[- \int_\Omega \nabla u \cdot \nabla v \mathrm{\,d}\Omega + \underbrace{\left[ (\nabla u \cdot n) v \right]_\Gamma}_{=0} + \omega^2 \int_\Omega u v \mathrm{\,d} \Omega = 0\]

\[\int_\Omega \nabla u \cdot \nabla v \mathrm{\,d} \Omega = \omega^2 \int_\Omega u v \mathrm{\,d} \Omega\]

Introducing to bilinear forms $a(u,v)$ and $b(u,v)$ for respectively the left and right side terms, this equation can be written

\[a(u,v) = \omega^2 b(u,v)\]

This is actually a generalized eigenvalue problem which can be written:

\[A u = \alpha B u\]

where

\[A u = \int_\Omega \nabla u \cdot \nabla v \mathrm{\,d} \Omega,~~ B u = \int_\Omega u v \mathrm{\,d} \Omega,~~ \alpha = \omega^2\]

Uncommented code

The code below solves the described Helmholtz eigen problem. The code with detailed comments is provided in the next section.


using Bcube
using LinearAlgebra

mesh = rectangle_mesh(21, 21)

degree = 1
fs = FunctionSpace(:Lagrange, degree)

U = TrialFESpace(fs, mesh)
V = TestFESpace(U)

domain = CellDomain(mesh)

dΩ = Measure(domain, Quadrature(2 * degree + 1))

a(u, v) = ∫(∇(u) ⋅ ∇(v))dΩ
b(u, v) = ∫(u ⋅ v)dΩ

A = assemble_bilinear(a, U, V)
B = assemble_bilinear(b, U, V)

vp, vecp = eigen(Matrix(A), Matrix(B))

@show sqrt.(abs.(vp[3:8]))

Commented code

Load the necessary packages

using Bcube
using LinearAlgebra

Mesh a 2D rectangular domain with quads.

mesh = rectangle_mesh(21, 21)

Next, create the function space that will be used for the trial and test spaces. The Lagrange polynomial space is used here. The degree is set to 1.

degree = 1
fs = FunctionSpace(:Lagrange, degree)

The trial space is created from the function space and the mesh. By default, a scalar "continuous" FESpace is created. For "discontinuous" ("DG") example, check out the linear transport tutorial.

U = TrialFESpace(fs, mesh)
V = TestFESpace(U)

Then, define the geometrical dommain on which the operators will apply. For this finite-element example, we only need a CellDomain (no FaceDomain).

domain = CellDomain(mesh)

Now, integrating on a domain necessitates a "measure", basically a quadrature of given degree.

dΩ = Measure(domain, Quadrature(2 * degree + 1))

The definition of the two bilinear forms is quite natural. Note that these definitions are lazy, no computation is done at this step : the computations will be triggered by the assembly.

a(u, v) = ∫(∇(u) ⋅ ∇(v))dΩ
b(u, v) = ∫(u ⋅ v)dΩ

To obtain the two sparse matrices corresponding to the discretisation of these bilinear forms, simply call the assemble_bilinear function, providing the trial and test spaces.

A = assemble_bilinear(a, U, V)
B = assemble_bilinear(b, U, V)

Compute eigen-values and vectors : we convert to dense matrix to avoid importing additionnal packages, but it is quite easy to solve it in a "sparse way".

vp, vecp = eigen(Matrix(A), Matrix(B))

Display the "first" five eigenvalues:

@show sqrt.(abs.(vp[3:8]))

Now we can export the solution (the eigenvectors) at nodes of the mesh for several eigenvalues. We will restrict to the first 20 eigenvectors. To do so, we will create a FEFunction for each eigenvector. This FEFunction can then be evaluated on the mesh centers, nodes, etc.

ϕ = FEFunction(U)
nvecs = min(20, get_ndofs(U))
values = zeros(nnodes(mesh), nvecs)
for ivec in 1:nvecs
    set_dof_values!(ϕ, vecp[:, ivec])
    values[:, ivec] = var_on_vertices(ϕ, mesh)
end

To write a VTK file, we need to build a dictionnary linking the variable name with its values and type

using WriteVTK
outputvtk = "../../myout/helmholtz/rectangle_mesh"
dict_vars = Dict("u_$i" => (values[:, i], VTKPointData()) for i in 1:nvecs)
write_vtk(joinpath(@__DIR__, outputvtk), 0, 0.0, mesh, dict_vars)

And here is the eigenvector corresponding to the 4th eigenvalue:


This page was generated using Literate.jl.