test(materials): add plasticity integration test

New 229-line test file included in main test/runtests.jl:
- Tests PerfectPlasticity material instantiation and traits
- Tests global material cache creation for stateful materials
- Tests direct NamedTuple state manipulation (compositional design)
- Tests cache access and manipulation (set_state!, update_cache!)
- Tests multi-step plasticity simulation workflow
- Tests state variable helpers (get_state_variable, set_state_variable)
- Validates type stability of key operations

Comprehensive integration test ensuring plasticity material works with cache system.
This commit is contained in:
Jukka Aho
2025-12-15 06:36:30 +02:00
parent b5772b141b
commit b7c4f64151
@@ -0,0 +1,229 @@
# 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 "PerfectPlasticity Integration" begin
@testset "Material instantiation and traits" begin
# Create material
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
@test mat isa PerfectPlasticity
@test mat.E == 210e9
@test mat.ν == 0.3
@test mat.σ_y == 250e6
@test mat.H == 1e9
# Test material traits
physics = supported_physics(mat)
@test physics isa Tuple
@test length(physics) == 1
@test physics[1] isa Elasticity{3}
# Test state variable requirements
state_vars = required_state_variables(mat)
@test state_vars == (PlasticStrain, Backstress, EquivalentPlasticStrain)
# Material should be stateful
@test is_stateful(mat)
end
@testset "Global material cache creation" begin
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
n_ips = 8
n_elems = 10
# Create cache automatically from material
cache = create_global_material_cache(mat, n_ips=n_ips, n_elems=n_elems)
@test cache isa GlobalMaterialCache
@test size(cache.states) == (n_ips, n_elems)
@test size(cache.states_old) == (n_ips, n_elems)
# All states should be initialized to zero
for elem_id in 1:n_elems
for ip in 1:n_ips
state = get_state(cache, ip, elem_id)
@test haskey(state, :ε_p)
@test haskey(state, :α)
@test haskey(state, )
@test state.ε_p == zero(SymmetricTensor{2,3})
@test state.α == zero(SymmetricTensor{2,3})
@test state.κ == 0.0
end
end
end
@testset "Direct NamedTuple state manipulation" begin
# Test compositional state creation and access
state_nt = (
ε_p=SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)),
α=SymmetricTensor{2,3}((100e6, 0.0, 0.0, 0.0, 0.0, 0.0)),
κ=0.01
)
# State is just a NamedTuple - no conversion needed!
@test state_nt isa NamedTuple
@test haskey(state_nt, :ε_p)
@test haskey(state_nt, :α)
@test haskey(state_nt, )
# Can use directly with compute_stress
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
ε = SymmetricTensor{2,3}((0.002, 0.0, 0.0, 0.0, 0.0, 0.0))
σ, 𝔻, state_new = compute_stress(mat, ε, state_nt, 0.0)
@test state_new isa NamedTuple
@test haskey(state_new, :ε_p)
@test haskey(state_new, :α)
@test haskey(state_new, )
end
@testset "Direct cache access and manipulation" begin
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
cache = create_global_material_cache(mat, n_ips=4, n_elems=2)
# Set states directly - no conversion needed
state1 = (
ε_p=SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)),
α=SymmetricTensor{2,3}((50e6, 0.0, 0.0, 0.0, 0.0, 0.0)),
κ=0.005
)
set_state!(cache, 1, 1, state1)
# Retrieve states directly
retrieved_state = get_state(cache, 1, 1)
@test retrieved_state.ε_p == state1.ε_p
@test retrieved_state.α == state1.α
@test retrieved_state.κ == state1.κ
# Modify state directly
state2 = (
ε_p=SymmetricTensor{2,3}((0.002, 0.0, 0.0, 0.0, 0.0, 0.0)),
α=SymmetricTensor{2,3}((100e6, 0.0, 0.0, 0.0, 0.0, 0.0)),
κ=0.01
)
set_state!(cache, 2, 1, state2)
# Verify stored correctly
state2_retrieved = get_state(cache, 2, 1)
@test state2_retrieved.ε_p SymmetricTensor{2,3}((0.002, 0.0, 0.0, 0.0, 0.0, 0.0))
@test state2_retrieved.α SymmetricTensor{2,3}((100e6, 0.0, 0.0, 0.0, 0.0, 0.0))
@test state2_retrieved.κ 0.01
end
@testset "Elastic response (below yield)" begin
# Test that material behaves elastically below yield
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
# Small elastic strain
ε = SymmetricTensor{2,3}((1e-4, 0.0, 0.0, 0.0, 0.0, 0.0)) # ~21 MPa << 250 MPa
state_old = NamedTuple() # Empty state = initial
# Compute stress
σ, 𝔻, state_new = compute_stress(mat, ε, state_old, 0.0)
# Should be purely elastic (no plastic strain)
@test state_new.ε_p == zero(SymmetricTensor{2,3})
@test state_new.α == zero(SymmetricTensor{2,3})
@test state_new.κ == 0.0
# Stress should be elastic prediction
@test norm(σ) > 0.0
@test norm(σ) < mat.σ_y # Below yield
end
@testset "Plastic response (above yield)" begin
# Test that material yields when stress exceeds yield
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
# Large elastic strain that will cause yielding
# σ_y = 250 MPa, E=210 GPa → ε ≈ 0.0012 should yield
ε = SymmetricTensor{2,3}((0.005, 0.0, 0.0, 0.0, 0.0, 0.0)) # Large strain
state_old = NamedTuple() # Empty state = initial
# Compute stress
σ, 𝔻, state_new = compute_stress(mat, ε, state_old, 0.0)
# Should have plastic strain
@test norm(state_new.ε_p) > 0.0
@test state_new.κ > 0.0
# Stress should be at/near yield surface
@test norm(σ) > 0.0
end
@testset "Time stepping workflow" begin
# Simulate a complete time-stepping workflow
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
cache = create_global_material_cache(mat, n_ips=4, n_elems=1)
# Step 1: Apply small load (elastic)
ε1 = SymmetricTensor{2,3}((1e-4, 0.0, 0.0, 0.0, 0.0, 0.0))
state1_old = get_state(cache, 1, 1) # Get directly from cache
σ1, 𝔻1, state1_new = compute_stress(mat, ε1, state1_old, 0.0)
set_state!(cache, 1, 1, state1_new) # Store directly in cache
# Verify elastic (no plastic strain)
@test get_state(cache, 1, 1).κ == 0.0
# Update cache for next step
update_cache!(cache)
# Step 2: Apply larger load (plastic)
ε2 = SymmetricTensor{2,3}((0.005, 0.0, 0.0, 0.0, 0.0, 0.0))
state2_old = get_state(cache, 1, 1) # Get directly from cache
σ2, 𝔻2, state2_new = compute_stress(mat, ε2, state2_old, 0.0)
set_state!(cache, 1, 1, state2_new) # Store directly in cache
# Verify plastic strain accumulated
@test get_state(cache, 1, 1).κ > 0.0
# Old state should still be from step 1 (elastic)
@test get_old_state(cache, 1, 1).κ == 0.0
end
@testset "State variable helpers" begin
# Test get/set state variable helpers
state = (
ε_p=SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)),
α=SymmetricTensor{2,3}((100e6, 0.0, 0.0, 0.0, 0.0, 0.0)),
κ=0.01
)
# Get individual variables
ε_p = get_state_variable(state, PlasticStrain)
α = get_state_variable(state, Backstress)
κ = get_state_variable(state, EquivalentPlasticStrain)
@test ε_p == state.ε_p
@test α == state.α
@test κ == state.κ
# Set individual variables (immutable update)
new_κ = 0.02
state_updated = set_state_variable(state, EquivalentPlasticStrain, new_κ)
@test state_updated.κ == new_κ
@test state_updated.ε_p == state.ε_p # Unchanged
@test state_updated.α == state.α # Unchanged
@test state.κ == 0.01 # Original unchanged
end
@testset "Type stability" begin
# Verify type stability of key operations
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
cache = create_global_material_cache(mat, n_ips=4, n_elems=2)
# Cache access should be type-stable
@inferred get_state(cache, 1, 1)
@inferred get_old_state(cache, 1, 1)
# compute_stress should be type-stable
state_nt = (ε_p=zero(SymmetricTensor{2,3}), α=zero(SymmetricTensor{2,3}), κ=0.0)
ε = SymmetricTensor{2,3}((1e-4, 0.0, 0.0, 0.0, 0.0, 0.0))
@inferred compute_stress(mat, ε, state_nt, 0.0)
end
end