docs(book): Add ADR-002 for topology without hardcoded node counts

New Architecture Decision Record for the comprehensive book (292 lines):

- Documents decision to remove node counts from topology types
- Explains 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
- Backwards compatibility via aliases and shims
This commit is contained in:
Jukka Aho
2025-11-09 09:31:45 +02:00
parent 5e210187d8
commit d8fc224c42
@@ -0,0 +1,292 @@
# ADR 002: Topology Types Without Hardcoded Node Counts
**Date:** November 9, 2025
**Status:** Accepted
**Supersedes:** ADR 001 (partial - corrects topology design)
## 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.