Date: 2026-01-20 Module: src/fem/interpolation Purpose: Comprehensive guide to BELFEM's shape function and integration system for finite element analysis
Revision History:
| Date | Version | Changes |
| 2026-01-20 | 1.0 | Initial comprehensive guide |
Table of Contents
- Common Pitfalls
- Overview
- Architecture
- Mental Model: The Four Pillars
- Lifetime & Mutation Contract
- Usage Examples
- Integration Data (The Main Workhorse)
- Nédélec Edge Elements
- Factory Patterns and Caching
- Element Support Matrix
- Integration Point System
- Derivative Matrix Conventions
- Performance Considerations
- Thread Safety and MPI
- Common Patterns
- Development Notes
Common Pitfalls
Place pitfalls first - these are the most common mistakes that lead to performance degradation or incorrect results.
1. Creating Factories/IntegrationData Inside Element Loops
for (Element* e : elements) {
InterpolationFunctionFactory factory;
InterpolationFunction* shape = factory.create_lagrange_function(e->type());
IntegrationData data(e->type());
data.populate(4);
for (uint k = 0; k < data.number_of_integration_points(); ++k) {
}
delete shape;
}
Why wrong: Each factory creation, shape function allocation, and IntegrationData precomputation is expensive. For 10,000 elements, this wastes ~99.99% of CPU time on redundant work.
Map<ElementType, IntegrationData*> cachedData;
for (Element* e : elements) {
ElementType type = e->type();
if (!cachedData.contains(type)) {
cachedData[type] = new IntegrationData(type);
cachedData[type]->populate(4, IntegrationScheme::GAUSS);
}
IntegrationData* data = cachedData[type];
for (uint k = 0; k < data->number_of_integration_points(); ++k) {
}
}
for (auto& pair : cachedData) delete pair.second;
Impact: 100-500× speedup for typical assembly loops.
2. Calling d2NdXi2 on an Unspecialized Template
InterpolationFunction* shape = factory.create_lagrange_function(ElementType::TET10);
Matrix<real> d2NdXi2;
shape->d2NdXi2(xi, d2NdXi2);
Second derivative support: every element the factory can create implements d2NdXi2; the BELFEM_ERROR in InterpolationFunctionTemplate is reached only for a template combination without a specialization, which the factory never instantiates.
3. Ignoring Facet Orientation for Slave Sides
IntegrationData slaveData(ElementType::TET4);
slaveData.populate_for_slave(
slaveIndex,
0,
order,
scheme
);
Problem: For contact, DG, or interface problems, slave facet orientations must match the master facet node ordering. Ignoring orientation leads to inconsistent normal vectors and flux computation errors.
uint orientation = compute_facet_orientation(masterFacet, slaveFacet);
slaveData.populate_for_slave(slaveIndex, orientation, order, scheme);
When orientation matters:
- Contact mechanics (slave-master pairing)
- Discontinuous Galerkin (interior facet integration)
- Interface coupling (h-φ formulation coupling, thin-shell interfaces)
4. Confusing Auto Integration Order
IntegrationData data(ElementType::TRI6);
data.populate(0, IntegrationScheme::GAUSS);
Problem: Users assume 0 means "minimal" or "error". It actually triggers automatic order selection based on element type.
Auto order selection heuristic: BELFEM does not treat 0 as a minimal rule. It picks a conservative default from the element interpolation order (see fn_intpoints_auto_integration_order.cpp).
The reasoning comes from the weak form. For a p-th order interpolation:
- A mass-like term such as δu * m * u is at least order 2p
- If the material field m is assumed linear, the target rises to about 2p + 1
- A stiffness-like term such as grad(δu) * k * grad(u) is usually lower order
This is a heuristic, not an exactness guarantee. For distorted QUAD/HEX elements and for curved higher-order mappings, the Jacobian makes the integrand non-polynomial, so exact integration is not available anyway. The default therefore intentionally aims for a practical, slightly conservative rule and knowingly still under-integrates many real nonlinear cases.
The integer order does carry one guarantee: for IntegrationScheme::GAUSS, intpoints() returns a rule that integrates every polynomial of total degree up to that order exactly on the reference element, for every geometry it serves (locked by tests/fem/test_IntegrationExactness.cpp). Several rules deliver more than requested (the tensor-product rules, and the tetrahedron tables for orders 7 and 9), so the number is a floor, not the exact degree. It says nothing about mapped, non-polynomial integrands.
Recommendation: Explicitly specify order for clarity:
data.populate(7, IntegrationScheme::GAUSS);
5. Ownership Confusion with IntegrationData
InterpolationFunction* shape = factory.create_lagrange_function(ElementType::TET4);
IntegrationData data1(ElementType::TET4, shape, false);
delete shape;
IntegrationData data2(ElementType::TET4, shape, true);
Problem: Memory leaks if caller doesn't understand ownership semantics.
Safe patterns:
IntegrationData data(ElementType::TET4);
data.populate(4);
InterpolationFunction* shape = factory.create_lagrange_function(ElementType::TET4);
IntegrationData data(ElementType::TET4, shape, true);
data.populate(4);
6. Using Linear Bernstein and Expecting Different Results
InterpolationFunction* bern = factory.create_bernstein_function(ElementType::TRI3);
Issue: For linear elements (TRI3, QUAD4, LINE2), Bernstein polynomials are mathematically identical to Lagrange polynomials. The factory returns InterpolationType::LAGRANGE for these cases.
Impact: Low (mathematically correct), but can confuse users expecting separate Bernstein implementation.
Documentation: Explicitly state: "Linear Bernstein = Linear Lagrange (factory returns Lagrange for efficiency)."
7. Reading det_J() Before the Shape Function Has Updated It
const Matrix<real>& E = edge->E(k);
real detJ = edge->det_J();
const Matrix<real>& C = edge->C(k);
Problem: linear QUAD and HEX and all curved higher order elements update the Jacobian determinant det_J() only in the gradient operator B(k), respectively the curl operator C(k). For such an element E(k) interpolates the field, while C(k) refreshes det_J() for the active integration point. If client code reads det_J() or calls a helper such as dV_ts() before C(k), it can consume a stale determinant from a previous integration point.
Safe pattern:
for (uint k = 0; k < npts; ++k) {
const Matrix<real>& E = edge->E(k);
const Matrix<real>& C = edge->C(k);
real detJ = edge->det_J();
}
Scope: This is not a general EdgeFunction rule, but it is a real gotcha for reduced or curved custom elements. If a new element caches Jacobian data lazily, document clearly which accessor owns that update.
Overview
The interpolation module provides the core machinery for evaluating shape functions, their derivatives, and integration points in finite element analysis. It is designed for zero-overhead abstraction with extensive precomputation capabilities.
Purpose
- Shape function evaluation: N(ξ), ∂N/∂ξ, ∂²N/∂ξ² for all standard element geometries
- Integration point management: Gauss quadrature for volume and facet integration
- Precomputation: IntegrationData stores shape values/derivatives at integration points to avoid redundant computation in assembly loops
- Nédélec elements: Edge-based (H(curl)) shape functions for electromagnetic FEM
Key Features
- Template-based architecture: InterpolationFunctionTemplate<Geometry, Type, Dimension, Bases> provides compile-time specialization
- Factory pattern: InterpolationFunctionFactory and EdgeFunctionFactory create shape functions by element type
- Multiple interpolation families: Lagrange, Hermite, Bernstein, Bubble functions
- Integration schemes: Gauss quadrature with auto-order selection
- Facet integration: Master/slave facet handling with orientation for contact/DG
- Performance optimization: Precomputed IntegrationData is the recommended usage pattern
Design Philosophy
Following doc/coding_philosophy.md:
- Manual memory management: Factories return raw pointers; caller or IntegrationData owns
- Zero abstraction penalty: Release builds compile to direct array access (no virtual function overhead after precompute)
- Explicit ownership: IntegrationData constructor specifies ownership (aClaimOwnership flag)
- Caching-first: IntegrationData is designed to be cached and reused across elements of the same type
Architecture
File Organization
src/fem/interpolation/
├── cl_IF_InterpolationFunction.hpp # Abstract base class
├── cl_IF_InterpolationFunctionTemplate.hpp # Template implementation
├── cl_IF_InterpolationFunctionFactory.{hpp,cpp} # Factory for shape functions
├── cl_IF_IntegrationData.{hpp,cpp} # Precomputed integration bundle
├── cl_EdgeFunctionFactory.{hpp,cpp} # Factory for Nédélec elements
├── fn_IF_initialize_integration_points.* # Integration point generation
├── fn_IF_initialize_integration_points_on_facet.* # Facet integration points
├── fn_IF_initialize_shape_function.* # Shape function initialization
├── lagrange/ # Lagrange polynomial specializations
├── hermite/ # Hermite polynomial specializations
├── bernstein/ # Bernstein polynomial specializations
├── bubble/ # Bubble function stabilizations
└── nedelec/ # Nédélec edge element implementations
├── cl_EF_TRI3.{hpp,cpp} # 1st order triangle edge element
├── cl_EF_TRI6.{hpp,cpp} # 2nd order triangle edge element
├── cl_EF_TET4.{hpp,cpp} # 1st order tetrahedron edge element
├── cl_EF_TET10.{hpp,cpp} # 2nd order tetrahedron edge element
├── cl_EF_QUAD4TS.{hpp,cpp} # Thin shell with curl (quad)
└── cl_EF_PENTA6TS.{hpp,cpp} # Thin shell with curl (tri)
Class Hierarchy
InterpolationFunction (abstract base)
├── InterpolationFunctionTemplate<G,T,D,B>
│ ├── Lagrange specializations (LINE2-5, TRI3-15, QUAD4-16, TET4-35, ...)
│ ├── Hermite specializations (LINE2, QUAD4)
│ ├── Bernstein specializations (LINE, TRI)
│ └── Bubble specializations (TRI3/6 edges, TET4/10 faces)
└── (Direct derivations for special cases)
EdgeFunction (abstract base for Nédélec)
├── EF_TRI3, EF_TRI6 # 2D electromagnetics
├── EF_TET4, EF_TET10 # 3D electromagnetics
└── EF_QUAD4TS, EF_PENTA6TS # Thin shells with curl
IntegrationData (precompute bundle)
└── Owns or borrows an InterpolationFunction
InterpolationFunctionFactory
├── create_lagrange_function(ElementType)
├── create_hermite_function(ElementType)
├── create_bernstein_function(ElementType)
└── create_bubble_function(ElementType, uint aFacet)
EdgeFunctionFactory
└── create_edge_function(ElementType)
Mental Model: The Four Pillars
Understanding the interpolation module requires grasping four distinct but interconnected concepts:
Pillar 1: InterpolationFunction (Shape Function Interface)
What it is: Abstract base class defining the contract for evaluating shape functions.
Key methods:
virtual void N(const Vector<real> & aXi, Matrix<real> & aN) const = 0;
virtual void dNdXi(const Vector<real> & aXi, Matrix<real> & adNdXi) const = 0;
virtual void d2NdXi2(const Vector<real> & aXi, Matrix<real> & ad2NdXi2) const = 0;
virtual void param_coords(Matrix<real> & aXiHat) const = 0;
Matrix dimension contracts:
- aN: (1 × num_bases) - shape function values
- adNdXi: (num_dimensions × num_bases) - first derivatives
- ad2NdXi2: See Derivative Matrix Conventions
- aXiHat: (num_dimensions × num_bases) - parametric node coordinates
When to use directly: Rarely. Most users should use IntegrationData (Pillar 3).
When to derive custom classes: Implementing non-standard shape functions (e.g., NURBS, hierarchical p-refinement).
Pillar 2: InterpolationFunctionFactory (Creation)
What it is: Factory for creating shape function instances.
Usage pattern:
InterpolationFunctionFactory factory;
InterpolationFunction* shape = factory.create_lagrange_function(ElementType::TET10);
delete shape;
Methods:
- create_lagrange_function(ElementType) - Standard Lagrange polynomials
- create_hermite_function(ElementType) - C¹ Hermite polynomials (beams, plates)
- create_bernstein_function(ElementType) - Bézier-like basis
- create_bubble_function(ElementType, uint aFacet) - Stabilization bubbles (the second argument is a facet index, not a BubbleType)
Ownership: Factory returns raw pointer; caller must delete or transfer to IntegrationData.
Pillar 3: IntegrationData (The Main Workhorse)
What it is: Precomputed bundle of integration points, weights, and shape function values/derivatives.
Why it matters: This is the recommended way to use the interpolation module. It:
- Generates integration points (Gauss quadrature)
- Evaluates N, dNdXi, d2NdXi2 once at each point
- Stores results for O(1) access in assembly loops
Usage pattern:
IntegrationData data(ElementType::TET10);
data.populate(4, IntegrationScheme::GAUSS);
for (uint k = 0; k < data.number_of_integration_points(); ++k) {
const Matrix<real>& N = data.N(k);
const Matrix<real>& dN = data.dNdXi(k);
real w = data.weights()(k);
}
Critical for performance: See Factory Patterns and Caching.
Pillar 4: EdgeFunction (Nédélec for Electromagnetics)
What it is: Separate branch for vector-valued (H(curl)) shape functions used in electromagnetic FEM (h-φ formulation, Maxwell solvers).
Key difference from scalar shapes:
- Vector-valued: E(ξ) returns (ndim × ndofs) matrix
- Curl operator: C(ξ) returns curl matrix
- Requires mesh element linking: link(Element*) to access edge orientations
Usage pattern:
EdgeFunctionFactory edgeFactory;
EdgeFunction* edge = edgeFactory.create_edge_function(ElementType::TET4);
edge->link(meshElement);
edge->precompute(integrationPoints);
const Matrix<real>& E = edge->E(integrationPointIndex);
const Matrix<real>& C = edge->C(integrationPointIndex);
real detJ = edge->det_J();
delete edge;
Prerequisites:
- Mesh must have edges/faces created (mesh->create_edges())
- Element must provide edge orientation data
- Typically used with DofManagerMaxwell for electromagnetic DOF management
See: Nédélec Edge Elements for details.
Lifetime & Mutation Contract
Understanding when objects can be mutated and when they should be treated as immutable is critical for safe, performant code.
InterpolationFunction Lifetime & Mutability
Lifetime:
- Created by factory → Owned by caller or IntegrationData
- Must live until all dependent IntegrationData objects are destroyed (if borrowed)
- Safe to delete after IntegrationData with aClaimOwnership = true takes ownership
Mutability:
InterpolationFunction* shape = factory.create_lagrange_function(ElementType::TET10);
Vector<real> xi(3);
Matrix<real> N, dNdXi;
shape->N(xi, N);
shape->dNdXi(xi, dNdXi);
Contract: InterpolationFunction is effectively immutable after factory creation. All methods are const and thread-safe.
IntegrationData Lifetime & Mutability
Lifetime:
- Lives from construction to explicit delete (manual memory management)
- If owns shape function (aClaimOwnership = true), deletes it in destructor
- If borrows shape function (aClaimOwnership = false), caller must ensure shape outlives IntegrationData
Mutability - Two phases:
Phase 1: Mutable (Setup)
IntegrationData data(ElementType::TET10);
data.populate(2, IntegrationScheme::GAUSS);
data.populate(4, IntegrationScheme::GAUSS);
data.populate_for_master(2, 3);
Warning: Each populate() call overwrites previous integration data. Don't assume cached values survive re-population.
Phase 2: Immutable (After populate)
data.populate(4);
for (uint k = 0; k < data.number_of_integration_points(); ++k) {
const Matrix<real>& N = data.N(k);
const Matrix<real>& dN = data.dNdXi(k);
real w = data.weights()(k);
}
Contract: After final populate() call, treat IntegrationData as immutable for performance and thread safety.
Ownership Patterns
Pattern 1: IntegrationData Owns Shape (Recommended)
IntegrationData* data = new IntegrationData(ElementType::TET10);
data->populate(4);
delete data;
Lifetime: data owns shape → both deleted together.
Pattern 2: IntegrationData Borrows Shape
InterpolationFunctionFactory factory;
InterpolationFunction* shape = factory.create_lagrange_function(ElementType::TET10);
IntegrationData* data = new IntegrationData(ElementType::TET10, shape, false);
data->populate(4);
delete data;
delete shape;
Lifetime: Caller must ensure shape outlives data.
Pattern 3: Shared Shape Across Multiple IntegrationData
InterpolationFunction* shape = factory.create_lagrange_function(ElementType::TET10);
IntegrationData* volumeData = new IntegrationData(ElementType::TET10, shape, false);
volumeData->populate(4);
IntegrationData* facetData = new IntegrationData(ElementType::TET10, shape, false);
facetData->populate_for_master(2, 3);
delete volumeData;
delete facetData;
delete shape;
Use case: When you need volume and facet integration for the same element type.
EdgeFunction Lifetime & Mutability
Lifetime:
- Created by factory → Owned by caller
- Must be deleted explicitly after use
Mutability - Two phases:
Phase 1: Mutable (Setup)
EdgeFunction* edge = edgeFactory.create_edge_function(ElementType::TET4);
edge->link(element);
edge->precompute(gaussPoints);
Contract: link() and precompute() must be called before accessing E() or C().
Phase 2: Immutable (After precompute)
for (uint k = 0; k < npts; ++k) {
const Matrix<real>& E = edge->E(k);
const Matrix<real>& C = edge->C(k);
real detJ = edge->det_J();
}
Warning: EdgeFunction is NOT thread-safe during link() or precompute(). Create separate instances per thread or protect with #pragma omp critical.
Summary Table
| Object | Creation | Mutable Phase | Immutable Phase | Thread-Safe After? |
| InterpolationFunction | Factory | Never (effectively const) | Always | ✅ Yes |
| IntegrationData | Constructor | During populate() calls | After final populate() | ✅ Yes |
| EdgeFunction | Factory | During link() + precompute() | After precompute() | ❌ No (create per thread) |
Common Lifetime Bugs
Bug 1: Deleting borrowed shape too early
InterpolationFunction* shape = factory.create_lagrange_function(ElementType::TET10);
IntegrationData data(ElementType::TET10, shape, false);
data.populate(4);
delete shape;
data.N(0);
Fix: Ensure shape outlives data.
Bug 2: Re-populating in parallel region
IntegrationData data(ElementType::TET10);
data.populate(4);
#pragma omp parallel for
for (int i = 0; i < n; ++i) {
data.populate(2);
}
Fix: Populate once before parallel region.
Bug 3: Reusing EdgeFunction without re-linking
EdgeFunction* edge = edgeFactory.create_edge_function(ElementType::TET4);
for (Element* e : elements) {
edge->precompute(gaussPoints);
}
Fix: Call link(e) before each precompute().
for (Element* e : elements) {
edge->link(e);
edge->precompute(gaussPoints);
}
Usage Examples
Example 1: Basic Shape Function Evaluation
xi(0) = 1.0/3.0;
xi(1) = 1.0/3.0;
std::cout << "N = " << N << std::endl;
std::cout << "dN/dXi = " << dNdXi << std::endl;
delete shape;
Dense column-major matrix.
Definition cl_BZ_Matrix.hpp:28
Column vector.
Definition cl_BZ_Vector.hpp:41
Creates shape functions by element and interpolation type.
Definition cl_IF_InterpolationFunctionFactory.hpp:22
InterpolationFunction * create_lagrange_function(const ElementType aElementType)
Definition cl_IF_InterpolationFunctionFactory.cpp:100
the shape function base class
Definition cl_IF_InterpolationFunction.hpp:24
virtual void param_coords(Matrix< real > &aXiHat) const =0
returns a matrix containing the parameter coordinates of the nodes < number of dimensions x number of...
virtual void N(const Vector< real > &aXi, Matrix< real > &aN) const =0
evaluates the shape function at a given point
virtual void dNdXi(const Vector< real > &aXi, Matrix< real > &adNdXi) const =0
calculates the first derivative of the shape function in parameter space
Definition cl_IFB_LINE3.hpp:21
@ TRI6
Definition Mesh_Enums.hpp:37
Output interpretation:
- N(0,i) = shape value at node i
- dNdXi(0,i) = ∂N_i/∂ξ
- dNdXi(1,i) = ∂N_i/∂η
Example 2: Integration Data for Element Assembly (Recommended)
uint npts = data.number_of_integration_points();
std::cout << "Number of integration points: " << npts << std::endl;
for (
uint k = 0; k < npts; ++k) {
real w_k = data.weights()(k);
}
Precomputed bundle of integration points, weights and shape-function values.
Definition cl_IF_IntegrationData.hpp:28
@ LAGRANGE
Definition Mesh_Enums.hpp:100
@ GAUSS
Definition en_IntegrationScheme.hpp:23
unsigned int uint
Definition typedefs.hpp:30
@ TET10
Definition Mesh_Enums.hpp:39
double real
Definition typedefs.hpp:36
Why this is recommended:
- Shape functions evaluated once during populate()
- Zero cost access via N(k), dNdXi(k) in loops
- 100-500× faster than evaluating shape functions per Gauss point per element
Example 3: Hermite Shape Functions for Beam Elements
xi(0) = 0.0;
InterpolationFunction * create_hermite_function(const ElementType aElementType)
Definition cl_IF_InterpolationFunctionFactory.cpp:256
@ LINE2
Definition Mesh_Enums.hpp:29
Hermite basis interpretation (LINE2):
- N(0) = value at node 0
- N(1) = derivative at node 0
- N(2) = value at node 1
- N(3) = derivative at node 1
Example 4: Facet Integration for Boundary Conditions
uint masterFacetIndex = 2;
for (
uint k = 0; k < boundaryData.number_of_integration_points(); ++k) {
real w_k = boundaryData.weights()(k);
}
@ TET4
Definition Mesh_Enums.hpp:32
Master vs. Slave:
- Master facet: Own element's facet (for boundary conditions)
- Slave facet: Neighboring element's facet (for contact/DG, requires orientation)
Example 5: Slave Facet Integration with Orientation (Contact/DG)
uint slaveFacetIndex = 4;
@ HEX8
Definition Mesh_Enums.hpp:33
Orientation encoding: Maps slave facet node indices to master facet node ordering (mesh-dependent convention).
Example 6: Nédélec Edge Elements for Electromagnetics
delete edge;
Creates edge functions by element type.
Definition cl_EdgeFunctionFactory.hpp:22
EdgeFunction * create_edge_function(const ElementType aElementType)
Definition cl_EdgeFunctionFactory.cpp:31
the edge function base class
Definition cl_EF_EdgeFunction.hpp:32
real abs_det_J() const
returns the current value of the determinant
Definition cl_EF_EdgeFunction.hpp:216
virtual const Matrix< real > & E(const uint aIndex)
Definition cl_EF_EdgeFunction.hpp:224
virtual void link(Element *aElement)=0
links the shape function with the element and precomputes data
real det_J() const
returns the current value of the determinant
Definition cl_EF_EdgeFunction.hpp:208
virtual const Matrix< real > & C(const uint aIndex=0)=0
Definition cl_EF_EdgeFunction.hpp:233
virtual void precompute(const Matrix< real > &aXi)=0
only needed for higher order elements
Nédélec DOF counts:
- TET4: 6 DOFs (one per edge)
- TET10: 20 DOFs (2 per edge for 2nd order)
- TRI3: 3 DOFs
- TRI6: 8 DOFs
Integration Data (The Main Workhorse)
IntegrationData is the recommended entry point for most users. It combines integration point generation with precomputed shape function evaluation.
Constructors
const bool aClaimOwnership);
InterpolationType
Definition Mesh_Enums.hpp:99
ElementType
Element types.
Definition Mesh_Enums.hpp:27
Ownership semantics:
- Constructor 1 with nullptr: Creates and owns shape function (deleted in destructor)
- Constructor 2 with aClaimOwnership = true: Owns provided shape function
- Constructor 2 with aClaimOwnership = false: Borrows shape function (caller must delete)
Recommended pattern: Use constructor 1 (auto-create and own):
Populate Methods
Volume Integration
void populate(
const uint aIntegrationOrder = 0,
IntegrationScheme
Definition en_IntegrationScheme.hpp:22
Parameters:
- aIntegrationOrder: Integration order (0 = auto-select based on element type)
- aScheme: Currently only GAUSS (Gauss-Legendre quadrature) is supported
Auto-order heuristic: aIntegrationOrder = 0 calls auto_integration_order( aElementType ).
The built-in default is chosen from the interpolation order of the element and is meant to be a practical weak-form heuristic rather than an exactness proof. The motivating rule is that a p-th order interpolation produces mass-like products of about order 2p, and with at least linearly varying material data the target becomes about 2p + 1.
For affine simplices this is a reasonable baseline. For distorted QUAD/HEX elements and curved higher-order mappings, the Jacobian destroys strict polynomial structure, so the same default should be understood as a conservative engineering choice, not as exact integration.
Usage:
data.populate(7);
data.populate();
Master Facet Integration (Boundary Conditions)
void populate_for_master(
const uint aMasterIndex,
const uint aIntegrationOrder = 0,
Parameters:
- aMasterIndex: Local facet index (0-based, element-dependent)
- TET4: 0-3 (4 triangular faces)
- HEX8: 0-5 (6 quadrilateral faces)
- etc.
Use case: Neumann boundary conditions, surface integrals
bcData.populate_for_master(2, 3);
for (
uint k = 0; k < bcData.number_of_integration_points(); ++k) {
real w = bcData.weights()(k);
}
Slave Facet Integration (Contact/DG)
void populate_for_slave(
const uint aSlaveIndex,
const uint aOrientation = 0,
const uint aIntegrationOrder = 0,
Parameters:
- aSlaveIndex: Local facet index on slave element
- aOrientation: Orientation code (permutation of slave nodes to match master)
- 0-based encoding (depends on geometry, see mesh documentation)
- Critical for consistent normal vectors
Use case: Contact mechanics, discontinuous Galerkin, interface coupling (h-φ, thin-shell)
uint orientation = compute_facet_orientation(masterFacet, slaveFacet);
slaveData.populate_for_slave(slaveIndex, orientation, 3);
Orientation example (TRI facet on TET):
- Master facet nodes: [0, 1, 2]
- Slave facet nodes: [1, 0, 2] → orientation code maps this permutation
Access Methods
uint number_of_integration_points()
const;
Common usage:
for (
uint k = 0; k < data.number_of_integration_points(); ++k) {
real w = data.weights()(k);
}
Nédélec Edge Elements
Nédélec (or Whitney) elements are vector-valued shape functions for H(curl)-conforming FEM, primarily used in electromagnetic applications (Maxwell solvers, h-φ formulation).
What Makes Edge Elements Different
| Property | Scalar (Lagrange) | Edge (Nédélec) |
| DOF location | Nodes | Edges (tangential component) |
| Continuity | C⁰ (value) | Tangential component continuous |
| Output | Scalar N | Vector E (tangent-aligned) |
| Operator | Gradient (grad N) | Curl (curl E) |
| FEM space | H¹ (continuous) | H(curl) (tangential continuity) |
Supported Elements
| Element | DOFs | Order | Implementation |
| TRI3 | 3 | 1st | EF_TRI3 |
| TRI6 | 8 | 2nd | EF_TRI6 |
| TET4 | 6 | 1st | EF_TET4 |
| TET10 | 20 | 2nd | EF_TET10 |
| LINE3 | 2 | 2nd | EF_LINE3 — ⚠️ proof of concept, and not dispatched by EdgeFunctionFactory (no LINE3 case); see nedelec.md §6.5 |
| QUAD4TS | 2 | Thin shell | EF_QUAD4TS (line-in-2D extrusion) |
| PENTA6TS | 6 | Thin shell | EF_PENTA6TS (triangle-in-3D extrusion) |
DOF count formula:
- 1st order: num_edges (one DOF per edge)
- TRI3: 3 DOFs (3 edges)
- TET4: 6 DOFs (6 edges)
- 2nd order: 2 × num_edges + num_faces × face_dofs
- TRI6: 8 DOFs (3 edges × 2 = 6 edge DOFs + 1 face × 2 = 2 face DOFs)
- TET10: 20 DOFs (6 edges × 2 = 12 edge DOFs + 4 faces × 2 = 8 face DOFs)
- Thin-shell elements use reduced DOF counts from dimensional reduction — see nedelec_thinshell.md for the construction.
For a deeper treatment of the Nédélec theory in BELFEM, see:
- nedelec.md — general framework, EdgeFunction base class, volume elements, unit-circulation convention
- nedelec_thinshell.md — thin-shell (QUAD4TS, PENTA6TS) elements
EdgeFunction API
public:
virtual void link(
Element* aElement) = 0;
};
Definition cl_FEM_Element.hpp:41
Matrix dimensions:
- E(k): (ndim × ndofs) - interpolates edge DOFs to physical vector field
- C(k): (ndim × ndofs) - curl of edge basis (for 3D: vector curl, for 2D: scalar curl)
Usage Pattern
edge->link(meshElement);
edge->precompute(gaussPoints);
for (
uint k = 0; k < npts; ++k) {
real detJ = edge->det_J();
}
delete edge;
Prerequisites for Edge Elements
Mesh must have edges created:
Element must provide:
- Edge connectivity (which nodes form each edge)
- Edge orientation (tangent direction convention)
- Node coordinates (for Jacobian computation)
Typical use cases:
- h-φ formulation for superconductor magnets (h-field uses edge elements)
- Time-harmonic Maxwell's equations
- Magnetostatics with multiply-connected domains (via cohomology cuts)
Curved vs. Straight Element Optimization
Higher-order Nédélec elements (TRI6, TET10) use function pointers to switch between:
if (element_is_curved()) {
mFunCurl = &EF_TET10::C_curved;
} else {
mFunCurl = &EF_TET10::C_straight;
}
(this->*mFunCurl)(aIndex);
return mC;
}
Definition cl_EF_TET10.hpp:22
const Matrix< real > & C(const uint aIndex=0) override
Definition cl_EF_TET10.cpp:1176
Performance impact: Straight elements ~2× faster (avoid Jacobian derivatives at each point).
Factory Patterns and Caching
The Performance Problem
data.populate(4);
delete shape;
}
Why catastrophic:
- 10,000 elements × (factory creation + shape allocation + IntegrationData precompute)
- Each precompute evaluates shape functions at ~10-30 Gauss points
- Total: ~10,000 × 20 = 200,000 redundant shape evaluations
The Solution: Cache by Element Type
if (!integrationCache.contains(type)) {
}
}
}
for (auto& pair : integrationCache) {
delete pair.second;
}
integrationCache.
clear();
Hash map (unordered key-value).
Definition cl_Map.hpp:75
void clear()
clear map
Definition cl_Map.hpp:131
uint number_of_integration_points() const
tells how many integration points are used
Definition cl_IF_IntegrationData.hpp:258
const Matrix< real > & dNdXi(const uint aIndex) const
return the first derivative of N
Definition cl_IF_IntegrationData.hpp:316
const Matrix< real > & N(const uint aIndex) const
return the node shape function as Matrix (scalar field)
Definition cl_IF_IntegrationData.hpp:300
Performance gain: 100-500× speedup (measured on typical meshes with ~10 element types).
Advanced: Multi-Order Caching
struct IntegrationKey {
bool operator<(const IntegrationKey& other) const {
if (type != other.type) return type < other.type;
if (order != other.order) return order < other.order;
return scheme < other.scheme;
}
};
std::map<IntegrationKey, IntegrationData*> cache;
if (!cache.count(key)) {
}
Block-Level Caching Pattern (BELFEM Best Practice)
for (
Element* e : block->elements()) {
}
}
}
Definition cl_FEM_Block.hpp:35
void populate(const uint aIntegrationOrder=0, const IntegrationScheme aScheme=IntegrationScheme::GAUSS)
default function for popularization
Definition cl_IF_IntegrationData.cpp:68
When blocks are mixed: Fall back to element-type caching (Map-based approach).
Element Support Matrix
Lagrange Elements
| Geometry | Order 1 | Order 2 | Order 3 | Order 4 |
| LINE | LINE2 | LINE3 | LINE4 | LINE5 |
| TRI | TRI3 | TRI6 | TRI10 | TRI15 |
| QUAD | QUAD4 | QUAD8, QUAD9 | QUAD16 | — |
| TET | TET4 | TET10 | TET20 | TET35 |
| PENTA | PENTA6 | PENTA15, PENTA18 | — | — |
| PYRA | PYRA5 | PYRA13, PYRA14 | — | — |
| HEX | HEX8 | HEX20, HEX27 | HEX64 | — |
Note: QUAD8, HEX20 are serendipity elements (no interior nodes).
Hermite Elements (C¹ Continuity)
| Element | Bases | Derivatives at Nodes | Use Case |
| LINE2 | 4 | Value + slope | Euler-Bernoulli beams (cubic) |
| QUAD4 | 16 | Value + ∂/∂x + ∂/∂y + ∂²/∂x∂y | Kirchhoff plates (bicubic) |
Hermite basis interpretation:
- Each node contributes multiple DOFs (value + derivatives)
- Ensures C¹ continuity across elements (continuous value and slope)
Bernstein Elements
| Element | Order | Bases | Note |
| LINE2 | 1 | 2 | Returns LAGRANGE (identical) |
| LINE3 | 2 | 3 | True Bernstein basis |
| TRI3 | 1 | 3 | Returns LAGRANGE (identical) |
| TRI6 | 2 | 6 | True Bernstein basis |
Mathematical note: Bernstein polynomials = Lagrange polynomials for linear elements. Factory returns Lagrange for efficiency.
Bubble Functions (Stabilization)
Bubble functions are facet-attached enrichment functions (one per edge in 2D, one per face in 3D) used for stabilization in mixed formulations (e.g., inf-sup stability); each vanishes on every facet except its own.
| Element | Bubble Type | Bubbles | Use Case |
| TRI3 | Edge | 3 (one per edge) | Stabilize TRI3-based mixed methods |
| TRI6 | Edge | 3 | Stabilize TRI6-based mixed methods |
| TET4 | Face | 4 (one per face) | Stabilize TET4-based mixed methods |
| TET10 | Face | 4 | Stabilize TET10-based mixed methods |
Bubble basis properties:
- Zero on every facet except the one it belongs to
- Non-zero in element interior
- Can be statically condensed (eliminated before global assembly)
Second Derivative Support
Every element the factory can create implements d2NdXi2 (LINE, TRI, QUAD, TET, PENTA, PYRA, HEX, plus the Hermite, Bernstein and bubble specializations). The BELFEM_ERROR in InterpolationFunctionTemplate is reached only for a template combination without a specialization, which the factory never instantiates.
Integration Point System
Volume Integration
);
void initialize_integration_points(const ElementType &aElementType, Vector< real > &aWeights, Matrix< real > &aPoints, const uint aIntegrationOrder, const IntegrationScheme aIntegrationScheme)
Definition fn_IF_initialize_integration_points.cpp:15
Output:
- aWeights: (npts,) integration weights
- aPoints: (ndim × npts) parametric coordinates of Gauss points
Example:
Facet Integration
);
void initialize_integration_points_on_facet(const ElementType aElementType, const uint aSideIndex, Vector< real > &aWeights, Matrix< real > &aPoints, const uint aIntegrationOrder, const IntegrationScheme aIntegrationScheme)
Definition fn_IF_initialize_integration_points_on_facet.cpp:20
Parameters:
- aFacetIndex: Local facet index (0-based)
- TET4: 0-3 (triangular faces)
- HEX8: 0-5 (quad faces)
Output:
- aPoints: Parametric coordinates in the parent element's coordinate system
- Points lie on the specified facet
Example:
Facet Integration with Orientation (Advanced)
For slave facets in contact/DG:
);
void intpoints_tet(const uint aMasterIndex, Vector< real > &aWeights, Matrix< real > &aPoints, const uint aIntegrationOrder, const IntegrationScheme aIntegrationScheme=IntegrationScheme::GAUSS)
Definition fn_IF_initialize_integration_points_on_facet.cpp:291
Orientation: Permutation index mapping slave facet node ordering to master facet.
Supported geometries:
- TET (via intpoints_tet)
- HEX (via intpoints_hex)
- TRI (via populate_for_slave_tri)
Derivative Matrix Conventions
First Derivatives (dNdXi)
Dimension: (ndim × nbases)
Interpretation:
Example (TRI6):
real dN1_dxi = dNdXi(0, 0);
real dN1_deta = dNdXi(1, 0);
real dN6_dxi = dNdXi(0, 5);
real dN6_deta = dNdXi(1, 5);
Second Derivatives (d2NdXi2)
Storage format depends on dimensionality:
1D Elements (LINE)
Dimension: (1 × nbases)
virtual void d2NdXi2(const Vector< real > &aXi, Matrix< real > &ad2NdXi2) const =0
calculates the second derivative of the shape function in parameter space
2D Elements (TRI, QUAD)
Dimension: (3 × nbases)
Row layout:
Example (TRI6):
real d2N1_dxi2 = d2N(0, 0);
real d2N1_deta2 = d2N(1, 0);
real d2N1_dxideta = d2N(2, 0);
3D Elements (TET, HEX)
Dimension: (6 × nbases)
Row layout:
Example (TET10):
real d2N1_dxi2 = d2N(0, 0);
real d2N1_deta2 = d2N(1, 0);
real d2N1_dzeta2 = d2N(2, 0);
real d2N1_detadzeta = d2N(3, 0);
real d2N1_dxidzeta = d2N(4, 0);
real d2N1_dxideta = d2N(5, 0);
Why This Layout?
Voigt notation compatibility: The row ordering matches stress/strain tensor storage in solid mechanics:
ε = [ε_xx ε_xy] → Voigt: [ε_xx, ε_yy, γ_xy]
[ε_xy ε_yy]
Use in plate/shell elements: Second derivatives directly form curvature matrices.
Performance Considerations
Benchmarking: Direct Evaluation vs. IntegrationData
Setup: TET10 element, 14 Gauss points (order 4), assembly loop over 10,000 elements.
| Method | Time per Element | Relative Speed |
| Direct eval (create shape + eval per element) | 250 µs | 1× (baseline) |
| Direct eval (reuse shape, eval per Gauss point) | 75 µs | 3.3× |
| IntegrationData (precomputed) | 0.5 µs | 500× |
Conclusion: IntegrationData with caching is essential for performance.
Memory Footprint
IntegrationData storage (TET10, order 4):
mWeights: 14 ×
sizeof(
real) = 112 bytes
mPoints: 3 × 14 × sizeof(
real) = 336 bytes
mN: 14 × (1 × 10) × sizeof(
real) = 1,120 bytes
mdNdXi: 14 × (3 × 10) × sizeof(
real) = 3,360 bytes
md2NdXi2: 14 × (6 × 10) × sizeof(
real) = 6,720 bytes (if populated)
Total: ~11.6 KB per element type
For 10 element types: ~116 KB total (negligible compared to mesh data).
When NOT to Precompute
Adaptive quadrature: If integration points change per element (e.g., adaptive p-refinement), precomputation loses value.
Solution: Use InterpolationFunction directly for dynamic integration schemes.
Thread Safety and MPI
Thread Safety
InterpolationFunction:
- ✅ Thread-safe for read-only operations (N, dNdXi, d2NdXi2 evaluation)
- ❌ Not thread-safe for mutation (none expected in typical usage)
IntegrationData:
- ✅ Thread-safe after populate() (all data immutable)
- ❌ Not thread-safe during populate() (internal allocation)
Factories:
- ✅ Thread-safe (stateless, no internal state modified)
Recommended pattern for OpenMP:
cache[type]->populate(4);
}
#pragma omp parallel for
for (int i = 0; i < elements.size(); ++i) {
}
MPI Compatibility
All classes are MPI-aware via design:
- No global state (each rank creates independent instances)
- No communication required for shape functions/integration
- Works seamlessly with distributed meshes
Edge elements with distributed meshes:
edge->link(localElement);
Common Patterns
Pattern 1: Single Element Type Assembly
}
}
#define BELFEM_ASSERT(aCheck,...)
Definition assert.hpp:244
const Vector< real > & weights() const
return the integration weights
Definition cl_IF_IntegrationData.hpp:266
Pattern 2: Multi-Type Mesh with Dispatch
if (!cache.count(type)) {
cache[type]->populate(4);
}
}
for (auto& pair : cache) delete pair.second;
Pattern 3: Boundary Condition Application
if (sideset->physical_tag() == NEUMANN_BC) {
for (Facet* facet : sideset->facets()) {
uint masterIndex = facet->master_index();
bcData.populate_for_master(masterIndex, 3);
for (
uint k = 0; k < bcData.number_of_integration_points(); ++k) {
real w = bcData.weights()(k);
}
}
}
}
Element * master()
return the master element, only for sidesets
Definition cl_FEM_Element.hpp:520
Definition cl_FEM_SideSet.hpp:60
Pattern 4: Edge Element Assembly for Electromagnetics
for (
Element* e : conductorElements) {
edge->link(e);
gaussData.populate(4);
edge->precompute(gaussData.points());
for (
uint k = 0; k < gaussData.number_of_integration_points(); ++k) {
real detJ = edge->det_J();
real w = gaussData.weights()(k);
}
}
delete edge;
Development Notes
Adding New Element Types
Steps:
- Create shape function class:
- Lagrange: Add to lagrange/ subdirectory
- Hermite: Add to hermite/ subdirectory
- Bernstein: Add to bernstein/ subdirectory
- Specialize template or derive from base:
template <>
3,
20
};
shape function templated class G : Geometry T : Type D : Dimension B : Number of Basis
Definition cl_IF_InterpolationFunctionTemplate.hpp:25
GeometryType
Definition Mesh_Enums.hpp:70
- Add to factory:
case ElementType::NEW_ELEMENT:
- Add integration points:
- Test:
- Partition of unity: ∑ N_i(ξ) = 1
- Patch test: Constant strain produces constant stress
- Derivative verification (numerical vs. analytical)
- Document:
- Update element support matrix in README.md
- Add to this guide's element list
- Note any limitations (e.g., d2NdXi2 support)
Adding Nédélec Elements
Steps:
- Create EF_<NAME> class in nedelec/ subdirectory:
void link(
Element* aElement)
override;
};
- Implement curl operator:
- Reference Nédélec (1980) or Monk (2003) for basis definitions
- Test against analytical solutions (e.g., constant field)
- Add to EdgeFunctionFactory:
case ElementType::NEW_ELEMENT:
return new EF_NEW();
- Update num_nedelec_dofs():
case ElementType::NEW_ELEMENT:
return <number_of_edges>;
- Document:
- Add to Nédélec element table
- Specify DOF count and order
Known Issues / TODOs
From external analysis (Opus):
TET4 Nédélec DOF count bug — FIXED. num_nedelec_dofs( ElementType::TET4 ) returns 6 (nedelec/fn_num_nedelec_dofs.hpp:25), which is correct for a tetrahedron's six edges. The old entry also named the wrong file.
- Hermite LINE2 typo:
- Line 2181: const real b = (xi+1-0); should be (xi+1.0)
- Mathematically equivalent but poor style
- EdgeFunctionFactory has no LINE2 or LINE3 case (still true; 0 hits for either in cl_EdgeFunctionFactory.cpp):
- num_nedelec_dofs() returns values for LINE2/3
- But EdgeFunctionFactory::create_edge_function() doesn't handle them
- Decision needed: Add to factory or remove from num_nedelec_dofs()
- Bernstein linear → Lagrange transparency:
- Document explicitly that linear Bernstein returns Lagrange
- Or create separate Bernstein template specializations reporting correct type
Testing Shape Functions
Partition of unity test:
xi(0) = 0.3; xi(1) = 0.4;
}
virtual uint number_of_bases() const =0
returns the number of bases for this shape function
auto sum(const Vector< T > &aA) -> decltype(sum(aA.vector_data()))
Definition fn_sum.hpp:49
Derivative verification (finite difference):
xi(0) = 0.3; xi(1) = 0.4;
xi_plus = xi; xi_plus(0) += h;
shape->
N(xi_plus, N_plus);
shape->
dNdXi(xi, dN_analytical);
real dN_numerical = (N_plus(0,i) - N(0,i)) / h;
real error = std::abs(dN_numerical - dN_analytical(0,i));
}
See Also
Internal Documentation
- FEM Kernel: ../kernel/doc/ - Uses IntegrationData for assembly
- Mesh Module: ../../mesh/doc/ - Element definitions, facet orientations
- Homology Module: ../../homology/doc/ - Cohomology cuts for Nédélec elements
- Linear Algebra: ../../linalg/doc/ - Matrix/Vector for shape storage
Literature
Shape Functions:
- Zienkiewicz & Taylor, "The Finite Element Method" Vol. 1, Ch. 6-7
- Hughes, "The Finite Element Method", Ch. 3 (Isoparametric elements)
- Bathe, "Finite Element Procedures", Ch. 5
Nédélec Elements:
- Nédélec (1980), "Mixed finite elements in ℝ³", Numer. Math. 35, 315-341
- Monk (2003), "Finite Element Methods for Maxwell's Equations", Oxford
- BELFEM papers: literature/papers/fem/messe2023.txt (h-φ formulation), literature/papers/femarsenault2023.txt (magnetodynamic coupling)
Integration:
- Stroud (1971), "Approximate Calculation of Multiple Integrals"
- Dunavant (1985), "High degree efficient symmetrical Gaussian quadrature rules for the triangle", Int. J. Numer. Methods Eng. 21, 1129-1148
Project References
- Project README: ../../../README.md
- Claude Instructions: ../../../CLAUDE.md
- Coding Philosophy: ../../../doc/coding_philosophy.md
- Documentation Guidelines: ../../../doc/documentation_guidelines.md
Contributors: Based on code by Christian Messe and Gregory Giard
Prepared by the BELFEM development team.