Files
JuliaFEM.jl/docs/book/lagrange_basis_functions.md
T
Jukka Aho ee02f9f37a feat: Separation of concerns architecture with zero-allocation foundation
**Architecture Decision: Element = Topology + Interpolation + Integration + Fields**

This commit establishes the architectural foundation for separating orthogonal concerns
in finite element implementation, preventing Abaqus-style combinatorial explosion.

## New Modules (Not Yet Integrated)

### src/topology/
Reference element geometries (pure mathematical objects):
- topology.jl: Abstract interface for reference elements
- tri3.jl: 3-node triangle reference element
- quad4.jl: 4-node quadrilateral reference element

**Zero-allocation design:**
- reference_coordinates() → NTuple{N, NTuple{D, Float64}}
- edges() → NTuple{Ne, Tuple{Int, Int}}
- faces() → NTuple{Nf, NTuple{Nn, Int}}

All topology queries return compile-time sized tuples (stack allocated, no heap).

### src/integration/
High-level integration scheme abstraction:
- integration.jl: Abstract types and IntegrationPoint struct
- gauss.jl: Gauss-Legendre quadrature wrapper around existing src/quadrature/

**Zero-allocation design:**
- integration_points() → Tuple{Vararg{IntegrationPoint{D}}}
- IntegrationPoint.ξ → NTuple{D, Float64}

**Key Insight:** Integration rules already exist in src/quadrature/ (consolidated from
FEMQuad.jl). New code is a thin architectural wrapper, not reimplementation.

## Documentation

### docs/book/element_architecture.md (NEW - 650+ lines)
Complete book chapter explaining:
- What is an Element? (composition of 4 orthogonal concerns)
- The Abaqus anti-pattern (C3D8, C3D8R, C3D8I explosion)
- JuliaFEM approach: Topology + Interpolation + Integration separation
- Type system enforcement
- Performance implications (100× speedup from type stability)
- Extending the system (adding new topologies/bases/quadrature)
- Comparison with Gridap.jl, Ferrite.jl, Deal.II

### llm/ARCHITECTURE.md (UPDATED)
Added "Architectural Decision: Separation of Concerns" section at top:
- Problem statement
- Anti-pattern example
- JuliaFEM solution
- Directory structure rationale
- Type system design
- Migration strategy

### scripts/generate_lagrange_basis.jl (UPDATED)
Added architectural context explaining Lagrange bases are INTERPOLATION SCHEMES
(not topologies, not integration rules).

## Performance: Zero-Allocation Foundation

**Why tuples matter:**
1. **Zero heap allocations** - All data stack-allocated
2. **Compile-time sizes** - Compiler can unroll loops
3. **Cache friendly** - Contiguous memory layout
4. **Type stable** - Concrete tuple types enable optimization
5. **Immutable** - No accidental mutation, thread-safe

**Example impact:**
```julia
# Compiler knows at compile time:
# - Tri3 has exactly 3 edges
# - Each edge has exactly 2 nodes
# → Loop unrolling, no bounds checks, SIMD vectorization

for edge in edges(Tri3())  # Tuple iteration, fully unrolled!
    node1, node2 = edge
    # ... assembly code (zero allocations)
end
```

**Principle from Roadmap to HPC:**
> "Zero allocations in hot paths" - Strategic Decision #2

Topology/integration queries happen billions of times in assembly loops.
Even small Vector allocations accumulate to GC pressure and cache misses.

**Rule:** If size known at compile time → use Tuple, not Vector

## Benefits

 Clear separation of mathematical concepts
 Mix-and-match: Tri3 + Lagrange + Gauss, Tri3 + Hierarchical + Lobatto, etc.
 Type system enforces correctness at compile time
 Compiler generates specialized code for each combination → 100× speedup
 Zero allocations in topology/integration queries
 No code duplication (each concern in one place)
 Educational: teaches proper software engineering

## Status

- **NOT YET INTEGRATED**: New modules not included in src/JuliaFEM.jl
- **SAFE**: Package loads successfully (verified with `using JuliaFEM`)
- **READY**: Architecture documented, zero-alloc foundation established

## Next Steps

1. Create remaining topology files (Tet4, Tet10, Hex8, Hex20, etc.)
2. Update src/JuliaFEM.jl to include new modules
3. Refactor existing Element to use new separation
4. Run generation script with new architecture
5. Integrate with existing codebase

## References

- Abaqus documentation (anti-pattern example)
- Gridap.jl (alternative approach)
- Ferrite.jl (mixed approach)
- Deal.II (C++ template approach)
- llm/ROADMAP_TO_HPC.md (performance philosophy)

See: docs/book/element_architecture.md for complete rationale and examples.
2025-11-09 05:46:34 +02:00

7.7 KiB

title, subtitle, description, date, author, categories, keywords, audience, level, type, series, chapter, math, prerequisites
title subtitle description date author categories keywords audience level type series chapter math prerequisites
Lagrange Basis Functions Mathematical foundations of finite element interpolation Complete derivation of Lagrange basis functions using Vandermonde matrix method 2025-11-09 Jukka Aho
theory
mathematics
fem
lagrange basis
shape functions
interpolation
vandermonde matrix
fem theory
researchers and advanced users expert theory The JuliaFEM Book Part I: Foundations true
linear algebra
numerical analysis
fem basics

Date: November 9, 2025
Author: JuliaFEM Development Team

Introduction

Lagrange basis functions are the foundation of the Finite Element Method. They provide a systematic way to construct polynomial interpolation functions that satisfy the Kronecker delta property: the basis function associated with node i equals 1 at that node and 0 at all other nodes.

N_i(\mathbf{x}_j) = \delta_{ij} = \begin{cases} 1 & \text{if } i = j \\ 0 & \text{if } i \neq j \end{cases}

This property makes it trivial to interpolate field values: u(\mathbf{x}) = \sum_i u_i N_i(\mathbf{x}) where u_i are nodal values.

Mathematical Foundation

Vandermonde Matrix Method

Given:

  • n nodes with coordinates \{\mathbf{x}_1, \mathbf{x}_2, \ldots, \mathbf{x}_n\} in reference element
  • A polynomial basis (ansatz) \{p_1(\mathbf{x}), p_2(\mathbf{x}), \ldots, p_n(\mathbf{x})\}

We seek coefficients \alpha_{ij} such that:

N_i(\mathbf{x}) = \sum_{j=1}^{n} \alpha_{ij} p_j(\mathbf{x})

The Kronecker delta property gives us:

N_i(\mathbf{x}_k) = \sum_{j=1}^{n} \alpha_{ij} p_j(\mathbf{x}_k) = \delta_{ik}

This is a linear system: \mathbf{V} \boldsymbol{\alpha}_i = \mathbf{e}_i

Where the Vandermonde matrix is:

V_{kj} = p_j(\mathbf{x}_k)

And \mathbf{e}_i is the $i$-th unit vector.

Example: 1D Linear Element (Seg2)

Ansatz: p(\xi) = 1 + \xi (complete linear polynomial)

Nodes: \xi_1 = 0, \xi_2 = 1

Vandermonde matrix:

$$\mathbf{V} = \begin{bmatrix} p_1(\xi_1) & p_2(\xi_1) \ p_1(\xi_2) & p_2(\xi_2) \end{bmatrix} = \begin{bmatrix} 1 & 0 \ 1 & 1 \end{bmatrix}$$

Solve for N_1: \mathbf{V} \boldsymbol{\alpha}_1 = [1, 0]^T

\begin{bmatrix} 1 & 0 \\ 1 & 1 \end{bmatrix} \begin{bmatrix} \alpha_{11} \\ \alpha_{12} \end{bmatrix} = \begin{bmatrix} 1 \\ 0 \end{bmatrix}

Solution: \alpha_{11} = 1, \alpha_{12} = -1

Therefore: N_1(\xi) = 1 \cdot 1 + (-1) \cdot \xi = 1 - \xi

Solve for N_2: \mathbf{V} \boldsymbol{\alpha}_2 = [0, 1]^T

Solution: \alpha_{21} = 0, \alpha_{22} = 1

Therefore: N_2(\xi) = 0 \cdot 1 + 1 \cdot \xi = \xi

Verification:

  • N_1(0) = 1, N_1(1) = 0
  • N_2(0) = 0, N_2(1) = 1
  • N_1(\xi) + N_2(\xi) = 1 (partition of unity) ✓

Polynomial Completeness

The ansatz polynomial must be complete to the desired order:

Order 1D 2D 3D Nodes Required
Linear 1 + \xi 1 + \xi + \eta 1 + \xi + \eta + \zeta d+1
Quadratic 1 + \xi + \xi^2 1 + \xi + \eta + \xi^2 + \xi\eta + \eta^2 ... (d+1)(d+2)/2

Example for 2D Triangle (Tri3):

Ansatz: p(\xi, \eta) = 1 + \xi + \eta (complete linear in 2D)

This is the minimal complete polynomial for 3 nodes.

Implementation in JuliaFEM

Automatic Generation Process

# 1. Define element geometry
coords = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]  # Tri3 nodes

# 2. Define ansatz polynomial
ansatz = :(1 + u + v)  # Complete linear in 2D

# 3. Build Vandermonde matrix
V[i,j] = eval_polynomial_term(ansatz_terms[j], coords[i])

# 4. For each node i:
coeffs = V \ e_i  # Solve linear system
N_i = sum(coeffs[j] * ansatz_terms[j])  # Construct basis function

# 5. Symbolic differentiation
∂N_i/∂ξ = differentiate(N_i, :u)
∂N_i/∂η = differentiate(N_i, :v)

Why This Works

  1. Completeness: Ansatz spans full polynomial space of given order
  2. Linear Independence: Vandermonde matrix is non-singular for distinct nodes
  3. Interpolation Property: Follows directly from \mathbf{V} \boldsymbol{\alpha}_i = \mathbf{e}_i

Derivatives

Once we have N_i(\xi, \eta, \zeta) symbolically, derivatives are straightforward:

\frac{\partial N_i}{\partial \xi}, \frac{\partial N_i}{\partial \eta}, \frac{\partial N_i}{\partial \zeta}

These are computed once symbolically, then pre-compiled into efficient Julia code.

Standard Lagrange Elements in JuliaFEM

1D Elements

  • Seg2: Linear (2 nodes)
  • Seg3: Quadratic (3 nodes, mid-edge node)

2D Elements

  • Tri3: Linear triangle (3 corner nodes)
  • Tri6: Quadratic triangle (6 nodes: 3 corners + 3 mid-edges)
  • Quad4: Bilinear quadrilateral (4 corner nodes)
  • Quad8: Serendipity quadrilateral (8 nodes: 4 corners + 4 mid-edges)
  • Quad9: Biquadratic quadrilateral (9 nodes: 4 corners + 4 mid-edges + 1 center)

3D Elements

  • Tet4: Linear tetrahedron (4 corner nodes)
  • Tet10: Quadratic tetrahedron (10 nodes: 4 corners + 6 mid-edges)
  • Hex8: Trilinear hexahedron (8 corner nodes)
  • Hex20: Serendipity hexahedron (20 nodes: 8 corners + 12 mid-edges)
  • Hex27: Triquadratic hexahedron (27 nodes: full tensor product)
  • Pyr5: Linear pyramid (5 nodes)
  • Wedge6: Linear wedge/prism (6 nodes)
  • Wedge15: Quadratic wedge (15 nodes)

Pre-Generation vs Runtime Generation

Historical Approach (JuliaFEM ≤ 0.5.1)

# At package load time:
create_basis_and_eval(:Tet10, "...", coords, ansatz)
# - Builds Vandermonde matrix
# - Solves n linear systems
# - Symbolic differentiation
# - Simplification
# - Code generation with eval()
# Result: __precompile__(false) - slow loading

Problems:

  • Symbolic math every package load (100+ ms)
  • Cannot precompile (eval() at module scope)
  • Opaque code generation
  • Hard to debug

Modern Approach (JuliaFEM ≥ 1.0)

# Once, during development:
scripts/generate_lagrange_basis.jl
# - Computes all bases symbolically
# - Writes clean Julia code to src/basis/lagrange_generated.jl

# At package load time:
include("basis/lagrange_generated.jl")
# - Just parses pre-written Julia code
# - Fully precompilable
# - Zero symbolic computation

Benefits:

  • Instant package loading
  • Full precompilation
  • Readable generated code
  • Easy to debug
  • Version controlled (can review changes)

Numerical Stability

Vandermonde Matrix Conditioning

The Vandermonde matrix can be ill-conditioned for:

  • High-order polynomials (p > 5)
  • Poorly distributed nodes
  • Reference elements far from unit cube/simplex

JuliaFEM's approach:

  • Use canonical reference elements (unit cube [-1,1]^d or unit simplex)
  • Lagrange elements rarely exceed order 3 in practice
  • For high-order: Consider hierarchical bases (not Lagrange)

Verification

Generated basis functions are verified by:

  1. Kronecker delta property: N_i(\mathbf{x}_j) = \delta_{ij}
  2. Partition of unity: \sum_i N_i(\mathbf{x}) = 1 everywhere
  3. Derivative correctness: Compare symbolic vs AD

See test/test_basis_functions.jl for comprehensive tests.

References

  1. Hughes, T.J.R., "The Finite Element Method: Linear Static and Dynamic Finite Element Analysis", Dover, 2000
  2. Zienkiewicz, O.C. and Taylor, R.L., "The Finite Element Method", Volumes 1-3, Butterworth-Heinemann, 2000
  3. Szabó, B. and Babuška, I., "Finite Element Analysis", Wiley, 1991

See Also

  • scripts/generate_lagrange_basis.jl - Generation script
  • src/basis/lagrange_generated.jl - Generated code (do not edit manually)
  • src/basis/lagrange_generator.jl - Generator functions (symbolic engine)
  • benchmarks/tet10_derivatives_benchmark.jl - Performance analysis (manual vs AD)