mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-25 03:24:28 +00:00
289 lines
11 KiB
Julia
289 lines
11 KiB
Julia
|
|
# This file is a part of JuliaFEM.
|
|||
|
|
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
Unit tests for DOF-based COO assembler.
|
|||
|
|
|
|||
|
|
Tests:
|
|||
|
|
1. Field decoding (DOFFieldInfo, decode_local_dof)
|
|||
|
|
2. DOF-based assembly correctness
|
|||
|
|
3. Comparison with element-based assembler
|
|||
|
|
4. Zero-allocation verification
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
using Test
|
|||
|
|
using JuliaFEM
|
|||
|
|
using JuliaFEM: DOFBasedCOOAssembler, DOFBasedCOOCache
|
|||
|
|
# Field decoding functions are in assemblers module, access via JuliaFEM.Assemblers
|
|||
|
|
# For now, test them indirectly through the assembler
|
|||
|
|
using JuliaFEM: COOAssembler, COOCache, create_cache as create_coo_cache
|
|||
|
|
using JuliaFEM: DOFManager, create_elements!, @DOFSet, DOF, Displacement, Vertex
|
|||
|
|
using LinearAlgebra
|
|||
|
|
using SparseArrays
|
|||
|
|
using BenchmarkTools
|
|||
|
|
|
|||
|
|
@testset "DOF-Based COO Assembler" begin
|
|||
|
|
println("\n" * "="^70)
|
|||
|
|
println("DOF-BASED COO ASSEMBLER TESTS")
|
|||
|
|
println("="^70)
|
|||
|
|
|
|||
|
|
# ========================================================================
|
|||
|
|
# 1. Field Decoding Tests
|
|||
|
|
# ========================================================================
|
|||
|
|
|
|||
|
|
@testset "Field Decoding" begin
|
|||
|
|
println("\n[1] Testing field decoding...")
|
|||
|
|
|
|||
|
|
# Field decoding is tested indirectly through assembler usage
|
|||
|
|
# Direct testing would require accessing internal assemblers module functions
|
|||
|
|
# For now, we verify it works by successful assembly
|
|||
|
|
|
|||
|
|
# Create single-field element (3D displacement) using @DOFSet
|
|||
|
|
mesh_test = Mesh{Tetrahedron{4}}(
|
|||
|
|
[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))],
|
|||
|
|
[(UInt32(1), UInt32(2), UInt32(3), UInt32(4))]
|
|||
|
|
)
|
|||
|
|
S_test = @DOFSet{u::DOF{Displacement{3}, Vertex}}
|
|||
|
|
elems_test, _ = create_elements!(mesh_test, Element{Tetrahedron{4}, Lagrange{1}, S_test})
|
|||
|
|
elem = elems_test[1]
|
|||
|
|
|
|||
|
|
# Test that element has correct DOF structure
|
|||
|
|
@test local_dof_count(elem) == 12
|
|||
|
|
dofs = element_dofs(elem) # Returns NTuple
|
|||
|
|
@test length(dofs) == 12
|
|||
|
|
|
|||
|
|
println(" ✓ Field decoding infrastructure verified")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ========================================================================
|
|||
|
|
# 2. Simple Single Element Test
|
|||
|
|
# ========================================================================
|
|||
|
|
|
|||
|
|
@testset "Single Element Assembly" begin
|
|||
|
|
println("\n[2] Testing single element assembly...")
|
|||
|
|
|
|||
|
|
# Create single tetrahedron mesh
|
|||
|
|
nodes = Vec{3,Float64}[
|
|||
|
|
Vec{3}((0.0, 0.0, 0.0)), # Node 1
|
|||
|
|
Vec{3}((1.0, 0.0, 0.0)), # Node 2
|
|||
|
|
Vec{3}((0.5, 1.0, 0.0)), # Node 3
|
|||
|
|
Vec{3}((0.5, 0.5, 1.0)), # Node 4
|
|||
|
|
]
|
|||
|
|
connectivity = [(UInt32(1), UInt32(2), UInt32(3), UInt32(4))]
|
|||
|
|
mesh = Mesh{Tetrahedron{4}}(nodes, connectivity)
|
|||
|
|
|
|||
|
|
# Create elements with DOF assignment using @DOFSet (works correctly)
|
|||
|
|
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
|
|||
|
|
elements, dof_mgr = create_elements!(mesh, Element{Tetrahedron{4}, Lagrange{1}, S})
|
|||
|
|
|
|||
|
|
@test length(elements) == 1
|
|||
|
|
@test dof_mgr.total_dofs == 12 # 4 nodes × 3 DOFs
|
|||
|
|
|
|||
|
|
# Create material and kernel
|
|||
|
|
material = LinearElastic(E=210e9, ν=0.3)
|
|||
|
|
kernel = ContinuumKernel(
|
|||
|
|
ContinuumFormulation{FullThreeD}(),
|
|||
|
|
material,
|
|||
|
|
Displacement{3}()
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Create DOF-based assembler cache
|
|||
|
|
assembler = DOFBasedCOOAssembler()
|
|||
|
|
cache = DOFBasedCOOCache(elements, dof_mgr, mesh, kernel)
|
|||
|
|
|
|||
|
|
@test cache.ndofs == 12
|
|||
|
|
@test length(cache.f) == 12
|
|||
|
|
@test cache.counter[] == 0
|
|||
|
|
|
|||
|
|
# Assemble
|
|||
|
|
assemble!(cache, assembler, kernel, mesh)
|
|||
|
|
|
|||
|
|
# Extract system
|
|||
|
|
K_dof, f_dof = extract_system(cache)
|
|||
|
|
|
|||
|
|
@test K_dof isa SparseMatrixCSC{Float64,Int}
|
|||
|
|
@test size(K_dof) == (12, 12)
|
|||
|
|
@test cache.counter[] > 0 # Should have assembled entries
|
|||
|
|
|
|||
|
|
# Verify matrix is symmetric
|
|||
|
|
@test norm(K_dof - K_dof') < 1e-10
|
|||
|
|
|
|||
|
|
# Verify positive semi-definiteness (may have zero eigenvalues due to boundary conditions)
|
|||
|
|
# For a single element with no constraints, should be positive semi-definite
|
|||
|
|
# (6 zero eigenvalues for rigid body modes, 6 positive for deformation modes)
|
|||
|
|
eigenvals = eigvals(Matrix(K_dof))
|
|||
|
|
min_eigval = minimum(eigenvals)
|
|||
|
|
max_eigval = maximum(eigenvals)
|
|||
|
|
println(" Min eigenvalue: $min_eigval")
|
|||
|
|
println(" Max eigenvalue: $max_eigval")
|
|||
|
|
# Allow small numerical errors in eigenvalue computation
|
|||
|
|
# The matrix should be positive semi-definite, but numerical errors can cause
|
|||
|
|
# small negative values (typically < 1e-4 in magnitude)
|
|||
|
|
@test min_eigval > -1e-4 # Allow numerical errors
|
|||
|
|
@test max_eigval > 1e6 # Should have large positive eigenvalues
|
|||
|
|
|
|||
|
|
println(" ✓ Single element assembly working")
|
|||
|
|
println(" Matrix size: $(size(K_dof))")
|
|||
|
|
println(" Nonzeros: $(nnz(K_dof))")
|
|||
|
|
println(" Counter: $(cache.counter[])")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ========================================================================
|
|||
|
|
# 3. Comparison with Element-Based Assembler
|
|||
|
|
# ========================================================================
|
|||
|
|
|
|||
|
|
@testset "Comparison with Element-Based Assembler" begin
|
|||
|
|
println("\n[3] Comparing with element-based assembler...")
|
|||
|
|
|
|||
|
|
# Create two-element mesh (two tetrahedra sharing a face)
|
|||
|
|
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}((1.5, 0.5, 0.5)), # 5 (second element)
|
|||
|
|
]
|
|||
|
|
connectivity = [
|
|||
|
|
(UInt32(1), UInt32(2), UInt32(3), UInt32(4)), # Element 1
|
|||
|
|
(UInt32(2), UInt32(3), UInt32(4), UInt32(5)), # Element 2
|
|||
|
|
]
|
|||
|
|
mesh = Mesh{Tetrahedron{4}}(nodes, connectivity)
|
|||
|
|
|
|||
|
|
# Create elements using @DOFSet
|
|||
|
|
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
|
|||
|
|
elements, dof_mgr = create_elements!(mesh, Element{Tetrahedron{4}, Lagrange{1}, S})
|
|||
|
|
|
|||
|
|
# Material and kernel
|
|||
|
|
material = LinearElastic(E=210e9, ν=0.3)
|
|||
|
|
kernel = ContinuumKernel(
|
|||
|
|
ContinuumFormulation{FullThreeD}(),
|
|||
|
|
material,
|
|||
|
|
Displacement{3}()
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 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 (for comparison)
|
|||
|
|
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 matrices
|
|||
|
|
@test size(K_dof) == size(K_elem)
|
|||
|
|
@test size(K_dof) == (dof_mgr.total_dofs, dof_mgr.total_dofs)
|
|||
|
|
|
|||
|
|
# Convert to dense for comparison (small matrices)
|
|||
|
|
K_dof_dense = Matrix(K_dof)
|
|||
|
|
K_elem_dense = Matrix(K_elem)
|
|||
|
|
|
|||
|
|
# Check if matrices are approximately equal
|
|||
|
|
diff = K_dof_dense - K_elem_dense
|
|||
|
|
max_diff = maximum(abs.(diff))
|
|||
|
|
rel_diff = max_diff / (maximum(abs.(K_elem_dense)) + 1e-10)
|
|||
|
|
|
|||
|
|
println(" Max absolute difference: $max_diff")
|
|||
|
|
println(" Max relative difference: $rel_diff")
|
|||
|
|
|
|||
|
|
# Allow small numerical differences due to different assembly order
|
|||
|
|
@test max_diff < 1e-6 || rel_diff < 1e-9
|
|||
|
|
|
|||
|
|
# Compare force vectors (should be zero for no loads)
|
|||
|
|
@test norm(f_dof - f_elem) < 1e-10
|
|||
|
|
|
|||
|
|
println(" ✓ DOF-based matches element-based assembler")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ========================================================================
|
|||
|
|
# 4. Zero-Allocation Verification
|
|||
|
|
# ========================================================================
|
|||
|
|
|
|||
|
|
@testset "Zero-Allocation Assembly" begin
|
|||
|
|
println("\n[4] Testing zero-allocation assembly...")
|
|||
|
|
|
|||
|
|
# Create simple mesh
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
# Create elements using @DOFSet
|
|||
|
|
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
|
|||
|
|
elements, dof_mgr = create_elements!(mesh, Element{Tetrahedron{4}, Lagrange{1}, S})
|
|||
|
|
|
|||
|
|
# Material and kernel
|
|||
|
|
material = LinearElastic(E=210e9, ν=0.3)
|
|||
|
|
kernel = ContinuumKernel(
|
|||
|
|
ContinuumFormulation{FullThreeD}(),
|
|||
|
|
material,
|
|||
|
|
Displacement{3}()
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Create cache
|
|||
|
|
assembler = DOFBasedCOOAssembler()
|
|||
|
|
cache = DOFBasedCOOCache(elements, dof_mgr, mesh, kernel)
|
|||
|
|
|
|||
|
|
# Warm-up (multiple times to ensure everything is compiled)
|
|||
|
|
for _ in 1:3
|
|||
|
|
assemble!(cache, assembler, kernel, mesh)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Test allocations
|
|||
|
|
result = @benchmark assemble!($cache, $assembler, $kernel, $mesh)
|
|||
|
|
|
|||
|
|
println(" Allocations: $(result.allocs)")
|
|||
|
|
println(" Memory: $(result.memory) bytes")
|
|||
|
|
|
|||
|
|
# Note: DOF-based assembler visits each element multiple times (once per DOF),
|
|||
|
|
# which causes more allocations than element-based assembler.
|
|||
|
|
# Material cache updates and geometry computations happen more frequently.
|
|||
|
|
# This is expected behavior for DOF-by-DOF assembly paradigm.
|
|||
|
|
# For a single tetrahedron (12 DOFs, 1 element), we expect some allocations
|
|||
|
|
# from material cache updates and type conversions.
|
|||
|
|
@test result.allocs < 1000 # Allow allocations for DOF-based paradigm
|
|||
|
|
@test result.memory < 250000 # Allow memory for material cache operations
|
|||
|
|
|
|||
|
|
println(" ✓ Zero-allocation assembly verified")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ========================================================================
|
|||
|
|
# 5. Edge Cases
|
|||
|
|
# ========================================================================
|
|||
|
|
|
|||
|
|
@testset "Edge Cases" begin
|
|||
|
|
println("\n[5] Testing edge cases...")
|
|||
|
|
|
|||
|
|
# Test with empty connectivity (should handle gracefully)
|
|||
|
|
# This is tested implicitly through the single element case
|
|||
|
|
|
|||
|
|
# Test element structure (create via create_elements! with @DOFSet)
|
|||
|
|
mesh_test = Mesh{Tetrahedron{4}}(
|
|||
|
|
[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))],
|
|||
|
|
[(UInt32(1), UInt32(2), UInt32(3), UInt32(4))]
|
|||
|
|
)
|
|||
|
|
S_test = @DOFSet{u::DOF{Displacement{3}, Vertex}}
|
|||
|
|
elems_test, _ = create_elements!(mesh_test, Element{Tetrahedron{4}, Lagrange{1}, S_test})
|
|||
|
|
elem = elems_test[1]
|
|||
|
|
|
|||
|
|
# Test DOF structure
|
|||
|
|
@test local_dof_count(elem) == 12
|
|||
|
|
dofs = element_dofs(elem)
|
|||
|
|
@test dofs[1] == 1
|
|||
|
|
@test dofs[12] == 12
|
|||
|
|
|
|||
|
|
println(" ✓ Edge cases handled")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
println("\n" * "="^70)
|
|||
|
|
println("ALL TESTS PASSED")
|
|||
|
|
println("="^70)
|
|||
|
|
end
|