test(materials): add damage mechanics new API visionary test

New 868-line test file for damage mechanics NEW API (test-driven development):
- Visionary tests for isotropic damage model
- Tests for ductile damage (Lemaitre model)
- Tests for crack band regularization (mesh independence)
- Tests for damage unloading and irreversibility
- Extensive documentation of intended API design
- Tests currently skipped (@test_skip) until implementation complete
- Documents damage variable, effective stress, damage evolution
- Documents regularization and coupled damage-plasticity

This test file serves as both test suite and API design documentation
for the new damage mechanics material model interface.
This commit is contained in:
Jukka Aho
2025-12-15 08:41:24 +02:00
parent 6526697497
commit c514c1bdcc
+868
View File
@@ -0,0 +1,868 @@
"""
# 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
"""