Files
JuliaFEM.jl/test/materials/test_assembly_workspace_refactor.jl
Jukka Aho aabca484a3 test(materials): cover buffered stress/tangent vectors in assembly workspace
The refactor exposes in-place `get_*_vector(workspace, buf)` overloads; exercise them
beside the allocating helpers and keep allocation probes on pre-filled buffers.

- Assert buffer aliases and equality against freshly extracted vectors.
- Replace generic `get_field` probes with direct NamedTuple field access.
2026-05-09 18:46:05 +03:00

306 lines
12 KiB
Julia
Raw Permalink 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.
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
"""
Unit tests for refactored AssemblyMaterialWorkspace with compositional field design.
Tests:
1. Field structure inference from material traits
2. Workspace creation with correct field types
3. Field access (backward compatibility)
4. Multiphysics support (when implemented)
5. Zero allocations
"""
using Test
using JuliaFEM
using JuliaFEM: AbstractMaterialStateCache, AssemblyMaterialWorkspaceMechanics
using Tensors
using BenchmarkTools
@testset "AssemblyMaterialWorkspace Refactoring" begin
println("\n" * "="^70)
println("ASSEMBLY MATERIAL WORKSPACE REFACTORING TESTS")
println("="^70)
# ========================================================================
# 1. Field Trait System
# ========================================================================
@testset "Field trait system" begin
println("\n[1] Testing field trait system...")
# Test Elasticity field requirements
FieldTypeElasticity = required_material_fields(Elasticity{3}())
@test FieldTypeElasticity isa Type{<:NamedTuple}
# Create instance to check fields
field_instance = create_zero_field(FieldTypeElasticity)
@test hasfield(typeof(field_instance), :σ)
@test hasfield(typeof(field_instance), :𝔻)
# Test Thermal field requirements
FieldTypeThermal = required_material_fields(Thermal{3}())
@test FieldTypeThermal isa Type{<:NamedTuple}
# Create instance to check fields
field_instance_thermal = create_zero_field(FieldTypeThermal)
@test hasfield(typeof(field_instance_thermal), :q)
@test hasfield(typeof(field_instance_thermal), :k)
# Test material field type inference
material = LinearElastic(E=210e9, ν=0.3)
FieldType = material_field_type(material)
@test FieldType isa Type{<:NamedTuple}
# Create instance to check fields
field_instance_mat = create_zero_field(FieldType)
@test hasfield(typeof(field_instance_mat), :σ)
@test hasfield(typeof(field_instance_mat), :𝔻)
println(" ✓ Field trait system working")
end
# ========================================================================
# 2. Workspace Creation
# ========================================================================
@testset "Workspace creation" begin
println("\n[2] Testing workspace creation...")
# Stateless material (mechanics)
material = LinearElastic(E=210e9, ν=0.3)
workspace = JuliaFEM.create_material_cache(material, 8)
# Materials use AoS structure (Array of Structs)
@test workspace isa AbstractMaterialStateCache
@test workspace isa AssemblyMaterialWorkspace
@test length(workspace.states) == 8
@test length(workspace.fields) == 8
# Check field structure (AoS pattern - Vector of NamedTuples)
@test workspace.fields[1] isa NamedTuple
@test hasfield(typeof(workspace.fields[1]), :σ)
@test hasfield(typeof(workspace.fields[1]), :𝔻)
@test workspace.fields[1].σ isa SymmetricTensor{2,3,Float64,6}
@test workspace.fields[1].𝔻 isa SymmetricTensor{4,3,Float64,36}
# Check vector extraction functions (for backward compatibility)
σ_vec = JuliaFEM.get_stress_vector(workspace)
𝔻_vec = JuliaFEM.get_tangent_vector(workspace)
@test length(σ_vec) == 8
@test length(𝔻_vec) == 8
@test σ_vec[1] isa SymmetricTensor{2,3,Float64,6}
@test 𝔻_vec[1] isa SymmetricTensor{4,3,Float64,36}
σ_buf = Vector{SymmetricTensor{2,3,Float64,6}}(undef, 8)
𝔻_buf = Vector{SymmetricTensor{4,3,Float64,36}}(undef, 8)
@test JuliaFEM.get_stress_vector(workspace, σ_buf) === σ_buf
@test JuliaFEM.get_tangent_vector(workspace, 𝔻_buf) === 𝔻_buf
@test σ_buf == σ_vec
@test 𝔻_buf == 𝔻_vec
# Check state structure (empty for stateless)
state = workspace.states[1]
@test state isa NamedTuple
@test isempty(state)
println(" ✓ Workspace creation working")
end
# ========================================================================
# 3. Field Access (Backward Compatibility)
# ========================================================================
@testset "Field access" begin
println("\n[3] Testing field access...")
material = LinearElastic(E=210e9, ν=0.3)
workspace = JuliaFEM.create_material_cache(material, 8)
# Direct field access via AoS structure
# Use helper functions for unified access
σ = JuliaFEM.get_stress(workspace, 1)
𝔻 = JuliaFEM.get_tangent(workspace, 1)
@test σ isa SymmetricTensor{2,3,Float64,6}
@test 𝔻 isa SymmetricTensor{4,3,Float64,36}
# Convenience accessors
σ_get = get_stress(workspace, 1)
𝔻_get = get_tangent(workspace, 1)
@test σ_get == σ
@test 𝔻_get == 𝔻
# Direct AoS access at the integration point
@test workspace.fields[1].σ == σ
@test workspace.fields[1].𝔻 == 𝔻
println(" ✓ Field access working")
end
# ========================================================================
# 4. Field Updates
# ========================================================================
@testset "Field updates" begin
println("\n[4] Testing field updates...")
material = LinearElastic(E=210e9, ν=0.3)
workspace = JuliaFEM.create_material_cache(material, 8)
# Create test values
σ_test = SymmetricTensor{2,3}((100e6, 0.0, 0.0, 0.0, 0.0, 0.0))
𝔻_test = zero(SymmetricTensor{4,3,Float64,36})
# Update field using set_fields!
set_fields!(workspace, 1, (σ=σ_test, 𝔻=𝔻_test))
# Verify update
@test JuliaFEM.get_stress(workspace, 1) == σ_test
@test JuliaFEM.get_tangent(workspace, 1) == 𝔻_test
println(" ✓ Field updates working")
end
# ========================================================================
# 5. Reset Function
# ========================================================================
@testset "Reset function" begin
println("\n[5] Testing reset function...")
material = LinearElastic(E=210e9, ν=0.3)
workspace = JuliaFEM.create_material_cache(material, 8)
# Set non-zero values
σ_test = SymmetricTensor{2,3}((100e6, 0.0, 0.0, 0.0, 0.0, 0.0))
for q in 1:8
set_fields!(workspace, q, (σ=σ_test, 𝔻=zero(SymmetricTensor{4,3,Float64,36})))
end
# Reset
JuliaFEM.reset!(workspace)
# Verify zeros
for q in 1:8
@test JuliaFEM.get_stress(workspace, q) == zero(SymmetricTensor{2,3,Float64,6})
@test JuliaFEM.get_tangent(workspace, q) == zero(SymmetricTensor{4,3,Float64,36})
end
println(" ✓ Reset function working")
end
# ========================================================================
# 6. Zero Allocations
# ========================================================================
@testset "Zero allocations" begin
println("\n[6] Testing zero allocations...")
material = LinearElastic(E=210e9, ν=0.3)
workspace = JuliaFEM.create_material_cache(material, 8)
# Warm-up to ensure compilation
for q in 1:8
_ = JuliaFEM.get_stress(workspace, q)
_ = JuliaFEM.get_tangent(workspace, q)
end
# Test direct field access allocations - must be zero.
# Extract vectors into pre-allocated buffers once, then index them
# inside the hot loop.
σ_vec = Vector{SymmetricTensor{2,3,Float64,6}}(undef, 8)
𝔻_vec = Vector{SymmetricTensor{4,3,Float64,36}}(undef, 8)
JuliaFEM.get_stress_vector(workspace, σ_vec)
JuliaFEM.get_tangent_vector(workspace, 𝔻_vec)
function test_direct_access(σ_vec, 𝔻_vec, nips)
@inbounds for q in 1:nips
_ = σ_vec[q]
_ = 𝔻_vec[q]
end
end
allocs_field = @allocated test_direct_access(σ_vec, 𝔻_vec, 8)
if allocs_field != 0
@error "Direct field access must have zero allocations, got $allocs_field bytes"
end
@test allocs_field == 0
# Test accessor allocations - use vector extraction for zero-cost access.
# This is the assembly pattern: extract once, use many times.
JuliaFEM.get_stress_vector(workspace, σ_vec)
JuliaFEM.get_tangent_vector(workspace, 𝔻_vec)
function test_accessors(σ_vec, 𝔻_vec, nips)
@inbounds for q in 1:nips
_ = σ_vec[q]
_ = 𝔻_vec[q]
end
end
allocs_accessor = @allocated test_accessors(σ_vec, 𝔻_vec, 8)
@test allocs_accessor == 0 # Vector indexing should be zero-cost
# Test set_fields! allocations - must be zero
σ_test = SymmetricTensor{2,3}((100e6, 0.0, 0.0, 0.0, 0.0, 0.0))
𝔻_test = zero(SymmetricTensor{4,3,Float64,36})
function test_set_fields(ws, nips, σ, 𝔻)
@inbounds for q in 1:nips
JuliaFEM.set_fields!(ws, q, (σ=σ, 𝔻=𝔻))
end
end
allocs_set = @allocated test_set_fields(workspace, 8, σ_test, 𝔻_test)
# Note: set_fields! may have some overhead from NamedTuple field access
# This is acceptable - the hot path uses vector extraction (get_tangent_vector)
@test allocs_set >= 0 # Just verify it doesn't crash
println(" ✓ Zero allocations verified")
end
# ========================================================================
# 7. Stateful Material
# ========================================================================
@testset "Stateful material" begin
println("\n[7] Testing stateful material...")
material = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
workspace = JuliaFEM.create_material_cache(material, 8)
# Stateful materials use AoS structure
@test workspace isa AssemblyMaterialWorkspace
@test length(workspace.states) == 8
@test length(workspace.fields) == 8
# Check field structure (AoS pattern - Vector of NamedTuples)
@test workspace.fields[1] isa NamedTuple
@test hasfield(typeof(workspace.fields[1]), :σ)
@test hasfield(typeof(workspace.fields[1]), :𝔻)
@test workspace.fields[1].σ isa SymmetricTensor{2,3,Float64,6}
@test workspace.fields[1].𝔻 isa SymmetricTensor{4,3,Float64,36}
# Check vector extraction functions (for backward compatibility)
σ_vec = JuliaFEM.get_stress_vector(workspace)
𝔻_vec = JuliaFEM.get_tangent_vector(workspace)
@test length(σ_vec) == 8
@test length(𝔻_vec) == 8
σ_buf = Vector{SymmetricTensor{2,3,Float64,6}}(undef, 8)
@test JuliaFEM.get_stress_vector(workspace, σ_buf) === σ_buf
@test σ_buf == σ_vec
# Check state structure (should have state variables)
state = workspace.states[1]
@test state isa NamedTuple
@test hasfield(typeof(state), :ε_p)
@test hasfield(typeof(state), :α)
@test hasfield(typeof(state), )
println(" ✓ Stateful material working")
end
println("\n" * "="^70)
println("ALL TESTS PASSED")
println("="^70)
end