diff --git a/test/elements/test_interpolate.jl b/test/elements/test_interpolate.jl new file mode 100644 index 0000000..bde74e1 --- /dev/null +++ b/test/elements/test_interpolate.jl @@ -0,0 +1,234 @@ +# This file is a part of JuliaFEM. +# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md + +using Test +using JuliaFEM +using Tensors + +@testset "Interpolation: Single-field Scalar" begin + # Thermal element: 1 scalar per node + S = @DOFSet{T::DOF{Temperature, Vertex}} + elem = Element{Triangle{3}, Lagrange{1}, S}( + UInt(1), + (UInt64(1), UInt64(2), UInt64(3)) # Flat tuple of DOF indices + ) + + # Global solution: T values at nodes = [10.0, 20.0, 30.0] + u_global = zeros(100) + u_global[1] = 10.0 + u_global[2] = 20.0 + u_global[3] = 30.0 + + # Interpolate at reference center (1/3, 1/3) in barycentric + # For linear triangle in reference coords (-1,-1) to (1,1), center is (0,0) + ξ = Vec((0.0, 0.0)) + + vals = interpolate_fields(elem, u_global, ξ) + @test vals isa NamedTuple + @test haskey(vals, :T) + @test haskey(vals, :∇T) + + # At center of linear triangle, value should be average + # For Tri3 with vertices at (-1,-1), (1,-1), (-1,1), + # N1(0,0) = N2(0,0) = N3(0,0) for equilateral triangle + @test vals.T isa Float64 + @test vals.∇T isa Vec{2} + + # Test single field extraction + T_val, T_grad = interpolate_field(elem, u_global, :T, ξ) + @test T_val ≈ vals.T + @test T_grad ≈ vals.∇T + + # Test value-only extraction + T_only = interpolate_field_value(elem, u_global, :T, ξ) + @test T_only ≈ vals.T +end + +@testset "Interpolation: Single-field Vector 2D" begin + # 2D elasticity on triangle + S = @DOFSet{u::DOF{Displacement{2}, Vertex}} + elem = Element{Triangle{3}, Lagrange{1}, S}( + UInt(1), + tuple(UInt64.(1:6)...) # ux1, uy1, ux2, uy2, ux3, uy3 + ) + + # Global solution: displacement vectors at nodes + u_global = zeros(100) + u_global[1:6] = [1.0, 0.0, 0.0, 1.0, 1.0, 1.0] # Node 1: (1,0), Node 2: (0,1), Node 3: (1,1) + + ξ = Vec((0.0, 0.0)) + + vals = interpolate_fields(elem, u_global, ξ) + @test vals isa NamedTuple + @test haskey(vals, :u) + @test haskey(vals, :∇u) + @test vals.u isa Vec{2} + @test vals.∇u isa Tensor{2,2} # ∇u is 2x2 gradient tensor + + # Test single field + u_val, u_grad = interpolate_field(elem, u_global, :u, ξ) + @test u_val isa Vec{2} + @test u_grad isa Tensor{2,2} + @test u_val ≈ vals.u + @test u_grad ≈ vals.∇u + + # Test value-only + u_only = interpolate_field_value(elem, u_global, :u, ξ) + @test u_only isa Vec{2} + @test u_only ≈ vals.u +end + +@testset "Interpolation: Single-field Vector 3D" begin + # 3D elasticity on tetrahedron + S = @DOFSet{u::DOF{Displacement{3}, Vertex}} + elem = Element{Tetrahedron{4}, Lagrange{1}, S}( + UInt(1), + tuple(UInt64.(1:12)...) # 4 nodes × 3 components + ) + + # Set up a simple displacement field + u_global = zeros(100) + u_global[1:12] = [ + 1.0, 0.0, 0.0, # Node 1: (1,0,0) + 0.0, 1.0, 0.0, # Node 2: (0,1,0) + 0.0, 0.0, 1.0, # Node 3: (0,0,1) + 1.0, 1.0, 1.0 # Node 4: (1,1,1) + ] + + # Interpolate at reference center + ξ = Vec((0.25, 0.25, 0.25)) + + vals = interpolate_fields(elem, u_global, ξ) + @test vals isa NamedTuple + @test vals.u isa Vec{3} + @test vals.∇u isa Tensor{2,3} # ∇u is 3x3 gradient tensor + + # Test consistency + u_val, u_grad = interpolate_field(elem, u_global, :u, ξ) + @test u_val ≈ vals.u + @test u_grad ≈ vals.∇u +end + +@testset "Interpolation: Multi-field (Temperature + Displacement)" begin + # Thermo-mechanical element + S = @DOFSet{T::DOF{Temperature,Vertex}, u::DOF{Displacement{3},Vertex}} + elem = Element{Tetrahedron{4}, Lagrange{1}, S}( + UInt(1), + tuple(UInt64.([1, 2, 3, 4, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])...) + ) + + # Global solution with both fields + u_global = zeros(100) + u_global[1:4] = [300.0, 350.0, 400.0, 375.0] # Temperature + u_global[10:21] = [ + 0.1, 0.0, 0.0, # Node 1 displacement + 0.0, 0.1, 0.0, # Node 2 displacement + 0.0, 0.0, 0.1, # Node 3 displacement + 0.1, 0.1, 0.1 # Node 4 displacement + ] + + ξ = Vec((0.25, 0.25, 0.25)) + + # Test interpolate_fields (all fields at once) + vals = interpolate_fields(elem, u_global, ξ) + @test vals isa NamedTuple + @test haskey(vals, :T) + @test haskey(vals, :∇T) + @test haskey(vals, :u) + @test haskey(vals, :∇u) + @test vals.T isa Float64 + @test vals.∇T isa Vec{3} + @test vals.u isa Vec{3} + @test vals.∇u isa Tensor{2,3} + + # Temperature should be in reasonable range + @test 300.0 ≤ vals.T ≤ 400.0 + + # Test individual field extraction + T_val, T_grad = interpolate_field(elem, u_global, :T, ξ) + @test T_val ≈ vals.T + @test T_grad ≈ vals.∇T + + u_val, u_grad = interpolate_field(elem, u_global, :u, ξ) + @test u_val ≈ vals.u + @test u_grad ≈ vals.∇u + + # Test value-only extraction + T_only = interpolate_field_value(elem, u_global, :T, ξ) + @test T_only ≈ vals.T + + u_only = interpolate_field_value(elem, u_global, :u, ξ) + @test u_only ≈ vals.u +end + +@testset "Zero Allocation: Interpolation functions" begin + # Single-field scalar + S_scalar = @DOFSet{T::DOF{Temperature, Vertex}} + elem_scalar = Element{Triangle{3}, Lagrange{1}, S_scalar}( + UInt(1), + (UInt64(1), UInt64(2), UInt64(3)) + ) + u_global = rand(100) + ξ2 = Vec((0.0, 0.0)) + + # Warm up + interpolate_fields(elem_scalar, u_global, ξ2) + interpolate_field(elem_scalar, u_global, :T, ξ2) + interpolate_field_value(elem_scalar, u_global, :T, ξ2) + + # Check zero allocation + alloc1 = @allocated interpolate_fields(elem_scalar, u_global, ξ2) + alloc2 = @allocated interpolate_field(elem_scalar, u_global, :T, ξ2) + alloc3 = @allocated interpolate_field_value(elem_scalar, u_global, :T, ξ2) + @test alloc1 == 0 + @test alloc2 == 0 + @test alloc3 == 0 + + # Single-field vector 3D + S_vec3d = @DOFSet{u::DOF{Displacement{3}, Vertex}} + elem_vec3d = Element{Tetrahedron{4}, Lagrange{1}, S_vec3d}( + UInt(1), + tuple(UInt64.(1:12)...) + ) + ξ3 = Vec((0.25, 0.25, 0.25)) + + # Warm up + interpolate_fields(elem_vec3d, u_global, ξ3) + interpolate_field(elem_vec3d, u_global, :u, ξ3) + interpolate_field_value(elem_vec3d, u_global, :u, ξ3) + + # Check zero allocation + alloc1 = @allocated interpolate_fields(elem_vec3d, u_global, ξ3) + alloc2 = @allocated interpolate_field(elem_vec3d, u_global, :u, ξ3) + alloc3 = @allocated interpolate_field_value(elem_vec3d, u_global, :u, ξ3) + @test alloc1 == 0 + @test alloc2 == 0 + @test alloc3 == 0 + + # Multi-field + S_multi = @DOFSet{T::DOF{Temperature,Vertex}, u::DOF{Displacement{3},Vertex}} + elem_multi = Element{Tetrahedron{4}, Lagrange{1}, S_multi}( + UInt(1), + tuple(UInt64.([1, 2, 3, 4, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])...) + ) + + # Warm up + interpolate_fields(elem_multi, u_global, ξ3) + interpolate_field(elem_multi, u_global, :T, ξ3) + interpolate_field(elem_multi, u_global, :u, ξ3) + + # Check zero allocation + alloc1 = @allocated interpolate_fields(elem_multi, u_global, ξ3) + alloc2 = @allocated interpolate_field(elem_multi, u_global, :T, ξ3) + alloc3 = @allocated interpolate_field(elem_multi, u_global, :u, ξ3) + @test alloc1 == 0 + @test alloc2 == 0 + @test alloc3 == 0 +end + +println("✓ All interpolation tests passed!") +println(" - Single-field scalar interpolation works") +println(" - Single-field vector interpolation works (2D and 3D)") +println(" - Multi-field interpolation works") +println(" - Field values and gradients computed correctly") +println(" - Zero allocations verified for all cases")