test(assemblers): add simple DOF-based COO test

Simple test case for DOF-based COO assembler.
Basic validation of DOF-based assembly workflow.
This commit is contained in:
Jukka Aho
2025-12-15 07:46:05 +02:00
parent 15cb606f86
commit 8c6e91b1ef
@@ -0,0 +1,353 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
"""
Simple unit tests for DOF-based assembler to debug correctness issues.
These tests are designed to be minimal and focused on specific aspects:
1. Single element correctness
2. Two-element correctness (shared node)
3. DOF mapping verification
4. Entry-by-entry comparison
"""
using Test
using JuliaFEM
using JuliaFEM: DOFBasedCOOAssembler, DOFBasedCOOCache
using JuliaFEM: COOAssembler, COOCache, create_cache as create_coo_cache
using JuliaFEM: DOFManager, create_elements!, @DOFSet, DOF, Displacement, Vertex
using LinearAlgebra
using SparseArrays
@testset "DOF-Based Assembler - Simple Debug Tests" begin
println("\n" * "="^70)
println("DOF-BASED ASSEMBLER - SIMPLE DEBUG TESTS")
println("="^70)
# ========================================================================
# Test 1: Single Tetrahedron - Full Matrix Comparison
# ========================================================================
@testset "Test 1: Single Tetrahedron" begin
println("\n[Test 1] Single tetrahedron element...")
# Create single tetrahedron
nodes = Vec{3,Float64}[
Vec{3}((0.0, 0.0, 0.0)),
Vec{3}((1.0, 0.0, 0.0)),
Vec{3}((0.5, 1.0, 0.0)),
Vec{3}((0.5, 0.5, 1.0)),
]
connectivity = [(UInt32(1), UInt32(2), UInt32(3), UInt32(4))]
mesh = Mesh{Tetrahedron{4}}(nodes, connectivity)
# Material and kernel
material = LinearElastic(E=210e9, ν=0.3)
kernel = ContinuumKernel(
ContinuumFormulation{FullThreeD}(),
material,
Displacement{3}()
)
# Create elements
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
elements, dof_mgr = create_elements!(mesh, Element{Tetrahedron{4}, Lagrange{1}, S})
# DOF-based assembly
assembler_dof = DOFBasedCOOAssembler()
cache_dof = DOFBasedCOOCache(elements, dof_mgr, mesh, kernel)
assemble!(cache_dof, assembler_dof, kernel, mesh)
K_dof, f_dof = extract_system(cache_dof)
# Element-based assembly
assembler_elem = COOAssembler()
cache_elem = create_coo_cache(assembler_elem, mesh, kernel)
assemble!(cache_elem, assembler_elem, kernel, mesh)
K_elem, f_elem = extract_system(cache_elem)
# Compare
K_dof_dense = Matrix(K_dof)
K_elem_dense = Matrix(K_elem)
diff = K_dof_dense - K_elem_dense
max_diff = maximum(abs.(diff))
max_val = maximum(abs.(K_elem_dense))
rel_diff = max_diff / (max_val + 1e-10)
println(" Matrix size: $(size(K_dof))")
println(" Max absolute difference: $max_diff")
println(" Max matrix value: $max_val")
println(" Max relative difference: $rel_diff")
# Print first few entries if they differ
if max_diff > 1e-6
println(" First differing entries:")
count = 0
for i in 1:min(12, size(K_dof, 1))
for j in 1:min(12, size(K_dof, 2))
if abs(diff[i, j]) > 1e-6
println(" K[$i,$j]: elem=$(K_elem_dense[i,j]), dof=$(K_dof_dense[i,j]), diff=$(diff[i,j])")
count += 1
if count >= 10
break
end
end
end
count >= 10 && break
end
end
@test size(K_dof) == size(K_elem)
@test nnz(K_dof) == nnz(K_elem)
@test max_diff < 1e-6 || rel_diff < 1e-9
@test norm(f_dof - f_elem) < 1e-10
end
# ========================================================================
# Test 2: Two Tetrahedra Sharing a Face
# ========================================================================
@testset "Test 2: Two Tetrahedra (Shared Face)" begin
println("\n[Test 2] Two tetrahedra sharing a face...")
# Two tetrahedra sharing face (1,2,3)
nodes = Vec{3,Float64}[
Vec{3}((0.0, 0.0, 0.0)), # 1
Vec{3}((1.0, 0.0, 0.0)), # 2
Vec{3}((0.5, 1.0, 0.0)), # 3
Vec{3}((0.5, 0.5, 1.0)), # 4
Vec{3}((0.5, 0.5, -1.0)), # 5
]
connectivity = [
(UInt32(1), UInt32(2), UInt32(3), UInt32(4)), # Element 1
(UInt32(1), UInt32(2), UInt32(3), UInt32(5)), # Element 2
]
mesh = Mesh{Tetrahedron{4}}(nodes, connectivity)
# Material and kernel
material = LinearElastic(E=210e9, ν=0.3)
kernel = ContinuumKernel(
ContinuumFormulation{FullThreeD}(),
material,
Displacement{3}()
)
# Create elements
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
elements, dof_mgr = create_elements!(mesh, Element{Tetrahedron{4}, Lagrange{1}, S})
println(" Elements: $(length(elements))")
println(" Total DOFs: $(dof_mgr.total_dofs)")
println(" Element 1 DOFs: $(element_dofs(elements[1]))")
println(" Element 2 DOFs: $(element_dofs(elements[2]))")
# DOF-based assembly
assembler_dof = DOFBasedCOOAssembler()
cache_dof = DOFBasedCOOCache(elements, dof_mgr, mesh, kernel)
assemble!(cache_dof, assembler_dof, kernel, mesh)
K_dof, f_dof = extract_system(cache_dof)
# Element-based assembly
assembler_elem = COOAssembler()
cache_elem = create_coo_cache(assembler_elem, mesh, kernel)
assemble!(cache_elem, assembler_elem, kernel, mesh)
K_elem, f_elem = extract_system(cache_elem)
# Compare
K_dof_dense = Matrix(K_dof)
K_elem_dense = Matrix(K_elem)
diff = K_dof_dense - K_elem_dense
max_diff = maximum(abs.(diff))
max_val = maximum(abs.(K_elem_dense))
rel_diff = max_diff / (max_val + 1e-10)
println(" Matrix size: $(size(K_dof))")
println(" Max absolute difference: $max_diff")
println(" Max relative difference: $rel_diff")
# Check shared node entries (nodes 1, 2, 3 are shared)
# These should have contributions from both elements
shared_node_dofs = [1, 2, 3, 4, 5, 6, 7, 8, 9] # DOFs for nodes 1, 2, 3
println(" Checking shared node entries (DOFs 1-9):")
max_shared_diff = 0.0
for i in shared_node_dofs, j in shared_node_dofs
d = abs(diff[i, j])
if d > max_shared_diff
max_shared_diff = d
end
if d > 1e-6
println(" K[$i,$j]: elem=$(K_elem_dense[i,j]), dof=$(K_dof_dense[i,j]), diff=$d")
end
end
println(" Max difference in shared node block: $max_shared_diff")
@test size(K_dof) == size(K_elem)
@test nnz(K_dof) == nnz(K_elem)
@test max_diff < 1e-6 || rel_diff < 1e-9
end
# ========================================================================
# Test 3: Single Hex8 Element
# ========================================================================
@testset "Test 3: Single Hex8 Element" begin
println("\n[Test 3] Single Hex8 element...")
# Create single Hex8 element
nodes = Vec{3,Float64}[
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
]
connectivity = [(UInt32(1), UInt32(2), UInt32(3), UInt32(4),
UInt32(5), UInt32(6), UInt32(7), UInt32(8))]
mesh = Mesh{8,Hexahedron{8}}(nodes, connectivity)
# Material and kernel
material = LinearElastic(E=210e9, ν=0.3)
kernel = ContinuumKernel(
ContinuumFormulation{FullThreeD}(),
material,
Displacement{3}()
)
# Create elements
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
elements, dof_mgr = create_elements!(mesh, Element{Hexahedron{8}, Lagrange{1}, S})
println(" Elements: $(length(elements))")
println(" Total DOFs: $(dof_mgr.total_dofs)")
println(" Element DOFs: $(element_dofs(elements[1]))")
# DOF-based assembly
assembler_dof = DOFBasedCOOAssembler()
cache_dof = DOFBasedCOOCache(elements, dof_mgr, mesh, kernel)
assemble!(cache_dof, assembler_dof, kernel, mesh)
K_dof, f_dof = extract_system(cache_dof)
# Element-based assembly
assembler_elem = COOAssembler()
cache_elem = create_coo_cache(assembler_elem, mesh, kernel)
assemble!(cache_elem, assembler_elem, kernel, mesh)
K_elem, f_elem = extract_system(cache_elem)
# Compare
K_dof_dense = Matrix(K_dof)
K_elem_dense = Matrix(K_elem)
diff = K_dof_dense - K_elem_dense
max_diff = maximum(abs.(diff))
max_val = maximum(abs.(K_elem_dense))
rel_diff = max_diff / (max_val + 1e-10)
println(" Matrix size: $(size(K_dof))")
println(" Max absolute difference: $max_diff")
println(" Max relative difference: $rel_diff")
@test size(K_dof) == size(K_elem)
@test nnz(K_dof) == nnz(K_elem)
@test max_diff < 1e-6 || rel_diff < 1e-9
end
# ========================================================================
# Test 4: Two Hex8 Elements (2×1×1 mesh)
# ========================================================================
@testset "Test 4: Two Hex8 Elements (2×1×1)" begin
println("\n[Test 4] Two Hex8 elements (2×1×1 mesh)...")
# Create 2×1×1 mesh (2 elements along X)
nodes = Vec{3,Float64}[]
for iz in 0:1, iy in 0:1, ix in 0:2
push!(nodes, Vec{3}((Float64(ix), Float64(iy), Float64(iz))))
end
connectivity = NTuple{8,UInt32}[]
for iz in 0:0, iy in 0:0, ix in 0:1
# Bottom face
n1 = ix + iy * 3 + iz * 3 * 2 + 1
n2 = (ix + 1) + iy * 3 + iz * 3 * 2 + 1
n3 = (ix + 1) + (iy + 1) * 3 + iz * 3 * 2 + 1
n4 = ix + (iy + 1) * 3 + iz * 3 * 2 + 1
# Top face
n5 = ix + iy * 3 + (iz + 1) * 3 * 2 + 1
n6 = (ix + 1) + iy * 3 + (iz + 1) * 3 * 2 + 1
n7 = (ix + 1) + (iy + 1) * 3 + (iz + 1) * 3 * 2 + 1
n8 = ix + (iy + 1) * 3 + (iz + 1) * 3 * 2 + 1
push!(connectivity, (n1, n2, n3, n4, n5, n6, n7, n8))
end
element_sets = Dict{Symbol,Set{UInt32}}(:all => Set(UInt32(1):UInt32(2)))
mesh = Mesh{8,Hexahedron{8}}(nodes, connectivity, element_sets)
# Material and kernel
material = LinearElastic(E=210e9, ν=0.3)
kernel = ContinuumKernel(
ContinuumFormulation{FullThreeD}(),
material,
Displacement{3}()
)
# Create elements
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
elements, dof_mgr = create_elements!(mesh, Element{Hexahedron{8}, Lagrange{1}, S})
println(" Nodes: $(length(nodes))")
println(" Elements: $(length(elements))")
println(" Total DOFs: $(dof_mgr.total_dofs)")
# DOF-based assembly
assembler_dof = DOFBasedCOOAssembler()
cache_dof = DOFBasedCOOCache(elements, dof_mgr, mesh, kernel)
assemble!(cache_dof, assembler_dof, kernel, mesh)
K_dof, f_dof = extract_system(cache_dof)
# Element-based assembly
assembler_elem = COOAssembler()
cache_elem = create_coo_cache(assembler_elem, mesh, kernel)
assemble!(cache_elem, assembler_elem, kernel, mesh)
K_elem, f_elem = extract_system(cache_elem)
# Compare
K_dof_dense = Matrix(K_dof)
K_elem_dense = Matrix(K_elem)
diff = K_dof_dense - K_elem_dense
max_diff = maximum(abs.(diff))
max_val = maximum(abs.(K_elem_dense))
rel_diff = max_diff / (max_val + 1e-10)
println(" Matrix size: $(size(K_dof))")
println(" Max absolute difference: $max_diff")
println(" Max relative difference: $rel_diff")
# Check shared face entries (nodes 2, 3, 6, 7 are shared between elements)
# Node 2: DOFs 4,5,6; Node 3: DOFs 7,8,9; Node 6: DOFs 16,17,18; Node 7: DOFs 19,20,21
shared_dofs = [4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21]
println(" Checking shared face entries:")
max_shared_diff = 0.0
count = 0
for i in shared_dofs, j in shared_dofs
d = abs(diff[i, j])
if d > max_shared_diff
max_shared_diff = d
end
if d > 1e-6 && count < 5
println(" K[$i,$j]: elem=$(K_elem_dense[i,j]), dof=$(K_dof_dense[i,j]), diff=$d")
count += 1
end
end
println(" Max difference in shared face block: $max_shared_diff")
@test size(K_dof) == size(K_elem)
@test nnz(K_dof) == nnz(K_elem)
@test max_diff < 1e-6 || rel_diff < 1e-9
end
println("\n" * "="^70)
println("ALL SIMPLE DEBUG TESTS COMPLETE")
println("="^70)
end