""" # Damage Mechanics - NEW API (Test-Driven Development) **What:** Shows how damage models SHOULD work with the NEW API **Why:** - **Stiffness degradation** - Material weakens (cracks, voids) - **Irreversible** - Damage cannot heal (unlike plasticity unloading) - **Mesh independence** - Regularization required (crack band, nonlocal) - **Failure prediction** - Crack initiation, propagation **NEW API Concepts:** 1. **Damage variable** - d ∈ [0,1] where 0=intact, 1=failed 2. **Effective stress** - σ̄ = σ/(1-d) (undamaged configuration) 3. **Damage evolution** - ḋ = f(ε, ε_max, damage parameters) 4. **Regularization** - Length scale to avoid mesh sensitivity 5. **Coupled damage-plasticity** - Combined degradation + permanent deformation **Test Problems:** ## Test 1: Isotropic Damage - Stiffness reduction: E_eff = (1-d) E - Damage driven by strain energy - Validates crack initiation ## Test 2: Ductile Damage (Lemaitre) - Coupled damage-plasticity - Damage from plastic dissipation - Validates void growth ## Test 3: Crack Band Regularization - Mesh-independent energy dissipation - Length scale h = element size - Validates objectivity ## Test 4: Damage Unloading - Permanent stiffness loss - No damage healing - Validates irreversibility **Expected Behavior (when implemented):** ✅ Damage variable d ∈ [0,1] ✅ Stiffness degrades smoothly ✅ Mesh-independent fracture energy ✅ No healing on unload ✅ Crack localization captured ✅ Failure criterion satisfied **Status:** 🚧 VISIONARY TEST - Implementation in progress """ using Test using JuliaFEM using Tensors using LinearAlgebra using Statistics @testset "Damage Mechanics - NEW API (TDD)" begin # ============================================================================= # ISOTROPIC DAMAGE # ============================================================================= @testset "Isotropic Damage - Strain-Based (Visionary)" begin @test_skip begin # Skip until implemented # Material parameters E = 200e3 # Young's modulus (MPa) ν = 0.3 # Poisson's ratio ε_d0 = 0.001 # Damage threshold strain ε_f = 0.01 # Failure strain # NEW: Isotropic damage material material = IsotropicDamage( E=E, ν=ν, damage_threshold=ε_d0, failure_strain=ε_f, evolution_law=:exponential # or :linear, :power ) # Strain history (uniaxial tension) ε_max = 0.015 # Beyond failure n_steps = 200 ε_history = range(0, ε_max, length=n_steps) σ_history = [] d_history = [] # Damage variable # Internal state state = DamageState( d=0.0, # Damage variable (0=intact, 1=failed) ε_eq_max=0.0 # Maximum equivalent strain (history) ) for ε in ε_history # Strain tensor (uniaxial tension) ε_tensor = SymmetricTensor{2,3}(( ε, 0.0, 0.0, 0.0, -ν * ε, 0.0, 0.0, 0.0, -ν * ε )) # Compute stress (with damage) σ, state_new = compute_stress_damage(material, ε_tensor, state) push!(σ_history, σ[1, 1]) push!(d_history, state_new.d) state = state_new end # Validate elastic region (no damage) elastic_indices = findall(ε_history .<= ε_d0) for i in elastic_indices @test isapprox(σ_history[i], E * ε_history[i], rtol=0.01) @test d_history[i] == 0.0 end # Validate damage growth damage_indices = findall((ε_history .> ε_d0) .& (ε_history .< ε_f)) for i in damage_indices @test 0.0 < d_history[i] < 1.0 # Effective stiffness reduces E_eff = E * (1 - d_history[i]) @test E_eff < E end # Validate failure failure_indices = findall(ε_history .>= ε_f) for i in failure_indices @test d_history[i] >= 0.99 # Nearly complete damage @test σ_history[i] < 0.1 * maximum(σ_history) # Stress vanishes end end end # ============================================================================= # DAMAGE EVOLUTION LAWS # ============================================================================= @testset "Damage Evolution Laws (Visionary)" begin @test_skip begin E = 200e3 ν = 0.3 ε_d0 = 0.001 ε_f = 0.01 # Test different evolution laws laws = [:linear, :exponential, :power] for law in laws material = IsotropicDamage( E=E, ν=ν, damage_threshold=ε_d0, failure_strain=ε_f, evolution_law=law ) state = DamageState(d=0.0, ε_eq_max=0.0) # Strain at 50% between threshold and failure ε_mid = (ε_d0 + ε_f) / 2 ε_tensor = SymmetricTensor{2,3}((ε_mid, 0.0, 0.0, 0.0, 0.0, 0.0)) σ, state_new = compute_stress_damage(material, ε_tensor, state) # Damage should be growing @test 0.0 < state_new.d < 1.0 # Different laws give different d values println("Law: $law, d = $(state_new.d)") end end end # ============================================================================= # DUCTILE DAMAGE (LEMAITRE MODEL) # ============================================================================= @testset "Ductile Damage - Coupled Plasticity (Visionary)" begin @test_skip begin # Coupled damage-plasticity material = DuctileDamage( # Elastic properties E=200e3, ν=0.3, # Plasticity yield_stress=250.0, hardening=IsotropicHardening(H=2000.0), # Damage (Lemaitre) damage_threshold=0.001, # Plastic strain threshold S_crit=1.0, # Critical damage value s_damage=1.5, # Triaxiality sensitivity damage_exponent=2.0 ) # Strain history (tension to failure) ε_max = 0.05 n_steps = 200 ε_history = range(0, ε_max, length=n_steps) σ_history = [] d_history = [] ε_p_history = [] state = DuctileDamageState( ε_p=zero(SymmetricTensor{2,3}), ε_p_eq=0.0, d=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_ductile_damage(material, ε_tensor, state) push!(σ_history, σ[1, 1]) push!(d_history, state.d) push!(ε_p_history, state.ε_p_eq) end # Validate coupling: Damage grows with plastic strain @test all(diff(d_history[ε_p_history.>material.damage_threshold]) .>= 0) # Validate softening: Peak stress followed by descent σ_max_idx = argmax(σ_history) @test σ_max_idx < length(σ_history) # Not at end # After peak, stress decreases (softening) @test σ_history[end] < σ_history[σ_max_idx] # Damage increases monotonically @test all(diff(d_history) .>= 0) end end # ============================================================================= # CRACK BAND REGULARIZATION # ============================================================================= @testset "Crack Band Regularization (Visionary)" begin @test_skip begin # Fracture energy per unit area (N/mm) G_f = 0.1 # Fracture energy # Two different mesh sizes h_coarse = 10.0 # mm h_fine = 2.0 # mm # Crack band materials (adjust ε_f based on mesh) # Energy = G_f = ∫ σ dε * h # For linear softening: G_f ≈ (1/2) σ_max ε_f * h σ_max = 10.0 # Tensile strength (MPa) # Coarse mesh: larger ε_f ε_f_coarse = 2 * G_f / (σ_max * h_coarse) material_coarse = IsotropicDamage( E=200e3, ν=0.3, damage_threshold=σ_max / 200e3, failure_strain=ε_f_coarse, evolution_law=:linear, crack_band_width=h_coarse ) # Fine mesh: smaller ε_f ε_f_fine = 2 * G_f / (σ_max * h_fine) material_fine = IsotropicDamage( E=200e3, ν=0.3, damage_threshold=σ_max / 200e3, failure_strain=ε_f_fine, evolution_law=:linear, crack_band_width=h_fine ) # Compute energy dissipation for both function compute_dissipation(material, ε_max, n_steps) ε_history = range(0, ε_max, length=n_steps) σ_history = [] state = DamageState(d=0.0, ε_eq_max=0.0) for ε in ε_history ε_tensor = SymmetricTensor{2,3}((ε, 0.0, 0.0, 0.0, 0.0, 0.0)) σ, state = compute_stress_damage(material, ε_tensor, state) push!(σ_history, σ[1, 1]) end # Integrate σ dε (trapezoid rule) W = sum((σ_history[i] + σ_history[i+1]) / 2 * (ε_history[i+1] - ε_history[i]) for i in 1:length(ε_history)-1) return W end W_coarse = compute_dissipation(material_coarse, 1.2 * ε_f_coarse, 500) W_fine = compute_dissipation(material_fine, 1.2 * ε_f_fine, 500) # Fracture energy per volume G_v_coarse = W_coarse * h_coarse G_v_fine = W_fine * h_fine # Should be mesh-independent! @test isapprox(G_v_coarse, G_v_fine, rtol=0.1) @test isapprox(G_v_coarse, G_f, rtol=0.1) end end # ============================================================================= # DAMAGE UNLOADING (IRREVERSIBILITY) # ============================================================================= @testset "Damage Unloading - Irreversible (Visionary)" begin @test_skip begin material = IsotropicDamage( E=200e3, ν=0.3, damage_threshold=0.001, failure_strain=0.01, evolution_law=:linear ) # Load-unload cycle ε_max = 0.005 # Partial damage # Loading ε_loading = range(0, ε_max, length=100) σ_loading = [] d_loading = [] state = DamageState(d=0.0, ε_eq_max=0.0) for ε in ε_loading ε_tensor = SymmetricTensor{2,3}((ε, 0.0, 0.0, 0.0, 0.0, 0.0)) σ, state = compute_stress_damage(material, ε_tensor, state) push!(σ_loading, σ[1, 1]) push!(d_loading, state.d) end d_max = state.d # Damage at peak load # Unloading ε_unloading = range(ε_max, 0, length=100) σ_unloading = [] d_unloading = [] for ε in ε_unloading ε_tensor = SymmetricTensor{2,3}((ε, 0.0, 0.0, 0.0, 0.0, 0.0)) σ, state = compute_stress_damage(material, ε_tensor, state) push!(σ_unloading, σ[1, 1]) push!(d_unloading, state.d) end # Validate irreversibility @test all(d_unloading .≈ d_max) # Damage does not heal! # Validate reduced stiffness E_damaged = (1 - d_max) * material.E # Unloading slope should match damaged stiffness # (linear regression on unloading curve) ε_unload_vals = collect(ε_unloading) slope = (σ_unloading[1] - σ_unloading[end]) / (ε_unload_vals[1] - ε_unload_vals[end]) @test isapprox(slope, E_damaged, rtol=0.1) end end # ============================================================================= # CONSISTENT TANGENT (DAMAGE) # ============================================================================= @testset "Consistent Tangent - Damage (Visionary)" begin @test_skip begin material = IsotropicDamage( E=200e3, ν=0.3, damage_threshold=0.001, failure_strain=0.01, evolution_law=:exponential ) # Strain state (damaged) ε = SymmetricTensor{2,3}((0.003, 0.001, 0.0, 0.001, 0.002, 0.0)) state = DamageState(d=0.3, ε_eq_max=0.003) # Compute stress and tangent σ, state_new, C_damage = compute_stress_tangent_damage(material, ε, state) # Validate tangent via finite difference δε = 1e-8 for i in 1:6 # Voigt notation ε_pert = ε + δε * basis_symmetric_tensor(i) σ_pert, _ = compute_stress_damage(material, ε_pert, state) dσ_numerical = (σ_pert - σ) / δε dσ_tangent = C_damage ⊡ basis_symmetric_tensor(i) @test isapprox(dσ_numerical, dσ_tangent, rtol=0.01) end # Validate symmetry for i in 1:6, j in 1:6 @test isapprox(C_damage[i, j], C_damage[j, i], atol=1e-10) end # Validate degradation C_elastic = compute_elastic_stiffness(material) # Damaged stiffness should be less @test norm(C_damage) < norm(C_elastic) end end # ============================================================================= # PSEUDO-CODE: DAMAGE INTEGRATION # ============================================================================= @testset "Damage Integration Pattern (Visionary)" begin # Pseudo-code showing damage evolution println("\n" * "="^70) println("DAMAGE INTEGRATION") println("="^70) integration_pseudo = """ # Damage evolution (strain-based isotropic) function compute_stress_damage(material, ε, state_old) # 1. Compute equivalent strain ε_eq = compute_equivalent_strain(ε) # e.g., sqrt(ε:ε) # 2. Update history (loading surface) ε_eq_max = max(state_old.ε_eq_max, ε_eq) # 3. Check damage threshold if ε_eq_max <= material.ε_d0 # No damage d = 0.0 else # Damage evolution d = compute_damage(material, ε_eq_max) end # 4. Effective stress # Strain energy equivalence: W = W̄ # σ : ε = σ̄ : ε in undamaged configuration # Elastic stress (undamaged) C_elastic = compute_elastic_stiffness(material) σ_undamaged = C_elastic ⊡ ε # Apply damage σ = (1 - d) * σ_undamaged # 5. Update state state_new = DamageState(d, ε_eq_max) return σ, state_new end # Damage evolution laws function compute_damage(material, ε_eq_max) ε_d0 = material.damage_threshold ε_f = material.failure_strain if material.evolution_law == :linear # Linear: d = (ε - ε_d0) / (ε_f - ε_d0) d = (ε_eq_max - ε_d0) / (ε_f - ε_d0) elseif material.evolution_law == :exponential # Exponential: d = 1 - exp(-α(ε - ε_d0)) α = -log(0.01) / (ε_f - ε_d0) # d(ε_f) ≈ 0.99 d = 1 - exp(-α * (ε_eq_max - ε_d0)) elseif material.evolution_law == :power # Power law: d = ((ε - ε_d0)/(ε_f - ε_d0))^n n = 2.0 d = ((ε_eq_max - ε_d0) / (ε_f - ε_d0))^n end return clamp(d, 0.0, 0.99) # Numerical: never fully failed end """ println(integration_pseudo) println("="^70) println("✓ Equivalent strain: History variable") println("✓ Loading surface: ε_eq_max = max(ε_eq_max_old, ε_eq)") println("✓ Damage evolution: d = f(ε_eq_max)") println("✓ Effective stress: σ = (1-d) σ_undamaged") println("✓ Irreversible: d never decreases") println("="^70) end # ============================================================================= # KEY ARCHITECTURAL INSIGHTS # ============================================================================= println("\n" * "="^70) println("DAMAGE MECHANICS ARCHITECTURE INSIGHTS (NEW API)") println("="^70) println("✓ Isotropic damage: Stiffness degradation (1-d)E") println("✓ Damage variable: d ∈ [0,1] (0=intact, 1=failed)") println("✓ Irreversible: d never decreases (no healing)") println("✓ History: ε_eq_max (maximum strain ever reached)") println("✓ Evolution laws: Linear, exponential, power") println("✓ Crack band: Mesh-independent G_f via length scale h") println("✓ Ductile damage: Coupled with plasticity (Lemaitre)") println("✓ Consistent tangent: C_damage = ∂σ/∂ε (with damage)") println("✓ Regularization: REQUIRED for mesh objectivity") println("✓ Works with Newton-Krylov (tangent from damage law)") println("="^70) end """ # IMPLEMENTATION NOTES ## Isotropic Damage ### Damage Variable **Definition:** d ∈ [0,1] - d = 0: Intact material - d = 1: Completely damaged (failed) **Effective stress concept:** σ̄ = σ / (1 - d) where σ̄ = stress in undamaged (effective) configuration. **Strain energy equivalence:** W(σ, ε) = W̄(σ̄, ε) Implies: σ = (1 - d) σ̄ where σ̄ = C_elastic : ε. ### Equivalent Strain **For isotropic damage:** Need scalar measure of strain state. **Tension-driven:** ε_eq = √(<ε_1>² + <ε_2>² + <ε_3>²) where <·> = positive part, ε_i = principal strains. **Reason:** Damage in tension (cracks open), not compression. **Alternative (modified von Mises):** ε_eq = κ I_1 / (1-2ν) + √(3J_2) / (1+ν) where κ weighs volumetric vs deviatoric. ### Damage Evolution Laws **Linear:** d = (ε_eq - ε_d0) / (ε_f - ε_d0) for ε_eq ∈ [ε_d0, ε_f] **Exponential (smoother):** d = 1 - exp(-α(ε_eq - ε_d0)) where α chosen such that d(ε_f) ≈ 0.99. **Power law:** d = ((ε_eq - ε_d0) / (ε_f - ε_d0))^n where n controls softening rate. ### History Variable **Loading surface:** ε_eq_max = max(ε_eq_history) **Damage depends on history:** d = f(ε_eq_max) NOT f(ε_eq) **Irreversibility:** ε_eq_max only increases. **Update:** ```julia ε_eq_max_new = max(ε_eq_max_old, ε_eq_current) ``` ## Crack Band Regularization ### Mesh Sensitivity Problem **Without regularization:** Fracture energy depends on mesh size! G_num = ∫ σ dε * h where h = element size. **Finer mesh → less energy dissipation → spurious brittleness.** ### Crack Band Model **Idea:** Fracture happens over a band of width h. **Energy balance:** G_f = ∫_0^{ε_f} σ dε * h where G_f = fracture energy per unit area (material property). **Adjust failure strain:** ε_f = G_f / (∫_0^{ε_f} σ dε * h) **For linear softening:** ε_f = 2 G_f / (σ_max h) where σ_max = tensile strength. **Result:** Mesh-independent fracture energy! ### Implementation ```julia struct IsotropicDamage E::Float64 ν::Float64 damage_threshold::Float64 failure_strain::Float64 # Computed from G_f and h! crack_band_width::Float64 # h (element size) end function IsotropicDamage(; E, ν, G_f, σ_max, h) ε_d0 = σ_max / E ε_f = ε_d0 + 2*G_f / (σ_max * h) # Linear softening return IsotropicDamage(E, ν, ε_d0, ε_f, h) end ``` ## Ductile Damage (Lemaitre Model) ### Coupling: Damage + Plasticity **Damage drives plasticity:** σ_y_eff = σ_y / (1 - d) **Plasticity drives damage:** ḋ = f(plastic dissipation) ### Lemaitre Damage Evolution **Damage rate:** ḋ = (Y / S)^s ε̇_p_eq where: - Y = damage energy release rate = (σ_eq²) / (2E(1-d)²) - S = material damage strength - s = damage exponent - ε̇_p_eq = equivalent plastic strain rate **Damage threshold:** d = 0 until ε_p_eq > ε_p_threshold **Triaxiality influence:** Y = Y(σ_eq, σ_m / σ_eq) where σ_m = mean stress (pressure). **High triaxiality → void growth → more damage.** ### Integration ```julia function compute_ductile_damage(material, σ, ε_p_eq, state) if ε_p_eq < material.ε_p_threshold return 0.0 end # Damage energy release rate σ_eq = von_mises_stress(σ) Y = σ_eq^2 / (2 * material.E * (1 - state.d)^2) # Triaxiality (optional) σ_m = trace(σ) / 3 η = σ_m / σ_eq # Damage increment Δε_p = ε_p_eq - state.ε_p_eq_old Δd = (Y / material.S)^material.s * Δε_p d_new = state.d + Δd return clamp(d_new, 0.0, 0.99) end ``` ## Consistent Tangent (Damage) **For Newton:** Need C_damage = dσ/dε. **Elastic damage:** σ = (1 - d) C_elastic : ε **Tangent:** C_damage = (1 - d) C_elastic + ∂d/∂ε ⊗ σ_elastic where ⊗ = outer product. **Derivative of damage:** ∂d/∂ε = (∂d/∂ε_eq) (∂ε_eq/∂ε) **Chain rule through damage evolution law.** **For exponential:** ∂d/∂ε_eq = α exp(-α(ε_eq - ε_d0)) **For linear:** ∂d/∂ε_eq = 1 / (ε_f - ε_d0) ### Symmetry **Major symmetry:** C_damage may NOT be symmetric if ∂d/∂ε ⊗ σ not symmetric. **Options:** 1. Symmetrize: C_sym = (C + C^T) / 2 2. Use unsymmetric solver (GMRES handles it!) ## Internal State Storage **Per integration point:** ```julia struct DamageState d::Float64 # Damage variable ε_eq_max::Float64 # Maximum equivalent strain (history) end # Coupled damage-plasticity struct DuctileDamageState{dim} ε_p::SymmetricTensor{2,dim} ε_p_eq::Float64 d::Float64 α::SymmetricTensor{2,dim} end ``` **Element-level:** ```julia struct DamageElement topology::AbstractTopology basis::AbstractBasis nodes::NTuple{N,Int} state::Vector{DamageState} # Per integration point end ``` ## Nodal Assembly (Damage) ```julia function tangent_matvec_damage!(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 state = states[elem][ip_idx] # Strain ε = compute_strain(elem, ip, u_current) # Consistent tangent (with damage) σ, state_new, C_damage = compute_stress_tangent_damage(material, ε, state) for node_j in elem.nodes # Tangent block (damaged stiffness) K_t_ij = compute_damage_tangent_block(elem, node_i, node_j, C_damage, ip) v_j = Vec{3}(v[3*(node_j-1)+1:3*node_j]) w_local += K_t_ij ⊡ v_j end # Update state states[elem][ip_idx] = state_new end end w[3*(node_i-1)+1:3*node_i] = w_local end end ``` **Key:** Damage state updated each iteration! ## Regularization Techniques ### 1. Crack Band (Local) **Pros:** Simple, fast **Cons:** Still some mesh sensitivity ### 2. Nonlocal Damage **Averaged equivalent strain:** ε̄_eq(x) = (1/V_R) ∫_{B_R(x)} α(||y-x||) ε_eq(y) dy where: - B_R(x) = ball of radius R around x - α = weight function (Gaussian) **Damage driven by ε̄_eq instead of ε_eq.** **Pros:** Mesh-independent **Cons:** Expensive (nonlocal averaging) ### 3. Gradient Damage **Higher-order PDE:** ε̄_eq - c ∇²ε̄_eq = ε_eq where c = internal length scale. **Requires additional DOF or coupled system.** **Pros:** Mesh-independent, smooth localization **Cons:** Complex implementation ### 4. Phase Field (Future) **Crack as diffuse interface:** φ(x) ∈ [0,1] where φ=1 is crack. **Coupled:** - Elasticity with φ-dependent stiffness - Allen-Cahn or Ginzburg-Landau equation for φ **Pros:** Arbitrary crack topology, no remeshing **Cons:** Very expensive ## Next Steps 1. Implement `IsotropicDamage` material type 2. Implement `DamageState` struct 3. Implement damage evolution laws 4. Implement crack band regularization 5. Implement `DuctileDamage` (coupled) 6. Implement consistent tangent 7. Validate mesh independence 8. Validate against experiments 9. Performance benchmarks """