Skip to main content

oxiphysics_materials/
alloy_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Metallic alloy material models: composition, mechanical properties,
5//! strengthening mechanisms, phase diagrams, thermal and corrosion properties.
6
7#![allow(dead_code)]
8#![allow(clippy::too_many_arguments)]
9
10// ─── AlloyComposition ────────────────────────────────────────────────────────
11
12/// Chemical composition of a metallic alloy.
13#[derive(Debug, Clone)]
14pub struct AlloyComposition {
15    /// List of (element symbol, weight fraction) pairs.
16    pub elements: Vec<(String, f64)>,
17    /// Bulk density in kg/m³.
18    pub density: f64,
19    /// (solidus, liquidus) melting range in K.
20    pub melting_range: (f64, f64),
21}
22
23impl AlloyComposition {
24    /// Construct a new `AlloyComposition`.
25    pub fn new(elements: Vec<(String, f64)>, density: f64, melting_range: (f64, f64)) -> Self {
26        Self {
27            elements,
28            density,
29            melting_range,
30        }
31    }
32
33    /// Return `true` if all weight fractions sum to approximately 1.
34    pub fn validate(&self) -> bool {
35        let sum: f64 = self.elements.iter().map(|(_, f)| f).sum();
36        (sum - 1.0).abs() < 1e-6
37    }
38
39    /// Compute mixture density using the rule of mixtures (weight-fraction average
40    /// of inverse densities — Reuss bound).
41    ///
42    /// `densities` is a list of `(symbol, density_kg_m3)` pairs.
43    pub fn density_mixture(&self, densities: &[(String, f64)]) -> f64 {
44        let mut inv_sum = 0.0;
45        let mut frac_sum = 0.0;
46        for (sym, frac) in &self.elements {
47            if let Some((_, d)) = densities.iter().find(|(s, _)| s == sym)
48                && *d > 0.0
49            {
50                inv_sum += frac / d;
51                frac_sum += frac;
52            }
53        }
54        if inv_sum < 1e-30 || frac_sum < 1e-12 {
55            self.density
56        } else {
57            frac_sum / inv_sum
58        }
59    }
60}
61
62// ─── AlloySeries ─────────────────────────────────────────────────────────────
63
64/// Classification of common alloy families.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum AlloySeries {
67    /// Aluminium 1xxx series (≥99% Al).
68    Series1xxx,
69    /// Aluminium 2xxx series (Cu-based).
70    Series2xxx,
71    /// Aluminium 3xxx series (Mn-based).
72    Series3xxx,
73    /// Aluminium 4xxx series (Si-based).
74    Series4xxx,
75    /// Aluminium 5xxx series (Mg-based).
76    Series5xxx,
77    /// Aluminium 6xxx series (Mg+Si).
78    Series6xxx,
79    /// Aluminium 7xxx series (Zn-based).
80    Series7xxx,
81    /// Aluminium 8xxx series (other elements).
82    Series8xxx,
83    /// Aluminium 9xxx series (reserved).
84    Series9xxx,
85    /// Austenitic stainless steel 304.
86    StainlessSteel304,
87    /// Austenitic stainless steel 316.
88    StainlessSteel316,
89    /// Nickel superalloy Inconel 718.
90    Inconel718,
91    /// Titanium alloy Ti-6Al-4V.
92    TitaniumTi6Al4V,
93    /// Alloy steel AISI 4140.
94    Tool4140,
95    /// Maraging steel 300 grade.
96    Maraging300,
97    /// Nickel-based alloy Hastelloy C-276.
98    Hastelloy,
99}
100
101// ─── AlloyMechanicalProps ─────────────────────────────────────────────────────
102
103/// Mechanical properties of a metallic alloy.
104#[derive(Debug, Clone)]
105pub struct AlloyMechanicalProps {
106    /// 0.2% proof stress / yield strength in MPa.
107    pub yield_strength: f64,
108    /// Ultimate tensile strength in MPa.
109    pub uts: f64,
110    /// Elongation at fracture in % (gauge length = 50 mm).
111    pub elongation: f64,
112    /// Vickers hardness HV.
113    pub hardness_hv: f64,
114    /// Young's modulus in GPa.
115    pub youngs_modulus: f64,
116    /// Poisson's ratio (dimensionless).
117    pub poisson_ratio: f64,
118    /// Plane-strain fracture toughness K_IC in MPa·√m.
119    pub fracture_toughness: f64,
120}
121
122impl AlloyMechanicalProps {
123    /// Construct `AlloyMechanicalProps` from individual values.
124    pub fn new(
125        yield_strength: f64,
126        uts: f64,
127        elongation: f64,
128        hardness_hv: f64,
129        youngs_modulus: f64,
130        poisson_ratio: f64,
131        fracture_toughness: f64,
132    ) -> Self {
133        Self {
134            yield_strength,
135            uts,
136            elongation,
137            hardness_hv,
138            youngs_modulus,
139            poisson_ratio,
140            fracture_toughness,
141        }
142    }
143
144    /// Compute safety factor = yield_strength / applied_stress.
145    pub fn safety_factor(&self, applied_stress: f64) -> f64 {
146        if applied_stress.abs() < 1e-15 {
147            f64::INFINITY
148        } else {
149            self.yield_strength / applied_stress
150        }
151    }
152
153    /// Return `true` if the alloy is considered brittle (elongation < 5 %).
154    pub fn is_brittle(&self) -> bool {
155        self.elongation < 5.0
156    }
157}
158
159// ─── Hall-Petch equation ──────────────────────────────────────────────────────
160
161/// Hall-Petch grain-boundary strengthening model.
162pub struct HallPetch;
163
164impl HallPetch {
165    /// Compute yield strength:  σ_y = σ_0 + k / √(grain_size).
166    ///
167    /// `grain_size` is in metres; `k` is the Hall-Petch slope in MPa·√m.
168    pub fn yield_strength(sigma_0: f64, k: f64, grain_size: f64) -> f64 {
169        sigma_0 + k / grain_size.max(1e-30).sqrt()
170    }
171
172    /// Invert Hall-Petch to find required grain size for a target yield stress.
173    pub fn grain_size_from_yield(sigma_y: f64, sigma_0: f64, k: f64) -> f64 {
174        let diff = sigma_y - sigma_0;
175        if diff.abs() < 1e-15 {
176            return f64::INFINITY;
177        }
178        (k / diff).powi(2)
179    }
180}
181
182// ─── Strengthening mechanisms ─────────────────────────────────────────────────
183
184/// Collection of metallic strengthening mechanism models.
185pub struct Strengthening;
186
187impl Strengthening {
188    /// Solid-solution strengthening increment: Δσ_ss = k_ss · c^(2/3).
189    ///
190    /// `c` is solute concentration (mol fraction); `k_ss` is a material constant (MPa).
191    pub fn solid_solution_strengthening(c: f64, k_ss: f64) -> f64 {
192        k_ss * c.max(0.0).powf(2.0 / 3.0)
193    }
194
195    /// Orowan precipitation-hardening increment.
196    ///
197    /// Δσ_ph ≈ 0.81 · G · b / (2π · λ · √(1-ν)) where ν ≈ 0.3 is absorbed.
198    ///
199    /// Here we use the simplified Orowan–Ashby form:
200    /// Δσ ≈ 0.13 · G · b / particle_spacing
201    pub fn precipitation_hardening(particle_spacing: f64, shear_modulus: f64, burgers: f64) -> f64 {
202        0.13 * shear_modulus * burgers / particle_spacing.max(1e-30)
203    }
204
205    /// Hollomon work-hardening (power-law): σ = k · ε_p^n.
206    ///
207    /// `eps_p` is plastic strain, `k` is strength coefficient (MPa), `n` is strain-hardening exponent.
208    pub fn work_hardening(eps_p: f64, k: f64, n: f64) -> f64 {
209        k * eps_p.max(0.0).powf(n)
210    }
211
212    /// Combine strengthening contributions by simple summation.
213    ///
214    /// `ss` = solid-solution, `ph` = precipitation, `wh` = work-hardening,
215    /// `gb` = grain-boundary (Hall-Petch) increment.
216    pub fn combined_strengthening(ss: f64, ph: f64, wh: f64, gb: f64) -> f64 {
217        ss + ph + wh + gb
218    }
219}
220
221// ─── Binary phase diagram ─────────────────────────────────────────────────────
222
223/// Phase label in a binary alloy phase diagram.
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum BinaryPhase {
226    /// Fully liquid.
227    Liquid,
228    /// Single-phase alpha solid solution.
229    Alpha,
230    /// Single-phase beta solid solution.
231    Beta,
232    /// Two-phase α + β region.
233    AlphaPlusBeta,
234    /// Eutectic mixture.
235    Eutectic,
236}
237
238/// Simple binary phase diagram helper.
239pub struct PhaseDiagram;
240
241impl PhaseDiagram {
242    /// Determine the equilibrium phase for composition `x` (mol fraction of B)
243    /// and temperature `temp` (K) in a simple eutectic binary system.
244    ///
245    /// `eutectic` = `(x_eutectic, T_eutectic)`.
246    /// `liquidus` = `[T_melt_A, T_melt_B]` (melting points of pure A and B).
247    pub fn phase_at_composition(
248        x: f64,
249        temp: f64,
250        eutectic: (f64, f64),
251        liquidus: [f64; 2],
252    ) -> BinaryPhase {
253        let (x_e, t_e) = eutectic;
254        let t_a = liquidus[0];
255        let t_b = liquidus[1];
256
257        // Linear liquidus on A-rich side: T_liq_a(x) = T_A - (T_A - T_e) * x / x_e
258        let t_liq_a = if x_e > 1e-12 {
259            t_a - (t_a - t_e) * x / x_e
260        } else {
261            t_a
262        };
263
264        // Linear liquidus on B-rich side: T_liq_b(x) = T_B - (T_B - T_e) * (1-x) / (1-x_e)
265        let t_liq_b = if (1.0 - x_e) > 1e-12 {
266            t_b - (t_b - t_e) * (1.0 - x) / (1.0 - x_e)
267        } else {
268            t_b
269        };
270
271        let t_liquidus = if x <= x_e { t_liq_a } else { t_liq_b };
272
273        if temp > t_liquidus {
274            BinaryPhase::Liquid
275        } else if (temp - t_e).abs() < 5.0 && (x - x_e).abs() < 0.02 {
276            BinaryPhase::Eutectic
277        } else if temp < t_e {
278            BinaryPhase::AlphaPlusBeta
279        } else if x < x_e {
280            BinaryPhase::Alpha
281        } else {
282            BinaryPhase::Beta
283        }
284    }
285}
286
287// ─── Thermal properties ───────────────────────────────────────────────────────
288
289/// Compute alloy thermal conductivity by linear rule of mixtures.
290///
291/// `k1`, `k2` are the component conductivities (W/m·K); `x` is the mole fraction of component 2.
292pub fn alloy_thermal_conductivity(k1: f64, k2: f64, x: f64) -> f64 {
293    (1.0 - x) * k1 + x * k2
294}
295
296/// Compute alloy specific heat capacity by mass-weighted mixture rule.
297///
298/// Each tuple is `(mass_fraction, cp)` in J/(kg·K).
299pub fn alloy_specific_heat(cp_components: &[(f64, f64)]) -> f64 {
300    cp_components.iter().map(|(f, cp)| f * cp).sum()
301}
302
303// ─── Corrosion ────────────────────────────────────────────────────────────────
304
305/// Compute the Pitting Resistance Equivalent Number (PREN) for stainless steels.
306///
307/// PREN = %Cr + 3.3·%Mo + 16·%N
308pub fn pitting_resistance_equivalent(cr: f64, mo: f64, n_pct: f64) -> f64 {
309    cr + 3.3 * mo + 16.0 * n_pct
310}
311
312/// Assess galvanic corrosion risk from the electrode-potential difference.
313///
314/// Returns a static string describing the risk level.
315pub fn galvanic_corrosion_risk(e1: f64, e2: f64) -> &'static str {
316    let delta = (e1 - e2).abs();
317    if delta < 0.1 {
318        "negligible"
319    } else if delta < 0.25 {
320        "low"
321    } else if delta < 0.5 {
322        "moderate"
323    } else {
324        "high"
325    }
326}
327
328// ─── AluminumAlloyT6 ──────────────────────────────────────────────────────────
329
330/// Typical T6-temper mechanical properties for common aluminium alloys.
331pub struct AluminumAlloyT6;
332
333impl AluminumAlloyT6 {
334    /// Return typical T6 mechanical properties for a given aluminium alloy series.
335    pub fn properties(series: AlloySeries) -> AlloyMechanicalProps {
336        match series {
337            AlloySeries::Series2xxx => {
338                AlloyMechanicalProps::new(324.0, 469.0, 10.0, 130.0, 73.1, 0.33, 37.0)
339            }
340            AlloySeries::Series6xxx => {
341                AlloyMechanicalProps::new(276.0, 310.0, 12.0, 95.0, 68.9, 0.33, 29.0)
342            }
343            AlloySeries::Series7xxx => {
344                AlloyMechanicalProps::new(503.0, 572.0, 11.0, 150.0, 71.7, 0.33, 29.0)
345            }
346            _ => AlloyMechanicalProps::new(100.0, 150.0, 8.0, 50.0, 69.0, 0.33, 20.0),
347        }
348    }
349}
350
351// ─── NickelSuperalloy ─────────────────────────────────────────────────────────
352
353/// Creep and oxidation model for nickel-based superalloys.
354pub struct NickelSuperalloy;
355
356impl NickelSuperalloy {
357    /// Compute steady-state creep rate using the Norton power law:
358    /// ε̇ = A · σ^n · exp(-Q / (R·T))
359    ///
360    /// - `sigma`: stress in MPa
361    /// - `temp`: temperature in K
362    /// - `a_coeff`: pre-exponential factor (s⁻¹·MPa⁻ⁿ)
363    /// - `n_exp`: stress exponent
364    /// - `q_activation`: activation energy in J/mol
365    pub fn creep_rate(sigma: f64, temp: f64, a_coeff: f64, n_exp: f64, q_activation: f64) -> f64 {
366        const R: f64 = 8.314; // J/(mol·K)
367        a_coeff * sigma.powf(n_exp) * (-q_activation / (R * temp.max(1e-3))).exp()
368    }
369
370    /// Estimate parabolic oxidation mass gain: Δm² = k_p · t.
371    ///
372    /// Returns Δm (mg/cm²) at time `t` (s) using parabolic rate constant `k_p`.
373    pub fn oxidation_mass_gain(k_p: f64, t: f64) -> f64 {
374        (k_p * t.max(0.0)).sqrt()
375    }
376}
377
378// ─── WeldabilityIndex ─────────────────────────────────────────────────────────
379
380/// Carbon equivalent for steel weldability assessment.
381pub struct WeldabilityIndex;
382
383impl WeldabilityIndex {
384    /// Compute the International Institute of Welding (IIW) carbon equivalent:
385    ///
386    /// CE = C + Mn/6 + (Cr+Mo+V)/5 + (Ni+Cu)/15
387    ///
388    /// All values in weight %.
389    pub fn carbon_equivalent_iiw(
390        c: f64,
391        mn: f64,
392        cr: f64,
393        mo: f64,
394        v: f64,
395        ni: f64,
396        cu: f64,
397    ) -> f64 {
398        c + mn / 6.0 + (cr + mo + v) / 5.0 + (ni + cu) / 15.0
399    }
400
401    /// Classify weldability from the IIW carbon equivalent.
402    pub fn weldability_class(ce: f64) -> &'static str {
403        if ce < 0.35 {
404            "excellent"
405        } else if ce < 0.45 {
406            "good"
407        } else if ce < 0.60 {
408            "fair"
409        } else {
410            "poor"
411        }
412    }
413}
414
415// ─── Tests ────────────────────────────────────────────────────────────────────
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    fn stainless_304() -> AlloyComposition {
422        AlloyComposition::new(
423            vec![
424                ("Fe".to_string(), 0.69),
425                ("Cr".to_string(), 0.19),
426                ("Ni".to_string(), 0.10),
427                ("Mn".to_string(), 0.02),
428            ],
429            7900.0,
430            (1673.0, 1723.0),
431        )
432    }
433
434    fn al6061_t6() -> AlloyMechanicalProps {
435        AlloyMechanicalProps::new(276.0, 310.0, 12.0, 95.0, 68.9, 0.33, 29.0)
436    }
437
438    #[test]
439    fn test_alloy_composition_validate_valid() {
440        let alloy = stainless_304();
441        assert!(alloy.validate());
442    }
443
444    #[test]
445    fn test_alloy_composition_validate_invalid() {
446        let alloy = AlloyComposition::new(
447            vec![("Fe".to_string(), 0.5), ("Cr".to_string(), 0.3)],
448            7900.0,
449            (1673.0, 1723.0),
450        );
451        assert!(!alloy.validate());
452    }
453
454    #[test]
455    fn test_density_mixture_rule_of_mixtures() {
456        let alloy = AlloyComposition::new(
457            vec![("A".to_string(), 0.5), ("B".to_string(), 0.5)],
458            0.0,
459            (1000.0, 1200.0),
460        );
461        let densities = vec![("A".to_string(), 2000.0), ("B".to_string(), 4000.0)];
462        let d = alloy.density_mixture(&densities);
463        // Reuss bound = 1 / (0.5/2000 + 0.5/4000) = 2666.67
464        assert!((d - 2666.67).abs() < 1.0, "d = {:.6}", d);
465    }
466
467    #[test]
468    fn test_density_mixture_missing_element() {
469        let alloy = AlloyComposition::new(vec![("X".to_string(), 1.0)], 7000.0, (1000.0, 1100.0));
470        let densities = vec![("Fe".to_string(), 7874.0)];
471        // Falls back to alloy.density
472        let d = alloy.density_mixture(&densities);
473        assert!((d - 7000.0).abs() < 1e-6);
474    }
475
476    #[test]
477    fn test_safety_factor_normal() {
478        let props = al6061_t6();
479        let sf = props.safety_factor(138.0);
480        assert!((sf - 2.0).abs() < 0.01);
481    }
482
483    #[test]
484    fn test_safety_factor_zero_stress() {
485        let props = al6061_t6();
486        assert!(props.safety_factor(0.0).is_infinite());
487    }
488
489    #[test]
490    fn test_is_brittle_ductile() {
491        let props = al6061_t6();
492        assert!(!props.is_brittle());
493    }
494
495    #[test]
496    fn test_is_brittle_true() {
497        let props = AlloyMechanicalProps::new(600.0, 700.0, 2.0, 700.0, 210.0, 0.28, 50.0);
498        assert!(props.is_brittle());
499    }
500
501    #[test]
502    fn test_hall_petch_yield_strength() {
503        // σ_0 = 50 MPa, k = 0.5 MPa·mm^0.5, grain_size = 0.25 mm² → k/√d = 1.0
504        let sy = HallPetch::yield_strength(50.0, 0.5, 0.25);
505        assert!((sy - 51.0).abs() < 1e-9, "sy = {:.6}", sy);
506    }
507
508    #[test]
509    fn test_hall_petch_grain_size_inversion() {
510        let sigma_0 = 50.0;
511        let k = 0.5;
512        let d0 = 1.0e-6;
513        let sy = HallPetch::yield_strength(sigma_0, k, d0);
514        let d_back = HallPetch::grain_size_from_yield(sy, sigma_0, k);
515        assert!((d_back - d0).abs() < 1e-18, "d_back = {:.6e}", d_back);
516    }
517
518    #[test]
519    fn test_hall_petch_grain_size_same_yield() {
520        let d = HallPetch::grain_size_from_yield(50.0, 50.0, 0.5);
521        assert!(d.is_infinite());
522    }
523
524    #[test]
525    fn test_solid_solution_strengthening_zero() {
526        assert!((Strengthening::solid_solution_strengthening(0.0, 100.0) - 0.0).abs() < 1e-10);
527    }
528
529    #[test]
530    fn test_solid_solution_strengthening_positive() {
531        let ds = Strengthening::solid_solution_strengthening(0.01, 500.0);
532        assert!(ds > 0.0);
533    }
534
535    #[test]
536    fn test_precipitation_hardening_positive() {
537        let ds = Strengthening::precipitation_hardening(1e-7, 26e3, 2.86e-10);
538        assert!(ds > 0.0);
539    }
540
541    #[test]
542    fn test_work_hardening_hollomon() {
543        // σ = 500 · 0.2^0.2
544        let expected = 500.0 * 0.2_f64.powf(0.2);
545        let result = Strengthening::work_hardening(0.2, 500.0, 0.2);
546        assert!((result - expected).abs() < 1e-9);
547    }
548
549    #[test]
550    fn test_combined_strengthening() {
551        let total = Strengthening::combined_strengthening(50.0, 30.0, 20.0, 10.0);
552        assert!((total - 110.0).abs() < 1e-10);
553    }
554
555    #[test]
556    fn test_phase_diagram_liquid() {
557        // Above liquidus → Liquid
558        let phase = PhaseDiagram::phase_at_composition(0.3, 1500.0, (0.5, 800.0), [1200.0, 1400.0]);
559        assert_eq!(phase, BinaryPhase::Liquid);
560    }
561
562    #[test]
563    fn test_phase_diagram_alpha() {
564        let phase = PhaseDiagram::phase_at_composition(0.1, 900.0, (0.5, 600.0), [1200.0, 1100.0]);
565        assert_eq!(phase, BinaryPhase::Alpha);
566    }
567
568    #[test]
569    fn test_phase_diagram_beta() {
570        let phase = PhaseDiagram::phase_at_composition(0.8, 900.0, (0.5, 600.0), [1200.0, 1100.0]);
571        assert_eq!(phase, BinaryPhase::Beta);
572    }
573
574    #[test]
575    fn test_phase_diagram_two_phase() {
576        let phase = PhaseDiagram::phase_at_composition(0.3, 500.0, (0.5, 600.0), [1200.0, 1100.0]);
577        assert_eq!(phase, BinaryPhase::AlphaPlusBeta);
578    }
579
580    #[test]
581    fn test_alloy_thermal_conductivity_pure_components() {
582        let k = alloy_thermal_conductivity(15.0, 400.0, 0.0);
583        assert!((k - 15.0).abs() < 1e-10);
584        let k2 = alloy_thermal_conductivity(15.0, 400.0, 1.0);
585        assert!((k2 - 400.0).abs() < 1e-10);
586    }
587
588    #[test]
589    fn test_alloy_thermal_conductivity_midpoint() {
590        let k = alloy_thermal_conductivity(10.0, 20.0, 0.5);
591        assert!((k - 15.0).abs() < 1e-10);
592    }
593
594    #[test]
595    fn test_alloy_specific_heat_single() {
596        let cp = alloy_specific_heat(&[(1.0, 500.0)]);
597        assert!((cp - 500.0).abs() < 1e-10);
598    }
599
600    #[test]
601    fn test_alloy_specific_heat_mixture() {
602        let cp = alloy_specific_heat(&[(0.7, 500.0), (0.3, 900.0)]);
603        assert!((cp - 620.0).abs() < 1e-10);
604    }
605
606    #[test]
607    fn test_pren_calculation() {
608        // 316L: Cr≈17, Mo≈2.5, N≈0.03
609        let pren = pitting_resistance_equivalent(17.0, 2.5, 0.03);
610        let expected = 17.0 + 3.3 * 2.5 + 16.0 * 0.03;
611        assert!((pren - expected).abs() < 1e-10);
612    }
613
614    #[test]
615    fn test_galvanic_corrosion_negligible() {
616        assert_eq!(galvanic_corrosion_risk(0.0, 0.05), "negligible");
617    }
618
619    #[test]
620    fn test_galvanic_corrosion_high() {
621        assert_eq!(galvanic_corrosion_risk(0.0, 1.0), "high");
622    }
623
624    #[test]
625    fn test_galvanic_corrosion_low() {
626        assert_eq!(galvanic_corrosion_risk(0.1, 0.3), "low");
627    }
628
629    #[test]
630    fn test_aluminum_t6_6xxx() {
631        let props = AluminumAlloyT6::properties(AlloySeries::Series6xxx);
632        assert!((props.yield_strength - 276.0).abs() < 1e-6);
633    }
634
635    #[test]
636    fn test_aluminum_t6_7xxx_high_strength() {
637        let p7 = AluminumAlloyT6::properties(AlloySeries::Series7xxx);
638        let p6 = AluminumAlloyT6::properties(AlloySeries::Series6xxx);
639        assert!(p7.yield_strength > p6.yield_strength);
640    }
641
642    #[test]
643    fn test_nickel_creep_rate_positive() {
644        let rate = NickelSuperalloy::creep_rate(200.0, 1073.0, 1e-15, 4.0, 290_000.0);
645        assert!(rate > 0.0);
646    }
647
648    #[test]
649    fn test_nickel_creep_rate_increases_with_stress() {
650        let r1 = NickelSuperalloy::creep_rate(100.0, 1073.0, 1e-15, 4.0, 290_000.0);
651        let r2 = NickelSuperalloy::creep_rate(200.0, 1073.0, 1e-15, 4.0, 290_000.0);
652        assert!(r2 > r1);
653    }
654
655    #[test]
656    fn test_oxidation_mass_gain_zero_time() {
657        let dm = NickelSuperalloy::oxidation_mass_gain(1e-12, 0.0);
658        assert!((dm - 0.0).abs() < 1e-20);
659    }
660
661    #[test]
662    fn test_oxidation_mass_gain_positive() {
663        let dm = NickelSuperalloy::oxidation_mass_gain(1e-12, 3600.0);
664        assert!(dm > 0.0);
665    }
666
667    #[test]
668    fn test_carbon_equivalent_low_alloy() {
669        let ce = WeldabilityIndex::carbon_equivalent_iiw(0.15, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
670        assert!((ce - 0.3167).abs() < 0.001, "ce = {:.6}", ce);
671    }
672
673    #[test]
674    fn test_weldability_class_excellent() {
675        assert_eq!(WeldabilityIndex::weldability_class(0.30), "excellent");
676    }
677
678    #[test]
679    fn test_weldability_class_poor() {
680        assert_eq!(WeldabilityIndex::weldability_class(0.65), "poor");
681    }
682
683    #[test]
684    fn test_alloy_series_debug() {
685        let s = format!("{:?}", AlloySeries::Inconel718);
686        assert!(s.contains("Inconel718"));
687    }
688
689    #[test]
690    fn test_binary_phase_debug() {
691        let s = format!("{:?}", BinaryPhase::AlphaPlusBeta);
692        assert!(s.contains("Alpha"));
693    }
694}