""" # Jacobian Computation Tests (test/geometry/) ## What Tests Jacobian matrix computation for mapping between parametric (reference) and physical (mesh) coordinates. The Jacobian **J = ∂x/∂ξ** relates shape function derivatives in parametric space to physical space. ## Why The Jacobian is **THE MOST CRITICAL** geometric computation in FEM: - **Coordinate transformation**: Maps ∂/∂ξ → ∂/∂x via J⁻¹ - **Differential volume**: det(J) provides dV = det(J) dξ for integration - **Element quality**: det(J) > 0 required (negative → inverted element) - **Strain computation**: ε = f(∇u), and ∇ requires Jacobian transformation Without correct Jacobian: - Stiffness matrices are wrong - Internal forces are wrong - EVERYTHING is wrong! This test validates: - **Correctness**: Known geometric transformations produce expected J - **Orientation**: det(J) > 0 for well-shaped elements - **Quality detection**: det(J) ≈ 0 for degenerate elements - **Physical derivatives**: dN/dx = J⁻¹ · dN/dξ correct - **Type stability**: Returns Tensor{2,D} (zero-allocation) - **Zero allocations**: Hot path allocates nothing ## How **Test Cases:** **1. Identity Mapping** (reference → reference): - Triangle: Vertices at (0,0), (1,0), (0,1) - Expected: J = I (identity matrix), det(J) = 1.0 **2. Scaled Elements**: - Triangle scaled 2× in x, 1.5× in y - Expected: J = diag(2.0, 1.5), det(J) = 3.0 (area scaling) - Tetrahedron scaled 2×, 3×, 4× - Expected: det(J) = 24.0 (volume scaling) **3. Rotated Elements**: - 90° rotation of triangle - Expected: det(J) = 1.0 (area preserved), J contains rotation matrix **4. Physical Derivatives**: - Computes dN/dx = J⁻¹ · dN/dξ - Validates constant strain condition: ∑ᵢ dNᵢ/dx = 0 (partition of unity derivative) **5. Element Quality**: - **Well-shaped**: det(J) > 0.1 (properly oriented, well-conditioned) - **Degenerate**: det(J) < 1e-10 (collapsed to line/plane, unusable) **6. Type Stability**: - `@inferred compute_jacobian(X, dN_dξ)` → Tensor{2,D} - `@inferred physical_derivatives(J, dN_dξ)` → Tuple - `@allocated` checks confirm zero allocations **7. Manual Verification**: - Triangle with vertices (1,2), (4,3), (2,6) - Hand-calculated J = [3 1; 1 4], det(J) = 11 - Verifies implementation matches theory **8. Vector vs Tuple Interface**: - Tests both NTuple{N,Vec{D}} (preferred) and Vector{Vec{D}} (legacy) - Both produce same results (tuple faster due to stack allocation) ## Expected Results - ✅ **Identity mapping**: J = I, det(J) = 1.0 - ✅ **Scaled mapping**: J diagonal with scale factors, det(J) = product of scales - ✅ **Rotated mapping**: det(J) = 1.0 (area/volume preserved) - ✅ **Physical derivatives**: ∑ᵢ dNᵢ/dx = 0 (constant strain condition) - ✅ **Quality detection**: det(J) > 0 for valid, ≈ 0 for degenerate - ✅ **Type stability**: All @inferred checks pass - ✅ **Zero allocations**: All @allocated checks return 0 - ✅ **Consistency**: Matches hand calculations ## Mathematical Background **Jacobian Matrix:** ``` J_ij = ∂xᵢ/∂ξⱼ = ∑ₖ Xₖ,ᵢ · dNₖ/dξⱼ ``` Where: - Xₖ = physical coordinates of node k (Vec{D}) - dNₖ/dξⱼ = parametric derivative of shape function k w.r.t. ξⱼ - D = physical dimension (2 or 3) - d = parametric dimension (1, 2, or 3) **Physical Derivatives:** ``` ∂Nₖ/∂x = J⁻¹ · ∂Nₖ/∂ξ ``` **Integration:** ``` ∫_Ω f(x) dV = ∫_Ω_ref f(ξ) |det(J)| dξ ``` ## Architecture Principle **Tensors.jl for ALL Math** All geometric quantities use Tensors.jl types: - Coordinates: Vec{D,Float64} - Jacobian: Tensor{2,D} (or Tensor{2,D,Float64,M} for D×d) - Derivatives: Vec{D,Float64} This provides: - Natural mathematical notation (J[i,j]) - Automatic differentiation compatible - Zero-allocation operations - GPU transferable ## Critical for Assembly Every integration point requires: 1. Evaluate dN/dξ at quadrature point 2. Compute J = X · dN/dξ 3. Compute dN/dx = J⁻¹ · dN/dξ 4. Compute det(J) for integration weight If ANY of these allocate, assembly is slow. These tests ensure they don't! """ using Test using JuliaFEM using Tensors using LinearAlgebra @testset "Jacobian Computation" begin @testset "2D Triangle - Identity Element" begin # Reference triangle mapped to itself (identity transformation) X = ( Vec{2}((0.0, 0.0)), Vec{2}((1.0, 0.0)), Vec{2}((0.0, 1.0)) ) # Evaluate at center xi = Vec{2}((1 / 3, 1 / 3)) dN_dξ = get_basis_derivatives(Triangle(), Lagrange{Triangle,1}(), xi) # Compute Jacobian J = compute_jacobian(X, dN_dξ) # For identity mapping, J should be identity matrix @test J ≈ Tensor{2,2}((1.0, 0.0, 0.0, 1.0)) @test det(J) ≈ 1.0 end @testset "2D Triangle - Scaled Element" begin # Triangle scaled by 2 in x and 1.5 in y X = ( Vec{2}((0.0, 0.0)), Vec{2}((2.0, 0.0)), Vec{2}((0.0, 1.5)) ) xi = Vec{2}((1 / 3, 1 / 3)) dN_dξ = get_basis_derivatives(Triangle(), Lagrange{Triangle,1}(), xi) J = compute_jacobian(X, dN_dξ) # Jacobian should reflect scaling @test J[1, 1] ≈ 2.0 # ∂x/∂ξ @test J[1, 2] ≈ 0.0 # ∂x/∂η @test J[2, 1] ≈ 0.0 # ∂y/∂ξ @test J[2, 2] ≈ 1.5 # ∂y/∂η @test det(J) ≈ 3.0 # Area scaling = 2 × 1.5 end @testset "2D Triangle - Rotated Element" begin # 90° counter-clockwise rotation θ = π / 2 R = [cos(θ) -sin(θ); sin(θ) cos(θ)] # Original nodes X_orig = [0.0 1.0 0.0; 0.0 0.0 1.0] # Rotate X_rot = R * X_orig X = ( Vec{2}((X_rot[1, 1], X_rot[2, 1])), Vec{2}((X_rot[1, 2], X_rot[2, 2])), Vec{2}((X_rot[1, 3], X_rot[2, 3])) ) xi = Vec{2}((1 / 3, 1 / 3)) dN_dξ = get_basis_derivatives(Triangle(), Lagrange{Triangle,1}(), xi) J = compute_jacobian(X, dN_dξ) # Jacobian should contain rotation @test det(J) ≈ 1.0 # Area preserved under rotation @test norm(J) > 0 # Well-conditioned end @testset "3D Tetrahedron - Identity Element" begin # Reference tetrahedron mapped to itself X = ( Vec{3}((0.0, 0.0, 0.0)), Vec{3}((1.0, 0.0, 0.0)), Vec{3}((0.0, 1.0, 0.0)), Vec{3}((0.0, 0.0, 1.0)) ) xi = Vec{3}((0.25, 0.25, 0.25)) dN_dξ = get_basis_derivatives(Tetrahedron(), Lagrange{Tetrahedron,1}(), xi) J = compute_jacobian(X, dN_dξ) # Identity mapping @test J ≈ Tensor{2,3}((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) @test det(J) ≈ 1.0 end @testset "3D Tetrahedron - Scaled Element" begin # Tetrahedron scaled differently in each direction X = ( Vec{3}((0.0, 0.0, 0.0)), Vec{3}((2.0, 0.0, 0.0)), Vec{3}((0.0, 3.0, 0.0)), Vec{3}((0.0, 0.0, 4.0)) ) xi = Vec{3}((0.25, 0.25, 0.25)) dN_dξ = get_basis_derivatives(Tetrahedron(), Lagrange{Tetrahedron,1}(), xi) J = compute_jacobian(X, dN_dξ) # Diagonal Jacobian (aligned with axes) @test J[1, 1] ≈ 2.0 @test J[2, 2] ≈ 3.0 @test J[3, 3] ≈ 4.0 @test det(J) ≈ 24.0 # Volume scaling = 2 × 3 × 4 end @testset "Physical Derivatives - 2D Triangle" begin X = ( Vec{2}((0.0, 0.0)), Vec{2}((2.0, 0.0)), Vec{2}((0.0, 1.5)) ) xi = Vec{2}((1 / 3, 1 / 3)) dN_dξ = get_basis_derivatives(Triangle(), Lagrange{Triangle,1}(), xi) J = compute_jacobian(X, dN_dξ) dN_dx = physical_derivatives(J, dN_dξ) # Verify constant strain condition: ∑ᵢ dNᵢ/dx = 0 sum_dN_dx = sum(dN_dx) @test norm(sum_dN_dx) < 1e-10 # Verify partition of unity holds # (Not directly, but derivatives should be consistent) @test length(dN_dx) == 3 end @testset "Physical Derivatives - 3D Tetrahedron" begin X = ( Vec{3}((0.0, 0.0, 0.0)), Vec{3}((1.0, 0.0, 0.0)), Vec{3}((0.0, 1.0, 0.0)), Vec{3}((0.0, 0.0, 1.0)) ) xi = Vec{3}((0.25, 0.25, 0.25)) dN_dξ = get_basis_derivatives(Tetrahedron(), Lagrange{Tetrahedron,1}(), xi) J = compute_jacobian(X, dN_dξ) dN_dx = physical_derivatives(J, dN_dξ) # Constant strain condition sum_dN_dx = sum(dN_dx) @test norm(sum_dN_dx) < 1e-10 # Check each derivative is a 3D vector for dN in dN_dx @test length(dN) == 3 end end @testset "Jacobian Determinant - Element Quality" begin # Well-shaped triangle X_good = ( Vec{2}((0.0, 0.0)), Vec{2}((1.0, 0.0)), Vec{2}((0.0, 1.0)) ) xi = Vec{2}((1 / 3, 1 / 3)) dN_dξ = get_basis_derivatives(Triangle(), Lagrange{Triangle,1}(), xi) J_good = compute_jacobian(X_good, dN_dξ) @test det(J_good) > 0 # Positive (properly oriented) @test abs(det(J_good)) > 0.1 # Well-conditioned # Degenerate triangle (collapsed to line) X_bad = ( Vec{2}((0.0, 0.0)), Vec{2}((1.0, 0.0)), Vec{2}((2.0, 0.0)) # Collinear! ) J_bad = compute_jacobian(X_bad, dN_dξ) @test abs(det(J_bad)) < 1e-10 # Nearly zero (degenerate) end @testset "Type Stability and Zero Allocation" begin X = ( Vec{2}((0.0, 0.0)), Vec{2}((1.0, 0.0)), Vec{2}((0.0, 1.0)) ) xi = Vec{2}((1 / 3, 1 / 3)) dN_dξ = get_basis_derivatives(Triangle(), Lagrange{Triangle,1}(), xi) # Type stability J = @inferred compute_jacobian(X, dN_dξ) @test J isa Tensor{2,2} dN_dx = @inferred physical_derivatives(J, dN_dξ) @test dN_dx isa Tuple # Zero allocation (run twice to avoid compilation) compute_jacobian(X, dN_dξ) allocs = @allocated compute_jacobian(X, dN_dξ) @test allocs == 0 physical_derivatives(J, dN_dξ) allocs = @allocated physical_derivatives(J, dN_dξ) @test allocs == 0 end @testset "Consistency with Manual Calculation" begin # Triangle with known Jacobian X = ( Vec{2}((1.0, 2.0)), Vec{2}((4.0, 3.0)), Vec{2}((2.0, 6.0)) ) xi = Vec{2}((0.5, 0.25)) dN_dξ = get_basis_derivatives(Triangle(), Lagrange{Triangle,1}(), xi) # dN_dξ = (Vec(-1, -1), Vec(1, 0), Vec(0, 1)) J = compute_jacobian(X, dN_dξ) # Manual calculation: # J = X2 - X1 in first column, X3 - X1 in second column # J = [4-1 2-1] = [3 1] # [3-2 6-2] [1 4] @test J[1, 1] ≈ 3.0 @test J[1, 2] ≈ 1.0 @test J[2, 1] ≈ 1.0 @test J[2, 2] ≈ 4.0 @test det(J) ≈ 11.0 # 3*4 - 1*1 = 11 end end @testset "Jacobian - AbstractVector Interface" begin # Test that Vector interface also works (less efficient) X_vec = [Vec{2}((0.0, 0.0)), Vec{2}((1.0, 0.0)), Vec{2}((0.0, 1.0))] xi = Vec{2}((1 / 3, 1 / 3)) dN_dξ_tuple = get_basis_derivatives(Triangle(), Lagrange{Triangle,1}(), xi) dN_dξ_vec = collect(dN_dξ_tuple) J_tuple = compute_jacobian(tuple(X_vec...), dN_dξ_tuple) J_vec = compute_jacobian(X_vec, dN_dξ_vec) @test J_tuple ≈ J_vec # Physical derivatives dN_dx_tuple = physical_derivatives(J_tuple, dN_dξ_tuple) dN_dx_vec = physical_derivatives(J_vec, dN_dξ_vec) @test all(dN_dx_tuple[i] ≈ dN_dx_vec[i] for i in 1:3) end println("✅ All Jacobian tests passed!")