test(dofs): add DOF connectivity test

Tests DOF connectivity system for inverse mapping.
Validates DOF-to-element connectivity for DOF-based assembly.
This commit is contained in:
Jukka Aho
2025-12-15 07:46:38 +02:00
parent b44f7b6d89
commit efbeeb2918
+305
View File
@@ -0,0 +1,305 @@
# 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 connectivity structures.
Tests:
1. DOFElementConnection creation and access
2. DOFConnectivity building and access
3. DOFConnectivityGPU building and access
4. Zero-allocation verification
5. GPU compatibility (bits types)
"""
using Test
using JuliaFEM
using JuliaFEM: DOFElementConnection, DOFConnectivity, DOFConnectivityGPU
using JuliaFEM: build_dof_connectivity, build_dof_connectivity_gpu
using JuliaFEM: connection_count, is_empty, elem_id, local_dof_idx
using JuliaFEM: DOFManager, create_elements!, @DOFSet, DOF, Displacement, Vertex
using BenchmarkTools
@testset "DOF Connectivity" begin
println("\n" * "="^70)
println("DOF CONNECTIVITY TESTS")
println("="^70)
# ========================================================================
# 1. DOFElementConnection
# ========================================================================
@testset "DOFElementConnection" begin
println("\n[1] Testing DOFElementConnection...")
# Create connection
conn = DOFElementConnection(5, 12)
@test elem_id(conn) == 5
@test local_dof_idx(conn) == 12
# Test type (bits type for GPU)
# Note: isbits() checks if an instance is a bits type (immutable, no pointers)
# DOFElementConnection is a struct with Int32 and Int16 fields - it IS a bits type
@test isbits(conn)
# Verify it's a struct (not mutable struct)
@test DOFElementConnection isa Type
# Test range validation
@test_throws ErrorException DOFElementConnection(0, 1)
@test_throws ErrorException DOFElementConnection(typemax(Int32) + 1, 1)
@test_throws ErrorException DOFElementConnection(1, 0)
@test_throws ErrorException DOFElementConnection(1, typemax(Int16) + 1)
println(" ✓ DOFElementConnection working")
end
# ========================================================================
# 2. DOFConnectivity (CPU Version)
# ========================================================================
@testset "DOFConnectivity - CPU" begin
println("\n[2] Testing DOFConnectivity (CPU)...")
# Create simple test mesh: 2 elements sharing a node
# Element 1: nodes [1, 2, 3] → DOFs [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Element 2: nodes [2, 4, 5] → DOFs [4, 5, 6, 10, 11, 12, 13, 14, 15]
# DOF 4, 5, 6 are shared (node 2)
# Create DOF manager
mgr = DOFManager()
# Create elements manually (simplified for testing)
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
ElemType = Element{Triangle{3}, Lagrange{1}, S}
# Element 1: DOFs 1-9 (3 nodes × 3 DOFs)
elem1 = ElemType(UInt(1), (UInt64(1), UInt64(2), UInt64(3), UInt64(4), UInt64(5), UInt64(6), UInt64(7), UInt64(8), UInt64(9)))
# Element 2: DOFs 4-6, 10-15 (shares node 2 with elem1)
elem2 = ElemType(UInt(2), (UInt64(4), UInt64(5), UInt64(6), UInt64(10), UInt64(11), UInt64(12), UInt64(13), UInt64(14), UInt64(15)))
elements = [elem1, elem2]
# Set total DOFs in manager
mgr.total_dofs = 15
# Build connectivity
connectivity = build_dof_connectivity(elements, mgr)
@test connectivity.n_total_dofs == 15
@test length(connectivity) == 15
# Test shared DOFs (4, 5, 6)
@test connection_count(connectivity, 4) == 2 # In both elements
@test connection_count(connectivity, 5) == 2
@test connection_count(connectivity, 6) == 2
# Test unique DOFs
@test connection_count(connectivity, 1) == 1 # Only in elem1
@test connection_count(connectivity, 10) == 1 # Only in elem2
# Test connections
conns_4 = connectivity[4]
@test length(conns_4) == 2
@test elem_id(conns_4[1]) in [1, 2]
@test elem_id(conns_4[2]) in [1, 2]
@test elem_id(conns_4[1]) != elem_id(conns_4[2])
# Test empty DOFs (if any)
# DOFs 16+ don't exist, but we only built for 1-15
# Test that accessing valid DOFs works
@test !is_empty(connectivity, 1)
@test !is_empty(connectivity, 4)
println(" ✓ DOFConnectivity (CPU) working")
end
# ========================================================================
# 3. DOFConnectivityGPU
# ========================================================================
@testset "DOFConnectivityGPU" begin
println("\n[3] Testing DOFConnectivityGPU...")
# Same test case as CPU version
mgr = DOFManager()
mgr.total_dofs = 15
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
ElemType = Element{Triangle{3}, Lagrange{1}, S}
elem1 = ElemType(UInt(1), (UInt64(1), UInt64(2), UInt64(3), UInt64(4), UInt64(5), UInt64(6), UInt64(7), UInt64(8), UInt64(9)))
elem2 = ElemType(UInt(2), (UInt64(4), UInt64(5), UInt64(6), UInt64(10), UInt64(11), UInt64(12), UInt64(13), UInt64(14), UInt64(15)))
elements = [elem1, elem2]
# Build GPU connectivity
connectivity_gpu = build_dof_connectivity_gpu(elements, mgr, max_connections=10)
@test connectivity_gpu.n_total_dofs == 15
@test connectivity_gpu.max_connections == 10
@test size(connectivity_gpu.elem_ids) == (10, 15)
@test size(connectivity_gpu.local_indices) == (10, 15)
@test length(connectivity_gpu.counts) == 15
# Test shared DOFs
@test connection_count(connectivity_gpu, 4) == 2
@test connection_count(connectivity_gpu, 5) == 2
@test connection_count(connectivity_gpu, 6) == 2
# Test unique DOFs
@test connection_count(connectivity_gpu, 1) == 1
@test connection_count(connectivity_gpu, 10) == 1
# Test data access
@test connectivity_gpu.elem_ids[1, 4] in [1, 2]
@test connectivity_gpu.elem_ids[2, 4] in [1, 2]
@test connectivity_gpu.elem_ids[1, 4] != connectivity_gpu.elem_ids[2, 4]
# Test array types (GPU-compatible - all bits types)
@test eltype(connectivity_gpu.elem_ids) == Int32
@test eltype(connectivity_gpu.local_indices) == Int16
@test eltype(connectivity_gpu.counts) == Int32
# Verify arrays are standard Julia arrays (contiguous by default for Matrix/Vector)
@test connectivity_gpu.elem_ids isa Matrix{Int32}
@test connectivity_gpu.local_indices isa Matrix{Int16}
@test connectivity_gpu.counts isa Vector{Int32}
println(" ✓ DOFConnectivityGPU working")
end
# ========================================================================
# 4. Zero-Allocation Verification
# ========================================================================
@testset "Zero-Allocation Access" begin
println("\n[4] Testing zero-allocation access...")
# Build connectivity
# Create 8 elements: 8 × 12 = 96 DOFs (within 100)
mgr = DOFManager()
mgr.total_dofs = 120 # Enough for 8 elements
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
ElemType = Element{Tetrahedron{4}, Lagrange{1}, S}
# Create 8 elements with DOFs
elements = ElemType[]
for i in 1:8
# Each element: 12 DOFs (4 nodes × 3)
start_dof = (i - 1) * 12 + 1
dofs = tuple((UInt64(d) for d in start_dof:(start_dof + 11))...)
push!(elements, ElemType(UInt(i), dofs))
end
connectivity = build_dof_connectivity(elements, mgr)
# Warm-up (ensure compilation)
n_warmup = min(96, connectivity.n_total_dofs)
for dof_i in 1:n_warmup
_ = connectivity[dof_i]
_ = connection_count(connectivity, dof_i)
end
# Test on subset of DOFs that have connections
n_test = 96 # All DOFs from 8 elements
# Test access allocations (define function in global scope for @benchmark)
test_access_func = let conn = connectivity, n = n_test
function()
@inbounds for dof_i in 1:n
connections = conn[dof_i]
count = length(connections)
for c in connections
_ = elem_id(c)
_ = local_dof_idx(c)
end
end
end
end
result = @benchmark $test_access_func()
println(" Access allocations: $(result.allocs)")
println(" Access memory: $(result.memory) bytes")
# Should be zero allocations after warmup
@test result.allocs == 0
@test result.memory == 0
println(" ✓ Zero-allocation access verified")
end
# ========================================================================
# 5. Edge Cases
# ========================================================================
@testset "Edge Cases" begin
println("\n[5] Testing edge cases...")
# Empty elements
mgr = DOFManager()
mgr.total_dofs = 0
connectivity = build_dof_connectivity(Element[], mgr)
@test connectivity.n_total_dofs == 0
@test length(connectivity.dof_to_elements) == 0
# Single element
mgr = DOFManager()
mgr.total_dofs = 12
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
ElemType = Element{Tetrahedron{4}, Lagrange{1}, S}
elem = ElemType(UInt(1), (UInt64(1), UInt64(2), UInt64(3), UInt64(4), UInt64(5), UInt64(6), UInt64(7), UInt64(8), UInt64(9), UInt64(10), UInt64(11), UInt64(12)))
connectivity = build_dof_connectivity([elem], mgr)
@test connectivity.n_total_dofs == 12
@test all(count -> count == 1, [connection_count(connectivity, i) for i in 1:12])
# Invalid DOF index
mgr2 = DOFManager()
mgr2.total_dofs = 10
elem_invalid = ElemType(UInt(1), (UInt64(1), UInt64(2), UInt64(3), UInt64(4), UInt64(5), UInt64(6), UInt64(7), UInt64(8), UInt64(9), UInt64(10), UInt64(11), UInt64(12))) # DOF 11, 12 invalid
@test_throws ErrorException build_dof_connectivity([elem_invalid], mgr2)
println(" ✓ Edge cases handled")
end
# ========================================================================
# 6. GPU Compatibility
# ========================================================================
@testset "GPU Compatibility" begin
println("\n[6] Testing GPU compatibility...")
# Test that DOFElementConnection is a bits type (GPU-compatible)
conn_test = DOFElementConnection(1, 1)
@test isbits(conn_test)
# Verify it's a struct type (not mutable)
@test DOFElementConnection isa Type
# Test that GPU connectivity arrays are transferable
mgr = DOFManager()
mgr.total_dofs = 50
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
ElemType = Element{Tetrahedron{4}, Lagrange{1}, S}
elements = [ElemType(UInt(i), tuple((UInt64(d) for d in 1:12)...)) for i in 1:5]
conn_gpu = build_dof_connectivity_gpu(elements, mgr, max_connections=10)
# All array element types should be bits types (for GPU transfer)
# Test with instances, not types
@test isbits(Int32(1))
@test isbits(Int16(1))
@test eltype(conn_gpu.elem_ids) == Int32
@test eltype(conn_gpu.local_indices) == Int16
@test eltype(conn_gpu.counts) == Int32
# Arrays should be standard Julia arrays (contiguous by default)
@test conn_gpu.elem_ids isa Matrix{Int32}
@test conn_gpu.local_indices isa Matrix{Int16}
@test conn_gpu.counts isa Vector{Int32}
println(" ✓ GPU compatibility verified")
end
println("\n" * "="^70)
println("ALL TESTS PASSED")
println("="^70)
end