mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-28 12:37:53 +00:00
385 lines
12 KiB
Julia
385 lines
12 KiB
Julia
|
|
"""
|
|||
|
|
# 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!")
|