test(assemblers): add assembler equivalence test

Tests equivalence between different assembler implementations.
Validates that different assembly approaches produce identical results.
This commit is contained in:
Jukka Aho
2025-12-15 07:45:54 +02:00
parent 48e6ff2cc4
commit 06592e115b
@@ -0,0 +1,333 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
"""
Test: Assembler Equivalence
Verify that all assembler implementations produce identical results:
- COOAssembler (element-based, coordinate format)
- CSCAssembler (element-based, compressed sparse column)
- NodeBasedCOOAssembler (node-based, coordinate format)
Test model: Two Hex8 elements sharing a face (minimal realistic mesh)
"""
using Test
using JuliaFEM
using LinearAlgebra
using SparseArrays
using Tensors
@testset "Assembler Equivalence - Two Hex8 Elements" begin
println("\n" * "="^70)
println("ASSEMBLER EQUIVALENCE TEST")
println("="^70)
# ========================================================================
# 1. Create Minimal Test Mesh (Two Hex8 Elements)
# ========================================================================
println("\n[1] Creating two-element mesh...")
# Two unit cubes sharing a face
# Element 1: X ∈ [0,1], Y ∈ [0,1], Z ∈ [0,1]
# Element 2: X ∈ [1,2], Y ∈ [0,1], Z ∈ [0,1]
nodes = Vec{3,Float64}[
# Element 1 nodes (bottom face at Z=0, top face at Z=1)
Vec{3}((0.0, 0.0, 0.0)), # 1
Vec{3}((1.0, 0.0, 0.0)), # 2
Vec{3}((1.0, 1.0, 0.0)), # 3
Vec{3}((0.0, 1.0, 0.0)), # 4
Vec{3}((0.0, 0.0, 1.0)), # 5
Vec{3}((1.0, 0.0, 1.0)), # 6
Vec{3}((1.0, 1.0, 1.0)), # 7
Vec{3}((0.0, 1.0, 1.0)), # 8
# Element 2 additional nodes (shares face with element 1)
Vec{3}((2.0, 0.0, 0.0)), # 9
Vec{3}((2.0, 1.0, 0.0)), # 10
Vec{3}((2.0, 0.0, 1.0)), # 11
Vec{3}((2.0, 1.0, 1.0)), # 12
]
# Connectivity (Hex8 standard ordering)
connectivity = [
(1, 2, 3, 4, 5, 6, 7, 8), # Element 1
(2, 9, 10, 3, 6, 11, 12, 7), # Element 2 (shares face 2-3-7-6 with element 1)
]
nnodes = length(nodes)
nelems = length(connectivity)
ndofs = 3 * nnodes
println(" Nodes: $nnodes")
println(" Elements: $nelems")
println(" DOFs: $ndofs")
# Create mesh
connectivity_uint32 = [NTuple{8,UInt32}(c) for c in connectivity]
element_sets = Dict{Symbol,Set{UInt32}}(:all => Set(UInt32(1):UInt32(nelems)))
mesh = Mesh{8,Hexahedron{8}}(nodes, connectivity_uint32, element_sets)
# ========================================================================
# 2. Material and Kernel
# ========================================================================
println("\n[2] Setting up physics...")
# Simple linear elastic material
E = 210e9 # Pa
ν = 0.3
material = LinearElastic(E=E, ν=ν)
# Create kernel
kernel = ContinuumKernel(
ContinuumFormulation{FullThreeD}(),
material,
Displacement{3}()
)
println(" Material: LinearElastic (E=$(E/1e9) GPa, ν=$ν)")
println(" Kernel: ContinuumKernel (FullThreeD, Displacement{3})")
# ========================================================================
# 3. Boundary Conditions
# ========================================================================
println("\n[3] Setting up boundary conditions...")
# Fix left face (X=0): nodes 1,4,5,8
fixed_nodes = [1, 4, 5, 8]
# Load right face (X=2): nodes 9,10,11,12
loaded_nodes = [9, 10, 11, 12]
# Apply unit load in X-direction
force_per_node = Vec{3}((1000.0, 0.0, 0.0)) # 1 kN per node
# Create boundary conditions
bc_dirichlet = DirichletBC()
for node in fixed_nodes
push!(bc_dirichlet.node_ids, node)
push!(bc_dirichlet.components, [1, 2, 3])
push!(bc_dirichlet.values, 0.0)
end
bc_neumann = NeumannBC()
for node in loaded_nodes
push!(bc_neumann.surface_ids, node)
push!(bc_neumann.values, force_per_node)
end
println(" Fixed nodes (X=0): $fixed_nodes")
println(" Loaded nodes (X=2): $loaded_nodes")
println(" Force per node: $(force_per_node[1]/1e3) kN")
# ========================================================================
# 4. Assemble with Each Assembler
# ========================================================================
println("\n[4] Assembling with all three assemblers...")
# -------------------------------------------------------------------------
# 4a. COOAssembler (Element-Based)
# -------------------------------------------------------------------------
println("\n [4a] COOAssembler (element-based)...")
assembler_coo = COOAssembler()
cache_coo = create_cache(assembler_coo, mesh, kernel)
t_coo = @elapsed begin
assemble!(cache_coo, assembler_coo, kernel, mesh)
K_coo, f_coo = extract_system(cache_coo)
apply_neumann_bcs!(f_coo, kernel, mesh, bc_neumann)
K_coo_bc = copy(K_coo)
f_coo_bc = copy(f_coo)
apply_dirichlet_bcs!(K_coo_bc, f_coo_bc, kernel, mesh, bc_dirichlet)
end
println(" Assembly time: $(round(t_coo*1e6, digits=2)) μs")
println(" Matrix nnz: $(nnz(K_coo))")
println(" Force norm: $(round(norm(f_coo), digits=6))")
# -------------------------------------------------------------------------
# 4b. CSCAssembler (Element-Based, Optimized)
# -------------------------------------------------------------------------
println("\n [4b] CSCAssembler (element-based, optimized)...")
assembler_csc = CSCAssembler()
cache_csc = create_cache(assembler_csc, mesh, kernel)
t_csc = @elapsed begin
assemble!(cache_csc, assembler_csc, kernel, mesh)
K_csc, f_csc = extract_system(cache_csc)
apply_neumann_bcs!(f_csc, kernel, mesh, bc_neumann)
K_csc_bc = copy(K_csc)
f_csc_bc = copy(f_csc)
apply_dirichlet_bcs!(K_csc_bc, f_csc_bc, kernel, mesh, bc_dirichlet)
end
println(" Assembly time: $(round(t_csc*1e6, digits=2)) μs")
println(" Matrix nnz: $(nnz(K_csc))")
println(" Force norm: $(round(norm(f_csc), digits=6))")
# -------------------------------------------------------------------------
# 4c. NodeBasedCOOAssembler (Node-Based)
# -------------------------------------------------------------------------
println("\n [4c] NodeBasedCOOAssembler (node-based)...")
assembler_nodal = NodeBasedCOOAssembler()
cache_nodal = create_cache(assembler_nodal, mesh, kernel)
t_nodal = @elapsed begin
assemble!(cache_nodal, assembler_nodal, kernel, mesh)
K_nodal, f_nodal = extract_system(cache_nodal)
apply_neumann_bcs!(f_nodal, kernel, mesh, bc_neumann)
K_nodal_bc = copy(K_nodal)
f_nodal_bc = copy(f_nodal)
apply_dirichlet_bcs!(K_nodal_bc, f_nodal_bc, kernel, mesh, bc_dirichlet)
end
println(" Assembly time: $(round(t_nodal*1e6, digits=2)) μs")
println(" Matrix nnz: $(nnz(K_nodal))")
println(" Force norm: $(round(norm(f_nodal), digits=6))")
# ========================================================================
# 5. Compare Results (Before BC Application)
# ========================================================================
println("\n[5] Comparing assembled systems (before BC)...")
# Compare stiffness matrices
K_diff_coo_csc = norm(K_coo - K_csc)
K_diff_coo_nodal = norm(K_coo - K_nodal)
K_diff_csc_nodal = norm(K_csc - K_nodal)
K_norm = norm(K_coo)
println(" Stiffness matrix differences:")
println(" ||K_coo - K_csc||: $(K_diff_coo_csc)")
println(" ||K_coo - K_nodal||: $(K_diff_coo_nodal)")
println(" ||K_csc - K_nodal||: $(K_diff_csc_nodal)")
println(" ||K_coo|| (reference): $(K_norm)")
# Compare force vectors
f_diff_coo_csc = norm(f_coo - f_csc)
f_diff_coo_nodal = norm(f_coo - f_nodal)
f_diff_csc_nodal = norm(f_csc - f_nodal)
f_norm = norm(f_coo)
println(" Force vector differences:")
println(" ||f_coo - f_csc||: $(f_diff_coo_csc)")
println(" ||f_coo - f_nodal||: $(f_diff_coo_nodal)")
println(" ||f_csc - f_nodal||: $(f_diff_csc_nodal)")
println(" ||f_coo|| (reference): $(f_norm)")
# ========================================================================
# 6. Solve and Compare Solutions
# ========================================================================
println("\n[6] Solving systems and comparing solutions...")
u_coo = K_coo_bc \ f_coo_bc
u_csc = K_csc_bc \ f_csc_bc
u_nodal = K_nodal_bc \ f_nodal_bc
u_diff_coo_csc = norm(u_coo - u_csc)
u_diff_coo_nodal = norm(u_coo - u_nodal)
u_diff_csc_nodal = norm(u_csc - u_nodal)
u_norm = norm(u_coo)
println(" Solution differences:")
println(" ||u_coo - u_csc||: $(u_diff_coo_csc)")
println(" ||u_coo - u_nodal||: $(u_diff_coo_nodal)")
println(" ||u_csc - u_nodal||: $(u_diff_csc_nodal)")
println(" ||u_coo|| (reference): $(u_norm)")
# ========================================================================
# 7. Test Assertions
# ========================================================================
println("\n[7] Running test assertions...")
# Tolerance for floating-point comparison
rtol = 1e-10 # Relative tolerance
atol = 1e-12 # Absolute tolerance
# Test 1: Stiffness matrices are identical
@test isapprox(K_coo, K_csc, rtol=rtol, atol=atol)
@test isapprox(K_coo, K_nodal, rtol=rtol, atol=atol)
@test isapprox(K_csc, K_nodal, rtol=rtol, atol=atol)
println(" ✓ All stiffness matrices are identical (within tolerance)")
# Test 2: Force vectors are identical (should be zero for internal forces)
@test isapprox(f_coo, f_csc, rtol=rtol, atol=atol)
@test isapprox(f_coo, f_nodal, rtol=rtol, atol=atol)
@test isapprox(f_csc, f_nodal, rtol=rtol, atol=atol)
println(" ✓ All force vectors are identical (within tolerance)")
# Test 3: Solutions are identical
@test isapprox(u_coo, u_csc, rtol=rtol, atol=atol)
@test isapprox(u_coo, u_nodal, rtol=rtol, atol=atol)
@test isapprox(u_csc, u_nodal, rtol=rtol, atol=atol)
println(" ✓ All solutions are identical (within tolerance)")
# Test 4: Solutions are physically reasonable
@test !any(isnan, u_coo)
@test !any(isinf, u_coo)
@test norm(u_coo) > 0 # Solution should not be zero
println(" ✓ Solutions are physically reasonable (finite, non-zero)")
# ========================================================================
# 8. Performance Comparison
# ========================================================================
println("\n[8] Performance comparison...")
println(" Assembly times:")
println(" COO: $(round(t_coo*1e6, digits=2)) μs (baseline)")
println(" CSC: $(round(t_csc*1e6, digits=2)) μs ($(round(t_coo/t_csc, digits=2))×)")
println(" Nodal: $(round(t_nodal*1e6, digits=2)) μs ($(round(t_coo/t_nodal, digits=2))×)")
if t_csc < t_coo
println(" → CSC is $(round(t_coo/t_csc, digits=2))× faster than COO")
end
# Note: For very small problems, nodal may be slower due to overhead
# But it should scale better for large problems and GPU
if t_nodal > t_coo
println(" → Nodal is slower for this tiny problem (expected)")
println(" (Nodal assembly excels on GPU and large problems)")
end
# ========================================================================
# 9. Summary
# ========================================================================
println("\n" * "="^70)
println("TEST SUMMARY - ASSEMBLER EQUIVALENCE")
println("="^70)
println("Problem:")
println(" Elements: $nelems Hex8")
println(" Nodes: $nnodes")
println(" DOFs: $ndofs")
println()
println("Results:")
println(" All assemblers produce IDENTICAL results:")
println(" ✓ Stiffness matrices match (||K_i - K_j|| < $rtol)")
println(" ✓ Force vectors match (||f_i - f_j|| < $rtol)")
println(" ✓ Solutions match (||u_i - u_j|| < $rtol)")
println()
println("Performance (tiny problem, CPU overhead dominant):")
println(" COO: $(round(t_coo*1e6, digits=2)) μs")
println(" CSC: $(round(t_csc*1e6, digits=2)) μs")
println(" Nodal: $(round(t_nodal*1e6, digits=2)) μs")
println()
println("Status: ✓ ALL TESTS PASSED")
println("="^70)
end