From e9ead51f2455fe95b1bf5730ccdd238f0c0ea391 Mon Sep 17 00:00:00 2001 From: Jukka Aho Date: Wed, 12 Nov 2025 00:00:59 +0200 Subject: [PATCH] test: Add comprehensive linear elastic material validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tests LinearElastic material construction with parameter validation - Validates Lamé parameter computation (λ and μ from E and ν) - Stress computation tests: uniaxial extension, pure shear, hydrostatic pressure, general strain - Verifies Hooke's law: σ = λ·tr(ε)·I + 2μ·ε - Tangent modulus validation: 𝔻 = λ·I⊗I + 2μ·𝕀ˢʸᵐ (4th-order tensor) - Double contraction consistency: σ = 𝔻 ⊡ ε - Symmetry and isotropy property verification - Tests both full and simplified compute_stress() interfaces - Zero allocation and type stability checks - 279 lines validating fundamental elasticity operations with Tensors.jl --- test/test_linear_elastic.jl | 279 ++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 test/test_linear_elastic.jl diff --git a/test/test_linear_elastic.jl b/test/test_linear_elastic.jl new file mode 100644 index 0000000..07c84c8 --- /dev/null +++ b/test/test_linear_elastic.jl @@ -0,0 +1,279 @@ +""" +Unit tests for LinearElastic material model. + +Tests cover: +1. Material construction and validation +2. Lamé parameter computation +3. Stress computation (uniaxial, shear, hydrostatic, general) +4. Tangent modulus verification +5. Symmetry and isotropy +6. Zero allocation verification +7. Type stability +""" + +using Test +using Tensors + +# Load implementation +include("../src/materials/linear_elastic.jl") + +@testset "Linear Elastic Material" begin + + @testset "Material Construction" begin + # Valid construction + steel = LinearElastic(E=200e9, ν=0.3) + @test steel.E == 200e9 + @test steel.ν == 0.3 + + # Keyword constructor + aluminum = LinearElastic(E=70e9, ν=0.33) + @test aluminum.E == 70e9 + @test aluminum.ν == 0.33 + + # Invalid inputs + @test_throws ArgumentError LinearElastic(E=-100e9, ν=0.3) # Negative E + @test_throws ArgumentError LinearElastic(E=200e9, ν=0.6) # ν too large + @test_throws ArgumentError LinearElastic(E=200e9, ν=-1.1) # ν too small + end + + @testset "Lamé Parameters" begin + steel = LinearElastic(E=200e9, ν=0.3) + + # First Lamé parameter: λ = E·ν/((1+ν)(1-2ν)) + λ_expected = 200e9 * 0.3 / ((1 + 0.3) * (1 - 2 * 0.3)) + @test λ(steel) ≈ λ_expected rtol = 1e-12 + @test λ(steel) ≈ 115.38461538461539e9 rtol = 1e-10 + + # Shear modulus: μ = E/(2(1+ν)) + μ_expected = 200e9 / (2 * (1 + 0.3)) + @test μ(steel) ≈ μ_expected rtol = 1e-12 + @test μ(steel) ≈ 76.92307692307693e9 rtol = 1e-10 + + # Test inline optimization (should compile to constants) + @test @inferred λ(steel) isa Float64 + @test @inferred μ(steel) isa Float64 + end + + @testset "Stress Computation - Uniaxial Extension" begin + steel = LinearElastic(E=200e9, ν=0.3) + + # Uniaxial extension in x-direction: ε = [ε₁₁, 0, 0; 0, 0, 0; 0, 0, 0] + ε₁₁ = 0.001 + ε = SymmetricTensor{2,3}((ε₁₁, 0.0, 0.0, 0.0, 0.0, 0.0)) + + σ, 𝔻, state_new = compute_stress(steel, ε, nothing, 0.0) + + # Expected stress: σ₁₁ = (λ + 2μ)·ε₁₁, σ₂₂ = σ₃₃ = λ·ε₁₁ + λ_val = λ(steel) + μ_val = μ(steel) + σ₁₁_expected = (λ_val + 2μ_val) * ε₁₁ + σ₂₂_expected = λ_val * ε₁₁ + + @test σ[1, 1] ≈ σ₁₁_expected rtol = 1e-12 + @test σ[2, 2] ≈ σ₂₂_expected rtol = 1e-12 + @test σ[3, 3] ≈ σ₂₂_expected rtol = 1e-12 + @test σ[1, 2] ≈ 0.0 atol = 1e-15 + @test σ[1, 3] ≈ 0.0 atol = 1e-15 + @test σ[2, 3] ≈ 0.0 atol = 1e-15 + + # State should be nothing (stateless material) + @test state_new === nothing + + # Numerical check: σ₁₁ = (λ + 2μ)·ε₁₁ ≈ 269.2 MPa + @test σ[1, 1] ≈ 269.2e6 rtol = 1e-2 + @test σ[2, 2] ≈ 115.4e6 rtol = 1e-2 # λ·ε₁₁ (positive for extension) + end + + @testset "Stress Computation - Pure Shear" begin + steel = LinearElastic(E=200e9, ν=0.3) + + # Pure shear: ε₁₂ = γ/2 (engineering shear strain γ = 0.002) + γ = 0.002 + ε₁₂ = γ / 2 # Tensor shear strain + ε = SymmetricTensor{2,3}((0.0, ε₁₂, 0.0, 0.0, 0.0, 0.0)) + + σ, 𝔻, state_new = compute_stress(steel, ε, nothing, 0.0) + + # Expected stress: σ₁₂ = 2μ·ε₁₂ + μ_val = μ(steel) + σ₁₂_expected = 2μ_val * ε₁₂ + + @test σ[1, 2] ≈ σ₁₂_expected rtol = 1e-12 + @test σ[1, 1] ≈ 0.0 atol = 1e-15 + @test σ[2, 2] ≈ 0.0 atol = 1e-15 + @test σ[3, 3] ≈ 0.0 atol = 1e-15 + + # Numerical check: σ₁₂ = 2μ·(γ/2) = μ·γ ≈ 77 GPa × 0.002 = 154 MPa + @test σ[1, 2] ≈ 154e6 rtol = 1e-2 + + @test state_new === nothing + end + + @testset "Stress Computation - Hydrostatic Pressure" begin + steel = LinearElastic(E=200e9, ν=0.3) + + # Hydrostatic strain: ε = ε_vol/3 · I + ε_vol = 0.003 # Volumetric strain + ε_iso = ε_vol / 3 + ε = SymmetricTensor{2,3}((ε_iso, 0.0, 0.0, ε_iso, 0.0, ε_iso)) + + σ, 𝔻, state_new = compute_stress(steel, ε, nothing, 0.0) + + # Expected stress: σ = (λ + 2μ/3)·ε_vol·I = K·ε_vol·I + # Bulk modulus: K = λ + 2μ/3 = E/(3(1-2ν)) + λ_val = λ(steel) + μ_val = μ(steel) + K = λ_val + 2μ_val / 3 + σ_expected = K * ε_vol + + @test σ[1, 1] ≈ σ_expected rtol = 1e-12 + @test σ[2, 2] ≈ σ_expected rtol = 1e-12 + @test σ[3, 3] ≈ σ_expected rtol = 1e-12 + @test σ[1, 2] ≈ 0.0 atol = 1e-15 + @test σ[1, 3] ≈ 0.0 atol = 1e-15 + @test σ[2, 3] ≈ 0.0 atol = 1e-15 + + # Bulk modulus check + K_expected = steel.E / (3 * (1 - 2 * steel.ν)) + @test K ≈ K_expected rtol = 1e-12 + + @test state_new === nothing + end + + @testset "Stress Computation - General Strain" begin + steel = LinearElastic(E=200e9, ν=0.3) + + # General strain tensor (all components non-zero) + ε = SymmetricTensor{2,3}((0.001, 0.0005, 0.0003, -0.0002, 0.0004, 0.0006)) + + σ, 𝔻, state_new = compute_stress(steel, ε, nothing, 0.0) + + # Verify Hooke's law: σ = λ·tr(ε)·I + 2μ·ε + λ_val = λ(steel) + μ_val = μ(steel) + I = one(ε) + σ_expected = λ_val * tr(ε) * I + 2μ_val * ε + + @test σ ≈ σ_expected rtol = 1e-12 + + # Check each component explicitly + @test σ[1, 1] ≈ σ_expected[1, 1] rtol = 1e-12 + @test σ[2, 2] ≈ σ_expected[2, 2] rtol = 1e-12 + @test σ[3, 3] ≈ σ_expected[3, 3] rtol = 1e-12 + @test σ[1, 2] ≈ σ_expected[1, 2] rtol = 1e-12 + @test σ[1, 3] ≈ σ_expected[1, 3] rtol = 1e-12 + @test σ[2, 3] ≈ σ_expected[2, 3] rtol = 1e-12 + + @test state_new === nothing + end + + @testset "Tangent Modulus - Structure" begin + steel = LinearElastic(E=200e9, ν=0.3) + ε = SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)) + + σ, 𝔻, _ = compute_stress(steel, ε, nothing, 0.0) + + # Verify tangent is 4th order symmetric tensor + @test 𝔻 isa SymmetricTensor{4,3} + + # Verify 𝔻 = λ·I⊗I + 2μ·𝕀ˢʸᵐ + λ_val = λ(steel) + μ_val = μ(steel) + I = one(ε) + 𝕀ˢʸᵐ = one(SymmetricTensor{4,3,Float64}) + 𝔻_expected = λ_val * (I ⊗ I) + 2μ_val * 𝕀ˢʸᵐ + + @test 𝔻 ≈ 𝔻_expected rtol = 1e-12 + end + + @testset "Tangent Modulus - Consistency" begin + steel = LinearElastic(E=200e9, ν=0.3) + + # Tangent should be constant (independent of strain) + ε1 = SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)) + ε2 = SymmetricTensor{2,3}((0.005, 0.002, 0.001, -0.003, 0.0, 0.0)) + + _, 𝔻1, _ = compute_stress(steel, ε1, nothing, 0.0) + _, 𝔻2, _ = compute_stress(steel, ε2, nothing, 0.0) + + @test 𝔻1 ≈ 𝔻2 rtol = 1e-12 + end + + @testset "Tangent Modulus - Double Contraction" begin + steel = LinearElastic(E=200e9, ν=0.3) + ε = SymmetricTensor{2,3}((0.001, 0.0005, 0.0003, -0.0002, 0.0004, 0.0006)) + + σ, 𝔻, _ = compute_stress(steel, ε, nothing, 0.0) + + # Verify σ = 𝔻 ⊡ ε (double contraction) + σ_from_tangent = 𝔻 ⊡ ε + + @test σ ≈ σ_from_tangent rtol = 1e-12 + end + + @testset "Symmetry Properties" begin + steel = LinearElastic(E=200e9, ν=0.3) + + # Stress tensor should be symmetric + ε = SymmetricTensor{2,3}((0.001, 0.0005, 0.0003, -0.0002, 0.0004, 0.0006)) + σ, _, _ = compute_stress(steel, ε, nothing, 0.0) + + @test σ[1, 2] ≈ σ[2, 1] rtol = 1e-15 + @test σ[1, 3] ≈ σ[3, 1] rtol = 1e-15 + @test σ[2, 3] ≈ σ[3, 2] rtol = 1e-15 + end + + @testset "Isotropy Verification" begin + steel = LinearElastic(E=200e9, ν=0.3) + + # Same strain magnitude in different directions → same stress magnitude + ε_x = SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)) + ε_y = SymmetricTensor{2,3}((0.0, 0.0, 0.0, 0.001, 0.0, 0.0)) + ε_z = SymmetricTensor{2,3}((0.0, 0.0, 0.0, 0.0, 0.0, 0.001)) + + σ_x, _, _ = compute_stress(steel, ε_x, nothing, 0.0) + σ_y, _, _ = compute_stress(steel, ε_y, nothing, 0.0) + σ_z, _, _ = compute_stress(steel, ε_z, nothing, 0.0) + + # σ₁₁(ε_x) should equal σ₂₂(ε_y) and σ₃₃(ε_z) + @test σ_x[1, 1] ≈ σ_y[2, 2] rtol = 1e-15 + @test σ_x[1, 1] ≈ σ_z[3, 3] rtol = 1e-15 + end + + @testset "Simplified Interface" begin + steel = LinearElastic(E=200e9, ν=0.3) + ε = SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)) + + # Test simplified call (without state and Δt) + σ1, 𝔻1, state1 = compute_stress(steel, ε) + σ2, 𝔻2, state2 = compute_stress(steel, ε, nothing, 0.0) + + @test σ1 ≈ σ2 + @test 𝔻1 ≈ 𝔻2 + @test state1 === nothing + @test state2 === nothing + end + + @testset "Zero Allocation" begin + steel = LinearElastic(E=200e9, ν=0.3) + ε = SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)) + + # First call to compile + compute_stress(steel, ε, nothing, 0.0) + + # Check allocations + allocs = @allocated compute_stress(steel, ε, nothing, 0.0) + @test allocs == 0 + end + + @testset "Type Stability" begin + steel = LinearElastic(E=200e9, ν=0.3) + ε = SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)) + + # Infer return types + result = @inferred compute_stress(steel, ε, nothing, 0.0) + + @test result isa Tuple{SymmetricTensor{2,3,Float64},SymmetricTensor{4,3,Float64},Nothing} + end + +end