Files
JuliaFEM.jl/test/materials/test_plasticity_new_api.jl
T
Jukka Aho 2bcc7ecd1e test(materials): add plasticity new API visionary test
New 747-line test file for plasticity NEW API (test-driven development):
- Visionary tests for J2 plasticity with isotropic hardening
- Tests for radial return mapping algorithm
- Tests for kinematic hardening and Bauschinger effect
- Tests for perfect plasticity and cyclic loading
- Extensive documentation of intended API design
- Tests currently skipped (@test_skip) until implementation complete
- Documents return mapping, consistent tangent, state storage
- Documents nodal assembly for plasticity

This test file serves as both test suite and API design documentation
for the new plasticity material model interface.
2025-12-15 08:36:54 +02:00

748 lines
22 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.
"""
# Plasticity - NEW API (Test-Driven Development)
**What:** Shows how plasticity models SHOULD work with the NEW API
**Why:**
- **Permanent deformation** - Irreversible (metals, soils)
- **Yield criterion** - von Mises, Tresca, Drucker-Prager
- **Hardening** - Isotropic, kinematic, mixed
- **Rate-independence** - Path-independent (classical plasticity)
- **History-dependent** - Internal state variables
**NEW API Concepts:**
1. **Plastic material types** - J2Plasticity, DruckerPrager
2. **Yield function** - f(σ, α) ≤ 0 (elastic domain)
3. **Flow rule** - Plastic strain rate direction
4. **Hardening laws** - Isotropic (expanding yield surface), kinematic (translation)
5. **Return mapping** - Radial return, closest point projection
**Test Problems:**
## Test 1: J2 Plasticity (von Mises)
- Yield: f = √(3J₂) - σ_y(ε_p)
- Isotropic hardening
- Validates return mapping algorithm
## Test 2: Kinematic Hardening
- Backstress α (yield surface translates)
- Armstrong-Frederick model
- Validates ratcheting behavior
## Test 3: Perfect Plasticity
- No hardening: σ_y = constant
- Validates elastic-perfectly plastic
- Tests limit load
## Test 4: Cyclic Loading (Bauschinger Effect)
- Load → Unload → Reverse load
- Validates kinematic hardening
- Tests hysteresis loop
**Expected Behavior (when implemented):**
✅ Yield criterion correctly evaluated
✅ Elastic-plastic split accurate
✅ Return mapping converges
✅ Hardening modulus computed correctly
✅ Consistent tangent for Newton
✅ Path-independence validated
**Status:** 🚧 VISIONARY TEST - Implementation in progress
"""
using Test
using JuliaFEM
using Tensors
using LinearAlgebra
using Statistics
@testset "Plasticity - NEW API (TDD)" begin
# =============================================================================
# J2 PLASTICITY (VON MISES)
# =============================================================================
@testset "J2 Plasticity - Isotropic Hardening (Visionary)" begin
@test_skip begin # Skip until implemented
# Material parameters
E = 200e3 # Young's modulus (MPa)
ν = 0.3 # Poisson's ratio
σ_y0 = 250.0 # Initial yield stress (MPa)
H = 2000.0 # Hardening modulus (MPa)
# NEW: J2 plasticity material
material = J2Plasticity(
E=E,
ν=ν,
yield_stress=σ_y0,
hardening=IsotropicHardening(H=H),
hardening_law=:linear # or :exponential, :voce
)
# Strain history (uniaxial tension)
ε_max = 0.005 # 0.5% total strain
n_steps = 100
ε_history = range(0, ε_max, length=n_steps)
# Strain tensor (uniaxial)
σ_history = []
ε_p_history = []
# Internal state
state = PlasticState(
ε_p=zero(SymmetricTensor{2,3}), # Plastic strain
ε_p_eq=0.0, # Equivalent plastic strain
α=zero(SymmetricTensor{2,3}) # Backstress (if kinematic)
)
for ε in ε_history
# Strain tensor (uniaxial tension in x)
ε_total = SymmetricTensor{2,3}((
ε, 0.0, 0.0,
0.0, -ν * ε, 0.0,
0.0, 0.0, -ν * ε
))
# Compute stress (with return mapping)
σ, state_new = compute_stress(material, ε_total, state)
push!(σ_history, σ[1, 1]) # Axial stress
push!(ε_p_history, state_new.ε_p_eq)
state = state_new
end
# Validate elastic region
ε_elastic = σ_y0 / E
elastic_indices = findall(ε_history .<= ε_elastic)
for i in elastic_indices
# Elastic: σ = E ε
@test isapprox(σ_history[i], E * ε_history[i], rtol=0.01)
@test ε_p_history[i] == 0.0
end
# Validate plastic region
plastic_indices = findall(ε_history .> ε_elastic)
for i in plastic_indices
# Plastic: σ_y(ε_p) = σ_y0 + H ε_p
ε_p = ε_p_history[i]
σ_y_current = σ_y0 + H * ε_p
# Stress should be at yield
@test isapprox(σ_history[i], σ_y_current, rtol=0.01)
end
end
end
# =============================================================================
# RETURN MAPPING ALGORITHM
# =============================================================================
@testset "Radial Return Mapping (Visionary)" begin
@test_skip begin
material = J2Plasticity(
E=200e3,
ν=0.3,
yield_stress=250.0,
hardening=IsotropicHardening(H=2000.0)
)
# Trial elastic step (exceed yield)
ε_trial = SymmetricTensor{2,3}((
0.003, 0.001, 0.0,
0.001, 0.002, 0.0,
0.0, 0.0, 0.0
))
state = PlasticState(
ε_p=zero(SymmetricTensor{2,3}),
ε_p_eq=0.0,
α=zero(SymmetricTensor{2,3})
)
# Elastic predictor
σ_trial = elastic_stress(material, ε_trial - state.ε_p)
# Yield function
s_trial = dev(σ_trial) # Deviatoric stress
q_trial = sqrt(1.5 * dcontract(s_trial, s_trial)) # von Mises stress
f_trial = q_trial - material.yield_stress
if f_trial > 0
# Plastic: Return mapping required
σ, state_new = return_mapping(material, σ_trial, state)
# Validate yield criterion satisfied
s = dev(σ)
q = sqrt(1.5 * dcontract(s, s))
σ_y_current = material.yield_stress + material.H * state_new.ε_p_eq
@test isapprox(q, σ_y_current, atol=1e-6)
# Validate plastic strain increased
@test state_new.ε_p_eq > state.ε_p_eq
else
# Elastic: No return mapping
@test f_trial <= 0
end
end
end
# =============================================================================
# KINEMATIC HARDENING (ARMSTRONG-FREDERICK)
# =============================================================================
@testset "Kinematic Hardening (Visionary)" begin
@test_skip begin
# Material with kinematic hardening
material = J2Plasticity(
E=200e3,
ν=0.3,
yield_stress=250.0,
hardening=KinematicHardening(
C=5000.0, # Kinematic hardening modulus
γ=50.0 # Armstrong-Frederick parameter
),
mixed_hardening=false
)
# Cyclic loading: tension → compression
ε_max = 0.005
n_cycles = 3
ε_history = []
σ_history = []
state = PlasticState(
ε_p=zero(SymmetricTensor{2,3}),
ε_p_eq=0.0,
α=zero(SymmetricTensor{2,3}) # Backstress
)
for cycle in 1:n_cycles
# Tension
for ε in range(0, ε_max, length=50)
ε_tensor = SymmetricTensor{2,3}((ε, 0.0, 0.0, 0.0, 0.0, 0.0))
σ, state = compute_stress(material, ε_tensor, state)
push!(ε_history, ε)
push!(σ_history, σ[1, 1])
end
# Compression
for ε in range(ε_max, -ε_max, length=100)
ε_tensor = SymmetricTensor{2,3}((ε, 0.0, 0.0, 0.0, 0.0, 0.0))
σ, state = compute_stress(material, ε_tensor, state)
push!(ε_history, ε)
push!(σ_history, σ[1, 1])
end
# Back to tension
for ε in range(-ε_max, 0, length=50)
ε_tensor = SymmetricTensor{2,3}((ε, 0.0, 0.0, 0.0, 0.0, 0.0))
σ, state = compute_stress(material, ε_tensor, state)
push!(ε_history, ε)
push!(σ_history, σ[1, 1])
end
end
# Validate Bauschinger effect
# Yield stress in compression < initial yield
σ_y_compression = minimum(σ_history[ε_history.<0])
@test abs(σ_y_compression) < material.yield_stress
# Validate hysteresis loop closes
# (For stabilized cycle)
@test length(ε_history) > 0
end
end
# =============================================================================
# PERFECT PLASTICITY (NO HARDENING)
# =============================================================================
@testset "Perfect Plasticity (Visionary)" begin
@test_skip begin
# No hardening: H = 0
material = J2Plasticity(
E=200e3,
ν=0.3,
yield_stress=250.0,
hardening=NoHardening() # H = 0
)
# Large strain (well into plastic)
ε_max = 0.01 # 1% strain
ε_history = range(0, ε_max, length=100)
σ_history = []
state = PlasticState(
ε_p=zero(SymmetricTensor{2,3}),
ε_p_eq=0.0,
α=zero(SymmetricTensor{2,3})
)
for ε in ε_history
ε_tensor = SymmetricTensor{2,3}((ε, 0.0, 0.0, 0.0, 0.0, 0.0))
σ, state = compute_stress(material, ε_tensor, state)
push!(σ_history, σ[1, 1])
end
# After yield, stress should be constant
ε_yield = material.yield_stress / material.E
plastic_indices = findall(ε_history .> ε_yield)
σ_plastic = σ_history[plastic_indices]
# All plastic stresses ≈ σ_y (no hardening!)
@test all(isapprox.(σ_plastic, material.yield_stress, rtol=0.01))
end
end
# =============================================================================
# CONSISTENT TANGENT (FOR NEWTON)
# =============================================================================
@testset "Consistent Tangent (Visionary)" begin
@test_skip begin
material = J2Plasticity(
E=200e3,
ν=0.3,
yield_stress=250.0,
hardening=IsotropicHardening(H=2000.0)
)
# Strain state (plastic)
ε = SymmetricTensor{2,3}((0.003, 0.001, 0.0, 0.001, 0.002, 0.0))
state = PlasticState(
ε_p=SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0)),
ε_p_eq=0.001,
α=zero(SymmetricTensor{2,3})
)
# Compute stress and tangent
σ, state_new, C_ep = compute_stress_tangent(material, ε, state)
# Validate tangent via finite difference
δε = 1e-8
for i in 1:6 # Voigt notation
ε_pert = ε + δε * basis_symmetric_tensor(i)
σ_pert, _ = compute_stress(material, ε_pert, state)
dσ_numerical = (σ_pert - σ) / δε
dσ_tangent = C_ep basis_symmetric_tensor(i)
@test isapprox(dσ_numerical, dσ_tangent, rtol=0.01)
end
# Validate symmetry (major)
for i in 1:6, j in 1:6
@test isapprox(C_ep[i, j], C_ep[j, i], atol=1e-10)
end
end
end
# =============================================================================
# MULTI-AXIAL LOADING
# =============================================================================
@testset "Multi-Axial Loading (Visionary)" begin
@test_skip begin
material = J2Plasticity(
E=200e3,
ν=0.3,
yield_stress=250.0,
hardening=IsotropicHardening(H=2000.0)
)
# Combined tension + shear
ε_axial_max = 0.003
ε_shear_max = 0.002
n_steps = 100
σ_history = []
state = PlasticState(
ε_p=zero(SymmetricTensor{2,3}),
ε_p_eq=0.0,
α=zero(SymmetricTensor{2,3})
)
for i in 1:n_steps
# Proportional loading
ε_axial = ε_axial_max * i / n_steps
ε_shear = ε_shear_max * i / n_steps
ε = SymmetricTensor{2,3}((
ε_axial, ε_shear, 0.0,
ε_shear, 0.0, 0.0,
0.0, 0.0, 0.0
))
σ, state = compute_stress(material, ε, state)
push!(σ_history, σ)
end
# Validate von Mises yield criterion
for σ in σ_history
s = dev(σ)
q = sqrt(1.5 * dcontract(s, s))
σ_y_current = material.yield_stress + material.H * state.ε_p_eq
# Should be at or below yield
@test q <= σ_y_current + 1e-6
end
end
end
# =============================================================================
# PSEUDO-CODE: PLASTICITY INTEGRATION
# =============================================================================
@testset "Plasticity Integration Pattern (Visionary)" begin
# Pseudo-code showing return mapping
println("\n" * "="^70)
println("PLASTICITY INTEGRATION (RETURN MAPPING)")
println("="^70)
integration_pseudo = """
# Return mapping algorithm (radial return for J2)
function compute_stress_plastic(material, ε_total, state_old)
# 1. Elastic predictor
ε_elastic_trial = ε_total - state_old.ε_p
σ_trial = C_elastic ⊡ ε_elastic_trial
# 2. Check yield
s_trial = dev(σ_trial) # Deviatoric
q_trial = sqrt(1.5 * s_trial : s_trial) # von Mises
σ_y = material.σ_y0 + H * state_old.ε_p_eq
f_trial = q_trial - σ_y
if f_trial <= 0
# Elastic: Accept trial state
return σ_trial, state_old
end
# 3. Plastic corrector (return mapping)
# Solve for Δλ (plastic multiplier)
# f = q - σ_y(ε_p + Δλ) = 0
# Newton iteration
Δλ = 0.0
for iter in 1:max_iter
σ_y_current = material.σ_y0 + H * (state_old.ε_p_eq + Δλ)
q_current = q_trial - 3*G*Δλ # G = shear modulus
f = q_current - σ_y_current
if abs(f) < tol
break
end
# Derivative: df/dΔλ
df_dΔλ = -3*G - H
# Update
Δλ -= f / df_dΔλ
end
# 4. Update stress and state
n = s_trial / norm(s_trial) # Flow direction
σ = σ_trial - 2*G*Δλ * n
ε_p_new = state_old.ε_p + Δλ * n
ε_p_eq_new = state_old.ε_p_eq + Δλ
state_new = PlasticState(ε_p_new, ε_p_eq_new, state_old.α)
return σ, state_new
end
"""
println(integration_pseudo)
println("="^70)
println("✓ Elastic predictor: Assume elastic step")
println("✓ Check yield: f(σ_trial) ≤ 0?")
println("✓ Return mapping: Project back to yield surface")
println("✓ Newton iteration: Solve for plastic multiplier Δλ")
println("✓ Update state: ε_p, ε_p_eq, α")
println("="^70)
end
# =============================================================================
# KEY ARCHITECTURAL INSIGHTS
# =============================================================================
println("\n" * "="^70)
println("PLASTICITY ARCHITECTURE INSIGHTS (NEW API)")
println("="^70)
println("✓ J2 plasticity: von Mises yield, isotropic/kinematic hardening")
println("✓ Yield function: f(σ, α) = √(3J₂) - σ_y(ε_p)")
println("✓ Return mapping: Radial return, closest point projection")
println("✓ Consistent tangent: C_ep for Newton quadratic convergence")
println("✓ Internal state: ε_p, ε_p_eq, α (per integration point!)")
println("✓ Isotropic hardening: Yield surface expands")
println("✓ Kinematic hardening: Yield surface translates (Bauschinger)")
println("✓ Path-independent: Same final state for same strain path")
println("✓ Works with Newton-Krylov (tangent from return mapping)")
println("="^70)
end
"""
# IMPLEMENTATION NOTES
## J2 Plasticity (von Mises)
### Yield Function
**Definition:**
f(σ, ε_p) = √(3J₂) - σ_y(ε_p)
where:
- J₂ = (1/2) s:s (second deviatoric invariant)
- s = σ - (1/3)tr(σ)I (deviatoric stress)
- σ_y(ε_p) = yield stress (function of plastic strain)
**Equivalent form:**
f = q - σ_y
where q = √(3J₂) = von Mises stress.
**Elastic domain:** f ≤ 0
**Yield surface:** f = 0
### Flow Rule
**Associative plasticity:** Plastic strain rate direction = yield gradient
ε̇_p = λ̇ ∂f/∂σ = λ̇ (3/2) s/q = λ̇ n
where:
- λ̇ = plastic multiplier (rate)
- n = (3/2) s/q = flow direction (unit deviatoric)
**Properties:**
- Incompressible: tr(ε̇_p) = 0 (volume preserving)
- Radial: ε̇_p ∝ s (proportional to deviatoric stress)
### Hardening Laws
**Isotropic (linear):**
σ_y(ε_p) = σ_y0 + H ε_p_eq
where:
- σ_y0 = initial yield stress
- H = hardening modulus
- ε_p_eq = ∫ √(2/3 ε̇_p:ε̇_p) dt = equivalent plastic strain
**Isotropic (exponential/Voce):**
σ_y(ε_p) = σ_∞ - (σ_∞ - σ_y0) exp(-b ε_p_eq)
Saturates to σ_∞.
**Kinematic (Armstrong-Frederick):**
α̇ = C ε̇_p - γ α λ̇
where:
- α = backstress (2nd order tensor)
- C = kinematic hardening modulus
- γ = recall parameter
**Modified yield:**
f = √(3/2 (s-α):(s-α)) - σ_y
### Return Mapping Algorithm
**Problem:** Given ε_{n+1}, find σ_{n+1} and state_{n+1}.
**Elastic predictor:**
```
ε_e_trial = ε_{n+1} - ε_p_n
σ_trial = C_elastic : ε_e_trial
```
**Check yield:**
```
f_trial = q_trial - σ_y(ε_p_eq_n)
```
**If f_trial ≤ 0:** Elastic, return (σ_trial, state_n)
**If f_trial > 0:** Plastic, solve for Δλ:
**Consistency condition:**
f(σ_{n+1}, ε_p_eq_{n+1}) = 0
**Discretized flow rule:**
ε_p_{n+1} = ε_p_n + Δλ n
**Stress update:**
σ_{n+1} = σ_trial - 2G Δλ n
where G = shear modulus.
**Yield condition:**
q_{n+1} = q_trial - 3G Δλ = σ_y(ε_p_eq_n + Δλ)
**Solve for Δλ (Newton):**
```julia
function return_mapping(σ_trial, state, material)
s_trial = dev(σ_trial)
q_trial = sqrt(1.5 * dcontract(s_trial, s_trial))
n = s_trial / norm(s_trial)
# Initial guess
Δλ = 0.0
ε_p_eq_old = state.ε_p_eq
G = material.E / (2*(1 + material.ν))
H = material.H
for iter in 1:max_iter
# Current yield stress
σ_y = material.σ_y0 + H * (ε_p_eq_old + Δλ)
# Residual
f = q_trial - 3*G*Δλ - σ_y
if abs(f) < tol
break
end
# Derivative
df_dΔλ = -3*G - H
# Newton update
Δλ -= f / df_dΔλ
end
# Update stress
σ = σ_trial - 2*G*Δλ * n
# Update state
ε_p_new = state.ε_p + Δλ * n
ε_p_eq_new = ε_p_eq_old + Δλ
return σ, PlasticState(ε_p_new, ε_p_eq_new, state.α)
end
```
### Consistent Tangent
**For Newton convergence:** Need C_ep = dσ/dε (algorithmic tangent).
**Elastic:**
C_ep = C_elastic
**Plastic:** More complex!
C_ep = C_elastic - (2G)² / (3G + H) * (n ⊗ n)
where ⊗ = outer product.
**Derivation:** Chain rule through return mapping.
**Properties:**
- Symmetric (major symmetry)
- Positive-definite (for H > 0)
- Converges to C_elastic as Δλ → 0
## Internal State Storage
**Per integration point:**
```julia
struct PlasticState{dim}
ε_p::SymmetricTensor{2,dim} # Plastic strain
ε_p_eq::Float64 # Equivalent plastic strain
α::SymmetricTensor{2,dim} # Backstress (kinematic)
end
```
**Element-level:**
```julia
struct PlasticElement
topology::AbstractTopology
basis::AbstractBasis
nodes::NTuple{N,Int}
state::Vector{PlasticState} # One per integration point!
end
```
**Key:** State is HISTORY-DEPENDENT, must be stored!
## Nodal Assembly (Plasticity)
```julia
function tangent_matvec_plastic!(w, v, u_current, material, elements, states)
Threads.@threads for node_i in 1:n_nodes
w_local = zero(Vec{3})
for elem in node_to_elements[node_i]
for (ip_idx, ip) in enumerate(integration_points(elem))
# Current state at this integration point
state = states[elem][ip_idx]
# Strain
ε = compute_strain(elem, ip, u_current)
# Consistent tangent (elastic or plastic)
σ, state_new, C_ep = compute_stress_tangent(material, ε, state)
for node_j in elem.nodes
# Tangent block
K_t_ij = compute_plastic_tangent_block(elem, node_i, node_j, C_ep, ip)
v_j = Vec{3}(v[3*(node_j-1)+1:3*node_j])
w_local += K_t_ij ⊡ v_j
end
# Update state (for next iteration)
states[elem][ip_idx] = state_new
end
end
w[3*(node_i-1)+1:3*node_i] = w_local
end
end
```
**Key:** State updated during tangent computation!
## Next Steps
1. Implement `J2Plasticity` material type
2. Implement `PlasticState` struct
3. Implement `return_mapping` algorithm
4. Implement `compute_stress_plastic`
5. Implement `consistent_tangent_plastic`
6. Implement hardening laws (isotropic, kinematic)
7. Implement state storage (per integration point)
8. Validate against analytical solutions
9. Validate against experimental data
10. Performance benchmarks
"""