Skip to main content

oxiphysics_materials/
aerospace_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Aerospace structural material models.
5//!
6//! Covers temperature-dependent titanium alloys (Ti-6Al-4V), nickel
7//! superalloys (Inconel 718 creep), carbon-fibre reinforced polymer (CFRP)
8//! with classical laminate theory, ceramic matrix composites (SiC/SiC CMC),
9//! thermal barrier coatings (7YSZ zirconia), ablative materials (char
10//! formation), metal foams (Gibson–Ashby), shape-memory alloys (NiTi
11//! Nitinol), high-entropy alloys (HEA), and reentry vehicle thermal
12//! protection systems (TPS).
13//!
14//! All computations use plain `f64` — no external linear-algebra dependency.
15
16#![allow(dead_code)]
17#![allow(clippy::too_many_arguments)]
18
19use std::f64::consts::PI;
20
21// ---------------------------------------------------------------------------
22// 1. Titanium alloy Ti-6Al-4V — temperature-dependent properties
23// ---------------------------------------------------------------------------
24
25/// Temperature-dependent mechanical and thermal properties of Ti-6Al-4V.
26///
27/// Data fit to MMPDS/MIL-HDBK-5 curves valid from room temperature to
28/// ~850 °C.  Above the beta-transus (~995 °C) the alloy softens rapidly;
29/// this model clamps at 950 °C.
30#[derive(Debug, Clone)]
31pub struct Ti6Al4V {
32    /// Reference temperature for property evaluation (°C).
33    pub temperature_c: f64,
34}
35
36impl Ti6Al4V {
37    /// Construct with a given temperature in °C.
38    pub fn new(temperature_c: f64) -> Self {
39        Self {
40            temperature_c: temperature_c.min(950.0),
41        }
42    }
43
44    /// Young's modulus (GPa) as a function of temperature.
45    ///
46    /// Fitted linear-quadratic curve through MMPDS data points.
47    pub fn youngs_modulus_gpa(&self) -> f64 {
48        let t = self.temperature_c;
49        // E(T) ≈ 113.8 − 0.051·T − 1.2e-5·T² GPa
50        113.8 - 0.051 * t - 1.2e-5 * t * t
51    }
52
53    /// Yield strength (MPa, 0.2 % proof) as a function of temperature.
54    pub fn yield_strength_mpa(&self) -> f64 {
55        let t = self.temperature_c;
56        // σ_y(T) ≈ 950 − 0.55·T GPa (decreasing above RT)
57        (950.0 - 0.55 * t).max(100.0)
58    }
59
60    /// Ultimate tensile strength (MPa).
61    pub fn uts_mpa(&self) -> f64 {
62        self.yield_strength_mpa() * 1.10 // ~10 % above yield across temperature range
63    }
64
65    /// Density (kg/m³) — effectively temperature-independent for engineering
66    /// purposes.
67    pub fn density_kg_m3(&self) -> f64 {
68        4430.0
69    }
70
71    /// Thermal conductivity (W/m·K).
72    pub fn thermal_conductivity_w_mk(&self) -> f64 {
73        let t = self.temperature_c;
74        // k(T) ≈ 6.7 + 0.0103·T
75        6.7 + 0.0103 * t
76    }
77
78    /// Specific heat capacity (J/kg·K).
79    pub fn specific_heat_j_kgk(&self) -> f64 {
80        let t = self.temperature_c;
81        // cp(T) ≈ 526 + 0.13·T J/(kg·K)
82        526.0 + 0.13 * t
83    }
84
85    /// Coefficient of thermal expansion (µm/m·K, i.e. 10⁻⁶ /K).
86    pub fn cte_per_k(&self) -> f64 {
87        let t = self.temperature_c;
88        (8.6 + 0.0014 * t) * 1e-6
89    }
90
91    /// Thermal diffusivity α = k / (ρ·cₚ) in m²/s.
92    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    /// Poisson's ratio (essentially constant with temperature).
100    pub fn poisson_ratio(&self) -> f64 {
101        0.342
102    }
103}
104
105// ---------------------------------------------------------------------------
106// 2. Nickel superalloy Inconel 718 — creep model
107// ---------------------------------------------------------------------------
108
109/// Inconel 718 material properties and Norton power-law creep model.
110///
111/// Creep data reference: Ludwigson & Berger (1969); temperature range
112/// 540–760 °C.
113#[derive(Debug, Clone)]
114pub struct Inconel718 {
115    /// Service temperature (°C).
116    pub temperature_c: f64,
117}
118
119impl Inconel718 {
120    /// Create with service temperature.
121    pub fn new(temperature_c: f64) -> Self {
122        Self { temperature_c }
123    }
124
125    /// Young's modulus (GPa).
126    pub fn youngs_modulus_gpa(&self) -> f64 {
127        let t = self.temperature_c;
128        200.0 - 0.08 * t
129    }
130
131    /// 0.2 % yield strength (MPa).
132    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    /// Density (kg/m³).
138    pub fn density_kg_m3(&self) -> f64 {
139        8190.0
140    }
141
142    /// Norton power-law creep rate (s⁻¹) at a given stress (MPa).
143    ///
144    /// `ε̇_cr = A · σⁿ · exp(−Q / RT)`
145    ///
146    /// Constants fit to Inconel 718 creep data:
147    /// - A = 3.0×10⁻²⁸ (MPa⁻ⁿ · s⁻¹)
148    /// - n = 4.8
149    /// - Q = 310 kJ/mol (activation energy)
150    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; // J/mol
154        const R: f64 = 8.314; // J/(mol·K)
155        let t_k = self.temperature_c + 273.15;
156        A * stress_mpa.powf(N) * (-Q / (R * t_k)).exp()
157    }
158
159    /// Creep rupture life estimate (hours) using the Larson–Miller parameter.
160    ///
161    /// LMP = T(K) × (log₁₀(t_r) + C) where C ≈ 20 for Inconel 718.
162    /// This is the inverse: given LMP, solve for t_r in hours.
163    pub fn rupture_life_hours(&self, stress_mpa: f64) -> f64 {
164        const C: f64 = 20.0;
165        // LMP master curve (simplified fit):  LMP = 29000 - 22·σ  (σ in MPa)
166        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    /// Thermal conductivity (W/m·K).
173    pub fn thermal_conductivity_w_mk(&self) -> f64 {
174        let t = self.temperature_c;
175        11.4 + 0.013 * t
176    }
177}
178
179// ---------------------------------------------------------------------------
180// 3. CFRP — Classical Laminate Theory (CLT)
181// ---------------------------------------------------------------------------
182
183/// A single unidirectional CFRP ply for CLT analysis.
184#[derive(Debug, Clone, Copy)]
185pub struct CfrpPly {
186    /// Ply orientation angle (degrees, measured from laminate x-axis).
187    pub angle_deg: f64,
188    /// Ply thickness (mm).
189    pub thickness_mm: f64,
190    /// Longitudinal modulus E₁ (GPa).
191    pub e1_gpa: f64,
192    /// Transverse modulus E₂ (GPa).
193    pub e2_gpa: f64,
194    /// In-plane shear modulus G₁₂ (GPa).
195    pub g12_gpa: f64,
196    /// Major Poisson's ratio ν₁₂.
197    pub nu12: f64,
198}
199
200impl CfrpPly {
201    /// Standard IM7/8552 carbon/epoxy ply.
202    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    /// Reduced stiffness components Q₁₁, Q₂₂, Q₁₂, Q₆₆ in the ply
214    /// material axes (GPa).
215    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    /// Transformed reduced stiffness Q̄ components \[Q̄₁₁, Q̄₂₂, Q̄₁₂, Q̄₆₆,
230    /// Q̄₁₆, Q̄₂₆] (GPa) in the laminate coordinate frame.
231    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/// Classical laminate theory analysis result for a symmetric balanced
252/// laminate.
253#[derive(Debug, Clone)]
254pub struct CltResult {
255    /// In-plane stiffness matrix A (N/mm): \[A₁₁, A₂₂, A₁₂, A₆₆, A₁₆, A₂₆\].
256    pub a_matrix: [f64; 6],
257    /// Total laminate thickness (mm).
258    pub total_thickness_mm: f64,
259    /// Effective in-plane modulus Ex (GPa).
260    pub ex_gpa: f64,
261    /// Effective in-plane modulus Ey (GPa).
262    pub ey_gpa: f64,
263    /// Effective in-plane shear modulus Gxy (GPa).
264    pub gxy_gpa: f64,
265}
266
267/// Compute CLT A-matrix and effective properties for a list of plies.
268pub 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; // A_ij = Σ Q̄_ij * t_k (GPa·mm = kN/mm effectively)
277        }
278    }
279    // Convert A from GPa·mm to N/mm by ×1000
280    let a_nmm: [f64; 6] = a.map(|v| v * 1000.0);
281    let h = total_t;
282    // Ex = (A11*A22 - A12²) / (A22*h)
283    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    }; // GPa
289    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// ---------------------------------------------------------------------------
305// 4. Ceramic matrix composite — SiC/SiC
306// ---------------------------------------------------------------------------
307
308/// Mechanical and thermophysical properties of a SiC/SiC 2-D woven CMC.
309///
310/// Property ranges are representative of Hi-Nicalon Type S / CVI-SiC matrix
311/// with BN interphase, as reported in UEET programme data.
312#[derive(Debug, Clone)]
313pub struct SicSicCmc {
314    /// Service temperature (°C).
315    pub temperature_c: f64,
316    /// Fibre volume fraction (0–1).
317    pub fibre_volume_fraction: f64,
318}
319
320impl SicSicCmc {
321    /// Construct with temperature and fibre volume fraction.
322    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    /// In-plane Young's modulus (GPa) using rule-of-mixtures.
330    pub fn youngs_modulus_gpa(&self) -> f64 {
331        let vf = self.fibre_volume_fraction;
332        let e_fibre = 380.0; // Hi-Nicalon Type S, GPa
333        let e_matrix = 350.0; // CVI SiC matrix, GPa
334        // For a 2-D woven fabric, modulus ≈ 0.5·E_longitudinal (average over ±0°/90°)
335        0.5 * (e_fibre * vf + e_matrix * (1.0 - vf))
336    }
337
338    /// Proportional limit stress (MPa) — onset of matrix cracking.
339    pub fn proportional_limit_mpa(&self) -> f64 {
340        150.0 - 0.08 * self.temperature_c
341    }
342
343    /// Ultimate tensile strength (MPa).
344    pub fn uts_mpa(&self) -> f64 {
345        (230.0 - 0.10 * self.temperature_c).max(50.0)
346    }
347
348    /// Interlaminar shear strength (MPa).
349    pub fn ilss_mpa(&self) -> f64 {
350        40.0 - 0.02 * self.temperature_c
351    }
352
353    /// Density (kg/m³).
354    pub fn density_kg_m3(&self) -> f64 {
355        2700.0
356    }
357
358    /// Thermal conductivity (W/m·K).
359    pub fn thermal_conductivity_w_mk(&self) -> f64 {
360        let t = self.temperature_c;
361        // Decreasing with temperature (phonon scattering)
362        (18.0 * (1.0 - t / 1600.0)).max(3.0)
363    }
364
365    /// Maximum use temperature for load-bearing applications (°C).
366    pub fn max_use_temperature_c(&self) -> f64 {
367        1200.0
368    }
369}
370
371// ---------------------------------------------------------------------------
372// 5. Thermal barrier coating (TBC) — 7YSZ zirconia
373// ---------------------------------------------------------------------------
374
375/// Thermal barrier coating properties for yttria-stabilised zirconia (7YSZ).
376///
377/// Represents an EB-PVD columnar TBC as used on high-pressure turbine blades.
378#[derive(Debug, Clone)]
379pub struct TbcYsz {
380    /// Coating thickness (µm).
381    pub thickness_um: f64,
382    /// Service gas-side surface temperature (°C).
383    pub surface_temperature_c: f64,
384    /// Substrate (bond coat) temperature (°C).
385    pub substrate_temperature_c: f64,
386}
387
388impl TbcYsz {
389    /// Create a TBC descriptor.
390    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    /// Mean temperature of the coating (°C).
403    pub fn mean_temperature_c(&self) -> f64 {
404        (self.surface_temperature_c + self.substrate_temperature_c) * 0.5
405    }
406
407    /// Thermal conductivity of 7YSZ (W/m·K).
408    ///
409    /// EB-PVD coatings have k ≈ 1.5–2.0 W/m·K; sintering reduces k above
410    /// ~1200 °C.
411    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            // Sintering-induced densification increases k
417            1.7 + 1.5e-4 * (t - 1200.0)
418        }
419    }
420
421    /// Temperature drop across the TBC (°C).
422    ///
423    /// ΔT = q'' · t / k, where q'' is the heat flux (W/m²).
424    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; // convert µm to m
427        heat_flux_w_m2 * t_m / k
428    }
429
430    /// Thermal cycling life estimate (cycles) using a simplified Paris-law
431    /// spallation model.
432    ///
433    /// N_f ≈ C / (ΔT_cycle)^m, with C = 5×10⁶, m = 2.5 (calibrated to
434    /// standard furnace-cycle data for 120 µm EB-PVD TBC).
435    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    /// Coefficient of thermal expansion mismatch strain with Ni superalloy
442    /// substrate (7YSZ α ≈ 11 µm/m·K vs substrate α ≈ 14 µm/m·K).
443    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// ---------------------------------------------------------------------------
451// 6. Ablative material — char formation and recession
452// ---------------------------------------------------------------------------
453
454/// Ablative heat shield material descriptor.
455///
456/// Models phenolic-impregnated carbon ablator (PICA-like) charring and
457/// surface recession during atmospheric reentry.
458#[derive(Debug, Clone)]
459pub struct AblativeMaterial {
460    /// Virgin material density (kg/m³).
461    pub virgin_density_kg_m3: f64,
462    /// Char density (kg/m³).
463    pub char_density_kg_m3: f64,
464    /// Pyrolysis onset temperature (°C).
465    pub pyrolysis_onset_c: f64,
466    /// Pyrolysis completion temperature (°C).
467    pub pyrolysis_complete_c: f64,
468    /// Effective heat of pyrolysis (kJ/kg).
469    pub heat_of_pyrolysis_kj_kg: f64,
470    /// Char specific heat (J/kg·K).
471    pub char_cp_j_kgk: f64,
472    /// Virgin specific heat (J/kg·K).
473    pub virgin_cp_j_kgk: f64,
474}
475
476impl AblativeMaterial {
477    /// PICA-representative ablator (Phenolic Impregnated Carbon Ablator).
478    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    /// Local char fraction β (0 = fully virgin, 1 = fully charred) as a
491    /// function of local temperature, using a linear pyrolysis model.
492    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    /// Local bulk density (kg/m³) as a function of char fraction.
504    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    /// Blowing correction factor B' (dimensionless) for surface energy
509    /// balance in a laminar boundary layer (simplified Sutton–Graves).
510    ///
511    /// B' = ṁ_w / (ρ_e · u_e · C_H) where ṁ_w is the ablation mass flux.
512    /// Uses a simplified correlation: B' ≈ 0.5 · (T_wall / T_recovery)^0.5
513    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    /// Surface recession rate (mm/s) as a function of heat flux (MW/m²) and
520    /// wall temperature (°C), using the Arrhenius char oxidation kinetics.
521    ///
522    /// ṡ = A_ox · exp(−E_a / RT) · q'' / (ρ_c · H_v)
523    pub fn recession_rate_mm_s(&self, heat_flux_mw_m2: f64, wall_temperature_c: f64) -> f64 {
524        const A_OX: f64 = 2.5e4; // pre-exponential factor
525        const E_A: f64 = 1.8e5; // J/mol activation energy
526        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; // J/kg
530        let rho_c = self.char_density_kg_m3;
531        let q = heat_flux_mw_m2 * 1e6; // W/m²
532        kinetic * q / (rho_c * hv) * 1000.0 // mm/s
533    }
534}
535
536// ---------------------------------------------------------------------------
537// 7. Metal foam — Gibson–Ashby model
538// ---------------------------------------------------------------------------
539
540/// Metal foam descriptor using the Gibson–Ashby cellular solid model.
541#[derive(Debug, Clone, Copy)]
542pub struct MetalFoam {
543    /// Relative density ρ*/ρₛ (0–1).
544    pub relative_density: f64,
545    /// Parent solid Young's modulus (GPa).
546    pub solid_modulus_gpa: f64,
547    /// Parent solid yield strength (MPa).
548    pub solid_yield_mpa: f64,
549    /// Parent solid density (kg/m³).
550    pub solid_density_kg_m3: f64,
551    /// Cell topology: true = open-cell, false = closed-cell.
552    pub open_cell: bool,
553}
554
555impl MetalFoam {
556    /// Aluminium open-cell foam (e.g. ERG Duocel 6101-T6).
557    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    /// Foam density (kg/m³).
568    pub fn density_kg_m3(&self) -> f64 {
569        self.relative_density * self.solid_density_kg_m3
570    }
571
572    /// Young's modulus (GPa) from Gibson–Ashby scaling.
573    ///
574    /// Open cell:   E* = C₁ · Eₛ · (ρ*/ρₛ)²
575    /// Closed cell: E* = φ² · Eₛ · (ρ*/ρₛ)² + (1−φ) · Eₛ · (ρ*/ρₛ)
576    ///                   (membrane term φ = 0.6 for typical closed-cell)
577    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    /// Plateau (yield) stress (MPa) — onset of plastic collapse.
588    ///
589    /// Open cell:   σ_y* = C₂ · σ_yₛ · (ρ*/ρₛ)^(3/2)
590    /// Closed cell: σ_y* = 0.3 · σ_yₛ · ((φ · ρ*/ρₛ)^(1/2) + (1−φ)(ρ*/ρₛ))
591    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    /// Densification strain ε_D (dimensionless) at which the foam has fully
602    /// collapsed.
603    pub fn densification_strain(&self) -> f64 {
604        1.0 - 1.4 * self.relative_density
605    }
606
607    /// Energy absorption capacity (MJ/m³) up to densification, assuming a
608    /// perfectly rectangular stress–strain plateau.
609    pub fn energy_absorption_mj_m3(&self) -> f64 {
610        self.plateau_stress_mpa() * self.densification_strain() * 1e-3
611    }
612}
613
614// ---------------------------------------------------------------------------
615// 8. Shape memory alloy (NiTi Nitinol) — superelastic model
616// ---------------------------------------------------------------------------
617
618/// Nitinol SMA material descriptor with superelastic behaviour.
619///
620/// Uses the simplified Brinson 1-D constitutive model.
621#[derive(Debug, Clone)]
622pub struct Nitinol {
623    /// Service temperature (°C).
624    pub temperature_c: f64,
625    /// Austenite finish temperature A_f (°C).
626    pub a_finish_c: f64,
627    /// Martensite start temperature M_s (°C).
628    pub m_start_c: f64,
629    /// Austenite Young's modulus (GPa).
630    pub ea_gpa: f64,
631    /// Martensite Young's modulus (GPa).
632    pub em_gpa: f64,
633}
634
635impl Nitinol {
636    /// Biomedical / interventional-device grade Nitinol.
637    pub fn biomedical_grade() -> Self {
638        Self {
639            temperature_c: 37.0, // body temperature
640            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    /// Returns `true` when the alloy is in the superelastic (austenite) regime
648    /// at the current temperature.
649    pub fn is_superelastic(&self) -> bool {
650        self.temperature_c > self.a_finish_c
651    }
652
653    /// Effective Young's modulus (GPa) using a rule-of-mixtures mixture
654    /// of austenite and martensite fractions.
655    ///
656    /// ξ = martensite fraction (0 = fully austenite, 1 = fully martensite)
657    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    /// Critical stress for onset of stress-induced martensite (MPa).
663    ///
664    /// σ_cr = C_A · (T − A_f) where C_A ≈ 8 MPa/°C for Nitinol.
665    pub fn critical_sim_stress_mpa(&self) -> f64 {
666        const C_A: f64 = 8.0; // MPa/°C
667        let delta_t = (self.temperature_c - self.a_finish_c).max(0.0);
668        200.0 + C_A * delta_t // 200 MPa is the lower plateau reference
669    }
670
671    /// Superelastic strain recovery (%) assuming the maximum transformation
672    /// strain is 8 %.
673    pub fn max_recoverable_strain_pct(&self) -> f64 {
674        if self.is_superelastic() { 8.0 } else { 2.0 }
675    }
676
677    /// Density (kg/m³).
678    pub fn density_kg_m3(&self) -> f64 {
679        6450.0
680    }
681}
682
683// ---------------------------------------------------------------------------
684// 9. High-entropy alloy (HEA) mechanical model
685// ---------------------------------------------------------------------------
686
687/// Mechanical property model for a CoCrFeMnNi (Cantor) equiatomic HEA.
688///
689/// Reference: Gali & George, Intermetallics 2013; Otto et al. 2013.
690#[derive(Debug, Clone)]
691pub struct CantorhHea {
692    /// Test/service temperature (°C).
693    pub temperature_c: f64,
694    /// Grain size (µm).
695    pub grain_size_um: f64,
696}
697
698impl CantorhHea {
699    /// Construct a Cantor HEA descriptor.
700    pub fn new(temperature_c: f64, grain_size_um: f64) -> Self {
701        Self {
702            temperature_c,
703            grain_size_um,
704        }
705    }
706
707    /// Young's modulus (GPa).
708    pub fn youngs_modulus_gpa(&self) -> f64 {
709        let t = self.temperature_c;
710        202.0 - 0.06 * t
711    }
712
713    /// Yield strength (MPa) including Hall–Petch grain-size strengthening.
714    ///
715    /// σ_y = σ₀ + k_HP / √d + thermal softening term
716    pub fn yield_strength_mpa(&self) -> f64 {
717        let sigma0 = 160.0; // MPa lattice friction stress
718        let k_hp = 800.0; // MPa·µm^0.5 Hall-Petch coefficient
719        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); // thermal softening ~0.4 MPa/°C
723        (sigma0 + hp - thermal).max(50.0)
724    }
725
726    /// Ultimate tensile strength (MPa).
727    pub fn uts_mpa(&self) -> f64 {
728        self.yield_strength_mpa() * 1.5 // HEA exhibits high work hardening
729    }
730
731    /// Fracture toughness KIC (MPa·√m) — the Cantor alloy is notable for
732    /// cryogenic toughening.
733    pub fn fracture_toughness_mpa_sqrtm(&self) -> f64 {
734        let t = self.temperature_c;
735        if t < 0.0 {
736            // Toughness increases at cryogenic temperatures
737            217.0 - 0.5 * t // increases as T decreases below 0 °C
738        } else {
739            (217.0 - 0.1 * t).max(100.0)
740        }
741    }
742
743    /// Stacking fault energy (mJ/m²) — low SFE in the Cantor alloy promotes
744    /// twinning-induced plasticity.
745    pub fn stacking_fault_energy_mj_m2(&self) -> f64 {
746        let t = self.temperature_c;
747        18.0 + 0.02 * t
748    }
749
750    /// Density (kg/m³).
751    pub fn density_kg_m3(&self) -> f64 {
752        8000.0
753    }
754}
755
756// ---------------------------------------------------------------------------
757// 10. Reentry vehicle thermal protection
758// ---------------------------------------------------------------------------
759
760/// Aerodynamic heating model for a blunt-nosed reentry vehicle.
761///
762/// Uses the Fay–Riddell stagnation-point heat flux correlation and a
763/// one-dimensional heat conduction model for TPS sizing.
764#[derive(Debug, Clone)]
765pub struct ReentryVehicle {
766    /// Nose radius (m).
767    pub nose_radius_m: f64,
768    /// Entry velocity (m/s).
769    pub entry_velocity_m_s: f64,
770    /// Freestream density (kg/m³).
771    pub freestream_density_kg_m3: f64,
772    /// TPS material type label (informational).
773    pub tps_material: String,
774    /// TPS thickness (mm).
775    pub tps_thickness_mm: f64,
776}
777
778impl ReentryVehicle {
779    /// Construct a reentry vehicle TPS descriptor.
780    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    /// Stagnation-point cold-wall heat flux (W/m²) using the Sutton–Graves
797    /// correlation:
798    ///
799    /// q'' = k_sg · √(ρ/R_N) · V³
800    ///
801    /// where k_sg = 1.742×10⁻⁴ (SI units; ρ in kg/m³, R_N in m, V in m/s).
802    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    /// Peak surface temperature at the stagnation point (K) estimated from
811    /// a radiation equilibrium condition:
812    ///
813    /// q'' = ε · σ · T⁴  →  T = (q'' / (ε·σ))^(1/4)
814    pub fn radiative_equilibrium_temperature_k(&self, emissivity: f64) -> f64 {
815        const SIGMA: f64 = 5.670_374_419e-8; // Stefan–Boltzmann constant, W/m²·K⁴
816        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    /// Integrated heat load (MJ/m²) for a given entry duration (s).
822    ///
823    /// Uses the parabolic profile q(t) = q_peak · 4t(t_entry-t)/t_entry².
824    pub fn integrated_heat_load_mj_m2(&self, entry_duration_s: f64) -> f64 {
825        let q_peak = self.stagnation_heat_flux_w_m2();
826        // Integral of 4·q·t(T-t)/T² dt from 0 to T = (2/3)·q·T
827        2.0 / 3.0 * q_peak * entry_duration_s / 1e6
828    }
829
830    /// Required TPS thickness (mm) to limit bondline temperature below
831    /// `max_bondline_c` (°C), using a 1-D semi-infinite slab approximation.
832    ///
833    /// δ ≈ 2 · √(α · t_entry) · erfinv(1 − ΔT_allowed / ΔT_surface)
834    /// (simplified as δ ≈ 1.5 · √(α · t_entry) for ΔT_allowed = 50 °C).
835    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 // convert m to mm
843    }
844
845    /// Vehicle ballistic coefficient (kg/m²): β = m / (C_D · A_ref).
846    ///
847    /// Higher β → deeper penetration before deceleration.
848    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// ---------------------------------------------------------------------------
855// 11. Additional helpers — Titanium structural member sizing
856// ---------------------------------------------------------------------------
857
858/// Titanium structural beam sizing for aerospace applications.
859///
860/// Sizes a hollow circular tube to carry a given axial load with a safety
861/// factor.
862#[derive(Debug, Clone, Copy)]
863pub struct TitaniumTube {
864    /// Outer diameter (mm).
865    pub outer_diameter_mm: f64,
866    /// Wall thickness (mm).
867    pub wall_thickness_mm: f64,
868    /// Temperature (°C).
869    pub temperature_c: f64,
870}
871
872impl TitaniumTube {
873    /// Construct a titanium tube.
874    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    /// Cross-sectional area (mm²).
883    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    /// Second moment of area I (mm⁴).
890    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    /// Euler buckling load (N) for a pin-ended column of length `l` (mm).
897    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; // MPa
900        PI * PI * e * self.second_moment_mm4() / (length_mm * length_mm)
901    }
902
903    /// Axial load capacity (N) limited by yield.
904    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    /// Margin of safety against yield under `applied_load_n` (positive = safe).
910    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// ---------------------------------------------------------------------------
916// 12. Oxidation kinetics of superalloy bond coat
917// ---------------------------------------------------------------------------
918
919/// TGO (thermally-grown oxide) growth kinetics for MCrAlY bond coat.
920///
921/// Uses a parabolic oxidation rate law: h² = k_p · t, where h is oxide
922/// thickness and k_p is the parabolic rate constant (µm²/h).
923#[derive(Debug, Clone)]
924pub struct BondCoatOxidation {
925    /// Temperature (°C).
926    pub temperature_c: f64,
927    /// Al content of the bond coat (wt%).
928    pub al_content_wt_pct: f64,
929}
930
931impl BondCoatOxidation {
932    /// Construct an oxidation model.
933    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    /// Parabolic rate constant kₚ (µm²/h) using an Arrhenius correlation.
941    ///
942    /// kₚ = A_0 · (Al/10)^0.5 · exp(−Q / RT)
943    /// with A_0 = 2×10⁶ µm²/h, Q = 220 kJ/mol.
944    pub fn parabolic_rate_constant_um2_h(&self) -> f64 {
945        const A0: f64 = 2.0e6; // µm²/h
946        const Q: f64 = 220_000.0; // J/mol
947        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    /// TGO thickness (µm) after `time_h` hours of oxidation.
954    pub fn tgo_thickness_um(&self, time_h: f64) -> f64 {
955        (self.parabolic_rate_constant_um2_h() * time_h).sqrt()
956    }
957
958    /// Critical TGO thickness (µm) above which spallation is likely (~7 µm
959    /// for MCrAlY/7YSZ systems).
960    pub fn critical_tgo_thickness_um() -> f64 {
961        7.0
962    }
963
964    /// Life fraction consumed (0–1) at a given TGO thickness.
965    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// ---------------------------------------------------------------------------
973// Unit tests
974// ---------------------------------------------------------------------------
975
976#[cfg(test)]
977mod tests {
978    use super::*;
979
980    // ── Ti-6Al-4V ────────────────────────────────────────────────────────────
981
982    #[test]
983    fn test_ti64_youngs_modulus_at_rt() {
984        let mat = Ti6Al4V::new(20.0);
985        let e = mat.youngs_modulus_gpa();
986        // At room temperature ~112–114 GPa
987        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    // ── Inconel 718 ──────────────────────────────────────────────────────────
1018
1019    #[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    // ── CFRP / CLT ───────────────────────────────────────────────────────────
1049
1050    #[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        // [0/45/-45/90]s quasi-isotropic laminate
1062        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        // Quasi-isotropic → Ex ≈ Ey within 5 %
1066        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        // Ex should be close to E1 = 161 GPa for all-0° laminate
1080        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    // ── SiC/SiC CMC ─────────────────────────────────────────────────────────
1095
1096    #[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    // ── TBC ─────────────────────────────────────────────────────────────────
1117
1118    #[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        // Substrate has higher CTE → mismatch strain is positive (tensile in coating)
1138        let strain = tbc.cte_mismatch_strain(500.0);
1139        assert!(strain > 0.0);
1140    }
1141
1142    // ── Ablative material ────────────────────────────────────────────────────
1143
1144    #[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    // ── Metal foam ───────────────────────────────────────────────────────────
1173
1174    #[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    // ── Nitinol ──────────────────────────────────────────────────────────────
1201
1202    #[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    // ── Cantor HEA ───────────────────────────────────────────────────────────
1228
1229    #[test]
1230    fn test_hea_yield_hall_petch_effect() {
1231        let fine = CantorhHea::new(25.0, 10.0); // fine grain
1232        let coarse = CantorhHea::new(25.0, 100.0); // coarse grain
1233        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    // ── Reentry vehicle ──────────────────────────────────────────────────────
1250
1251    #[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; // m²/s for PICA char
1276        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    // ── Titanium tube ────────────────────────────────────────────────────────
1282
1283    #[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); // 100 N — very light load
1301        assert!(mos > 0.0, "MoS = {mos}");
1302    }
1303
1304    // ── Bond coat oxidation ──────────────────────────────────────────────────
1305
1306    #[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        // Parabolic: h(4t) = √4 · h(t) = 2·h(t)
1312        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}