Skip to main content

oxiphysics_materials/creep/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6
7/// Thermally activated rate model for creep.
8///
9/// Strain rate: eps_dot = nu_0 * exp(-ΔG / (k_B * T)) * sinh(V * sigma / (k_B * T))
10///
11/// where nu_0 is the attempt frequency, ΔG is the activation free energy,
12/// V is the activation volume, and k_B is Boltzmann's constant.
13pub struct ThermallyActivatedCreep {
14    /// Attempt frequency ν₀ (1/s).
15    pub nu0: f64,
16    /// Activation free energy ΔG (J).
17    pub delta_g: f64,
18    /// Activation volume V (m³).
19    pub activation_volume: f64,
20    /// Boltzmann constant k_B (J/K).
21    pub k_b: f64,
22}
23impl ThermallyActivatedCreep {
24    /// Create a thermally activated creep model.
25    pub fn new(nu0: f64, delta_g: f64, activation_volume: f64) -> Self {
26        Self {
27            nu0,
28            delta_g,
29            activation_volume,
30            k_b: 1.380_649e-23,
31        }
32    }
33    /// Strain rate at given stress and temperature.
34    pub fn strain_rate(&self, stress: f64, temperature: f64) -> f64 {
35        let kt = self.k_b * temperature;
36        let boltzmann = (-self.delta_g / kt).exp();
37        let sinh_term = (self.activation_volume * stress / kt).sinh();
38        self.nu0 * boltzmann * sinh_term
39    }
40    /// Activation stress: σ* = k_B * T / V * arcsinh(eps_dot / nu_0 / exp(-ΔG/k_BT))
41    pub fn activation_stress(&self, strain_rate: f64, temperature: f64) -> f64 {
42        let kt = self.k_b * temperature;
43        let boltzmann = (-self.delta_g / kt).exp();
44        let arg = strain_rate / (self.nu0 * boltzmann);
45        (kt / self.activation_volume) * arg.asinh()
46    }
47    /// Apparent activation energy at constant stress (from Arrhenius plot slope).
48    pub fn apparent_activation_energy(&self, stress: f64) -> f64 {
49        (self.delta_g - stress * self.activation_volume).max(0.0)
50    }
51}
52/// Monkman-Grant creep life relation.
53///
54/// Empirical rule: ε_ss^m * t_f = C_MG
55///
56/// where ε_ss is the steady-state creep rate, t_f is the rupture life,
57/// m is typically close to 1, and C_MG is a material constant.
58pub struct MonkmanGrant {
59    /// Monkman-Grant constant C_MG.
60    pub c_mg: f64,
61    /// Exponent m (typically ~1.0).
62    pub m_exp: f64,
63}
64impl MonkmanGrant {
65    /// Create a new Monkman-Grant model.
66    pub fn new(c_mg: f64, m_exp: f64) -> Self {
67        Self { c_mg, m_exp }
68    }
69    /// Predict rupture life from steady-state creep rate.
70    ///
71    /// t_f = C_MG / ε_ss^m
72    pub fn rupture_life(&self, steady_state_rate: f64) -> f64 {
73        if steady_state_rate <= 0.0 {
74            return f64::INFINITY;
75        }
76        self.c_mg / steady_state_rate.powf(self.m_exp)
77    }
78    /// Back-calculate steady-state rate from known rupture life.
79    ///
80    /// ε_ss = (C_MG / t_f)^(1/m)
81    pub fn steady_state_rate_from_life(&self, rupture_life: f64) -> f64 {
82        if rupture_life <= 0.0 {
83            return f64::INFINITY;
84        }
85        (self.c_mg / rupture_life).powf(1.0 / self.m_exp)
86    }
87    /// Check consistency: given ε_ss, compute t_f, then back-calculate ε_ss.
88    pub fn is_consistent(&self, steady_state_rate: f64, tolerance: f64) -> bool {
89        let t_f = self.rupture_life(steady_state_rate);
90        let rate_back = self.steady_state_rate_from_life(t_f);
91        (rate_back - steady_state_rate).abs() / steady_state_rate < tolerance
92    }
93}
94/// Chaboche nonlinear kinematic hardening (back-stress evolution).
95pub struct ChabocheKinematicHardening {
96    /// Kinematic hardening modulus C
97    pub c: f64,
98    /// Dynamic recovery coefficient gamma
99    pub gamma: f64,
100}
101impl ChabocheKinematicHardening {
102    /// Create a new ChabocheKinematicHardening model.
103    pub fn new(c: f64, gamma: f64) -> Self {
104        Self { c, gamma }
105    }
106    /// Rate of back stress evolution.
107    ///
108    /// dot(X) = C * eps_dot_p - gamma * X * |eps_dot_p|
109    pub fn back_stress_rate(&self, plastic_strain_rate: f64, back_stress: f64) -> f64 {
110        self.c * plastic_strain_rate - self.gamma * back_stress * plastic_strain_rate.abs()
111    }
112    /// Explicit Euler update of back stress over a plastic strain increment dep.
113    pub fn update_back_stress(&self, x: f64, dep: f64, dt: f64) -> f64 {
114        let rate = self.back_stress_rate(dep / dt.max(f64::EPSILON), x);
115        x + rate * dt
116    }
117    /// Saturation back stress: X_sat = C / gamma.
118    pub fn saturation_stress(&self) -> f64 {
119        if self.gamma.abs() > f64::EPSILON {
120            self.c / self.gamma
121        } else {
122            f64::INFINITY
123        }
124    }
125}
126/// Primary creep using Bailey-Norton time-hardening: ε_p = A * σ^n * t^m.
127#[allow(dead_code)]
128pub struct PrimaryCreepModel {
129    /// Constant A
130    pub a: f64,
131    /// Stress exponent n
132    pub n: f64,
133    /// Time exponent m (0 < m < 1 for primary hardening)
134    pub m: f64,
135}
136impl PrimaryCreepModel {
137    /// Create a primary creep model.
138    pub fn new(a: f64, n: f64, m: f64) -> Self {
139        Self { a, n, m }
140    }
141    /// Accumulated primary creep strain at stress `sigma` and time `t`.
142    pub fn strain(&self, sigma: f64, t: f64) -> f64 {
143        self.a * sigma.powf(self.n) * t.powf(self.m)
144    }
145    /// Primary creep rate: dε/dt = A * σ^n * m * t^(m-1).
146    pub fn strain_rate(&self, sigma: f64, t: f64) -> f64 {
147        if t <= 0.0 {
148            return f64::INFINITY;
149        }
150        self.a * sigma.powf(self.n) * self.m * t.powf(self.m - 1.0)
151    }
152    /// Time at which primary rate equals the steady-state (Norton) rate.
153    ///
154    /// Returns the transition time from primary to secondary creep.
155    pub fn transition_time(&self, norton: &NortonCreep, sigma: f64, temperature: f64) -> f64 {
156        let eps_dot_ss = norton.creep_strain_rate(sigma, temperature);
157        if eps_dot_ss <= 0.0 || self.m >= 1.0 {
158            return f64::INFINITY;
159        }
160        let numerator = self.a * sigma.powf(self.n) * self.m;
161        (numerator / eps_dot_ss).powf(1.0 / (1.0 - self.m))
162    }
163}
164/// Generalised power-law creep: eps_dot = B * (sigma/sigma_0)^n.
165///
166/// No temperature dependence (use NortonCreep for Arrhenius activation).
167pub struct PowerLawCreep {
168    /// Reference strain rate B (1/s).
169    pub b: f64,
170    /// Reference stress sigma_0 (Pa).
171    pub sigma_0: f64,
172    /// Stress exponent n.
173    pub n: f64,
174}
175impl PowerLawCreep {
176    /// Create a new power-law creep model.
177    pub fn new(b: f64, sigma_0: f64, n: f64) -> Self {
178        Self { b, sigma_0, n }
179    }
180    /// Strain rate at given stress.
181    pub fn strain_rate(&self, stress: f64) -> f64 {
182        self.b * (stress / self.sigma_0).powf(self.n)
183    }
184    /// Creep strain after time t at constant stress.
185    pub fn strain_at_time(&self, stress: f64, t: f64) -> f64 {
186        self.strain_rate(stress) * t
187    }
188
189    /// Creep compliance J(t) = b + sigma_0 · t^n.
190    ///
191    /// Interprets the power-law parameters as a linear viscoelastic
192    /// compliance function where `b` is the instantaneous compliance Jâ‚€,
193    /// `sigma_0` is the time-dependent coefficient, and `n` is the time
194    /// exponent.
195    pub fn creep_compliance(&self, t: f64) -> f64 {
196        self.b + self.sigma_0 * t.powf(self.n)
197    }
198
199    /// Time derivative of creep compliance: dJ/dt = sigma_0 · n · t^(n−1).
200    pub fn creep_rate(&self, t: f64) -> f64 {
201        if t <= 0.0 {
202            return f64::INFINITY;
203        }
204        self.sigma_0 * self.n * t.powf(self.n - 1.0)
205    }
206
207    /// Creep strain at stress σ and time t: ε(t) = σ · J(t).
208    pub fn creep_strain(&self, stress: f64, t: f64) -> f64 {
209        stress * self.creep_compliance(t)
210    }
211}
212/// Empirical three-stage creep curve.
213pub struct CreepCurve {
214    /// Primary creep coefficient A
215    pub primary_rate: f64,
216    /// Secondary (steady-state) creep rate (1/s)
217    pub secondary_rate: f64,
218    /// Strain at which tertiary stage begins
219    pub tertiary_start_strain: f64,
220}
221impl CreepCurve {
222    /// Create a new CreepCurve.
223    pub fn new(primary_rate: f64, secondary_rate: f64, tertiary_start_strain: f64) -> Self {
224        Self {
225            primary_rate,
226            secondary_rate,
227            tertiary_start_strain,
228        }
229    }
230    /// Total creep strain at time t.
231    ///
232    /// eps(t) = primary_rate * sqrt(t) + secondary_rate * t
233    pub fn strain_at_time(&self, t: f64) -> f64 {
234        self.primary_rate * t.sqrt() + self.secondary_rate * t
235    }
236    /// Instantaneous creep rate at time t.
237    ///
238    /// d(eps)/dt = primary_rate/(2*sqrt(t)) + secondary_rate
239    pub fn strain_rate_at_time(&self, t: f64) -> f64 {
240        if t < 1e-30 {
241            return f64::INFINITY;
242        }
243        self.primary_rate / (2.0 * t.sqrt()) + self.secondary_rate
244    }
245    /// Find the time at which a target strain is first reached using bisection.
246    pub fn time_to_strain(&self, target_strain: f64) -> Option<f64> {
247        if target_strain <= 0.0 {
248            return Some(0.0);
249        }
250        let t_max = if self.secondary_rate > 0.0 {
251            target_strain / self.secondary_rate * 100.0
252        } else if self.primary_rate > 0.0 {
253            (target_strain / self.primary_rate).powi(2) * 10.0
254        } else {
255            return None;
256        };
257        if self.strain_at_time(t_max) < target_strain {
258            return None;
259        }
260        let mut lo = 0.0_f64;
261        let mut hi = t_max;
262        for _ in 0..64 {
263            let mid = 0.5 * (lo + hi);
264            if self.strain_at_time(mid) < target_strain {
265                lo = mid;
266            } else {
267                hi = mid;
268            }
269        }
270        Some(0.5 * (lo + hi))
271    }
272    /// Determine which creep stage we are in at a given strain level.
273    pub fn stage_at_strain(&self, strain: f64) -> CreepStage {
274        if strain < self.primary_rate * 100.0_f64.sqrt() {
275            CreepStage::Primary { exponent: 0.5 }
276        } else if strain < self.tertiary_start_strain {
277            CreepStage::Secondary
278        } else {
279            CreepStage::Tertiary { acceleration: 2.0 }
280        }
281    }
282}
283/// Andrade primary creep model.
284///
285/// The Andrade equation describes the transient (primary) creep stage:
286///
287///   ε(t) = β * t^(1/3) + k * t
288///
289/// where β is the primary creep coefficient and k is the steady-state rate.
290/// The first term dominates early (primary stage) and the second at longer times.
291pub struct AndradeCreep {
292    /// Primary creep coefficient β (dimensionless per s^(1/3)).
293    pub beta: f64,
294    /// Steady-state creep coefficient k (1/s).
295    pub k_steady: f64,
296}
297impl AndradeCreep {
298    /// Create a new Andrade creep model.
299    pub fn new(beta: f64, k_steady: f64) -> Self {
300        Self { beta, k_steady }
301    }
302    /// Total creep strain at time t.
303    ///
304    /// ε(t) = β * t^(1/3) + k * t
305    pub fn strain(&self, t: f64) -> f64 {
306        if t <= 0.0 {
307            return 0.0;
308        }
309        self.beta * t.powf(1.0 / 3.0) + self.k_steady * t
310    }
311    /// Instantaneous creep rate.
312    ///
313    /// dε/dt = β/(3*t^(2/3)) + k
314    pub fn strain_rate(&self, t: f64) -> f64 {
315        if t < 1e-30 {
316            return f64::INFINITY;
317        }
318        self.beta / (3.0 * t.powf(2.0 / 3.0)) + self.k_steady
319    }
320    /// Time at which the primary and secondary contributions are equal.
321    ///
322    /// β * t^(1/3) = k * t  →  t_transition = (β/k)^(3/2)
323    pub fn transition_time(&self) -> f64 {
324        if self.k_steady <= 0.0 {
325            return f64::INFINITY;
326        }
327        (self.beta / self.k_steady).powf(1.5)
328    }
329}
330/// Manson-Haferd time-temperature parameter.
331///
332/// P_MH = (log10(t_r) - log10(t_a)) / (T - T_a)
333///
334/// where t_a and T_a are material constants (coordinates of the convergence point).
335pub struct MansonHaferdParameter {
336    /// Log(ta) — log of the convergence time.
337    pub log10_ta: f64,
338    /// T_a — convergence temperature (K).
339    pub t_a: f64,
340}
341impl MansonHaferdParameter {
342    /// Create a new Manson-Haferd parameter.
343    pub fn new(log10_ta: f64, t_a: f64) -> Self {
344        Self { log10_ta, t_a }
345    }
346    /// Compute the MH parameter for given rupture time (hours) and temperature (K).
347    pub fn mh_parameter(&self, time_hours: f64, temperature: f64) -> f64 {
348        (time_hours.log10() - self.log10_ta) / (temperature - self.t_a)
349    }
350    /// Compute rupture time (hours) given temperature and parameter P.
351    pub fn rupture_time(&self, temperature: f64, p_mh: f64) -> f64 {
352        let log10_t = self.log10_ta + p_mh * (temperature - self.t_a);
353        10.0_f64.powf(log10_t)
354    }
355    /// Maximum allowable temperature for a given life and parameter.
356    pub fn max_temperature(&self, time_hours: f64, p_mh: f64) -> f64 {
357        let log10_t = time_hours.log10();
358        self.t_a + (log10_t - self.log10_ta) / p_mh
359    }
360}
361/// Sherby-Dorn parameter for creep life prediction under variable temperature.
362///
363/// P_SD = ∫ exp(-Q / (R * T)) dt ≈ Σ exp(-Q/(RT_i)) * Δt_i
364///
365/// For constant conditions: P_SD = t * exp(-Q/(R*T))
366pub struct SherbyDornParameter {
367    /// Activation energy Q (J/mol).
368    pub activation_energy: f64,
369}
370impl SherbyDornParameter {
371    /// Create a new Sherby-Dorn parameter.
372    pub fn new(activation_energy: f64) -> Self {
373        Self { activation_energy }
374    }
375    /// Compute P_SD for constant temperature and time.
376    ///
377    /// P_SD = t_hours * exp(-Q/(R*T))
378    pub fn parameter_constant_temp(&self, time_hours: f64, temperature: f64) -> f64 {
379        let arrhenius = (-self.activation_energy / (GAS_CONSTANT * temperature)).exp();
380        time_hours * arrhenius
381    }
382    /// Compute temperature-compensated time from variable temperature history.
383    ///
384    /// P_SD = Σ exp(-Q/(R*T_i)) * Δt_i
385    ///
386    /// Accepts a slice of (delta_t, temperature) pairs.
387    pub fn parameter_variable_temp(&self, segments: &[(f64, f64)]) -> f64 {
388        segments
389            .iter()
390            .map(|&(dt, temp)| {
391                let arr = (-self.activation_energy / (GAS_CONSTANT * temp)).exp();
392                dt * arr
393            })
394            .sum()
395    }
396    /// Equivalent rupture time at a reference temperature that gives the same P_SD.
397    ///
398    /// t_ref = P_SD / exp(-Q/(R * T_ref))
399    pub fn equivalent_time(&self, p_sd: f64, temperature_ref: f64) -> f64 {
400        let arr = (-self.activation_energy / (GAS_CONSTANT * temperature_ref)).exp();
401        if arr <= 0.0 {
402            return f64::INFINITY;
403        }
404        p_sd / arr
405    }
406}
407/// Coble (grain-boundary diffusion) creep model.
408///
409/// eps_dot = A_C * D_gb * delta * sigma * Omega / (k_B * T * d^3)
410///
411/// where D_gb is grain-boundary diffusion coefficient, delta is GB width.
412pub struct CobleCreep {
413    /// GB diffusion pre-exponential D_gb0 (m^2/s).
414    pub d_gb0: f64,
415    /// Activation energy for GB diffusion (J/mol).
416    pub activation_energy: f64,
417    /// Grain boundary width delta (m).
418    pub gb_width: f64,
419    /// Atomic volume Omega (m^3).
420    pub atomic_volume: f64,
421    /// Grain size d (m).
422    pub grain_size: f64,
423    /// Numerical constant A_C (typically ~50).
424    pub a_c: f64,
425}
426impl CobleCreep {
427    /// Create a new Coble creep model.
428    #[allow(clippy::too_many_arguments)]
429    pub fn new(
430        d_gb0: f64,
431        activation_energy: f64,
432        gb_width: f64,
433        atomic_volume: f64,
434        grain_size: f64,
435    ) -> Self {
436        Self {
437            d_gb0,
438            activation_energy,
439            gb_width,
440            atomic_volume,
441            grain_size,
442            a_c: 50.0,
443        }
444    }
445    /// Grain-boundary diffusion coefficient at temperature T.
446    pub fn gb_diffusion_coefficient(&self, temperature: f64) -> f64 {
447        self.d_gb0 * (-self.activation_energy / (GAS_CONSTANT * temperature)).exp()
448    }
449    /// Creep strain rate.
450    pub fn strain_rate(&self, stress: f64, temperature: f64) -> f64 {
451        let k_b = 1.380_649e-23;
452        let d_gb = self.gb_diffusion_coefficient(temperature);
453        self.a_c * d_gb * self.gb_width * stress * self.atomic_volume
454            / (k_b * temperature * self.grain_size.powi(3))
455    }
456}
457impl CobleCreep {
458    /// Effective grain-boundary diffusivity at temperature.
459    pub fn gb_diffusivity(&self, temperature: f64) -> f64 {
460        self.d_gb0 * (-self.activation_energy / (GAS_CONSTANT * temperature)).exp()
461    }
462    /// Relative contribution of Coble vs Nabarro-Herring at given conditions.
463    ///
464    /// Returns Coble fraction in \[0, 1\].
465    pub fn coble_fraction(&self, nh: &NabarroHerringCreep, stress: f64, temperature: f64) -> f64 {
466        let rate_coble = self.strain_rate(stress, temperature);
467        let rate_nh = nh.strain_rate(stress, temperature);
468        let total = rate_coble + rate_nh;
469        if total > 0.0 { rate_coble / total } else { 0.5 }
470    }
471}
472/// Nabarro-Herring (lattice diffusion) creep model.
473///
474/// eps_dot = A_NH * D_v * sigma * Omega / (k_B * T * d^2)
475///
476/// where D_v is lattice diffusion coefficient, Omega is atomic volume,
477/// d is grain size.
478pub struct NabarroHerringCreep {
479    /// Diffusion pre-exponential D_0 (m^2/s).
480    pub d0: f64,
481    /// Activation energy for lattice diffusion (J/mol).
482    pub activation_energy: f64,
483    /// Atomic volume Omega (m^3).
484    pub atomic_volume: f64,
485    /// Grain size d (m).
486    pub grain_size: f64,
487    /// Numerical constant A_NH (typically ~14).
488    pub a_nh: f64,
489}
490impl NabarroHerringCreep {
491    /// Create a new Nabarro-Herring creep model.
492    pub fn new(d0: f64, activation_energy: f64, atomic_volume: f64, grain_size: f64) -> Self {
493        Self {
494            d0,
495            activation_energy,
496            atomic_volume,
497            grain_size,
498            a_nh: 14.0,
499        }
500    }
501    /// Lattice diffusion coefficient at temperature T.
502    pub fn diffusion_coefficient(&self, temperature: f64) -> f64 {
503        self.d0 * (-self.activation_energy / (GAS_CONSTANT * temperature)).exp()
504    }
505    /// Creep strain rate at given stress and temperature.
506    pub fn strain_rate(&self, stress: f64, temperature: f64) -> f64 {
507        let k_b = 1.380_649e-23;
508        let d_v = self.diffusion_coefficient(temperature);
509        self.a_nh * d_v * stress * self.atomic_volume
510            / (k_b * temperature * self.grain_size * self.grain_size)
511    }
512}
513impl NabarroHerringCreep {
514    /// Activation energy Q (J/mol) for diffusion.
515    pub fn activation_energy(&self) -> f64 {
516        self.activation_energy
517    }
518    /// Effective diffusivity at temperature (D_eff = D_0 * exp(-Q/RT)).
519    pub fn effective_diffusivity(&self, temperature: f64) -> f64 {
520        self.d0 * (-self.activation_energy / (GAS_CONSTANT * temperature)).exp()
521    }
522    /// Strain rate normalised by stress (creep compliance, s^-1 Pa^-1).
523    pub fn compliance(&self, temperature: f64) -> f64 {
524        let d_eff = self.effective_diffusivity(temperature);
525        self.a_nh * d_eff * self.atomic_volume / (self.grain_size * self.grain_size)
526    }
527}
528/// Tertiary creep model using Kachanov-Rabotnov damage mechanics.
529///
530/// Strain rate: ε̇ = A * σ^n / (1 − ω)^n
531/// Damage rate: ω̇ = B * σ^χ / (1 − ω)^φ
532#[allow(dead_code)]
533pub struct TertiaryCreepModel {
534    /// Creep constant A
535    pub a: f64,
536    /// Creep stress exponent n
537    pub n: f64,
538    /// Damage constant B
539    pub b_damage: f64,
540    /// Damage stress exponent χ
541    pub chi: f64,
542    /// Damage softening exponent φ
543    pub phi: f64,
544    /// Current damage variable ω ∈ \[0, 1)
545    pub omega: f64,
546}
547impl TertiaryCreepModel {
548    /// Create a new tertiary creep model.
549    #[allow(clippy::too_many_arguments)]
550    pub fn new(a: f64, n: f64, b_damage: f64, chi: f64, phi: f64) -> Self {
551        Self {
552            a,
553            n,
554            b_damage,
555            chi,
556            phi,
557            omega: 0.0,
558        }
559    }
560    /// Creep strain rate at current damage state.
561    pub fn strain_rate(&self, stress: f64) -> f64 {
562        let denom = (1.0 - self.omega).max(1e-9);
563        self.a * stress.powf(self.n) / denom.powf(self.n)
564    }
565    /// Damage rate dω/dt at current damage state.
566    pub fn damage_rate(&self, stress: f64) -> f64 {
567        let denom = (1.0 - self.omega).max(1e-9);
568        self.b_damage * stress.powf(self.chi) / denom.powf(self.phi)
569    }
570    /// Advance one time step using forward Euler.
571    pub fn step(&mut self, stress: f64, dt: f64) {
572        let eps_dot = self.strain_rate(stress);
573        let omega_dot = self.damage_rate(stress);
574        let _ = eps_dot;
575        self.omega = (self.omega + omega_dot * dt).min(0.9999);
576    }
577    /// True when tertiary stage is complete (ω → 1 numerically).
578    pub fn is_ruptured(&self) -> bool {
579        self.omega >= 0.999
580    }
581    /// Estimated time to rupture (approximate, constant stress).
582    pub fn rupture_time(&self, stress: f64) -> f64 {
583        let omega_dot = self.damage_rate(stress);
584        if omega_dot > 0.0 {
585            (1.0 - self.omega) / omega_dot
586        } else {
587            f64::INFINITY
588        }
589    }
590    /// Reset damage to zero.
591    pub fn reset(&mut self) {
592        self.omega = 0.0;
593    }
594}
595/// Kachanov-Rabotnov continuum damage mechanics model.
596pub struct CreepDamage {
597    /// Damage rate constant A
598    pub a: f64,
599    /// Stress exponent m
600    pub m: f64,
601    /// Damage exponent r
602    pub r: f64,
603}
604impl CreepDamage {
605    /// Create a new CreepDamage model.
606    pub fn new(a: f64, m: f64, r: f64) -> Self {
607        Self { a, m, r }
608    }
609    /// Damage evolution rate.
610    ///
611    /// dot_omega = A * sigma^m / (1 - omega)^r
612    pub fn damage_rate(&self, stress: f64, damage: f64) -> f64 {
613        let denominator = (1.0 - damage).powf(self.r);
614        if denominator < f64::EPSILON {
615            f64::INFINITY
616        } else {
617            self.a * stress.powf(self.m) / denominator
618        }
619    }
620    /// Integrate damage evolution from 0.
621    pub fn integrate(&self, stress: f64, dt: f64, n_steps: usize) -> Vec<f64> {
622        let mut omega = 0.0_f64;
623        let mut history = Vec::with_capacity(n_steps);
624        for _ in 0..n_steps {
625            let rate = self.damage_rate(stress, omega);
626            omega = (omega + rate * dt).min(1.0);
627            history.push(omega);
628            if omega >= 0.99 {
629                break;
630            }
631        }
632        history
633    }
634    /// Analytical time to failure.
635    ///
636    /// t_f = 1 / ((r+1) * A * sigma^m)
637    pub fn time_to_failure(&self, stress: f64) -> f64 {
638        let base_rate = self.a * stress.powf(self.m);
639        if base_rate <= 0.0 {
640            return f64::INFINITY;
641        }
642        1.0 / ((self.r + 1.0) * base_rate)
643    }
644    /// Effective stress accounting for damage: sigma_eff = sigma / (1 - omega).
645    pub fn effective_stress(&self, stress: f64, damage: f64) -> f64 {
646        if damage >= 1.0 {
647            f64::INFINITY
648        } else {
649            stress / (1.0 - damage)
650        }
651    }
652}
653/// Stress-dependent Larson-Miller parameter using a power-law fit.
654///
655/// The LM parameter P is a function of stress:
656///   P(σ) = A - B * log10(σ/σ_ref)
657///
658/// where A and B are material constants fit to rupture data.
659pub struct LarsonMillerStress {
660    /// LM parameter at reference stress: P(σ_ref) = A.
661    pub a_lm: f64,
662    /// Slope dP/d(log σ): B (negative for decreasing P with stress).
663    pub b_lm: f64,
664    /// Reference stress (Pa).
665    pub sigma_ref: f64,
666    /// Larson-Miller constant C.
667    pub c_lm: f64,
668}
669impl LarsonMillerStress {
670    /// Create a new stress-dependent LM model.
671    pub fn new(a_lm: f64, b_lm: f64, sigma_ref: f64, c_lm: f64) -> Self {
672        Self {
673            a_lm,
674            b_lm,
675            sigma_ref,
676            c_lm,
677        }
678    }
679    /// Compute the LM parameter for a given stress.
680    pub fn lm_parameter(&self, sigma: f64) -> f64 {
681        if sigma <= 0.0 {
682            return f64::INFINITY;
683        }
684        self.a_lm - self.b_lm * (sigma / self.sigma_ref).log10()
685    }
686    /// Predict rupture life (hours) for a given stress and temperature.
687    ///
688    /// t_r = 10^(P(σ)/T - C)
689    pub fn rupture_time(&self, sigma: f64, temperature: f64) -> f64 {
690        let p = self.lm_parameter(sigma);
691        let log10_t = p / temperature - self.c_lm;
692        10.0_f64.powf(log10_t)
693    }
694    /// Maximum allowable stress for a given life and temperature.
695    ///
696    /// Inverts P(σ) = T*(C + log10(t)) to give σ.
697    pub fn max_stress(&self, temperature: f64, time_hours: f64) -> f64 {
698        let p_required = temperature * (self.c_lm + time_hours.log10());
699        if self.b_lm.abs() < 1e-30 {
700            return self.sigma_ref;
701        }
702        let log_sigma_ratio = (self.a_lm - p_required) / self.b_lm;
703        self.sigma_ref * 10.0_f64.powf(log_sigma_ratio)
704    }
705}
706/// Creep rupture envelope using Larson-Miller master curve.
707///
708/// Maps (temperature, stress) → predicted rupture life using a polynomial
709/// master curve fit to the Larson-Miller parameter.
710#[derive(Debug, Clone)]
711pub struct RuptureEnvelope {
712    /// Larson-Miller constant C.
713    pub c: f64,
714    /// Polynomial coefficients \[a0, a1, a2\] for LM master curve:
715    /// P_LM = a0 + a1*log10(sigma) + a2*log10(sigma)^2
716    pub curve_coeffs: [f64; 3],
717}
718impl RuptureEnvelope {
719    /// Create a rupture envelope.
720    pub fn new(c: f64, curve_coeffs: [f64; 3]) -> Self {
721        Self { c, curve_coeffs }
722    }
723    /// Evaluate Larson-Miller parameter from stress (Pa).
724    pub fn lm_parameter_from_stress(&self, stress: f64) -> f64 {
725        if stress <= 0.0 {
726            return 0.0;
727        }
728        let log_s = stress.log10();
729        self.curve_coeffs[0] + self.curve_coeffs[1] * log_s + self.curve_coeffs[2] * log_s * log_s
730    }
731    /// Rupture life (s) at given stress and temperature (K).
732    pub fn rupture_life(&self, stress: f64, temperature: f64) -> f64 {
733        let p = self.lm_parameter_from_stress(stress);
734        if temperature <= 0.0 {
735            return f64::INFINITY;
736        }
737        let exponent = p / temperature - self.c;
738        10.0_f64.powf(exponent)
739    }
740    /// Maximum allowable stress for a given temperature and design life (s).
741    ///
742    /// Finds σ such that lm_parameter_from_stress(σ) = T*(C + log10(t_r)).
743    /// Assumes the LM master curve is monotonically decreasing with stress
744    /// (negative first coefficient).
745    pub fn allowable_stress(&self, temperature: f64, life_s: f64) -> f64 {
746        if life_s <= 0.0 || temperature <= 0.0 {
747            return 0.0;
748        }
749        let p_target = temperature * (self.c + life_s.log10());
750        let mut lo = 1.0e3_f64;
751        let mut hi = 1.0e9_f64;
752        let p_lo = self.lm_parameter_from_stress(lo);
753        let p_hi = self.lm_parameter_from_stress(hi);
754        if p_lo <= p_hi {
755            for _ in 0..80 {
756                let mid = (lo + hi) / 2.0;
757                if self.lm_parameter_from_stress(mid) < p_target {
758                    lo = mid;
759                } else {
760                    hi = mid;
761                }
762            }
763        } else {
764            for _ in 0..80 {
765                let mid = (lo + hi) / 2.0;
766                if self.lm_parameter_from_stress(mid) > p_target {
767                    lo = mid;
768                } else {
769                    hi = mid;
770                }
771            }
772        }
773        (lo + hi) / 2.0
774    }
775}
776/// Composite three-stage creep model combining primary, secondary, and
777/// tertiary contributions.
778#[allow(dead_code)]
779pub struct ThreeStageCreep {
780    /// Primary creep parameters.
781    pub primary: PrimaryCreepModel,
782    /// Secondary (steady-state) creep.
783    pub secondary: SecondaryCreepModel,
784    /// Tertiary damage model.
785    pub tertiary: TertiaryCreepModel,
786    /// Strain threshold beyond which tertiary stage begins.
787    pub tertiary_onset_strain: f64,
788    /// Accumulated total strain.
789    pub strain: f64,
790    /// Elapsed time.
791    pub time: f64,
792}
793impl ThreeStageCreep {
794    /// Create a three-stage creep model.
795    pub fn new(
796        primary: PrimaryCreepModel,
797        secondary: SecondaryCreepModel,
798        tertiary: TertiaryCreepModel,
799        tertiary_onset_strain: f64,
800    ) -> Self {
801        Self {
802            primary,
803            secondary,
804            tertiary,
805            tertiary_onset_strain,
806            strain: 0.0,
807            time: 0.0,
808        }
809    }
810    /// Advance one time step at given stress and temperature.
811    ///
812    /// Returns the incremental strain for this step.
813    pub fn step(&mut self, stress: f64, temperature: f64, dt: f64) -> f64 {
814        let eps_dot_primary = self.primary.strain_rate(stress, self.time + dt);
815        let eps_dot_secondary = self.secondary.strain_rate(stress, temperature);
816        let eps_dot = if self.strain < self.tertiary_onset_strain {
817            eps_dot_primary.max(eps_dot_secondary)
818        } else {
819            self.tertiary.step(stress, dt);
820            self.tertiary.strain_rate(stress)
821        };
822        let d_eps = eps_dot * dt;
823        self.strain += d_eps;
824        self.time += dt;
825        d_eps
826    }
827    /// Current creep stage as a string tag.
828    pub fn current_stage(&self) -> &'static str {
829        if self.tertiary.omega > 0.01 {
830            "tertiary"
831        } else if self.strain > self.tertiary_onset_strain * 0.5 {
832            "secondary"
833        } else {
834            "primary"
835        }
836    }
837    /// True when material has ruptured.
838    pub fn is_ruptured(&self) -> bool {
839        self.tertiary.is_ruptured()
840    }
841}
842/// Predict creep rupture life using multiple extrapolation methods.
843///
844/// Returns a struct with results from different time-temperature parameters.
845#[derive(Debug, Clone)]
846pub struct RuptureLifePrediction {
847    /// Larson-Miller prediction (hours).
848    pub larson_miller: f64,
849    /// Orr-Sherby-Dorn prediction (hours).
850    pub orr_sherby_dorn: f64,
851    /// Monkman-Grant prediction (hours).
852    pub monkman_grant: f64,
853}
854/// Integrates Norton creep over a stress-time history at constant temperature.
855pub struct ThermalCreepIntegrator {
856    /// Norton creep law
857    pub norton: NortonCreep,
858}
859impl ThermalCreepIntegrator {
860    /// Create a new ThermalCreepIntegrator.
861    pub fn new(norton: NortonCreep) -> Self {
862        Self { norton }
863    }
864    /// Creep strain increment over a time step dt.
865    pub fn creep_strain_increment(&self, sigma: f64, temperature: f64, dt: f64) -> f64 {
866        self.norton.creep_strain_rate(sigma, temperature) * dt
867    }
868    /// Integrate stress history (time, stress) pairs at constant temperature.
869    pub fn integrate(&self, stress_history: &[(f64, f64)], temperature: f64, dt: f64) -> Vec<f64> {
870        let mut accumulated = 0.0_f64;
871        let mut strains = Vec::with_capacity(stress_history.len());
872        for &(_t, sigma) in stress_history {
873            accumulated += self.creep_strain_increment(sigma, temperature, dt);
874            strains.push(accumulated);
875        }
876        strains
877    }
878}
879/// Stress relaxation model (Maxwell model).
880///
881/// sigma(t) = sigma_0 * exp(-t / tau)
882///
883/// where tau = eta / E is the relaxation time.
884pub struct StressRelaxation {
885    /// Young's modulus E (Pa).
886    pub young_modulus: f64,
887    /// Viscosity eta (Pa*s).
888    pub viscosity: f64,
889}
890impl StressRelaxation {
891    /// Create a new stress relaxation model.
892    pub fn new(young_modulus: f64, viscosity: f64) -> Self {
893        Self {
894            young_modulus,
895            viscosity,
896        }
897    }
898    /// Relaxation time tau = eta / E (s).
899    pub fn relaxation_time(&self) -> f64 {
900        self.viscosity / self.young_modulus
901    }
902    /// Stress at time t given initial stress sigma_0.
903    pub fn stress_at_time(&self, sigma_0: f64, t: f64) -> f64 {
904        let tau = self.relaxation_time();
905        sigma_0 * (-t / tau).exp()
906    }
907    /// Time to relax to a fraction f of the initial stress.
908    ///
909    /// t = -tau * ln(f)
910    pub fn time_to_fraction(&self, fraction: f64) -> f64 {
911        let tau = self.relaxation_time();
912        -tau * fraction.ln()
913    }
914    /// Stress history from t=0 to t_end with n_steps.
915    pub fn stress_history(&self, sigma_0: f64, t_end: f64, n_steps: usize) -> Vec<(f64, f64)> {
916        (0..=n_steps)
917            .map(|i| {
918                let t = t_end * (i as f64) / (n_steps as f64);
919                (t, self.stress_at_time(sigma_0, t))
920            })
921            .collect()
922    }
923}
924/// Creep-fatigue interaction model using the damage summation approach.
925///
926/// The interaction rule combines fatigue damage fraction (N/Nf) and creep
927/// damage fraction (t/tr) with an interaction factor:
928///
929///   D_total = Σ(N_i / N_fi) + Σ(t_j / t_rj) + interaction_term
930///
931/// Failure is predicted when D_total ≥ 1.
932///
933/// Interaction diagram (Langer-Halford): tri-linear failure boundary.
934pub struct CreepFatigueInteraction {
935    /// Fatigue intercept on the creep-fatigue diagram (creep damage at zero fatigue).
936    pub creep_intercept: f64,
937    /// Fatigue intercept (fatigue damage at zero creep).
938    pub fatigue_intercept: f64,
939    /// Interaction exponent.
940    pub interaction_exponent: f64,
941}
942impl CreepFatigueInteraction {
943    /// Create a new creep-fatigue interaction model.
944    pub fn new(creep_intercept: f64, fatigue_intercept: f64, interaction_exponent: f64) -> Self {
945        Self {
946            creep_intercept,
947            fatigue_intercept,
948            interaction_exponent,
949        }
950    }
951    /// Check whether a given (fatigue_damage, creep_damage) combination is safe.
952    ///
953    /// Uses the Langer interaction locus:
954    ///   (Df/Di_f)^n + (Dc/Di_c)^n <= 1
955    pub fn is_safe(&self, fatigue_damage: f64, creep_damage: f64) -> bool {
956        let n = self.interaction_exponent;
957        let term_f = (fatigue_damage / self.fatigue_intercept).powf(n);
958        let term_c = (creep_damage / self.creep_intercept).powf(n);
959        term_f + term_c <= 1.0
960    }
961    /// Total interaction damage index.
962    pub fn interaction_damage(&self, fatigue_damage: f64, creep_damage: f64) -> f64 {
963        let n = self.interaction_exponent;
964        let term_f = (fatigue_damage / self.fatigue_intercept).powf(n);
965        let term_c = (creep_damage / self.creep_intercept).powf(n);
966        (term_f + term_c).powf(1.0 / n)
967    }
968    /// Remaining allowable creep damage given a fatigue damage fraction.
969    pub fn remaining_creep(&self, fatigue_damage: f64) -> f64 {
970        let n = self.interaction_exponent;
971        let term_f = (fatigue_damage / self.fatigue_intercept).powf(n);
972        let remaining = (1.0 - term_f).max(0.0);
973        self.creep_intercept * remaining.powf(1.0 / n)
974    }
975}
976/// Multi-axial creep model using the von Mises equivalent stress.
977///
978/// For a general stress state (σ1, σ2, σ3), the equivalent (von Mises) stress is:
979///   σ_eq = sqrt\[ ((σ1-σ2)² + (σ2-σ3)² + (σ3-σ1)²) / 2 \]
980///
981/// The creep strain rate in each principal direction is:
982///   eps_dot_i = eps_dot_eq * (σ_i - (σ1+σ2+σ3)/3) / (σ_eq / (3/2))
983///
984/// where eps_dot_eq = A * σ_eq^n * exp(-Q/RT).
985pub struct MultiaxialCreep {
986    /// Norton model for equivalent strain rate.
987    pub norton: NortonCreep,
988}
989impl MultiaxialCreep {
990    /// Create a new multiaxial creep model.
991    pub fn new(norton: NortonCreep) -> Self {
992        Self { norton }
993    }
994    /// Von Mises equivalent stress.
995    pub fn von_mises_stress(&self, s1: f64, s2: f64, s3: f64) -> f64 {
996        (0.5 * ((s1 - s2).powi(2) + (s2 - s3).powi(2) + (s3 - s1).powi(2))).sqrt()
997    }
998    /// Hydrostatic (mean) stress.
999    pub fn mean_stress(&self, s1: f64, s2: f64, s3: f64) -> f64 {
1000        (s1 + s2 + s3) / 3.0
1001    }
1002    /// Principal creep strain rates \[eps1_dot, eps2_dot, eps3_dot\].
1003    ///
1004    /// Returns `[0, 0, 0]` if von Mises stress is zero.
1005    pub fn principal_creep_rates(&self, s1: f64, s2: f64, s3: f64, temperature: f64) -> [f64; 3] {
1006        let s_eq = self.von_mises_stress(s1, s2, s3);
1007        if s_eq < 1e-30 {
1008            return [0.0; 3];
1009        }
1010        let s_mean = self.mean_stress(s1, s2, s3);
1011        let eps_dot_eq = self.norton.creep_strain_rate(s_eq, temperature);
1012        let scale = 1.5 * eps_dot_eq / s_eq;
1013        [
1014            scale * (s1 - s_mean),
1015            scale * (s2 - s_mean),
1016            scale * (s3 - s_mean),
1017        ]
1018    }
1019    /// Effective volumetric creep strain rate (should be zero for volume-preserving).
1020    pub fn volumetric_creep_rate(&self, s1: f64, s2: f64, s3: f64, temperature: f64) -> f64 {
1021        let rates = self.principal_creep_rates(s1, s2, s3, temperature);
1022        rates[0] + rates[1] + rates[2]
1023    }
1024}
1025/// Simplified deformation mechanism map for a material.
1026///
1027/// Based on homologous temperature (T/T_m) and normalised stress (σ/μ),
1028/// determines the dominant deformation mechanism.
1029#[derive(Debug, Clone)]
1030pub struct DeformationMechanismMap {
1031    /// Melting temperature T_m (K).
1032    pub t_melting: f64,
1033    /// Shear modulus μ (Pa).
1034    pub shear_modulus: f64,
1035    /// Peierls stress (normalised, σ/μ threshold for dislocation glide).
1036    pub peierls_stress_norm: f64,
1037    /// Boundary for power-law creep vs diffusion creep (normalised stress).
1038    pub power_law_diff_boundary: f64,
1039}
1040impl DeformationMechanismMap {
1041    /// Create a deformation mechanism map.
1042    pub fn new(
1043        t_melting: f64,
1044        shear_modulus: f64,
1045        peierls_stress_norm: f64,
1046        power_law_diff_boundary: f64,
1047    ) -> Self {
1048        Self {
1049            t_melting,
1050            shear_modulus,
1051            peierls_stress_norm,
1052            power_law_diff_boundary,
1053        }
1054    }
1055    /// Nickel approximation.
1056    pub fn nickel() -> Self {
1057        Self::new(1728.0, 76.0e9, 3e-3, 1e-4)
1058    }
1059    /// Identify dominant creep mechanism.
1060    ///
1061    /// Returns: "elastic", "dislocation_glide", "power_law_creep", "diffusion_creep", or "superplastic".
1062    pub fn dominant_mechanism(&self, stress: f64, temperature: f64) -> &'static str {
1063        let t_hom = temperature / self.t_melting;
1064        let sigma_norm = stress / self.shear_modulus;
1065        if sigma_norm > self.peierls_stress_norm {
1066            "dislocation_glide"
1067        } else if t_hom < 0.3 {
1068            "elastic"
1069        } else if sigma_norm > self.power_law_diff_boundary {
1070            "power_law_creep"
1071        } else if t_hom > 0.85 {
1072            "superplastic"
1073        } else {
1074            "diffusion_creep"
1075        }
1076    }
1077    /// Approximate steady-state strain rate combining power law + diffusion creep.
1078    ///
1079    /// eps_dot ≈ A_PL * (σ/μ)^5 * exp(-Q_PL/(R*T)) + A_D * (σ/μ) * exp(-Q_D/(R*T))
1080    pub fn combined_strain_rate(
1081        &self,
1082        stress: f64,
1083        temperature: f64,
1084        a_pl: f64,
1085        q_pl: f64,
1086        a_d: f64,
1087        q_d: f64,
1088    ) -> f64 {
1089        let r = 8.314;
1090        let sigma_norm = stress / self.shear_modulus;
1091        let pl = a_pl * sigma_norm.powi(5) * (-q_pl / (r * temperature)).exp();
1092        let diff = a_d * sigma_norm * (-q_d / (r * temperature)).exp();
1093        pl + diff
1094    }
1095}
1096/// Norton power-law creep model.
1097///
1098/// Strain rate: eps_dot = A * sigma^n * exp(-Q / (R * T))
1099pub struct NortonCreep {
1100    /// Norton constant A (s^-1 Pa^-n)
1101    pub a: f64,
1102    /// Stress exponent n
1103    pub n: f64,
1104    /// Activation energy Q (J/mol)
1105    pub activation_energy: f64,
1106    /// Universal gas constant R (J/mol/K)
1107    pub gas_constant: f64,
1108}
1109impl NortonCreep {
1110    /// Create a new NortonCreep model.
1111    pub fn new(a: f64, n: f64, activation_energy: f64) -> Self {
1112        Self {
1113            a,
1114            n,
1115            activation_energy,
1116            gas_constant: GAS_CONSTANT,
1117        }
1118    }
1119    /// Compute steady-state creep strain rate at given stress and temperature.
1120    pub fn creep_strain_rate(&self, stress: f64, temperature: f64) -> f64 {
1121        let arrhenius = (-self.activation_energy / (self.gas_constant * temperature)).exp();
1122        self.a * stress.powf(self.n) * arrhenius
1123    }
1124    /// Normalized Norton-Bailey creep rate using yield stress.
1125    pub fn creep_strain_rate_normalized(
1126        &self,
1127        stress: f64,
1128        yield_stress: f64,
1129        temperature: f64,
1130    ) -> f64 {
1131        let sigma_norm = stress / yield_stress;
1132        let arrhenius = (-self.activation_energy / (self.gas_constant * temperature)).exp();
1133        self.a * sigma_norm.powf(self.n) * arrhenius
1134    }
1135    /// Estimate time to rupture (simplified).
1136    pub fn time_to_rupture(&self, stress: f64, temperature: f64) -> f64 {
1137        let rate = self.creep_strain_rate(stress, temperature);
1138        if rate > 0.0 {
1139            1.0 / rate
1140        } else {
1141            f64::INFINITY
1142        }
1143    }
1144    /// Alias for [`creep_strain_rate`](Self::creep_strain_rate).
1145    pub fn creep_rate(&self, stress: f64, temperature: f64) -> f64 {
1146        self.creep_strain_rate(stress, temperature)
1147    }
1148
1149    /// Effective viscosity at given stress and temperature.
1150    ///
1151    /// eta = sigma / (3 * eps_dot)
1152    pub fn effective_viscosity(&self, stress: f64, temperature: f64) -> f64 {
1153        let rate = self.creep_strain_rate(stress, temperature);
1154        if rate > 1e-30 {
1155            stress / (3.0 * rate)
1156        } else {
1157            f64::INFINITY
1158        }
1159    }
1160}
1161/// Creep stage classification.
1162pub enum CreepStage {
1163    /// Primary (transient) creep: eps ~ t^m with m < 1.
1164    Primary {
1165        /// Time exponent (typically 0.3-0.5).
1166        exponent: f64,
1167    },
1168    /// Secondary (steady-state) creep: constant strain rate.
1169    Secondary,
1170    /// Tertiary creep: accelerating rate leading to fracture.
1171    Tertiary {
1172        /// Acceleration coefficient.
1173        acceleration: f64,
1174    },
1175}
1176/// Orr-Sherby-Dorn (OSD) time-temperature parameter.
1177///
1178/// P_OSD = log10(t_r) - Q_OSD / (2.303 * R * T)
1179///
1180/// where Q_OSD is the creep activation energy.
1181pub struct OrrSherbyDornParameter {
1182    /// Activation energy Q (J/mol).
1183    pub q: f64,
1184}
1185impl OrrSherbyDornParameter {
1186    /// Create a new OSD parameter.
1187    pub fn new(q: f64) -> Self {
1188        Self { q }
1189    }
1190    /// Compute OSD parameter for given rupture time (hours) and temperature (K).
1191    pub fn osd_parameter(&self, time_hours: f64, temperature: f64) -> f64 {
1192        time_hours.log10() - self.q / (2.303 * GAS_CONSTANT * temperature)
1193    }
1194    /// Compute rupture time (hours) given temperature and parameter.
1195    pub fn rupture_time(&self, temperature: f64, p_osd: f64) -> f64 {
1196        let log10_t = p_osd + self.q / (2.303 * GAS_CONSTANT * temperature);
1197        10.0_f64.powf(log10_t)
1198    }
1199}
1200/// Coupled creep-damage model: damage accelerates the creep rate.
1201///
1202/// Effective creep strain rate accounting for damage:
1203///   eps_dot = A * (sigma / (1 - omega))^n * exp(-Q / RT)
1204///
1205/// Damage evolution:
1206///   omega_dot = B * sigma^m / (1 - omega)^phi
1207pub struct CoupledCreepDamage {
1208    /// Norton creep constant A (s^-1 Pa^-n).
1209    pub a: f64,
1210    /// Creep stress exponent n.
1211    pub n: f64,
1212    /// Activation energy Q (J/mol).
1213    pub q: f64,
1214    /// Damage rate constant B.
1215    pub b_damage: f64,
1216    /// Damage stress exponent m.
1217    pub m_damage: f64,
1218    /// Damage evolution exponent phi.
1219    pub phi_damage: f64,
1220}
1221impl CoupledCreepDamage {
1222    /// Create a new coupled creep-damage model.
1223    #[allow(clippy::too_many_arguments)]
1224    pub fn new(a: f64, n: f64, q: f64, b_damage: f64, m_damage: f64, phi_damage: f64) -> Self {
1225        Self {
1226            a,
1227            n,
1228            q,
1229            b_damage,
1230            m_damage,
1231            phi_damage,
1232        }
1233    }
1234    /// Effective creep strain rate at given stress, temperature and damage.
1235    pub fn creep_rate(&self, stress: f64, temperature: f64, damage: f64) -> f64 {
1236        let eff_stress = stress / (1.0 - damage).max(f64::EPSILON);
1237        let arr = (-self.q / (GAS_CONSTANT * temperature)).exp();
1238        self.a * eff_stress.powf(self.n) * arr
1239    }
1240    /// Damage evolution rate.
1241    pub fn damage_rate(&self, stress: f64, damage: f64) -> f64 {
1242        let denom = (1.0 - damage).powf(self.phi_damage);
1243        if denom < f64::EPSILON {
1244            f64::INFINITY
1245        } else {
1246            self.b_damage * stress.powf(self.m_damage) / denom
1247        }
1248    }
1249    /// Integrate coupled creep-damage equations using explicit Euler.
1250    ///
1251    /// Returns (strain history, damage history) each of length n_steps+1.
1252    pub fn integrate(
1253        &self,
1254        stress: f64,
1255        temperature: f64,
1256        dt: f64,
1257        n_steps: usize,
1258    ) -> (Vec<f64>, Vec<f64>) {
1259        let mut eps = 0.0_f64;
1260        let mut omega = 0.0_f64;
1261        let mut eps_hist = Vec::with_capacity(n_steps + 1);
1262        let mut dam_hist = Vec::with_capacity(n_steps + 1);
1263        eps_hist.push(eps);
1264        dam_hist.push(omega);
1265        for _ in 0..n_steps {
1266            let cr = self.creep_rate(stress, temperature, omega);
1267            let dr = self.damage_rate(stress, omega);
1268            eps += cr * dt;
1269            omega = (omega + dr * dt).min(1.0);
1270            eps_hist.push(eps);
1271            dam_hist.push(omega);
1272            if omega >= 0.99 {
1273                break;
1274            }
1275        }
1276        (eps_hist, dam_hist)
1277    }
1278}
1279/// Secondary (steady-state) creep stage — delegates to NortonCreep.
1280///
1281/// This is a thin wrapper that represents the constant-rate stage.
1282#[allow(dead_code)]
1283pub struct SecondaryCreepModel {
1284    /// Underlying Norton power-law parameters.
1285    pub norton: NortonCreep,
1286}
1287impl SecondaryCreepModel {
1288    /// Create from Norton parameters.
1289    pub fn new(a: f64, n: f64, activation_energy: f64) -> Self {
1290        Self {
1291            norton: NortonCreep::new(a, n, activation_energy),
1292        }
1293    }
1294    /// Steady-state creep rate (same as NortonCreep).
1295    pub fn strain_rate(&self, stress: f64, temperature: f64) -> f64 {
1296        self.norton.creep_strain_rate(stress, temperature)
1297    }
1298    /// Accumulated strain after duration `dt` at steady state.
1299    pub fn accumulated_strain(&self, stress: f64, temperature: f64, dt: f64) -> f64 {
1300        self.strain_rate(stress, temperature) * dt
1301    }
1302    /// Effective viscosity η = σ / (3 ε̇).
1303    pub fn viscosity(&self, stress: f64, temperature: f64) -> f64 {
1304        self.norton.effective_viscosity(stress, temperature)
1305    }
1306}
1307/// Perzyna viscoplastic model.
1308pub struct ViscoplasticModel {
1309    /// Initial yield stress (Pa)
1310    pub yield_stress: f64,
1311    /// Viscosity parameter eta (Pa*s)
1312    pub viscosity: f64,
1313    /// Isotropic hardening modulus H (Pa)
1314    pub hardening: f64,
1315}
1316impl ViscoplasticModel {
1317    /// Create a new ViscoplasticModel.
1318    pub fn new(yield_stress: f64, viscosity: f64, hardening: f64) -> Self {
1319        Self {
1320            yield_stress,
1321            viscosity,
1322            hardening,
1323        }
1324    }
1325    /// Overstress function (Macaulay bracket).
1326    pub fn overstress(&self, total_stress: f64, accumulated_plastic: f64) -> f64 {
1327        let flow_stress = self.yield_stress + self.hardening * accumulated_plastic;
1328        let excess = total_stress.abs() - flow_stress;
1329        if excess > 0.0 {
1330            excess / self.yield_stress
1331        } else {
1332            0.0
1333        }
1334    }
1335    /// Plastic strain rate: eps_dot_p = (1/eta) * phi.
1336    pub fn plastic_strain_rate(&self, stress: f64, plastic_strain: f64) -> f64 {
1337        let phi = self.overstress(stress, plastic_strain);
1338        phi / self.viscosity
1339    }
1340    /// Explicit Euler step: returns (new_plastic_strain, new_accumulated_plastic).
1341    pub fn step(&self, stress: f64, plastic_strain: f64, dt: f64) -> (f64, f64) {
1342        let rate = self.plastic_strain_rate(stress, plastic_strain);
1343        let dep = rate * dt * stress.signum();
1344        let new_plastic = plastic_strain + dep;
1345        let new_accumulated = plastic_strain.abs() + dep.abs();
1346        (new_plastic, new_accumulated)
1347    }
1348}
1349/// Larson-Miller parameter for creep rupture life prediction.
1350pub struct LarsonMillerParameter {
1351    /// Larson-Miller constant C (typically ~20 for metals)
1352    pub c_lm: f64,
1353}
1354impl LarsonMillerParameter {
1355    /// Create a new LarsonMillerParameter.
1356    pub fn new(c_lm: f64) -> Self {
1357        Self { c_lm }
1358    }
1359    /// Compute Larson-Miller parameter P = T * (C + log10(t_r)).
1360    pub fn lm_parameter(&self, temperature: f64, time_hours: f64) -> f64 {
1361        temperature * (self.c_lm + time_hours.log10())
1362    }
1363    /// Compute rupture time in hours given temperature (K) and LM parameter.
1364    pub fn rupture_time(&self, temperature: f64, lm_param: f64) -> f64 {
1365        let log10_t = lm_param / temperature - self.c_lm;
1366        10_f64.powf(log10_t)
1367    }
1368    /// Maximum allowable temperature (K) for a given life and LM parameter.
1369    pub fn max_temperature(&self, time_hours: f64, lm_param: f64) -> f64 {
1370        lm_param / (self.c_lm + time_hours.log10())
1371    }
1372    /// Equivalent time at a different temperature for the same LM parameter.
1373    ///
1374    /// Given a known (T1, t1), compute t2 at temperature T2.
1375    pub fn equivalent_time(&self, t1: f64, temp1: f64, temp2: f64) -> f64 {
1376        let p = self.lm_parameter(temp1, t1);
1377        self.rupture_time(temp2, p)
1378    }
1379}
1380/// Zener-Hollomon parameter Z = ε_dot * exp(Q/(R*T)).
1381///
1382/// Combines the effect of strain rate and temperature into a single
1383/// temperature-compensated strain rate. Useful for hot deformation.
1384pub struct ZenerHollomon {
1385    /// Activation energy Q (J/mol).
1386    pub activation_energy: f64,
1387}
1388impl ZenerHollomon {
1389    /// Create a new Zener-Hollomon parameter.
1390    pub fn new(activation_energy: f64) -> Self {
1391        Self { activation_energy }
1392    }
1393    /// Compute the Zener-Hollomon parameter Z.
1394    ///
1395    /// Z = ε_dot * exp(Q / (R * T))
1396    pub fn z_parameter(&self, strain_rate: f64, temperature: f64) -> f64 {
1397        let arr = (self.activation_energy / (GAS_CONSTANT * temperature)).exp();
1398        strain_rate * arr
1399    }
1400    /// Compute the equivalent strain rate at a different temperature
1401    /// that gives the same Z value.
1402    ///
1403    /// ε_dot_2 = Z * exp(-Q/(R*T_2))
1404    pub fn equivalent_strain_rate(&self, z: f64, temperature: f64) -> f64 {
1405        let arr = (-self.activation_energy / (GAS_CONSTANT * temperature)).exp();
1406        z * arr
1407    }
1408    /// Compute the temperature required to achieve a given Z at a given strain rate.
1409    ///
1410    /// T = Q / (R * ln(Z / ε_dot))
1411    pub fn temperature_for_z(&self, z: f64, strain_rate: f64) -> f64 {
1412        if strain_rate <= 0.0 || z <= 0.0 {
1413            return f64::NAN;
1414        }
1415        self.activation_energy / (GAS_CONSTANT * (z / strain_rate).ln())
1416    }
1417}
1418/// Norton-Bailey time-hardening creep model.
1419///
1420/// ε(t) = A * σ^n * t^m
1421///
1422/// where m is the time hardening exponent (0 < m < 1) and A, n are material
1423/// constants. The instantaneous strain rate is:
1424///
1425/// dε/dt = A * m * σ^n * t^(m-1)
1426pub struct NortonBaileyTimeHardening {
1427    /// Creep coefficient A.
1428    pub a: f64,
1429    /// Stress exponent n.
1430    pub n: f64,
1431    /// Time exponent m (0 < m ≤ 1).
1432    pub m: f64,
1433}
1434impl NortonBaileyTimeHardening {
1435    /// Create a new Norton-Bailey time-hardening model.
1436    pub fn new(a: f64, n: f64, m: f64) -> Self {
1437        Self { a, n, m }
1438    }
1439    /// Creep strain at time t.
1440    pub fn strain(&self, sigma: f64, t: f64) -> f64 {
1441        if t <= 0.0 {
1442            return 0.0;
1443        }
1444        self.a * sigma.powf(self.n) * t.powf(self.m)
1445    }
1446    /// Creep strain rate at time t.
1447    pub fn strain_rate(&self, sigma: f64, t: f64) -> f64 {
1448        if t < 1e-30 {
1449            return f64::INFINITY;
1450        }
1451        self.a * self.m * sigma.powf(self.n) * t.powf(self.m - 1.0)
1452    }
1453    /// Equivalent time method: given accumulated strain ε_0 at stress σ_0,
1454    /// find the time t_eq on the new-stress creep curve.
1455    ///
1456    /// t_eq = (ε_0 / (A * σ_new^n))^(1/m)
1457    pub fn equivalent_time(&self, accumulated_strain: f64, sigma_new: f64) -> f64 {
1458        if accumulated_strain <= 0.0 || sigma_new <= 0.0 {
1459            return 0.0;
1460        }
1461        let base = accumulated_strain / (self.a * sigma_new.powf(self.n));
1462        base.powf(1.0 / self.m)
1463    }
1464}