1#![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#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Material {
109 pub name: String,
111 pub density: f64,
113 pub friction: f64,
115 pub restitution: f64,
117}
118
119impl Material {
120 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#[derive(Debug, Default)]
133pub struct MaterialLibrary {
134 materials: HashMap<String, Material>,
135}
136
137impl MaterialLibrary {
138 pub fn new() -> Self {
140 Self::default()
141 }
142
143 pub fn add(&mut self, material: Material) {
145 self.materials.insert(material.name.clone(), material);
146 }
147
148 pub fn get(&self, name: &str) -> Option<&Material> {
150 self.materials.get(name)
151 }
152
153 pub fn len(&self) -> usize {
155 self.materials.len()
156 }
157
158 pub fn is_empty(&self) -> bool {
160 self.materials.is_empty()
161 }
162
163 pub fn remove(&mut self, name: &str) -> Option<Material> {
165 self.materials.remove(name)
166 }
167
168 pub fn iter(&self) -> impl Iterator<Item = (&String, &Material)> {
170 self.materials.iter()
171 }
172
173 pub fn contains(&self, name: &str) -> bool {
175 self.materials.contains_key(name)
176 }
177
178 pub fn names(&self) -> Vec<String> {
180 self.materials.keys().cloned().collect()
181 }
182}
183
184impl Material {
185 pub fn impedance(&self, speed_of_sound: f64) -> f64 {
189 self.density * speed_of_sound
190 }
191
192 pub fn is_heavier_than(&self, other: &Material) -> bool {
194 self.density > other.density
195 }
196
197 pub fn is_bouncier_than(&self, other: &Material) -> bool {
199 self.restitution > other.restitution
200 }
201
202 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 pub fn weight(&self, volume: f64, gravity: f64) -> f64 {
214 self.density * volume * gravity
215 }
216
217 pub fn mass_from_volume(&self, volume: f64) -> f64 {
219 self.density * volume
220 }
221}
222
223#[allow(dead_code)]
225pub fn lerp_property(a: f64, b: f64, t: f64) -> f64 {
226 a + (b - a) * t
227}
228
229#[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#[allow(dead_code)]
243pub fn critical_damping(stiffness: f64, mass: f64) -> f64 {
244 2.0 * (stiffness * mass).sqrt()
245}
246
247#[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#[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#[allow(dead_code)]
273#[derive(Debug, Clone)]
274pub struct ExtendedMaterial {
275 pub name: String,
277 pub density: f64,
279 pub young_modulus: f64,
281 pub poisson_ratio: f64,
283 pub yield_strength: f64,
285 pub ultimate_strength: f64,
287 pub thermal_conductivity: f64,
289 pub specific_heat: f64,
291 pub thermal_expansion: f64,
293 pub melting_point: f64,
295 pub speed_of_sound: f64,
297 pub electrical_resistivity: f64,
299 pub emissivity: f64,
301}
302
303impl ExtendedMaterial {
304 #[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 pub fn shear_modulus(&self) -> f64 {
340 self.young_modulus / (2.0 * (1.0 + self.poisson_ratio))
341 }
342
343 pub fn bulk_modulus(&self) -> f64 {
345 self.young_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
346 }
347
348 pub fn thermal_diffusivity(&self) -> f64 {
350 self.thermal_conductivity / (self.density * self.specific_heat)
351 }
352
353 pub fn acoustic_impedance(&self) -> f64 {
355 self.density * self.speed_of_sound
356 }
357
358 pub fn specific_strength(&self) -> f64 {
360 self.yield_strength / self.density
361 }
362
363 pub fn specific_stiffness(&self) -> f64 {
365 self.young_modulus / self.density
366 }
367
368 pub fn thermal_shock_resistance(&self) -> f64 {
371 self.yield_strength * self.thermal_conductivity
372 / (self.young_modulus * self.thermal_expansion)
373 }
374
375 pub fn acoustic_reflection_coefficient_from_air(&self) -> f64 {
378 const Z_AIR: f64 = 415.0; let z2 = self.acoustic_impedance();
380 let r = (z2 - Z_AIR) / (z2 + Z_AIR);
381 r * r
382 }
383
384 pub fn skin_depth(&self, frequency_hz: f64) -> f64 {
387 const MU_0: f64 = 1.256_637_061_4e-6; (self.electrical_resistivity / (std::f64::consts::PI * frequency_hz * MU_0)).sqrt()
389 }
390
391 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 pub fn is_stiffer_specific(&self, other: &ExtendedMaterial) -> bool {
411 self.specific_stiffness() > other.specific_stiffness()
412 }
413
414 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 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 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 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 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#[allow(dead_code)]
514pub struct MaterialMixture;
515
516impl MaterialMixture {
517 pub fn voigt_modulus(e1: f64, e2: f64, phi: f64) -> f64 {
521 (1.0 - phi) * e1 + phi * e2
522 }
523
524 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 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 pub fn voigt_conductivity(k1: f64, k2: f64, phi: f64) -> f64 {
539 (1.0 - phi) * k1 + phi * k2
540 }
541
542 pub fn hashin_shtrikman_upper_bulk(k1: f64, k2: f64, g1: f64, _g2: f64, phi: f64) -> f64 {
549 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 pub fn hashin_shtrikman_lower_bulk(k1: f64, k2: f64, g1: f64, g2: f64, phi: f64) -> f64 {
561 Self::hashin_shtrikman_upper_bulk(k2, k1, g2, g1, 1.0 - phi)
563 }
564
565 pub fn mixture_density(rho1: f64, rho2: f64, phi: f64) -> f64 {
567 (1.0 - phi) * rho1 + phi * rho2
568 }
569
570 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#[allow(dead_code)]
590pub struct MaterialIndex;
591
592impl MaterialIndex {
593 pub fn beam_stiffness_index(young_modulus: f64, density: f64) -> f64 {
597 young_modulus.sqrt() / density
598 }
599
600 pub fn column_buckling_index(young_modulus: f64, density: f64) -> f64 {
602 young_modulus.powf(1.0 / 3.0) / density
603 }
604
605 pub fn panel_stiffness_index(young_modulus: f64, density: f64) -> f64 {
607 young_modulus.powf(1.0 / 3.0) / density
608 }
609
610 pub fn beam_strength_index(yield_strength: f64, density: f64) -> f64 {
612 yield_strength.powf(2.0 / 3.0) / density
613 }
614
615 pub fn thermal_insulation_index(conductivity: f64, density: f64, specific_heat: f64) -> f64 {
618 conductivity / (density * specific_heat)
619 }
620
621 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 pub fn wear_resistance_index(yield_strength: f64, young_modulus: f64) -> f64 {
632 3.0 * yield_strength / young_modulus
633 }
634}
635
636#[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#[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#[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#[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#[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#[allow(dead_code)]
696pub fn s_wave_speed(shear_modulus: f64, density: f64) -> f64 {
697 (shear_modulus / density).sqrt()
698}
699
700#[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#[allow(dead_code)]
710pub fn bar_wave_speed(young_modulus: f64, density: f64) -> f64 {
711 (young_modulus / density).sqrt()
712}
713
714#[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#[allow(dead_code)]
728pub fn acoustic_transmission_coefficient(z1: f64, z2: f64) -> f64 {
729 1.0 - acoustic_reflection_coefficient(z1, z2)
730}
731
732#[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#[allow(dead_code)]
745pub fn thermal_strain(alpha: f64, delta_t: f64) -> f64 {
746 alpha * delta_t
747}
748
749#[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#[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#[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#[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#[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#[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#[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 let mat = LinearElastic::new(200.0e9, 0.3);
859 let k = mat.bulk_modulus();
860 let g = mat.shear_modulus();
861 assert!((k - 166.667e9).abs() < 1.0e8);
863 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 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 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 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 assert!(e.abs() < 1e-10);
1065 }
1066
1067 #[test]
1068 fn test_critical_damping() {
1069 let cc = critical_damping(100.0, 1.0);
1070 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 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 assert!(presets::steel().density > presets::water().density);
1119 assert!(presets::rubber().restitution > presets::steel().restitution);
1121 }
1122
1123 #[test]
1126 fn test_extended_material_steel_shear_modulus() {
1127 let s = ExtendedMaterial::steel();
1128 let g = s.shear_modulus();
1129 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 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 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 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 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 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 assert!(al.specific_stiffness() > 0.0);
1214 assert!(cfrp.specific_stiffness() > 0.0);
1215 }
1216
1217 #[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 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 let k_hs =
1273 MaterialMixture::hashin_shtrikman_upper_bulk(100.0e9, 300.0e9, 40.0e9, 100.0e9, 0.0);
1274 assert!((100.0e9 - 1.0..=300.0e9 + 1.0).contains(&k_hs));
1276 }
1277
1278 #[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 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 #[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; 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 assert!(elastic_moduli_admissible(50.0e9, -0.5));
1352 }
1353
1354 #[test]
1355 fn test_elastic_inadmissible_incompressible() {
1356 assert!(!elastic_moduli_admissible(200.0e9, 0.5));
1358 }
1359
1360 #[test]
1363 fn test_p_wave_speed_steel() {
1364 let (k, g, _) = elastic_moduli_from_E_nu(200.0e9, 0.3);
1366 let cp = p_wave_speed(k, g, 7850.0);
1367 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 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 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 #[test]
1419 fn test_thermal_stress_constrained_compressive() {
1420 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 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 #[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 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 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;