Solving the Helmholtz equation
Resolution of the complex-valued Helmholtz equation using Rodin.

Introduction
The Helmholtz equation models time-harmonic wave phenomena in acoustics, electromagnetics, and vibration analysis. In this example we solve the complex-valued Helmholtz equation with Dirichlet boundary conditions:
where is the wave number, is the wavelength, is a known source term, and is a known exact solution imposed on the boundary.
This example demonstrates several important Rodin features:
- Complex-valued finite element spaces (P1<Complex>)
- Extracting real and imaginary parts of complex GridFunction objects
- The Helmholtz bilinear form combining stiffness and mass terms
Weak Formulation
The weak formulation of the Helmholtz equation is:
Note the key difference from the Poisson equation: the presence of the mass term , which makes the bilinear form indefinite for sufficiently large .
Implementation
Includes and Namespaces
We include the standard Rodin modules plus the math constants for :
#include <Rodin/Solver.h> #include <Rodin/Geometry.h> #include <Rodin/Assembly.h> #include <Rodin/Variational.h> using namespace Rodin; using namespace Rodin::Geometry; using namespace Rodin::Variational;
Mesh and Parameters
We use a fine mesh ( ) to adequately resolve the wave, following the rule of thumb that the mesh size should satisfy . The mesh is created using Mesh::UniformGrid() and scaled to the unit domain via Mesh::scale():
static constexpr Real waveLength = 0.5; const Real waveNumber = 2 * Math::Constants::pi() / waveLength; Mesh mesh; mesh = mesh.UniformGrid(Polytope::Type::Triangle, { 64, 64 }); mesh.getConnectivity().compute(1, 2); mesh.scale(1.0 / 63.0);
Complex Finite Element Space
The Helmholtz equation naturally involves complex-valued fields. Rodin supports this through template-parameterized finite element spaces. Using P1<Complex> creates a space where each DOF is a complex number:
P1<Complex> vh(mesh); TrialFunction u(vh); TestFunction v(vh);
Manufactured Exact Solution
For verification we use a manufactured exact solution and compute the corresponding right-hand side :
// Exact solution: u_ex(x,y) = exp(i k (x cos(theta) + y sin(theta))) ComplexFunction u_ex = [&](const Geometry::Point& p) -> Complex { const Real x = p.x(), y = p.y(); return std::exp(Complex(0, waveNumber * (x * cosTheta + y * sinTheta))); }; // Right-hand side computed from u_ex ComplexFunction f = [&](const Geometry::Point& p) -> Complex { const Real x = p.x(), y = p.y(); return /* ... manufactured source term ... */; };
Problem Definition
The Problem is assembled using the same form language as for real-valued PDEs. The key terms are:
Problem helmholtz(u, v); helmholtz = Integral(Grad(u), Grad(v)) // stiffness - waveNumber * waveNumber * Integral(u, v) // mass (shift) - Integral(f, v) // load + DirichletBC(u, u_ex); // boundary data
Here:
Integral(Grad(u), Grad(v))is the stiffness termwaveNumber * waveNumber * Integral(u, v)is the scaled mass termIntegral(f, v)is the right-hand sideDirichletBC(u, u_ex)imposes the exact solution on the boundary
Solving the System
Because the system is complex-valued and symmetric but not positive-definite (the bilinear form is indefinite), we use a direct solver:
Solver::SparseLU(helmholtz).solve();
Extracting Real and Imaginary Parts
Since standard visualization tools typically work with real-valued data, we extract the real and imaginary parts of the solution into separate real-valued GridFunction objects using Re() and Im():
P1<Real> rh(mesh); GridFunction uRe(rh), uIm(rh); uRe = Re(u.getSolution()); uIm = Im(u.getSolution()); uRe.save("uRe.gf", IO::FileFormat::MFEM); uIm.save("uIm.gf", IO::FileFormat::MFEM);
Full Source Code
The complete source code is:
See Also
- Poisson equation — Simplest real-valued PDE
- Finite element spaces — Complex-valued spaces
- Variational formulations
- Gallery — Visual results