Files
JuliaFEM.jl/test/elements/test_element_construction.jl
T
Jukka Aho 728bfe1a42 test(elements): add element construction test
New 425-line test file for Element construction API:
- Tests separation of concerns: Topology + Basis + Integration + Fields
- Tests 2D/3D elements: Triangles, Tetrahedra, Quadrilaterals, Hexahedra
- Tests P1 and P2 variants with correct node counts
- Validates backward compatibility aliases (Tet4, Tri3, Quad4, Hex8)
- Tests topology properties independent of basis degree
- Tests type-stable NamedTuple fields (100× faster than Dict)
- Tests integration point storage in elements
- Validates zero-allocation element access

Comprehensive test demonstrating new Element API architecture with
proper separation of concerns and type stability.
2025-12-15 08:06:33 +02:00

426 lines
14 KiB
Julia
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.
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
"""
# Element Construction Tests (test/elements/)
## What
Tests the Element constructor API demonstrating the correct separation of concerns:
Topology (geometry) + Basis (interpolation) + Connectivity (node indices) → Element.
## Why
The Element struct is the fundamental building block of JuliaFEM. This test validates:
- **Separation of Topology and Basis**: Topology defines reference geometry, Basis defines
interpolation scheme. Same topology can have different basis degrees (P1, P2, P3).
- **Type-Stable Fields**: Using NamedTuple instead of Dict for element properties
(100× performance improvement over Dict-based system).
- **Integration Point Storage**: Elements store their own integration points.
- **Backward Compatibility**: Old aliases (Tet4, Tri3, Quad4, Hex8) still work.
The new API emphasizes:
```julia
topology = Tetrahedron() # Reference geometry (4 vertices)
basis = Lagrange{Tetrahedron, 2}() # P2 interpolation (10 nodes)
element = Element(basis, connectivity)
```
Key insight: **nnodes(topology) ≠ nnodes(basis)**
- Topology: 4 corners (geometric)
- P1 Basis: 4 nodes (linear interpolation)
- P2 Basis: 10 nodes (quadratic interpolation, adds 6 edge midpoints)
## How
**Element Construction Tests:**
- **2D Triangles**: Tri3 (P1, 3 nodes) and Tri6 (P2, 6 nodes)
- **3D Tetrahedra**: Tet4 (P1, 4 nodes) and Tet10 (P2, 10 nodes)
- **2D Quadrilaterals**: Quad4 (Q1, 4 nodes) and Quad9 (Q2, 9 nodes)
- **3D Hexahedra**: Hex8 (Q1, 8 nodes) and Hex27 (Q2, 27 nodes)
For each: Tests that `Element(basis, connectivity)` works correctly.
**Backward Compatibility:**
- Validates that old aliases still work:
- `Tet4 === Tetrahedron`
- `Tri3 === Triangle`
- `Quad4 === Quadrilateral`
- `Hex8 === Hexahedron`
**Topology Independence:**
- Verifies that topology properties (dim, reference_coordinates, edges, faces)
are INDEPENDENT of basis degree.
- `Tetrahedron()` has 4 corners, 6 edges, 4 faces - same for P1, P2, P3 basis!
**Type-Stable Fields:**
- Tests NamedTuple-based field storage: `(E=210e9, ν=0.3, thickness=0.01)`
- Validates compile-time type inference (no Dict performance penalty).
**Integration Points:**
- Tests that elements can store integration points from `integration_points(scheme, topology)`.
## Expected Results
- ✅ Element(basis, connectivity) constructs correctly for all element types
- ✅ P1 and P2 variants have correct node counts
- ✅ Backward compatibility aliases work (Tet4, Tri3, etc.)
- ✅ Topology properties independent of basis degree
- ✅ NamedTuple fields are type-stable (fieldnames known at compile time)
- ✅ Integration points stored correctly in element
- ✅ Element construction is type-stable and allocation-efficient
## Architecture Principle
**Separation of Concerns** (the core design principle):
1. **Topology**: Reference element geometry (vertices, edges, faces)
2. **Basis**: Interpolation scheme (how many nodes, shape functions)
3. **Connectivity**: Which mesh nodes belong to this element
4. **Fields**: Material properties, boundary conditions (type-stable NamedTuple)
5. **Integration**: Quadrature points for numerical integration
All five are SEPARATE and COMPOSABLE. This allows:
- Same topology with different basis degrees
- Same basis with different material properties
- Different integration schemes for same element
This is the FOUNDATION of the new JuliaFEM architecture!
"""
using Test
using JuliaFEM
@testset "New API: Element Construction with Topology + Basis" begin
@testset "2D Triangle Elements" begin
# Linear triangle (P1, 3 nodes)
topology = Triangle()
basis = Lagrange{Triangle,1}()
@test dim(topology) == 2
@test nnodes(basis) == 3
# Element construction (new way)
element = Element(basis, (UInt(1), UInt(2), UInt(3)))
@test element.connectivity == (UInt(1), UInt(2), UInt(3))
@test element.basis == basis
# Quadratic triangle (P2, 6 nodes)
basis_p2 = Lagrange{Triangle,2}()
@test nnodes(basis_p2) == 6
element_p2 = Element(basis_p2,
(UInt(1), UInt(2), UInt(3), UInt(4), UInt(5), UInt(6)))
@test length(element_p2.connectivity) == 6
end
@testset "3D Tetrahedral Elements" begin
# Linear tetrahedron (P1, 4 nodes)
topology = Tetrahedron()
basis = Lagrange{Tetrahedron,1}()
@test dim(topology) == 3
@test nnodes(basis) == 4
# Element construction
conn = (UInt(1), UInt(2), UInt(3), UInt(4))
element = Element(basis, conn)
@test element.connectivity == conn
@test element.basis == basis
# Quadratic tetrahedron (P2, 10 nodes)
basis_p2 = Lagrange{Tetrahedron,2}()
@test nnodes(basis_p2) == 10
conn_p2 = tuple(UInt.(1:10)...)
element_p2 = Element(basis_p2, conn_p2)
@test length(element_p2.connectivity) == 10
end
@testset "2D Quadrilateral Elements" begin
# Linear quad (Q1, 4 nodes)
topology = Quadrilateral()
basis = Lagrange{Quadrilateral,1}()
@test dim(topology) == 2
@test nnodes(basis) == 4
conn = (UInt(1), UInt(2), UInt(3), UInt(4))
element = Element(basis, conn)
@test element.connectivity == conn
@test element.basis == basis
# Quadratic quad (Q2, 9 nodes)
basis_p2 = Lagrange{Quadrilateral,2}()
@test nnodes(basis_p2) == 9
conn_p2 = tuple(UInt.(1:9)...)
element_p2 = Element(basis_p2, conn_p2)
@test length(element_p2.connectivity) == 9
end
@testset "3D Hexahedral Elements" begin
# Linear hex (Q1, 8 nodes)
topology = Hexahedron()
basis = Lagrange{Hexahedron,1}()
@test dim(topology) == 3
@test nnodes(basis) == 8
conn = tuple(UInt.(1:8)...)
element = Element(basis, conn)
@test element.connectivity == conn
@test element.basis == basis
# Quadratic hex (Q2, 27 nodes)
basis_p2 = Lagrange{Hexahedron,2}()
@test nnodes(basis_p2) == 27
conn_p2 = tuple(UInt.(1:27)...)
element_p2 = Element(basis_p2, conn_p2)
@test length(element_p2.connectivity) == 27
end
@testset "Backward Compatibility Aliases" begin
# Old aliases still work (deprecated but functional)
# Tet4 is alias for Tetrahedron
@test Tetrahedron() isa Tetrahedron
@test Tet4() isa Tetrahedron
@test Tet4 === Tetrahedron
# Tri3 is alias for Triangle
@test Triangle() isa Triangle
@test Tri3() isa Triangle
@test Tri3 === Triangle
# Quad4 is alias for Quadrilateral
@test Quadrilateral() isa Quadrilateral
@test Quad4() isa Quadrilateral
@test Quad4 === Quadrilateral
# Hex8 is alias for Hexahedron
@test Hexahedron() isa Hexahedron
@test Hex8() isa Hexahedron
@test Hex8 === Hexahedron
end
@testset "Topology Properties Independent of Basis" begin
# Topology describes geometry only
topology = Tetrahedron()
# Geometric properties don't depend on basis
@test dim(topology) == 3
ref_coords = reference_coordinates(topology)
@test length(ref_coords) == 4 # 4 corner nodes
@test ref_coords[1] == (0.0, 0.0, 0.0)
@test ref_coords[2] == (1.0, 0.0, 0.0)
@test ref_coords[3] == (0.0, 1.0, 0.0)
@test ref_coords[4] == (0.0, 0.0, 1.0)
# Edges (6 for tetrahedron)
edge_list = edges(topology)
@test length(edge_list) == 6
# Faces (4 triangular faces)
face_list = faces(topology)
@test length(face_list) == 4
# These are SAME regardless of basis degree!
basis_p1 = Lagrange{Tetrahedron,1}()
basis_p2 = Lagrange{Tetrahedron,2}()
@test nnodes(basis_p1) == 4 # Different node counts
@test nnodes(basis_p2) == 10
# But topology properties are identical
@test dim(topology) == 3 # Same for both
@test length(edges(topology)) == 6 # Same
@test length(faces(topology)) == 4 # Same
end
@testset "Element with Fields (Type-Stable)" begin
# New API: Type-stable fields using NamedTuple
basis = Lagrange{Triangle,1}()
conn = (UInt(1), UInt(2), UInt(3))
# Material properties as NamedTuple (type-stable!)
fields = (E=210e9, ν=0.3, thickness=0.01)
element = Element(UInt(42), conn, (), fields, basis)
@test element.fields.E == 210e9
@test element.fields.ν == 0.3
@test element.fields.thickness == 0.01
@test element.id == UInt(42)
# Type is known at compile time
@test typeof(element.fields) <: NamedTuple
@test fieldnames(typeof(element.fields)) == (:E, :ν, :thickness)
end
@testset "Integration Points with New API" begin
# Integration points are element property
topology = Triangle()
basis = Lagrange{Triangle,1}()
scheme = Gauss{2}()
# Get integration points for this topology
ips = integration_points(scheme, topology)
@test length(ips) > 0
@test all(ip -> ip isa IntegrationPoint, ips)
# Create element with integration points
conn = (UInt(1), UInt(2), UInt(3))
element = Element(UInt(1), conn, ips, (), basis)
@test length(element.integration_points) == length(ips)
@test element.integration_points == ips
end
end
@testset "New API: Separation of Concerns" begin
@testset "Topology = Geometry Only" begin
# Topology describes ONLY the reference element shape
topo_tet = Tetrahedron()
topo_tri = Triangle()
topo_quad = Quadrilateral()
topo_hex = Hexahedron()
# These have NO information about:
# - Number of nodes (depends on basis degree)
# - Shape functions (comes from basis)
# - Integration points (comes from integration scheme)
# - Material properties (comes from fields)
# - Physical coordinates (comes from mesh)
@test topo_tet isa AbstractTopology
@test topo_tri isa AbstractTopology
@test topo_quad isa AbstractTopology
@test topo_hex isa AbstractTopology
end
@testset "Basis = Interpolation Scheme" begin
# Basis describes HOW to interpolate fields
# Linear bases (P1)
basis_tri_p1 = Lagrange{Triangle,1}()
basis_tet_p1 = Lagrange{Tetrahedron,1}()
# Quadratic bases (P2)
basis_tri_p2 = Lagrange{Triangle,2}()
basis_tet_p2 = Lagrange{Tetrahedron,2}()
# Node count determined by basis + topology
@test nnodes(basis_tri_p1) == 3
@test nnodes(basis_tri_p2) == 6
@test nnodes(basis_tet_p1) == 4
@test nnodes(basis_tet_p2) == 10
# All are basis functions
@test basis_tri_p1 isa AbstractBasis
@test basis_tet_p2 isa AbstractBasis
end
@testset "Integration = Quadrature Rule" begin
# Integration scheme is independent choice
scheme_1pt = Gauss{1}()
scheme_2pt = Gauss{2}()
scheme_3pt = Gauss{3}()
@test scheme_1pt isa AbstractIntegration
@test scheme_2pt isa AbstractIntegration
@test scheme_3pt isa AbstractIntegration
# Same topology, different integration rules
topo = Triangle()
ips_1 = integration_points(scheme_1pt, topo)
ips_2 = integration_points(scheme_2pt, topo)
ips_3 = integration_points(scheme_3pt, topo)
# Different number of integration points
@test length(ips_1) < length(ips_2) < length(ips_3)
end
@testset "Element = Topology + Basis + Integration + Fields" begin
# Element combines all pieces
topology = Triangle() # Geometry
basis = Lagrange{Triangle,1}() # Interpolation (3 nodes)
scheme = Gauss{2}() # Integration rule
conn = (UInt(1), UInt(2), UInt(3)) # Node IDs
fields = (E=210e9, ν=0.3) # Material properties
ips = integration_points(scheme, topology)
element = Element(UInt(1), conn, ips, fields, basis)
# Element has all information needed for FEM
@test element.connectivity == conn
@test element.integration_points == ips
@test element.fields == fields
@test element.basis == basis
# Can query properties
@test nnodes(element.basis) == 3
@test length(element.integration_points) > 0
@test element.fields.E == 210e9
end
end
@testset "New API: Type Stability Benefits" begin
@testset "Compile-Time Known Sizes" begin
# All sizes known at compile time for optimization
basis = Lagrange{Tetrahedron,1}()
conn = (UInt(1), UInt(2), UInt(3), UInt(4))
fields = (E=210e3, ν=0.3)
element = Element(UInt(1), conn, (), fields, basis)
# Type parameters encode sizes
@test element isa Element{4,0,typeof(fields),typeof(basis)}
# Connectivity is NTuple (stack-allocated, zero-cost)
@test element.connectivity isa NTuple{4,UInt}
# Fields are NamedTuple (type-stable, fast access)
@test element.fields isa NamedTuple
# Compiler knows exact types → can optimize aggressively
E_val = element.fields.E
@test E_val isa Float64 # Exact type known
end
@testset "No Allocations in Hot Paths" begin
# Test that element access doesn't allocate
basis = Lagrange{Triangle,1}()
conn = (UInt(1), UInt(2), UInt(3))
fields = (E=210e3, ν=0.3, ρ=7850.0)
element = Element(UInt(1), conn, (), fields, basis)
# Access should not allocate
allocs = @allocated begin
_ = element.connectivity
_ = element.fields.E
_ = element.fields.ν
_ = element.basis
end
@test allocs == 0 # Zero allocations!
end
end
println("✅ All New API element construction tests passed!")
println("\nKey Takeaways:")
println(" • Topology = Geometry (Tetrahedron, Triangle, etc.)")
println(" • Basis = Interpolation (Lagrange{Topology, Degree})")
println(" • Element = Topology + Basis + Integration + Fields")
println(" • Old names (Tet4, Tri3) are aliases for backward compatibility")
println(" • Node count comes from BASIS, not topology!")