1use super::functions::R_GAS;
6#[allow(unused_imports)]
7use super::functions::*;
8
9pub struct PhaseChangeModel {
15 pub t_liquidus: f64,
17 pub t_solidus: f64,
19 pub latent_heat: f64,
21 pub cp_solid: f64,
23 pub cp_liquid: f64,
25 pub density: f64,
27}
28impl PhaseChangeModel {
29 #[allow(clippy::too_many_arguments)]
31 pub fn new(
32 t_solidus: f64,
33 t_liquidus: f64,
34 latent_heat: f64,
35 cp_solid: f64,
36 cp_liquid: f64,
37 density: f64,
38 ) -> Self {
39 assert!(t_liquidus >= t_solidus, "t_liquidus must be >= t_solidus");
40 Self {
41 t_liquidus,
42 t_solidus,
43 latent_heat,
44 cp_solid,
45 cp_liquid,
46 density,
47 }
48 }
49 pub fn liquid_fraction(&self, temp: f64) -> f64 {
51 if temp <= self.t_solidus {
52 0.0
53 } else if temp >= self.t_liquidus {
54 1.0
55 } else {
56 (temp - self.t_solidus) / (self.t_liquidus - self.t_solidus)
57 }
58 }
59 pub fn apparent_specific_heat(&self, temp: f64) -> f64 {
63 let fl = self.liquid_fraction(temp);
64 let cp_mix = fl * self.cp_liquid + (1.0 - fl) * self.cp_solid;
65 if temp > self.t_solidus && temp < self.t_liquidus {
66 let dt = (self.t_liquidus - self.t_solidus).max(f64::EPSILON);
67 cp_mix + self.latent_heat / dt
68 } else {
69 cp_mix
70 }
71 }
72 pub fn enthalpy(&self, temp: f64) -> f64 {
74 if temp <= self.t_solidus {
75 self.cp_solid * temp
76 } else if temp >= self.t_liquidus {
77 self.cp_solid * self.t_solidus
78 + self.latent_heat
79 + self.cp_liquid * (temp - self.t_liquidus)
80 } else {
81 let fl = self.liquid_fraction(temp);
82 self.cp_solid * self.t_solidus
83 + fl * self.latent_heat
84 + (fl * self.cp_liquid + (1.0 - fl) * self.cp_solid) * (temp - self.t_solidus)
85 }
86 }
87 pub fn conductivity(&self, k_solid: f64, k_liquid: f64, temp: f64) -> f64 {
89 let fl = self.liquid_fraction(temp);
90 fl * k_liquid + (1.0 - fl) * k_solid
91 }
92}
93pub struct ThermalStressCoupling {
97 pub n_nodes: usize,
99 pub temperature: Vec<f64>,
101 pub t_ref: f64,
103 pub alpha: f64,
105 pub young_modulus: f64,
107 pub poisson_ratio: f64,
109}
110impl ThermalStressCoupling {
111 pub fn new(
113 n_nodes: usize,
114 t_init: f64,
115 t_ref: f64,
116 alpha: f64,
117 young_modulus: f64,
118 poisson_ratio: f64,
119 ) -> Self {
120 Self {
121 n_nodes,
122 temperature: vec![t_init; n_nodes],
123 t_ref,
124 alpha,
125 young_modulus,
126 poisson_ratio,
127 }
128 }
129 pub fn thermal_strain(&self, node: usize) -> f64 {
131 self.alpha * (self.temperature[node] - self.t_ref)
132 }
133 pub fn thermal_stress(&self, node: usize) -> f64 {
137 -self.young_modulus * self.alpha * (self.temperature[node] - self.t_ref)
138 / (1.0 - self.poisson_ratio)
139 }
140 pub fn max_thermal_stress(&self) -> f64 {
142 (0..self.n_nodes)
143 .map(|i| self.thermal_stress(i).abs())
144 .fold(0.0_f64, f64::max)
145 }
146 pub fn has_fracture(&self, fracture_stress: f64) -> bool {
148 self.max_thermal_stress() > fracture_stress
149 }
150}
151#[allow(dead_code)]
159#[derive(Debug, Clone)]
160pub struct HeatAffectedZone {
161 pub heat_input: f64,
163 pub rho: f64,
165 pub cp: f64,
167 pub k: f64,
169 pub welding_speed: f64,
171 pub t_preheat: f64,
173 pub t_melt: f64,
175}
176impl HeatAffectedZone {
177 #[allow(clippy::too_many_arguments)]
179 pub fn new(
180 heat_input: f64,
181 rho: f64,
182 cp: f64,
183 k: f64,
184 welding_speed: f64,
185 t_preheat: f64,
186 t_melt: f64,
187 ) -> Self {
188 Self {
189 heat_input,
190 rho,
191 cp,
192 k,
193 welding_speed,
194 t_preheat,
195 t_melt,
196 }
197 }
198 pub fn diffusivity(&self) -> f64 {
200 self.k / (self.rho * self.cp)
201 }
202 pub fn peak_temperature_thick_plate(&self, y: f64) -> f64 {
209 let alpha = self.diffusivity();
210 let coeff = self.heat_input * self.welding_speed / (2.0 * std::f64::consts::PI * alpha);
211 self.t_preheat
212 + coeff / (2.0 * std::f64::consts::PI) * (-self.welding_speed * y / (2.0 * alpha)).exp()
213 }
214 pub fn haz_halfwidth(&self, t_crit: f64) -> f64 {
219 let alpha = self.diffusivity();
220 if t_crit <= self.t_preheat {
221 return f64::INFINITY;
222 }
223 let dt = t_crit - self.t_preheat;
224 self.heat_input / (std::f64::consts::E * std::f64::consts::PI * self.rho * self.cp * dt)
225 * (alpha / self.welding_speed).sqrt()
226 }
227 pub fn cooling_rate_centreline(&self, t_crit: f64) -> f64 {
231 2.0 * std::f64::consts::PI * self.k * (t_crit - self.t_preheat).powi(2) / self.heat_input
232 }
233 pub fn forms_martensite(&self, critical_cooling_rate: f64, t_crit: f64) -> bool {
236 self.cooling_rate_centreline(t_crit) >= critical_cooling_rate
237 }
238}
239#[allow(dead_code)]
247#[derive(Debug, Clone)]
248pub struct JohnsonCookModel {
249 pub a: f64,
251 pub b: f64,
253 pub n: f64,
255 pub c: f64,
257 pub m: f64,
259 pub eps_dot0: f64,
261 pub t_room: f64,
263 pub t_melt: f64,
265}
266impl JohnsonCookModel {
267 #[allow(clippy::too_many_arguments)]
269 pub fn new(
270 a: f64,
271 b: f64,
272 n: f64,
273 c: f64,
274 m: f64,
275 eps_dot0: f64,
276 t_room: f64,
277 t_melt: f64,
278 ) -> Self {
279 Self {
280 a,
281 b,
282 n,
283 c,
284 m,
285 eps_dot0,
286 t_room,
287 t_melt,
288 }
289 }
290 pub fn aisi_4340() -> Self {
292 Self::new(792e6, 510e6, 0.26, 0.014, 1.03, 1.0, 293.0, 1793.0)
293 }
294 pub fn homologous_temperature(&self, temperature: f64) -> f64 {
296 if temperature <= self.t_room {
297 return 0.0;
298 }
299 if temperature >= self.t_melt {
300 return 1.0;
301 }
302 (temperature - self.t_room) / (self.t_melt - self.t_room)
303 }
304 #[allow(clippy::too_many_arguments)]
311 #[allow(non_snake_case)]
312 pub fn flow_stress(&self, eps: f64, eps_dot: f64, temp: f64) -> f64 {
313 let term_A = self.a + self.b * eps.max(0.0).powf(self.n);
314 let eps_dot_ratio = (eps_dot / self.eps_dot0).max(1.0);
315 let term_B = 1.0 + self.c * eps_dot_ratio.ln();
316 let t_star = self.homologous_temperature(temp);
317 let term_C = 1.0 - t_star.powf(self.m);
318 term_A * term_B * term_C
319 }
320 pub fn isothermal_flow_stress(&self, eps: f64) -> f64 {
322 self.flow_stress(eps, self.eps_dot0, self.t_room)
323 }
324 pub fn strain_rate_sensitivity(&self, eps_dot: f64, eps: f64, temp: f64) -> f64 {
326 let sig_ref = self.flow_stress(eps, self.eps_dot0, temp);
327 if sig_ref < f64::EPSILON {
328 return 1.0;
329 }
330 self.flow_stress(eps, eps_dot, temp) / sig_ref
331 }
332}
333pub struct ThermalInterfaceResistance {
338 pub resistance: f64,
340}
341impl ThermalInterfaceResistance {
342 pub fn new(resistance: f64) -> Self {
344 Self { resistance }
345 }
346 pub fn metal_metal() -> Self {
348 Self::new(1e-4)
349 }
350 pub fn metal_ceramic() -> Self {
352 Self::new(1e-3)
353 }
354 pub fn semiconductor() -> Self {
356 Self::new(1e-8)
357 }
358 pub fn heat_flux(&self, delta_t: f64) -> f64 {
360 delta_t / self.resistance
361 }
362 pub fn temperature_drop(&self, heat_flux: f64) -> f64 {
364 heat_flux * self.resistance
365 }
366 pub fn conductance(&self) -> f64 {
368 1.0 / self.resistance
369 }
370}
371pub struct ThermalConductivityTensor {
376 pub k: [[f64; 3]; 3],
378}
379impl ThermalConductivityTensor {
380 pub fn new(k: [[f64; 3]; 3]) -> Self {
382 Self { k }
383 }
384 pub fn isotropic(k: f64) -> Self {
386 Self {
387 k: [[k, 0.0, 0.0], [0.0, k, 0.0], [0.0, 0.0, k]],
388 }
389 }
390 pub fn orthotropic(kx: f64, ky: f64, kz: f64) -> Self {
392 Self {
393 k: [[kx, 0.0, 0.0], [0.0, ky, 0.0], [0.0, 0.0, kz]],
394 }
395 }
396 #[allow(clippy::needless_range_loop)]
403 pub fn heat_flux(&self, grad_t: [f64; 3]) -> [f64; 3] {
404 let mut q = [0.0_f64; 3];
405 for i in 0..3 {
406 for j in 0..3 {
407 q[i] -= self.k[i][j] * grad_t[j];
408 }
409 }
410 q
411 }
412 pub fn effective_conductivity(&self) -> f64 {
414 (self.k[0][0] * self.k[1][1] * self.k[2][2]).powf(1.0 / 3.0)
415 }
416 pub fn is_symmetric(&self, tol: f64) -> bool {
418 for i in 0..3 {
419 for j in 0..3 {
420 if (self.k[i][j] - self.k[j][i]).abs() > tol {
421 return false;
422 }
423 }
424 }
425 true
426 }
427}
428pub struct AblationModel {
434 pub t_ablation: f64,
436 pub heat_of_ablation: f64,
438 pub density: f64,
440 pub arrhenius_a: f64,
442 pub activation_temp: f64,
444}
445impl AblationModel {
446 pub fn new(
448 t_ablation: f64,
449 heat_of_ablation: f64,
450 density: f64,
451 arrhenius_a: f64,
452 activation_temp: f64,
453 ) -> Self {
454 Self {
455 t_ablation,
456 heat_of_ablation,
457 density,
458 arrhenius_a,
459 activation_temp,
460 }
461 }
462 pub fn recession_rate(&self, temp: f64) -> f64 {
466 if temp <= self.t_ablation {
467 return 0.0;
468 }
469 self.arrhenius_a * (-self.activation_temp / temp).exp()
470 }
471 pub fn mass_loss_rate(&self, temp: f64) -> f64 {
473 self.density * self.recession_rate(temp)
474 }
475 pub fn ablative_heat_flux(&self, temp: f64) -> f64 {
477 self.heat_of_ablation * self.mass_loss_rate(temp)
478 }
479 pub fn net_heat_flux(&self, applied_flux: f64, temp: f64) -> f64 {
483 applied_flux - self.ablative_heat_flux(temp)
484 }
485}
486#[allow(dead_code)]
495#[derive(Debug, Clone)]
496pub struct EnthalpyMethod {
497 pub rho: f64,
499 pub cp: f64,
501 pub latent_heat: f64,
503 pub t_solidus: f64,
505 pub t_liquidus: f64,
507 pub temperature: f64,
509 pub enthalpy: f64,
511}
512impl EnthalpyMethod {
513 pub fn new(rho: f64, cp: f64, latent_heat: f64, t_solidus: f64, t_liquidus: f64) -> Self {
515 let t0 = t_solidus - 1.0;
516 let h0 = rho * cp * t0;
517 Self {
518 rho,
519 cp,
520 latent_heat,
521 t_solidus,
522 t_liquidus,
523 temperature: t0,
524 enthalpy: h0,
525 }
526 }
527 pub fn liquid_fraction(&self, temp: f64) -> f64 {
529 if temp <= self.t_solidus {
530 0.0
531 } else if temp >= self.t_liquidus {
532 1.0
533 } else {
534 (temp - self.t_solidus) / (self.t_liquidus - self.t_solidus)
535 }
536 }
537 pub fn enthalpy_at(&self, temp: f64) -> f64 {
539 self.rho * self.cp * temp + self.rho * self.latent_heat * self.liquid_fraction(temp)
540 }
541 pub fn temperature_from_enthalpy(&self, h: f64) -> f64 {
543 let mut t = h / (self.rho * self.cp);
544 for _ in 0..50 {
545 let h_t = self.enthalpy_at(t);
546 let dh_dt = self.effective_cp(t);
547 let dt = (h - h_t) / dh_dt.max(f64::EPSILON);
548 t += dt;
549 if dt.abs() < 1e-6 {
550 break;
551 }
552 }
553 t
554 }
555 pub fn effective_cp(&self, temp: f64) -> f64 {
557 if temp > self.t_solidus && temp < self.t_liquidus {
558 let dfl_dt = 1.0 / (self.t_liquidus - self.t_solidus);
559 self.rho * (self.cp + self.latent_heat * dfl_dt)
560 } else {
561 self.rho * self.cp
562 }
563 }
564 pub fn apply_heat_flux(&mut self, q: f64, dt: f64) {
566 self.enthalpy += q * dt;
567 self.temperature = self.temperature_from_enthalpy(self.enthalpy);
568 }
569 pub fn is_mushy(&self) -> bool {
571 self.temperature > self.t_solidus && self.temperature < self.t_liquidus
572 }
573}
574pub struct TemperatureDependentSpecificHeat {
576 pub temps: Vec<f64>,
578 pub values: Vec<f64>,
580}
581impl TemperatureDependentSpecificHeat {
582 pub fn new(temps: Vec<f64>, values: Vec<f64>) -> Self {
584 assert_eq!(temps.len(), values.len());
585 Self { temps, values }
586 }
587 pub fn cp_at(&self, temperature: f64) -> f64 {
589 let n = self.temps.len();
590 if n == 0 {
591 return 0.0;
592 }
593 if n == 1 || temperature <= self.temps[0] {
594 return self.values[0];
595 }
596 if temperature >= self.temps[n - 1] {
597 return self.values[n - 1];
598 }
599 let idx = self
600 .temps
601 .partition_point(|&t| t <= temperature)
602 .saturating_sub(1);
603 let t0 = self.temps[idx];
604 let t1 = self.temps[idx + 1];
605 let c0 = self.values[idx];
606 let c1 = self.values[idx + 1];
607 let frac = (temperature - t0) / (t1 - t0);
608 c0 + frac * (c1 - c0)
609 }
610}
611pub struct ThermalShockResistance {
617 pub fracture_strength: f64,
619 pub young_modulus: f64,
621 pub poisson_ratio: f64,
623 pub thermal_expansion: f64,
625 pub thermal_conductivity: f64,
627}
628impl ThermalShockResistance {
629 #[allow(clippy::too_many_arguments)]
631 pub fn new(
632 fracture_strength: f64,
633 young_modulus: f64,
634 poisson_ratio: f64,
635 thermal_expansion: f64,
636 thermal_conductivity: f64,
637 ) -> Self {
638 Self {
639 fracture_strength,
640 young_modulus,
641 poisson_ratio,
642 thermal_expansion,
643 thermal_conductivity,
644 }
645 }
646 pub fn r_parameter(&self) -> f64 {
650 self.fracture_strength * (1.0 - self.poisson_ratio)
651 / (self.young_modulus * self.thermal_expansion)
652 }
653 pub fn r_prime(&self) -> f64 {
655 self.r_parameter() * self.thermal_conductivity
656 }
657 pub fn r_double_prime(&self) -> f64 {
659 self.fracture_strength * self.fracture_strength * (1.0 - self.poisson_ratio)
660 / (self.young_modulus * self.thermal_expansion * self.thermal_expansion)
661 }
662 pub fn will_fail(&self, delta_t: f64) -> bool {
664 delta_t.abs() > self.r_parameter()
665 }
666}
667pub struct TemperatureDependentConductivity {
669 pub temps: Vec<f64>,
671 pub conductivities: Vec<f64>,
673}
674impl TemperatureDependentConductivity {
675 pub fn new(temps: Vec<f64>, conductivities: Vec<f64>) -> Self {
677 assert_eq!(temps.len(), conductivities.len());
678 Self {
679 temps,
680 conductivities,
681 }
682 }
683 pub fn conductivity_at(&self, temperature: f64) -> f64 {
685 let n = self.temps.len();
686 if n == 0 {
687 return 0.0;
688 }
689 if n == 1 || temperature <= self.temps[0] {
690 return self.conductivities[0];
691 }
692 if temperature >= self.temps[n - 1] {
693 return self.conductivities[n - 1];
694 }
695 let idx = self
696 .temps
697 .partition_point(|&t| t <= temperature)
698 .saturating_sub(1);
699 let t0 = self.temps[idx];
700 let t1 = self.temps[idx + 1];
701 let k0 = self.conductivities[idx];
702 let k1 = self.conductivities[idx + 1];
703 let frac = (temperature - t0) / (t1 - t0);
704 k0 + frac * (k1 - k0)
705 }
706 pub fn mean_conductivity(&self, t_low: f64, t_high: f64) -> f64 {
708 if (t_high - t_low).abs() < f64::EPSILON {
709 return self.conductivity_at(t_low);
710 }
711 let mut pts: Vec<f64> = vec![t_low];
712 for &t in &self.temps {
713 if t > t_low && t < t_high {
714 pts.push(t);
715 }
716 }
717 pts.push(t_high);
718 pts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
719 pts.dedup_by(|a, b| (*a - *b).abs() < f64::EPSILON);
720 let mut integral = 0.0;
721 for i in 0..pts.len() - 1 {
722 let ta = pts[i];
723 let tb = pts[i + 1];
724 let ka = self.conductivity_at(ta);
725 let kb = self.conductivity_at(tb);
726 integral += 0.5 * (ka + kb) * (tb - ta);
727 }
728 integral / (t_high - t_low)
729 }
730}
731pub struct DebyeModel {
738 pub theta_d: f64,
740 pub n_atoms: f64,
742}
743impl DebyeModel {
744 pub fn new(theta_d: f64, n_atoms: f64) -> Self {
746 Self { theta_d, n_atoms }
747 }
748 pub fn molar_cv(&self, temperature: f64) -> f64 {
752 if temperature < 1e-10 {
753 return 0.0;
754 }
755 let x_max = self.theta_d / temperature;
756 let n_steps = 200;
757 let dx = x_max / (n_steps as f64);
758 let integrand = |x: f64| -> f64 {
759 if x < 1e-12 {
760 return 0.0;
761 }
762 if x > 500.0 {
763 return 0.0;
764 }
765 let ex = x.exp();
766 x.powi(4) * ex / (ex - 1.0).powi(2)
767 };
768 let mut sum = integrand(0.0) + integrand(x_max);
769 for i in 1..n_steps {
770 let x = i as f64 * dx;
771 let weight = if i % 2 == 0 { 2.0 } else { 4.0 };
772 sum += weight * integrand(x);
773 }
774 let integral = sum * dx / 3.0;
775 let ratio = temperature / self.theta_d;
776 9.0 * self.n_atoms * R_GAS * ratio.powi(3) * integral
777 }
778 pub fn dulong_petit(&self) -> f64 {
780 3.0 * self.n_atoms * R_GAS
781 }
782}
783#[allow(dead_code)]
790#[derive(Debug, Clone)]
791pub struct ThermalExpansion {
792 pub alpha_x: f64,
794 pub alpha_y: f64,
796 pub alpha_z: f64,
798 pub t_ref: f64,
800}
801#[allow(dead_code)]
802impl ThermalExpansion {
803 pub fn isotropic(alpha: f64, t_ref: f64) -> Self {
809 Self {
810 alpha_x: alpha,
811 alpha_y: alpha,
812 alpha_z: alpha,
813 t_ref,
814 }
815 }
816 pub fn orthotropic(alpha_x: f64, alpha_y: f64, alpha_z: f64, t_ref: f64) -> Self {
822 Self {
823 alpha_x,
824 alpha_y,
825 alpha_z,
826 t_ref,
827 }
828 }
829 pub fn strain_x(&self, temperature: f64) -> f64 {
831 self.alpha_x * (temperature - self.t_ref)
832 }
833 pub fn strain_y(&self, temperature: f64) -> f64 {
835 self.alpha_y * (temperature - self.t_ref)
836 }
837 pub fn strain_z(&self, temperature: f64) -> f64 {
839 self.alpha_z * (temperature - self.t_ref)
840 }
841 pub fn compute_volumetric_strain(&self, temperature: f64) -> f64 {
857 let delta_t = temperature - self.t_ref;
858 (self.alpha_x + self.alpha_y + self.alpha_z) * delta_t
859 }
860 pub fn compute_volumetric_strain_exact(&self, temperature: f64) -> f64 {
864 let ex = 1.0 + self.strain_x(temperature);
865 let ey = 1.0 + self.strain_y(temperature);
866 let ez = 1.0 + self.strain_z(temperature);
867 ex * ey * ez - 1.0
868 }
869 pub fn strain_tensor(&self, temperature: f64) -> [f64; 3] {
871 [
872 self.strain_x(temperature),
873 self.strain_y(temperature),
874 self.strain_z(temperature),
875 ]
876 }
877}
878#[allow(dead_code)]
886#[derive(Debug, Clone)]
887pub struct ThermalShockParam {
888 pub sigma_f: f64,
890 pub young_modulus: f64,
892 pub nu: f64,
894 pub alpha: f64,
896 pub k: f64,
898 pub k_ic: f64,
900}
901impl ThermalShockParam {
902 #[allow(clippy::too_many_arguments)]
904 pub fn new(sigma_f: f64, young_modulus: f64, nu: f64, alpha: f64, k: f64, k_ic: f64) -> Self {
905 Self {
906 sigma_f,
907 young_modulus,
908 nu,
909 alpha,
910 k,
911 k_ic,
912 }
913 }
914 pub fn r_first(&self) -> f64 {
917 self.sigma_f * (1.0 - self.nu) / (self.young_modulus * self.alpha)
918 }
919 pub fn r_second(&self) -> f64 {
922 self.k * self.r_first()
923 }
924 pub fn r_third(&self) -> f64 {
927 let g_f = self.k_ic * self.k_ic / self.young_modulus;
928 g_f / (self.young_modulus * self.alpha * self.alpha)
929 }
930 pub fn r_hasselman(&self) -> f64 {
933 let g_f = self.k_ic * self.k_ic / self.young_modulus;
934 self.young_modulus * g_f / (self.sigma_f * self.sigma_f * (1.0 - self.nu))
935 }
936}
937pub struct EinsteinModel {
941 pub theta_e: f64,
943 pub n_atoms: f64,
945}
946impl EinsteinModel {
947 pub fn new(theta_e: f64, n_atoms: f64) -> Self {
949 Self { theta_e, n_atoms }
950 }
951 pub fn molar_cv(&self, temperature: f64) -> f64 {
953 if temperature < 1e-10 {
954 return 0.0;
955 }
956 let x = self.theta_e / temperature;
957 if x > 500.0 {
958 return 0.0;
959 }
960 let ex = x.exp();
961 3.0 * self.n_atoms * R_GAS * x * x * ex / (ex - 1.0).powi(2)
962 }
963}
964pub struct HeatConduction1D {
966 pub n_nodes: usize,
968 pub length: f64,
970 pub temperature: Vec<f64>,
972 pub material: ThermalMaterial,
974}
975impl HeatConduction1D {
976 pub fn new(n_nodes: usize, length: f64, t_init: f64, material: ThermalMaterial) -> Self {
978 assert!(n_nodes >= 2, "need at least 2 nodes");
979 Self {
980 n_nodes,
981 length,
982 temperature: vec![t_init; n_nodes],
983 material,
984 }
985 }
986 pub fn set_temperature_bc(&mut self, t_left: f64, t_right: f64) {
988 self.temperature[0] = t_left;
989 self.temperature[self.n_nodes - 1] = t_right;
990 }
991 pub fn step_explicit(&mut self, dt: f64) -> bool {
993 let dx = self.length / (self.n_nodes - 1) as f64;
994 let alpha = self.material.diffusivity();
995 let r = alpha * dt / (dx * dx);
996 if r > 0.5 {
997 return false;
998 }
999 let old = self.temperature.clone();
1000 for i in 1..self.n_nodes - 1 {
1001 self.temperature[i] = old[i] + r * (old[i + 1] - 2.0 * old[i] + old[i - 1]);
1002 }
1003 true
1004 }
1005 pub fn step_explicit_with_source(&mut self, dt: f64, source: f64) -> bool {
1007 let dx = self.length / (self.n_nodes - 1) as f64;
1008 let alpha = self.material.diffusivity();
1009 let r = alpha * dt / (dx * dx);
1010 if r > 0.5 {
1011 return false;
1012 }
1013 let old = self.temperature.clone();
1014 let rho_cp = self.material.volumetric_heat_capacity();
1015 for i in 1..self.n_nodes - 1 {
1016 self.temperature[i] =
1017 old[i] + r * (old[i + 1] - 2.0 * old[i] + old[i - 1]) + source * dt / rho_cp;
1018 }
1019 true
1020 }
1021 pub fn critical_dt(&self) -> f64 {
1023 let dx = self.length / (self.n_nodes - 1) as f64;
1024 let alpha = self.material.diffusivity();
1025 (dx * dx) / (2.0 * alpha)
1026 }
1027 pub fn steady_state_temperature(&self, t_left: f64, t_right: f64) -> Vec<f64> {
1029 (0..self.n_nodes)
1030 .map(|i| {
1031 let frac = i as f64 / (self.n_nodes - 1) as f64;
1032 t_left + frac * (t_right - t_left)
1033 })
1034 .collect()
1035 }
1036 pub fn heat_flux(&self) -> Vec<f64> {
1038 let dx = self.length / (self.n_nodes - 1) as f64;
1039 let k = self.material.thermal_conductivity;
1040 (1..self.n_nodes - 1)
1041 .map(|i| {
1042 let dt_dx = (self.temperature[i + 1] - self.temperature[i - 1]) / (2.0 * dx);
1043 -k * dt_dx
1044 })
1045 .collect()
1046 }
1047 pub fn total_heat_content(&self) -> f64 {
1049 let dx = self.length / (self.n_nodes - 1) as f64;
1050 let rho_cp = self.material.volumetric_heat_capacity();
1051 self.temperature.iter().map(|&t| rho_cp * t * dx).sum()
1052 }
1053}
1054pub struct ThermalMaterial {
1056 pub thermal_conductivity: f64,
1058 pub specific_heat: f64,
1060 pub density: f64,
1062 pub thermal_expansion: f64,
1064}
1065impl ThermalMaterial {
1066 pub fn new(k: f64, cp: f64, rho: f64, alpha: f64) -> Self {
1068 Self {
1069 thermal_conductivity: k,
1070 specific_heat: cp,
1071 density: rho,
1072 thermal_expansion: alpha,
1073 }
1074 }
1075 pub fn steel() -> Self {
1077 Self::new(50.0, 500.0, 7850.0, 12e-6)
1078 }
1079 pub fn aluminum() -> Self {
1081 Self::new(237.0, 900.0, 2700.0, 23e-6)
1082 }
1083 pub fn copper() -> Self {
1085 Self::new(401.0, 385.0, 8960.0, 17e-6)
1086 }
1087 pub fn concrete() -> Self {
1089 Self::new(1.5, 880.0, 2300.0, 10e-6)
1090 }
1091 pub fn silicon_carbide() -> Self {
1093 Self::new(120.0, 750.0, 3210.0, 4.0e-6)
1094 }
1095 pub fn alumina() -> Self {
1097 Self::new(30.0, 880.0, 3950.0, 8.0e-6)
1098 }
1099 pub fn diffusivity(&self) -> f64 {
1101 self.thermal_conductivity / (self.density * self.specific_heat)
1102 }
1103 pub fn effusivity(&self) -> f64 {
1105 (self.thermal_conductivity * self.density * self.specific_heat).sqrt()
1106 }
1107 pub fn thermal_strain(&self, delta_t: f64) -> f64 {
1109 self.thermal_expansion * delta_t
1110 }
1111 pub fn thermal_stress(&self, delta_t: f64, young_modulus: f64, poisson_ratio: f64) -> f64 {
1115 -young_modulus * self.thermal_expansion * delta_t / (1.0 - poisson_ratio)
1116 }
1117 pub fn volumetric_heat_capacity(&self) -> f64 {
1119 self.density * self.specific_heat
1120 }
1121 pub fn penetration_depth(&self, t: f64) -> f64 {
1125 (4.0 * self.diffusivity() * t).sqrt()
1126 }
1127 pub fn compute_thermal_shock_resistance(
1143 &self,
1144 fracture_strength: f64,
1145 young_modulus: f64,
1146 poisson_ratio: f64,
1147 ) -> f64 {
1148 fracture_strength * (1.0 - poisson_ratio) / (young_modulus * self.thermal_expansion)
1149 }
1150 pub fn compute_wiedemann_franz(&self, temperature: f64, electrical_resistivity: f64) -> f64 {
1166 const LORENZ: f64 = 2.4427e-8_f64;
1167 LORENZ * temperature / electrical_resistivity
1168 }
1169}
1170pub struct NewtonianCooling {
1172 pub mass: f64,
1174 pub specific_heat: f64,
1176 pub heat_transfer_coeff: f64,
1178 pub surface_area: f64,
1180 pub temperature: f64,
1182}
1183impl NewtonianCooling {
1184 pub fn new(mass: f64, cp: f64, h: f64, area: f64, t_init: f64) -> Self {
1186 Self {
1187 mass,
1188 specific_heat: cp,
1189 heat_transfer_coeff: h,
1190 surface_area: area,
1191 temperature: t_init,
1192 }
1193 }
1194 pub fn time_constant(&self) -> f64 {
1196 self.mass * self.specific_heat / (self.heat_transfer_coeff * self.surface_area)
1197 }
1198 pub fn temperature_at_time(&self, t_inf: f64, time: f64) -> f64 {
1200 let tau = self.time_constant();
1201 t_inf + (self.temperature - t_inf) * (-time / tau).exp()
1202 }
1203 pub fn step(&mut self, t_inf: f64, dt: f64) {
1205 let tau = self.time_constant();
1206 self.temperature += -dt / tau * (self.temperature - t_inf);
1207 }
1208 pub fn time_to_temperature(&self, t_inf: f64, t_target: f64) -> Option<f64> {
1210 let tau = self.time_constant();
1211 let denom = self.temperature - t_inf;
1212 if denom.abs() < f64::EPSILON {
1213 return None;
1214 }
1215 let ratio = (t_target - t_inf) / denom;
1216 if ratio <= 0.0 {
1217 return None;
1218 }
1219 Some(-tau * ratio.ln())
1220 }
1221 pub fn biot_number(&self, characteristic_length: f64, thermal_conductivity: f64) -> f64 {
1226 self.heat_transfer_coeff * characteristic_length / thermal_conductivity
1227 }
1228}
1229#[allow(dead_code)]
1237#[derive(Debug, Clone)]
1238pub struct ThermalFatigue {
1239 pub c_cm: f64,
1241 pub beta: f64,
1243 pub alpha: f64,
1245 pub young_modulus: f64,
1247 pub damage: f64,
1249}
1250impl ThermalFatigue {
1251 pub fn new(c_cm: f64, beta: f64, alpha: f64, young_modulus: f64) -> Self {
1253 Self {
1254 c_cm,
1255 beta,
1256 alpha,
1257 young_modulus,
1258 damage: 0.0,
1259 }
1260 }
1261 pub fn plastic_strain_range(&self, delta_t: f64) -> f64 {
1265 self.alpha * delta_t.abs()
1266 }
1267 pub fn fatigue_life(&self, delta_t: f64) -> f64 {
1271 let deps = self.plastic_strain_range(delta_t);
1272 if deps < f64::EPSILON {
1273 return f64::INFINITY;
1274 }
1275 self.c_cm / deps.powf(self.beta)
1276 }
1277 pub fn accumulate(&mut self, delta_t: f64, n_cycles: f64) {
1279 let n_f = self.fatigue_life(delta_t);
1280 if n_f.is_finite() {
1281 self.damage = (self.damage + n_cycles / n_f).min(1.0);
1282 }
1283 }
1284 pub fn is_failed(&self) -> bool {
1286 self.damage >= 1.0 - f64::EPSILON
1287 }
1288 pub fn critical_delta_t(&self, n_cycles: f64) -> f64 {
1292 if n_cycles <= 0.0 {
1293 return 0.0;
1294 }
1295 let deps = (self.c_cm / n_cycles).powf(1.0 / self.beta);
1296 deps / self.alpha
1297 }
1298}