Skip to main content

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}