mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-08-18 11:20:12 +00:00
c9f951ef16
Tests for element-by-element assembly approach with sparse matrix operations. Test coverage: - ElementAssemblyData construction and initialization - DOF indexing for sequential and non-sequential nodes - Element contribution structures (K_local, f_int, f_ext) - Scatter operation to global arrays - Overlapping element accumulation - Residual computation (f_ext - f_int) - Matrix-vector product - Dirichlet BC application (penalty method) - Symmetry preservation - Reset functionality - Assembly statistics printing
326 lines
9.6 KiB
Julia
326 lines
9.6 KiB
Julia
# Tests for traditional element assembly structures
|
||
#
|
||
# Validates the element-by-element assembly approach
|
||
|
||
using Test
|
||
using Tensors
|
||
using LinearAlgebra
|
||
using SparseArrays
|
||
using Printf
|
||
|
||
include("../src/element_assembly_structures.jl")
|
||
|
||
@testset "Element Assembly Structures" begin
|
||
|
||
@testset "ElementAssemblyData - Construction" begin
|
||
ndof = 30 # 10 nodes × 3 DOF
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
@test assembly.ndof == 30
|
||
@test size(assembly.K_global) == (30, 30)
|
||
@test length(assembly.r_global) == 30
|
||
@test length(assembly.f_int_global) == 30
|
||
@test length(assembly.f_ext_global) == 30
|
||
|
||
@test nnz(assembly.K_global) == 0 # Empty initially
|
||
@test all(iszero, assembly.r_global)
|
||
@test all(iszero, assembly.f_int_global)
|
||
@test all(iszero, assembly.f_ext_global)
|
||
|
||
println("\n✓ ElementAssemblyData construction")
|
||
end
|
||
|
||
@testset "DOF Indexing" begin
|
||
# Single Tet4 element with nodes [1,2,3,4]
|
||
conn = (1, 2, 3, 4)
|
||
gdofs = get_dof_indices(conn, 3)
|
||
|
||
expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
||
@test gdofs == expected
|
||
|
||
# Element with non-sequential nodes
|
||
conn2 = (5, 7, 12, 15)
|
||
gdofs2 = get_dof_indices(conn2, 3)
|
||
|
||
expected2 = [13, 14, 15, 19, 20, 21, 34, 35, 36, 43, 44, 45]
|
||
@test gdofs2 == expected2
|
||
|
||
println("✓ DOF indexing")
|
||
end
|
||
|
||
@testset "Element Contribution - Single Element" begin
|
||
# Create simple element contribution
|
||
conn = (1, 2, 3, 4)
|
||
gdofs = get_dof_indices(conn, 3)
|
||
|
||
contrib = ElementContribution(1, gdofs, Float64)
|
||
|
||
@test contrib.element_id == 1
|
||
@test contrib.gdofs == gdofs
|
||
@test size(contrib.K_local) == (12, 12)
|
||
@test length(contrib.f_int_local) == 12
|
||
@test length(contrib.f_ext_local) == 12
|
||
|
||
# Fill with test values
|
||
contrib.K_local[1, 1] = 100.0
|
||
contrib.K_local[1, 2] = 50.0
|
||
contrib.f_int_local[1] = 10.0
|
||
contrib.f_ext_local[1] = 5.0
|
||
|
||
@test contrib.K_local[1, 1] == 100.0
|
||
@test contrib.f_int_local[1] == 10.0
|
||
|
||
println("✓ Element contribution construction")
|
||
end
|
||
|
||
@testset "Scatter to Global - Single Element" begin
|
||
ndof = 12 # 4 nodes × 3 DOF
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
# Create element contribution
|
||
conn = (1, 2, 3, 4)
|
||
gdofs = get_dof_indices(conn, 3)
|
||
contrib = ElementContribution(1, gdofs, Float64)
|
||
|
||
# Fill with identity-like stiffness
|
||
for i in 1:12
|
||
contrib.K_local[i, i] = 1.0
|
||
end
|
||
contrib.f_int_local[1] = 10.0
|
||
contrib.f_ext_local[1] = 5.0
|
||
|
||
# Scatter
|
||
scatter_to_global!(assembly, contrib)
|
||
|
||
# Check results
|
||
@test assembly.K_global[1, 1] == 1.0
|
||
@test assembly.K_global[6, 6] == 1.0
|
||
@test assembly.f_int_global[1] == 10.0
|
||
@test assembly.f_ext_global[1] == 5.0
|
||
|
||
println("✓ Scatter to global (single element)")
|
||
end
|
||
|
||
@testset "Scatter to Global - Overlapping Elements" begin
|
||
ndof = 15 # 5 nodes × 3 DOF
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
# Element 1: nodes [1,2,3,4]
|
||
conn1 = (1, 2, 3, 4)
|
||
gdofs1 = get_dof_indices(conn1, 3)
|
||
contrib1 = ElementContribution(1, gdofs1, Float64)
|
||
|
||
# Element 2: nodes [2,3,4,5] - shares nodes with element 1
|
||
conn2 = (2, 3, 4, 5)
|
||
gdofs2 = get_dof_indices(conn2, 3)
|
||
contrib2 = ElementContribution(2, gdofs2, Float64)
|
||
|
||
# Fill element 1
|
||
for i in 1:12
|
||
contrib1.K_local[i, i] = 1.0
|
||
end
|
||
contrib1.f_int_local[4] = 10.0 # DOF 4 (node 2, x-direction)
|
||
|
||
# Fill element 2
|
||
for i in 1:12
|
||
contrib2.K_local[i, i] = 2.0
|
||
end
|
||
contrib2.f_int_local[1] = 5.0 # DOF 4 (node 2, x-direction) - same global DOF!
|
||
|
||
# Scatter both
|
||
scatter_to_global!(assembly, contrib1)
|
||
scatter_to_global!(assembly, contrib2)
|
||
|
||
# Check accumulation
|
||
# Node 2, DOF x (global DOF 4): should have contributions from both elements
|
||
@test assembly.K_global[4, 4] == 1.0 + 2.0 # Diagonal accumulated
|
||
@test assembly.f_int_global[4] == 10.0 + 5.0 # Force accumulated
|
||
|
||
# Node 1 (only in element 1)
|
||
@test assembly.K_global[1, 1] == 1.0
|
||
|
||
# Node 5 (only in element 2)
|
||
@test assembly.K_global[13, 13] == 2.0
|
||
|
||
println("✓ Scatter to global (overlapping elements)")
|
||
end
|
||
|
||
@testset "Residual Computation" begin
|
||
ndof = 12
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
# Set up simple forces
|
||
assembly.f_int_global[1] = 100.0
|
||
assembly.f_ext_global[1] = 30.0
|
||
assembly.f_int_global[5] = 50.0
|
||
assembly.f_ext_global[5] = 50.0 # Balanced
|
||
|
||
compute_residual!(assembly)
|
||
|
||
@test assembly.r_global[1] == 70.0 # 100 - 30
|
||
@test assembly.r_global[5] == 0.0 # 50 - 50
|
||
|
||
println("✓ Residual computation")
|
||
end
|
||
|
||
@testset "Full Assembly Workflow" begin
|
||
ndof = 15 # 5 nodes × 3 DOF
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
# Create two elements
|
||
contributions = ElementContribution{Float64}[]
|
||
|
||
# Element 1
|
||
conn1 = (1, 2, 3, 4)
|
||
contrib1 = ElementContribution(1, get_dof_indices(conn1, 3), Float64)
|
||
for i in 1:12
|
||
contrib1.K_local[i, i] = 10.0
|
||
end
|
||
contrib1.f_int_local .= 1.0
|
||
contrib1.f_ext_local .= 0.5
|
||
push!(contributions, contrib1)
|
||
|
||
# Element 2
|
||
conn2 = (2, 3, 4, 5)
|
||
contrib2 = ElementContribution(2, get_dof_indices(conn2, 3), Float64)
|
||
for i in 1:12
|
||
contrib2.K_local[i, i] = 20.0
|
||
end
|
||
contrib2.f_int_local .= 2.0
|
||
contrib2.f_ext_local .= 1.0
|
||
push!(contributions, contrib2)
|
||
|
||
# Assemble all
|
||
assemble_elements!(assembly, contributions)
|
||
|
||
# Check results
|
||
# Node 1 (only element 1)
|
||
@test assembly.K_global[1, 1] == 10.0
|
||
@test assembly.f_int_global[1] == 1.0
|
||
@test assembly.r_global[1] == 0.5 # 1.0 - 0.5
|
||
|
||
# Node 2 (both elements)
|
||
@test assembly.K_global[4, 4] == 10.0 + 20.0
|
||
@test assembly.f_int_global[4] == 1.0 + 2.0
|
||
@test assembly.r_global[4] == 1.5 # (1+2) - (0.5+1) = 3 - 1.5
|
||
|
||
# Node 5 (only element 2)
|
||
@test assembly.K_global[13, 13] == 20.0
|
||
|
||
println("✓ Full assembly workflow")
|
||
end
|
||
|
||
@testset "Matrix-Vector Product" begin
|
||
ndof = 9 # 3 nodes × 3 DOF
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
# Create simple diagonal matrix
|
||
assembly.K_global = spdiagm(0 => ones(9) .* 2.0)
|
||
|
||
v = ones(9)
|
||
w = matrix_vector_product(assembly, v)
|
||
|
||
@test w ≈ 2.0 .* ones(9)
|
||
|
||
# More complex test
|
||
v2 = collect(1.0:9.0)
|
||
w2 = matrix_vector_product(assembly, v2)
|
||
@test w2 ≈ 2.0 .* v2
|
||
|
||
println("✓ Matrix-vector product")
|
||
end
|
||
|
||
@testset "Dirichlet BC Application" begin
|
||
ndof = 12
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
# Create simple stiffness
|
||
assembly.K_global = spdiagm(0 => ones(12) .* 100.0)
|
||
assembly.r_global .= 1.0
|
||
|
||
# Fix first node (DOFs 1,2,3) to zero
|
||
fixed_dofs = [1, 2, 3]
|
||
apply_dirichlet_bc!(assembly, fixed_dofs)
|
||
|
||
# Check that diagonal increased significantly
|
||
@test assembly.K_global[1, 1] > 1e10
|
||
@test assembly.K_global[2, 2] > 1e10
|
||
@test assembly.K_global[3, 3] > 1e10
|
||
|
||
# Unfixed DOFs should be unchanged
|
||
@test assembly.K_global[4, 4] ≈ 100.0
|
||
|
||
println("✓ Dirichlet BC application")
|
||
end
|
||
|
||
@testset "Symmetry Preservation" begin
|
||
ndof = 12
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
# Create symmetric element contribution
|
||
conn = (1, 2, 3, 4)
|
||
contrib = ElementContribution(1, get_dof_indices(conn, 3), Float64)
|
||
|
||
# Symmetric matrix
|
||
for i in 1:12, j in 1:12
|
||
contrib.K_local[i, j] = i + j
|
||
contrib.K_local[j, i] = i + j # Ensure symmetry
|
||
end
|
||
|
||
scatter_to_global!(assembly, contrib)
|
||
|
||
# Check global matrix is symmetric
|
||
K_full = Matrix(assembly.K_global)
|
||
@test issymmetric(K_full)
|
||
|
||
println("✓ Symmetry preservation")
|
||
end
|
||
|
||
@testset "Reset Functionality" begin
|
||
ndof = 12
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
# Fill with data
|
||
assembly.K_global = spdiagm(0 => ones(12) .* 5.0)
|
||
assembly.f_int_global .= 10.0
|
||
assembly.f_ext_global .= 5.0
|
||
assembly.r_global .= 5.0
|
||
|
||
# Reset
|
||
reset!(assembly)
|
||
|
||
# Check everything is zero
|
||
@test nnz(assembly.K_global) == 0
|
||
@test all(iszero, assembly.f_int_global)
|
||
@test all(iszero, assembly.f_ext_global)
|
||
@test all(iszero, assembly.r_global)
|
||
|
||
println("✓ Reset functionality")
|
||
end
|
||
|
||
@testset "Statistics Printing" begin
|
||
ndof = 12
|
||
assembly = ElementAssemblyData(ndof, Float64)
|
||
|
||
# Create sparse matrix
|
||
assembly.K_global = spdiagm(0 => ones(12) .* 100.0,
|
||
1 => ones(11) .* 50.0,
|
||
-1 => ones(11) .* 50.0)
|
||
assembly.f_int_global .= 10.0
|
||
assembly.f_ext_global .= 5.0
|
||
compute_residual!(assembly)
|
||
|
||
println("\n")
|
||
print_assembly_stats(assembly)
|
||
|
||
# Just check it doesn't error
|
||
@test true
|
||
end
|
||
end
|
||
|
||
println("\n" * "="^70)
|
||
println("SUMMARY: Traditional Element Assembly Validated ✓")
|
||
println("="^70)
|
||
println("All element assembly structures working correctly!")
|
||
println("Ready for comparison with nodal assembly.")
|