mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-22 02:40:51 +00:00
1952f9c6cb
- Tests compute_jacobian() for 2D triangles and 3D tetrahedra - Validates identity, scaling, and rotation transformations - Tests physical_derivatives() conversion from reference to physical coordinates - Verifies constant strain condition (∑ dNᵢ/dx = 0) - Element quality checks via determinant (positive = proper orientation) - Detects degenerate elements (det ≈ 0) - Type stability and zero allocation verification - Manual calculation consistency checks for known Jacobians - Tests both tuple and vector interfaces - 261 lines covering fundamental isoparametric mapping operations
262 lines
7.7 KiB
Julia
262 lines
7.7 KiB
Julia
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!")
|