mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-29 13:00:29 +00:00
0cfe966063
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
303 lines
11 KiB
Markdown
303 lines
11 KiB
Markdown
---
|
||
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.
|