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