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 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#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Material {
101 pub name: String,
103 pub density: f64,
105 pub friction: f64,
107 pub restitution: f64,
109}
110
111impl Material {
112 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#[derive(Debug, Default)]
125pub struct MaterialLibrary {
126 materials: HashMap<String, Material>,
127}
128
129impl MaterialLibrary {
130 pub fn new() -> Self {
132 Self::default()
133 }
134
135 pub fn add(&mut self, material: Material) {
137 self.materials.insert(material.name.clone(), material);
138 }
139
140 pub fn get(&self, name: &str) -> Option<&Material> {
142 self.materials.get(name)
143 }
144
145 pub fn len(&self) -> usize {
147 self.materials.len()
148 }
149
150 pub fn is_empty(&self) -> bool {
152 self.materials.is_empty()
153 }
154
155 pub fn remove(&mut self, name: &str) -> Option<Material> {
157 self.materials.remove(name)
158 }
159
160 pub fn iter(&self) -> impl Iterator<Item = (&String, &Material)> {
162 self.materials.iter()
163 }
164
165 pub fn contains(&self, name: &str) -> bool {
167 self.materials.contains_key(name)
168 }
169
170 pub fn names(&self) -> Vec<String> {
172 self.materials.keys().cloned().collect()
173 }
174}
175
176impl Material {
177 pub fn impedance(&self, speed_of_sound: f64) -> f64 {
181 self.density * speed_of_sound
182 }
183
184 pub fn is_heavier_than(&self, other: &Material) -> bool {
186 self.density > other.density
187 }
188
189 pub fn is_bouncier_than(&self, other: &Material) -> bool {
191 self.restitution > other.restitution
192 }
193
194 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 pub fn weight(&self, volume: f64, gravity: f64) -> f64 {
206 self.density * volume * gravity
207 }
208
209 pub fn mass_from_volume(&self, volume: f64) -> f64 {
211 self.density * volume
212 }
213}
214
215#[allow(dead_code)]
217pub fn lerp_property(a: f64, b: f64, t: f64) -> f64 {
218 a + (b - a) * t
219}
220
221#[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#[allow(dead_code)]
235pub fn critical_damping(stiffness: f64, mass: f64) -> f64 {
236 2.0 * (stiffness * mass).sqrt()
237}
238
239#[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#[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#[allow(dead_code)]
265#[derive(Debug, Clone)]
266pub struct ExtendedMaterial {
267 pub name: String,
269 pub density: f64,
271 pub young_modulus: f64,
273 pub poisson_ratio: f64,
275 pub yield_strength: f64,
277 pub ultimate_strength: f64,
279 pub thermal_conductivity: f64,
281 pub specific_heat: f64,
283 pub thermal_expansion: f64,
285 pub melting_point: f64,
287 pub speed_of_sound: f64,
289 pub electrical_resistivity: f64,
291 pub emissivity: f64,
293}
294
295impl ExtendedMaterial {
296 #[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 pub fn shear_modulus(&self) -> f64 {
332 self.young_modulus / (2.0 * (1.0 + self.poisson_ratio))
333 }
334
335 pub fn bulk_modulus(&self) -> f64 {
337 self.young_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
338 }
339
340 pub fn thermal_diffusivity(&self) -> f64 {
342 self.thermal_conductivity / (self.density * self.specific_heat)
343 }
344
345 pub fn acoustic_impedance(&self) -> f64 {
347 self.density * self.speed_of_sound
348 }
349
350 pub fn specific_strength(&self) -> f64 {
352 self.yield_strength / self.density
353 }
354
355 pub fn specific_stiffness(&self) -> f64 {
357 self.young_modulus / self.density
358 }
359
360 pub fn thermal_shock_resistance(&self) -> f64 {
363 self.yield_strength * self.thermal_conductivity
364 / (self.young_modulus * self.thermal_expansion)
365 }
366
367 pub fn acoustic_reflection_coefficient_from_air(&self) -> f64 {
370 const Z_AIR: f64 = 415.0; let z2 = self.acoustic_impedance();
372 let r = (z2 - Z_AIR) / (z2 + Z_AIR);
373 r * r
374 }
375
376 pub fn skin_depth(&self, frequency_hz: f64) -> f64 {
379 const MU_0: f64 = 1.256_637_061_4e-6; (self.electrical_resistivity / (std::f64::consts::PI * frequency_hz * MU_0)).sqrt()
381 }
382
383 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 pub fn is_stiffer_specific(&self, other: &ExtendedMaterial) -> bool {
403 self.specific_stiffness() > other.specific_stiffness()
404 }
405
406 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 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 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 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 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#[allow(dead_code)]
506pub struct MaterialMixture;
507
508impl MaterialMixture {
509 pub fn voigt_modulus(e1: f64, e2: f64, phi: f64) -> f64 {
513 (1.0 - phi) * e1 + phi * e2
514 }
515
516 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 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 pub fn voigt_conductivity(k1: f64, k2: f64, phi: f64) -> f64 {
531 (1.0 - phi) * k1 + phi * k2
532 }
533
534 pub fn hashin_shtrikman_upper_bulk(k1: f64, k2: f64, g1: f64, _g2: f64, phi: f64) -> f64 {
541 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 pub fn hashin_shtrikman_lower_bulk(k1: f64, k2: f64, g1: f64, g2: f64, phi: f64) -> f64 {
553 Self::hashin_shtrikman_upper_bulk(k2, k1, g2, g1, 1.0 - phi)
555 }
556
557 pub fn mixture_density(rho1: f64, rho2: f64, phi: f64) -> f64 {
559 (1.0 - phi) * rho1 + phi * rho2
560 }
561
562 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#[allow(dead_code)]
582pub struct MaterialIndex;
583
584impl MaterialIndex {
585 pub fn beam_stiffness_index(young_modulus: f64, density: f64) -> f64 {
589 young_modulus.sqrt() / density
590 }
591
592 pub fn column_buckling_index(young_modulus: f64, density: f64) -> f64 {
594 young_modulus.powf(1.0 / 3.0) / density
595 }
596
597 pub fn panel_stiffness_index(young_modulus: f64, density: f64) -> f64 {
599 young_modulus.powf(1.0 / 3.0) / density
600 }
601
602 pub fn beam_strength_index(yield_strength: f64, density: f64) -> f64 {
604 yield_strength.powf(2.0 / 3.0) / density
605 }
606
607 pub fn thermal_insulation_index(conductivity: f64, density: f64, specific_heat: f64) -> f64 {
610 conductivity / (density * specific_heat)
611 }
612
613 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 pub fn wear_resistance_index(yield_strength: f64, young_modulus: f64) -> f64 {
624 3.0 * yield_strength / young_modulus
625 }
626}
627
628#[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#[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#[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#[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#[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#[allow(dead_code)]
688pub fn s_wave_speed(shear_modulus: f64, density: f64) -> f64 {
689 (shear_modulus / density).sqrt()
690}
691
692#[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#[allow(dead_code)]
702pub fn bar_wave_speed(young_modulus: f64, density: f64) -> f64 {
703 (young_modulus / density).sqrt()
704}
705
706#[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#[allow(dead_code)]
720pub fn acoustic_transmission_coefficient(z1: f64, z2: f64) -> f64 {
721 1.0 - acoustic_reflection_coefficient(z1, z2)
722}
723
724#[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#[allow(dead_code)]
737pub fn thermal_strain(alpha: f64, delta_t: f64) -> f64 {
738 alpha * delta_t
739}
740
741#[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#[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#[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#[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#[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#[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#[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 let mat = LinearElastic::new(200.0e9, 0.3);
851 let k = mat.bulk_modulus();
852 let g = mat.shear_modulus();
853 assert!((k - 166.667e9).abs() < 1.0e8);
855 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 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 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 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 assert!(e.abs() < 1e-10);
1057 }
1058
1059 #[test]
1060 fn test_critical_damping() {
1061 let cc = critical_damping(100.0, 1.0);
1062 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 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 assert!(presets::steel().density > presets::water().density);
1111 assert!(presets::rubber().restitution > presets::steel().restitution);
1113 }
1114
1115 #[test]
1118 fn test_extended_material_steel_shear_modulus() {
1119 let s = ExtendedMaterial::steel();
1120 let g = s.shear_modulus();
1121 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 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 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 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 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 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 assert!(al.specific_stiffness() > 0.0);
1206 assert!(cfrp.specific_stiffness() > 0.0);
1207 }
1208
1209 #[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 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 let k_hs =
1265 MaterialMixture::hashin_shtrikman_upper_bulk(100.0e9, 300.0e9, 40.0e9, 100.0e9, 0.0);
1266 assert!((100.0e9 - 1.0..=300.0e9 + 1.0).contains(&k_hs));
1268 }
1269
1270 #[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 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 #[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; 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 assert!(elastic_moduli_admissible(50.0e9, -0.5));
1344 }
1345
1346 #[test]
1347 fn test_elastic_inadmissible_incompressible() {
1348 assert!(!elastic_moduli_admissible(200.0e9, 0.5));
1350 }
1351
1352 #[test]
1355 fn test_p_wave_speed_steel() {
1356 let (k, g, _) = elastic_moduli_from_E_nu(200.0e9, 0.3);
1358 let cp = p_wave_speed(k, g, 7850.0);
1359 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 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 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 #[test]
1411 fn test_thermal_stress_constrained_compressive() {
1412 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 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 #[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 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 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;