General concepts » Finite element spaces

Overview of the finite element spaces available in Rodin.

Introduction

A finite element space $ V_h $ is a finite-dimensional subspace of a function space (such as $ H^1(\Omega) $ or $ L^2(\Omega) $ ) that is constructed by assembling local finite elements on a mesh. In Rodin, finite element spaces are the bridge between the mesh geometry and the variational formulation of a PDE.

Every finite element computation in Rodin begins by choosing a mesh and a finite element space defined on that mesh:

Mesh mesh;
mesh = mesh.UniformGrid(Polytope::Type::Triangle, {16, 16});

P1 Vh(mesh);  // Creates a P1 finite element space

Available Spaces

Rodin provides several standard finite element spaces:

SpacePolynomial DegreeContinuityTypical Use
P00 (constant)DiscontinuousPiecewise constant quantities, indicators
P11 (linear) $ C^0 $ (continuous)Most PDEs: Poisson, elasticity, heat
H1Arbitrary (order $ k $ ) $ C^0 $ (continuous)Higher-order methods, mixed problems (e.g. Taylor-Hood for Navier-Stokes)

P0: Piecewise Constant Space

The P0 space consists of functions that are constant on each cell of the mesh. Functions in P0 are generally discontinuous across cell boundaries.

\[ V_h^0 = \{ v \in L^2(\Omega) : v|_K \in \mathbb{P}_0(K), \ \forall K \in \mathcal{T}_h \} \]

Properties:**

  • One DOF per cell (the cell-average value)
  • Number of DOFs = number of cells
  • Discontinuous across cell boundaries
  • Used for piecewise constant approximations (e.g. material properties, indicator functions)
Mesh mesh;
mesh = mesh.UniformGrid(Polytope::Type::Triangle, {8, 8});

P0 Wh(mesh);
std::cout << "P0 DOFs: " << Wh.getSize() << std::endl;
// For an 8x8 grid of triangles: 128 DOFs (= 128 cells)

GridFunction indicator(Wh);
indicator = [](const Geometry::Point& p) {
  return (p.x() < 0.5) ? 1.0 : 0.0;
};

P1: Piecewise Linear Space

The P1 space is the most commonly used finite element space. It consists of continuous functions that are linear on each cell.

\[ V_h^1 = \{ v \in H^1(\Omega) : v|_K \in \mathbb{P}_1(K), \ \forall K \in \mathcal{T}_h \} \]

Properties:**

  • One DOF per vertex (the function value at the vertex)
  • Number of DOFs = number of vertices
  • Continuous across cell boundaries ( $ H^1 $ -conforming)
  • Approximation error: $ \|u - u_h\|_{H^1} = O(h) $ where $ h $ is the mesh size
Mesh mesh;
mesh = mesh.UniformGrid(Polytope::Type::Triangle, {8, 8});

P1 Vh(mesh);
std::cout << "P1 DOFs: " << Vh.getSize() << std::endl;
// For an 8x8 grid: 81 DOFs (= 9x9 = 81 vertices)

// P1 is the default choice for most scalar PDEs
TrialFunction u(Vh);
TestFunction  v(Vh);

H1: Higher-Order Space

The H1 space provides $ H^1 $ -conforming finite elements of arbitrary polynomial order. This is useful for problems requiring higher accuracy or mixed formulations (e.g. Taylor-Hood elements for the Navier-Stokes equations).

The polynomial order is specified as a compile-time constant:

#include <Rodin/Variational/H1.h>

// Quadratic scalar space (order 2)
H1 Vh(std::integral_constant<size_t, 2>{}, mesh);

// Quadratic vector space (order 2, d components)
H1 Uh(std::integral_constant<size_t, 2>{}, mesh, mesh.getSpaceDimension());

A common application is the Taylor-Hood pair for incompressible flow:

  • P2 velocity (order 2): H1 uh(std::integral_constant<size_t, 2>{}, mesh, d)
  • P1 pressure (order 1): H1 ph(std::integral_constant<size_t, 1>{}, mesh)

Vector-Valued Spaces

P0, P1, and H1 all support vector-valued function spaces by specifying the number of components. This is essential for problems involving vector fields such as displacement (elasticity) or velocity (fluid dynamics).

Mesh mesh;
mesh = mesh.UniformGrid(Polytope::Type::Triangle, {16, 16});

// Scalar space (default, 1 component)
P1 scalarVh(mesh);

// 2D vector space (2 components)
P1 vectorVh(mesh, 2);

// 3D vector space (3 components)
P1 vector3Vh(mesh, 3);

For the elasticity equation, a vector-valued P1 space represents the displacement field $ u : \Omega \to \mathbb{R}^d $ :

size_t d = mesh.getSpaceDimension();
P1 Vh(mesh, d);

TrialFunction u(Vh);  // u is a vector function
TestFunction  v(Vh);  // v is a vector function

Complex-Valued Spaces

Rodin supports complex-valued finite element spaces using C++ complex types. This is useful for problems in wave propagation, acoustics, and electromagnetics:

// Complex-valued P1 space
P1<Complex> Vh(mesh);

GridFunction gf(Vh);
gf = [](const Geometry::Point& p) {
  return Complex(p.x(), p.y());  // real + imaginary
};

// Extract real and imaginary parts for output
P1<Real> realVh(mesh);
GridFunction gfRe(realVh), gfIm(realVh);
gfRe = Re(gf);
gfIm = Im(gf);

Grid Functions

A GridFunction is a concrete function living in a finite element space. It stores the coefficients (DOF values) and can be evaluated, saved, and loaded.

Creating and Projecting

Grid functions are created by associating them with a finite element space. They can be initialized from lambda functions, constants, or other grid functions:

P1 Vh(mesh);

// Create a grid function in the P1 space
GridFunction gf(Vh);

// Project a smooth function onto the FE space
gf = [](const Geometry::Point& p) {
  return std::sin(M_PI * p.x()) * std::sin(M_PI * p.y());
};

// Project from a constant
GridFunction constant(Vh);
constant = 1.0;

For P0 (piecewise constant) spaces, the projection computes cell averages:

P0 Wh(mesh);
GridFunction indicator(Wh);
indicator = [](const Point& p) {
  return p.x() * p.x() + p.y() * p.y();
};

Computing Gradients

The gradient of a scalar grid function can be projected into a vector-valued space:

P1 scalarVh(mesh);
GridFunction temperature(scalarVh);
temperature = [](const Point& p) {
  return 1 - (p.x() - 0.5) * (p.x() - 0.5)
           - (p.y() - 0.5) * (p.y() - 0.5);
};

// Project the gradient into a vector P1 space
size_t d = mesh.getSpaceDimension();
P1 vectorVh(mesh, d);

GridFunction grad(vectorVh);
grad = Grad(temperature);

The Grad() operator computes the element-wise gradient. For face-based trace evaluation (needed in shape optimization), use traceOf():

GridFunction trace(vectorVh);
trace.project(Region::Faces, Grad(temperature).traceOf(1));

Accessing Solutions

After solving a problem, the solution is stored as a grid function accessible via the trial function:

TrialFunction u(Vh);
TestFunction  v(Vh);

Problem poisson(u, v);
// ... define problem ...
Solver::CG(poisson).solve();

// Access the solution grid function
auto& solution = u.getSolution();

Boolean Operations on Grid Functions

Grid functions support boolean comparison operators, which produce piecewise constant (0 or 1) results useful for indicator functions:

P1 Vh(mesh);

auto f = cos(2 * pi() * F::x + pi() / 2) * sin(2 * pi() * F::y);
auto g = sin(2 * pi() * F::x) * sin(2 * pi() * F::y);

GridFunction gf(Vh);
gf = f > g;         // 1 where f > g, 0 otherwise
gf = f < g;         // 1 where f < g, 0 otherwise
gf = (f > g) || (f < g);  // Logical OR
gf = (f > g) && (f < g);  // Logical AND (always 0 here)

Saving and Loading

Grid functions can be saved and loaded in MFEM format:

// Save
gf.save("temperature.gf", IO::FileFormat::MFEM);

// Load
GridFunction loaded(Vh);
loaded.load("temperature.gf", IO::FileFormat::MFEM);

For visualization, use the XDMF format:

IO::XDMF xdmf("output");
xdmf.grid().setMesh(mesh).add("temperature", gf);
xdmf.write();

Choosing the Right Space

Problem TypeRecommended SpaceReason
Scalar PDE (Poisson, heat)P1Standard $ H^1 $ conforming
Vector PDE (elasticity)P1(mesh, d)Vector-valued $ H^1 $
Wave problemsP1<Complex>Complex-valued fields
Material indicatorsP0Piecewise constant
Incompressible flow (Navier-Stokes)H1 order 2 (velocity) + H1 order 1 (pressure)Taylor-Hood pair satisfying the inf-sup condition

See Also