Skip to main content

oxiphysics_materials/
lib.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Material properties and material library.
5//!
6//! Defines physical material properties such as density, friction,
7//! and restitution, along with a library for managing named materials.
8#![warn(missing_docs)]
9#![allow(ambiguous_glob_reexports)]
10
11mod error;
12pub use error::*;
13
14pub mod acoustics;
15pub mod additive_manufacturing;
16pub mod aerospace;
17pub mod anisotropic;
18pub mod battery_materials;
19pub mod biological_materials;
20pub mod biomaterials;
21pub mod biomechanics;
22pub mod ceramic_materials;
23pub mod combination;
24pub mod composite;
25pub mod composite_failure;
26pub mod composite_materials;
27pub mod construction;
28pub mod creep;
29pub mod crystal_plasticity;
30pub mod damage;
31pub mod dielectric_materials;
32pub mod elastic;
33pub mod electrochemistry;
34pub mod energy_materials;
35pub mod eos;
36pub mod fatigue;
37pub mod fiber_composites;
38pub mod fracture;
39pub mod geological;
40pub mod geomechanics;
41pub mod hydrogen_storage;
42pub mod hyperelastic;
43pub mod metamaterials;
44pub mod multiphysics;
45pub mod nano;
46pub mod nano_materials;
47pub mod nanocomposites;
48pub mod nanomaterials;
49pub mod nuclear_materials;
50pub mod optical;
51pub mod optical_materials;
52pub mod phase_transform;
53pub mod plasticity;
54pub mod polymer_mechanics;
55pub mod polymer_physics;
56pub mod porous_media;
57pub mod presets;
58pub mod quantum_materials;
59pub mod radiation;
60pub mod radiation_shielding;
61pub mod semiconductor;
62pub mod shape_memory;
63pub mod smart_materials;
64pub mod superconductor;
65pub mod thermal;
66pub mod thermoelectrics;
67pub mod tribology;
68pub mod tribology_ext;
69pub mod viscoelastic;
70
71pub mod aerospace_materials;
72
73pub mod corrosion;
74
75pub mod additive_manufacturing_materials;
76pub mod foam_materials;
77pub mod magnetocaloric_materials;
78
79pub use combination::{
80    ContactMaterialPair, FrictionCombineRule, ModulusCombineRule, RestitutionCombineRule,
81    ThermalContactResistance, combine_friction, combine_modulus, combine_restitution,
82    hertz_contact_force, hertz_effective_modulus, hertz_effective_radius, maxwell_diffusivity,
83};
84pub use composite::*;
85pub use creep::*;
86pub use elastic::{LinearElastic, NeoHookean};
87pub use eos::{
88    EosWithEnergy, EquationOfState, IdealGasEos, MieGruneisenEos as MieGruneisenEosShock,
89    PolynomialEos, StiffenedGasEos, TaitEos, TillotsonEos, VanDerWaalsEos,
90};
91pub use hyperelastic::{DruckerPrager, J2Plasticity, JwlEos, MieGruneisenEos, MooneyRivlin, Ogden};
92pub use phase_transform::*;
93pub use viscoelastic::{KelvinVoigt, Maxwell, StandardLinearSolid};
94
95use serde::{Deserialize, Serialize};
96use std::collections::HashMap;
97
98/// Physical material properties.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Material {
101    /// Material name
102    pub name: String,
103    /// Density in kg/m^3
104    pub density: f64,
105    /// Coefficient of friction (0..1)
106    pub friction: f64,
107    /// Coefficient of restitution (bounciness, 0..1)
108    pub restitution: f64,
109}
110
111impl Material {
112    /// Create a new material with the given properties.
113    pub fn new(name: impl Into<String>, density: f64, friction: f64, restitution: f64) -> Self {
114        Self {
115            name: name.into(),
116            density,
117            friction,
118            restitution,
119        }
120    }
121}
122
123/// A library of named materials.
124#[derive(Debug, Default)]
125pub struct MaterialLibrary {
126    materials: HashMap<String, Material>,
127}
128
129impl MaterialLibrary {
130    /// Create a new empty material library.
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// Add a material to the library.
136    pub fn add(&mut self, material: Material) {
137        self.materials.insert(material.name.clone(), material);
138    }
139
140    /// Look up a material by name.
141    pub fn get(&self, name: &str) -> Option<&Material> {
142        self.materials.get(name)
143    }
144
145    /// Return the number of materials in the library.
146    pub fn len(&self) -> usize {
147        self.materials.len()
148    }
149
150    /// Check whether the library is empty.
151    pub fn is_empty(&self) -> bool {
152        self.materials.is_empty()
153    }
154
155    /// Remove a material by name. Returns the removed material if it existed.
156    pub fn remove(&mut self, name: &str) -> Option<Material> {
157        self.materials.remove(name)
158    }
159
160    /// Return an iterator over all materials.
161    pub fn iter(&self) -> impl Iterator<Item = (&String, &Material)> {
162        self.materials.iter()
163    }
164
165    /// Check if a material with the given name exists.
166    pub fn contains(&self, name: &str) -> bool {
167        self.materials.contains_key(name)
168    }
169
170    /// Get all material names.
171    pub fn names(&self) -> Vec<String> {
172        self.materials.keys().cloned().collect()
173    }
174}
175
176impl Material {
177    /// Compute the impedance (density * speed_of_sound).
178    ///
179    /// `speed_of_sound` - Speed of sound in the material (m/s).
180    pub fn impedance(&self, speed_of_sound: f64) -> f64 {
181        self.density * speed_of_sound
182    }
183
184    /// Check whether this material is heavier than another.
185    pub fn is_heavier_than(&self, other: &Material) -> bool {
186        self.density > other.density
187    }
188
189    /// Check whether this material is bouncier than another.
190    pub fn is_bouncier_than(&self, other: &Material) -> bool {
191        self.restitution > other.restitution
192    }
193
194    /// Combine this material with another using average rules.
195    pub fn combine_average(&self, other: &Material) -> Material {
196        Material {
197            name: format!("{}+{}", self.name, other.name),
198            density: (self.density + other.density) * 0.5,
199            friction: (self.friction + other.friction) * 0.5,
200            restitution: (self.restitution + other.restitution) * 0.5,
201        }
202    }
203
204    /// Compute the weight of a given volume of this material (density * volume * g).
205    pub fn weight(&self, volume: f64, gravity: f64) -> f64 {
206        self.density * volume * gravity
207    }
208
209    /// Compute the mass of a given volume of this material.
210    pub fn mass_from_volume(&self, volume: f64) -> f64 {
211        self.density * volume
212    }
213}
214
215/// Linearly interpolate between two material property values.
216#[allow(dead_code)]
217pub fn lerp_property(a: f64, b: f64, t: f64) -> f64 {
218    a + (b - a) * t
219}
220
221/// Compute the effective coefficient of restitution for a collision
222/// using the velocity-dependent model:
223/// e_eff = e * max(0, 1 - k * |v_rel|)
224///
225/// where `k` controls velocity-dependence (typically small, e.g. 0.01).
226#[allow(dead_code)]
227pub fn velocity_dependent_restitution(e: f64, v_rel: f64, k: f64) -> f64 {
228    (e * (1.0 - k * v_rel.abs())).max(0.0)
229}
230
231/// Compute the critical damping coefficient for a spring-mass system.
232///
233/// c_crit = 2 * sqrt(k * m)
234#[allow(dead_code)]
235pub fn critical_damping(stiffness: f64, mass: f64) -> f64 {
236    2.0 * (stiffness * mass).sqrt()
237}
238
239/// Compute the damping ratio: zeta = c / c_crit.
240#[allow(dead_code)]
241pub fn damping_ratio(damping: f64, stiffness: f64, mass: f64) -> f64 {
242    let c_crit = critical_damping(stiffness, mass);
243    if c_crit.abs() < 1e-15 {
244        0.0
245    } else {
246        damping / c_crit
247    }
248}
249
250/// Compute the natural frequency of a spring-mass system: omega_n = sqrt(k/m).
251#[allow(dead_code)]
252pub fn natural_frequency(stiffness: f64, mass: f64) -> f64 {
253    if mass.abs() < 1e-15 {
254        0.0
255    } else {
256        (stiffness / mass).sqrt()
257    }
258}
259
260// ─── Extended Material with Full Physical Properties ──────────────────────────
261
262/// Extended material with full physical, mechanical, thermal, acoustic, and
263/// optical properties for simulation of complex multi-physics problems.
264#[allow(dead_code)]
265#[derive(Debug, Clone)]
266pub struct ExtendedMaterial {
267    /// Material name
268    pub name: String,
269    /// Density \[kg/m³\]
270    pub density: f64,
271    /// Young's modulus \[Pa\]
272    pub young_modulus: f64,
273    /// Poisson's ratio \[-\]
274    pub poisson_ratio: f64,
275    /// Yield strength \[Pa\]
276    pub yield_strength: f64,
277    /// Ultimate tensile strength \[Pa\]
278    pub ultimate_strength: f64,
279    /// Thermal conductivity \[W/(m·K)\]
280    pub thermal_conductivity: f64,
281    /// Specific heat capacity at constant pressure \[J/(kg·K)\]
282    pub specific_heat: f64,
283    /// Thermal expansion coefficient \[1/K\]
284    pub thermal_expansion: f64,
285    /// Melting point \[K\]
286    pub melting_point: f64,
287    /// Speed of sound \[m/s\]
288    pub speed_of_sound: f64,
289    /// Electrical resistivity \[Ω·m\]
290    pub electrical_resistivity: f64,
291    /// Emissivity (0 = perfect reflector, 1 = blackbody)
292    pub emissivity: f64,
293}
294
295impl ExtendedMaterial {
296    /// Create a new extended material with all properties.
297    #[allow(clippy::too_many_arguments)]
298    pub fn new(
299        name: impl Into<String>,
300        density: f64,
301        young_modulus: f64,
302        poisson_ratio: f64,
303        yield_strength: f64,
304        ultimate_strength: f64,
305        thermal_conductivity: f64,
306        specific_heat: f64,
307        thermal_expansion: f64,
308        melting_point: f64,
309        speed_of_sound: f64,
310        electrical_resistivity: f64,
311        emissivity: f64,
312    ) -> Self {
313        Self {
314            name: name.into(),
315            density,
316            young_modulus,
317            poisson_ratio,
318            yield_strength,
319            ultimate_strength,
320            thermal_conductivity,
321            specific_heat,
322            thermal_expansion,
323            melting_point,
324            speed_of_sound,
325            electrical_resistivity,
326            emissivity,
327        }
328    }
329
330    /// Shear modulus G = E / (2(1+ν)) \[Pa\].
331    pub fn shear_modulus(&self) -> f64 {
332        self.young_modulus / (2.0 * (1.0 + self.poisson_ratio))
333    }
334
335    /// Bulk modulus K = E / (3(1-2ν)) \[Pa\].
336    pub fn bulk_modulus(&self) -> f64 {
337        self.young_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
338    }
339
340    /// Thermal diffusivity α = λ / (ρ·cp) \[m²/s\].
341    pub fn thermal_diffusivity(&self) -> f64 {
342        self.thermal_conductivity / (self.density * self.specific_heat)
343    }
344
345    /// Acoustic impedance Z = ρ·c \[kg/(m²·s) = Pa·s/m\].
346    pub fn acoustic_impedance(&self) -> f64 {
347        self.density * self.speed_of_sound
348    }
349
350    /// Specific strength (yield strength / density) \[Pa·m³/kg = N·m/kg\].
351    pub fn specific_strength(&self) -> f64 {
352        self.yield_strength / self.density
353    }
354
355    /// Specific stiffness (Young's modulus / density) \[Pa·m³/kg\].
356    pub fn specific_stiffness(&self) -> f64 {
357        self.young_modulus / self.density
358    }
359
360    /// Thermal shock resistance (simplified Biot-type):
361    /// R = σ_y · λ / (E · α) \[W/m\].
362    pub fn thermal_shock_resistance(&self) -> f64 {
363        self.yield_strength * self.thermal_conductivity
364            / (self.young_modulus * self.thermal_expansion)
365    }
366
367    /// Reflection coefficient at normal incidence from medium 1 (air, Z₁ = 415)
368    /// to this material: R = ((Z₂ - Z₁) / (Z₂ + Z₁))².
369    pub fn acoustic_reflection_coefficient_from_air(&self) -> f64 {
370        const Z_AIR: f64 = 415.0; // kg/(m²·s) at 20°C
371        let z2 = self.acoustic_impedance();
372        let r = (z2 - Z_AIR) / (z2 + Z_AIR);
373        r * r
374    }
375
376    /// Skin depth at frequency f \[Hz\] for electromagnetic waves:
377    /// δ = sqrt(ρ_e / (π · f · μ₀)) \[m\].
378    pub fn skin_depth(&self, frequency_hz: f64) -> f64 {
379        const MU_0: f64 = 1.256_637_061_4e-6; // H/m
380        (self.electrical_resistivity / (std::f64::consts::PI * frequency_hz * MU_0)).sqrt()
381    }
382
383    /// Thermal boundary layer thickness for flow over a flat plate at x:
384    /// δ_T = 5x / sqrt(Re_x) · Pr^{-1/3},  Re_x = ρ·v·x/η.
385    ///
386    /// Returns δ_T \[m\].
387    pub fn thermal_boundary_layer(
388        &self,
389        x: f64,
390        velocity: f64,
391        dynamic_viscosity: f64,
392        prandtl: f64,
393    ) -> f64 {
394        let re_x = self.density * velocity * x / dynamic_viscosity;
395        if re_x < 1e-15 {
396            return 0.0;
397        }
398        5.0 * x / re_x.sqrt() * prandtl.powf(-1.0 / 3.0)
399    }
400
401    /// Return `true` if this material has higher specific stiffness than another.
402    pub fn is_stiffer_specific(&self, other: &ExtendedMaterial) -> bool {
403        self.specific_stiffness() > other.specific_stiffness()
404    }
405
406    /// Preset: structural steel (S355).
407    pub fn steel() -> Self {
408        Self::new(
409            "steel_s355",
410            7850.0,
411            200.0e9,
412            0.30,
413            355.0e6,
414            490.0e6,
415            50.0,
416            486.0,
417            12.0e-6,
418            1808.0,
419            5960.0,
420            1.7e-7,
421            0.28,
422        )
423    }
424
425    /// Preset: aluminium alloy (6061-T6).
426    pub fn aluminium_6061() -> Self {
427        Self::new(
428            "aluminium_6061",
429            2700.0,
430            68.9e9,
431            0.33,
432            276.0e6,
433            310.0e6,
434            167.0,
435            896.0,
436            23.6e-6,
437            933.0,
438            6320.0,
439            4.0e-8,
440            0.09,
441        )
442    }
443
444    /// Preset: Ti-6Al-4V titanium alloy.
445    pub fn titanium_6al4v() -> Self {
446        Self::new(
447            "titanium_6al4v",
448            4430.0,
449            114.0e9,
450            0.34,
451            880.0e6,
452            950.0e6,
453            6.7,
454            560.0,
455            8.6e-6,
456            1878.0,
457            6070.0,
458            1.7e-6,
459            0.35,
460        )
461    }
462
463    /// Preset: Carbon Fibre Reinforced Polymer (quasi-isotropic laminate).
464    pub fn cfrp_quasi_isotropic() -> Self {
465        Self::new(
466            "cfrp_quasi_iso",
467            1550.0,
468            70.0e9,
469            0.30,
470            600.0e6,
471            700.0e6,
472            5.0,
473            800.0,
474            2.0e-6,
475            3800.0,
476            3000.0,
477            1e4,
478            0.95,
479        )
480    }
481
482    /// Preset: borosilicate glass (Pyrex).
483    pub fn borosilicate_glass() -> Self {
484        Self::new(
485            "borosilicate_glass",
486            2230.0,
487            64.0e9,
488            0.20,
489            40.0e6,
490            40.0e6,
491            1.2,
492            830.0,
493            3.3e-6,
494            1100.0,
495            5640.0,
496            1e12,
497            0.92,
498        )
499    }
500}
501
502// ─── Material Mixture and Interpolation ───────────────────────────────────────
503
504/// Material mixing rules for composite and alloy properties.
505#[allow(dead_code)]
506pub struct MaterialMixture;
507
508impl MaterialMixture {
509    /// Linear (isostrain / Voigt) rule for Young's modulus.
510    ///
511    /// E_V = φ·E₂ + (1-φ)·E₁
512    pub fn voigt_modulus(e1: f64, e2: f64, phi: f64) -> f64 {
513        (1.0 - phi) * e1 + phi * e2
514    }
515
516    /// Isostress (Reuss) rule for Young's modulus.
517    ///
518    /// 1/E_R = φ/E₂ + (1-φ)/E₁
519    pub fn reuss_modulus(e1: f64, e2: f64, phi: f64) -> f64 {
520        let inv = (1.0 - phi) / e1 + phi / e2;
521        1.0 / inv
522    }
523
524    /// Hill average: E_H = (E_V + E_R) / 2.
525    pub fn hill_modulus(e1: f64, e2: f64, phi: f64) -> f64 {
526        0.5 * (Self::voigt_modulus(e1, e2, phi) + Self::reuss_modulus(e1, e2, phi))
527    }
528
529    /// Voigt (linear) rule for thermal conductivity.
530    pub fn voigt_conductivity(k1: f64, k2: f64, phi: f64) -> f64 {
531        (1.0 - phi) * k1 + phi * k2
532    }
533
534    /// Hashin-Shtrikman upper bound for bulk modulus.
535    ///
536    /// # Arguments
537    /// * `k1`, `k2` — bulk moduli of phases 1 and 2 \[Pa\]
538    /// * `g1`, `g2` — shear moduli of phases 1 and 2 \[Pa\]
539    /// * `phi`      — volume fraction of phase 2
540    pub fn hashin_shtrikman_upper_bulk(k1: f64, k2: f64, g1: f64, _g2: f64, phi: f64) -> f64 {
541        // HS+ uses phase 1 (stiffer) as reference if k1 > k2
542        let (k_ref, k_inc, _g_ref, phi_inc) = if k1 >= k2 {
543            (k1, k2, g1, phi)
544        } else {
545            (k2, k1, _g2, 1.0 - phi)
546        };
547        let denom = 3.0 * k_ref + 4.0 * _g_ref;
548        k_ref + phi_inc / ((1.0 / (k_inc - k_ref)) + (1.0 - phi_inc) * 3.0 / denom)
549    }
550
551    /// Hashin-Shtrikman lower bound for bulk modulus.
552    pub fn hashin_shtrikman_lower_bulk(k1: f64, k2: f64, g1: f64, g2: f64, phi: f64) -> f64 {
553        // HS- uses the softer phase as reference
554        Self::hashin_shtrikman_upper_bulk(k2, k1, g2, g1, 1.0 - phi)
555    }
556
557    /// Mixture density: ρ = φ·ρ₂ + (1-φ)·ρ₁ \[kg/m³\].
558    pub fn mixture_density(rho1: f64, rho2: f64, phi: f64) -> f64 {
559        (1.0 - phi) * rho1 + phi * rho2
560    }
561
562    /// Effective thermal expansion coefficient (Turner model):
563    ///
564    /// α_eff = (φ·α₂·K₂ + (1-φ)·α₁·K₁) / (φ·K₂ + (1-φ)·K₁)
565    ///
566    /// where K_i are bulk moduli of the individual phases.
567    pub fn turner_thermal_expansion(alpha1: f64, k1: f64, alpha2: f64, k2: f64, phi: f64) -> f64 {
568        let num = (1.0 - phi) * alpha1 * k1 + phi * alpha2 * k2;
569        let den = (1.0 - phi) * k1 + phi * k2;
570        if den.abs() < f64::EPSILON {
571            0.0
572        } else {
573            num / den
574        }
575    }
576}
577
578// ─── Material Selection Utilities ─────────────────────────────────────────────
579
580/// Multi-criterion material selection index (Ashby-type).
581#[allow(dead_code)]
582pub struct MaterialIndex;
583
584impl MaterialIndex {
585    /// Minimum mass beam (stiffness-limited): E^{1/2} / ρ.
586    ///
587    /// Higher is better for minimum weight beams under bending.
588    pub fn beam_stiffness_index(young_modulus: f64, density: f64) -> f64 {
589        young_modulus.sqrt() / density
590    }
591
592    /// Minimum mass column (buckling-limited): E^{1/3} / ρ.
593    pub fn column_buckling_index(young_modulus: f64, density: f64) -> f64 {
594        young_modulus.powf(1.0 / 3.0) / density
595    }
596
597    /// Minimum mass panel (stiffness-limited): E^{1/3} / ρ.
598    pub fn panel_stiffness_index(young_modulus: f64, density: f64) -> f64 {
599        young_modulus.powf(1.0 / 3.0) / density
600    }
601
602    /// Strength-limited beam: σ_y^{2/3} / ρ.
603    pub fn beam_strength_index(yield_strength: f64, density: f64) -> f64 {
604        yield_strength.powf(2.0 / 3.0) / density
605    }
606
607    /// Thermal insulation index (low conductivity, high specific heat):
608    /// λ / (ρ · cp).  Lower thermal diffusivity = better insulator.
609    pub fn thermal_insulation_index(conductivity: f64, density: f64, specific_heat: f64) -> f64 {
610        conductivity / (density * specific_heat)
611    }
612
613    /// Pressure vessel (fracture-limited): K_Ic² / (π · σ_y²)
614    ///
615    /// Proportional to critical crack length — higher is safer.
616    pub fn pressure_vessel_index(k_ic: f64, yield_strength: f64) -> f64 {
617        k_ic * k_ic / (std::f64::consts::PI * yield_strength * yield_strength)
618    }
619
620    /// Wear resistance index: H / (E · λ) where H ≈ 3σ_y (Vickers).
621    ///
622    /// Higher yield → better wear resistance relative to contact mechanics.
623    pub fn wear_resistance_index(yield_strength: f64, young_modulus: f64) -> f64 {
624        3.0 * yield_strength / young_modulus
625    }
626}
627
628// ─── Elastic Moduli Conversions ────────────────────────────────────────────────
629
630/// Convert (E, ν) → (K, G, λ) — the standard elastic moduli family.
631///
632/// Returns `(K, G, lambda)` where:
633/// - K = bulk modulus \[Pa\]
634/// - G = shear modulus \[Pa\]
635/// - λ = Lamé's first parameter \[Pa\]
636#[allow(dead_code)]
637#[allow(non_snake_case)]
638pub fn elastic_moduli_from_E_nu(E: f64, nu: f64) -> (f64, f64, f64) {
639    let K = E / (3.0 * (1.0 - 2.0 * nu));
640    let G = E / (2.0 * (1.0 + nu));
641    let lambda = E * nu / ((1.0 + nu) * (1.0 - 2.0 * nu));
642    (K, G, lambda)
643}
644
645/// Convert (K, G) → (E, ν, λ).
646///
647/// Returns `(E, nu, lambda)`.
648#[allow(dead_code)]
649pub fn elastic_moduli_from_kg(k: f64, g: f64) -> (f64, f64, f64) {
650    let e = 9.0 * k * g / (3.0 * k + g);
651    let nu = (3.0 * k - 2.0 * g) / (2.0 * (3.0 * k + g));
652    let lambda = k - 2.0 * g / 3.0;
653    (e, nu, lambda)
654}
655
656/// Convert (λ, G) → (E, ν, K).
657///
658/// Returns `(E, nu, K)`.
659#[allow(dead_code)]
660pub fn elastic_moduli_from_lame(lambda: f64, g: f64) -> (f64, f64, f64) {
661    let e = g * (3.0 * lambda + 2.0 * g) / (lambda + g);
662    let nu = lambda / (2.0 * (lambda + g));
663    let k = lambda + 2.0 * g / 3.0;
664    (e, nu, k)
665}
666
667/// Check physical admissibility of (E, ν):
668/// - E > 0
669/// - -1 < ν < 0.5 (for isotropic solids; auxetic materials allow ν < 0)
670#[allow(dead_code)]
671pub fn elastic_moduli_admissible(e: f64, nu: f64) -> bool {
672    e > 0.0 && nu > -1.0 && nu < 0.5
673}
674
675// ─── Wave Speed Utilities ──────────────────────────────────────────────────────
676
677/// Longitudinal (P-wave) speed: c_p = sqrt((K + 4G/3) / ρ) \[m/s\].
678///
679/// This is the speed of dilatational waves in an infinite elastic solid.
680#[allow(dead_code)]
681pub fn p_wave_speed(bulk_modulus: f64, shear_modulus: f64, density: f64) -> f64 {
682    let m = bulk_modulus + 4.0 * shear_modulus / 3.0;
683    (m / density).sqrt()
684}
685
686/// Shear (S-wave) speed: c_s = sqrt(G / ρ) \[m/s\].
687#[allow(dead_code)]
688pub fn s_wave_speed(shear_modulus: f64, density: f64) -> f64 {
689    (shear_modulus / density).sqrt()
690}
691
692/// Rayleigh surface wave speed (Viktorov approximation):
693/// c_R ≈ c_s · (0.862 + 1.14·ν) / (1 + ν).
694#[allow(dead_code)]
695pub fn rayleigh_wave_speed(shear_modulus: f64, density: f64, poisson_ratio: f64) -> f64 {
696    let cs = s_wave_speed(shear_modulus, density);
697    cs * (0.862 + 1.14 * poisson_ratio) / (1.0 + poisson_ratio)
698}
699
700/// Bar (longitudinal in thin rod) wave speed: c_bar = sqrt(E / ρ) \[m/s\].
701#[allow(dead_code)]
702pub fn bar_wave_speed(young_modulus: f64, density: f64) -> f64 {
703    (young_modulus / density).sqrt()
704}
705
706/// Reflection coefficient at normal incidence between two media:
707/// R = ((Z₂ - Z₁) / (Z₂ + Z₁))² (intensity).
708#[allow(dead_code)]
709pub fn acoustic_reflection_coefficient(z1: f64, z2: f64) -> f64 {
710    if (z1 + z2).abs() < f64::EPSILON {
711        return 0.0;
712    }
713    let r = (z2 - z1) / (z2 + z1);
714    r * r
715}
716
717/// Transmission coefficient at normal incidence:
718/// T = 1 - R = 4·Z₁·Z₂ / (Z₁ + Z₂)².
719#[allow(dead_code)]
720pub fn acoustic_transmission_coefficient(z1: f64, z2: f64) -> f64 {
721    1.0 - acoustic_reflection_coefficient(z1, z2)
722}
723
724// ─── Thermal Expansion Stress ──────────────────────────────────────────────────
725
726/// Thermal stress in a fully constrained bar:
727/// σ = -E · α · ΔT  \[Pa\].
728///
729/// Negative = compression when ΔT > 0 (material wants to expand but is constrained).
730#[allow(dead_code)]
731pub fn thermal_stress_constrained(young_modulus: f64, alpha: f64, delta_t: f64) -> f64 {
732    -young_modulus * alpha * delta_t
733}
734
735/// Free thermal strain: ε_th = α · ΔT (dimensionless).
736#[allow(dead_code)]
737pub fn thermal_strain(alpha: f64, delta_t: f64) -> f64 {
738    alpha * delta_t
739}
740
741/// Mismatch stress at a bi-material interface (Suhir model, simplified):
742/// σ_mismatch = E_eff · (α₂ - α₁) · ΔT / (1 - ν_eff)
743///
744/// where E_eff and ν_eff are harmonic averages.
745#[allow(dead_code)]
746pub fn bimaterial_mismatch_stress(
747    e1: f64,
748    nu1: f64,
749    alpha1: f64,
750    e2: f64,
751    nu2: f64,
752    alpha2: f64,
753    delta_t: f64,
754) -> f64 {
755    let e_eff = 2.0 * e1 * e2 / (e1 + e2);
756    let nu_eff = (nu1 + nu2) / 2.0;
757    e_eff * (alpha2 - alpha1) * delta_t / (1.0 - nu_eff)
758}
759
760// ─── Material Search / Filter Utilities ───────────────────────────────────────
761
762/// Filter a slice of `ExtendedMaterial` by minimum Young's modulus.
763#[allow(dead_code)]
764pub fn filter_by_min_stiffness(
765    materials: &[ExtendedMaterial],
766    min_e: f64,
767) -> Vec<&ExtendedMaterial> {
768    materials
769        .iter()
770        .filter(|m| m.young_modulus >= min_e)
771        .collect()
772}
773
774/// Filter by maximum density (lightweight materials).
775#[allow(dead_code)]
776pub fn filter_by_max_density(
777    materials: &[ExtendedMaterial],
778    max_rho: f64,
779) -> Vec<&ExtendedMaterial> {
780    materials.iter().filter(|m| m.density <= max_rho).collect()
781}
782
783/// Find the material with the highest specific stiffness (E/ρ).
784#[allow(dead_code)]
785pub fn highest_specific_stiffness(materials: &[ExtendedMaterial]) -> Option<&ExtendedMaterial> {
786    materials.iter().max_by(|a, b| {
787        a.specific_stiffness()
788            .partial_cmp(&b.specific_stiffness())
789            .expect("operation should succeed")
790    })
791}
792
793/// Find the material with the best thermal shock resistance.
794#[allow(dead_code)]
795pub fn best_thermal_shock_resistance(materials: &[ExtendedMaterial]) -> Option<&ExtendedMaterial> {
796    materials.iter().max_by(|a, b| {
797        a.thermal_shock_resistance()
798            .partial_cmp(&b.thermal_shock_resistance())
799            .expect("operation should succeed")
800    })
801}
802
803/// Rank materials by an Ashby beam-stiffness index E^(1/2)/ρ.
804///
805/// Returns indices into the input slice sorted best → worst.
806#[allow(dead_code)]
807pub fn rank_by_beam_stiffness_index(materials: &[ExtendedMaterial]) -> Vec<usize> {
808    let mut indices: Vec<usize> = (0..materials.len()).collect();
809    indices.sort_by(|&a, &b| {
810        let ia =
811            MaterialIndex::beam_stiffness_index(materials[a].young_modulus, materials[a].density);
812        let ib =
813            MaterialIndex::beam_stiffness_index(materials[b].young_modulus, materials[b].density);
814        ib.partial_cmp(&ia).unwrap_or(std::cmp::Ordering::Equal)
815    });
816    indices
817}
818
819// ─── Simple Material Catalogue ────────────────────────────────────────────────
820
821/// Return a small catalogue of extended materials for testing and demonstration.
822#[allow(dead_code)]
823pub fn standard_material_catalogue() -> Vec<ExtendedMaterial> {
824    vec![
825        ExtendedMaterial::steel(),
826        ExtendedMaterial::aluminium_6061(),
827        ExtendedMaterial::titanium_6al4v(),
828        ExtendedMaterial::cfrp_quasi_isotropic(),
829        ExtendedMaterial::borosilicate_glass(),
830    ]
831}
832
833#[cfg(test)]
834mod tests {
835    use super::*;
836
837    #[test]
838    fn test_material_library() {
839        let mut lib = MaterialLibrary::new();
840        assert!(lib.is_empty());
841        lib.add(Material::new("steel", 7800.0, 0.6, 0.3));
842        assert_eq!(lib.len(), 1);
843        let steel = lib.get("steel").unwrap();
844        assert_eq!(steel.density, 7800.0);
845    }
846
847    #[test]
848    fn test_linear_elastic_bulk_shear_modulus_steel() {
849        // Steel: E = 200 GPa, nu = 0.3
850        let mat = LinearElastic::new(200.0e9, 0.3);
851        let k = mat.bulk_modulus();
852        let g = mat.shear_modulus();
853        // K = 200e9 / (3*(1-0.6)) = 200e9 / 1.2 ≈ 166.67e9
854        assert!((k - 166.667e9).abs() < 1.0e8);
855        // G = 200e9 / (2*1.3) ≈ 76.923e9
856        assert!((g - 76.923e9).abs() < 1.0e8);
857    }
858
859    #[test]
860    #[allow(clippy::needless_range_loop)]
861    fn test_linear_elastic_stress_strain_symmetry() {
862        let mat = LinearElastic::new(200.0e9, 0.3);
863        let c = mat.stress_strain_matrix_3d();
864        for i in 0..6 {
865            for j in 0..6 {
866                assert!(
867                    (c[i][j] - c[j][i]).abs() < 1.0e-6,
868                    "C[{i}][{j}] != C[{j}][{i}]"
869                );
870            }
871        }
872    }
873
874    #[test]
875    #[allow(clippy::needless_range_loop)]
876    fn test_neo_hookean_identity_zero_stress() {
877        let mat = NeoHookean::new(1.0e6, 1.0e9);
878        let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
879        let p = mat.first_piola_kirchhoff_stress(&identity);
880        for i in 0..3 {
881            for j in 0..3 {
882                assert!(
883                    p[i][j].abs() < 1.0e-6,
884                    "P[{i}][{j}] = {} should be ~0",
885                    p[i][j]
886                );
887            }
888        }
889    }
890
891    #[test]
892    fn test_tait_eos_pressure_at_reference() {
893        let eos = TaitEos::new(1000.0, 0.0, 1500.0, 7.0);
894        let p = eos.pressure(1000.0);
895        // At reference density, (rho/rho0)^gamma - 1 = 0, so pressure = reference_pressure = 0
896        assert!(
897            p.abs() < 1.0e-6,
898            "Pressure at ref density should be ~0, got {p}"
899        );
900    }
901
902    #[test]
903    fn test_tait_eos_sound_speed() {
904        let eos = TaitEos::new(1000.0, 0.0, 1500.0, 7.0);
905        let cs = eos.sound_speed(1000.0);
906        // At reference density, sound speed should equal c0
907        assert!(
908            (cs - 1500.0).abs() < 1.0e-6,
909            "Sound speed at ref density should be 1500, got {cs}"
910        );
911    }
912
913    #[test]
914    fn test_friction_combine_average() {
915        let r = combine_friction(0.4, 0.6, FrictionCombineRule::Average);
916        assert!((r - 0.5).abs() < 1.0e-10);
917    }
918
919    #[test]
920    fn test_friction_combine_min() {
921        let r = combine_friction(0.4, 0.6, FrictionCombineRule::Min);
922        assert!((r - 0.4).abs() < 1.0e-10);
923    }
924
925    #[test]
926    fn test_friction_combine_max() {
927        let r = combine_friction(0.4, 0.6, FrictionCombineRule::Max);
928        assert!((r - 0.6).abs() < 1.0e-10);
929    }
930
931    #[test]
932    fn test_friction_combine_multiply() {
933        let r = combine_friction(0.4, 0.6, FrictionCombineRule::Multiply);
934        assert!((r - 0.24).abs() < 1.0e-10);
935    }
936
937    #[test]
938    fn test_restitution_combine_rules() {
939        assert!(
940            (combine_restitution(0.3, 0.7, RestitutionCombineRule::Average) - 0.5).abs() < 1.0e-10
941        );
942        assert!((combine_restitution(0.3, 0.7, RestitutionCombineRule::Min) - 0.3).abs() < 1.0e-10);
943        assert!((combine_restitution(0.3, 0.7, RestitutionCombineRule::Max) - 0.7).abs() < 1.0e-10);
944        assert!(
945            (combine_restitution(0.3, 0.7, RestitutionCombineRule::Multiply) - 0.21).abs()
946                < 1.0e-10
947        );
948    }
949
950    #[test]
951    fn test_material_remove() {
952        let mut lib = MaterialLibrary::new();
953        lib.add(Material::new("steel", 7800.0, 0.6, 0.3));
954        lib.add(Material::new("rubber", 1100.0, 0.8, 0.9));
955        assert_eq!(lib.len(), 2);
956        let removed = lib.remove("steel");
957        assert!(removed.is_some());
958        assert_eq!(lib.len(), 1);
959        assert!(!lib.contains("steel"));
960        assert!(lib.contains("rubber"));
961    }
962
963    #[test]
964    fn test_material_contains() {
965        let mut lib = MaterialLibrary::new();
966        lib.add(Material::new("wood", 600.0, 0.5, 0.4));
967        assert!(lib.contains("wood"));
968        assert!(!lib.contains("gold"));
969    }
970
971    #[test]
972    fn test_material_names() {
973        let mut lib = MaterialLibrary::new();
974        lib.add(Material::new("a", 1.0, 0.1, 0.1));
975        lib.add(Material::new("b", 2.0, 0.2, 0.2));
976        let names = lib.names();
977        assert_eq!(names.len(), 2);
978        assert!(names.contains(&"a".to_string()));
979        assert!(names.contains(&"b".to_string()));
980    }
981
982    #[test]
983    fn test_material_iter() {
984        let mut lib = MaterialLibrary::new();
985        lib.add(Material::new("x", 1.0, 0.1, 0.1));
986        lib.add(Material::new("y", 2.0, 0.2, 0.2));
987        let count = lib.iter().count();
988        assert_eq!(count, 2);
989    }
990
991    #[test]
992    fn test_material_impedance() {
993        let m = Material::new("steel", 7800.0, 0.6, 0.3);
994        let z = m.impedance(5000.0);
995        assert!((z - 39_000_000.0).abs() < 1.0);
996    }
997
998    #[test]
999    fn test_material_is_heavier_than() {
1000        let steel = Material::new("steel", 7800.0, 0.6, 0.3);
1001        let wood = Material::new("wood", 600.0, 0.5, 0.4);
1002        assert!(steel.is_heavier_than(&wood));
1003        assert!(!wood.is_heavier_than(&steel));
1004    }
1005
1006    #[test]
1007    fn test_material_is_bouncier_than() {
1008        let rubber = Material::new("rubber", 1100.0, 0.8, 0.9);
1009        let steel = Material::new("steel", 7800.0, 0.6, 0.3);
1010        assert!(rubber.is_bouncier_than(&steel));
1011    }
1012
1013    #[test]
1014    fn test_material_combine_average() {
1015        let a = Material::new("a", 1000.0, 0.4, 0.2);
1016        let b = Material::new("b", 2000.0, 0.6, 0.8);
1017        let c = a.combine_average(&b);
1018        assert!((c.density - 1500.0).abs() < 1e-10);
1019        assert!((c.friction - 0.5).abs() < 1e-10);
1020        assert!((c.restitution - 0.5).abs() < 1e-10);
1021        assert_eq!(c.name, "a+b");
1022    }
1023
1024    #[test]
1025    fn test_material_weight() {
1026        let m = Material::new("water", 1000.0, 0.0, 0.0);
1027        let w = m.weight(1.0, 9.81);
1028        assert!((w - 9810.0).abs() < 1e-6);
1029    }
1030
1031    #[test]
1032    fn test_material_mass_from_volume() {
1033        let m = Material::new("iron", 7874.0, 0.5, 0.3);
1034        let mass = m.mass_from_volume(2.0);
1035        assert!((mass - 15748.0).abs() < 1e-6);
1036    }
1037
1038    #[test]
1039    fn test_lerp_property() {
1040        assert!((lerp_property(0.0, 10.0, 0.5) - 5.0).abs() < 1e-10);
1041        assert!((lerp_property(0.0, 10.0, 0.0)).abs() < 1e-10);
1042        assert!((lerp_property(0.0, 10.0, 1.0) - 10.0).abs() < 1e-10);
1043    }
1044
1045    #[test]
1046    fn test_velocity_dependent_restitution() {
1047        let e = velocity_dependent_restitution(0.8, 5.0, 0.01);
1048        // 0.8 * (1 - 0.01 * 5) = 0.8 * 0.95 = 0.76
1049        assert!((e - 0.76).abs() < 1e-10);
1050    }
1051
1052    #[test]
1053    fn test_velocity_dependent_restitution_clamped() {
1054        let e = velocity_dependent_restitution(0.8, 200.0, 0.01);
1055        // 0.8 * (1 - 0.01 * 200) = 0.8 * (-1.0) = -0.8 => clamped to 0
1056        assert!(e.abs() < 1e-10);
1057    }
1058
1059    #[test]
1060    fn test_critical_damping() {
1061        let cc = critical_damping(100.0, 1.0);
1062        // 2 * sqrt(100) = 20
1063        assert!((cc - 20.0).abs() < 1e-10);
1064    }
1065
1066    #[test]
1067    fn test_damping_ratio() {
1068        let zeta = damping_ratio(20.0, 100.0, 1.0);
1069        // c_crit = 20, so zeta = 20/20 = 1.0 (critically damped)
1070        assert!((zeta - 1.0).abs() < 1e-10);
1071    }
1072
1073    #[test]
1074    fn test_natural_frequency() {
1075        let omega = natural_frequency(100.0, 1.0);
1076        assert!((omega - 10.0).abs() < 1e-10);
1077    }
1078
1079    #[test]
1080    fn test_natural_frequency_zero_mass() {
1081        let omega = natural_frequency(100.0, 0.0);
1082        assert!(omega.abs() < 1e-10);
1083    }
1084
1085    #[test]
1086    fn test_preset_materials_reasonable_values() {
1087        let materials = [
1088            presets::steel(),
1089            presets::aluminum(),
1090            presets::rubber(),
1091            presets::wood(),
1092            presets::glass(),
1093            presets::water(),
1094            presets::concrete(),
1095        ];
1096        for m in &materials {
1097            assert!(m.density > 0.0, "{} density should be positive", m.name);
1098            assert!(
1099                m.friction >= 0.0,
1100                "{} friction should be non-negative",
1101                m.name
1102            );
1103            assert!(
1104                m.restitution >= 0.0 && m.restitution <= 1.0,
1105                "{} restitution should be in [0, 1]",
1106                m.name
1107            );
1108        }
1109        // Steel should be denser than water
1110        assert!(presets::steel().density > presets::water().density);
1111        // Rubber should have highest restitution
1112        assert!(presets::rubber().restitution > presets::steel().restitution);
1113    }
1114
1115    // ── ExtendedMaterial tests ─────────────────────────────────────────────────
1116
1117    #[test]
1118    fn test_extended_material_steel_shear_modulus() {
1119        let s = ExtendedMaterial::steel();
1120        let g = s.shear_modulus();
1121        // For E=200e9, nu=0.3: G ≈ 76.9 GPa
1122        assert!((g - 76.9e9).abs() / 76.9e9 < 0.01, "G = {g}");
1123    }
1124
1125    #[test]
1126    fn test_extended_material_steel_bulk_modulus() {
1127        let s = ExtendedMaterial::steel();
1128        let k = s.bulk_modulus();
1129        // For E=200e9, nu=0.3: K ≈ 166.7 GPa
1130        assert!((k - 166.7e9).abs() / 166.7e9 < 0.01, "K = {k}");
1131    }
1132
1133    #[test]
1134    fn test_extended_material_thermal_diffusivity_steel() {
1135        let s = ExtendedMaterial::steel();
1136        let alpha = s.thermal_diffusivity();
1137        // λ=50, ρ=7850, cp=486 → α ≈ 1.31e-5 m²/s
1138        assert!(
1139            alpha > 1.0e-5 && alpha < 2.0e-5,
1140            "thermal diffusivity = {alpha}"
1141        );
1142    }
1143
1144    #[test]
1145    fn test_extended_material_acoustic_impedance() {
1146        let s = ExtendedMaterial::steel();
1147        let z = s.acoustic_impedance();
1148        // Z = 7850 * 5960 ≈ 4.68e7
1149        assert!(z > 4.0e7, "steel acoustic impedance = {z}");
1150    }
1151
1152    #[test]
1153    fn test_extended_material_specific_strength_ti_gt_steel() {
1154        let ti = ExtendedMaterial::titanium_6al4v();
1155        let fe = ExtendedMaterial::steel();
1156        assert!(
1157            ti.specific_strength() > fe.specific_strength(),
1158            "Ti specific strength ({}) should exceed steel ({})",
1159            ti.specific_strength(),
1160            fe.specific_strength()
1161        );
1162    }
1163
1164    #[test]
1165    fn test_extended_material_thermal_shock_resistance_positive() {
1166        let al = ExtendedMaterial::aluminium_6061();
1167        let r = al.thermal_shock_resistance();
1168        assert!(r > 0.0, "thermal shock resistance = {r}");
1169    }
1170
1171    #[test]
1172    fn test_extended_material_acoustic_reflection_coefficient() {
1173        let s = ExtendedMaterial::steel();
1174        let r = s.acoustic_reflection_coefficient_from_air();
1175        // Steel has very high impedance compared to air → R ≈ 1
1176        assert!(
1177            r > 0.99,
1178            "reflection from air-steel interface should be ~1, got {r}"
1179        );
1180    }
1181
1182    #[test]
1183    fn test_extended_material_skin_depth_steel() {
1184        let s = ExtendedMaterial::steel();
1185        // At 1 MHz: δ = sqrt(1.7e-7 / (π * 1e6 * 4π*1e-7)) ≈ 6.6 μm for steel
1186        let delta = s.skin_depth(1.0e6);
1187        assert!(
1188            delta > 1.0e-6 && delta < 1.0e-3,
1189            "skin depth at 1 MHz = {delta}"
1190        );
1191    }
1192
1193    #[test]
1194    fn test_extended_material_cfrp_lighter_than_steel() {
1195        let cfrp = ExtendedMaterial::cfrp_quasi_isotropic();
1196        let steel = ExtendedMaterial::steel();
1197        assert!(cfrp.density < steel.density);
1198    }
1199
1200    #[test]
1201    fn test_extended_material_specific_stiffness_comparison() {
1202        let al = ExtendedMaterial::aluminium_6061();
1203        let cfrp = ExtendedMaterial::cfrp_quasi_isotropic();
1204        // Both should have reasonable specific stiffness
1205        assert!(al.specific_stiffness() > 0.0);
1206        assert!(cfrp.specific_stiffness() > 0.0);
1207    }
1208
1209    // ── MaterialMixture tests ──────────────────────────────────────────────────
1210
1211    #[test]
1212    fn test_voigt_modulus_at_zero_phi() {
1213        let e = MaterialMixture::voigt_modulus(100.0e9, 300.0e9, 0.0);
1214        assert!((e - 100.0e9).abs() < 1.0, "Voigt at φ=0 should be E1");
1215    }
1216
1217    #[test]
1218    fn test_voigt_modulus_at_full_phi() {
1219        let e = MaterialMixture::voigt_modulus(100.0e9, 300.0e9, 1.0);
1220        assert!((e - 300.0e9).abs() < 1.0, "Voigt at φ=1 should be E2");
1221    }
1222
1223    #[test]
1224    fn test_reuss_modulus_bounded_below_voigt() {
1225        let e_voigt = MaterialMixture::voigt_modulus(100.0e9, 300.0e9, 0.4);
1226        let e_reuss = MaterialMixture::reuss_modulus(100.0e9, 300.0e9, 0.4);
1227        assert!(
1228            e_reuss <= e_voigt + 1.0,
1229            "Reuss ({e_reuss}) should ≤ Voigt ({e_voigt})"
1230        );
1231    }
1232
1233    #[test]
1234    fn test_hill_modulus_between_bounds() {
1235        let e1 = 70.0e9_f64;
1236        let e2 = 400.0e9_f64;
1237        let phi = 0.3_f64;
1238        let hill = MaterialMixture::hill_modulus(e1, e2, phi);
1239        let voigt = MaterialMixture::voigt_modulus(e1, e2, phi);
1240        let reuss = MaterialMixture::reuss_modulus(e1, e2, phi);
1241        assert!(
1242            hill >= reuss - 1.0 && hill <= voigt + 1.0,
1243            "Hill ({hill}) not between Reuss ({reuss}) and Voigt ({voigt})"
1244        );
1245    }
1246
1247    #[test]
1248    fn test_mixture_density_linear() {
1249        let rho = MaterialMixture::mixture_density(1000.0, 3000.0, 0.5);
1250        assert!((rho - 2000.0).abs() < 1e-10);
1251    }
1252
1253    #[test]
1254    fn test_turner_thermal_expansion_equal_phases() {
1255        // If both phases have the same α and K, the mixture α = that value
1256        let alpha_eff =
1257            MaterialMixture::turner_thermal_expansion(10e-6, 1.0e11, 10e-6, 1.0e11, 0.4);
1258        assert!((alpha_eff - 10e-6).abs() < 1e-14, "α_eff = {alpha_eff}");
1259    }
1260
1261    #[test]
1262    fn test_hashin_shtrikman_upper_at_zero_phi() {
1263        // At φ=0 the mixture should approach phase 1
1264        let k_hs =
1265            MaterialMixture::hashin_shtrikman_upper_bulk(100.0e9, 300.0e9, 40.0e9, 100.0e9, 0.0);
1266        // Very close to k1 = 100e9 or k2 = 300e9 depending on ordering
1267        assert!((100.0e9 - 1.0..=300.0e9 + 1.0).contains(&k_hs));
1268    }
1269
1270    // ── MaterialIndex tests ────────────────────────────────────────────────────
1271
1272    #[test]
1273    fn test_beam_stiffness_index_positive() {
1274        let idx = MaterialIndex::beam_stiffness_index(200.0e9, 7850.0);
1275        assert!(idx > 0.0, "index = {idx}");
1276    }
1277
1278    #[test]
1279    fn test_column_buckling_index_positive() {
1280        let idx = MaterialIndex::column_buckling_index(200.0e9, 7850.0);
1281        assert!(idx > 0.0, "index = {idx}");
1282    }
1283
1284    #[test]
1285    fn test_beam_strength_index_positive() {
1286        let idx = MaterialIndex::beam_strength_index(355.0e6, 7850.0);
1287        assert!(idx > 0.0);
1288    }
1289
1290    #[test]
1291    fn test_pressure_vessel_index_positive() {
1292        let idx = MaterialIndex::pressure_vessel_index(50.0e6, 355.0e6);
1293        assert!(idx > 0.0, "index = {idx}");
1294    }
1295
1296    #[test]
1297    fn test_wear_resistance_index_positive() {
1298        let idx = MaterialIndex::wear_resistance_index(355.0e6, 200.0e9);
1299        assert!(idx > 0.0);
1300    }
1301
1302    #[test]
1303    fn test_thermal_insulation_index_lower_for_higher_conductivity() {
1304        // Lower conductivity → lower thermal diffusivity → better insulator
1305        let i_steel = MaterialIndex::thermal_insulation_index(50.0, 7850.0, 486.0);
1306        let i_glass = MaterialIndex::thermal_insulation_index(1.0, 2500.0, 750.0);
1307        assert!(
1308            i_glass < i_steel,
1309            "glass insulation index should be lower: glass={i_glass}, steel={i_steel}"
1310        );
1311    }
1312
1313    // ── Elastic moduli conversion tests ───────────────────────────────────────
1314
1315    #[test]
1316    fn test_elastic_moduli_round_trip() {
1317        let e = 200.0e9_f64;
1318        let nu = 0.3_f64;
1319        let (k, g, _lambda) = elastic_moduli_from_E_nu(e, nu);
1320        let (e2, nu2, _) = elastic_moduli_from_kg(k, g);
1321        assert!((e2 - e).abs() / e < 1e-10, "E round-trip: {e2} vs {e}");
1322        assert!((nu2 - nu).abs() < 1e-10, "ν round-trip: {nu2} vs {nu}");
1323    }
1324
1325    #[test]
1326    fn test_elastic_moduli_from_lame_round_trip() {
1327        let lambda = 115.38e9_f64; // steel Lamé
1328        let g = 76.92e9_f64;
1329        let (e, nu, k) = elastic_moduli_from_lame(lambda, g);
1330        assert!(e > 0.0);
1331        assert!(nu > 0.0 && nu < 0.5);
1332        assert!(k > 0.0);
1333    }
1334
1335    #[test]
1336    fn test_elastic_admissible_steel() {
1337        assert!(elastic_moduli_admissible(200.0e9, 0.30));
1338    }
1339
1340    #[test]
1341    fn test_elastic_admissible_negative_nu_auxetic() {
1342        // Auxetic material: ν in (-1, 0) is valid
1343        assert!(elastic_moduli_admissible(50.0e9, -0.5));
1344    }
1345
1346    #[test]
1347    fn test_elastic_inadmissible_incompressible() {
1348        // ν = 0.5 → K → ∞: not admissible
1349        assert!(!elastic_moduli_admissible(200.0e9, 0.5));
1350    }
1351
1352    // ── Wave speed tests ──────────────────────────────────────────────────────
1353
1354    #[test]
1355    fn test_p_wave_speed_steel() {
1356        // Steel: K ≈ 166.7e9, G ≈ 76.9e9, ρ = 7850
1357        let (k, g, _) = elastic_moduli_from_E_nu(200.0e9, 0.3);
1358        let cp = p_wave_speed(k, g, 7850.0);
1359        // Expected ~5960 m/s
1360        assert!(cp > 5000.0 && cp < 7000.0, "P-wave speed = {cp}");
1361    }
1362
1363    #[test]
1364    fn test_s_wave_speed_steel() {
1365        let g = 76.9e9_f64;
1366        let cs = s_wave_speed(g, 7850.0);
1367        // ≈ 3130 m/s
1368        assert!(cs > 2500.0 && cs < 4000.0, "S-wave speed = {cs}");
1369    }
1370
1371    #[test]
1372    fn test_rayleigh_wave_speed_less_than_s() {
1373        let g = 76.9e9_f64;
1374        let cs = s_wave_speed(g, 7850.0);
1375        let cr = rayleigh_wave_speed(g, 7850.0, 0.3);
1376        assert!(
1377            cr < cs,
1378            "Rayleigh speed should be < S-wave speed: {cr} vs {cs}"
1379        );
1380    }
1381
1382    #[test]
1383    fn test_bar_wave_speed_positive() {
1384        let cb = bar_wave_speed(200.0e9, 7850.0);
1385        assert!(cb > 0.0);
1386    }
1387
1388    #[test]
1389    fn test_acoustic_reflection_same_medium() {
1390        // Z1 = Z2 → R = 0 (no reflection)
1391        let r = acoustic_reflection_coefficient(1.0e7, 1.0e7);
1392        assert!(r.abs() < 1e-15);
1393    }
1394
1395    #[test]
1396    fn test_acoustic_transmission_plus_reflection_unity() {
1397        let z1 = 415.0_f64;
1398        let z2 = 46.8e6_f64;
1399        let r = acoustic_reflection_coefficient(z1, z2);
1400        let t = acoustic_transmission_coefficient(z1, z2);
1401        assert!(
1402            (r + t - 1.0).abs() < 1e-12,
1403            "R + T should = 1, got {}",
1404            r + t
1405        );
1406    }
1407
1408    // ── Thermal stress tests ───────────────────────────────────────────────────
1409
1410    #[test]
1411    fn test_thermal_stress_constrained_compressive() {
1412        // ΔT > 0 in constrained bar → compressive (negative) stress
1413        let sigma = thermal_stress_constrained(200.0e9, 12e-6, 100.0);
1414        assert!(
1415            sigma < 0.0,
1416            "Thermal stress should be compressive, got {sigma}"
1417        );
1418    }
1419
1420    #[test]
1421    fn test_thermal_strain_positive() {
1422        let eps = thermal_strain(12e-6, 50.0);
1423        assert!((eps - 6e-4).abs() < 1e-15);
1424    }
1425
1426    #[test]
1427    fn test_bimaterial_mismatch_stress_direction() {
1428        // α₂ > α₁ and ΔT > 0 → σ > 0 (phase 2 wants to expand more)
1429        let sigma = bimaterial_mismatch_stress(200.0e9, 0.30, 12e-6, 70.0e9, 0.33, 23e-6, 100.0);
1430        assert!(sigma > 0.0, "mismatch stress should be positive: {sigma}");
1431    }
1432
1433    #[test]
1434    fn test_bimaterial_zero_mismatch_equal_alphas() {
1435        let sigma = bimaterial_mismatch_stress(200.0e9, 0.30, 12e-6, 70.0e9, 0.33, 12e-6, 200.0);
1436        assert!(sigma.abs() < 1e-3, "zero mismatch for equal α: {sigma}");
1437    }
1438
1439    // ── Filter / search tests ──────────────────────────────────────────────────
1440
1441    #[test]
1442    fn test_filter_by_min_stiffness() {
1443        let catalogue = standard_material_catalogue();
1444        let stiff = filter_by_min_stiffness(&catalogue, 100.0e9);
1445        // Steel, Al, Ti, glass all above 100 GPa; CFRP ≈ 70 GPa might be excluded
1446        assert!(!stiff.is_empty());
1447        for m in &stiff {
1448            assert!(
1449                m.young_modulus >= 100.0e9,
1450                "{} E={}",
1451                m.name,
1452                m.young_modulus
1453            );
1454        }
1455    }
1456
1457    #[test]
1458    fn test_filter_by_max_density() {
1459        let catalogue = standard_material_catalogue();
1460        let light = filter_by_max_density(&catalogue, 3000.0);
1461        for m in &light {
1462            assert!(m.density <= 3000.0);
1463        }
1464    }
1465
1466    #[test]
1467    fn test_highest_specific_stiffness_found() {
1468        let catalogue = standard_material_catalogue();
1469        let best = highest_specific_stiffness(&catalogue);
1470        assert!(best.is_some());
1471        let best_ss = best.unwrap().specific_stiffness();
1472        for m in &catalogue {
1473            assert!(m.specific_stiffness() <= best_ss + 1.0);
1474        }
1475    }
1476
1477    #[test]
1478    fn test_best_thermal_shock_resistance_found() {
1479        let catalogue = standard_material_catalogue();
1480        let best = best_thermal_shock_resistance(&catalogue);
1481        assert!(best.is_some());
1482    }
1483
1484    #[test]
1485    fn test_rank_by_beam_stiffness_index() {
1486        let catalogue = standard_material_catalogue();
1487        let ranked = rank_by_beam_stiffness_index(&catalogue);
1488        assert_eq!(ranked.len(), catalogue.len());
1489        // First-ranked should have highest E^0.5/ρ
1490        let best_idx = ranked[0];
1491        let best_val = MaterialIndex::beam_stiffness_index(
1492            catalogue[best_idx].young_modulus,
1493            catalogue[best_idx].density,
1494        );
1495        for &i in &ranked[1..] {
1496            let val = MaterialIndex::beam_stiffness_index(
1497                catalogue[i].young_modulus,
1498                catalogue[i].density,
1499            );
1500            assert!(val <= best_val + 1.0);
1501        }
1502    }
1503
1504    #[test]
1505    fn test_standard_catalogue_count() {
1506        let catalogue = standard_material_catalogue();
1507        assert_eq!(catalogue.len(), 5);
1508    }
1509}
1510
1511pub mod electromagnetic;
1512
1513pub mod geomaterials;
1514
1515pub mod nuclear_materials_advanced;
1516
1517pub mod alloy_materials;
1518pub mod biomedical_materials;
1519pub mod composites_advanced;
1520
1521pub mod biomechanical_materials;
1522pub mod ceramics_materials;
1523pub mod geomaterial_models;
1524pub mod polymers_materials;
1525pub mod semiconductor_materials;
1526pub mod shape_memory_alloy;
1527pub mod tribology_materials;