1use super::functions::*;
6
7pub struct ThermallyActivatedCreep {
14 pub nu0: f64,
16 pub delta_g: f64,
18 pub activation_volume: f64,
20 pub k_b: f64,
22}
23impl ThermallyActivatedCreep {
24 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 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 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 pub fn apparent_activation_energy(&self, stress: f64) -> f64 {
49 (self.delta_g - stress * self.activation_volume).max(0.0)
50 }
51}
52pub struct MonkmanGrant {
59 pub c_mg: f64,
61 pub m_exp: f64,
63}
64impl MonkmanGrant {
65 pub fn new(c_mg: f64, m_exp: f64) -> Self {
67 Self { c_mg, m_exp }
68 }
69 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 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 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}
94pub struct ChabocheKinematicHardening {
96 pub c: f64,
98 pub gamma: f64,
100}
101impl ChabocheKinematicHardening {
102 pub fn new(c: f64, gamma: f64) -> Self {
104 Self { c, gamma }
105 }
106 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 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 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#[allow(dead_code)]
128pub struct PrimaryCreepModel {
129 pub a: f64,
131 pub n: f64,
133 pub m: f64,
135}
136impl PrimaryCreepModel {
137 pub fn new(a: f64, n: f64, m: f64) -> Self {
139 Self { a, n, m }
140 }
141 pub fn strain(&self, sigma: f64, t: f64) -> f64 {
143 self.a * sigma.powf(self.n) * t.powf(self.m)
144 }
145 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 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}
164pub struct PowerLawCreep {
168 pub b: f64,
170 pub sigma_0: f64,
172 pub n: f64,
174}
175impl PowerLawCreep {
176 pub fn new(b: f64, sigma_0: f64, n: f64) -> Self {
178 Self { b, sigma_0, n }
179 }
180 pub fn strain_rate(&self, stress: f64) -> f64 {
182 self.b * (stress / self.sigma_0).powf(self.n)
183 }
184 pub fn strain_at_time(&self, stress: f64, t: f64) -> f64 {
186 self.strain_rate(stress) * t
187 }
188
189 pub fn creep_compliance(&self, t: f64) -> f64 {
196 self.b + self.sigma_0 * t.powf(self.n)
197 }
198
199 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 pub fn creep_strain(&self, stress: f64, t: f64) -> f64 {
209 stress * self.creep_compliance(t)
210 }
211}
212pub struct CreepCurve {
214 pub primary_rate: f64,
216 pub secondary_rate: f64,
218 pub tertiary_start_strain: f64,
220}
221impl CreepCurve {
222 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 pub fn strain_at_time(&self, t: f64) -> f64 {
234 self.primary_rate * t.sqrt() + self.secondary_rate * t
235 }
236 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 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 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}
283pub struct AndradeCreep {
292 pub beta: f64,
294 pub k_steady: f64,
296}
297impl AndradeCreep {
298 pub fn new(beta: f64, k_steady: f64) -> Self {
300 Self { beta, k_steady }
301 }
302 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 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 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}
330pub struct MansonHaferdParameter {
336 pub log10_ta: f64,
338 pub t_a: f64,
340}
341impl MansonHaferdParameter {
342 pub fn new(log10_ta: f64, t_a: f64) -> Self {
344 Self { log10_ta, t_a }
345 }
346 pub fn mh_parameter(&self, time_hours: f64, temperature: f64) -> f64 {
348 (time_hours.log10() - self.log10_ta) / (temperature - self.t_a)
349 }
350 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 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}
361pub struct SherbyDornParameter {
367 pub activation_energy: f64,
369}
370impl SherbyDornParameter {
371 pub fn new(activation_energy: f64) -> Self {
373 Self { activation_energy }
374 }
375 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 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 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}
407pub struct CobleCreep {
413 pub d_gb0: f64,
415 pub activation_energy: f64,
417 pub gb_width: f64,
419 pub atomic_volume: f64,
421 pub grain_size: f64,
423 pub a_c: f64,
425}
426impl CobleCreep {
427 #[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 pub fn gb_diffusion_coefficient(&self, temperature: f64) -> f64 {
447 self.d_gb0 * (-self.activation_energy / (GAS_CONSTANT * temperature)).exp()
448 }
449 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 pub fn gb_diffusivity(&self, temperature: f64) -> f64 {
460 self.d_gb0 * (-self.activation_energy / (GAS_CONSTANT * temperature)).exp()
461 }
462 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}
472pub struct NabarroHerringCreep {
479 pub d0: f64,
481 pub activation_energy: f64,
483 pub atomic_volume: f64,
485 pub grain_size: f64,
487 pub a_nh: f64,
489}
490impl NabarroHerringCreep {
491 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 pub fn diffusion_coefficient(&self, temperature: f64) -> f64 {
503 self.d0 * (-self.activation_energy / (GAS_CONSTANT * temperature)).exp()
504 }
505 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 pub fn activation_energy(&self) -> f64 {
516 self.activation_energy
517 }
518 pub fn effective_diffusivity(&self, temperature: f64) -> f64 {
520 self.d0 * (-self.activation_energy / (GAS_CONSTANT * temperature)).exp()
521 }
522 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#[allow(dead_code)]
533pub struct TertiaryCreepModel {
534 pub a: f64,
536 pub n: f64,
538 pub b_damage: f64,
540 pub chi: f64,
542 pub phi: f64,
544 pub omega: f64,
546}
547impl TertiaryCreepModel {
548 #[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 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 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 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 pub fn is_ruptured(&self) -> bool {
579 self.omega >= 0.999
580 }
581 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 pub fn reset(&mut self) {
592 self.omega = 0.0;
593 }
594}
595pub struct CreepDamage {
597 pub a: f64,
599 pub m: f64,
601 pub r: f64,
603}
604impl CreepDamage {
605 pub fn new(a: f64, m: f64, r: f64) -> Self {
607 Self { a, m, r }
608 }
609 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 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 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 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}
653pub struct LarsonMillerStress {
660 pub a_lm: f64,
662 pub b_lm: f64,
664 pub sigma_ref: f64,
666 pub c_lm: f64,
668}
669impl LarsonMillerStress {
670 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 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 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 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#[derive(Debug, Clone)]
711pub struct RuptureEnvelope {
712 pub c: f64,
714 pub curve_coeffs: [f64; 3],
717}
718impl RuptureEnvelope {
719 pub fn new(c: f64, curve_coeffs: [f64; 3]) -> Self {
721 Self { c, curve_coeffs }
722 }
723 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 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 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#[allow(dead_code)]
779pub struct ThreeStageCreep {
780 pub primary: PrimaryCreepModel,
782 pub secondary: SecondaryCreepModel,
784 pub tertiary: TertiaryCreepModel,
786 pub tertiary_onset_strain: f64,
788 pub strain: f64,
790 pub time: f64,
792}
793impl ThreeStageCreep {
794 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 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 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 pub fn is_ruptured(&self) -> bool {
839 self.tertiary.is_ruptured()
840 }
841}
842#[derive(Debug, Clone)]
846pub struct RuptureLifePrediction {
847 pub larson_miller: f64,
849 pub orr_sherby_dorn: f64,
851 pub monkman_grant: f64,
853}
854pub struct ThermalCreepIntegrator {
856 pub norton: NortonCreep,
858}
859impl ThermalCreepIntegrator {
860 pub fn new(norton: NortonCreep) -> Self {
862 Self { norton }
863 }
864 pub fn creep_strain_increment(&self, sigma: f64, temperature: f64, dt: f64) -> f64 {
866 self.norton.creep_strain_rate(sigma, temperature) * dt
867 }
868 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}
879pub struct StressRelaxation {
885 pub young_modulus: f64,
887 pub viscosity: f64,
889}
890impl StressRelaxation {
891 pub fn new(young_modulus: f64, viscosity: f64) -> Self {
893 Self {
894 young_modulus,
895 viscosity,
896 }
897 }
898 pub fn relaxation_time(&self) -> f64 {
900 self.viscosity / self.young_modulus
901 }
902 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 pub fn time_to_fraction(&self, fraction: f64) -> f64 {
911 let tau = self.relaxation_time();
912 -tau * fraction.ln()
913 }
914 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}
924pub struct CreepFatigueInteraction {
935 pub creep_intercept: f64,
937 pub fatigue_intercept: f64,
939 pub interaction_exponent: f64,
941}
942impl CreepFatigueInteraction {
943 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 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 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 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}
976pub struct MultiaxialCreep {
986 pub norton: NortonCreep,
988}
989impl MultiaxialCreep {
990 pub fn new(norton: NortonCreep) -> Self {
992 Self { norton }
993 }
994 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 pub fn mean_stress(&self, s1: f64, s2: f64, s3: f64) -> f64 {
1000 (s1 + s2 + s3) / 3.0
1001 }
1002 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 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#[derive(Debug, Clone)]
1030pub struct DeformationMechanismMap {
1031 pub t_melting: f64,
1033 pub shear_modulus: f64,
1035 pub peierls_stress_norm: f64,
1037 pub power_law_diff_boundary: f64,
1039}
1040impl DeformationMechanismMap {
1041 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 pub fn nickel() -> Self {
1057 Self::new(1728.0, 76.0e9, 3e-3, 1e-4)
1058 }
1059 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 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}
1096pub struct NortonCreep {
1100 pub a: f64,
1102 pub n: f64,
1104 pub activation_energy: f64,
1106 pub gas_constant: f64,
1108}
1109impl NortonCreep {
1110 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 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 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 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 pub fn creep_rate(&self, stress: f64, temperature: f64) -> f64 {
1146 self.creep_strain_rate(stress, temperature)
1147 }
1148
1149 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}
1161pub enum CreepStage {
1163 Primary {
1165 exponent: f64,
1167 },
1168 Secondary,
1170 Tertiary {
1172 acceleration: f64,
1174 },
1175}
1176pub struct OrrSherbyDornParameter {
1182 pub q: f64,
1184}
1185impl OrrSherbyDornParameter {
1186 pub fn new(q: f64) -> Self {
1188 Self { q }
1189 }
1190 pub fn osd_parameter(&self, time_hours: f64, temperature: f64) -> f64 {
1192 time_hours.log10() - self.q / (2.303 * GAS_CONSTANT * temperature)
1193 }
1194 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}
1200pub struct CoupledCreepDamage {
1208 pub a: f64,
1210 pub n: f64,
1212 pub q: f64,
1214 pub b_damage: f64,
1216 pub m_damage: f64,
1218 pub phi_damage: f64,
1220}
1221impl CoupledCreepDamage {
1222 #[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 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 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 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#[allow(dead_code)]
1283pub struct SecondaryCreepModel {
1284 pub norton: NortonCreep,
1286}
1287impl SecondaryCreepModel {
1288 pub fn new(a: f64, n: f64, activation_energy: f64) -> Self {
1290 Self {
1291 norton: NortonCreep::new(a, n, activation_energy),
1292 }
1293 }
1294 pub fn strain_rate(&self, stress: f64, temperature: f64) -> f64 {
1296 self.norton.creep_strain_rate(stress, temperature)
1297 }
1298 pub fn accumulated_strain(&self, stress: f64, temperature: f64, dt: f64) -> f64 {
1300 self.strain_rate(stress, temperature) * dt
1301 }
1302 pub fn viscosity(&self, stress: f64, temperature: f64) -> f64 {
1304 self.norton.effective_viscosity(stress, temperature)
1305 }
1306}
1307pub struct ViscoplasticModel {
1309 pub yield_stress: f64,
1311 pub viscosity: f64,
1313 pub hardening: f64,
1315}
1316impl ViscoplasticModel {
1317 pub fn new(yield_stress: f64, viscosity: f64, hardening: f64) -> Self {
1319 Self {
1320 yield_stress,
1321 viscosity,
1322 hardening,
1323 }
1324 }
1325 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 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 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}
1349pub struct LarsonMillerParameter {
1351 pub c_lm: f64,
1353}
1354impl LarsonMillerParameter {
1355 pub fn new(c_lm: f64) -> Self {
1357 Self { c_lm }
1358 }
1359 pub fn lm_parameter(&self, temperature: f64, time_hours: f64) -> f64 {
1361 temperature * (self.c_lm + time_hours.log10())
1362 }
1363 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 pub fn max_temperature(&self, time_hours: f64, lm_param: f64) -> f64 {
1370 lm_param / (self.c_lm + time_hours.log10())
1371 }
1372 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}
1380pub struct ZenerHollomon {
1385 pub activation_energy: f64,
1387}
1388impl ZenerHollomon {
1389 pub fn new(activation_energy: f64) -> Self {
1391 Self { activation_energy }
1392 }
1393 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 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 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}
1418pub struct NortonBaileyTimeHardening {
1427 pub a: f64,
1429 pub n: f64,
1431 pub m: f64,
1433}
1434impl NortonBaileyTimeHardening {
1435 pub fn new(a: f64, n: f64, m: f64) -> Self {
1437 Self { a, n, m }
1438 }
1439 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 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 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}