A from-scratch 2D finite-element solver for steady-state heat conduction, written in modern C++17 and validated by the Method of Manufactured Solutions.
Temperature field from femheat --case mms2d on the unit square
(real solver output, rendered from the exported node values).
- From-scratch FEM — Galerkin weak form, element matrices and sparse assembly written by hand, with no external FEM library.
- 1D and 2D — linear segment (
Line2) and triangle (Tri3) elements behind a single polymorphic interface, so one assembly/solve path serves both. - Validated, not just running — an MMS convergence study confirms the theoretical second-order accuracy: observed L2 order 2.00 (1D) and 1.996 (2D).
- Sparse SPD solve — Jacobi-preconditioned conjugate gradient (Eigen).
- Real I/O — HDF5 + XDMF output for ParaView and a Boost.Program_options CLI.
- Engineered — 14 GoogleTest cases, a warning-free build (
-Wall -Wextra), and GitHub Actions CI on every push.
git clone https://github.com/thefcan/femheat.git
cd femheat
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
# solve the 2D manufactured problem and write ParaView output
./build/femheat --case mms2d --nx 41 --ny 41 --out result.h5Eigen, HighFive and GoogleTest are fetched automatically; you only need a C++17 compiler, CMake, HDF5 and Boost on the system. See Build & test below for the per-platform package list.
femheat solves the steady-state heat equation
with Dirichlet (
By the Galerkin finite element method: multiplying the PDE by a test function
The temperature is approximated by piecewise-linear shape functions on a mesh
of elements — 2-node segments (Line2) in 1D, 3-node triangles (Tri3) in 2D —
which turns the weak form into a sparse symmetric positive-definite system
Because the elements are linear, the discretization is second-order accurate:
halving the mesh size cuts the L2 error by ≈ 4×. We confirm this with the Method
of Manufactured Solutions (MMS) — pick an exact
┌──────────────┐
mesh + material │ HeatProblem │ orchestration (solve())
+ source + BCs └──────┬───────┘
│
Mesh1D / TriMesh ──build──► IElement ◄── Material (k)
(geometry) Line2 / Tri3
│ elementStiffness() / elementLoad()
▼
┌───────────┐ IBoundaryCondition
│ Assembler │ ◄──── DirichletBC (penalty)
└─────┬─────┘ NeumannBC (flux)
sparse K, f │
▼
┌──────────────┐
│ LinearSolver │ Eigen ConjugateGradient (SPD)
└──────┬───────┘
nodal T │
▼
┌──────────────┐
│ Hdf5Writer │ → result.h5 + result.xdmf (ParaView)
└──────────────┘
IElementis an abstract element interface (runtime polymorphism), so the sameAssemblerserves both 1D and 2D.IBoundaryConditionis a strategy interface (DirichletBC,NeumannBC).
| Concern | Tool |
|---|---|
| Language / build | C++17, CMake (≥ 3.16) |
| Linear algebra | Eigen 3 — sparse matrices + CG |
| File I/O | HDF5 via HighFive, with a companion XDMF |
| CLI | Boost.Program_options |
| Testing | GoogleTest + CTest |
| CI | GitHub Actions (ubuntu-latest) |
Eigen, HighFive and GoogleTest are fetched automatically via CMake
FetchContent; HDF5 and Boost are located with find_package.
sudo apt-get update
sudo apt-get install -y build-essential cmake libhdf5-dev libboost-program-options-dev
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failurebrew install cmake hdf5 boost
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failureA fresh clone builds with just cmake -B build && cmake --build build.
# 2D manufactured solution on the unit square -> HDF5 + XDMF for ParaView
./build/femheat --case mms2d --nx 40 --ny 40 --k 1.0 --out result.h5
# 1D manufactured solution, reports its L2 error
./build/femheat --case mms1d --nx 80 --k 1.0
# options
./build/femheat --helpOpen result.xdmf in ParaView to visualize the
temperature field. The optional --csv field.csv flag also dumps the node
values, which scripts/plot_field.py renders to a PNG with no third-party
Python packages required:
./build/femheat --case mms2d --nx 81 --ny 81 --csv field.csv --out result.h5
python3 scripts/plot_field.py field.csv 81 81 docs/temperature_field.pngThe CLI is a thin front-end over femheat_lib. The library API mirrors the
math — build a mesh, define the problem, add boundary conditions, solve:
#include <iostream>
#include "femheat/hdf5_writer.hpp"
#include "femheat/heat_problem.hpp"
#include "femheat/material.hpp"
#include "femheat/tri_mesh.hpp"
using namespace femheat;
int main() {
// 1. Triangulate the unit square with a 41 x 41 node grid.
TriMesh mesh(/*Lx=*/1.0, /*Ly=*/1.0, /*nx=*/41, /*ny=*/41);
// 2. Conductivity k = 1, with a constant volumetric heat source f = 10.
HeatProblem problem(mesh, Material(/*k=*/1.0));
problem.setConstantSource(10.0);
// 3. Hold T = 0 on the whole boundary (penalty Dirichlet).
for (int node : mesh.boundaryNodes()) {
problem.addDirichlet(node, 0.0);
}
// 4. Assemble and solve the sparse SPD system K T = f with CG.
const auto result = problem.solveDetailed();
std::cout << "CG converged in " << result.iterations << " iterations\n";
// 5. Write result.h5 + result.xdmf for ParaView.
Hdf5Writer::writeTriMesh("result.h5", mesh, result.x);
}Both solvers are validated with MMS. The observed order is
and the regression tests assert
1D,
elements n |
L2 error | observed order |
|---|---|---|
| 10 | 9.0855e-03 | — |
| 20 | 2.2747e-03 | 1.998 |
| 40 | 5.6887e-04 | 1.999 |
| 80 | 1.4223e-04 | 2.000 |
2D,
| nodes / side | L2 error | observed order |
|---|---|---|
| 11 | 1.5778e-02 | — |
| 21 | 3.9913e-03 | 1.983 |
| 41 | 1.0008e-03 | 1.996 |
The error drops by ≈ 4× per refinement and the order approaches 2, exactly as theory predicts for linear elements. ✅
femheat/
├─ include/femheat/ public headers (Mesh, IElement, Assembler, ... )
├─ src/ library implementation -> femheat_lib
├─ apps/femheat.cpp CLI executable -> femheat
├─ tests/ GoogleTest unit + regression tests -> femheat_tests
├─ scripts/ stdlib-only field plotter
└─ docs/ figures
femheat is a focused, correctness-first solver with a deliberately small and honestly bounded scope:
- Steady state only — no time-dependent term.
- Linear elements —
Line2in 1D,Tri3in 2D (second-order accurate). - Structured meshes — uniform 1D intervals and structured triangulations of a rectangle; no unstructured-mesh import yet.
- Isotropic, single material —
Materialstores one global conductivityk. - Serial — no threading or MPI.
The architecture was built to grow past these. Because every element lives behind
the IElement interface and the assembly/solve path is element-agnostic, the
natural next steps — 3D tetrahedra, a transient term, per-element
materials, or higher-order elements — slot in without touching the core
pipeline.
- T. J. R. Hughes, The Finite Element Method: Linear Static and Dynamic Finite Element Analysis, Dover, 2000.
- P. J. Roache, "Code Verification by the Method of Manufactured Solutions," Journal of Fluids Engineering, 124(1), 4–10, 2002.
- G. Guennebaud, B. Jacob, et al., Eigen v3, https://eigen.tuxfamily.org.
MIT © 2026 Furkan Karafil