Architecture Decision Record documenting topology/basis separation (292 lines):
- Explains decision to remove node counts from topology types
- Documents topology = pure geometry, basis determines node count
- Shows old Code Aster anti-pattern (TRIA3, TRIA6, QUAD4, QUAD8)
- Describes new design: Triangle + Lagrange{Triangle, P}
- Rationale: mathematical correctness, separation of concerns
- Enables edge/face DOFs (Nédélec, Raviart-Thomas)
- Eliminates combinatorial explosion (8 topologies vs hundreds)
- Consequences: extensible, correct, but breaking change
- Implementation strategy and migration plan
- Includes proper YAML front matter for book chapter
11 KiB
title, description, date, author, categories, keywords, audience, level, type, series, chapter, status, supersedes
| title | description | date | author | categories | keywords | audience | level | type | series | chapter | status | supersedes | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ADR 002: Topology Types Without Hardcoded Node Counts | Architecture decision to separate topology (geometry) from node count (determined by basis) | November 9, 2025 | Jukka Aho |
|
|
researchers | advanced | adr | The JuliaFEM Book | 3 | accepted | ADR 001 (partial) |
Context
Commercial FEM codes (Code Aster, Abaqus, Nastran) use element type names like TRIA3, TRIA6, QUAD4, QUAD8, HEXA8, HEXA20, HEXA27. These names hardcode the node count into the type identifier.
This was JuliaFEM's original approach too (inherited from Code Aster heritage):
struct Tri3 <: AbstractTopology end # 3-node triangle
struct Tri6 <: AbstractTopology end # 6-node triangle
struct Quad4 <: AbstractTopology end # 4-node quadrilateral
struct Quad8 <: AbstractTopology end # 8-node quad (serendipity)
struct Quad9 <: AbstractTopology end # 9-node quad (full tensor product)
Problem: This conflates three orthogonal concepts:
- Geometric shape (triangle, quadrilateral, hexahedron)
- Node count (3, 6, 4, 8, 9, ...)
- Implied polynomial degree (linear, quadratic, cubic)
Decision
We remove node counts from topology types. Topology defines ONLY the geometric shape of the reference element.
New Design
# Topology = Pure geometry (NO node count)
abstract type AbstractTopology end
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
# Node count is DERIVED from basis + topology
abstract type AbstractBasis end
struct Lagrange{T<:AbstractTopology, P} <: AbstractBasis end # P = polynomial degree
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
struct Serendipity{T<:AbstractTopology, P} <: AbstractBasis end
nnodes(::Serendipity{Quadrilateral, 2}) = 8 # Q2 without center node
# Element composition
Element(Triangle(), Lagrange{Triangle, 1}(), Gauss{2}(), (1,2,3)) # 3 nodes
Element(Triangle(), Lagrange{Triangle, 2}(), Gauss{3}(), (1,2,3,4,5,6)) # 6 nodes
Element(Triangle(), Lagrange{Triangle, 3}(), Gauss{4}(), (1,...,10)) # 10 nodes
Rationale
1. Mathematical Correctness
In mathematics, there is no "3-node triangle" vs "6-node triangle". There is:
- Triangle (the geometric shape)
- P1 Lagrange interpolation (implies 3 nodes at vertices)
- P2 Lagrange interpolation (implies 6 nodes)
The node count is a consequence of choosing a polynomial approximation space over a given topology.
2. Separation of Concerns
| Concern | What it defines | Example |
|---|---|---|
| Topology | Geometric shape, parametric domain | Triangle, Hexahedron |
| Basis | Polynomial space + DOF placement | Lagrange{Triangle, 2} |
| Integration | Numerical quadrature | Gauss{3} |
Old way (conflated):
Tri3conflates: Triangle + P1 Lagrange + 3 nodesTri6conflates: Triangle + P2 Lagrange + 6 nodes- Cannot use Triangle with Nédélec basis (edge DOFs)
- Cannot use Triangle with hierarchical basis
- Cannot add interior DOFs for pressure
New way (separated):
Triangle()= just geometryLagrange{Triangle, 1}= P1 interpolation → implies 3 nodesNedelec{Triangle, 1}= edge DOFs → still 3 nodes, DOFs on edgesHierarchical{Triangle, P}= different basis, same topology
3. Eliminates Combinatorial Explosion
Old Code Aster approach:
TRIA3, TRIA6, TRIA7, TRIA10 (cubic)
QUAD4, QUAD8, QUAD9
TETRA4, TETRA10
HEXA8, HEXA20, HEXA27
PENTA6, PENTA15 (wedge/prism)
PYRA5, PYRA13
Each is a separate type with duplicated code. Want reduced integration? Add HEXA8R. Want hybrid formulation? Add HEXA8H. Result: hundreds of element types.
New JuliaFEM approach:
# 8 topology types
topologies = [Point, Segment, Triangle, Quadrilateral,
Tetrahedron, Hexahedron, Pyramid, Wedge]
# × N basis families
bases = [Lagrange{T, P}, Serendipity{T, P}, Nedelec{T, P},
RaviartThomas{T, P}, Hermite{T, P}, Hierarchical{T, P}, ...]
# × M integration schemes
integrations = [Gauss{N}, Lobatto{N}, Reduced, ...]
# All combinations work automatically via composition!
No code duplication. One assemble_element() function works for all.
4. Enables Advanced Element Types
Edge elements (Nédélec) for electromagnetics:
# DOFs are on EDGES, not at nodes!
element = Element(Triangle(), Nedelec{Triangle, 1}(), Gauss{2}(), (1,2,3))
nnodes(element) # → 3 (geometric connectivity)
nedges(element) # → 3
ndofs(element) # → 3 (DOFs on edges, not nodes!)
Cannot represent this with TRIA3 (assumes nodal DOFs).
Face elements (Raviart-Thomas) for fluids:
# DOFs are on FACES
element = Element(Tetrahedron(), RaviartThomas{Tetrahedron, 1}(), Gauss{2}(),
(1,2,3,4))
nnodes(element) # → 4
nfaces(element) # → 4
ndofs(element) # → 4 (DOFs on faces!)
Mixed formulations (Taylor-Hood):
# Velocity: Q2 (9 nodes)
# Pressure: Q1 (4 nodes) but with DOFs at subset of velocity nodes
# Or: pressure DOF at element center (not at any node!)
5. Correctness: Node Count ≠ DOF Count
Critical insight: Nodes are for connectivity (graph structure). DOFs are for unknowns (linear system).
# Standard nodal element: nodes = DOFs
el = Element(Triangle(), Lagrange{Triangle, 1}(), Gauss{1}(), (1,2,3))
nnodes(el) # 3
ndofs(el) # 3 (1 DOF per node for scalar field)
# Edge element: DOFs ≠ nodes
el = Element(Triangle(), Nedelec{Triangle, 1}(), Gauss{2}(), (1,2,3))
nnodes(el) # 3 (geometric nodes for connectivity)
ndofs(el) # 3 (but DOFs are on edges, not nodes!)
# Mixed element: Multiple fields
el = Element(Quadrilateral(), TaylorHood{Quadrilateral}(), Gauss{3}(), (...))
nnodes(el) # Depends on formulation
ndofs(el, :velocity) # Q2 → 9 DOFs
ndofs(el, :pressure) # Q1 → 4 DOFs (or 1 at center)
Consequences
Positive
✅ One topology type per geometric shape (8 types total, not hundreds)
✅ Mathematically correct (topology = geometry, not interpolation)
✅ Extensible (add Nédélec, Raviart-Thomas, Hermite, ... without new topologies)
✅ No code duplication (one assembly function for all)
✅ Type system enforces correctness (basis must match topology)
✅ Educational (code teaches FEM mathematics properly)
Negative
⚠️ Breaking change from JuliaFEM v0.5.1 (but necessary for correctness)
⚠️ More complex type signatures (Element{Triangle, Lagrange{Triangle,1}, Gauss{2}, 3})
⚠️ Requires understanding separation of concerns (topology ≠ basis)
⚠️ Migration needed for old code (provide adapters and deprecation warnings)
Neutral
⚡ Type parameter becomes longer but compile-time specialization still works
⚡ Need convenience constructors for common cases
⚡ Documentation must be excellent (this ADR is part of that!)
Implementation Notes
Migration Strategy
- Phase 1 (Current): Keep old
Tri3,Quad4, etc. as topology types for compatibility - Phase 1B (Next):
- Rename topology files:
tri3.jl→triangle.jl - Create
struct Triangle <: AbstractTopology end - Keep
Tri3 = Triangleas alias
- Rename topology files:
- Phase 2:
- Implement
Lagrange{T, P}parametric basis - Map old constructors:
Element(Tri3, ...)→Element(Triangle(), Lagrange{Triangle,1}(), ...)
- Implement
- Phase 3: Deprecate old names, migration guide
Backwards Compatibility Shims
# Type aliases for transition
const Tri3 = Triangle
const Tri6 = Triangle # Wait, this doesn't make sense anymore!
# Better: Basis 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}
# Constructor adapter
function Element(::Type{Tri3}, connectivity::NTuple{3, Int})
@warn "Element(Tri3, ...) is deprecated, use Element(Triangle(), Lagrange{Triangle,1}(), ...)"
Element(Triangle(), Lagrange{Triangle, 1}(), Gauss{2}(), connectivity)
end
File Organization
src/topology/
point.jl # struct Point <: AbstractTopology end
segment.jl # struct Segment <: AbstractTopology end
triangle.jl # struct Triangle <: AbstractTopology end (not tri3.jl!)
quadrilateral.jl # struct Quadrilateral <: AbstractTopology end
tetrahedron.jl # struct Tetrahedron <: AbstractTopology end
hexahedron.jl # struct Hexahedron <: AbstractTopology end
pyramid.jl # struct Pyramid <: AbstractTopology end
wedge.jl # struct Wedge <: AbstractTopology end
src/basis/
lagrange.jl # Lagrange{T, P} implementation
serendipity.jl # Serendipity{T, P}
nedelec.jl # Nedelec{T, P}
raviart_thomas.jl # RaviartThomas{T, P}
# ... other basis families
References
- Mathematics: Ciarlet, P.G. (1978). The Finite Element Method for Elliptic Problems. Chapter on finite element spaces.
- Edge elements: Nédélec, J.C. (1980). "Mixed finite elements in R³". Numerische Mathematik.
- Code Aster documentation: Examples of
TRIA3,TRIA6naming (the anti-pattern we're fixing). - JuliaFEM v0.5.1: Previous implementation with hardcoded node counts.
Related ADRs
- ADR 001: Separation of Concerns (Topology/Interpolation/Integration) - This ADR refines the topology part
- ADR 003 (future): Parametric Basis Types (
Lagrange{T, P}implementation details) - ADR 004 (future): Element Composition Type System
Approval
Proposed by: Jukka Aho
Discussed: November 9, 2025 (AI-assisted design review session)
Status: Accepted - This is the correct mathematical and architectural approach
Implementation: Phase 1B (immediate next step after current topology/integration work)
Note: This ADR represents a fundamental insight that corrects a decades-old industry anti-pattern inherited from early FEM codes. The mathematical correctness and extensibility benefits far outweigh the migration costs.