docs(architecture): Complete rewrite explaining topology vs basis separation

Major documentation update (380 additions, 167 deletions):

- Explained topology is pure geometry (NO hardcoded node counts)
- Clarified basis determines BOTH polynomial degree AND node count
- Distinguished node count (connectivity) vs DOF count (unknowns)
- Added examples: Nedelec (edge DOFs), Raviart-Thomas (face DOFs)
- Documented Lagrange{Topology, P} parametric architecture
- Showed why Tri3/Quad4/Tet10 names are anti-pattern
- Updated all code examples to use new architecture
- Explained Serendipity vs full Lagrange tensor products
- Added performance implications and trade-offs
- Showed how one Triangle topology works for P1/P2/P3/Nedelec/etc
This commit is contained in:
Jukka Aho
2025-11-09 09:29:58 +02:00
parent 3cf39bb14d
commit 5e210187d8
+377 -164
View File
@@ -21,19 +21,21 @@ What is a finite element? This seemingly simple question has profound implicatio
A finite element is fundamentally composed of **four independent concerns**:
### 1. Topology (Connectivity/Graph Theory)
### 1. Topology (Reference Element Geometry)
**What it is:** The combinatorial structure of how nodes connect to form an element.
**What it is:** The **geometric shape** of the reference element in parametric space.
- **Examples:** `Tri3` (3-node triangle), `Quad4` (4-node quadrilateral), `Tet10` (10-node tetrahedron)
- **Properties:** Number of nodes, edges, faces; reference element geometry
- **Mathematics:** Graph theory, combinatorics
- **Rarely changes:** Topology is a mathematical object, not implementation-dependent
- **Examples:** `Triangle`, `Quadrilateral`, `Tetrahedron`, `Hexahedron`, `Pyramid`, `Wedge`
- **Properties:** Dimension, edges, faces; parametric domain
- **Mathematics:** Differential geometry, topology
- **Key insight:** Topology is **pure geometry**, independent of node count or DOF placement
**Reference element:** The element in parametric coordinates $\xi \in [-1, 1]^d$
**Critical:** Topology does NOT specify node count! That's determined by the interpolation scheme.
**Reference element:** The element in parametric coordinates $\xi \in \Omega_{ref}$
```text
Tri3 reference element:
Triangle reference element (parametric domain):
η
^
|
@@ -43,28 +45,54 @@ Tri3 reference element:
| \
+---------> ξ
(0,0) (1,0)
Same topology works for:
- 3 nodes (linear, P1)
- 6 nodes (quadratic, P2)
- 10 nodes (cubic, P3)
- Edge DOFs (Nédélec)
- Face DOFs (Raviart-Thomas)
```
### 2. Interpolation (Basis Functions)
### 2. Interpolation (Basis Functions + DOF Placement)
**What it is:** How to interpolate field values between nodes.
**What it is:** How to interpolate field values AND where degrees of freedom live.
- **Examples:** Lagrange polynomials, hierarchical polynomials, NURBS
- **Properties:** Polynomial order, continuity, partition of unity
- **Examples:** Lagrange (nodal DOFs), Nédélec (edge DOFs), Raviart-Thomas (face DOFs)
- **Properties:** Polynomial order, continuity, DOF location, partition of unity
- **Mathematics:** Approximation theory, functional analysis
- **Can vary:** Same topology with different interpolation schemes
**Interpolation formula:** $u(\xi) = \sum_{i=1}^n N_i(\xi) u_i$
where $N_i(\xi)$ are basis functions and $u_i$ are nodal values.
where $N_i(\xi)$ are basis functions and $u_i$ are DOF values (not necessarily at nodes!).
**Key property:** Basis functions are **independent of topology** (mostly).
**Critical insight:** Interpolation determines BOTH polynomial degree AND node count:
- `Lagrange{Triangle, 1}` → P1 → 3 nodes (vertices only)
- `Lagrange{Triangle, 2}` → P2 → 6 nodes (vertices + edge midpoints)
- `Lagrange{Triangle, 3}` → P3 → 10 nodes (vertices + edges + interior)
- `Nedelec{Triangle, 1}` → Edge elements → 3 DOFs on edges, NOT at nodes!
- Linear Lagrange on `Tri3`: $N_1 = 1 - \xi - \eta$, $N_2 = \xi$, $N_3 = \eta$
- Linear Lagrange on `Quad4`: $N_1 = (1-\xi)(1-\eta)/4$, ...
- Hierarchical on `Tri3`: $N_1 = 1 - \xi - \eta$, $N_2 = \xi(1-\xi-\eta)$, ...
**Node count vs DOF count:**
- **Nodes:** Geometric points for element connectivity (graph structure)
- **DOFs:** Where unknowns live (can be at nodes, edges, faces, interior)
**Important:** Interpolation scheme determines polynomial order, NOT topology.
```julia
# Electromagnetics: DOFs on edges, not nodes
element = Element(Triangle(), Nedelec{Triangle, 1}(), Gauss{2}(), (1,2,3))
nnodes(element) # → 3 (vertices for connectivity)
ndofs(element) # → 3 (one DOF per edge)
# Standard mechanics: DOFs at nodes
element = Element(Triangle(), Lagrange{Triangle, 1}(), Gauss{1}(), (1,2,3))
nnodes(element) # → 3
ndofs(element) # → 3 (coincide for nodal elements)
# Quadratic: More nodes than linear
element = Element(Triangle(), Lagrange{Triangle, 2}(), Gauss{3}(), (1,2,3,4,5,6))
nnodes(element) # → 6 (vertices + edge midpoints)
ndofs(element) # → 6
```
### 3. Integration (Quadrature Rules)
@@ -106,94 +134,124 @@ fields = Dict(
**Key property:** Fields are **completely independent** of topology, interpolation, and integration.
## The Anti-Pattern: Abaqus's Mistake
## The Anti-Pattern: Code Aster (and Abaqus)
Abaqus (and many commercial codes) conflate these concerns, leading to **combinatorial explosion**:
Commercial FEM codes conflate topology, node count, and interpolation, leading to **hardcoded combinatorial explosion**:
### Hexahedral Element Examples
### The Hardcoded Node Count Anti-Pattern
| Element Type | Topology | Interpolation | Integration | Modes |
|--------------|----------|---------------|-------------|-------|
| `C3D8` | Hex8 | Linear | Full (2×2×2)| None |
| `C3D8R` | Hex8 | Linear | Reduced (1) | None |
| `C3D8I` | Hex8 | Linear | Full (2×2×2)| Incompatible |
| `C3D20` | Hex20 | Quadratic | Full (3×3×3)| None |
| `C3D20R` | Hex20 | Quadratic | Reduced (2×2×2) | None |
| `C3D20RH` | Hex20 | Quadratic | Reduced | Hybrid |
| `C3D27` | Hex27 | Quadratic | Full (3×3×3)| None |
| `C3D27R` | Hex27 | Quadratic | Reduced | None |
| Element Type | Topology | Node Count | Polynomial Order | Integration |
|--------------|----------|------------|------------------|-------------|
| `TRIA3` | Triangle | **3** (hardcoded!) | P1 | Default |
| `TRIA6` | Triangle | **6** (hardcoded!) | P2 | Default |
| `QUAD4` | Quadrilateral | **4** | Q1 | Full |
| `QUAD8` | Quadrilateral | **8** | Q2 Serendipity | Full |
| `QUAD9` | Quadrilateral | **9** | Q2 | Full |
| `TETRA4` | Tetrahedron | **4** | P1 | Default |
| `TETRA10` | Tetrahedron | **10** | P2 | Default |
| `HEXA8` | Hexahedron | **8** | Q1 | Full (2×2×2) |
| `HEXA20` | Hexahedron | **20** | Q2 Serendipity | Full |
| `HEXA27` | Hexahedron | **27** | Q2 | Full (3×3×3) |
**Result:** 8 different "element types" for what is fundamentally **one topology** with different choices for interpolation and integration!
**The fundamental mistake:** Node count is **hardcoded into the type name**, when it should be a *consequence* of:
1. Topology (geometric shape)
2. Interpolation scheme (polynomial degree)
**Result:** Cannot use Triangle with edge DOFs (Nédélec), cannot add interior DOFs for pressure, cannot use hierarchical basis with same topology.
### The Problem with Conflation
```c
// Abaqus-style (pseudo-code)
class C3D8 {
// Everything mixed together
Node nodes[8];
// Code Aster style (pseudo-code)
class TRIA3 {
// Topology, node count, interpolation all mixed
Node nodes[3]; // Hardcoded!
void stiffness_matrix() {
// Hardcoded: 8 nodes, linear shape functions, 2×2×2 Gauss
// Hardcoded: 3 nodes, P1 shape functions, default integration
}
};
class C3D8R {
// Almost identical code, but different integration
Node nodes[8];
class TRIA6 {
// Almost identical code for same topology!
Node nodes[6]; // Different hardcoded count
void stiffness_matrix() {
// Hardcoded: 8 nodes, linear shape functions, 1 point
// Hardcoded: 6 nodes, P2 shape functions, more integration points
}
};
// Now need C3D8I, C3D20, C3D20R, ... → code duplication nightmare
// Now need TRIA7, TRIA10 (cubic), QUAD4, QUAD8, QUAD9, ... → explosion
```
**Issues:**
-Code duplication (each element type reimplements similar logic)
-Combinatorial explosion (n topologies × m interpolations × k integrations)
-Maintenance nightmare (bug fix must be repeated in all variants)
-Cannot mix-and-match (user stuck with pre-defined combinations)
-No compile-time optimization (runtime dispatch on element type)
-**Hardcoded node count** prevents using same topology with different basis
-**Cannot use edge/face DOFs** (Nédélec, Raviart-Thomas for electromagnetics)
-**Cannot add interior DOFs** (pressure in mixed formulations)
-**Code duplication** (TRIA3 and TRIA6 nearly identical except node count)
-**Combinatorial explosion** (n shapes × m node-counts × k integrations × ...)
-**Maintenance nightmare** (bug fix must be repeated in all variants)
-**No runtime dispatch** possible (everything statically hardcoded)
## JuliaFEM's Approach: Composition Over Conflation
### Separation of Concerns
```julia
# 1. Define topology (reference element)
# 1. Define topology (pure geometry, NO node count)
abstract type AbstractTopology end
struct Tri3 <: AbstractTopology
nnodes::Int = 3
dim::Int = 2
end
struct Quad4 <: AbstractTopology
nnodes::Int = 4
dim::Int = 2
end
struct Hex8 <: AbstractTopology
nnodes::Int = 8
dim::Int = 3
end
# 2. Define interpolation schemes
struct Point <: AbstractTopology end
struct Segment <: AbstractTopology end
struct Triangle <: AbstractTopology end
struct Quadrilateral <: AbstractTopology end
struct Tetrahedron <: AbstractTopology end
struct Hexahedron <: AbstractTopology end
struct Pyramid <: AbstractTopology end
struct Wedge <: AbstractTopology end # Prism
# Properties come from topology itself
dim(::Triangle) = 2
dim(::Tetrahedron) = 3
# 2. Define interpolation schemes (determines node count AND DOF placement)
abstract type AbstractBasis end
struct Lagrange{P} <: AbstractBasis end # P = polynomial order
struct Hierarchical{P} <: AbstractBasis end
struct NURBS{P} <: AbstractBasis end
# Lagrange family: nodal DOFs, polynomial degree P
struct Lagrange{T<:AbstractTopology, P} <: AbstractBasis end
# Serendipity: reduced node count (no center nodes)
struct Serendipity{T<:AbstractTopology, P} <: AbstractBasis end
# Nédélec: edge DOFs for H(curl) spaces (electromagnetics)
struct Nedelec{T<:AbstractTopology, P} <: AbstractBasis end
# Raviart-Thomas: face DOFs for H(div) spaces (fluid flow)
struct RaviartThomas{T<:AbstractTopology, P} <: AbstractBasis end
# Hermite: nodal values + derivatives
struct Hermite{T<:AbstractTopology, P} <: AbstractBasis end
# Node count is DERIVED from topology + basis:
nnodes(::Lagrange{Triangle, 1}) = 3 # P1: vertices only
nnodes(::Lagrange{Triangle, 2}) = 6 # P2: vertices + edge midpoints
nnodes(::Lagrange{Triangle, 3}) = 10 # P3: vertices + edges + interior
nnodes(::Lagrange{Quadrilateral, 1}) = 4 # Q1: corners
nnodes(::Lagrange{Quadrilateral, 2}) = 9 # Q2: full tensor product
nnodes(::Serendipity{Quadrilateral, 2}) = 8 # Q2 without center
# 3. Define integration rules
abstract type AbstractIntegration end
struct Gauss{N} <: AbstractIntegration end # N = number of points
struct Gauss{N} <: AbstractIntegration end # N = order (not point count!)
struct Lobatto{N} <: AbstractIntegration end
struct Reduced <: AbstractIntegration end
# 4. Element composes all three
struct Element{T <: AbstractTopology, B <: AbstractBasis, I <: AbstractIntegration, N}
# 4. Element composes all three + connectivity
struct Element{T<:AbstractTopology, B<:AbstractBasis, I<:AbstractIntegration, N}
topology::T
basis::B
integration::I
connectivity::NTuple{N, UInt}
connectivity::NTuple{N, UInt} # Tuple, not Vector!
fields::Dict{Symbol, Any} # TODO: Type-stable structure
end
```
@@ -202,15 +260,33 @@ end
```julia
# Create element by composing concerns
topology = Tri3()
basis = Lagrange{1}() # Linear interpolation
integration = Gauss{3}() # 3-point Gauss quadrature
topology = Triangle()
basis = Lagrange{Triangle, 1}() # Linear P1 → 3 nodes
integration = Gauss{2}() # Order 2 (3 points for triangles)
element = Element(topology, basis, integration,
connectivity=(1, 2, 3))
element = Element(topology, basis, integration, (1, 2, 3)) # Tuple!
# Type-stable construction (preferred)
element = Element{Tri3, Lagrange{1}, Gauss{3}}(...)
element = Element{Triangle, Lagrange{Triangle,1}, Gauss{2}, 3}(
Triangle(),
Lagrange{Triangle, 1}(),
Gauss{2}(),
(1, 2, 3), # Tuple for connectivity
Dict{Symbol, Any}()
)
# Same topology, different polynomial order:
element_p2 = Element(Triangle(), Lagrange{Triangle, 2}(), Gauss{3}(),
(1, 2, 3, 4, 5, 6)) # 6 nodes for P2
# Same topology, edge DOFs (electromagnetics):
element_nedelec = Element(Triangle(), Nedelec{Triangle, 1}(), Gauss{2}(),
(1, 2, 3)) # 3 nodes, but DOFs on edges!
# Same topology, different integration:
element_reduced = Element(Triangle(), Lagrange{Triangle, 1}(), Reduced(),
(1, 2, 3))
```
```
**Benefits:**
@@ -225,30 +301,47 @@ element = Element{Tri3, Lagrange{1}, Gauss{3}}(...)
```text
src/
topology/
tri3.jl # Reference triangle
quad4.jl # Reference quadrilateral
tet10.jl # Reference tetrahedron
hex8.jl # Reference hexahedron
...
point.jl # 0D: Point
segment.jl # 1D: Segment (line)
triangle.jl # 2D: Triangle (no node count!)
quadrilateral.jl # 2D: Quadrilateral
tetrahedron.jl # 3D: Tetrahedron
hexahedron.jl # 3D: Hexahedron (brick)
pyramid.jl # 3D: Pyramid
wedge.jl # 3D: Wedge/prism
# Each file defines pure geometry:
# - Parametric domain
# - Edges, faces
# - Reference coordinates (for standard node placements)
# NO node count hardcoded!
basis/
lagrange.jl # Lagrange polynomial bases
lagrange_generated.jl # Pre-generated for compile-time
hierarchical.jl # Hierarchical/p-refinement
nurbs.jl # NURBS for isogeometric
...
lagrange.jl # Lagrange{T, P} implementation
serendipity.jl # Serendipity{T, P} (reduced nodes)
nedelec.jl # Nedelec{T, P} (edge elements)
raviart_thomas.jl # RaviartThomas{T, P} (face elements)
hermite.jl # Hermite{T, P} (C1 continuous)
hierarchical.jl # Hierarchical{T, P} (p-refinement)
nurbs.jl # NURBS (isogeometric analysis)
# Each basis determines:
# - Node count (function of topology + polynomial degree)
# - DOF placement (nodes, edges, faces, interior)
# - Basis function evaluation
integration/
gauss.jl # Gauss-Legendre quadrature
lobatto.jl # Gauss-Lobatto quadrature
reduced.jl # Reduced integration
...
reduced.jl # Reduced integration (underintegration)
# Maps integration scheme + topology → quadrature points
elements/
element.jl # Element type definition
integrate.jl # Integration loop
assemble.jl # Global assembly
...
```
```
**Rationale:**
@@ -276,9 +369,11 @@ where:
```julia
function element_stiffness(element::Element{T, B, I}) where {T, B, I}
K = zeros(nnodes(T) * ndofs, nnodes(T) * ndofs)
n = nnodes(element.basis) # Node count from BASIS, not topology
ndof = ndofs_per_node(element.basis)
K = zeros(n * ndof, n * ndof)
# Get integration points from integration scheme
# Get integration points from integration scheme + topology
ips = integration_points(element.integration, element.topology)
for ip in ips
@@ -286,7 +381,7 @@ function element_stiffness(element::Element{T, B, I}) where {T, B, I}
N = evaluate_basis(element.basis, ip.ξ)
dN = evaluate_basis_derivatives(element.basis, ip.ξ)
# Jacobian (depends on topology + node coordinates)
# Jacobian (depends on topology + actual node coordinates)
J = jacobian(element.topology, element.connectivity, dN)
# Strain-displacement matrix (depends on basis derivatives)
@@ -302,9 +397,10 @@ end
**Notice:** Each concern is accessed through clean interfaces:
- `integration_points()` → integration scheme
- `evaluate_basis()` → interpolation scheme
- `jacobian()` → topology + connectivity
- `nnodes(basis)` → basis determines node count, NOT topology!
- `integration_points(integration, topology)` → integration scheme
- `evaluate_basis(basis, ξ)` → interpolation scheme
- `jacobian(topology, connectivity, dN)` → geometric mapping
### Type-Stability for Performance
@@ -313,12 +409,13 @@ With concrete types, the compiler can specialize:
```julia
# This becomes a specialized function with no runtime overhead
function element_stiffness(
element::Element{Tri3, Lagrange{1}, Gauss{3}, 3}
element::Element{Triangle, Lagrange{Triangle,1}, Gauss{2}, 3}
)
# Compiler knows at compile time:
# - 3 nodes (Tri3)
# - 3 basis functions (Lagrange{1})
# - 3 integration points (Gauss{3})
# - Triangle topology (2D, 3 edges)
# - 3 nodes (from Lagrange{Triangle, 1})
# - 3 basis functions (P1)
# - 3 integration points (Gauss{2} on triangle)
# - connectivity is NTuple{3, UInt}
# Generated code has:
@@ -336,57 +433,105 @@ end
### Adding a New Topology
```julia
# File: src/topology/hex27.jl
struct Hex27 <: AbstractTopology
nnodes::Int = 27
dim::Int = 3
# File: src/topology/prism.jl
"""
Prism/Wedge element: triangular cross-section extruded in z-direction.
Parametric domain: Triangle × [-1, 1]
- (ξ, η) ∈ Triangle (base)
- ζ ∈ [-1, 1] (height)
Note: Does NOT specify node count! That comes from basis.
"""
struct Prism <: AbstractTopology end
dim(::Prism) = 3
# Parametric domain edges/faces
function edges(::Prism)
# 9 edges: 3 on bottom, 3 on top, 3 vertical
return ((1,2), (2,3), (3,1), (4,5), (5,6), (6,4), (1,4), (2,5), (3,6))
end
# Reference element coordinates
reference_coordinates(::Hex27) = [
# 8 corner nodes
(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
(-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1),
# 12 mid-edge nodes
(0, -1, -1), (1, 0, -1), (0, 1, -1), (-1, 0, -1),
# ... (continue for all 27 nodes)
]
function faces(::Prism)
# 5 faces: 2 triangular (top/bottom), 3 quadrilateral (sides)
return ((1,2,3), (4,5,6), (1,2,5,4), (2,3,6,5), (3,1,4,6))
end
# Topology is a pure mathematical object
# No need to implement assembly, integration, etc.
# Standard node placements for common basis functions
function reference_node_positions(::Prism, ::Type{Lagrange{Prism, 1}})
# 6 nodes for linear (P1)
return [(-1,0,0), (1,0,0), (0,1,0), # Bottom triangle
(-1,0,1), (1,0,1), (0,1,1)] # Top triangle
end
function reference_node_positions(::Prism, ::Type{Lagrange{Prism, 2}})
# 18 nodes for quadratic (P2)
# 6 corners + 9 edge midpoints + 3 face centers
return [...] # Full list
end
```
**Usage:**
```julia
element = Element{Hex27, Lagrange{2}, Gauss{3}}(...)
element = Element(Prism(), Lagrange{Prism, 1}(), Gauss{2}(), (1,2,3,4,5,6))
# Automatically works with existing assembly code!
element_p2 = Element(Prism(), Lagrange{Prism, 2}(), Gauss{3}(),
(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18))
# Same topology, quadratic basis → 18 nodes
```
### Adding a New Interpolation Scheme
```julia
# File: src/basis/hierarchical.jl
struct Hierarchical{P} <: AbstractBasis end
"""
Hierarchical polynomial basis for p-refinement.
Unlike Lagrange (nodal basis), hierarchical basis has:
- Low-order modes at vertices (vertex bubbles)
- Higher-order modes as edge/face/volume bubbles
- Easier adaptivity (can increase P without changing low-order modes)
Node count depends on polynomial degree:
P=1: same as Lagrange (vertices only)
P=2: vertices + edge modes
P=3: vertices + edge modes + face modes + volume modes
"""
struct Hierarchical{T<:AbstractTopology, P} <: AbstractBasis end
# Node count = vertices + edges*(P-1) + faces*(P-1)*(P-2)/2 + ...
nnodes(::Hierarchical{Triangle, 1}) = 3
nnodes(::Hierarchical{Triangle, 2}) = 3 + 3*1 = 6
nnodes(::Hierarchical{Triangle, 3}) = 3 + 3*2 + 1 = 10
# Evaluate basis functions
function evaluate_basis(basis::Hierarchical{P}, ξ::Vec) where P
function evaluate_basis(basis::Hierarchical{Triangle, P}, ξ::Vec) where P
# Implement hierarchical polynomial evaluation
# Return NTuple{N, Float64}
# First 3 are vertex functions (same as Lagrange P1)
# Next modes are edge bubbles, then face bubbles
return NTuple{nnodes(basis), Float64}(...) # Zero allocation!
end
# Evaluate basis derivatives
function evaluate_basis_derivatives(basis::Hierarchical{P}, ξ::Vec) where P
# Implement derivatives
# Return NTuple{N, Vec}
function evaluate_basis_derivatives(basis::Hierarchical{Triangle, P}, ξ::Vec) where P
# Return tuple of gradients
return NTuple{nnodes(basis), Vec}(...)
end
```
**Usage:**
```julia
element = Element{Tri3, Hierarchical{3}, Gauss{4}}(...)
# Use same Tri3 topology with hierarchical basis!
# Same triangle topology, hierarchical basis instead of Lagrange
element = Element(Triangle(), Hierarchical{Triangle, 3}(), Gauss{4}(),
(1,2,3,4,5,6,7,8,9,10)) # 10 nodes for P3
# Can do p-refinement by just changing basis degree!
element_p2 = Element(Triangle(), Hierarchical{Triangle, 2}(), Gauss{3}(),
(1,2,3,4,5,6))
```
### Adding a New Integration Rule
@@ -395,16 +540,27 @@ element = Element{Tri3, Hierarchical{3}, Gauss{4}}(...)
# File: src/integration/lobatto.jl
struct Lobatto{N} <: AbstractIntegration end
function integration_points(::Lobatto{N}, topology::T) where {N, T <: AbstractTopology}
function integration_points(scheme::Lobatto{N}, topology::T) where {N, T<:AbstractTopology}
# Return integration points and weights for Lobatto quadrature
# Lobatto includes endpoints (useful for spectral methods)
# Specific to topology dimension
if T === Segment
# 1D Lobatto points
return lobatto_1d(N)
elseif T === Triangle
# 2D Lobatto-like scheme for triangles
return lobatto_triangle(N)
# ... other topologies
end
end
```
**Usage:**
```julia
element = Element{Quad4, Lagrange{1}, Lobatto{3}}(...)
element = Element(Quadrilateral(), Lagrange{Quadrilateral, 1}(), Lobatto{3}(),
(1,2,3,4))
# Use Lobatto instead of Gauss for same element!
```
@@ -415,32 +571,52 @@ element = Element{Quad4, Lagrange{1}, Lobatto{3}}(...)
The type system prevents invalid combinations:
```julia
# ✅ Valid: Tri3 with 2D basis
element = Element{Tri3, Lagrange{1}, Gauss{3}}(...)
# ✅ Valid: Triangle with P1 Lagrange basis
element = Element(Triangle(), Lagrange{Triangle, 1}(), Gauss{2}(), (1,2,3))
# ❌ Compile error: Cannot use 3D topology with 2D basis (if we enforce)
element = Element{Hex8, TriangularBasis, Gauss{3}}(...)
# ✅ Valid: Triangle with P2 Lagrange basis (6 nodes)
element = Element(Triangle(), Lagrange{Triangle, 2}(), Gauss{3}(),
(1,2,3,4,5,6))
# ✅ Valid: Mix different integration rules
element1 = Element{Quad4, Lagrange{1}, Gauss{4}}(...) # Full integration
element2 = Element{Quad4, Lagrange{1}, Reduced}(...) # Reduced integration
element3 = Element{Quad4, Lagrange{2}, Gauss{9}}(...) # Quadratic + more points
# ✅ Valid: Triangle with Nédélec edge elements
element = Element(Triangle(), Nedelec{Triangle, 1}(), Gauss{2}(), (1,2,3))
# Note: 3 nodes, but DOFs are on edges!
# ❌ Compile error: Hexahedron basis on Triangle topology (if enforced)
element = Element(Triangle(), Lagrange{Hexahedron, 1}(), Gauss{2}(), (1,2,3))
# Type mismatch: basis topology must match element topology
# ✅ Valid: Same topology, different integration rules
element1 = Element(Quadrilateral(), Lagrange{Quadrilateral,1}(), Gauss{4}(),
(1,2,3,4)) # Full integration
element2 = Element(Quadrilateral(), Lagrange{Quadrilateral,1}(), Reduced(),
(1,2,3,4)) # Reduced integration
element3 = Element(Quadrilateral(), Lagrange{Quadrilateral,2}(), Gauss{9}(),
(1,2,3,4,5,6,7,8,9)) # Quadratic + more points
```
### Number of Nodes Known at Compile Time
```julia
# Connectivity is NTuple{N, UInt} where N is known at compile time
# Connectivity is NTuple{N, UInt} where N is determined by basis, not topology!
struct Element{T, B, I, N}
topology::T
basis::B
integration::I
connectivity::NTuple{N, UInt} # N from topology
connectivity::NTuple{N, UInt} # N = nnodes(B)
end
# For Lagrange{Triangle, 1}: N = 3
# For Lagrange{Triangle, 2}: N = 6
# For Nedelec{Triangle, 1}: N = 3 (still 3 nodes, DOFs on edges)
# Compiler can unroll loops over connectivity
for i in 1:length(element.connectivity)
# Loop is unrolled at compile time!
function process_element(element::Element{T, B, I, N}) where {T, B, I, N}
for i in 1:N # N known at compile time
# Loop is unrolled at compile time!
node_id = element.connectivity[i]
# ...
end
end
```
@@ -448,32 +624,44 @@ end
## Backward Compatibility
### Type Aliases for Old Code
### Migration from Hardcoded Types
```julia
# Old code used to write:
# element = Element("Tri3", ...)
# Old Code Aster style (what we're moving away from):
# element_type = "TRIA3" # Hardcoded node count
# Provide type aliases:
const Tri3Element = Element{Tri3, Lagrange{1}, Gauss{3}}
const Quad4Element = Element{Quad4, Lagrange{1}, Gauss{4}}
# JuliaFEM modern approach:
element = Element(Triangle(), Lagrange{Triangle, 1}(), Gauss{2}(), (1,2,3))
# Old code still works:
element = Tri3Element(connectivity=(1,2,3))
# For transition, provide type aliases:
const TRIA3 = Lagrange{Triangle, 1}
const TRIA6 = Lagrange{Triangle, 2}
const QUAD4 = Lagrange{Quadrilateral, 1}
const QUAD8 = Serendipity{Quadrilateral, 2}
const QUAD9 = Lagrange{Quadrilateral, 2}
# Old code can use aliases:
element = Element(Triangle(), TRIA3(), Gauss{2}(), (1,2,3))
```
### Constructor Convenience
```julia
# Convenience constructors for common cases
function Element(::Type{Tri3}, connectivity::NTuple{3, UInt})
Element{Tri3, Lagrange{1}, Gauss{3}}(
Tri3(), Lagrange{1}(), Gauss{3}(), connectivity, Dict()
)
# Convenience constructors for common cases (implicit defaults)
function Element(topology::Triangle, connectivity::NTuple{3, Int})
# Assume: P1 Lagrange + standard Gauss quadrature
Element(topology, Lagrange{Triangle, 1}(), Gauss{2}(),
UInt.(connectivity))
end
function Element(topology::Triangle, connectivity::NTuple{6, Int})
# Infer P2 from 6 nodes
Element(topology, Lagrange{Triangle, 2}(), Gauss{3}(),
UInt.(connectivity))
end
# User can still write simple code:
element = Element(Tri3, (1, 2, 3))
element = Element(Triangle(), (1, 2, 3)) # Defaults to P1 + Gauss{2}
```
## Performance Implications
@@ -592,28 +780,53 @@ Deal.II has sophisticated separation:
**Element** = Topology + Interpolation + Integration + Fields
This simple equation guides JuliaFEM's architecture:
But with critical insight:
1. **Topology** defines the reference element (mathematical object)
2. **Interpolation** defines how to interpolate (approximation theory)
3. **Integration** defines how to integrate (numerical analysis)
4. **Fields** define what data lives on the element (problem-specific)
- **Topology** = geometric shape ONLY (no hardcoded node count!)
- **Interpolation** = polynomial space + DOF placement (determines node count)
- **Integration** = numerical quadrature (independent choice)
- **Fields** = problem-specific data
**The Code Aster/Abaqus anti-pattern we avoid:**
-`TRIA3`, `TRIA6`, `TRIA7`, `TRIA10` → hardcoded node counts
-`Triangle` + `Lagrange{Triangle, P}` → node count derived from P
**Benefits:**
-Clear separation of concerns
-Mix-and-match flexibility
- ✅ Type system enforcement
- ✅ 100× performance improvement
-Maintainable, extensible codebase
-**One topology, infinite possibilities** (P1, P2, P3, Nédélec, Raviart-Thomas, ...)
-**Clear separation** (geometry ≠ approximation ≠ integration)
-**Type system enforcement** (compiler catches mismatches)
-**Zero-cost abstractions** (100× performance improvement)
-**Extensible** (add new basis without touching topology)
-**Educational** (code teaches FEM mathematics correctly)
**Trade-off:**
**Key architectural decision:**
```julia
// WRONG (Code Aster style)
struct TRIA3 { int nnodes = 3; } // Hardcoded!
- ⚠️ More complex initial setup
- ⚠️ Requires understanding of Julia's type system
- ⚠️ Documentation must be excellent
// CORRECT (JuliaFEM style)
struct Triangle <: AbstractTopology end // Pure geometry
nnodes(::Lagrange{Triangle, 1}) = 3 // Derived from basis
nnodes(::Lagrange{Triangle, 2}) = 6 // Different basis different count
nnodes(::Nedelec{Triangle, 1}) = 3 // Edge DOFs, still 3 nodes
```
**Result:** A modern, high-performance, extensible FEM library that teaches good software engineering alongside finite element methods.
**Trade-offs:**
- ⚠️ More complex type system (but Julia handles it elegantly)
- ⚠️ Requires understanding separation of concerns
- ⚠️ Documentation must be excellent (this document!)
**Result:** A modern, mathematically correct, high-performance, extensible FEM library that can handle:
- Standard nodal FEM (Lagrange)
- Edge elements (electromagnetics with Nédélec)
- Face elements (fluid flow with Raviart-Thomas)
- Mixed formulations (Taylor-Hood, MINI, ...)
- Isogeometric analysis (NURBS)
- hp-refinement (hierarchical basis)
All with **one unified Element type** and **zero runtime overhead**.
---