#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct Ti6Al4V {
pub temperature_c: f64,
}
impl Ti6Al4V {
pub fn new(temperature_c: f64) -> Self {
Self {
temperature_c: temperature_c.min(950.0),
}
}
pub fn youngs_modulus_gpa(&self) -> f64 {
let t = self.temperature_c;
113.8 - 0.051 * t - 1.2e-5 * t * t
}
pub fn yield_strength_mpa(&self) -> f64 {
let t = self.temperature_c;
(950.0 - 0.55 * t).max(100.0)
}
pub fn uts_mpa(&self) -> f64 {
self.yield_strength_mpa() * 1.10 }
pub fn density_kg_m3(&self) -> f64 {
4430.0
}
pub fn thermal_conductivity_w_mk(&self) -> f64 {
let t = self.temperature_c;
6.7 + 0.0103 * t
}
pub fn specific_heat_j_kgk(&self) -> f64 {
let t = self.temperature_c;
526.0 + 0.13 * t
}
pub fn cte_per_k(&self) -> f64 {
let t = self.temperature_c;
(8.6 + 0.0014 * t) * 1e-6
}
pub fn thermal_diffusivity_m2_s(&self) -> f64 {
let k = self.thermal_conductivity_w_mk();
let rho = self.density_kg_m3();
let cp = self.specific_heat_j_kgk();
k / (rho * cp)
}
pub fn poisson_ratio(&self) -> f64 {
0.342
}
}
#[derive(Debug, Clone)]
pub struct Inconel718 {
pub temperature_c: f64,
}
impl Inconel718 {
pub fn new(temperature_c: f64) -> Self {
Self { temperature_c }
}
pub fn youngs_modulus_gpa(&self) -> f64 {
let t = self.temperature_c;
200.0 - 0.08 * t
}
pub fn yield_strength_mpa(&self) -> f64 {
let t = self.temperature_c;
(1185.0 - 0.85 * t).max(200.0)
}
pub fn density_kg_m3(&self) -> f64 {
8190.0
}
pub fn norton_creep_rate_per_s(&self, stress_mpa: f64) -> f64 {
const A: f64 = 3.0e-28;
const N: f64 = 4.8;
const Q: f64 = 310_000.0; const R: f64 = 8.314; let t_k = self.temperature_c + 273.15;
A * stress_mpa.powf(N) * (-Q / (R * t_k)).exp()
}
pub fn rupture_life_hours(&self, stress_mpa: f64) -> f64 {
const C: f64 = 20.0;
let lmp = 29_000.0 - 22.0 * stress_mpa;
let t_k = self.temperature_c + 273.15;
let log_tr = lmp / t_k - C;
10.0_f64.powf(log_tr)
}
pub fn thermal_conductivity_w_mk(&self) -> f64 {
let t = self.temperature_c;
11.4 + 0.013 * t
}
}
#[derive(Debug, Clone, Copy)]
pub struct CfrpPly {
pub angle_deg: f64,
pub thickness_mm: f64,
pub e1_gpa: f64,
pub e2_gpa: f64,
pub g12_gpa: f64,
pub nu12: f64,
}
impl CfrpPly {
pub fn im7_8552(angle_deg: f64) -> Self {
Self {
angle_deg,
thickness_mm: 0.125,
e1_gpa: 161.0,
e2_gpa: 11.38,
g12_gpa: 5.17,
nu12: 0.32,
}
}
pub fn reduced_stiffness_gpa(&self) -> [f64; 4] {
let e1 = self.e1_gpa;
let e2 = self.e2_gpa;
let g12 = self.g12_gpa;
let nu12 = self.nu12;
let nu21 = nu12 * e2 / e1;
let denom = 1.0 - nu12 * nu21;
let q11 = e1 / denom;
let q22 = e2 / denom;
let q12 = nu12 * e2 / denom;
let q66 = g12;
[q11, q22, q12, q66]
}
pub fn transformed_stiffness_gpa(&self) -> [f64; 6] {
let [q11, q22, q12, q66] = self.reduced_stiffness_gpa();
let theta = self.angle_deg.to_radians();
let c = theta.cos();
let s = theta.sin();
let c2 = c * c;
let s2 = s * s;
let c4 = c2 * c2;
let s4 = s2 * s2;
let c2s2 = c2 * s2;
let q11b = q11 * c4 + 2.0 * (q12 + 2.0 * q66) * c2s2 + q22 * s4;
let q22b = q11 * s4 + 2.0 * (q12 + 2.0 * q66) * c2s2 + q22 * c4;
let q12b = (q11 + q22 - 4.0 * q66) * c2s2 + q12 * (c4 + s4);
let q66b = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * c2s2 + q66 * (c2 - s2).powi(2);
let q16b = (q11 - q12 - 2.0 * q66) * c * c2 * s - (q22 - q12 - 2.0 * q66) * s * s2 * c;
let q26b = (q11 - q12 - 2.0 * q66) * s * s2 * c - (q22 - q12 - 2.0 * q66) * c * c2 * s;
[q11b, q22b, q12b, q66b, q16b, q26b]
}
}
#[derive(Debug, Clone)]
pub struct CltResult {
pub a_matrix: [f64; 6],
pub total_thickness_mm: f64,
pub ex_gpa: f64,
pub ey_gpa: f64,
pub gxy_gpa: f64,
}
pub fn clt_analysis(plies: &[CfrpPly]) -> CltResult {
let mut a = [0.0f64; 6];
let mut total_t = 0.0f64;
for ply in plies {
let q_bar = ply.transformed_stiffness_gpa();
let t = ply.thickness_mm;
total_t += t;
for i in 0..6 {
a[i] += q_bar[i] * t; }
}
let a_nmm: [f64; 6] = a.map(|v| v * 1000.0);
let h = total_t;
let det = a_nmm[0] * a_nmm[1] - a_nmm[2] * a_nmm[2];
let ex = if a_nmm[1] * h > 1e-12 {
det / (a_nmm[1] * h) / 1000.0
} else {
0.0
}; let ey = if a_nmm[0] * h > 1e-12 {
det / (a_nmm[0] * h) / 1000.0
} else {
0.0
};
let gxy = a_nmm[3] / h / 1000.0;
CltResult {
a_matrix: a_nmm,
total_thickness_mm: h,
ex_gpa: ex,
ey_gpa: ey,
gxy_gpa: gxy,
}
}
#[derive(Debug, Clone)]
pub struct SicSicCmc {
pub temperature_c: f64,
pub fibre_volume_fraction: f64,
}
impl SicSicCmc {
pub fn new(temperature_c: f64, fibre_volume_fraction: f64) -> Self {
Self {
temperature_c,
fibre_volume_fraction: fibre_volume_fraction.clamp(0.35, 0.55),
}
}
pub fn youngs_modulus_gpa(&self) -> f64 {
let vf = self.fibre_volume_fraction;
let e_fibre = 380.0; let e_matrix = 350.0; 0.5 * (e_fibre * vf + e_matrix * (1.0 - vf))
}
pub fn proportional_limit_mpa(&self) -> f64 {
150.0 - 0.08 * self.temperature_c
}
pub fn uts_mpa(&self) -> f64 {
(230.0 - 0.10 * self.temperature_c).max(50.0)
}
pub fn ilss_mpa(&self) -> f64 {
40.0 - 0.02 * self.temperature_c
}
pub fn density_kg_m3(&self) -> f64 {
2700.0
}
pub fn thermal_conductivity_w_mk(&self) -> f64 {
let t = self.temperature_c;
(18.0 * (1.0 - t / 1600.0)).max(3.0)
}
pub fn max_use_temperature_c(&self) -> f64 {
1200.0
}
}
#[derive(Debug, Clone)]
pub struct TbcYsz {
pub thickness_um: f64,
pub surface_temperature_c: f64,
pub substrate_temperature_c: f64,
}
impl TbcYsz {
pub fn new(
thickness_um: f64,
surface_temperature_c: f64,
substrate_temperature_c: f64,
) -> Self {
Self {
thickness_um,
surface_temperature_c,
substrate_temperature_c,
}
}
pub fn mean_temperature_c(&self) -> f64 {
(self.surface_temperature_c + self.substrate_temperature_c) * 0.5
}
pub fn thermal_conductivity_w_mk(&self) -> f64 {
let t = self.mean_temperature_c();
if t < 1200.0 {
1.9 - 2.0e-4 * t
} else {
1.7 + 1.5e-4 * (t - 1200.0)
}
}
pub fn temperature_drop_c(&self, heat_flux_w_m2: f64) -> f64 {
let k = self.thermal_conductivity_w_mk();
let t_m = self.thickness_um * 1e-6; heat_flux_w_m2 * t_m / k
}
pub fn spallation_life_cycles(&self, delta_t_cycle_c: f64) -> f64 {
const C: f64 = 5.0e6;
const M: f64 = 2.5;
C / delta_t_cycle_c.powf(M)
}
pub fn cte_mismatch_strain(&self, delta_t_c: f64) -> f64 {
let alpha_ysz = 11.0e-6;
let alpha_substrate = 14.0e-6;
(alpha_substrate - alpha_ysz) * delta_t_c
}
}
#[derive(Debug, Clone)]
pub struct AblativeMaterial {
pub virgin_density_kg_m3: f64,
pub char_density_kg_m3: f64,
pub pyrolysis_onset_c: f64,
pub pyrolysis_complete_c: f64,
pub heat_of_pyrolysis_kj_kg: f64,
pub char_cp_j_kgk: f64,
pub virgin_cp_j_kgk: f64,
}
impl AblativeMaterial {
pub fn pica() -> Self {
Self {
virgin_density_kg_m3: 256.0,
char_density_kg_m3: 182.0,
pyrolysis_onset_c: 400.0,
pyrolysis_complete_c: 850.0,
heat_of_pyrolysis_kj_kg: 1750.0,
char_cp_j_kgk: 1500.0,
virgin_cp_j_kgk: 1200.0,
}
}
pub fn char_fraction(&self, temperature_c: f64) -> f64 {
if temperature_c <= self.pyrolysis_onset_c {
0.0
} else if temperature_c >= self.pyrolysis_complete_c {
1.0
} else {
(temperature_c - self.pyrolysis_onset_c)
/ (self.pyrolysis_complete_c - self.pyrolysis_onset_c)
}
}
pub fn local_density_kg_m3(&self, beta: f64) -> f64 {
self.virgin_density_kg_m3 * (1.0 - beta) + self.char_density_kg_m3 * beta
}
pub fn blowing_correction(&self, t_wall_c: f64, t_recovery_c: f64) -> f64 {
let t_wall = t_wall_c + 273.15;
let t_rec = (t_recovery_c + 273.15).max(1.0);
0.5 * (t_wall / t_rec).sqrt()
}
pub fn recession_rate_mm_s(&self, heat_flux_mw_m2: f64, wall_temperature_c: f64) -> f64 {
const A_OX: f64 = 2.5e4; const E_A: f64 = 1.8e5; const R: f64 = 8.314;
let t_k = wall_temperature_c + 273.15;
let kinetic = A_OX * (-E_A / (R * t_k)).exp();
let hv = self.heat_of_pyrolysis_kj_kg * 1000.0; let rho_c = self.char_density_kg_m3;
let q = heat_flux_mw_m2 * 1e6; kinetic * q / (rho_c * hv) * 1000.0 }
}
#[derive(Debug, Clone, Copy)]
pub struct MetalFoam {
pub relative_density: f64,
pub solid_modulus_gpa: f64,
pub solid_yield_mpa: f64,
pub solid_density_kg_m3: f64,
pub open_cell: bool,
}
impl MetalFoam {
pub fn aluminium_open_cell(relative_density: f64) -> Self {
Self {
relative_density,
solid_modulus_gpa: 70.0,
solid_yield_mpa: 270.0,
solid_density_kg_m3: 2700.0,
open_cell: true,
}
}
pub fn density_kg_m3(&self) -> f64 {
self.relative_density * self.solid_density_kg_m3
}
pub fn youngs_modulus_gpa(&self) -> f64 {
let r = self.relative_density;
if self.open_cell {
self.solid_modulus_gpa * r * r
} else {
let phi = 0.6f64;
self.solid_modulus_gpa * (phi * phi * r * r + (1.0 - phi) * r)
}
}
pub fn plateau_stress_mpa(&self) -> f64 {
let r = self.relative_density;
if self.open_cell {
0.3 * self.solid_yield_mpa * r.powf(1.5)
} else {
let phi = 0.6f64;
0.3 * self.solid_yield_mpa * ((phi * r).sqrt() + (1.0 - phi) * r)
}
}
pub fn densification_strain(&self) -> f64 {
1.0 - 1.4 * self.relative_density
}
pub fn energy_absorption_mj_m3(&self) -> f64 {
self.plateau_stress_mpa() * self.densification_strain() * 1e-3
}
}
#[derive(Debug, Clone)]
pub struct Nitinol {
pub temperature_c: f64,
pub a_finish_c: f64,
pub m_start_c: f64,
pub ea_gpa: f64,
pub em_gpa: f64,
}
impl Nitinol {
pub fn biomedical_grade() -> Self {
Self {
temperature_c: 37.0, a_finish_c: 10.0,
m_start_c: -5.0,
ea_gpa: 83.0,
em_gpa: 28.0,
}
}
pub fn is_superelastic(&self) -> bool {
self.temperature_c > self.a_finish_c
}
pub fn effective_modulus_gpa(&self, martensite_fraction: f64) -> f64 {
let xi = martensite_fraction.clamp(0.0, 1.0);
self.ea_gpa * (1.0 - xi) + self.em_gpa * xi
}
pub fn critical_sim_stress_mpa(&self) -> f64 {
const C_A: f64 = 8.0; let delta_t = (self.temperature_c - self.a_finish_c).max(0.0);
200.0 + C_A * delta_t }
pub fn max_recoverable_strain_pct(&self) -> f64 {
if self.is_superelastic() { 8.0 } else { 2.0 }
}
pub fn density_kg_m3(&self) -> f64 {
6450.0
}
}
#[derive(Debug, Clone)]
pub struct CantorhHea {
pub temperature_c: f64,
pub grain_size_um: f64,
}
impl CantorhHea {
pub fn new(temperature_c: f64, grain_size_um: f64) -> Self {
Self {
temperature_c,
grain_size_um,
}
}
pub fn youngs_modulus_gpa(&self) -> f64 {
let t = self.temperature_c;
202.0 - 0.06 * t
}
pub fn yield_strength_mpa(&self) -> f64 {
let sigma0 = 160.0; let k_hp = 800.0; let d = self.grain_size_um.max(1.0);
let t = self.temperature_c;
let hp = k_hp / d.sqrt();
let thermal = (0.4 * t).max(0.0); (sigma0 + hp - thermal).max(50.0)
}
pub fn uts_mpa(&self) -> f64 {
self.yield_strength_mpa() * 1.5 }
pub fn fracture_toughness_mpa_sqrtm(&self) -> f64 {
let t = self.temperature_c;
if t < 0.0 {
217.0 - 0.5 * t } else {
(217.0 - 0.1 * t).max(100.0)
}
}
pub fn stacking_fault_energy_mj_m2(&self) -> f64 {
let t = self.temperature_c;
18.0 + 0.02 * t
}
pub fn density_kg_m3(&self) -> f64 {
8000.0
}
}
#[derive(Debug, Clone)]
pub struct ReentryVehicle {
pub nose_radius_m: f64,
pub entry_velocity_m_s: f64,
pub freestream_density_kg_m3: f64,
pub tps_material: String,
pub tps_thickness_mm: f64,
}
impl ReentryVehicle {
pub fn new(
nose_radius_m: f64,
entry_velocity_m_s: f64,
freestream_density_kg_m3: f64,
tps_material: &str,
tps_thickness_mm: f64,
) -> Self {
Self {
nose_radius_m,
entry_velocity_m_s,
freestream_density_kg_m3,
tps_material: tps_material.to_string(),
tps_thickness_mm,
}
}
pub fn stagnation_heat_flux_w_m2(&self) -> f64 {
const K_SG: f64 = 1.742e-4;
let rho = self.freestream_density_kg_m3;
let r = self.nose_radius_m;
let v = self.entry_velocity_m_s;
K_SG * (rho / r).sqrt() * v.powi(3)
}
pub fn radiative_equilibrium_temperature_k(&self, emissivity: f64) -> f64 {
const SIGMA: f64 = 5.670_374_419e-8; let q = self.stagnation_heat_flux_w_m2();
let eps = emissivity.clamp(0.1, 1.0);
(q / (eps * SIGMA)).powf(0.25)
}
pub fn integrated_heat_load_mj_m2(&self, entry_duration_s: f64) -> f64 {
let q_peak = self.stagnation_heat_flux_w_m2();
2.0 / 3.0 * q_peak * entry_duration_s / 1e6
}
pub fn required_tps_thickness_mm(
&self,
tps_thermal_diffusivity_m2_s: f64,
entry_duration_s: f64,
) -> f64 {
let alpha = tps_thermal_diffusivity_m2_s;
let t = entry_duration_s;
1.5 * (alpha * t).sqrt() * 1000.0 }
pub fn ballistic_coefficient_kg_m2(&self, mass_kg: f64, cd: f64) -> f64 {
let a_ref = PI * self.nose_radius_m * self.nose_radius_m;
mass_kg / (cd * a_ref)
}
}
#[derive(Debug, Clone, Copy)]
pub struct TitaniumTube {
pub outer_diameter_mm: f64,
pub wall_thickness_mm: f64,
pub temperature_c: f64,
}
impl TitaniumTube {
pub fn new(outer_diameter_mm: f64, wall_thickness_mm: f64, temperature_c: f64) -> Self {
Self {
outer_diameter_mm,
wall_thickness_mm,
temperature_c,
}
}
pub fn cross_section_area_mm2(&self) -> f64 {
let ro = self.outer_diameter_mm * 0.5;
let ri = ro - self.wall_thickness_mm;
PI * (ro * ro - ri * ri)
}
pub fn second_moment_mm4(&self) -> f64 {
let ro = self.outer_diameter_mm * 0.5;
let ri = ro - self.wall_thickness_mm;
PI / 4.0 * (ro.powi(4) - ri.powi(4))
}
pub fn euler_buckling_load_n(&self, length_mm: f64) -> f64 {
let mat = Ti6Al4V::new(self.temperature_c);
let e = mat.youngs_modulus_gpa() * 1e3; PI * PI * e * self.second_moment_mm4() / (length_mm * length_mm)
}
pub fn axial_yield_load_n(&self) -> f64 {
let mat = Ti6Al4V::new(self.temperature_c);
mat.yield_strength_mpa() * self.cross_section_area_mm2()
}
pub fn margin_of_safety_yield(&self, applied_load_n: f64) -> f64 {
self.axial_yield_load_n() / applied_load_n - 1.0
}
}
#[derive(Debug, Clone)]
pub struct BondCoatOxidation {
pub temperature_c: f64,
pub al_content_wt_pct: f64,
}
impl BondCoatOxidation {
pub fn new(temperature_c: f64, al_content_wt_pct: f64) -> Self {
Self {
temperature_c,
al_content_wt_pct,
}
}
pub fn parabolic_rate_constant_um2_h(&self) -> f64 {
const A0: f64 = 2.0e6; const Q: f64 = 220_000.0; const R: f64 = 8.314;
let t_k = self.temperature_c + 273.15;
let al_factor = (self.al_content_wt_pct / 10.0).sqrt().max(0.1);
A0 * al_factor * (-Q / (R * t_k)).exp()
}
pub fn tgo_thickness_um(&self, time_h: f64) -> f64 {
(self.parabolic_rate_constant_um2_h() * time_h).sqrt()
}
pub fn critical_tgo_thickness_um() -> f64 {
7.0
}
pub fn life_fraction(&self, tgo_thickness_um: f64) -> f64 {
(tgo_thickness_um / Self::critical_tgo_thickness_um())
.powi(2)
.min(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ti64_youngs_modulus_at_rt() {
let mat = Ti6Al4V::new(20.0);
let e = mat.youngs_modulus_gpa();
assert!(e > 110.0 && e < 115.0, "E = {e}");
}
#[test]
fn test_ti64_modulus_decreases_with_temperature() {
let e_rt = Ti6Al4V::new(20.0).youngs_modulus_gpa();
let e_hot = Ti6Al4V::new(600.0).youngs_modulus_gpa();
assert!(e_hot < e_rt);
}
#[test]
fn test_ti64_yield_strength_decreases() {
let sy_rt = Ti6Al4V::new(20.0).yield_strength_mpa();
let sy_hot = Ti6Al4V::new(500.0).yield_strength_mpa();
assert!(sy_hot < sy_rt);
}
#[test]
fn test_ti64_thermal_conductivity_increases() {
let k_rt = Ti6Al4V::new(20.0).thermal_conductivity_w_mk();
let k_hot = Ti6Al4V::new(500.0).thermal_conductivity_w_mk();
assert!(k_hot > k_rt);
}
#[test]
fn test_ti64_thermal_diffusivity_positive() {
let alpha = Ti6Al4V::new(300.0).thermal_diffusivity_m2_s();
assert!(alpha > 0.0);
}
#[test]
fn test_inconel_creep_rate_increases_with_stress() {
let mat = Inconel718::new(650.0);
let cr_low = mat.norton_creep_rate_per_s(300.0);
let cr_high = mat.norton_creep_rate_per_s(600.0);
assert!(cr_high > cr_low);
}
#[test]
fn test_inconel_creep_rate_increases_with_temperature() {
let cr_cool = Inconel718::new(540.0).norton_creep_rate_per_s(500.0);
let cr_hot = Inconel718::new(760.0).norton_creep_rate_per_s(500.0);
assert!(cr_hot > cr_cool);
}
#[test]
fn test_inconel_rupture_life_decreases_with_stress() {
let mat = Inconel718::new(650.0);
let life_low = mat.rupture_life_hours(400.0);
let life_high = mat.rupture_life_hours(700.0);
assert!(life_high < life_low);
}
#[test]
fn test_inconel_density() {
let mat = Inconel718::new(25.0);
assert!((mat.density_kg_m3() - 8190.0).abs() < 1.0);
}
#[test]
fn test_cfrp_ply_reduced_stiffness_positive() {
let ply = CfrpPly::im7_8552(0.0);
let q = ply.reduced_stiffness_gpa();
for &qi in &q {
assert!(qi > 0.0, "Q component non-positive: {qi}");
}
}
#[test]
fn test_clt_quasi_isotropic_laminate() {
let angles = [0.0, 45.0, -45.0, 90.0, 90.0, -45.0, 45.0, 0.0];
let plies: Vec<CfrpPly> = angles.iter().map(|&a| CfrpPly::im7_8552(a)).collect();
let res = clt_analysis(&plies);
let diff = (res.ex_gpa - res.ey_gpa).abs() / res.ex_gpa;
assert!(
diff < 0.05,
"Ex={:.2} Ey={:.2} diff={diff:.4}",
res.ex_gpa,
res.ey_gpa
);
}
#[test]
fn test_clt_zero_only_laminate_max_modulus() {
let plies = vec![CfrpPly::im7_8552(0.0); 8];
let unidirectional = clt_analysis(&plies);
assert!(unidirectional.ex_gpa > 100.0);
}
#[test]
fn test_clt_thickness_sum() {
let plies: Vec<CfrpPly> = [0.0, 90.0, 0.0, 90.0]
.iter()
.map(|&a| CfrpPly::im7_8552(a))
.collect();
let res = clt_analysis(&plies);
let expected = 4.0 * 0.125;
assert!((res.total_thickness_mm - expected).abs() < 1e-9);
}
#[test]
fn test_sic_sic_modulus_range() {
let cmc = SicSicCmc::new(1000.0, 0.45);
let e = cmc.youngs_modulus_gpa();
assert!(e > 100.0 && e < 220.0, "E = {e}");
}
#[test]
fn test_sic_sic_uts_decreases_with_temperature() {
let uts_low = SicSicCmc::new(800.0, 0.45).uts_mpa();
let uts_high = SicSicCmc::new(1200.0, 0.45).uts_mpa();
assert!(uts_high < uts_low);
}
#[test]
fn test_sic_sic_max_use_temperature() {
let cmc = SicSicCmc::new(25.0, 0.40);
assert!((cmc.max_use_temperature_c() - 1200.0).abs() < 1.0);
}
#[test]
fn test_tbc_temperature_drop_proportional_to_flux() {
let tbc = TbcYsz::new(120.0, 1300.0, 950.0);
let dt1 = tbc.temperature_drop_c(1e6);
let dt2 = tbc.temperature_drop_c(2e6);
assert!((dt2 - 2.0 * dt1).abs() < 1e-6, "dt1={dt1} dt2={dt2}");
}
#[test]
fn test_tbc_spallation_life_decreases_with_delta_t() {
let tbc = TbcYsz::new(120.0, 1300.0, 950.0);
let n_low = tbc.spallation_life_cycles(100.0);
let n_high = tbc.spallation_life_cycles(200.0);
assert!(n_high < n_low);
}
#[test]
fn test_tbc_cte_mismatch_strain_sign() {
let tbc = TbcYsz::new(120.0, 1300.0, 950.0);
let strain = tbc.cte_mismatch_strain(500.0);
assert!(strain > 0.0);
}
#[test]
fn test_ablative_char_fraction_below_onset() {
let mat = AblativeMaterial::pica();
assert!((mat.char_fraction(300.0) - 0.0).abs() < 1e-12);
}
#[test]
fn test_ablative_char_fraction_above_complete() {
let mat = AblativeMaterial::pica();
assert!((mat.char_fraction(1000.0) - 1.0).abs() < 1e-12);
}
#[test]
fn test_ablative_char_fraction_midpoint() {
let mat = AblativeMaterial::pica();
let mid = (mat.pyrolysis_onset_c + mat.pyrolysis_complete_c) * 0.5;
let beta = mat.char_fraction(mid);
assert!((beta - 0.5).abs() < 1e-9);
}
#[test]
fn test_ablative_recession_rate_increases_with_flux() {
let mat = AblativeMaterial::pica();
let r1 = mat.recession_rate_mm_s(1.0, 800.0);
let r2 = mat.recession_rate_mm_s(5.0, 800.0);
assert!(r2 > r1);
}
#[test]
fn test_foam_modulus_increases_with_density() {
let f1 = MetalFoam::aluminium_open_cell(0.05);
let f2 = MetalFoam::aluminium_open_cell(0.10);
assert!(f2.youngs_modulus_gpa() > f1.youngs_modulus_gpa());
}
#[test]
fn test_foam_plateau_stress_positive() {
let foam = MetalFoam::aluminium_open_cell(0.08);
assert!(foam.plateau_stress_mpa() > 0.0);
}
#[test]
fn test_foam_densification_strain_range() {
let foam = MetalFoam::aluminium_open_cell(0.10);
let eps_d = foam.densification_strain();
assert!(eps_d > 0.0 && eps_d < 1.0, "eps_d = {eps_d}");
}
#[test]
fn test_foam_energy_absorption_positive() {
let foam = MetalFoam::aluminium_open_cell(0.10);
assert!(foam.energy_absorption_mj_m3() > 0.0);
}
#[test]
fn test_nitinol_superelastic_at_body_temp() {
let niti = Nitinol::biomedical_grade();
assert!(niti.is_superelastic());
}
#[test]
fn test_nitinol_effective_modulus_bounds() {
let niti = Nitinol::biomedical_grade();
let e_aust = niti.effective_modulus_gpa(0.0);
let e_mart = niti.effective_modulus_gpa(1.0);
assert!((e_aust - 83.0).abs() < 1e-9);
assert!((e_mart - 28.0).abs() < 1e-9);
}
#[test]
fn test_nitinol_critical_stress_increases_with_temperature() {
let niti_hot = Nitinol {
temperature_c: 60.0,
..Nitinol::biomedical_grade()
};
let niti_rt = Nitinol::biomedical_grade();
assert!(niti_hot.critical_sim_stress_mpa() > niti_rt.critical_sim_stress_mpa());
}
#[test]
fn test_hea_yield_hall_petch_effect() {
let fine = CantorhHea::new(25.0, 10.0); let coarse = CantorhHea::new(25.0, 100.0); assert!(fine.yield_strength_mpa() > coarse.yield_strength_mpa());
}
#[test]
fn test_hea_cryogenic_toughening() {
let cryo = CantorhHea::new(-196.0, 50.0);
let rt = CantorhHea::new(25.0, 50.0);
assert!(cryo.fracture_toughness_mpa_sqrtm() > rt.fracture_toughness_mpa_sqrtm());
}
#[test]
fn test_hea_uts_greater_than_yield() {
let hea = CantorhHea::new(25.0, 50.0);
assert!(hea.uts_mpa() > hea.yield_strength_mpa());
}
#[test]
fn test_reentry_heat_flux_positive() {
let rv = ReentryVehicle::new(0.5, 7500.0, 0.001, "PICA", 80.0);
assert!(rv.stagnation_heat_flux_w_m2() > 0.0);
}
#[test]
fn test_reentry_equilibrium_temperature_increases_with_flux() {
let rv_fast = ReentryVehicle::new(0.5, 8000.0, 0.001, "PICA", 80.0);
let rv_slow = ReentryVehicle::new(0.5, 5000.0, 0.001, "PICA", 80.0);
let t_fast = rv_fast.radiative_equilibrium_temperature_k(0.85);
let t_slow = rv_slow.radiative_equilibrium_temperature_k(0.85);
assert!(t_fast > t_slow);
}
#[test]
fn test_reentry_integrated_heat_load_positive() {
let rv = ReentryVehicle::new(0.5, 7500.0, 0.001, "PICA", 80.0);
assert!(rv.integrated_heat_load_mj_m2(60.0) > 0.0);
}
#[test]
fn test_required_tps_thickness_increases_with_time() {
let rv = ReentryVehicle::new(0.5, 7500.0, 0.001, "PICA", 80.0);
let alpha = 3.0e-7; let t1 = rv.required_tps_thickness_mm(alpha, 60.0);
let t2 = rv.required_tps_thickness_mm(alpha, 120.0);
assert!(t2 > t1);
}
#[test]
fn test_ti_tube_cross_section_positive() {
let tube = TitaniumTube::new(25.0, 1.5, 20.0);
assert!(tube.cross_section_area_mm2() > 0.0);
}
#[test]
fn test_ti_tube_euler_buckling_longer_is_weaker() {
let tube = TitaniumTube::new(25.0, 1.5, 20.0);
let p_short = tube.euler_buckling_load_n(200.0);
let p_long = tube.euler_buckling_load_n(500.0);
assert!(p_short > p_long);
}
#[test]
fn test_ti_tube_margin_of_safety_positive_under_low_load() {
let tube = TitaniumTube::new(25.0, 1.5, 20.0);
let mos = tube.margin_of_safety_yield(100.0); assert!(mos > 0.0, "MoS = {mos}");
}
#[test]
fn test_tgo_growth_parabolic() {
let ox = BondCoatOxidation::new(1050.0, 8.0);
let h1 = ox.tgo_thickness_um(100.0);
let h4 = ox.tgo_thickness_um(400.0);
assert!((h4 - 2.0 * h1).abs() < 1e-6, "h1={h1} h4={h4}");
}
#[test]
fn test_tgo_life_fraction_at_critical() {
let ox = BondCoatOxidation::new(1050.0, 8.0);
let h_crit = BondCoatOxidation::critical_tgo_thickness_um();
let lf = ox.life_fraction(h_crit);
assert!((lf - 1.0).abs() < 1e-9);
}
#[test]
fn test_tgo_rate_constant_increases_with_temperature() {
let kp_low = BondCoatOxidation::new(950.0, 8.0).parabolic_rate_constant_um2_h();
let kp_high = BondCoatOxidation::new(1100.0, 8.0).parabolic_rate_constant_um2_h();
assert!(kp_high > kp_low);
}
}