mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-26 20:01:32 +00:00
dffdf01af2
The stiffness check already targets the shipped COO assembler; drop marketing-style labels from comments and console banners so failures read like ordinary regressions.
332 lines
13 KiB
Julia
332 lines
13 KiB
Julia
"""
|
||
# Validation Test: Hex8 Element Stiffness Matrix
|
||
|
||
**What:** Validates Hex8 assembly against analytical reference from Felippa's AFEM textbook
|
||
|
||
**Why:**
|
||
- **Academic validation**: Uses Professor Felippa's standard FEM benchmark
|
||
- **Numerical accuracy**: Verifies 2×2×2 Gauss quadrature gives exact result
|
||
- **Reference implementation**: Compared against Python/NumPy symbolic computation
|
||
- **Trust**: If this passes, we know assembly is mathematically correct
|
||
- Critical for all Hex8-based structural analysis
|
||
|
||
**Validation Source:**
|
||
- Author: Professor Carlos A. Felippa
|
||
- Course: Advanced Finite Element Method (AFEM)
|
||
- Institution: University of Colorado Boulder - Center for Aerospace Structures
|
||
- Chapter 17: "The Linear Hexahedron"
|
||
- URL: https://www.colorado.edu/engineering/CAS/courses.d/AFEM.d/AFEM.Ch17.pdf
|
||
|
||
**Reference Implementation:**
|
||
- File: `test/symbolic_hex8_stiffness.py`
|
||
- Method: Numerical integration using 2×2×2 Gauss quadrature
|
||
- Tool: Python with NumPy (lambdified SymPy shape functions)
|
||
- Status: ✅ Verified (difference < 10^-14)
|
||
|
||
**Test Geometry:**
|
||
Unit cube Hex8 element (1m × 1m × 1m):
|
||
- Node 1: (0, 0, 0), Node 2: (1, 0, 0), Node 3: (1, 1, 0), Node 4: (0, 1, 0)
|
||
- Node 5: (0, 0, 1), Node 6: (1, 0, 1), Node 7: (1, 1, 1), Node 8: (0, 1, 1)
|
||
|
||
**Material:**
|
||
- Young's modulus E = 210 GPa (steel)
|
||
- Poisson's ratio ν = 0.3
|
||
- Lamé parameters: λ ≈ 121 GPa, μ ≈ 81 GPa
|
||
|
||
**Expected Results:**
|
||
✅ K_e is 24×24 symmetric matrix
|
||
✅ All entries match Felippa's analytical values within 1e-12
|
||
✅ Positive-definite (all eigenvalues > 0)
|
||
✅ Proper rank (6 zero modes for rigid body motion)
|
||
✅ Diagonal entries represent node stiffness
|
||
✅ Off-diagonal blocks represent node-to-node coupling
|
||
|
||
**Key Insight:**
|
||
This is NOT a random test - it's comparing against the gold standard FEM textbook.
|
||
If this fails, there's a fundamental error in shape functions, Jacobian, or integration.
|
||
"""
|
||
|
||
# test_assembly_validation_hex8.jl
|
||
#
|
||
# Validation of Hex8 element stiffness matrix against analytical solution
|
||
#
|
||
# VALIDATION SOURCE
|
||
# =================
|
||
# Professor Carlos A. Felippa
|
||
# "Advanced Finite Element Method (AFEM)"
|
||
# University of Colorado Boulder - Center for Aerospace Structures
|
||
# Chapter 17: "The Linear Hexahedron"
|
||
# URL: https://www.colorado.edu/engineering/CAS/courses.d/AFEM.d/AFEM.Ch17.pdf
|
||
#
|
||
# REFERENCE IMPLEMENTATION
|
||
# ========================
|
||
# Test file: test/symbolic_hex8_stiffness.py
|
||
# Method: Numerical integration using 2×2×2 Gauss quadrature
|
||
# Tool: Python with NumPy (lambdified SymPy shape functions)
|
||
# Status: ✅ Verified (difference < 10^-14)
|
||
#
|
||
# GEOMETRY
|
||
# ========
|
||
# Unit cube element (8 nodes):
|
||
# Node 1: (0, 0, 0)
|
||
# Node 2: (1, 0, 0)
|
||
# Node 3: (1, 1, 0)
|
||
# Node 4: (0, 1, 0)
|
||
# Node 5: (0, 0, 1)
|
||
# Node 6: (1, 0, 1)
|
||
# Node 7: (1, 1, 1)
|
||
# Node 8: (0, 1, 1)
|
||
#
|
||
# MATERIAL
|
||
# ========
|
||
# Young's modulus: E = 96
|
||
# Poisson's ratio: ν = 1/3
|
||
# Lamé parameters: λ = 72, μ = 36
|
||
#
|
||
# EXPECTED RESULT
|
||
# ===============
|
||
# The 24×24 stiffness matrix has been verified by:
|
||
# 1. Numerical integration (2×2×2 Gauss quadrature)
|
||
# 2. SymPy symbolic computation
|
||
# 3. Felippa's AFEM Chapter 17 methodology
|
||
# 4. Direct comparison with Python reference implementation
|
||
#
|
||
# Maximum difference between methods: < 10^-14 (machine precision)
|
||
#
|
||
# KEY VALIDATION POINTS
|
||
# =====================
|
||
# 1. Diagonal blocks (3×3) must match analytical values
|
||
# 2. Off-diagonal blocks must show proper coupling
|
||
# 3. Matrix must be symmetric
|
||
# 4. Row/column sums verify rigid body modes (zero energy)
|
||
# 5. Positive definiteness for stability
|
||
#
|
||
# TEST STRUCTURE
|
||
# ==============
|
||
# @testset "Hex8 Unit Cube - Analytical Validation"
|
||
# - Compute stiffness using the COO assembler
|
||
# - Compare with expected matrix (from symbolic computation)
|
||
# - Verify matrix properties (symmetry, positive definiteness)
|
||
# - Test rigid body modes (zero energy for translations/rotations)
|
||
# @testset "Hex8 Structural Properties"
|
||
# - Energy conservation
|
||
# - Patch test compatibility
|
||
# - Mesh refinement convergence
|
||
|
||
using Test
|
||
using LinearAlgebra
|
||
using JuliaFEM
|
||
using JuliaFEM: assemble!
|
||
using Tensors
|
||
|
||
@testset "Hex8 Unit Cube - Analytical Validation" begin
|
||
# Expected stiffness matrix from symbolic computation
|
||
# Source: test/symbolic_hex8_stiffness.py
|
||
# Material: E=96, ν=1/3
|
||
# Geometry: Unit cube
|
||
# Method: 2×2×2 Gauss quadrature
|
||
K_expected = [
|
||
24.0 9.0 9.0 -12.0 3.0 3.0 -9.0 -9.0 1.5 6.0 -3.0 4.5 6.0 4.5 -3.0 -9.0 1.5 -9.0 -6.0 -4.5 -4.5 -0.0 -1.5 -1.5
|
||
9.0 24.0 9.0 -3.0 6.0 4.5 -9.0 -9.0 1.5 3.0 -12.0 3.0 4.5 6.0 -3.0 -1.5 -0.0 -1.5 -4.5 -6.0 -4.5 1.5 -9.0 -9.0
|
||
9.0 9.0 24.0 -3.0 4.5 6.0 -1.5 -1.5 -0.0 4.5 -3.0 6.0 3.0 3.0 -12.0 -9.0 1.5 -9.0 -4.5 -4.5 -6.0 1.5 -9.0 -9.0
|
||
-12.0 -3.0 -3.0 24.0 -9.0 -9.0 6.0 3.0 -4.5 -9.0 9.0 -1.5 -9.0 -1.5 9.0 6.0 -4.5 3.0 -0.0 1.5 1.5 -6.0 4.5 4.5
|
||
3.0 6.0 4.5 -9.0 24.0 9.0 -3.0 -12.0 3.0 9.0 -9.0 1.5 1.5 -0.0 -1.5 -4.5 6.0 -3.0 -1.5 -9.0 -9.0 4.5 -6.0 -4.5
|
||
3.0 4.5 6.0 -9.0 9.0 24.0 -4.5 -3.0 6.0 1.5 -1.5 -0.0 9.0 1.5 -9.0 -3.0 3.0 -12.0 -1.5 -9.0 -9.0 4.5 -4.5 -6.0
|
||
-9.0 -9.0 -1.5 6.0 -3.0 -4.5 24.0 9.0 -9.0 -12.0 3.0 -3.0 -6.0 -4.5 4.5 -0.0 -1.5 1.5 6.0 4.5 3.0 -9.0 1.5 9.0
|
||
-9.0 -9.0 -1.5 3.0 -12.0 -3.0 9.0 24.0 -9.0 -3.0 6.0 -4.5 -4.5 -6.0 4.5 1.5 -9.0 9.0 4.5 6.0 3.0 -1.5 -0.0 1.5
|
||
1.5 1.5 -0.0 -4.5 3.0 6.0 -9.0 -9.0 24.0 3.0 -4.5 6.0 4.5 4.5 -6.0 -1.5 9.0 -9.0 -3.0 -3.0 -12.0 9.0 -1.5 -9.0
|
||
6.0 3.0 4.5 -9.0 9.0 1.5 -12.0 -3.0 3.0 24.0 -9.0 9.0 -0.0 1.5 -1.5 -6.0 4.5 -4.5 -9.0 -1.5 -9.0 6.0 -4.5 -3.0
|
||
-3.0 -12.0 -3.0 9.0 -9.0 -1.5 3.0 6.0 -4.5 -9.0 24.0 -9.0 -1.5 -9.0 9.0 4.5 -6.0 4.5 1.5 -0.0 1.5 -4.5 6.0 3.0
|
||
4.5 3.0 6.0 -1.5 1.5 -0.0 -3.0 -4.5 6.0 9.0 -9.0 24.0 1.5 9.0 -9.0 -4.5 4.5 -6.0 -9.0 -1.5 -9.0 3.0 -3.0 -12.0
|
||
6.0 4.5 3.0 -9.0 1.5 9.0 -6.0 -4.5 4.5 -0.0 -1.5 1.5 24.0 9.0 -9.0 -12.0 3.0 -3.0 -9.0 -9.0 -1.5 6.0 -3.0 -4.5
|
||
4.5 6.0 3.0 -1.5 -0.0 1.5 -4.5 -6.0 4.5 1.5 -9.0 9.0 9.0 24.0 -9.0 -3.0 6.0 -4.5 -9.0 -9.0 -1.5 3.0 -12.0 -3.0
|
||
-3.0 -3.0 -12.0 9.0 -1.5 -9.0 4.5 4.5 -6.0 -1.5 9.0 -9.0 -9.0 -9.0 24.0 3.0 -4.5 6.0 1.5 1.5 -0.0 -4.5 3.0 6.0
|
||
-9.0 -1.5 -9.0 6.0 -4.5 -3.0 -0.0 1.5 -1.5 -6.0 4.5 -4.5 -12.0 -3.0 3.0 24.0 -9.0 9.0 6.0 3.0 4.5 -9.0 9.0 1.5
|
||
1.5 -0.0 1.5 -4.5 6.0 3.0 -1.5 -9.0 9.0 4.5 -6.0 4.5 3.0 6.0 -4.5 -9.0 24.0 -9.0 -3.0 -12.0 -3.0 9.0 -9.0 -1.5
|
||
-9.0 -1.5 -9.0 3.0 -3.0 -12.0 1.5 9.0 -9.0 -4.5 4.5 -6.0 -3.0 -4.5 6.0 9.0 -9.0 24.0 4.5 3.0 6.0 -1.5 1.5 -0.0
|
||
-6.0 -4.5 -4.5 -0.0 -1.5 -1.5 6.0 4.5 -3.0 -9.0 1.5 -9.0 -9.0 -9.0 1.5 6.0 -3.0 4.5 24.0 9.0 9.0 -12.0 3.0 3.0
|
||
-4.5 -6.0 -4.5 1.5 -9.0 -9.0 4.5 6.0 -3.0 -1.5 -0.0 -1.5 -9.0 -9.0 1.5 3.0 -12.0 3.0 9.0 24.0 9.0 -3.0 6.0 4.5
|
||
-4.5 -4.5 -6.0 1.5 -9.0 -9.0 3.0 3.0 -12.0 -9.0 1.5 -9.0 -1.5 -1.5 -0.0 4.5 -3.0 6.0 9.0 9.0 24.0 -3.0 4.5 6.0
|
||
-0.0 1.5 1.5 -6.0 4.5 4.5 -9.0 -1.5 9.0 6.0 -4.5 3.0 6.0 3.0 -4.5 -9.0 9.0 -1.5 -12.0 -3.0 -3.0 24.0 -9.0 -9.0
|
||
-1.5 -9.0 -9.0 4.5 -6.0 -4.5 1.5 -0.0 -1.5 -4.5 6.0 -3.0 -3.0 -12.0 3.0 9.0 -9.0 1.5 3.0 6.0 4.5 -9.0 24.0 9.0
|
||
-1.5 -9.0 -9.0 4.5 -4.5 -6.0 9.0 1.5 -9.0 -3.0 3.0 -12.0 -4.5 -3.0 6.0 1.5 -1.5 -0.0 3.0 4.5 6.0 -9.0 9.0 24.0
|
||
]
|
||
|
||
println("\n" * "="^70)
|
||
println("Hex8 Unit Cube Validation")
|
||
println("="^70)
|
||
println("Reference: Felippa's AFEM Chapter 17")
|
||
println("Material: E=96, ν=1/3 (λ=72, μ=36)")
|
||
println("Geometry: Unit cube with 8 nodes")
|
||
println("Expected K: 24×24 from symbolic computation")
|
||
println("="^70)
|
||
|
||
# Create mesh with unit cube
|
||
X = Dict(
|
||
1 => Vec(0.0, 0.0, 0.0),
|
||
2 => Vec(1.0, 0.0, 0.0),
|
||
3 => Vec(1.0, 1.0, 0.0),
|
||
4 => Vec(0.0, 1.0, 0.0),
|
||
5 => Vec(0.0, 0.0, 1.0),
|
||
6 => Vec(1.0, 0.0, 1.0),
|
||
7 => Vec(1.0, 1.0, 1.0),
|
||
8 => Vec(0.0, 1.0, 1.0),
|
||
)
|
||
|
||
# Create mesh with unit cube (conn unused - just for reference)
|
||
conn = (1, 2, 3, 4, 5, 6, 7, 8)
|
||
|
||
# Material properties (E=96, ν=1/3)
|
||
E = 96.0
|
||
ν = 1.0 / 3.0
|
||
λ = E * ν / ((1 + ν) * (1 - 2ν))
|
||
μ = E / (2 * (1 + ν))
|
||
|
||
println("\nMaterial properties:")
|
||
println(" E = $E")
|
||
println(" ν = $ν")
|
||
println(" λ = $λ")
|
||
println(" μ = $μ")
|
||
|
||
# Create mesh
|
||
nodes = [X[i] for i in 1:8]
|
||
connectivity = [NTuple{8,UInt32}((1, 2, 3, 4, 5, 6, 7, 8))]
|
||
element_sets = Dict(:all => Set([UInt32(1)]))
|
||
node_sets = Dict{Symbol,Set{UInt32}}()
|
||
mesh = Mesh{8,Hexahedron{8}}(nodes, connectivity, element_sets, node_sets)
|
||
|
||
# Material
|
||
material = LinearElastic(E=E, ν=ν)
|
||
|
||
# Create kernel
|
||
kernel = ContinuumKernel(
|
||
ContinuumFormulation{FullThreeD}(),
|
||
material,
|
||
Displacement{3}()
|
||
)
|
||
|
||
# Assemble stiffness matrix
|
||
assembler = COOAssembler()
|
||
cache = create_cache(assembler, mesh, kernel)
|
||
assemble!(cache, assembler, kernel, mesh)
|
||
K, f = extract_system(cache)
|
||
K_computed = Matrix(K)
|
||
|
||
# Compare with expected
|
||
diff = K_computed - K_expected
|
||
max_diff = maximum(abs.(diff))
|
||
max_rel_error = maximum(abs.(diff) ./ (abs.(K_expected) .+ 1e-10))
|
||
|
||
println("\nComparison with symbolic computation:")
|
||
println(" Maximum absolute difference: $max_diff")
|
||
println(" Maximum relative error: $max_rel_error")
|
||
|
||
# Test 1: Exact match (within numerical precision)
|
||
@test max_diff < 1e-10
|
||
println("\n✅ Test 1 PASSED: Stiffness matrix matches analytical solution")
|
||
|
||
# Test 2: Matrix symmetry
|
||
@test norm(K_computed - K_computed') < 1e-10
|
||
println("✅ Test 2 PASSED: Stiffness matrix is symmetric")
|
||
|
||
# Test 3: Positive definiteness (all eigenvalues > 0 after removing rigid body modes)
|
||
# For unconstrained structure, first 6 eigenvalues should be near zero (rigid body modes)
|
||
eigs = eigvals(K_computed)
|
||
eigs_sorted = sort(eigs)
|
||
println("\nEigenvalue analysis:")
|
||
println(" First 6 (rigid body): $(eigs_sorted[1:6])")
|
||
println(" Last 3 (stiffest): $(eigs_sorted[end-2:end])")
|
||
@test all(eigs_sorted[1:6] .< 1e-8) # Rigid body modes
|
||
@test all(eigs_sorted[7:end] .> 0) # Deformation modes positive
|
||
println("✅ Test 3 PASSED: Eigenvalue structure correct")
|
||
|
||
# Test 4: Check specific matrix blocks
|
||
# Corner block K[1:3, 1:3] (node 1 self-coupling)
|
||
K11_computed = K_computed[1:3, 1:3]
|
||
K11_expected = K_expected[1:3, 1:3]
|
||
@test maximum(abs.(K11_computed - K11_expected)) < 1e-10
|
||
println("✅ Test 4 PASSED: Corner block K[1:3,1:3] matches")
|
||
|
||
# Test 5: Rigid body translation (zero energy)
|
||
u_trans_x = repeat([1.0, 0.0, 0.0], 8)
|
||
energy_x = dot(u_trans_x, K_computed * u_trans_x)
|
||
@test abs(energy_x) < 1e-8
|
||
println("✅ Test 5 PASSED: Zero energy for rigid translation")
|
||
|
||
# Test 6: Row sum check (equilibrium)
|
||
# For unit cube under constant stress, row sums should balance
|
||
row_sums = sum(K_computed, dims=2)
|
||
@test maximum(abs.(row_sums)) < 1e-10
|
||
println("✅ Test 6 PASSED: Row sums near zero (equilibrium)")
|
||
|
||
println("\n" * "="^70)
|
||
println("ALL TESTS PASSED! ✅")
|
||
println("Hex8 assembly produces correct stiffness matrix")
|
||
println("="^70 * "\n")
|
||
end
|
||
|
||
@testset "Hex8 Energy Conservation" begin
|
||
# Test that element conserves energy under uniform strain
|
||
nodes = [
|
||
Vec(0.0, 0.0, 0.0),
|
||
Vec(1.0, 0.0, 0.0),
|
||
Vec(1.0, 1.0, 0.0),
|
||
Vec(0.0, 1.0, 0.0),
|
||
Vec(0.0, 0.0, 1.0),
|
||
Vec(1.0, 0.0, 1.0),
|
||
Vec(1.0, 1.0, 1.0),
|
||
Vec(0.0, 1.0, 1.0),
|
||
]
|
||
|
||
connectivity = [NTuple{8,UInt32}((1, 2, 3, 4, 5, 6, 7, 8))]
|
||
element_sets = Dict(:all => Set([UInt32(1)]))
|
||
node_sets = Dict{Symbol,Set{UInt32}}()
|
||
mesh = Mesh{8,Hexahedron{8}}(nodes, connectivity, element_sets, node_sets)
|
||
|
||
E = 96.0
|
||
ν = 1.0 / 3.0
|
||
material = LinearElastic(E=E, ν=ν)
|
||
|
||
kernel = ContinuumKernel(
|
||
ContinuumFormulation{FullThreeD}(),
|
||
material,
|
||
Displacement{3}()
|
||
)
|
||
|
||
assembler = COOAssembler()
|
||
cache = create_cache(assembler, mesh, kernel)
|
||
assemble!(cache, assembler, kernel, mesh)
|
||
K, f = extract_system(cache)
|
||
K = Matrix(K)
|
||
|
||
# Uniform extension in x-direction
|
||
u = zeros(24)
|
||
for i in [2, 3, 6, 7] # Nodes on x=1 face
|
||
u[3*(i-1)+1] = 0.1 # 10% strain
|
||
end
|
||
|
||
# Strain energy
|
||
energy = 0.5 * dot(u, K * u)
|
||
|
||
# Analytical: U = 0.5 * E * ε² * Volume for uniaxial strain
|
||
# For constrained condition (ν=0 effective): U ≈ 0.5 * E * ε² * V
|
||
ε = 0.1
|
||
V = 1.0
|
||
# With full 3D stiffness, energy should be positive and reasonable
|
||
@test energy > 0
|
||
@test energy < 10.0 # Sanity check
|
||
|
||
println("Energy conservation test: Strain energy = $energy")
|
||
end
|
||
|
||
@testset "Hex8 Mesh Refinement" begin
|
||
# Test that refined mesh converges
|
||
# (Similar to Tet4 validation test structure)
|
||
|
||
println("\nMesh refinement test for Hex8 elements")
|
||
println("(Placeholder for future implementation)")
|
||
|
||
# TODO: Implement multi-element mesh refinement study
|
||
@test true
|
||
end
|