From d8fc224c42cc9cd3f991dffb9754767383e64817 Mon Sep 17 00:00:00 2001 From: Jukka Aho Date: Sun, 9 Nov 2025 09:31:45 +0200 Subject: [PATCH] docs(book): Add ADR-002 for topology without hardcoded node counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../adr-002-topology-without-node-count.md | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 docs/book/adr-002-topology-without-node-count.md diff --git a/docs/book/adr-002-topology-without-node-count.md b/docs/book/adr-002-topology-without-node-count.md new file mode 100644 index 0000000..7de79e1 --- /dev/null +++ b/docs/book/adr-002-topology-without-node-count.md @@ -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.