Files
JuliaFEM.jl/docs/book/adr-002-topology-without-node-count.md
T
Jukka Aho 0cfe966063 docs(book): Add ADR-002 for topology without hardcoded node counts
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
2025-11-09 09:33:41 +02:00

303 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "ADR 002: Topology Types Without Hardcoded Node Counts"
description: "Architecture decision to separate topology (geometry) from node count (determined by basis)"
date: "November 9, 2025"
author: "Jukka Aho"
categories: ["architecture", "design-decision", "adr"]
keywords: ["topology", "basis", "lagrange", "separation-of-concerns", "node-count"]
audience: "researchers"
level: "advanced"
type: "adr"
series: "The JuliaFEM Book"
chapter: 3
status: "accepted"
supersedes: "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):
```julia
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:
1. **Geometric shape** (triangle, quadrilateral, hexahedron)
2. **Node count** (3, 6, 4, 8, 9, ...)
3. **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
```julia
# 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):**
- `Tri3` conflates: Triangle + P1 Lagrange + 3 nodes
- `Tri6` conflates: 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 geometry
- `Lagrange{Triangle, 1}` = P1 interpolation → implies 3 nodes
- `Nedelec{Triangle, 1}` = edge DOFs → still 3 nodes, DOFs on edges
- `Hierarchical{Triangle, P}` = different basis, same topology
### 3. Eliminates Combinatorial Explosion
**Old Code Aster approach:**
```text
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:**
```julia
# 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:**
```julia
# 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:**
```julia
# 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):**
```julia
# 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).
```julia
# 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
1. **Phase 1** (Current): Keep old `Tri3`, `Quad4`, etc. as topology types for compatibility
2. **Phase 1B** (Next):
- Rename topology files: `tri3.jl` → `triangle.jl`
- Create `struct Triangle <: AbstractTopology end`
- Keep `Tri3 = Triangle` as alias
3. **Phase 2**:
- Implement `Lagrange{T, P}` parametric basis
- Map old constructors: `Element(Tri3, ...)` → `Element(Triangle(), Lagrange{Triangle,1}(), ...)`
4. **Phase 3**: Deprecate old names, migration guide
### Backwards Compatibility Shims
```julia
# 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
```text
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`, `TRIA6` naming (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.