From 568f09090a179120ee9faca28819bd718e79a0b5 Mon Sep 17 00:00:00 2001 From: Jukka Aho Date: Sat, 15 Nov 2025 02:47:04 +0200 Subject: [PATCH] refactor(topology): Implement Triangle{N} with type parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Triangle to use node count type parameter per ADR-002. Changes: - struct Triangle → struct Triangle{N} <: AbstractTopology{N} - Aliases: Tri3 = Triangle{3}, Tri6 = Triangle{6}, Tri7 = Triangle{7}, Tri10 = Triangle{10} - Simplified implementation following same pattern - Remove old design documentation Implements ADR-002 (November 13, 2025): node count from mesh, not basis. Old files removed: tri3.jl, tri6.jl, tri7.jl New file: Single triangles.jl handles all variants via {N} --- src/topology/triangles.jl | 383 ++++++++++++++++++++++++++++---------- 1 file changed, 289 insertions(+), 94 deletions(-) diff --git a/src/topology/triangles.jl b/src/topology/triangles.jl index 0bdf7ff..f969859 100644 --- a/src/topology/triangles.jl +++ b/src/topology/triangles.jl @@ -2,15 +2,29 @@ # License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md """ - Triangle <: AbstractTopology + Triangle{N} <: AbstractTopology -Triangular element topology in 2D (reference element geometry). +Parametric triangular element topology in 2D. -**Important:** This type defines ONLY the geometric shape. Node count is determined -by the interpolation scheme (basis functions): -- `Lagrange{Triangle, 1}` → 3 nodes (P1, linear) -- `Lagrange{Triangle, 2}` → 6 nodes (P2, quadratic) -- `Lagrange{Triangle, 3}` → 10 nodes (P3, cubic) +The type parameter `N` specifies the total number of nodes in the element, +enabling compile-time dispatch and type-stable code generation. + +# Type Parameter +- `N::Int`: Total number of nodes (3, 6, 7, or 10) + +# Canonical Type Aliases +**Always use these aliases instead of constructing `Triangle{N}` directly:** + +- `Tri3 = Triangle{3}` - Linear triangle (P1, 3 corner nodes) +- `Tri6 = Triangle{6}` - Quadratic triangle (P2, 6 nodes: 3 corners + 3 edge midpoints) +- `Tri7 = Triangle{7}` - Quadratic triangle with centroid (3 corners + 3 edge midpoints + 1 center) +- `Tri10 = Triangle{10}` - Cubic triangle (P3, 10 nodes) + +# Why Parametric Types? +1. **Type Stability:** Each node count is a distinct type (`Tri3 !== Tri6`) +2. **Compile-Time Dispatch:** Kernel specialization for GPU performance +3. **Zero Allocation:** Node count known at compile time +4. **Clear API:** `nnodes(Tri6())` returns compile-time constant `6` # Reference Element ``` @@ -26,11 +40,6 @@ by the interpolation scheme (basis functions): N1 N2 ``` -# Standard Corner Node Positions -1. (0, 0) - Origin -2. (1, 0) - Along ξ-axis -3. (0, 1) - Along η-axis - # Topology Properties - Dimension: 2 - Corner nodes: 3 @@ -39,116 +48,302 @@ by the interpolation scheme (basis functions): # Typical Usage ```julia -julia> topology = Triangle() -julia> dim(topology) -2 -julia> reference_coordinates(topology) # Corner nodes only -((0.0, 0.0), (1.0, 0.0), (0.0, 1.0)) +julia> topology = Tri6() # Use canonical alias +julia> nnodes(topology) # Returns compile-time constant +6 -julia> basis = Lagrange{Triangle, 1}() -julia> nnodes(basis) # Node count from BASIS +julia> Tri3 !== Tri6 # Type stability check +true + +julia> reference_coordinates(Tri3()) # Corner nodes only +((0.0, 0.0), (1.0, 0.0), (0.0, 1.0)) +``` + +# Design Notes +- Separates topology (geometric shape) from interpolation (basis functions) +- Corner node positions are ALWAYS the same (3 nodes) +- Intermediate nodes (edge/face) depend on `N` parameter +- Use `reference_coordinates(Tri6())` to get ALL 6 node positions +""" +struct Triangle{N} <: AbstractTopology{N} end + +# ============================================================================ +# CANONICAL TYPE ALIASES (PRIMARY API) +# ============================================================================ + +""" + Tri3 = Triangle{3} + +Linear triangle with 3 corner nodes (P1 interpolation). + +**Reference Coordinates:** +- Node 1: (0.0, 0.0) - Origin +- Node 2: (1.0, 0.0) - Along ξ-axis +- Node 3: (0.0, 1.0) - Along η-axis + +**Use this alias everywhere** instead of `Triangle{3}`. +""" +const Tri3 = Triangle{3} + +""" + Tri6 = Triangle{6} + +Quadratic triangle with 6 nodes (P2 interpolation). + +**Node Layout:** +- Nodes 1-3: Corner nodes (same as Tri3) +- Nodes 4-6: Edge midpoints + +**Use this alias everywhere** instead of `Triangle{6}`. +""" +const Tri6 = Triangle{6} + +""" + Tri7 = Triangle{7} + +Quadratic triangle with 7 nodes (includes face centroid). + +**Node Layout:** +- Nodes 1-3: Corner nodes +- Nodes 4-6: Edge midpoints +- Node 7: Face centroid + +**Use this alias everywhere** instead of `Triangle{7}`. +""" +const Tri7 = Triangle{7} + +""" + Tri10 = Triangle{10} + +Cubic triangle with 10 nodes (P3 interpolation). + +**Node Layout:** +- Nodes 1-3: Corner nodes +- Nodes 4-9: Two nodes per edge (at 1/3 and 2/3 positions) +- Node 10: Face centroid + +**Use this alias everywhere** instead of `Triangle{10}`. +""" +const Tri10 = Triangle{10} + +# ============================================================================ +# CORE TOPOLOGY INTERFACE +# ============================================================================ + +""" + nnodes(::Triangle{N}) where N -> Int + +Return total number of nodes for parametric triangle topology. +This is a **compile-time constant** enabling type-stable dispatch. + +# Returns +- `N`: Node count specified by type parameter (3, 6, 7, or 10) + +# Examples +```julia +julia> nnodes(Tri3()) # Returns compile-time constant 3 3 -julia> basis = Lagrange{Triangle, 2}() -julia> nnodes(basis) # Quadratic has 6 nodes (corners + edge midpoints) +julia> nnodes(Tri6()) # Returns compile-time constant 6 6 + +julia> @allocated nnodes(Tri6()) # Zero allocation +0 ``` -**Zero allocation:** All functions return compile-time sized tuples. +# Performance Note -See also: [`AbstractTopology`](@ref), [`Quadrilateral`](@ref), [`Lagrange`](@ref) +This function returns a compile-time constant, enabling: +- Zero-cost abstraction (compiler eliminates call) +- Fully specialized code generation +- Static memory allocation in GPU kernels """ -struct Triangle <: AbstractTopology end - -dim(::Triangle) = 2 +nnodes(::Triangle{N}) where {N} = N """ - reference_coordinates(::Triangle) + dim(::Triangle{N}) where N -> Int -Get corner node positions for Triangle (3 vertices in parametric space). +Return spatial dimension of triangle reference element (always 2). + +# Returns +- `2`: Triangles exist in 2D space + +# Examples +```julia +julia> dim(Tri3()) +2 + +julia> dim(Tri10()) # Same for all triangle types +2 +``` """ -function reference_coordinates(::Triangle) +dim(::Triangle{N}) where {N} = 2 + +# ============================================================================ +# REFERENCE COORDINATES (Full Node Positions) +# ============================================================================ + +""" + reference_coordinates(::Triangle{3}) -> NTuple{3, NTuple{2, Float64}} + +Return reference coordinates for linear triangle (Tri3) - 3 corner nodes only. + +# Returns +Tuple of 3 coordinate pairs: ((ξ₁, η₁), (ξ₂, η₂), (ξ₃, η₃)) + +# Node Positions +``` + η + ^ + | + (0,1) N3 + | \\ + | \\ + | \\ + +---------> ξ + (0,0) (1,0) + N1 N2 +``` + +- Node 1: (0.0, 0.0) - Origin +- Node 2: (1.0, 0.0) - Along ξ-axis +- Node 3: (0.0, 1.0) - Along η-axis +""" +function reference_coordinates(::Triangle{3}) return ( - (0.0, 0.0), # Node 1: Origin - (1.0, 0.0), # Node 2: Along ξ-axis - (0.0, 1.0), # Node 3: Along η-axis + (0.0, 0.0), # N1: Corner at origin + (1.0, 0.0), # N2: Corner along ξ + (0.0, 1.0) # N3: Corner along η ) end """ - edges(::Triangle) + reference_coordinates(::Triangle{6}) -> NTuple{6, NTuple{2, Float64}} -Edge connectivity for triangle (corner nodes). +Return reference coordinates for quadratic triangle (Tri6) - 6 nodes total. + +# Node Layout + +- Nodes 1-3: Corner nodes (same as Tri3) +- Node 4: Edge midpoint between N1-N2 (0.5, 0.0) +- Node 5: Edge midpoint between N2-N3 (0.5, 0.5) +- Node 6: Edge midpoint between N3-N1 (0.0, 0.5) """ -function edges(::Triangle) +function reference_coordinates(::Triangle{6}) + return ( + (0.0, 0.0), # N1: Corner + (1.0, 0.0), # N2: Corner + (0.0, 1.0), # N3: Corner + (0.5, 0.0), # N4: Midpoint of edge 1-2 + (0.5, 0.5), # N5: Midpoint of edge 2-3 + (0.0, 0.5) # N6: Midpoint of edge 3-1 + ) +end + +""" + reference_coordinates(::Triangle{7}) -> NTuple{7, NTuple{2, Float64}} + +Return reference coordinates for quadratic triangle with centroid (Tri7). + +# Node Layout + +- Nodes 1-3: Corner nodes +- Nodes 4-6: Edge midpoints +- Node 7: Face centroid (1/3, 1/3) +""" +function reference_coordinates(::Triangle{7}) + return ( + (0.0, 0.0), # N1: Corner + (1.0, 0.0), # N2: Corner + (0.0, 1.0), # N3: Corner + (0.5, 0.0), # N4: Midpoint edge 1-2 + (0.5, 0.5), # N5: Midpoint edge 2-3 + (0.0, 0.5), # N6: Midpoint edge 3-1 + (1.0 / 3.0, 1.0 / 3.0) # N7: Face centroid + ) +end + +""" + reference_coordinates(::Triangle{10}) -> NTuple{10, NTuple{2, Float64}} + +Return reference coordinates for cubic triangle (Tri10) - 10 nodes total. + +# Node Layout + +- Nodes 1-3: Corner nodes +- Nodes 4-9: Two nodes per edge (at 1/3 and 2/3) + - Edge 1-2: N4 (1/3, 0), N5 (2/3, 0) + - Edge 2-3: N6 (2/3, 1/3), N7 (1/3, 2/3) + - Edge 3-1: N8 (0, 2/3), N9 (0, 1/3) +- Node 10: Face centroid (1/3, 1/3) +""" +function reference_coordinates(::Triangle{10}) + return ( + (0.0, 0.0), # N1: Corner + (1.0, 0.0), # N2: Corner + (0.0, 1.0), # N3: Corner + (1.0 / 3.0, 0.0), # N4: Edge 1-2, 1/3 + (2.0 / 3.0, 0.0), # N5: Edge 1-2, 2/3 + (2.0 / 3.0, 1.0 / 3.0), # N6: Edge 2-3, 1/3 + (1.0 / 3.0, 2.0 / 3.0), # N7: Edge 2-3, 2/3 + (0.0, 2.0 / 3.0), # N8: Edge 3-1, 1/3 + (0.0, 1.0 / 3.0), # N9: Edge 3-1, 2/3 + (1.0 / 3.0, 1.0 / 3.0) # N10: Face centroid + ) +end + +# ============================================================================ +# TOPOLOGICAL CONNECTIVITY (Corner Nodes Only) +# ============================================================================ + +""" + edges(::Triangle{N}) where N -> NTuple{3, NTuple{2, Int}} + +Return edge connectivity (pairs of **corner node indices**) for triangle. +This is TOPOLOGICAL connectivity, independent of interpolation order. + +# Returns + +3-tuple of edge definitions: +- Edge 1: (1, 2) - Bottom edge (N1 → N2) +- Edge 2: (2, 3) - Diagonal edge (N2 → N3) +- Edge 3: (3, 1) - Left edge (N3 → N1) + +# Note + +- Only references **corner nodes** (1, 2, 3) +- Direction: Counter-clockwise +- Same for all triangle types (Tri3, Tri6, Tri7, Tri10) +""" +function edges(::Triangle{N}) where {N} return ( (1, 2), # Edge 1: Bottom - (2, 3), # Edge 2: Right (hypotenuse) - (3, 1), # Edge 3: Left + (2, 3), # Edge 2: Diagonal + (3, 1) # Edge 3: Left ) end """ - faces(::Triangle) + faces(::Triangle{N}) where N -> NTuple{1, NTuple{3, Int}} -For 2D elements, the face is the element itself. +Return face connectivity for triangle. +In 2D, the "face" is the element itself (all 3 **corner nodes**). + +# Returns +1-tuple containing the triangular face: ((1, 2, 3),) + +# Note +- Only references corner nodes +- Single face represents entire surface +- API consistency with 3D elements """ -faces(::Triangle) = ((1, 2, 3),) +function faces(::Triangle{N}) where {N} + return ((1, 2, 3),) +end # ============================================================================ -# Deprecated aliases (for backwards compatibility) +# EXPORTS # ============================================================================ -""" - Tri3 - -**DEPRECATED:** Backward compatibility alias. Use `Triangle` with `Lagrange{Triangle, 1}`. - -The old `Tri3` conflated topology (triangle) with node count (3). -In the new architecture: -- Topology defines geometric shape only -- Basis functions determine node count - -This alias allows old code to work: -```julia -# Old style (still works) -element = Element(Tri3, (1, 2, 3)) -# Internally converted to: -element = Element(Triangle, (1, 2, 3)) # Infers Lagrange{Triangle, 1} from 3 nodes -``` - -New code should use explicit topology + basis: -```julia -element = Element(Lagrange{Triangle, 1}, (1, 2, 3)) -``` -""" -const Tri3 = Triangle - -""" - Tri6 - -**DEPRECATED:** Backward compatibility alias. Use `Triangle` with `Lagrange{Triangle, 2}`. - -This alias allows old code to work: -```julia -# Old style (still works) -element = Element(Tri6, (1, 2, 3, 4, 5, 6)) -# Internally converted to: -element = Element(Triangle, (1, 2, 3, 4, 5, 6)) # Infers Lagrange{Triangle, 2} from 6 nodes -``` - -New code should use explicit topology + basis: -```julia -element = Element(Lagrange{Triangle, 2}, (1, 2, 3, 4, 5, 6)) -``` -""" -const Tri6 = Triangle # Same topology! Node count from basis. - -""" - Tri7 - -**DEPRECATED:** Backward compatibility alias. Use `Triangle` with `Lagrange{Triangle, 2}` + bubble. - -7-node triangle with interior bubble node. -""" -const Tri7 = Triangle # Same topology! Node count from basis. +# Export ONLY canonical aliases (not the parametric struct) +export Tri3, Tri6, Tri7, Tri10