# This file is a part of JuliaFEM. # License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md """ # Deformation Gradient Tests (test/geometry/) ## What Tests computation of the deformation gradient **F = ∂x/∂X = I + ∂u/∂X**, which maps from reference (undeformed) to current (deformed) configuration in finite strain mechanics. ## Why The deformation gradient is **FUNDAMENTAL** to finite strain analysis: - **Green-Lagrange strain**: E = ½(F^T F - I) - **Cauchy-Green deformation tensor**: C = F^T F - **Volume ratio**: J = det(F) (incompressibility requires J = 1) - **Polar decomposition**: F = RU (rotation × stretch) - **Material frame**: All stress/strain in reference configuration Physical requirements: - **det(F) > 0**: No material inversion (orientation preserved) - **det(F) ≈ 1**: Nearly incompressible materials (rubber, metal plasticity) - **F = I**: Undeformed configuration (u = 0) This test validates: - **Identity case**: u = 0 → F = I, det(F) = 1 - **Pure translation**: ∇u = 0 → F = I (rigid body motion) - **Pure stretch**: Diagonal F with stretch ratios - **Simple shear**: Off-diagonal F terms - **Small vs finite strain**: Difference in formulations - **Physical constraints**: det(F) > 0 always - **Zero allocations**: Hot path allocates nothing ## How **Test Cases:** **1. Identity (u = 0)**: - Unit cube Hex8 element, zero displacement - Expected: F = I, det(F) = 1.0 - Both finite strain and small strain give same result **2. Pure Translation**: - Uniform displacement u = (0.5, 0.5, 0.5) at all nodes - Expected: ∇u = 0 → F = I + 0 = I - Validates that rigid body motion doesn't deform material **3. Pure Stretch (x-direction)**: - Displacement u_x = 0.1·X (10% engineering strain) - Expected: F = diag(1.1, 1.0, 1.0), det(F) = 1.1 - Validates uniaxial extension **4. Simple Shear**: - Displacement u_x = 0.1·y (shear deformation) - Expected: F_{12} = 0.1, det(F) = 1.0 (volume preserving) - Validates shear kinematics **5. Small vs Finite Strain Difference**: - 20% stretch: significant displacement gradient - **Finite strain**: F = I + ∇u (includes gradient) - **Small strain**: F = I (ignores gradient, approximation) - Validates that formulations differ for large deformations **6. Physical Constraint**: - All physical deformations must have det(F) > 0 - Negative det(F) → inverted element (unphysical) **7. Tet10 (Quadratic) Elements**: - Higher-order elements with 10 nodes - Same tests as Hex8 but with quadratic basis - Validates that API works for all element types **8. Zero Allocations**: - `@allocated compute_deformation_gradient(...)` must return 0 - Critical for performance in assembly loops ## Expected Results - ✅ **Identity**: F = I, det(F) = 1.0 - ✅ **Translation**: F = I (∇u = 0) - ✅ **Stretch**: F_{ii} = 1 + ε_{ii}, det(F) = product of stretches - ✅ **Shear**: Off-diagonal terms non-zero, det(F) = 1.0 - ✅ **Finite ≠ Small**: Different F for large deformations (20%+) - ✅ **Physical**: det(F) > 0 always - ✅ **Tet10**: Works with quadratic elements - ✅ **Zero allocations**: @allocated = 0 ## Mathematical Background **Deformation Gradient (Finite Strain):** ``` F = ∂x/∂X = I + ∂u/∂X ``` Where: - x = X + u (current position = reference + displacement) - X = reference coordinates - u = displacement vector - ∂u/∂X = displacement gradient **Small Strain Approximation:** ``` F ≈ I (ignores ∂u/∂X, valid for ||∇u|| << 1) ``` **Computation:** ```julia # Displacement gradient ∇u = ∑ᵢ uᵢ ⊗ (∂Nᵢ/∂X) # Physical derivatives ∂Nᵢ/∂X = J⁻¹ · ∂Nᵢ/∂ξ # Deformation gradient F = I + ∇u ``` ## Formulation Comparison **Finite Strain** (use when ||∇u|| > 0.01): - F = I + ∇u (full nonlinear kinematics) - E = ½(F^T F - I) (Green-Lagrange strain) - S = ∂W/∂E (2nd Piola-Kirchhoff stress) - Required for: Rubber, large rotations, metal forming **Small Strain** (use when ||∇u|| < 0.01): - F ≈ I (linear approximation) - ε = ½(∇u + ∇u^T) (engineering strain) - σ = C:ε (Cauchy stress) - Valid for: Linear elasticity, small vibrations ## Architecture Principle **Tensors.jl for Kinematics** All kinematic quantities use Tensors.jl: - Displacement: Vec{3,Float64} - Gradient: Tensor{2,3} (⊗ outer product) - Deformation gradient: Tensor{2,3} - Identity: one(Tensor{2,3}) This provides: - Natural tensor notation (F[i,j]) - Automatic differentiation ready - Zero-allocation operations - GPU compatible ## Usage Pattern ```julia # In assembly loop: for ip in integration_points # Get basis derivatives dN_dξ = get_basis_derivatives(topology, basis, ip.ξ) # Compute Jacobian J = compute_jacobian(X_nodes, dN_dξ) # Compute deformation gradient F = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) # Compute strain E = 0.5 * (F' ⊡ F - I) # Material model S, 𝔻, state = compute_stress(material, E, state_old, Δt) # ... assemble end ``` ## Critical Performance Path Deformation gradient is computed at EVERY integration point, EVERY Newton iteration. For 100k elements × 8 integration points × 10 Newton iterations = 8M calls! Zero allocations are MANDATORY! """ using Test using Tensors using LinearAlgebra # Use JuliaFEM for basis functions using JuliaFEM # Load our new deformation gradient code include("../src/physics/deformation_gradient.jl") @testset "Deformation Gradient - Low Level API" begin @testset "Identity case (u = 0)" begin # Unit cube element, no displacement X_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) ) # Zero displacement u_nodes = tuple([zero(Vec{3,Float64}) for _ in 1:8]...) # At element center ξ = (0, 0, 0) ξ = Vec(0.0, 0.0, 0.0) # Hex8 basis function derivatives at center (new API) dN_dξ = get_basis_derivatives(Hexahedron(), Lagrange{Hexahedron,1}(), ξ) # Compute Jacobian J = zero(Tensor{2,3,Float64,9}) for i in 1:8 J += X_nodes[i] ⊗ dN_dξ[i] end # Finite strain: Should give F = I + 0 = I F_finite = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) @test F_finite ≈ one(Tensor{2,3}) @test det(F_finite) ≈ 1.0 # Small strain: Should also give F = I F_small = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, SmallStrain()) @test F_small ≈ one(Tensor{2,3}) @test det(F_small) ≈ 1.0 end @testset "Pure translation" begin # Unit cube X_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) ) # Uniform translation: u = (0.5, 0.5, 0.5) everywhere u_const = Vec(0.5, 0.5, 0.5) u_nodes = tuple([u_const for _ in 1:8]...) ξ = Vec(0.0, 0.0, 0.0) dN_dξ = get_basis_derivatives(Hexahedron(), Lagrange{Hexahedron,1}(), ξ) J = zero(Tensor{2,3,Float64,9}) for i in 1:8 J += X_nodes[i] ⊗ dN_dξ[i] end # Pure translation ⇒ ∇u = 0 ⇒ F = I F = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) @test F ≈ one(Tensor{2,3}) @test det(F) ≈ 1.0 end @testset "Pure stretch in x-direction" begin # Unit cube X_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) ) # Stretch: u_x = 0.1 * X (10% stretch in x) u_nodes = ( Vec(0.0, 0.0, 0.0), # u = 0.1 * 0 = 0 Vec(0.1, 0.0, 0.0), # u = 0.1 * 1 = 0.1 Vec(0.1, 0.0, 0.0), # u = 0.1 * 1 = 0.1 Vec(0.0, 0.0, 0.0), # u = 0.1 * 0 = 0 Vec(0.0, 0.0, 0.0), # u = 0.1 * 0 = 0 Vec(0.1, 0.0, 0.0), # u = 0.1 * 1 = 0.1 Vec(0.1, 0.0, 0.0), # u = 0.1 * 1 = 0.1 Vec(0.0, 0.0, 0.0) # u = 0.1 * 0 = 0 ) ξ = Vec(0.0, 0.0, 0.0) dN_dξ = get_basis_derivatives(Hexahedron(), Lagrange{Hexahedron,1}(), ξ) J = zero(Tensor{2,3,Float64,9}) for i in 1:8 J += X_nodes[i] ⊗ dN_dξ[i] end F = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) # Expected: F = [1.1 0 0] # [0 1 0] # [0 0 1] @test F[1, 1] ≈ 1.1 atol = 1e-10 @test F[2, 2] ≈ 1.0 atol = 1e-10 @test F[3, 3] ≈ 1.0 atol = 1e-10 @test F[1, 2] ≈ 0.0 atol = 1e-10 @test F[1, 3] ≈ 0.0 atol = 1e-10 @test F[2, 3] ≈ 0.0 atol = 1e-10 @test det(F) ≈ 1.1 atol = 1e-10 end @testset "Simple shear" begin # Unit cube X_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) ) # Shear: u_x = 0.1 * y u_nodes = ( Vec(0.0, 0.0, 0.0), # y=0 Vec(0.0, 0.0, 0.0), # y=0 Vec(0.1, 0.0, 0.0), # y=1 Vec(0.1, 0.0, 0.0), # y=1 Vec(0.0, 0.0, 0.0), # y=0 Vec(0.0, 0.0, 0.0), # y=0 Vec(0.1, 0.0, 0.0), # y=1 Vec(0.1, 0.0, 0.0) # y=1 ) ξ = Vec(0.0, 0.0, 0.0) dN_dξ = get_basis_derivatives(Hexahedron(), Lagrange{Hexahedron,1}(), ξ) J = zero(Tensor{2,3,Float64,9}) for i in 1:8 J += X_nodes[i] ⊗ dN_dξ[i] end F = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) # Expected: F = [1 0.1 0] # [0 1 0] # [0 0 1] @test F[1, 1] ≈ 1.0 atol = 1e-10 @test F[1, 2] ≈ 0.1 atol = 1e-10 @test F[2, 2] ≈ 1.0 atol = 1e-10 @test F[3, 3] ≈ 1.0 atol = 1e-10 @test det(F) ≈ 1.0 atol = 1e-10 end @testset "Small vs Finite strain difference" begin # Setup with significant displacement gradient X_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) ) # 20% stretch in x u_nodes = ( Vec(0.0, 0.0, 0.0), Vec(0.2, 0.0, 0.0), Vec(0.2, 0.0, 0.0), Vec(0.0, 0.0, 0.0), Vec(0.0, 0.0, 0.0), Vec(0.2, 0.0, 0.0), Vec(0.2, 0.0, 0.0), Vec(0.0, 0.0, 0.0) ) ξ = Vec(0.0, 0.0, 0.0) dN_dξ = get_basis_derivatives(Hexahedron(), Lagrange{Hexahedron,1}(), ξ) J = zero(Tensor{2,3,Float64,9}) for i in 1:8 J += X_nodes[i] ⊗ dN_dξ[i] end F_finite = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) F_small = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, SmallStrain()) # Finite strain includes gradient @test F_finite[1, 1] ≈ 1.2 atol = 1e-10 # Small strain ignores gradient @test F_small[1, 1] ≈ 1.0 atol = 1e-10 # They should be different! @test !(F_finite ≈ F_small) end @testset "Physical constraint: det(F) > 0" begin # Physical deformation must preserve orientation X_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) ) # Small positive stretch u_nodes = ( Vec(0.0, 0.0, 0.0), Vec(0.05, 0.0, 0.0), Vec(0.05, 0.0, 0.0), Vec(0.0, 0.0, 0.0), Vec(0.0, 0.0, 0.0), Vec(0.05, 0.0, 0.0), Vec(0.05, 0.0, 0.0), Vec(0.0, 0.0, 0.0) ) ξ = Vec(0.0, 0.0, 0.0) dN_dξ = get_basis_derivatives(Hexahedron(), Lagrange{Hexahedron,1}(), ξ) J = zero(Tensor{2,3,Float64,9}) for i in 1:8 J += X_nodes[i] ⊗ dN_dξ[i] end F = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) @test det(F) > 0 # Physical requirement end end @testset "Deformation Gradient - Tet10 Element" begin @testset "Tet10: Identity case" begin # Regular tetrahedron nodes (4 corners + 6 edge midpoints) X_nodes = ( Vec(0.0, 0.0, 0.0), # 1: corner Vec(1.0, 0.0, 0.0), # 2: corner Vec(0.0, 1.0, 0.0), # 3: corner Vec(0.0, 0.0, 1.0), # 4: corner Vec(0.5, 0.0, 0.0), # 5: edge 1-2 Vec(0.5, 0.5, 0.0), # 6: edge 2-3 Vec(0.0, 0.5, 0.0), # 7: edge 3-1 Vec(0.0, 0.0, 0.5), # 8: edge 1-4 Vec(0.5, 0.0, 0.5), # 9: edge 2-4 Vec(0.0, 0.5, 0.5) # 10: edge 3-4 ) # Zero displacement u_nodes = tuple([zero(Vec{3,Float64}) for _ in 1:10]...) # At element centroid ξ = (1/4, 1/4, 1/4) ξ = Vec(0.25, 0.25, 0.25) # Tet10 basis function derivatives (new API) dN_dξ = get_basis_derivatives(Tetrahedron(), Lagrange{Tetrahedron,2}(), ξ) # Compute Jacobian J = zero(Tensor{2,3,Float64,9}) for i in 1:10 J += X_nodes[i] ⊗ dN_dξ[i] end F = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) @test F ≈ one(Tensor{2,3}) atol = 1e-10 @test det(F) ≈ 1.0 atol = 1e-10 end @testset "Tet10: Uniform stretch" begin # Regular tetrahedron X_nodes = ( Vec(0.0, 0.0, 0.0), Vec(1.0, 0.0, 0.0), Vec(0.0, 1.0, 0.0), Vec(0.0, 0.0, 1.0), Vec(0.5, 0.0, 0.0), Vec(0.5, 0.5, 0.0), Vec(0.0, 0.5, 0.0), Vec(0.0, 0.0, 0.5), Vec(0.5, 0.0, 0.5), Vec(0.0, 0.5, 0.5) ) # Isotropic expansion: u = 0.1 * X u_nodes = ( Vec(0.0, 0.0, 0.0), Vec(0.1, 0.0, 0.0), Vec(0.0, 0.1, 0.0), Vec(0.0, 0.0, 0.1), Vec(0.05, 0.0, 0.0), Vec(0.05, 0.05, 0.0), Vec(0.0, 0.05, 0.0), Vec(0.0, 0.0, 0.05), Vec(0.05, 0.0, 0.05), Vec(0.0, 0.05, 0.05) ) ξ = Vec(0.25, 0.25, 0.25) dN_dξ = get_basis_derivatives(Tetrahedron(), Lagrange{Tetrahedron,2}(), ξ) J = zero(Tensor{2,3,Float64,9}) for i in 1:10 J += X_nodes[i] ⊗ dN_dξ[i] end F = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) # Expected: F ≈ 1.1 * I @test F[1, 1] ≈ 1.1 atol = 1e-10 @test F[2, 2] ≈ 1.1 atol = 1e-10 @test F[3, 3] ≈ 1.1 atol = 1e-10 @test abs(F[1, 2]) < 1e-10 @test abs(F[1, 3]) < 1e-10 @test abs(F[2, 3]) < 1e-10 @test det(F) ≈ 1.1^3 atol = 1e-10 end end @testset "Deformation Gradient - Zero Allocation" begin @testset "Verify zero allocations" begin # Setup X_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) ) u_nodes = ( Vec(0.0, 0.0, 0.0), Vec(0.1, 0.0, 0.0), Vec(0.1, 0.0, 0.0), Vec(0.0, 0.0, 0.0), Vec(0.0, 0.0, 0.0), Vec(0.1, 0.0, 0.0), Vec(0.1, 0.0, 0.0), Vec(0.0, 0.0, 0.0) ) ξ = Vec(0.0, 0.0, 0.0) dN_dξ = get_basis_derivatives(Hexahedron(), Lagrange{Hexahedron,1}(), ξ) J = zero(Tensor{2,3,Float64,9}) for i in 1:8 J += X_nodes[i] ⊗ dN_dξ[i] end # Warm up (compile) F = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) # Measure allocations allocs = @allocated compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain()) @test allocs == 0 # Zero allocations! end end