1#![allow(dead_code)]
17#![allow(clippy::too_many_arguments)]
18
19use std::f64::consts::PI;
20
21#[derive(Debug, Clone)]
31pub struct Ti6Al4V {
32 pub temperature_c: f64,
34}
35
36impl Ti6Al4V {
37 pub fn new(temperature_c: f64) -> Self {
39 Self {
40 temperature_c: temperature_c.min(950.0),
41 }
42 }
43
44 pub fn youngs_modulus_gpa(&self) -> f64 {
48 let t = self.temperature_c;
49 113.8 - 0.051 * t - 1.2e-5 * t * t
51 }
52
53 pub fn yield_strength_mpa(&self) -> f64 {
55 let t = self.temperature_c;
56 (950.0 - 0.55 * t).max(100.0)
58 }
59
60 pub fn uts_mpa(&self) -> f64 {
62 self.yield_strength_mpa() * 1.10 }
64
65 pub fn density_kg_m3(&self) -> f64 {
68 4430.0
69 }
70
71 pub fn thermal_conductivity_w_mk(&self) -> f64 {
73 let t = self.temperature_c;
74 6.7 + 0.0103 * t
76 }
77
78 pub fn specific_heat_j_kgk(&self) -> f64 {
80 let t = self.temperature_c;
81 526.0 + 0.13 * t
83 }
84
85 pub fn cte_per_k(&self) -> f64 {
87 let t = self.temperature_c;
88 (8.6 + 0.0014 * t) * 1e-6
89 }
90
91 pub fn thermal_diffusivity_m2_s(&self) -> f64 {
93 let k = self.thermal_conductivity_w_mk();
94 let rho = self.density_kg_m3();
95 let cp = self.specific_heat_j_kgk();
96 k / (rho * cp)
97 }
98
99 pub fn poisson_ratio(&self) -> f64 {
101 0.342
102 }
103}
104
105#[derive(Debug, Clone)]
114pub struct Inconel718 {
115 pub temperature_c: f64,
117}
118
119impl Inconel718 {
120 pub fn new(temperature_c: f64) -> Self {
122 Self { temperature_c }
123 }
124
125 pub fn youngs_modulus_gpa(&self) -> f64 {
127 let t = self.temperature_c;
128 200.0 - 0.08 * t
129 }
130
131 pub fn yield_strength_mpa(&self) -> f64 {
133 let t = self.temperature_c;
134 (1185.0 - 0.85 * t).max(200.0)
135 }
136
137 pub fn density_kg_m3(&self) -> f64 {
139 8190.0
140 }
141
142 pub fn norton_creep_rate_per_s(&self, stress_mpa: f64) -> f64 {
151 const A: f64 = 3.0e-28;
152 const N: f64 = 4.8;
153 const Q: f64 = 310_000.0; const R: f64 = 8.314; let t_k = self.temperature_c + 273.15;
156 A * stress_mpa.powf(N) * (-Q / (R * t_k)).exp()
157 }
158
159 pub fn rupture_life_hours(&self, stress_mpa: f64) -> f64 {
164 const C: f64 = 20.0;
165 let lmp = 29_000.0 - 22.0 * stress_mpa;
167 let t_k = self.temperature_c + 273.15;
168 let log_tr = lmp / t_k - C;
169 10.0_f64.powf(log_tr)
170 }
171
172 pub fn thermal_conductivity_w_mk(&self) -> f64 {
174 let t = self.temperature_c;
175 11.4 + 0.013 * t
176 }
177}
178
179#[derive(Debug, Clone, Copy)]
185pub struct CfrpPly {
186 pub angle_deg: f64,
188 pub thickness_mm: f64,
190 pub e1_gpa: f64,
192 pub e2_gpa: f64,
194 pub g12_gpa: f64,
196 pub nu12: f64,
198}
199
200impl CfrpPly {
201 pub fn im7_8552(angle_deg: f64) -> Self {
203 Self {
204 angle_deg,
205 thickness_mm: 0.125,
206 e1_gpa: 161.0,
207 e2_gpa: 11.38,
208 g12_gpa: 5.17,
209 nu12: 0.32,
210 }
211 }
212
213 pub fn reduced_stiffness_gpa(&self) -> [f64; 4] {
216 let e1 = self.e1_gpa;
217 let e2 = self.e2_gpa;
218 let g12 = self.g12_gpa;
219 let nu12 = self.nu12;
220 let nu21 = nu12 * e2 / e1;
221 let denom = 1.0 - nu12 * nu21;
222 let q11 = e1 / denom;
223 let q22 = e2 / denom;
224 let q12 = nu12 * e2 / denom;
225 let q66 = g12;
226 [q11, q22, q12, q66]
227 }
228
229 pub fn transformed_stiffness_gpa(&self) -> [f64; 6] {
232 let [q11, q22, q12, q66] = self.reduced_stiffness_gpa();
233 let theta = self.angle_deg.to_radians();
234 let c = theta.cos();
235 let s = theta.sin();
236 let c2 = c * c;
237 let s2 = s * s;
238 let c4 = c2 * c2;
239 let s4 = s2 * s2;
240 let c2s2 = c2 * s2;
241 let q11b = q11 * c4 + 2.0 * (q12 + 2.0 * q66) * c2s2 + q22 * s4;
242 let q22b = q11 * s4 + 2.0 * (q12 + 2.0 * q66) * c2s2 + q22 * c4;
243 let q12b = (q11 + q22 - 4.0 * q66) * c2s2 + q12 * (c4 + s4);
244 let q66b = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * c2s2 + q66 * (c2 - s2).powi(2);
245 let q16b = (q11 - q12 - 2.0 * q66) * c * c2 * s - (q22 - q12 - 2.0 * q66) * s * s2 * c;
246 let q26b = (q11 - q12 - 2.0 * q66) * s * s2 * c - (q22 - q12 - 2.0 * q66) * c * c2 * s;
247 [q11b, q22b, q12b, q66b, q16b, q26b]
248 }
249}
250
251#[derive(Debug, Clone)]
254pub struct CltResult {
255 pub a_matrix: [f64; 6],
257 pub total_thickness_mm: f64,
259 pub ex_gpa: f64,
261 pub ey_gpa: f64,
263 pub gxy_gpa: f64,
265}
266
267pub fn clt_analysis(plies: &[CfrpPly]) -> CltResult {
269 let mut a = [0.0f64; 6];
270 let mut total_t = 0.0f64;
271 for ply in plies {
272 let q_bar = ply.transformed_stiffness_gpa();
273 let t = ply.thickness_mm;
274 total_t += t;
275 for i in 0..6 {
276 a[i] += q_bar[i] * t; }
278 }
279 let a_nmm: [f64; 6] = a.map(|v| v * 1000.0);
281 let h = total_t;
282 let det = a_nmm[0] * a_nmm[1] - a_nmm[2] * a_nmm[2];
284 let ex = if a_nmm[1] * h > 1e-12 {
285 det / (a_nmm[1] * h) / 1000.0
286 } else {
287 0.0
288 }; let ey = if a_nmm[0] * h > 1e-12 {
290 det / (a_nmm[0] * h) / 1000.0
291 } else {
292 0.0
293 };
294 let gxy = a_nmm[3] / h / 1000.0;
295 CltResult {
296 a_matrix: a_nmm,
297 total_thickness_mm: h,
298 ex_gpa: ex,
299 ey_gpa: ey,
300 gxy_gpa: gxy,
301 }
302}
303
304#[derive(Debug, Clone)]
313pub struct SicSicCmc {
314 pub temperature_c: f64,
316 pub fibre_volume_fraction: f64,
318}
319
320impl SicSicCmc {
321 pub fn new(temperature_c: f64, fibre_volume_fraction: f64) -> Self {
323 Self {
324 temperature_c,
325 fibre_volume_fraction: fibre_volume_fraction.clamp(0.35, 0.55),
326 }
327 }
328
329 pub fn youngs_modulus_gpa(&self) -> f64 {
331 let vf = self.fibre_volume_fraction;
332 let e_fibre = 380.0; let e_matrix = 350.0; 0.5 * (e_fibre * vf + e_matrix * (1.0 - vf))
336 }
337
338 pub fn proportional_limit_mpa(&self) -> f64 {
340 150.0 - 0.08 * self.temperature_c
341 }
342
343 pub fn uts_mpa(&self) -> f64 {
345 (230.0 - 0.10 * self.temperature_c).max(50.0)
346 }
347
348 pub fn ilss_mpa(&self) -> f64 {
350 40.0 - 0.02 * self.temperature_c
351 }
352
353 pub fn density_kg_m3(&self) -> f64 {
355 2700.0
356 }
357
358 pub fn thermal_conductivity_w_mk(&self) -> f64 {
360 let t = self.temperature_c;
361 (18.0 * (1.0 - t / 1600.0)).max(3.0)
363 }
364
365 pub fn max_use_temperature_c(&self) -> f64 {
367 1200.0
368 }
369}
370
371#[derive(Debug, Clone)]
379pub struct TbcYsz {
380 pub thickness_um: f64,
382 pub surface_temperature_c: f64,
384 pub substrate_temperature_c: f64,
386}
387
388impl TbcYsz {
389 pub fn new(
391 thickness_um: f64,
392 surface_temperature_c: f64,
393 substrate_temperature_c: f64,
394 ) -> Self {
395 Self {
396 thickness_um,
397 surface_temperature_c,
398 substrate_temperature_c,
399 }
400 }
401
402 pub fn mean_temperature_c(&self) -> f64 {
404 (self.surface_temperature_c + self.substrate_temperature_c) * 0.5
405 }
406
407 pub fn thermal_conductivity_w_mk(&self) -> f64 {
412 let t = self.mean_temperature_c();
413 if t < 1200.0 {
414 1.9 - 2.0e-4 * t
415 } else {
416 1.7 + 1.5e-4 * (t - 1200.0)
418 }
419 }
420
421 pub fn temperature_drop_c(&self, heat_flux_w_m2: f64) -> f64 {
425 let k = self.thermal_conductivity_w_mk();
426 let t_m = self.thickness_um * 1e-6; heat_flux_w_m2 * t_m / k
428 }
429
430 pub fn spallation_life_cycles(&self, delta_t_cycle_c: f64) -> f64 {
436 const C: f64 = 5.0e6;
437 const M: f64 = 2.5;
438 C / delta_t_cycle_c.powf(M)
439 }
440
441 pub fn cte_mismatch_strain(&self, delta_t_c: f64) -> f64 {
444 let alpha_ysz = 11.0e-6;
445 let alpha_substrate = 14.0e-6;
446 (alpha_substrate - alpha_ysz) * delta_t_c
447 }
448}
449
450#[derive(Debug, Clone)]
459pub struct AblativeMaterial {
460 pub virgin_density_kg_m3: f64,
462 pub char_density_kg_m3: f64,
464 pub pyrolysis_onset_c: f64,
466 pub pyrolysis_complete_c: f64,
468 pub heat_of_pyrolysis_kj_kg: f64,
470 pub char_cp_j_kgk: f64,
472 pub virgin_cp_j_kgk: f64,
474}
475
476impl AblativeMaterial {
477 pub fn pica() -> Self {
479 Self {
480 virgin_density_kg_m3: 256.0,
481 char_density_kg_m3: 182.0,
482 pyrolysis_onset_c: 400.0,
483 pyrolysis_complete_c: 850.0,
484 heat_of_pyrolysis_kj_kg: 1750.0,
485 char_cp_j_kgk: 1500.0,
486 virgin_cp_j_kgk: 1200.0,
487 }
488 }
489
490 pub fn char_fraction(&self, temperature_c: f64) -> f64 {
493 if temperature_c <= self.pyrolysis_onset_c {
494 0.0
495 } else if temperature_c >= self.pyrolysis_complete_c {
496 1.0
497 } else {
498 (temperature_c - self.pyrolysis_onset_c)
499 / (self.pyrolysis_complete_c - self.pyrolysis_onset_c)
500 }
501 }
502
503 pub fn local_density_kg_m3(&self, beta: f64) -> f64 {
505 self.virgin_density_kg_m3 * (1.0 - beta) + self.char_density_kg_m3 * beta
506 }
507
508 pub fn blowing_correction(&self, t_wall_c: f64, t_recovery_c: f64) -> f64 {
514 let t_wall = t_wall_c + 273.15;
515 let t_rec = (t_recovery_c + 273.15).max(1.0);
516 0.5 * (t_wall / t_rec).sqrt()
517 }
518
519 pub fn recession_rate_mm_s(&self, heat_flux_mw_m2: f64, wall_temperature_c: f64) -> f64 {
524 const A_OX: f64 = 2.5e4; const E_A: f64 = 1.8e5; const R: f64 = 8.314;
527 let t_k = wall_temperature_c + 273.15;
528 let kinetic = A_OX * (-E_A / (R * t_k)).exp();
529 let hv = self.heat_of_pyrolysis_kj_kg * 1000.0; let rho_c = self.char_density_kg_m3;
531 let q = heat_flux_mw_m2 * 1e6; kinetic * q / (rho_c * hv) * 1000.0 }
534}
535
536#[derive(Debug, Clone, Copy)]
542pub struct MetalFoam {
543 pub relative_density: f64,
545 pub solid_modulus_gpa: f64,
547 pub solid_yield_mpa: f64,
549 pub solid_density_kg_m3: f64,
551 pub open_cell: bool,
553}
554
555impl MetalFoam {
556 pub fn aluminium_open_cell(relative_density: f64) -> Self {
558 Self {
559 relative_density,
560 solid_modulus_gpa: 70.0,
561 solid_yield_mpa: 270.0,
562 solid_density_kg_m3: 2700.0,
563 open_cell: true,
564 }
565 }
566
567 pub fn density_kg_m3(&self) -> f64 {
569 self.relative_density * self.solid_density_kg_m3
570 }
571
572 pub fn youngs_modulus_gpa(&self) -> f64 {
578 let r = self.relative_density;
579 if self.open_cell {
580 self.solid_modulus_gpa * r * r
581 } else {
582 let phi = 0.6f64;
583 self.solid_modulus_gpa * (phi * phi * r * r + (1.0 - phi) * r)
584 }
585 }
586
587 pub fn plateau_stress_mpa(&self) -> f64 {
592 let r = self.relative_density;
593 if self.open_cell {
594 0.3 * self.solid_yield_mpa * r.powf(1.5)
595 } else {
596 let phi = 0.6f64;
597 0.3 * self.solid_yield_mpa * ((phi * r).sqrt() + (1.0 - phi) * r)
598 }
599 }
600
601 pub fn densification_strain(&self) -> f64 {
604 1.0 - 1.4 * self.relative_density
605 }
606
607 pub fn energy_absorption_mj_m3(&self) -> f64 {
610 self.plateau_stress_mpa() * self.densification_strain() * 1e-3
611 }
612}
613
614#[derive(Debug, Clone)]
622pub struct Nitinol {
623 pub temperature_c: f64,
625 pub a_finish_c: f64,
627 pub m_start_c: f64,
629 pub ea_gpa: f64,
631 pub em_gpa: f64,
633}
634
635impl Nitinol {
636 pub fn biomedical_grade() -> Self {
638 Self {
639 temperature_c: 37.0, a_finish_c: 10.0,
641 m_start_c: -5.0,
642 ea_gpa: 83.0,
643 em_gpa: 28.0,
644 }
645 }
646
647 pub fn is_superelastic(&self) -> bool {
650 self.temperature_c > self.a_finish_c
651 }
652
653 pub fn effective_modulus_gpa(&self, martensite_fraction: f64) -> f64 {
658 let xi = martensite_fraction.clamp(0.0, 1.0);
659 self.ea_gpa * (1.0 - xi) + self.em_gpa * xi
660 }
661
662 pub fn critical_sim_stress_mpa(&self) -> f64 {
666 const C_A: f64 = 8.0; let delta_t = (self.temperature_c - self.a_finish_c).max(0.0);
668 200.0 + C_A * delta_t }
670
671 pub fn max_recoverable_strain_pct(&self) -> f64 {
674 if self.is_superelastic() { 8.0 } else { 2.0 }
675 }
676
677 pub fn density_kg_m3(&self) -> f64 {
679 6450.0
680 }
681}
682
683#[derive(Debug, Clone)]
691pub struct CantorhHea {
692 pub temperature_c: f64,
694 pub grain_size_um: f64,
696}
697
698impl CantorhHea {
699 pub fn new(temperature_c: f64, grain_size_um: f64) -> Self {
701 Self {
702 temperature_c,
703 grain_size_um,
704 }
705 }
706
707 pub fn youngs_modulus_gpa(&self) -> f64 {
709 let t = self.temperature_c;
710 202.0 - 0.06 * t
711 }
712
713 pub fn yield_strength_mpa(&self) -> f64 {
717 let sigma0 = 160.0; let k_hp = 800.0; let d = self.grain_size_um.max(1.0);
720 let t = self.temperature_c;
721 let hp = k_hp / d.sqrt();
722 let thermal = (0.4 * t).max(0.0); (sigma0 + hp - thermal).max(50.0)
724 }
725
726 pub fn uts_mpa(&self) -> f64 {
728 self.yield_strength_mpa() * 1.5 }
730
731 pub fn fracture_toughness_mpa_sqrtm(&self) -> f64 {
734 let t = self.temperature_c;
735 if t < 0.0 {
736 217.0 - 0.5 * t } else {
739 (217.0 - 0.1 * t).max(100.0)
740 }
741 }
742
743 pub fn stacking_fault_energy_mj_m2(&self) -> f64 {
746 let t = self.temperature_c;
747 18.0 + 0.02 * t
748 }
749
750 pub fn density_kg_m3(&self) -> f64 {
752 8000.0
753 }
754}
755
756#[derive(Debug, Clone)]
765pub struct ReentryVehicle {
766 pub nose_radius_m: f64,
768 pub entry_velocity_m_s: f64,
770 pub freestream_density_kg_m3: f64,
772 pub tps_material: String,
774 pub tps_thickness_mm: f64,
776}
777
778impl ReentryVehicle {
779 pub fn new(
781 nose_radius_m: f64,
782 entry_velocity_m_s: f64,
783 freestream_density_kg_m3: f64,
784 tps_material: &str,
785 tps_thickness_mm: f64,
786 ) -> Self {
787 Self {
788 nose_radius_m,
789 entry_velocity_m_s,
790 freestream_density_kg_m3,
791 tps_material: tps_material.to_string(),
792 tps_thickness_mm,
793 }
794 }
795
796 pub fn stagnation_heat_flux_w_m2(&self) -> f64 {
803 const K_SG: f64 = 1.742e-4;
804 let rho = self.freestream_density_kg_m3;
805 let r = self.nose_radius_m;
806 let v = self.entry_velocity_m_s;
807 K_SG * (rho / r).sqrt() * v.powi(3)
808 }
809
810 pub fn radiative_equilibrium_temperature_k(&self, emissivity: f64) -> f64 {
815 const SIGMA: f64 = 5.670_374_419e-8; let q = self.stagnation_heat_flux_w_m2();
817 let eps = emissivity.clamp(0.1, 1.0);
818 (q / (eps * SIGMA)).powf(0.25)
819 }
820
821 pub fn integrated_heat_load_mj_m2(&self, entry_duration_s: f64) -> f64 {
825 let q_peak = self.stagnation_heat_flux_w_m2();
826 2.0 / 3.0 * q_peak * entry_duration_s / 1e6
828 }
829
830 pub fn required_tps_thickness_mm(
836 &self,
837 tps_thermal_diffusivity_m2_s: f64,
838 entry_duration_s: f64,
839 ) -> f64 {
840 let alpha = tps_thermal_diffusivity_m2_s;
841 let t = entry_duration_s;
842 1.5 * (alpha * t).sqrt() * 1000.0 }
844
845 pub fn ballistic_coefficient_kg_m2(&self, mass_kg: f64, cd: f64) -> f64 {
849 let a_ref = PI * self.nose_radius_m * self.nose_radius_m;
850 mass_kg / (cd * a_ref)
851 }
852}
853
854#[derive(Debug, Clone, Copy)]
863pub struct TitaniumTube {
864 pub outer_diameter_mm: f64,
866 pub wall_thickness_mm: f64,
868 pub temperature_c: f64,
870}
871
872impl TitaniumTube {
873 pub fn new(outer_diameter_mm: f64, wall_thickness_mm: f64, temperature_c: f64) -> Self {
875 Self {
876 outer_diameter_mm,
877 wall_thickness_mm,
878 temperature_c,
879 }
880 }
881
882 pub fn cross_section_area_mm2(&self) -> f64 {
884 let ro = self.outer_diameter_mm * 0.5;
885 let ri = ro - self.wall_thickness_mm;
886 PI * (ro * ro - ri * ri)
887 }
888
889 pub fn second_moment_mm4(&self) -> f64 {
891 let ro = self.outer_diameter_mm * 0.5;
892 let ri = ro - self.wall_thickness_mm;
893 PI / 4.0 * (ro.powi(4) - ri.powi(4))
894 }
895
896 pub fn euler_buckling_load_n(&self, length_mm: f64) -> f64 {
898 let mat = Ti6Al4V::new(self.temperature_c);
899 let e = mat.youngs_modulus_gpa() * 1e3; PI * PI * e * self.second_moment_mm4() / (length_mm * length_mm)
901 }
902
903 pub fn axial_yield_load_n(&self) -> f64 {
905 let mat = Ti6Al4V::new(self.temperature_c);
906 mat.yield_strength_mpa() * self.cross_section_area_mm2()
907 }
908
909 pub fn margin_of_safety_yield(&self, applied_load_n: f64) -> f64 {
911 self.axial_yield_load_n() / applied_load_n - 1.0
912 }
913}
914
915#[derive(Debug, Clone)]
924pub struct BondCoatOxidation {
925 pub temperature_c: f64,
927 pub al_content_wt_pct: f64,
929}
930
931impl BondCoatOxidation {
932 pub fn new(temperature_c: f64, al_content_wt_pct: f64) -> Self {
934 Self {
935 temperature_c,
936 al_content_wt_pct,
937 }
938 }
939
940 pub fn parabolic_rate_constant_um2_h(&self) -> f64 {
945 const A0: f64 = 2.0e6; const Q: f64 = 220_000.0; const R: f64 = 8.314;
948 let t_k = self.temperature_c + 273.15;
949 let al_factor = (self.al_content_wt_pct / 10.0).sqrt().max(0.1);
950 A0 * al_factor * (-Q / (R * t_k)).exp()
951 }
952
953 pub fn tgo_thickness_um(&self, time_h: f64) -> f64 {
955 (self.parabolic_rate_constant_um2_h() * time_h).sqrt()
956 }
957
958 pub fn critical_tgo_thickness_um() -> f64 {
961 7.0
962 }
963
964 pub fn life_fraction(&self, tgo_thickness_um: f64) -> f64 {
966 (tgo_thickness_um / Self::critical_tgo_thickness_um())
967 .powi(2)
968 .min(1.0)
969 }
970}
971
972#[cfg(test)]
977mod tests {
978 use super::*;
979
980 #[test]
983 fn test_ti64_youngs_modulus_at_rt() {
984 let mat = Ti6Al4V::new(20.0);
985 let e = mat.youngs_modulus_gpa();
986 assert!(e > 110.0 && e < 115.0, "E = {e}");
988 }
989
990 #[test]
991 fn test_ti64_modulus_decreases_with_temperature() {
992 let e_rt = Ti6Al4V::new(20.0).youngs_modulus_gpa();
993 let e_hot = Ti6Al4V::new(600.0).youngs_modulus_gpa();
994 assert!(e_hot < e_rt);
995 }
996
997 #[test]
998 fn test_ti64_yield_strength_decreases() {
999 let sy_rt = Ti6Al4V::new(20.0).yield_strength_mpa();
1000 let sy_hot = Ti6Al4V::new(500.0).yield_strength_mpa();
1001 assert!(sy_hot < sy_rt);
1002 }
1003
1004 #[test]
1005 fn test_ti64_thermal_conductivity_increases() {
1006 let k_rt = Ti6Al4V::new(20.0).thermal_conductivity_w_mk();
1007 let k_hot = Ti6Al4V::new(500.0).thermal_conductivity_w_mk();
1008 assert!(k_hot > k_rt);
1009 }
1010
1011 #[test]
1012 fn test_ti64_thermal_diffusivity_positive() {
1013 let alpha = Ti6Al4V::new(300.0).thermal_diffusivity_m2_s();
1014 assert!(alpha > 0.0);
1015 }
1016
1017 #[test]
1020 fn test_inconel_creep_rate_increases_with_stress() {
1021 let mat = Inconel718::new(650.0);
1022 let cr_low = mat.norton_creep_rate_per_s(300.0);
1023 let cr_high = mat.norton_creep_rate_per_s(600.0);
1024 assert!(cr_high > cr_low);
1025 }
1026
1027 #[test]
1028 fn test_inconel_creep_rate_increases_with_temperature() {
1029 let cr_cool = Inconel718::new(540.0).norton_creep_rate_per_s(500.0);
1030 let cr_hot = Inconel718::new(760.0).norton_creep_rate_per_s(500.0);
1031 assert!(cr_hot > cr_cool);
1032 }
1033
1034 #[test]
1035 fn test_inconel_rupture_life_decreases_with_stress() {
1036 let mat = Inconel718::new(650.0);
1037 let life_low = mat.rupture_life_hours(400.0);
1038 let life_high = mat.rupture_life_hours(700.0);
1039 assert!(life_high < life_low);
1040 }
1041
1042 #[test]
1043 fn test_inconel_density() {
1044 let mat = Inconel718::new(25.0);
1045 assert!((mat.density_kg_m3() - 8190.0).abs() < 1.0);
1046 }
1047
1048 #[test]
1051 fn test_cfrp_ply_reduced_stiffness_positive() {
1052 let ply = CfrpPly::im7_8552(0.0);
1053 let q = ply.reduced_stiffness_gpa();
1054 for &qi in &q {
1055 assert!(qi > 0.0, "Q component non-positive: {qi}");
1056 }
1057 }
1058
1059 #[test]
1060 fn test_clt_quasi_isotropic_laminate() {
1061 let angles = [0.0, 45.0, -45.0, 90.0, 90.0, -45.0, 45.0, 0.0];
1063 let plies: Vec<CfrpPly> = angles.iter().map(|&a| CfrpPly::im7_8552(a)).collect();
1064 let res = clt_analysis(&plies);
1065 let diff = (res.ex_gpa - res.ey_gpa).abs() / res.ex_gpa;
1067 assert!(
1068 diff < 0.05,
1069 "Ex={:.2} Ey={:.2} diff={diff:.4}",
1070 res.ex_gpa,
1071 res.ey_gpa
1072 );
1073 }
1074
1075 #[test]
1076 fn test_clt_zero_only_laminate_max_modulus() {
1077 let plies = vec![CfrpPly::im7_8552(0.0); 8];
1078 let unidirectional = clt_analysis(&plies);
1079 assert!(unidirectional.ex_gpa > 100.0);
1081 }
1082
1083 #[test]
1084 fn test_clt_thickness_sum() {
1085 let plies: Vec<CfrpPly> = [0.0, 90.0, 0.0, 90.0]
1086 .iter()
1087 .map(|&a| CfrpPly::im7_8552(a))
1088 .collect();
1089 let res = clt_analysis(&plies);
1090 let expected = 4.0 * 0.125;
1091 assert!((res.total_thickness_mm - expected).abs() < 1e-9);
1092 }
1093
1094 #[test]
1097 fn test_sic_sic_modulus_range() {
1098 let cmc = SicSicCmc::new(1000.0, 0.45);
1099 let e = cmc.youngs_modulus_gpa();
1100 assert!(e > 100.0 && e < 220.0, "E = {e}");
1101 }
1102
1103 #[test]
1104 fn test_sic_sic_uts_decreases_with_temperature() {
1105 let uts_low = SicSicCmc::new(800.0, 0.45).uts_mpa();
1106 let uts_high = SicSicCmc::new(1200.0, 0.45).uts_mpa();
1107 assert!(uts_high < uts_low);
1108 }
1109
1110 #[test]
1111 fn test_sic_sic_max_use_temperature() {
1112 let cmc = SicSicCmc::new(25.0, 0.40);
1113 assert!((cmc.max_use_temperature_c() - 1200.0).abs() < 1.0);
1114 }
1115
1116 #[test]
1119 fn test_tbc_temperature_drop_proportional_to_flux() {
1120 let tbc = TbcYsz::new(120.0, 1300.0, 950.0);
1121 let dt1 = tbc.temperature_drop_c(1e6);
1122 let dt2 = tbc.temperature_drop_c(2e6);
1123 assert!((dt2 - 2.0 * dt1).abs() < 1e-6, "dt1={dt1} dt2={dt2}");
1124 }
1125
1126 #[test]
1127 fn test_tbc_spallation_life_decreases_with_delta_t() {
1128 let tbc = TbcYsz::new(120.0, 1300.0, 950.0);
1129 let n_low = tbc.spallation_life_cycles(100.0);
1130 let n_high = tbc.spallation_life_cycles(200.0);
1131 assert!(n_high < n_low);
1132 }
1133
1134 #[test]
1135 fn test_tbc_cte_mismatch_strain_sign() {
1136 let tbc = TbcYsz::new(120.0, 1300.0, 950.0);
1137 let strain = tbc.cte_mismatch_strain(500.0);
1139 assert!(strain > 0.0);
1140 }
1141
1142 #[test]
1145 fn test_ablative_char_fraction_below_onset() {
1146 let mat = AblativeMaterial::pica();
1147 assert!((mat.char_fraction(300.0) - 0.0).abs() < 1e-12);
1148 }
1149
1150 #[test]
1151 fn test_ablative_char_fraction_above_complete() {
1152 let mat = AblativeMaterial::pica();
1153 assert!((mat.char_fraction(1000.0) - 1.0).abs() < 1e-12);
1154 }
1155
1156 #[test]
1157 fn test_ablative_char_fraction_midpoint() {
1158 let mat = AblativeMaterial::pica();
1159 let mid = (mat.pyrolysis_onset_c + mat.pyrolysis_complete_c) * 0.5;
1160 let beta = mat.char_fraction(mid);
1161 assert!((beta - 0.5).abs() < 1e-9);
1162 }
1163
1164 #[test]
1165 fn test_ablative_recession_rate_increases_with_flux() {
1166 let mat = AblativeMaterial::pica();
1167 let r1 = mat.recession_rate_mm_s(1.0, 800.0);
1168 let r2 = mat.recession_rate_mm_s(5.0, 800.0);
1169 assert!(r2 > r1);
1170 }
1171
1172 #[test]
1175 fn test_foam_modulus_increases_with_density() {
1176 let f1 = MetalFoam::aluminium_open_cell(0.05);
1177 let f2 = MetalFoam::aluminium_open_cell(0.10);
1178 assert!(f2.youngs_modulus_gpa() > f1.youngs_modulus_gpa());
1179 }
1180
1181 #[test]
1182 fn test_foam_plateau_stress_positive() {
1183 let foam = MetalFoam::aluminium_open_cell(0.08);
1184 assert!(foam.plateau_stress_mpa() > 0.0);
1185 }
1186
1187 #[test]
1188 fn test_foam_densification_strain_range() {
1189 let foam = MetalFoam::aluminium_open_cell(0.10);
1190 let eps_d = foam.densification_strain();
1191 assert!(eps_d > 0.0 && eps_d < 1.0, "eps_d = {eps_d}");
1192 }
1193
1194 #[test]
1195 fn test_foam_energy_absorption_positive() {
1196 let foam = MetalFoam::aluminium_open_cell(0.10);
1197 assert!(foam.energy_absorption_mj_m3() > 0.0);
1198 }
1199
1200 #[test]
1203 fn test_nitinol_superelastic_at_body_temp() {
1204 let niti = Nitinol::biomedical_grade();
1205 assert!(niti.is_superelastic());
1206 }
1207
1208 #[test]
1209 fn test_nitinol_effective_modulus_bounds() {
1210 let niti = Nitinol::biomedical_grade();
1211 let e_aust = niti.effective_modulus_gpa(0.0);
1212 let e_mart = niti.effective_modulus_gpa(1.0);
1213 assert!((e_aust - 83.0).abs() < 1e-9);
1214 assert!((e_mart - 28.0).abs() < 1e-9);
1215 }
1216
1217 #[test]
1218 fn test_nitinol_critical_stress_increases_with_temperature() {
1219 let niti_hot = Nitinol {
1220 temperature_c: 60.0,
1221 ..Nitinol::biomedical_grade()
1222 };
1223 let niti_rt = Nitinol::biomedical_grade();
1224 assert!(niti_hot.critical_sim_stress_mpa() > niti_rt.critical_sim_stress_mpa());
1225 }
1226
1227 #[test]
1230 fn test_hea_yield_hall_petch_effect() {
1231 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());
1234 }
1235
1236 #[test]
1237 fn test_hea_cryogenic_toughening() {
1238 let cryo = CantorhHea::new(-196.0, 50.0);
1239 let rt = CantorhHea::new(25.0, 50.0);
1240 assert!(cryo.fracture_toughness_mpa_sqrtm() > rt.fracture_toughness_mpa_sqrtm());
1241 }
1242
1243 #[test]
1244 fn test_hea_uts_greater_than_yield() {
1245 let hea = CantorhHea::new(25.0, 50.0);
1246 assert!(hea.uts_mpa() > hea.yield_strength_mpa());
1247 }
1248
1249 #[test]
1252 fn test_reentry_heat_flux_positive() {
1253 let rv = ReentryVehicle::new(0.5, 7500.0, 0.001, "PICA", 80.0);
1254 assert!(rv.stagnation_heat_flux_w_m2() > 0.0);
1255 }
1256
1257 #[test]
1258 fn test_reentry_equilibrium_temperature_increases_with_flux() {
1259 let rv_fast = ReentryVehicle::new(0.5, 8000.0, 0.001, "PICA", 80.0);
1260 let rv_slow = ReentryVehicle::new(0.5, 5000.0, 0.001, "PICA", 80.0);
1261 let t_fast = rv_fast.radiative_equilibrium_temperature_k(0.85);
1262 let t_slow = rv_slow.radiative_equilibrium_temperature_k(0.85);
1263 assert!(t_fast > t_slow);
1264 }
1265
1266 #[test]
1267 fn test_reentry_integrated_heat_load_positive() {
1268 let rv = ReentryVehicle::new(0.5, 7500.0, 0.001, "PICA", 80.0);
1269 assert!(rv.integrated_heat_load_mj_m2(60.0) > 0.0);
1270 }
1271
1272 #[test]
1273 fn test_required_tps_thickness_increases_with_time() {
1274 let rv = ReentryVehicle::new(0.5, 7500.0, 0.001, "PICA", 80.0);
1275 let alpha = 3.0e-7; let t1 = rv.required_tps_thickness_mm(alpha, 60.0);
1277 let t2 = rv.required_tps_thickness_mm(alpha, 120.0);
1278 assert!(t2 > t1);
1279 }
1280
1281 #[test]
1284 fn test_ti_tube_cross_section_positive() {
1285 let tube = TitaniumTube::new(25.0, 1.5, 20.0);
1286 assert!(tube.cross_section_area_mm2() > 0.0);
1287 }
1288
1289 #[test]
1290 fn test_ti_tube_euler_buckling_longer_is_weaker() {
1291 let tube = TitaniumTube::new(25.0, 1.5, 20.0);
1292 let p_short = tube.euler_buckling_load_n(200.0);
1293 let p_long = tube.euler_buckling_load_n(500.0);
1294 assert!(p_short > p_long);
1295 }
1296
1297 #[test]
1298 fn test_ti_tube_margin_of_safety_positive_under_low_load() {
1299 let tube = TitaniumTube::new(25.0, 1.5, 20.0);
1300 let mos = tube.margin_of_safety_yield(100.0); assert!(mos > 0.0, "MoS = {mos}");
1302 }
1303
1304 #[test]
1307 fn test_tgo_growth_parabolic() {
1308 let ox = BondCoatOxidation::new(1050.0, 8.0);
1309 let h1 = ox.tgo_thickness_um(100.0);
1310 let h4 = ox.tgo_thickness_um(400.0);
1311 assert!((h4 - 2.0 * h1).abs() < 1e-6, "h1={h1} h4={h4}");
1313 }
1314
1315 #[test]
1316 fn test_tgo_life_fraction_at_critical() {
1317 let ox = BondCoatOxidation::new(1050.0, 8.0);
1318 let h_crit = BondCoatOxidation::critical_tgo_thickness_um();
1319 let lf = ox.life_fraction(h_crit);
1320 assert!((lf - 1.0).abs() < 1e-9);
1321 }
1322
1323 #[test]
1324 fn test_tgo_rate_constant_increases_with_temperature() {
1325 let kp_low = BondCoatOxidation::new(950.0, 8.0).parabolic_rate_constant_um2_h();
1326 let kp_high = BondCoatOxidation::new(1100.0, 8.0).parabolic_rate_constant_um2_h();
1327 assert!(kp_high > kp_low);
1328 }
1329}