use super::functions::R_GAS;
#[allow(unused_imports)]
use super::functions::*;
pub struct PhaseChangeModel {
pub t_liquidus: f64,
pub t_solidus: f64,
pub latent_heat: f64,
pub cp_solid: f64,
pub cp_liquid: f64,
pub density: f64,
}
impl PhaseChangeModel {
#[allow(clippy::too_many_arguments)]
pub fn new(
t_solidus: f64,
t_liquidus: f64,
latent_heat: f64,
cp_solid: f64,
cp_liquid: f64,
density: f64,
) -> Self {
assert!(t_liquidus >= t_solidus, "t_liquidus must be >= t_solidus");
Self {
t_liquidus,
t_solidus,
latent_heat,
cp_solid,
cp_liquid,
density,
}
}
pub fn liquid_fraction(&self, temp: f64) -> f64 {
if temp <= self.t_solidus {
0.0
} else if temp >= self.t_liquidus {
1.0
} else {
(temp - self.t_solidus) / (self.t_liquidus - self.t_solidus)
}
}
pub fn apparent_specific_heat(&self, temp: f64) -> f64 {
let fl = self.liquid_fraction(temp);
let cp_mix = fl * self.cp_liquid + (1.0 - fl) * self.cp_solid;
if temp > self.t_solidus && temp < self.t_liquidus {
let dt = (self.t_liquidus - self.t_solidus).max(f64::EPSILON);
cp_mix + self.latent_heat / dt
} else {
cp_mix
}
}
pub fn enthalpy(&self, temp: f64) -> f64 {
if temp <= self.t_solidus {
self.cp_solid * temp
} else if temp >= self.t_liquidus {
self.cp_solid * self.t_solidus
+ self.latent_heat
+ self.cp_liquid * (temp - self.t_liquidus)
} else {
let fl = self.liquid_fraction(temp);
self.cp_solid * self.t_solidus
+ fl * self.latent_heat
+ (fl * self.cp_liquid + (1.0 - fl) * self.cp_solid) * (temp - self.t_solidus)
}
}
pub fn conductivity(&self, k_solid: f64, k_liquid: f64, temp: f64) -> f64 {
let fl = self.liquid_fraction(temp);
fl * k_liquid + (1.0 - fl) * k_solid
}
}
pub struct ThermalStressCoupling {
pub n_nodes: usize,
pub temperature: Vec<f64>,
pub t_ref: f64,
pub alpha: f64,
pub young_modulus: f64,
pub poisson_ratio: f64,
}
impl ThermalStressCoupling {
pub fn new(
n_nodes: usize,
t_init: f64,
t_ref: f64,
alpha: f64,
young_modulus: f64,
poisson_ratio: f64,
) -> Self {
Self {
n_nodes,
temperature: vec![t_init; n_nodes],
t_ref,
alpha,
young_modulus,
poisson_ratio,
}
}
pub fn thermal_strain(&self, node: usize) -> f64 {
self.alpha * (self.temperature[node] - self.t_ref)
}
pub fn thermal_stress(&self, node: usize) -> f64 {
-self.young_modulus * self.alpha * (self.temperature[node] - self.t_ref)
/ (1.0 - self.poisson_ratio)
}
pub fn max_thermal_stress(&self) -> f64 {
(0..self.n_nodes)
.map(|i| self.thermal_stress(i).abs())
.fold(0.0_f64, f64::max)
}
pub fn has_fracture(&self, fracture_stress: f64) -> bool {
self.max_thermal_stress() > fracture_stress
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HeatAffectedZone {
pub heat_input: f64,
pub rho: f64,
pub cp: f64,
pub k: f64,
pub welding_speed: f64,
pub t_preheat: f64,
pub t_melt: f64,
}
impl HeatAffectedZone {
#[allow(clippy::too_many_arguments)]
pub fn new(
heat_input: f64,
rho: f64,
cp: f64,
k: f64,
welding_speed: f64,
t_preheat: f64,
t_melt: f64,
) -> Self {
Self {
heat_input,
rho,
cp,
k,
welding_speed,
t_preheat,
t_melt,
}
}
pub fn diffusivity(&self) -> f64 {
self.k / (self.rho * self.cp)
}
pub fn peak_temperature_thick_plate(&self, y: f64) -> f64 {
let alpha = self.diffusivity();
let coeff = self.heat_input * self.welding_speed / (2.0 * std::f64::consts::PI * alpha);
self.t_preheat
+ coeff / (2.0 * std::f64::consts::PI) * (-self.welding_speed * y / (2.0 * alpha)).exp()
}
pub fn haz_halfwidth(&self, t_crit: f64) -> f64 {
let alpha = self.diffusivity();
if t_crit <= self.t_preheat {
return f64::INFINITY;
}
let dt = t_crit - self.t_preheat;
self.heat_input / (std::f64::consts::E * std::f64::consts::PI * self.rho * self.cp * dt)
* (alpha / self.welding_speed).sqrt()
}
pub fn cooling_rate_centreline(&self, t_crit: f64) -> f64 {
2.0 * std::f64::consts::PI * self.k * (t_crit - self.t_preheat).powi(2) / self.heat_input
}
pub fn forms_martensite(&self, critical_cooling_rate: f64, t_crit: f64) -> bool {
self.cooling_rate_centreline(t_crit) >= critical_cooling_rate
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct JohnsonCookModel {
pub a: f64,
pub b: f64,
pub n: f64,
pub c: f64,
pub m: f64,
pub eps_dot0: f64,
pub t_room: f64,
pub t_melt: f64,
}
impl JohnsonCookModel {
#[allow(clippy::too_many_arguments)]
pub fn new(
a: f64,
b: f64,
n: f64,
c: f64,
m: f64,
eps_dot0: f64,
t_room: f64,
t_melt: f64,
) -> Self {
Self {
a,
b,
n,
c,
m,
eps_dot0,
t_room,
t_melt,
}
}
pub fn aisi_4340() -> Self {
Self::new(792e6, 510e6, 0.26, 0.014, 1.03, 1.0, 293.0, 1793.0)
}
pub fn homologous_temperature(&self, temperature: f64) -> f64 {
if temperature <= self.t_room {
return 0.0;
}
if temperature >= self.t_melt {
return 1.0;
}
(temperature - self.t_room) / (self.t_melt - self.t_room)
}
#[allow(clippy::too_many_arguments)]
#[allow(non_snake_case)]
pub fn flow_stress(&self, eps: f64, eps_dot: f64, temp: f64) -> f64 {
let term_A = self.a + self.b * eps.max(0.0).powf(self.n);
let eps_dot_ratio = (eps_dot / self.eps_dot0).max(1.0);
let term_B = 1.0 + self.c * eps_dot_ratio.ln();
let t_star = self.homologous_temperature(temp);
let term_C = 1.0 - t_star.powf(self.m);
term_A * term_B * term_C
}
pub fn isothermal_flow_stress(&self, eps: f64) -> f64 {
self.flow_stress(eps, self.eps_dot0, self.t_room)
}
pub fn strain_rate_sensitivity(&self, eps_dot: f64, eps: f64, temp: f64) -> f64 {
let sig_ref = self.flow_stress(eps, self.eps_dot0, temp);
if sig_ref < f64::EPSILON {
return 1.0;
}
self.flow_stress(eps, eps_dot, temp) / sig_ref
}
}
pub struct ThermalInterfaceResistance {
pub resistance: f64,
}
impl ThermalInterfaceResistance {
pub fn new(resistance: f64) -> Self {
Self { resistance }
}
pub fn metal_metal() -> Self {
Self::new(1e-4)
}
pub fn metal_ceramic() -> Self {
Self::new(1e-3)
}
pub fn semiconductor() -> Self {
Self::new(1e-8)
}
pub fn heat_flux(&self, delta_t: f64) -> f64 {
delta_t / self.resistance
}
pub fn temperature_drop(&self, heat_flux: f64) -> f64 {
heat_flux * self.resistance
}
pub fn conductance(&self) -> f64 {
1.0 / self.resistance
}
}
pub struct ThermalConductivityTensor {
pub k: [[f64; 3]; 3],
}
impl ThermalConductivityTensor {
pub fn new(k: [[f64; 3]; 3]) -> Self {
Self { k }
}
pub fn isotropic(k: f64) -> Self {
Self {
k: [[k, 0.0, 0.0], [0.0, k, 0.0], [0.0, 0.0, k]],
}
}
pub fn orthotropic(kx: f64, ky: f64, kz: f64) -> Self {
Self {
k: [[kx, 0.0, 0.0], [0.0, ky, 0.0], [0.0, 0.0, kz]],
}
}
#[allow(clippy::needless_range_loop)]
pub fn heat_flux(&self, grad_t: [f64; 3]) -> [f64; 3] {
let mut q = [0.0_f64; 3];
for i in 0..3 {
for j in 0..3 {
q[i] -= self.k[i][j] * grad_t[j];
}
}
q
}
pub fn effective_conductivity(&self) -> f64 {
(self.k[0][0] * self.k[1][1] * self.k[2][2]).powf(1.0 / 3.0)
}
pub fn is_symmetric(&self, tol: f64) -> bool {
for i in 0..3 {
for j in 0..3 {
if (self.k[i][j] - self.k[j][i]).abs() > tol {
return false;
}
}
}
true
}
}
pub struct AblationModel {
pub t_ablation: f64,
pub heat_of_ablation: f64,
pub density: f64,
pub arrhenius_a: f64,
pub activation_temp: f64,
}
impl AblationModel {
pub fn new(
t_ablation: f64,
heat_of_ablation: f64,
density: f64,
arrhenius_a: f64,
activation_temp: f64,
) -> Self {
Self {
t_ablation,
heat_of_ablation,
density,
arrhenius_a,
activation_temp,
}
}
pub fn recession_rate(&self, temp: f64) -> f64 {
if temp <= self.t_ablation {
return 0.0;
}
self.arrhenius_a * (-self.activation_temp / temp).exp()
}
pub fn mass_loss_rate(&self, temp: f64) -> f64 {
self.density * self.recession_rate(temp)
}
pub fn ablative_heat_flux(&self, temp: f64) -> f64 {
self.heat_of_ablation * self.mass_loss_rate(temp)
}
pub fn net_heat_flux(&self, applied_flux: f64, temp: f64) -> f64 {
applied_flux - self.ablative_heat_flux(temp)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct EnthalpyMethod {
pub rho: f64,
pub cp: f64,
pub latent_heat: f64,
pub t_solidus: f64,
pub t_liquidus: f64,
pub temperature: f64,
pub enthalpy: f64,
}
impl EnthalpyMethod {
pub fn new(rho: f64, cp: f64, latent_heat: f64, t_solidus: f64, t_liquidus: f64) -> Self {
let t0 = t_solidus - 1.0;
let h0 = rho * cp * t0;
Self {
rho,
cp,
latent_heat,
t_solidus,
t_liquidus,
temperature: t0,
enthalpy: h0,
}
}
pub fn liquid_fraction(&self, temp: f64) -> f64 {
if temp <= self.t_solidus {
0.0
} else if temp >= self.t_liquidus {
1.0
} else {
(temp - self.t_solidus) / (self.t_liquidus - self.t_solidus)
}
}
pub fn enthalpy_at(&self, temp: f64) -> f64 {
self.rho * self.cp * temp + self.rho * self.latent_heat * self.liquid_fraction(temp)
}
pub fn temperature_from_enthalpy(&self, h: f64) -> f64 {
let mut t = h / (self.rho * self.cp);
for _ in 0..50 {
let h_t = self.enthalpy_at(t);
let dh_dt = self.effective_cp(t);
let dt = (h - h_t) / dh_dt.max(f64::EPSILON);
t += dt;
if dt.abs() < 1e-6 {
break;
}
}
t
}
pub fn effective_cp(&self, temp: f64) -> f64 {
if temp > self.t_solidus && temp < self.t_liquidus {
let dfl_dt = 1.0 / (self.t_liquidus - self.t_solidus);
self.rho * (self.cp + self.latent_heat * dfl_dt)
} else {
self.rho * self.cp
}
}
pub fn apply_heat_flux(&mut self, q: f64, dt: f64) {
self.enthalpy += q * dt;
self.temperature = self.temperature_from_enthalpy(self.enthalpy);
}
pub fn is_mushy(&self) -> bool {
self.temperature > self.t_solidus && self.temperature < self.t_liquidus
}
}
pub struct TemperatureDependentSpecificHeat {
pub temps: Vec<f64>,
pub values: Vec<f64>,
}
impl TemperatureDependentSpecificHeat {
pub fn new(temps: Vec<f64>, values: Vec<f64>) -> Self {
assert_eq!(temps.len(), values.len());
Self { temps, values }
}
pub fn cp_at(&self, temperature: f64) -> f64 {
let n = self.temps.len();
if n == 0 {
return 0.0;
}
if n == 1 || temperature <= self.temps[0] {
return self.values[0];
}
if temperature >= self.temps[n - 1] {
return self.values[n - 1];
}
let idx = self
.temps
.partition_point(|&t| t <= temperature)
.saturating_sub(1);
let t0 = self.temps[idx];
let t1 = self.temps[idx + 1];
let c0 = self.values[idx];
let c1 = self.values[idx + 1];
let frac = (temperature - t0) / (t1 - t0);
c0 + frac * (c1 - c0)
}
}
pub struct ThermalShockResistance {
pub fracture_strength: f64,
pub young_modulus: f64,
pub poisson_ratio: f64,
pub thermal_expansion: f64,
pub thermal_conductivity: f64,
}
impl ThermalShockResistance {
#[allow(clippy::too_many_arguments)]
pub fn new(
fracture_strength: f64,
young_modulus: f64,
poisson_ratio: f64,
thermal_expansion: f64,
thermal_conductivity: f64,
) -> Self {
Self {
fracture_strength,
young_modulus,
poisson_ratio,
thermal_expansion,
thermal_conductivity,
}
}
pub fn r_parameter(&self) -> f64 {
self.fracture_strength * (1.0 - self.poisson_ratio)
/ (self.young_modulus * self.thermal_expansion)
}
pub fn r_prime(&self) -> f64 {
self.r_parameter() * self.thermal_conductivity
}
pub fn r_double_prime(&self) -> f64 {
self.fracture_strength * self.fracture_strength * (1.0 - self.poisson_ratio)
/ (self.young_modulus * self.thermal_expansion * self.thermal_expansion)
}
pub fn will_fail(&self, delta_t: f64) -> bool {
delta_t.abs() > self.r_parameter()
}
}
pub struct TemperatureDependentConductivity {
pub temps: Vec<f64>,
pub conductivities: Vec<f64>,
}
impl TemperatureDependentConductivity {
pub fn new(temps: Vec<f64>, conductivities: Vec<f64>) -> Self {
assert_eq!(temps.len(), conductivities.len());
Self {
temps,
conductivities,
}
}
pub fn conductivity_at(&self, temperature: f64) -> f64 {
let n = self.temps.len();
if n == 0 {
return 0.0;
}
if n == 1 || temperature <= self.temps[0] {
return self.conductivities[0];
}
if temperature >= self.temps[n - 1] {
return self.conductivities[n - 1];
}
let idx = self
.temps
.partition_point(|&t| t <= temperature)
.saturating_sub(1);
let t0 = self.temps[idx];
let t1 = self.temps[idx + 1];
let k0 = self.conductivities[idx];
let k1 = self.conductivities[idx + 1];
let frac = (temperature - t0) / (t1 - t0);
k0 + frac * (k1 - k0)
}
pub fn mean_conductivity(&self, t_low: f64, t_high: f64) -> f64 {
if (t_high - t_low).abs() < f64::EPSILON {
return self.conductivity_at(t_low);
}
let mut pts: Vec<f64> = vec![t_low];
for &t in &self.temps {
if t > t_low && t < t_high {
pts.push(t);
}
}
pts.push(t_high);
pts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
pts.dedup_by(|a, b| (*a - *b).abs() < f64::EPSILON);
let mut integral = 0.0;
for i in 0..pts.len() - 1 {
let ta = pts[i];
let tb = pts[i + 1];
let ka = self.conductivity_at(ta);
let kb = self.conductivity_at(tb);
integral += 0.5 * (ka + kb) * (tb - ta);
}
integral / (t_high - t_low)
}
}
pub struct DebyeModel {
pub theta_d: f64,
pub n_atoms: f64,
}
impl DebyeModel {
pub fn new(theta_d: f64, n_atoms: f64) -> Self {
Self { theta_d, n_atoms }
}
pub fn molar_cv(&self, temperature: f64) -> f64 {
if temperature < 1e-10 {
return 0.0;
}
let x_max = self.theta_d / temperature;
let n_steps = 200;
let dx = x_max / (n_steps as f64);
let integrand = |x: f64| -> f64 {
if x < 1e-12 {
return 0.0;
}
if x > 500.0 {
return 0.0;
}
let ex = x.exp();
x.powi(4) * ex / (ex - 1.0).powi(2)
};
let mut sum = integrand(0.0) + integrand(x_max);
for i in 1..n_steps {
let x = i as f64 * dx;
let weight = if i % 2 == 0 { 2.0 } else { 4.0 };
sum += weight * integrand(x);
}
let integral = sum * dx / 3.0;
let ratio = temperature / self.theta_d;
9.0 * self.n_atoms * R_GAS * ratio.powi(3) * integral
}
pub fn dulong_petit(&self) -> f64 {
3.0 * self.n_atoms * R_GAS
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ThermalExpansion {
pub alpha_x: f64,
pub alpha_y: f64,
pub alpha_z: f64,
pub t_ref: f64,
}
#[allow(dead_code)]
impl ThermalExpansion {
pub fn isotropic(alpha: f64, t_ref: f64) -> Self {
Self {
alpha_x: alpha,
alpha_y: alpha,
alpha_z: alpha,
t_ref,
}
}
pub fn orthotropic(alpha_x: f64, alpha_y: f64, alpha_z: f64, t_ref: f64) -> Self {
Self {
alpha_x,
alpha_y,
alpha_z,
t_ref,
}
}
pub fn strain_x(&self, temperature: f64) -> f64 {
self.alpha_x * (temperature - self.t_ref)
}
pub fn strain_y(&self, temperature: f64) -> f64 {
self.alpha_y * (temperature - self.t_ref)
}
pub fn strain_z(&self, temperature: f64) -> f64 {
self.alpha_z * (temperature - self.t_ref)
}
pub fn compute_volumetric_strain(&self, temperature: f64) -> f64 {
let delta_t = temperature - self.t_ref;
(self.alpha_x + self.alpha_y + self.alpha_z) * delta_t
}
pub fn compute_volumetric_strain_exact(&self, temperature: f64) -> f64 {
let ex = 1.0 + self.strain_x(temperature);
let ey = 1.0 + self.strain_y(temperature);
let ez = 1.0 + self.strain_z(temperature);
ex * ey * ez - 1.0
}
pub fn strain_tensor(&self, temperature: f64) -> [f64; 3] {
[
self.strain_x(temperature),
self.strain_y(temperature),
self.strain_z(temperature),
]
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ThermalShockParam {
pub sigma_f: f64,
pub young_modulus: f64,
pub nu: f64,
pub alpha: f64,
pub k: f64,
pub k_ic: f64,
}
impl ThermalShockParam {
#[allow(clippy::too_many_arguments)]
pub fn new(sigma_f: f64, young_modulus: f64, nu: f64, alpha: f64, k: f64, k_ic: f64) -> Self {
Self {
sigma_f,
young_modulus,
nu,
alpha,
k,
k_ic,
}
}
pub fn r_first(&self) -> f64 {
self.sigma_f * (1.0 - self.nu) / (self.young_modulus * self.alpha)
}
pub fn r_second(&self) -> f64 {
self.k * self.r_first()
}
pub fn r_third(&self) -> f64 {
let g_f = self.k_ic * self.k_ic / self.young_modulus;
g_f / (self.young_modulus * self.alpha * self.alpha)
}
pub fn r_hasselman(&self) -> f64 {
let g_f = self.k_ic * self.k_ic / self.young_modulus;
self.young_modulus * g_f / (self.sigma_f * self.sigma_f * (1.0 - self.nu))
}
}
pub struct EinsteinModel {
pub theta_e: f64,
pub n_atoms: f64,
}
impl EinsteinModel {
pub fn new(theta_e: f64, n_atoms: f64) -> Self {
Self { theta_e, n_atoms }
}
pub fn molar_cv(&self, temperature: f64) -> f64 {
if temperature < 1e-10 {
return 0.0;
}
let x = self.theta_e / temperature;
if x > 500.0 {
return 0.0;
}
let ex = x.exp();
3.0 * self.n_atoms * R_GAS * x * x * ex / (ex - 1.0).powi(2)
}
}
pub struct HeatConduction1D {
pub n_nodes: usize,
pub length: f64,
pub temperature: Vec<f64>,
pub material: ThermalMaterial,
}
impl HeatConduction1D {
pub fn new(n_nodes: usize, length: f64, t_init: f64, material: ThermalMaterial) -> Self {
assert!(n_nodes >= 2, "need at least 2 nodes");
Self {
n_nodes,
length,
temperature: vec![t_init; n_nodes],
material,
}
}
pub fn set_temperature_bc(&mut self, t_left: f64, t_right: f64) {
self.temperature[0] = t_left;
self.temperature[self.n_nodes - 1] = t_right;
}
pub fn step_explicit(&mut self, dt: f64) -> bool {
let dx = self.length / (self.n_nodes - 1) as f64;
let alpha = self.material.diffusivity();
let r = alpha * dt / (dx * dx);
if r > 0.5 {
return false;
}
let old = self.temperature.clone();
for i in 1..self.n_nodes - 1 {
self.temperature[i] = old[i] + r * (old[i + 1] - 2.0 * old[i] + old[i - 1]);
}
true
}
pub fn step_explicit_with_source(&mut self, dt: f64, source: f64) -> bool {
let dx = self.length / (self.n_nodes - 1) as f64;
let alpha = self.material.diffusivity();
let r = alpha * dt / (dx * dx);
if r > 0.5 {
return false;
}
let old = self.temperature.clone();
let rho_cp = self.material.volumetric_heat_capacity();
for i in 1..self.n_nodes - 1 {
self.temperature[i] =
old[i] + r * (old[i + 1] - 2.0 * old[i] + old[i - 1]) + source * dt / rho_cp;
}
true
}
pub fn critical_dt(&self) -> f64 {
let dx = self.length / (self.n_nodes - 1) as f64;
let alpha = self.material.diffusivity();
(dx * dx) / (2.0 * alpha)
}
pub fn steady_state_temperature(&self, t_left: f64, t_right: f64) -> Vec<f64> {
(0..self.n_nodes)
.map(|i| {
let frac = i as f64 / (self.n_nodes - 1) as f64;
t_left + frac * (t_right - t_left)
})
.collect()
}
pub fn heat_flux(&self) -> Vec<f64> {
let dx = self.length / (self.n_nodes - 1) as f64;
let k = self.material.thermal_conductivity;
(1..self.n_nodes - 1)
.map(|i| {
let dt_dx = (self.temperature[i + 1] - self.temperature[i - 1]) / (2.0 * dx);
-k * dt_dx
})
.collect()
}
pub fn total_heat_content(&self) -> f64 {
let dx = self.length / (self.n_nodes - 1) as f64;
let rho_cp = self.material.volumetric_heat_capacity();
self.temperature.iter().map(|&t| rho_cp * t * dx).sum()
}
}
pub struct ThermalMaterial {
pub thermal_conductivity: f64,
pub specific_heat: f64,
pub density: f64,
pub thermal_expansion: f64,
}
impl ThermalMaterial {
pub fn new(k: f64, cp: f64, rho: f64, alpha: f64) -> Self {
Self {
thermal_conductivity: k,
specific_heat: cp,
density: rho,
thermal_expansion: alpha,
}
}
pub fn steel() -> Self {
Self::new(50.0, 500.0, 7850.0, 12e-6)
}
pub fn aluminum() -> Self {
Self::new(237.0, 900.0, 2700.0, 23e-6)
}
pub fn copper() -> Self {
Self::new(401.0, 385.0, 8960.0, 17e-6)
}
pub fn concrete() -> Self {
Self::new(1.5, 880.0, 2300.0, 10e-6)
}
pub fn silicon_carbide() -> Self {
Self::new(120.0, 750.0, 3210.0, 4.0e-6)
}
pub fn alumina() -> Self {
Self::new(30.0, 880.0, 3950.0, 8.0e-6)
}
pub fn diffusivity(&self) -> f64 {
self.thermal_conductivity / (self.density * self.specific_heat)
}
pub fn effusivity(&self) -> f64 {
(self.thermal_conductivity * self.density * self.specific_heat).sqrt()
}
pub fn thermal_strain(&self, delta_t: f64) -> f64 {
self.thermal_expansion * delta_t
}
pub fn thermal_stress(&self, delta_t: f64, young_modulus: f64, poisson_ratio: f64) -> f64 {
-young_modulus * self.thermal_expansion * delta_t / (1.0 - poisson_ratio)
}
pub fn volumetric_heat_capacity(&self) -> f64 {
self.density * self.specific_heat
}
pub fn penetration_depth(&self, t: f64) -> f64 {
(4.0 * self.diffusivity() * t).sqrt()
}
pub fn compute_thermal_shock_resistance(
&self,
fracture_strength: f64,
young_modulus: f64,
poisson_ratio: f64,
) -> f64 {
fracture_strength * (1.0 - poisson_ratio) / (young_modulus * self.thermal_expansion)
}
pub fn compute_wiedemann_franz(&self, temperature: f64, electrical_resistivity: f64) -> f64 {
const LORENZ: f64 = 2.4427e-8_f64;
LORENZ * temperature / electrical_resistivity
}
}
pub struct NewtonianCooling {
pub mass: f64,
pub specific_heat: f64,
pub heat_transfer_coeff: f64,
pub surface_area: f64,
pub temperature: f64,
}
impl NewtonianCooling {
pub fn new(mass: f64, cp: f64, h: f64, area: f64, t_init: f64) -> Self {
Self {
mass,
specific_heat: cp,
heat_transfer_coeff: h,
surface_area: area,
temperature: t_init,
}
}
pub fn time_constant(&self) -> f64 {
self.mass * self.specific_heat / (self.heat_transfer_coeff * self.surface_area)
}
pub fn temperature_at_time(&self, t_inf: f64, time: f64) -> f64 {
let tau = self.time_constant();
t_inf + (self.temperature - t_inf) * (-time / tau).exp()
}
pub fn step(&mut self, t_inf: f64, dt: f64) {
let tau = self.time_constant();
self.temperature += -dt / tau * (self.temperature - t_inf);
}
pub fn time_to_temperature(&self, t_inf: f64, t_target: f64) -> Option<f64> {
let tau = self.time_constant();
let denom = self.temperature - t_inf;
if denom.abs() < f64::EPSILON {
return None;
}
let ratio = (t_target - t_inf) / denom;
if ratio <= 0.0 {
return None;
}
Some(-tau * ratio.ln())
}
pub fn biot_number(&self, characteristic_length: f64, thermal_conductivity: f64) -> f64 {
self.heat_transfer_coeff * characteristic_length / thermal_conductivity
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ThermalFatigue {
pub c_cm: f64,
pub beta: f64,
pub alpha: f64,
pub young_modulus: f64,
pub damage: f64,
}
impl ThermalFatigue {
pub fn new(c_cm: f64, beta: f64, alpha: f64, young_modulus: f64) -> Self {
Self {
c_cm,
beta,
alpha,
young_modulus,
damage: 0.0,
}
}
pub fn plastic_strain_range(&self, delta_t: f64) -> f64 {
self.alpha * delta_t.abs()
}
pub fn fatigue_life(&self, delta_t: f64) -> f64 {
let deps = self.plastic_strain_range(delta_t);
if deps < f64::EPSILON {
return f64::INFINITY;
}
self.c_cm / deps.powf(self.beta)
}
pub fn accumulate(&mut self, delta_t: f64, n_cycles: f64) {
let n_f = self.fatigue_life(delta_t);
if n_f.is_finite() {
self.damage = (self.damage + n_cycles / n_f).min(1.0);
}
}
pub fn is_failed(&self) -> bool {
self.damage >= 1.0 - f64::EPSILON
}
pub fn critical_delta_t(&self, n_cycles: f64) -> f64 {
if n_cycles <= 0.0 {
return 0.0;
}
let deps = (self.c_cm / n_cycles).powf(1.0 / self.beta);
deps / self.alpha
}
}