""" # 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 """