oxiphysics_materials/damage/types.rs
1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7/// Bailey-Norton primary creep model.
8///
9/// ε_cr = B * σ^m * t^p
10#[derive(Debug, Clone)]
11pub struct BaileyNortonCreep {
12 /// Creep coefficient B
13 pub b: f64,
14 /// Stress exponent m
15 pub m: f64,
16 /// Time exponent p (< 1 for primary creep)
17 pub p: f64,
18}
19impl BaileyNortonCreep {
20 /// Create a new Bailey-Norton creep model.
21 pub fn new(b: f64, m: f64, p: f64) -> Self {
22 Self { b, m, p }
23 }
24 /// Creep strain: ε_cr = B * σ^m * t^p
25 pub fn creep_strain(&self, sigma: f64, time: f64) -> f64 {
26 self.b * sigma.powf(self.m) * time.powf(self.p)
27 }
28 /// Creep rate: ε̇_cr = B * σ^m * p * t^(p-1)
29 pub fn creep_rate(&self, sigma: f64, time: f64) -> f64 {
30 self.b * sigma.powf(self.m) * self.p * time.powf(self.p - 1.0)
31 }
32}
33/// General CDM model with selectable damage evolution law.
34///
35/// Tracks a scalar damage variable D that degrades effective stress
36/// and stiffness according to the principle of strain equivalence.
37#[derive(Debug, Clone)]
38pub struct CdmDamage {
39 /// Current damage variable D ∈ \[0, 1\].
40 pub d: f64,
41 /// Damage initiation strain ε_0.
42 pub eps0: f64,
43 /// Failure strain ε_f (used in linear and power-law).
44 pub eps_f: f64,
45 /// Evolution rate parameter (exponential: a, power-law: n).
46 pub param: f64,
47 /// The damage evolution law to use.
48 pub law: DamageEvolutionLaw,
49 /// Maximum historical equivalent strain (for irreversibility).
50 pub kappa: f64,
51}
52impl CdmDamage {
53 /// Create a new CDM damage model.
54 ///
55 /// # Arguments
56 /// * `eps0` - Damage initiation strain
57 /// * `eps_f` - Failure strain (for Linear/PowerLaw)
58 /// * `param` - Rate parameter (exponential: a, power-law exponent: n)
59 /// * `law` - Which evolution law to use
60 pub fn new(eps0: f64, eps_f: f64, param: f64, law: DamageEvolutionLaw) -> Self {
61 Self {
62 d: 0.0,
63 eps0,
64 eps_f,
65 param,
66 law,
67 kappa: 0.0,
68 }
69 }
70 /// Update damage based on current equivalent strain.
71 ///
72 /// Damage only increases (irreversibility via internal variable κ).
73 pub fn update(&mut self, equivalent_strain: f64) {
74 if equivalent_strain <= self.kappa {
75 return;
76 }
77 self.kappa = equivalent_strain;
78 if equivalent_strain <= self.eps0 {
79 return;
80 }
81 let d_new = match self.law {
82 DamageEvolutionLaw::Linear => {
83 let range = self.eps_f - self.eps0;
84 if range.abs() < f64::EPSILON {
85 1.0
86 } else {
87 let eps = equivalent_strain;
88 (self.eps_f / eps) * (eps - self.eps0) / range
89 }
90 }
91 DamageEvolutionLaw::Exponential => {
92 let delta = equivalent_strain - self.eps0;
93 1.0 - (-self.param * delta).exp()
94 }
95 DamageEvolutionLaw::PowerLaw => {
96 let range = self.eps_f - self.eps0;
97 if range.abs() < f64::EPSILON {
98 1.0
99 } else {
100 let ratio = ((equivalent_strain - self.eps0) / range).min(1.0);
101 ratio.powf(self.param)
102 }
103 }
104 };
105 self.d = d_new.clamp(self.d, 1.0);
106 }
107 /// Effective stiffness: E_eff = (1 - D) * E.
108 #[allow(dead_code)]
109 pub fn effective_stiffness(&self, young_modulus: f64) -> f64 {
110 (1.0 - self.d) * young_modulus
111 }
112 /// Effective stress: σ_eff = σ / (1 - D).
113 #[allow(dead_code)]
114 pub fn effective_stress(&self, nominal_stress: f64) -> f64 {
115 let w = 1.0 - self.d;
116 if w < 1e-15 {
117 f64::INFINITY
118 } else {
119 nominal_stress / w
120 }
121 }
122 /// Compute the strain localization indicator based on acoustic tensor analysis.
123 ///
124 /// In continuum damage mechanics, strain localization (shear band initiation)
125 /// occurs when the acoustic tensor Q(n) becomes singular. The indicator is:
126 ///
127 /// L = D * E / ((1-D)² * (1 - 2ν) * (1 + ν) / (3*(1-ν)))
128 ///
129 /// or more precisely the determinant-like measure derived from the damaged
130 /// tangent modulus. Here we use a practical scalar indicator:
131 ///
132 /// L = D / ((1-D)² + ε_reg)
133 ///
134 /// scaled by the ratio of shear to bulk modulus terms for 3D isotropy.
135 ///
136 /// # Arguments
137 /// * `young_modulus` - Undamaged Young's modulus E \[Pa\]
138 /// * `poisson_ratio` - Poisson's ratio ν (0 ≤ ν < 0.5)
139 ///
140 /// # Returns
141 /// Localization indicator L (dimensionless, positive; → ∞ near full damage)
142 pub fn compute_localization_indicator(&self, young_modulus: f64, poisson_ratio: f64) -> f64 {
143 let nu = poisson_ratio;
144 let w = 1.0 - self.d;
145 let reg = 1e-15_f64;
146 let damage_term = self.d / (w * w + reg);
147 let geometric_factor = if (1.0 - nu).abs() > 1e-15 {
148 (1.0 - 2.0 * nu) / (2.0 * (1.0 - nu))
149 } else {
150 1.0
151 };
152 let _ = young_modulus;
153 damage_term * geometric_factor.max(1e-15)
154 }
155}
156/// Non-local integral averaging for damage regularization.
157///
158/// Computes a weighted average of a local field (e.g., equivalent strain)
159/// over a neighborhood defined by a characteristic length l_c.
160///
161/// ε̄(x) = Σ w(|x - xᵢ|) * ε(xᵢ) / Σ w(|x - xᵢ|)
162///
163/// where w(r) = exp(-r² / (2*l_c²)) is the Gaussian weight.
164#[derive(Debug, Clone)]
165pub struct NonLocalRegularization {
166 /// Characteristic (internal) length l_c (m).
167 pub lc: f64,
168}
169impl NonLocalRegularization {
170 /// Create a new non-local regularization with characteristic length l_c.
171 pub fn new(lc: f64) -> Self {
172 Self { lc }
173 }
174 /// Gaussian weight function: w(r) = exp(-r² / (2*l_c²)).
175 #[allow(dead_code)]
176 pub fn weight(&self, distance: f64) -> f64 {
177 (-distance * distance / (2.0 * self.lc * self.lc)).exp()
178 }
179 /// Bell-shaped (polynomial) weight function (compact support at r = l_c).
180 ///
181 /// w(r) = (1 - (r/l_c)²)² for r < l_c, 0 otherwise
182 #[allow(dead_code)]
183 pub fn weight_bell(&self, distance: f64) -> f64 {
184 if distance >= self.lc {
185 0.0
186 } else {
187 let ratio = distance / self.lc;
188 let t = 1.0 - ratio * ratio;
189 t * t
190 }
191 }
192 /// Compute non-local average of a field at a given point.
193 ///
194 /// # Arguments
195 /// * `point` - Position at which to evaluate the non-local average
196 /// * `positions` - Positions of all integration points
197 /// * `values` - Local field values at each integration point
198 #[allow(dead_code)]
199 pub fn nonlocal_average(
200 &self,
201 point: &[f64; 3],
202 positions: &[[f64; 3]],
203 values: &[f64],
204 ) -> f64 {
205 let mut weighted_sum = 0.0;
206 let mut weight_sum = 0.0;
207 for (pos, &val) in positions.iter().zip(values.iter()) {
208 let dx = point[0] - pos[0];
209 let dy = point[1] - pos[1];
210 let dz = point[2] - pos[2];
211 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
212 let w = self.weight(dist);
213 weighted_sum += w * val;
214 weight_sum += w;
215 }
216 if weight_sum.abs() < f64::EPSILON {
217 0.0
218 } else {
219 weighted_sum / weight_sum
220 }
221 }
222 /// Compute non-local averages for all points simultaneously.
223 ///
224 /// Returns a vector of non-local averages, one per point.
225 #[allow(dead_code)]
226 pub fn nonlocal_average_all(&self, positions: &[[f64; 3]], values: &[f64]) -> Vec<f64> {
227 positions
228 .iter()
229 .map(|pt| self.nonlocal_average(pt, positions, values))
230 .collect()
231 }
232}
233/// Isotropic damage model (Lemaitre 1985).
234///
235/// Damage variable D ∈ \[0, 1\]:
236/// - D = 0: undamaged
237/// - D = 1: fully damaged (cannot carry load)
238///
239/// Effective stress: σ_eff = σ / (1 - D)
240#[derive(Debug, Clone)]
241pub struct IsotropicDamage {
242 /// Current damage variable D ∈ \[0, 1\]
243 pub d: f64,
244 /// Damage initiation threshold Y_0
245 pub energy_release_threshold: f64,
246 /// Exponential softening rate a
247 pub damage_evolution_rate: f64,
248}
249impl IsotropicDamage {
250 /// Create a new isotropic damage model.
251 ///
252 /// # Arguments
253 /// * `y0` - Energy release threshold for damage initiation
254 /// * `evolution_rate` - Exponential softening rate `a`
255 pub fn new(y0: f64, evolution_rate: f64) -> Self {
256 Self {
257 d: 0.0,
258 energy_release_threshold: y0,
259 damage_evolution_rate: evolution_rate,
260 }
261 }
262 /// Strain energy release rate: Y = σ² / (2 * E * (1-D)²)
263 pub fn strain_energy_release_rate(&self, sigma: f64, young_modulus: f64) -> f64 {
264 let integrity = 1.0 - self.d;
265 sigma * sigma / (2.0 * young_modulus * integrity * integrity)
266 }
267 /// Update damage state based on current stress and stiffness.
268 ///
269 /// D = 1 - (Y_0/Y) * exp(-a*(Y-Y_0)/Y_0) if Y > Y_0 and D_new > D
270 pub fn update(&mut self, sigma: f64, young_modulus: f64) {
271 let y = self.strain_energy_release_rate(sigma, young_modulus);
272 let y0 = self.energy_release_threshold;
273 if y > y0 {
274 let a = self.damage_evolution_rate;
275 let d_new = 1.0 - (y0 / y) * (-(a * (y - y0) / y0)).exp();
276 let d_new = d_new.clamp(self.d, 1.0);
277 self.d = d_new;
278 }
279 }
280 /// Effective (degraded) stiffness: E_eff = E * (1 - D)
281 pub fn effective_stiffness(&self, young_modulus: f64) -> f64 {
282 young_modulus * (1.0 - self.d)
283 }
284 /// Effective stress (nominal stress divided by integrity): σ_eff = σ / (1 - D)
285 pub fn effective_stress(&self, nominal_stress: f64) -> f64 {
286 let integrity = 1.0 - self.d;
287 if integrity < 1.0e-15 {
288 f64::INFINITY
289 } else {
290 nominal_stress / integrity
291 }
292 }
293}
294/// Progressive failure analysis (PFA) for a composite laminate.
295///
296/// Each ply is assigned a scalar damage variable d ∈ \[0, 1\].
297/// When d = 1 for a ply, it is considered fully failed and its stiffness
298/// contribution is set to zero (ply-discount method).
299#[allow(dead_code)]
300#[derive(Debug, Clone)]
301pub struct ProgressiveFailureAnalysis {
302 /// Number of plies.
303 pub n_plies: usize,
304 /// Damage state of each ply (0 = intact, 1 = failed).
305 pub ply_damage: Vec<f64>,
306 /// Hashin criteria for this laminate.
307 pub criteria: HashinFailureCriteria,
308}
309impl ProgressiveFailureAnalysis {
310 /// Create a new PFA model with `n_plies` plies.
311 #[allow(clippy::too_many_arguments)]
312 pub fn new(n_plies: usize, x_t: f64, x_c: f64, y_t: f64, y_c: f64, s12: f64) -> Self {
313 Self {
314 n_plies,
315 ply_damage: vec![0.0; n_plies],
316 criteria: HashinFailureCriteria::new(x_t, x_c, y_t, y_c, s12),
317 }
318 }
319 /// Degrade ply `i` by setting damage to `d` (irreversible).
320 pub fn degrade_ply(&mut self, ply_idx: usize, d: f64) {
321 if ply_idx < self.n_plies {
322 self.ply_damage[ply_idx] = self.ply_damage[ply_idx].max(d).clamp(0.0, 1.0);
323 }
324 }
325 /// Check if ply `i` is fully failed (d ≥ 1).
326 pub fn is_ply_failed(&self, ply_idx: usize) -> bool {
327 self.ply_damage.get(ply_idx).copied().unwrap_or(0.0) >= 1.0 - f64::EPSILON
328 }
329 /// Check if the entire laminate has failed (all plies failed).
330 pub fn is_fully_failed(&self) -> bool {
331 self.ply_damage.iter().all(|&d| d >= 1.0 - f64::EPSILON)
332 }
333 /// Mean damage across all plies.
334 pub fn mean_damage(&self) -> f64 {
335 if self.n_plies == 0 {
336 return 0.0;
337 }
338 self.ply_damage.iter().sum::<f64>() / self.n_plies as f64
339 }
340 /// Apply ply-level stress state and update damage based on Hashin criteria.
341 pub fn apply_stress(
342 &mut self,
343 ply_idx: usize,
344 sigma1: f64,
345 sigma2: f64,
346 tau12: f64,
347 d_increment: f64,
348 ) {
349 if ply_idx < self.n_plies && self.criteria.is_failed(sigma1, sigma2, tau12) {
350 self.degrade_ply(ply_idx, self.ply_damage[ply_idx] + d_increment);
351 }
352 }
353 /// Effective laminate stiffness factor (mean over surviving plies).
354 pub fn laminate_stiffness_factor(&self) -> f64 {
355 if self.n_plies == 0 {
356 return 0.0;
357 }
358 self.ply_damage.iter().map(|&d| 1.0 - d).sum::<f64>() / self.n_plies as f64
359 }
360}
361/// Damage-induced anisotropy model.
362///
363/// Maintains separate damage variables for the three principal directions
364/// (d11, d22, d33) and the three shear planes (d12, d13, d23).
365/// The effective compliance tensor accounts for directional degradation.
366#[allow(dead_code)]
367#[derive(Debug, Clone)]
368pub struct DamageInducedAnisotropy {
369 /// Undamaged Young's modulus \[Pa\].
370 pub e0: f64,
371 /// Undamaged Poisson's ratio.
372 pub nu0: f64,
373 /// Normal damage in 1-direction.
374 pub d11: f64,
375 /// Normal damage in 2-direction.
376 pub d22: f64,
377 /// Normal damage in 3-direction.
378 pub d33: f64,
379 /// Shear damage in 1-2 plane.
380 pub d12: f64,
381 /// Shear damage in 1-3 plane.
382 pub d13: f64,
383 /// Shear damage in 2-3 plane.
384 pub d23: f64,
385}
386impl DamageInducedAnisotropy {
387 /// Create a new anisotropic damage model (initially undamaged).
388 pub fn new(e0: f64, nu0: f64) -> Self {
389 Self {
390 e0,
391 nu0,
392 d11: 0.0,
393 d22: 0.0,
394 d33: 0.0,
395 d12: 0.0,
396 d13: 0.0,
397 d23: 0.0,
398 }
399 }
400 /// Update shear damage variables (irreversible).
401 pub fn update_shear_damage(&mut self, d12: f64, d13: f64, d23: f64) {
402 self.d12 = self.d12.max(d12).clamp(0.0, 1.0 - f64::EPSILON);
403 self.d13 = self.d13.max(d13).clamp(0.0, 1.0 - f64::EPSILON);
404 self.d23 = self.d23.max(d23).clamp(0.0, 1.0 - f64::EPSILON);
405 }
406 /// Update normal damage variables (irreversible).
407 pub fn update_normal_damage(&mut self, d11: f64, d22: f64, d33: f64) {
408 self.d11 = self.d11.max(d11).clamp(0.0, 1.0 - f64::EPSILON);
409 self.d22 = self.d22.max(d22).clamp(0.0, 1.0 - f64::EPSILON);
410 self.d33 = self.d33.max(d33).clamp(0.0, 1.0 - f64::EPSILON);
411 }
412 /// Effective Young's modulus in direction 1: E1_eff = (1-D11)*E0.
413 pub fn e1_eff(&self) -> f64 {
414 (1.0 - self.d11) * self.e0
415 }
416 /// Effective Young's modulus in direction 2: E2_eff = (1-D22)*E0.
417 pub fn e2_eff(&self) -> f64 {
418 (1.0 - self.d22) * self.e0
419 }
420 /// Effective shear modulus G12_eff = (1-D12)*G0.
421 pub fn effective_shear_modulus_12(&self) -> f64 {
422 let g0 = self.e0 / (2.0 * (1.0 + self.nu0));
423 (1.0 - self.d12) * g0
424 }
425 /// Scalar damage intensity (Frobenius norm of damage tensor components).
426 pub fn damage_intensity(&self) -> f64 {
427 (self.d11.powi(2)
428 + self.d22.powi(2)
429 + self.d33.powi(2)
430 + 2.0 * self.d12.powi(2)
431 + 2.0 * self.d13.powi(2)
432 + 2.0 * self.d23.powi(2))
433 .sqrt()
434 }
435}
436/// Damage evolution law types for general CDM models.
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub enum DamageEvolutionLaw {
439 /// Linear softening: D grows linearly with equivalent strain.
440 Linear,
441 /// Exponential softening: D = 1 - exp(-a*(ε-ε0)).
442 Exponential,
443 /// Power-law softening: D = ((ε-ε0)/(εf-ε0))^n.
444 PowerLaw,
445}
446/// Mazars concrete damage model (biaxial damage).
447///
448/// Uses positive principal strains to distinguish tension/compression.
449#[derive(Debug, Clone)]
450pub struct MazarsDamage {
451 /// Current damage variable D ∈ \[0, 1\]
452 pub d: f64,
453 /// Damage threshold (strain) k
454 pub k: f64,
455 /// Tension parameter A_t
456 pub at: f64,
457 /// Tension parameter B_t
458 pub bt: f64,
459 /// Compression parameter A_c
460 pub ac: f64,
461 /// Compression parameter B_c
462 pub bc: f64,
463}
464impl MazarsDamage {
465 /// Create a new Mazars damage model.
466 ///
467 /// # Arguments
468 /// * `k0` - Initial damage threshold (strain)
469 /// * `at`, `bt` - Tension damage parameters
470 /// * `ac`, `bc` - Compression damage parameters
471 pub fn new(k0: f64, at: f64, bt: f64, ac: f64, bc: f64) -> Self {
472 Self {
473 d: 0.0,
474 k: k0,
475 at,
476 bt,
477 ac,
478 bc,
479 }
480 }
481 /// Equivalent strain from positive principal strains only.
482 ///
483 /// ε̃ = sqrt(Σ <ε_i>+²) where `x`+ = max(x, 0)
484 pub fn equivalent_strain(eps_principal: &[f64; 3]) -> f64 {
485 let sum_sq: f64 = eps_principal
486 .iter()
487 .map(|&e| {
488 let ep = e.max(0.0);
489 ep * ep
490 })
491 .sum();
492 sum_sq.sqrt()
493 }
494 /// Update damage based on principal strains.
495 ///
496 /// D = at*(1 - k/ε̃)*exp(-bt*(ε̃-k)) + ac*(1 - k/ε̃)*exp(-bc*(ε̃-k))
497 pub fn update(&mut self, eps_principal: &[f64; 3]) {
498 let eps_tilde = Self::equivalent_strain(eps_principal);
499 if eps_tilde > self.k {
500 let k = self.k;
501 let factor = 1.0 - k / eps_tilde;
502 let delta = eps_tilde - k;
503 let d_new = self.at * factor * (-self.bt * delta).exp()
504 + self.ac * factor * (-self.bc * delta).exp();
505 let d_new = d_new.clamp(self.d, 1.0);
506 self.d = d_new;
507 }
508 }
509}
510/// Tsai-Wu polynomial failure criterion for composite laminates.
511///
512/// Combined criterion:
513/// F₁σ₁ + F₂σ₂ + F₁₁σ₁² + F₂₂σ₂² + F₆₆τ₁₂² + 2F₁₂σ₁σ₂ = 1 (failure)
514///
515/// Reference: Tsai & Wu (1971).
516#[allow(dead_code)]
517#[derive(Debug, Clone)]
518pub struct TsaiWuCriterion {
519 /// Longitudinal tensile strength X_T \[Pa\].
520 pub x_t: f64,
521 /// Longitudinal compressive strength X_C \[Pa\].
522 pub x_c: f64,
523 /// Transverse tensile strength Y_T \[Pa\].
524 pub y_t: f64,
525 /// Transverse compressive strength Y_C \[Pa\].
526 pub y_c: f64,
527 /// Shear strength S_12 \[Pa\].
528 pub s12: f64,
529 /// Interaction coefficient F₁₂* (typically -0.5).
530 pub f12_star: f64,
531}
532impl TsaiWuCriterion {
533 /// Create a new Tsai-Wu criterion.
534 #[allow(clippy::too_many_arguments)]
535 pub fn new(x_t: f64, x_c: f64, y_t: f64, y_c: f64, s12: f64, f12_star: f64) -> Self {
536 Self {
537 x_t,
538 x_c,
539 y_t,
540 y_c,
541 s12,
542 f12_star,
543 }
544 }
545 /// Compute the Tsai-Wu failure index.
546 ///
547 /// Returns value < 1 → safe, ≥ 1 → failure.
548 pub fn failure_index(&self, sigma1: f64, sigma2: f64, tau12: f64) -> f64 {
549 let f1 = 1.0 / self.x_t - 1.0 / self.x_c;
550 let f2 = 1.0 / self.y_t - 1.0 / self.y_c;
551 let f11 = 1.0 / (self.x_t * self.x_c);
552 let f22 = 1.0 / (self.y_t * self.y_c);
553 let f66 = 1.0 / (self.s12 * self.s12);
554 let f12 = self.f12_star / (self.x_t * self.x_c * self.y_t * self.y_c).sqrt();
555 f1 * sigma1
556 + f2 * sigma2
557 + f11 * sigma1 * sigma1
558 + f22 * sigma2 * sigma2
559 + f66 * tau12 * tau12
560 + 2.0 * f12 * sigma1 * sigma2
561 }
562 /// Strength ratio R: actual load can be multiplied by R to reach failure.
563 ///
564 /// Solves quadratic: A·R² + B·R - 1 = 0
565 pub fn strength_ratio(&self, sigma1: f64, sigma2: f64, tau12: f64) -> f64 {
566 let f1 = 1.0 / self.x_t - 1.0 / self.x_c;
567 let f2 = 1.0 / self.y_t - 1.0 / self.y_c;
568 let f11 = 1.0 / (self.x_t * self.x_c);
569 let f22 = 1.0 / (self.y_t * self.y_c);
570 let f66 = 1.0 / (self.s12 * self.s12);
571 let f12 = self.f12_star / (self.x_t * self.x_c * self.y_t * self.y_c).sqrt();
572 let a_coef = f11 * sigma1 * sigma1
573 + f22 * sigma2 * sigma2
574 + f66 * tau12 * tau12
575 + 2.0 * f12 * sigma1 * sigma2;
576 let b_coef = f1 * sigma1 + f2 * sigma2;
577 if a_coef.abs() < f64::EPSILON {
578 if b_coef.abs() < f64::EPSILON {
579 return f64::INFINITY;
580 }
581 return 1.0 / b_coef;
582 }
583 let discriminant = b_coef * b_coef + 4.0 * a_coef;
584 if discriminant < 0.0 {
585 return f64::INFINITY;
586 }
587 (-b_coef + discriminant.sqrt()) / (2.0 * a_coef)
588 }
589}
590/// Repair effectiveness model for damaged composite structures.
591///
592/// After repair, the material recovers some fraction of its original strength
593/// depending on the repair method quality and residual damage level.
594///
595/// Repair effectiveness η ∈ \[0, 1\]:
596/// - η = 1: perfect repair (full strength recovery)
597/// - η = 0: no repair (residual damage remains)
598#[allow(dead_code)]
599#[derive(Debug, Clone)]
600pub struct RepairEffectiveness {
601 /// Repair quality factor κ ∈ \[0, 1\] (1 = perfect repair quality).
602 pub repair_factor: f64,
603 /// Residual damage after repair d_res ∈ \[0, 1).
604 pub residual_damage: f64,
605}
606impl RepairEffectiveness {
607 /// Create a new repair effectiveness model.
608 ///
609 /// # Arguments
610 /// * `repair_factor` - Quality of repair κ ∈ \[0, 1\]
611 /// * `residual_damage` - Remaining damage after repair d_res
612 pub fn new(repair_factor: f64, residual_damage: f64) -> Self {
613 Self {
614 repair_factor: repair_factor.clamp(0.0, 1.0),
615 residual_damage: residual_damage.clamp(0.0, 1.0 - f64::EPSILON),
616 }
617 }
618 /// Repair effectiveness η = κ · (1 - d_res).
619 pub fn effectiveness(&self) -> f64 {
620 self.repair_factor * (1.0 - self.residual_damage)
621 }
622 /// Recovered strength after repair.
623 ///
624 /// σ_rep = σ_0 · η
625 pub fn strength_recovery(&self, original_strength: f64) -> f64 {
626 original_strength * self.effectiveness()
627 }
628 /// Post-repair stiffness factor relative to undamaged.
629 ///
630 /// E_rep / E_0 = 1 - d_res · (1 - κ)
631 pub fn stiffness_recovery_factor(&self) -> f64 {
632 1.0 - self.residual_damage * (1.0 - self.repair_factor)
633 }
634 /// Check if the repair meets a minimum effectiveness threshold.
635 pub fn is_acceptable(&self, min_effectiveness: f64) -> bool {
636 self.effectiveness() >= min_effectiveness
637 }
638}
639/// Lemaitre ductile damage model with plastic strain coupling.
640///
641/// Ḋ = (Y/S)^s * ε̇_p for ε_p > ε_D
642///
643/// where Y is the energy release rate, S and s are material parameters,
644/// and ε_D is the damage threshold plastic strain.
645#[derive(Debug, Clone)]
646pub struct LemaitreDuctileDamage {
647 /// Current damage variable D ∈ \[0, 1\].
648 pub d: f64,
649 /// Accumulated plastic strain.
650 pub eps_p: f64,
651 /// Damage threshold plastic strain ε_D.
652 pub eps_d: f64,
653 /// Damage strength parameter S (Pa).
654 pub s_param: f64,
655 /// Damage exponent s.
656 pub s_exp: f64,
657 /// Critical damage D_c at which rupture occurs.
658 pub d_critical: f64,
659 /// Poisson's ratio ν (for triaxiality function).
660 pub nu: f64,
661}
662impl LemaitreDuctileDamage {
663 /// Create a new Lemaitre ductile damage model.
664 #[allow(clippy::too_many_arguments)]
665 pub fn new(eps_d: f64, s_param: f64, s_exp: f64, d_critical: f64, nu: f64) -> Self {
666 Self {
667 d: 0.0,
668 eps_p: 0.0,
669 eps_d,
670 s_param,
671 s_exp,
672 d_critical,
673 nu,
674 }
675 }
676 /// Stress triaxiality function R_ν.
677 ///
678 /// R_ν = (2/3)(1 + ν) + 3(1 - 2ν)(σ_H/σ_eq)²
679 ///
680 /// where σ_H/σ_eq is the triaxiality ratio.
681 #[allow(dead_code)]
682 pub fn triaxiality_function(&self, triaxiality: f64) -> f64 {
683 (2.0 / 3.0) * (1.0 + self.nu) + 3.0 * (1.0 - 2.0 * self.nu) * triaxiality * triaxiality
684 }
685 /// Energy release rate: Y = σ_eq² * R_ν / (2*E*(1-D)²)
686 #[allow(dead_code)]
687 pub fn energy_release_rate(&self, sigma_eq: f64, triaxiality: f64, young_modulus: f64) -> f64 {
688 let rv = self.triaxiality_function(triaxiality);
689 let w = 1.0 - self.d;
690 sigma_eq * sigma_eq * rv / (2.0 * young_modulus * w * w)
691 }
692 /// Update damage given an increment of plastic strain.
693 ///
694 /// Ḋ = (Y/S)^s * Δε_p if ε_p > ε_D and D < D_c
695 #[allow(dead_code)]
696 pub fn update(
697 &mut self,
698 sigma_eq: f64,
699 triaxiality: f64,
700 young_modulus: f64,
701 delta_eps_p: f64,
702 ) {
703 self.eps_p += delta_eps_p;
704 if self.eps_p <= self.eps_d || self.d >= self.d_critical {
705 return;
706 }
707 let y = self.energy_release_rate(sigma_eq, triaxiality, young_modulus);
708 let dd = (y / self.s_param).powf(self.s_exp) * delta_eps_p;
709 self.d = (self.d + dd).min(self.d_critical);
710 }
711 /// Check if rupture has occurred (D >= D_c).
712 #[allow(dead_code)]
713 pub fn is_ruptured(&self) -> bool {
714 self.d >= self.d_critical - 1e-15
715 }
716 /// Compute the stress triaxiality correction factor R_ν.
717 ///
718 /// The triaxiality correction amplifies the damage driving force under
719 /// multiaxial stress states:
720 ///
721 /// R_ν = (2/3)(1 + ν) + 3(1 - 2ν) η²
722 ///
723 /// where η = σ_H / σ_eq is the stress triaxiality ratio.
724 /// This factor accounts for the accelerated ductile fracture at high
725 /// triaxiality (e.g., notch root, crack front).
726 ///
727 /// # Arguments
728 /// * `triaxiality` - η = σ_hydrostatic / σ_von_Mises (dimensionless)
729 ///
730 /// # Returns
731 /// Triaxiality correction factor R_ν ≥ (2/3)(1+ν)
732 pub fn compute_triaxiality_correction(&self, triaxiality: f64) -> f64 {
733 (2.0 / 3.0) * (1.0 + self.nu) + 3.0 * (1.0 - 2.0 * self.nu) * triaxiality * triaxiality
734 }
735}
736/// Progressive stiffness degradation model for composite materials.
737///
738/// Tracks separate damage variables D11 (fiber direction) and D22
739/// (transverse direction). Effective stiffness:
740///
741/// E11_eff = (1 - D11) * E11, E22_eff = (1 - D22) * E22
742#[allow(dead_code)]
743#[derive(Debug, Clone)]
744pub struct ProgressiveDamage {
745 /// Undamaged Young's modulus (isotropic assumption) \[Pa\].
746 pub e0: f64,
747 /// Poisson's ratio.
748 pub nu: f64,
749 /// Fiber-direction damage variable D11 ∈ \[0, 1).
750 pub d11: f64,
751 /// Transverse-direction damage variable D22 ∈ \[0, 1).
752 pub d22: f64,
753}
754impl ProgressiveDamage {
755 /// Create a new progressive damage model.
756 pub fn new(e0: f64, nu: f64, d11: f64, d22: f64) -> Self {
757 Self {
758 e0,
759 nu,
760 d11: d11.clamp(0.0, 1.0 - f64::EPSILON),
761 d22: d22.clamp(0.0, 1.0 - f64::EPSILON),
762 }
763 }
764 /// Effective fiber-direction stiffness.
765 pub fn e11_eff(&self) -> f64 {
766 (1.0 - self.d11) * self.e0
767 }
768 /// Effective transverse stiffness.
769 pub fn e22_eff(&self) -> f64 {
770 (1.0 - self.d22) * self.e0
771 }
772 /// 3×3 effective stiffness tensor (plane stress, Voigt notation: 11, 22, 12).
773 ///
774 /// Returns `[[C11, C12, 0], [C12, C22, 0], [0, 0, C66]]`.
775 pub fn effective_stiffness_tensor(&self) -> [[f64; 3]; 3] {
776 let e1 = self.e11_eff();
777 let e2 = self.e22_eff();
778 let nu12 = self.nu;
779 let nu21 = nu12 * e2 / e1;
780 let denom = 1.0 - nu12 * nu21;
781 let c11 = e1 / denom;
782 let c22 = e2 / denom;
783 let c12 = nu12 * e2 / denom;
784 let g0 = self.e0 / (2.0 * (1.0 + self.nu));
785 let d_shear = 1.0 - (1.0 - self.d11) * (1.0 - self.d22);
786 let c66 = (1.0 - d_shear) * g0;
787 [[c11, c12, 0.0], [c12, c22, 0.0], [0.0, 0.0, c66]]
788 }
789 /// Update damage variables (irreversible – only increases).
790 pub fn update_damage(&mut self, d11_new: f64, d22_new: f64) {
791 self.d11 = self.d11.max(d11_new).clamp(0.0, 1.0 - f64::EPSILON);
792 self.d22 = self.d22.max(d22_new).clamp(0.0, 1.0 - f64::EPSILON);
793 }
794 /// Residual strength in fiber direction after damage.
795 ///
796 /// σ_res = σ_0 · (1 - D11)^alpha_f · (1 - D22)^alpha_m
797 pub fn residual_strength(&self, sigma0: f64, _alpha_m: f64) -> f64 {
798 sigma0 * (1.0 - self.d11)
799 }
800}
801/// Gradient-enhanced damage regularization.
802///
803/// Implicit gradient formulation:
804/// ε̄ - l_c² ∇²ε̄ = ε
805///
806/// This is a simplified 1D discretization for a single bar element.
807#[derive(Debug, Clone)]
808pub struct GradientEnhancedDamage {
809 /// Characteristic length squared c = l_c².
810 pub c: f64,
811}
812impl GradientEnhancedDamage {
813 /// Create gradient-enhanced regularization from characteristic length l_c.
814 pub fn new(lc: f64) -> Self {
815 Self { c: lc * lc }
816 }
817 /// Solve the implicit gradient equation in 1D for a uniform bar.
818 ///
819 /// Discretized with finite differences:
820 /// ε̄_i - c*(ε̄_{i-1} - 2*ε̄_i + ε̄_{i+1})/h² = ε_i
821 ///
822 /// Uses Thomas algorithm (tridiagonal solver).
823 #[allow(dead_code)]
824 pub fn solve_1d(&self, local_strains: &[f64], element_size: f64) -> Vec<f64> {
825 let n = local_strains.len();
826 if n == 0 {
827 return vec![];
828 }
829 if n == 1 {
830 return vec![local_strains[0]];
831 }
832 let h2 = element_size * element_size;
833 let alpha = self.c / h2;
834 let mut a_coeff = vec![0.0; n];
835 let mut b_coeff = vec![0.0; n];
836 let mut c_coeff = vec![0.0; n];
837 let mut d_vec = vec![0.0; n];
838 for i in 0..n {
839 b_coeff[i] = 1.0 + 2.0 * alpha;
840 d_vec[i] = local_strains[i];
841 if i > 0 {
842 a_coeff[i] = -alpha;
843 }
844 if i < n - 1 {
845 c_coeff[i] = -alpha;
846 }
847 }
848 b_coeff[0] = 1.0 + alpha;
849 b_coeff[n - 1] = 1.0 + alpha;
850 let mut cp = vec![0.0; n];
851 let mut dp = vec![0.0; n];
852 cp[0] = c_coeff[0] / b_coeff[0];
853 dp[0] = d_vec[0] / b_coeff[0];
854 for i in 1..n {
855 let m = b_coeff[i] - a_coeff[i] * cp[i - 1];
856 cp[i] = c_coeff[i] / m;
857 dp[i] = (d_vec[i] - a_coeff[i] * dp[i - 1]) / m;
858 }
859 let mut result = vec![0.0; n];
860 result[n - 1] = dp[n - 1];
861 for i in (0..n - 1).rev() {
862 result[i] = dp[i] - cp[i] * result[i + 1];
863 }
864 result
865 }
866}
867/// Hashin failure criteria for unidirectional fiber composites.
868///
869/// Four independent failure modes:
870/// 1. Fiber tension (σ_1 > 0)
871/// 2. Fiber compression (σ_1 < 0)
872/// 3. Matrix tension (σ_2 > 0)
873/// 4. Matrix compression (σ_2 < 0)
874///
875/// Reference: Hashin (1980).
876#[allow(dead_code)]
877#[derive(Debug, Clone)]
878pub struct HashinFailureCriteria {
879 /// Longitudinal tensile strength X_T \[Pa\].
880 pub x_t: f64,
881 /// Longitudinal compressive strength X_C \[Pa\].
882 pub x_c: f64,
883 /// Transverse tensile strength Y_T \[Pa\].
884 pub y_t: f64,
885 /// Transverse compressive strength Y_C \[Pa\].
886 pub y_c: f64,
887 /// In-plane shear strength S_12 \[Pa\].
888 pub s12: f64,
889}
890impl HashinFailureCriteria {
891 /// Create a new Hashin failure criteria instance.
892 pub fn new(x_t: f64, x_c: f64, y_t: f64, y_c: f64, s12: f64) -> Self {
893 Self {
894 x_t,
895 x_c,
896 y_t,
897 y_c,
898 s12,
899 }
900 }
901 /// Fiber tension criterion: f_ft = (σ_1/X_T)^2 + (τ_12/S_12)^2.
902 pub fn fiber_tension(&self, sigma1: f64, tau12: f64, _tau13: f64) -> f64 {
903 if sigma1 <= 0.0 {
904 return 0.0;
905 }
906 (sigma1 / self.x_t).powi(2) + (tau12 / self.s12).powi(2)
907 }
908 /// Fiber compression criterion: f_fc = (σ_1/X_C)^2.
909 pub fn fiber_compression(&self, sigma1: f64) -> f64 {
910 if sigma1 >= 0.0 {
911 return 0.0;
912 }
913 (sigma1 / self.x_c).powi(2)
914 }
915 /// Matrix tension criterion: f_mt = (σ_2/Y_T)^2 + (τ_12/S_12)^2.
916 pub fn matrix_tension(&self, sigma2: f64, tau12: f64) -> f64 {
917 if sigma2 <= 0.0 {
918 return 0.0;
919 }
920 (sigma2 / self.y_t).powi(2) + (tau12 / self.s12).powi(2)
921 }
922 /// Matrix compression criterion (Hashin 1980):
923 ///
924 /// f_mc = (σ_2 / (2·S_23))^2 + \[(Y_C / (2·S_23))^2 - 1\]·(σ_2/Y_C) + (τ_12/S_12)^2
925 pub fn matrix_compression(&self, sigma2: f64, tau12: f64) -> f64 {
926 if sigma2 >= 0.0 {
927 return 0.0;
928 }
929 let s23 = self.y_c / 2.0;
930 let t1 = (sigma2 / (2.0 * s23)).powi(2);
931 let t2 = ((self.y_c / (2.0 * s23)).powi(2) - 1.0) * (sigma2 / self.y_c);
932 let t3 = (tau12 / self.s12).powi(2);
933 t1 + t2 + t3
934 }
935 /// Check if any failure mode is triggered (criterion ≥ 1).
936 pub fn is_failed(&self, sigma1: f64, sigma2: f64, tau12: f64) -> bool {
937 self.fiber_tension(sigma1, tau12, 0.0) >= 1.0
938 || self.fiber_compression(sigma1) >= 1.0
939 || self.matrix_tension(sigma2, tau12) >= 1.0
940 || self.matrix_compression(sigma2, tau12) >= 1.0
941 }
942}
943/// Norton power-law creep model.
944///
945/// ε̇_cr = A * |σ|^n * sign(σ) * exp(-Q/(R*T))
946#[derive(Debug, Clone)]
947pub struct NortonCreep {
948 /// Pre-exponential factor A
949 pub a: f64,
950 /// Stress exponent n
951 pub n: f64,
952 /// Activation energy Q (J/mol)
953 pub q: f64,
954 /// Gas constant R (J/(mol·K))
955 pub r: f64,
956}
957impl NortonCreep {
958 /// Create a new Norton creep model with R = 8.314 J/(mol·K).
959 ///
960 /// # Arguments
961 /// * `a` - Pre-exponential factor
962 /// * `n` - Stress exponent
963 /// * `q` - Activation energy (J/mol)
964 pub fn new(a: f64, n: f64, q: f64) -> Self {
965 Self { a, n, q, r: 8.314 }
966 }
967 /// Creep strain rate: ε̇_cr = A * |σ|^n * sign(σ) * exp(-Q/(R*T))
968 pub fn creep_rate(&self, sigma: f64, temperature: f64) -> f64 {
969 let sign = sigma.signum();
970 let abs_sigma = sigma.abs();
971 self.a * abs_sigma.powf(self.n) * sign * (-self.q / (self.r * temperature)).exp()
972 }
973 /// Creep strain increment in time dt.
974 pub fn creep_increment(&self, sigma: f64, temperature: f64, dt: f64) -> f64 {
975 self.creep_rate(sigma, temperature) * dt
976 }
977}
978/// Puck failure criteria for unidirectional composites.
979///
980/// Separates failure into:
981/// - Mode A: fiber failure (tension/compression)
982/// - Mode B/C: inter-fiber failure (IFF)
983///
984/// Reference: Puck & Schürmann (1998).
985#[allow(dead_code)]
986#[derive(Debug, Clone)]
987pub struct PuckFailureCriteria {
988 /// Longitudinal tensile strength X_T \[Pa\].
989 pub x_t: f64,
990 /// Longitudinal compressive strength X_C \[Pa\].
991 pub x_c: f64,
992 /// Transverse tensile strength Y_T \[Pa\].
993 pub y_t: f64,
994 /// Transverse compressive strength Y_C \[Pa\].
995 pub y_c: f64,
996 /// In-plane shear strength S_12 \[Pa\].
997 pub s12: f64,
998}
999impl PuckFailureCriteria {
1000 /// Create a new Puck failure criteria instance.
1001 #[allow(clippy::too_many_arguments)]
1002 pub fn new(x_t: f64, x_c: f64, y_t: f64, y_c: f64, s12: f64) -> Self {
1003 Self {
1004 x_t,
1005 x_c,
1006 y_t,
1007 y_c,
1008 s12,
1009 }
1010 }
1011 /// Fiber failure criterion under longitudinal tension.
1012 ///
1013 /// f_FF = (σ_1 + ν_f12·σ_2) / (E_f1 * ε_1f)
1014 /// Simplified: f_FF = σ_1 / X_T
1015 pub fn fiber_failure_tension(&self, sigma1: f64, _e_f1: f64, sigma2: f64, _e_f2: f64) -> f64 {
1016 if sigma1 <= 0.0 {
1017 return 0.0;
1018 }
1019 let correction = 1.0 + sigma2 / self.y_t * 0.01;
1020 (sigma1 / self.x_t) * correction
1021 }
1022 /// Fiber failure criterion under longitudinal compression.
1023 pub fn fiber_failure_compression(&self, sigma1: f64) -> f64 {
1024 if sigma1 >= 0.0 {
1025 return 0.0;
1026 }
1027 (-sigma1) / self.x_c
1028 }
1029 /// Inter-fiber failure Mode A (transverse tension + shear).
1030 ///
1031 /// f_IFF = sqrt((τ_12/S_12)^2 + (1 - p_tt * Y_T / S_12)^2 * (σ_2 / Y_T)^2)
1032 /// + p_tt * σ_2 / S_12
1033 ///
1034 /// where p_tt is the slope parameter (typically 0.2..0.3).
1035 pub fn inter_fiber_failure_mode_a(
1036 &self,
1037 sigma2: f64,
1038 tau12: f64,
1039 p_tt: f64,
1040 _p_ct: f64,
1041 ) -> f64 {
1042 if sigma2 < 0.0 {
1043 return 0.0;
1044 }
1045 let term_tau = (tau12 / self.s12).powi(2);
1046 let factor = 1.0 - p_tt * self.y_t / self.s12;
1047 let term_sig = (factor * sigma2 / self.y_t).powi(2);
1048 (term_tau + term_sig).sqrt() + p_tt * sigma2 / self.s12
1049 }
1050 /// Inter-fiber failure Mode B/C (transverse compression + shear).
1051 pub fn inter_fiber_failure_mode_bc(&self, sigma2: f64, tau12: f64, p_ct: f64) -> f64 {
1052 if sigma2 >= 0.0 {
1053 return 0.0;
1054 }
1055 let term_tau = (tau12 / self.s12 + p_ct * sigma2 / self.s12).powi(2);
1056 let term_sig = (sigma2 / self.y_c).powi(2);
1057 (term_tau + term_sig).sqrt()
1058 }
1059 /// Maximum failure index across all modes.
1060 pub fn max_failure_index(
1061 &self,
1062 sigma1: f64,
1063 sigma2: f64,
1064 tau12: f64,
1065 e_f1: f64,
1066 e_f2: f64,
1067 ) -> f64 {
1068 let ff_t = self.fiber_failure_tension(sigma1, e_f1, sigma2, e_f2);
1069 let ff_c = self.fiber_failure_compression(sigma1);
1070 let iff_a = self.inter_fiber_failure_mode_a(sigma2, tau12, 0.25, 0.2);
1071 let iff_bc = self.inter_fiber_failure_mode_bc(sigma2, tau12, 0.2);
1072 ff_t.max(ff_c).max(iff_a).max(iff_bc)
1073 }
1074}
1075/// Crack band model for mesh-objective softening (Bažant & Oh 1983).
1076///
1077/// Regularizes strain-softening by relating the softening curve to fracture
1078/// energy G_f and the element characteristic length h. This ensures mesh
1079/// independence of the energy dissipation.
1080#[derive(Debug, Clone)]
1081pub struct CrackBandModel {
1082 /// Fracture energy G_f (J/m² or N/m).
1083 pub gf: f64,
1084 /// Tensile strength f_t (Pa).
1085 pub ft: f64,
1086 /// Young's modulus E (Pa).
1087 pub young_modulus: f64,
1088 /// Characteristic element length h (m).
1089 pub h: f64,
1090 /// Current damage variable.
1091 pub d: f64,
1092 /// Maximum historical strain (for irreversibility).
1093 pub kappa: f64,
1094}
1095impl CrackBandModel {
1096 /// Create a new crack band model.
1097 ///
1098 /// The failure strain is computed as ε_f = 2*G_f / (f_t * h).
1099 pub fn new(gf: f64, ft: f64, young_modulus: f64, h: f64) -> Self {
1100 Self {
1101 gf,
1102 ft,
1103 young_modulus,
1104 h,
1105 d: 0.0,
1106 kappa: 0.0,
1107 }
1108 }
1109 /// Damage initiation strain: ε_0 = f_t / E.
1110 #[allow(dead_code)]
1111 pub fn initiation_strain(&self) -> f64 {
1112 self.ft / self.young_modulus
1113 }
1114 /// Failure strain: ε_f = 2*G_f / (f_t * h).
1115 ///
1116 /// This ensures the correct fracture energy is dissipated
1117 /// regardless of element size.
1118 #[allow(dead_code)]
1119 pub fn failure_strain(&self) -> f64 {
1120 2.0 * self.gf / (self.ft * self.h)
1121 }
1122 /// Check snap-back condition: ε_f must be > ε_0.
1123 ///
1124 /// If h > 2*E*G_f / f_t², snap-back occurs and the element is too large.
1125 #[allow(dead_code)]
1126 pub fn has_snapback(&self) -> bool {
1127 self.failure_strain() <= self.initiation_strain()
1128 }
1129 /// Maximum allowable element size to avoid snap-back.
1130 #[allow(dead_code)]
1131 pub fn max_element_size(&self) -> f64 {
1132 2.0 * self.young_modulus * self.gf / (self.ft * self.ft)
1133 }
1134 /// Update damage based on current strain.
1135 ///
1136 /// Uses linear softening between ε_0 and ε_f.
1137 #[allow(dead_code)]
1138 pub fn update(&mut self, strain: f64) {
1139 if strain <= self.kappa {
1140 return;
1141 }
1142 self.kappa = strain;
1143 let eps0 = self.initiation_strain();
1144 let eps_f = self.failure_strain();
1145 if strain <= eps0 {
1146 return;
1147 }
1148 let d_new = if strain >= eps_f {
1149 1.0
1150 } else {
1151 eps_f / strain * (strain - eps0) / (eps_f - eps0)
1152 };
1153 self.d = d_new.clamp(self.d, 1.0);
1154 }
1155 /// Stress for a given strain considering damage.
1156 ///
1157 /// σ = (1 - D) * E * ε
1158 #[allow(dead_code)]
1159 pub fn stress(&self, strain: f64) -> f64 {
1160 (1.0 - self.d) * self.young_modulus * strain
1161 }
1162 /// Energy dissipated per unit volume so far.
1163 ///
1164 /// For linear softening: g = 0.5 * f_t * (ε_f - ε_0) * D
1165 /// Total energy dissipated per unit area = g * h ≈ G_f when D=1.
1166 #[allow(dead_code)]
1167 pub fn dissipated_energy_density(&self) -> f64 {
1168 let eps0 = self.initiation_strain();
1169 let eps_f = self.failure_strain();
1170 0.5 * self.ft * (eps_f - eps0) * self.d
1171 }
1172}
1173/// Gurson-Tvergaard-Needleman (GTN) porous plasticity model.
1174///
1175/// Yield function:
1176/// Φ = (σ_eq/σ_y)² + 2*q₁*f* cosh(3*q₂*σ_H/(2*σ_y)) - 1 - (q₁*f*)² = 0
1177///
1178/// where f* is the effective void volume fraction.
1179#[derive(Debug, Clone)]
1180pub struct GursonDamage {
1181 /// Current void volume fraction f ∈ \[0, 1\].
1182 pub f: f64,
1183 /// Initial void volume fraction f_0.
1184 pub f0: f64,
1185 /// Critical void volume fraction f_c for coalescence.
1186 pub fc: f64,
1187 /// Failure void volume fraction f_F.
1188 pub f_f: f64,
1189 /// Tvergaard parameter q₁ (typically 1.5).
1190 pub q1: f64,
1191 /// Tvergaard parameter q₂ (typically 1.0).
1192 pub q2: f64,
1193 /// Tvergaard parameter q₃ (typically q₁²).
1194 pub q3: f64,
1195 /// Nucleation amplitude A_n.
1196 pub a_n: f64,
1197 /// Mean nucleation strain ε_N.
1198 pub eps_n: f64,
1199 /// Standard deviation of nucleation s_N.
1200 pub s_n: f64,
1201}
1202impl GursonDamage {
1203 /// Create a new GTN model with default Tvergaard parameters.
1204 #[allow(clippy::too_many_arguments)]
1205 pub fn new(f0: f64, fc: f64, f_f: f64, a_n: f64, eps_n: f64, s_n: f64) -> Self {
1206 let q1 = 1.5;
1207 let q2 = 1.0;
1208 Self {
1209 f: f0,
1210 f0,
1211 fc,
1212 f_f,
1213 q1,
1214 q2,
1215 q3: q1 * q1,
1216 a_n,
1217 eps_n,
1218 s_n,
1219 }
1220 }
1221 /// Effective void volume fraction f* accounting for coalescence.
1222 ///
1223 /// f* = f if f <= f_c
1224 /// f* = f_c + (f̄_F - f_c)/(f_F - f_c) * (f - f_c) if f > f_c
1225 ///
1226 /// where f̄_F = (q₁ + sqrt(q₁² - q₃))/q₃ ≈ 1/q₁
1227 #[allow(dead_code)]
1228 pub fn effective_void_fraction(&self) -> f64 {
1229 if self.f <= self.fc {
1230 self.f
1231 } else {
1232 let f_bar_f = 1.0 / self.q1;
1233 let kappa = (f_bar_f - self.fc) / (self.f_f - self.fc);
1234 self.fc + kappa * (self.f - self.fc)
1235 }
1236 }
1237 /// Evaluate the GTN yield function.
1238 ///
1239 /// Φ = (σ_eq/σ_y)² + 2*q₁*f* cosh(3*q₂*σ_H/(2*σ_y)) - 1 - q₃*f*²
1240 #[allow(dead_code)]
1241 pub fn yield_function(&self, sigma_eq: f64, sigma_h: f64, sigma_y: f64) -> f64 {
1242 let f_star = self.effective_void_fraction();
1243 let term1 = (sigma_eq / sigma_y).powi(2);
1244 let term2 = 2.0 * self.q1 * f_star * (1.5 * self.q2 * sigma_h / sigma_y).cosh();
1245 let term3 = 1.0 + self.q3 * f_star * f_star;
1246 term1 + term2 - term3
1247 }
1248 /// Void nucleation rate from strain-controlled nucleation (Chu & Needleman).
1249 ///
1250 /// ḟ_nucleation = A_n / (s_N * sqrt(2π)) * exp(-0.5*((ε_p - ε_N)/s_N)²) * ε̇_p
1251 #[allow(dead_code)]
1252 pub fn nucleation_rate(&self, eps_p: f64, eps_p_dot: f64) -> f64 {
1253 let z = (eps_p - self.eps_n) / self.s_n;
1254 let coeff = self.a_n / (self.s_n * (2.0 * std::f64::consts::PI).sqrt());
1255 coeff * (-0.5 * z * z).exp() * eps_p_dot
1256 }
1257 /// Void growth rate from plastic dilatation.
1258 ///
1259 /// ḟ_growth = (1 - f) * ε̇_kk^p
1260 #[allow(dead_code)]
1261 pub fn growth_rate(&self, plastic_volumetric_strain_rate: f64) -> f64 {
1262 (1.0 - self.f) * plastic_volumetric_strain_rate
1263 }
1264 /// Update void volume fraction given plastic strain increment.
1265 #[allow(dead_code)]
1266 pub fn update(
1267 &mut self,
1268 eps_p: f64,
1269 eps_p_dot: f64,
1270 plastic_volumetric_strain_rate: f64,
1271 dt: f64,
1272 ) {
1273 let f_nucleation = self.nucleation_rate(eps_p, eps_p_dot) * dt;
1274 let f_growth = self.growth_rate(plastic_volumetric_strain_rate) * dt;
1275 self.f = (self.f + f_nucleation + f_growth).clamp(0.0, self.f_f);
1276 }
1277 /// Check if material has failed (f >= f_F).
1278 #[allow(dead_code)]
1279 pub fn is_failed(&self) -> bool {
1280 self.f >= self.f_f - 1e-15
1281 }
1282 /// Compute the total void growth rate including nucleation and growth.
1283 ///
1284 /// The Rice-Tracey void growth model extended for GTN:
1285 ///
1286 /// ḟ_total = ḟ_growth + ḟ_nucleation
1287 ///
1288 /// where:
1289 /// - ḟ_growth = (1 - f) * A * sinh(B * η) * ε̇_p
1290 /// - ḟ_nucleation = Gaussian nucleation from Chu-Needleman model
1291 /// - η = σ_H / σ_eq (stress triaxiality)
1292 /// - A = 0.283 (Rice-Tracey coefficient), B = 1.5 (triaxiality amplifier)
1293 ///
1294 /// # Arguments
1295 /// * `sigma_h` - Hydrostatic (mean) stress \[Pa\]
1296 /// * `sigma_eq` - Von Mises equivalent stress \[Pa\]
1297 /// * `sigma_y` - Current yield stress \[Pa\]
1298 /// * `deps_p` - Equivalent plastic strain increment (dimensionless)
1299 ///
1300 /// # Returns
1301 /// Total void volume fraction rate df/dε (per unit plastic strain)
1302 #[allow(clippy::too_many_arguments)]
1303 pub fn compute_void_growth_rate(
1304 &self,
1305 sigma_h: f64,
1306 sigma_eq: f64,
1307 sigma_y: f64,
1308 deps_p: f64,
1309 ) -> f64 {
1310 let triaxiality = if sigma_eq.abs() > 1e-30 {
1311 sigma_h / sigma_eq
1312 } else {
1313 0.0
1314 };
1315 let a_rt = 0.283_f64;
1316 let b_rt = 1.5_f64;
1317 let growth = (1.0 - self.f) * a_rt * (b_rt * triaxiality).sinh() * deps_p;
1318 let eps_p_effective = if sigma_eq.abs() > 1e-30 {
1319 deps_p * sigma_y / sigma_eq
1320 } else {
1321 deps_p
1322 };
1323 let nucleation = self.nucleation_rate(eps_p_effective, 1.0) * deps_p;
1324 (growth + nucleation).max(0.0)
1325 }
1326}
1327/// Simple high-cycle fatigue damage accumulation (Miner's rule).
1328///
1329/// D = Σ nᵢ / Nᵢ
1330///
1331/// where nᵢ is the number of cycles at stress level i and Nᵢ is the
1332/// fatigue life at that stress level from the S-N curve.
1333#[derive(Debug, Clone)]
1334pub struct MinerFatigueDamage {
1335 /// Accumulated damage D.
1336 pub d: f64,
1337 /// S-N curve coefficient C (N = C / S^m).
1338 pub c: f64,
1339 /// S-N curve exponent m.
1340 pub m: f64,
1341}
1342impl MinerFatigueDamage {
1343 /// Create a new Miner fatigue damage model.
1344 pub fn new(c: f64, m: f64) -> Self {
1345 Self { d: 0.0, c, m }
1346 }
1347 /// Fatigue life N at stress amplitude S from the S-N curve.
1348 ///
1349 /// N = C / S^m
1350 #[allow(dead_code)]
1351 pub fn fatigue_life(&self, stress_amplitude: f64) -> f64 {
1352 if stress_amplitude.abs() < f64::EPSILON {
1353 return f64::INFINITY;
1354 }
1355 self.c / stress_amplitude.abs().powf(self.m)
1356 }
1357 /// Accumulate damage for n cycles at the given stress amplitude.
1358 #[allow(dead_code)]
1359 pub fn accumulate(&mut self, stress_amplitude: f64, n_cycles: f64) {
1360 let n_f = self.fatigue_life(stress_amplitude);
1361 if n_f.is_finite() {
1362 self.d = (self.d + n_cycles / n_f).min(1.0);
1363 }
1364 }
1365 /// Check if fatigue failure has occurred (D >= 1).
1366 #[allow(dead_code)]
1367 pub fn is_failed(&self) -> bool {
1368 self.d >= 1.0 - 1e-15
1369 }
1370}