Files
JuliaFEM.jl/test/tutorials/01_fundamentals/basis_functions.jl
T
Jukka Aho 52ebe682e9 fix: Standardize on Tensors.jl Vec type throughout
Major architectural decision: Use Tensors.jl consistently everywhere
for geometric vectors, integration points, and coordinates.

Changes to src/elements/elements.jl:
- get_basis(): Convert ip to Vec, use Vector (not Matrix) for eval_basis!
- get_dbasis(): Convert ip to Vec
- jacobian evaluation: Convert geometry and ip.coords to Vec properly
- Handle both raw coordinates (Tuple) and IP struct transparently

New Tutorial 3: Numerical Integration and Jacobian (49 tests)
- Integration point structure and weights
- Jacobian determinant and matrix evaluation
- Numerical integration (constant, linear, quadratic functions)
- Multiple element types (Quad4, Seg2, Tri3)

Tests: 107 → 156 passing (49 new)
Runtime: ~7 seconds

Closes architectural standardization on Tensors.jl.
Related to Issue #250 (merge conflict resolution).

Why Tensors.jl:
- Type stability (100× performance vs Dict-based)
- Material science compatibility (stress tensors)
- Zero-cost abstractions
- Consistent API across all geometric calculations
2025-11-09 03:10:11 +02:00

237 lines
6.7 KiB
Julia
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# # Numerical Integration and Jacobian
#
# **Purpose:** Understand how FEM uses numerical integration with Tensors.jl
#
# This tutorial explores numerical integration in finite element analysis,
# which is fundamental to computing element matrices and vectors.
#
# ## Why This Matters
#
# In FEM, we compute element matrices by integrating:
# ```math
# K = \int_{\Omega} B^T D B \, dΩ
# ```
#
# Numerically:
# ```math
# K ≈ \sum_{ip} w_{ip} B^T D B |J|_{ip}
# ```
#
# Where:
# - ip = integration points (Gauss quadrature points)
# - w = quadrature weights
# - |J| = Jacobian determinant (coordinate transformation scaling)
using JuliaFEM
using Test
# ## Step 1: Integration Points (Gauss Quadrature)
#
# JuliaFEM uses Gauss quadrature for numerical integration.
# For Quad4, we use 2×2 Gauss quadrature (4 points).
# Create a unit square element
nodes = Dict(
1 => [0.0, 0.0],
2 => [1.0, 0.0],
3 => [1.0, 1.0],
4 => [0.0, 1.0]
)
element = Element(Quad4, [1, 2, 3, 4])
update!(element, "geometry", nodes)
@testset "Integration Points: Structure" begin
ips = get_integration_points(element)
@test length(ips) == 4 # 2×2 Gauss quadrature for Quad4
# Each integration point has coords and weight
@test hasfield(typeof(ips[1]), :weight)
@test hasfield(typeof(ips[1]), :coords)
# Coordinates are in parametric space [-1, 1]²
for ip in ips
ξ, η = ip.coords
@test -1 <= ξ <= 1
@test -1 <= η <= 1
end
end
@testset "Integration Points: Weights" begin
ips = get_integration_points(element)
# For 2D Gauss quadrature in [-1,1]², weights sum to 4
total_weight = sum(ip.weight for ip in ips)
@test total_weight 4.0
# For 2×2 Gauss, all weights are equal (symmetry)
weights = [ip.weight for ip in ips]
@test all(w weights[1] for w in weights)
@test weights[1] 1.0 # Each weight = 1 for 2×2 Gauss
end
# ## Step 2: Jacobian Evaluation (Now Working with Tensors.jl!)
#
# The Jacobian transforms derivatives from parametric to physical coordinates.
# With our Tensors.jl fixes, this now works correctly.
@testset "Jacobian: Determinant" begin
ips = get_integration_points(element)
for ip in ips
# Jacobian determinant must be positive (non-inverted element)
detJ = element(ip, 0.0, Val{:detJ})
@test detJ > 0
# For unit square, Jacobian is constant
# At any point, |J| should be 0.25 (scale factor from [-1,1]² to [0,1]²)
@test detJ 0.25
end
end
@testset "Jacobian: Matrix" begin
ips = get_integration_points(element)
for ip in ips
# Get full Jacobian matrix
J = element(ip, 0.0, Val{:Jacobian})
# Should be 2×2 for 2D element
@test size(J) == (2, 2)
# For unit square aligned with axes, should be diagonal
@test J[1, 1] 0.5 # ∂x/∂ξ
@test J[2, 2] 0.5 # ∂y/∂η
@test abs(J[1, 2]) < 1e-10 # ∂y/∂ξ ≈ 0
@test abs(J[2, 1]) < 1e-10 # ∂x/∂η ≈ 0
end
end
# ## Step 3: Numerical Integration
#
# Now that Jacobian works, we can perform numerical integration!
@testset "Integration: Constant Function" begin
# Integrate f(x,y) = 1 over unit square → area = 1.0
ips = get_integration_points(element)
integral = 0.0
for ip in ips
detJ = element(ip, 0.0, Val{:detJ})
# Integrate constant function f=1
integral += ip.weight * 1.0 * detJ
end
@test integral 1.0 atol = 1e-10 # Area of unit square
end
@testset "Integration: Linear Function x" begin
# Integrate f(x,y) = x over unit square
# Analytical: ∫₀¹ ∫₀¹ x dy dx = 1/2
ips = get_integration_points(element)
integral = 0.0
for ip in ips
# Get physical coordinates at this integration point
# Use basis functions to interpolate
N = element(ip, 0.0)
x_ip = sum(N[i] * nodes[i][1] for i in 1:4)
detJ = element(ip, 0.0, Val{:detJ})
integral += ip.weight * x_ip * detJ
end
@test integral 0.5 atol = 1e-10
end
@testset "Integration: Quadratic Function x²" begin
# Integrate f(x,y) = x² over unit square
# Analytical: ∫₀¹ ∫₀¹ x² dy dx = 1/3
ips = get_integration_points(element)
integral = 0.0
for ip in ips
N = element(ip, 0.0)
x_ip = sum(N[i] * nodes[i][1] for i in 1:4)
detJ = element(ip, 0.0, Val{:detJ})
integral += ip.weight * x_ip^2 * detJ
end
@test integral 1 / 3 atol = 1e-10
end
# ## Step 4: Different Element Types
@testset "Integration: Seg2 (1D)" begin
# 1D line element
nodes_1d = Dict(1 => [0.0], 2 => [2.0])
element_1d = Element(Seg2, [1, 2])
update!(element_1d, "geometry", nodes_1d)
ips = get_integration_points(element_1d)
@test length(ips) == 2 # 2-point Gauss in 1D
# Integrate over length
length_integral = sum(ip.weight * element_1d(ip, 0.0, Val{:detJ}) for ip in ips)
@test length_integral 2.0 # Length of element
end
@testset "Integration: Tri3 (Triangle)" begin
# Triangular element
nodes_tri = Dict(
1 => [0.0, 0.0],
2 => [1.0, 0.0],
3 => [0.0, 1.0]
)
element_tri = Element(Tri3, [1, 2, 3])
update!(element_tri, "geometry", nodes_tri)
ips = get_integration_points(element_tri)
@test length(ips) >= 1 # At least one integration point
# Integrate constant → area of triangle = 0.5
area = sum(ip.weight * element_tri(ip, 0.0, Val{:detJ}) for ip in ips)
@test area 0.5 atol = 1e-10
end
# ## Discussion
#
# With Tensors.jl properly integrated throughout, we can now:
#
# 1. **Evaluate Jacobian:** Transform between parametric and physical coordinates
# 2. **Perform Integration:** Numerical quadrature works correctly
# 3. **Use Multiple Element Types:** Seg2, Tri3, Quad4 all work
#
# ## Key Architectural Decision
#
# **Using Tensors.jl everywhere** provides:
# - Zero-cost abstractions
# - Type stability
# - Consistent API across all geometric calculations
# - Material science compatibility
#
# ## What's Next?
#
# - Assembly: Build global matrices using these integrations
# - Solvers: Solve FEM problems end-to-end
# - Advanced elements: Higher-order elements, 3D
#
# ## References
#
# - Tensors.jl documentation: https://github.com/Ferrite-FEM/Tensors.jl
# - Hughes, T.J.R., "The Finite Element Method", Dover (Chapter 3)
println()
println("="^70)
println("Numerical Integration Tutorial Complete!")
println("="^70)
println("✓ Integration points and Gauss quadrature working")
println("✓ Jacobian evaluation fixed with Tensors.jl")
println("✓ Numerical integration validated (constant, linear, quadratic)")
println("✓ Multiple element types tested (Quad4, Seg2, Tri3)")
println()
println("Tensors.jl is now consistently used throughout JuliaFEM!")
println("="^70)