//! Linear elastic material models. //! //! Provides linear elastic material properties commonly used in finite element analysis. use serde::{Deserialize, Serialize}; /// Linear elastic material properties. /// /// This is a simple isotropic elastic material defined by Young's modulus /// and Poisson's ratio. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct LinearElastic { /// Young's modulus (E) in Pa pub youngs_modulus: f64, /// Poisson's ratio (ν), typically 0.0 to 0.5 pub poissons_ratio: f64, /// Density in kg/m³ pub density: f64, } impl LinearElastic { /// Create a new linear elastic material. /// /// # Arguments /// * `youngs_modulus` - Young's modulus (E) in Pa /// * `poissons_ratio` - Poisson's ratio (ν) /// * `density` - Density in kg/m³ /// /// # Panics /// Panics if Poisson's ratio is outside [0, 0.5] or if E or density are negative. pub fn new(youngs_modulus: f64, poissons_ratio: f64, density: f64) -> Self { assert!( youngs_modulus >= 0.0, "Young's modulus must be non-negative" ); assert!( (0.0..=0.5).contains(&poissons_ratio), "Poisson's ratio must be between 0 and 0.5" ); assert!(density >= 0.0, "Density must be non-negative"); Self { youngs_modulus, poissons_ratio, density, } } /// Create a material from shear modulus (G) and bulk modulus (K). pub fn from_shear_bulk(shear_modulus: f64, bulk_modulus: f64, density: f64) -> Self { let youngs_modulus = 9.0 * bulk_modulus * shear_modulus / (3.0 * bulk_modulus + shear_modulus); let poissons_ratio = (3.0 * bulk_modulus - 2.0 * shear_modulus) / (6.0 * bulk_modulus + 2.0 * shear_modulus); Self::new(youngs_modulus, poissons_ratio, density) } /// Create a material from Lamé parameters (λ, μ). pub fn from_lame(lambda: f64, mu: f64, density: f64) -> Self { let youngs_modulus = mu * (3.0 * lambda + 2.0 * mu) / (lambda + mu); let poissons_ratio = lambda / (2.0 * (lambda + mu)); Self::new(youngs_modulus, poissons_ratio, density) } /// Calculate shear modulus (G or μ). pub fn shear_modulus(&self) -> f64 { self.youngs_modulus / (2.0 * (1.0 + self.poissons_ratio)) } /// Calculate bulk modulus (K). pub fn bulk_modulus(&self) -> f64 { self.youngs_modulus / (3.0 * (1.0 - 2.0 * self.poissons_ratio)) } /// Calculate first Lamé parameter (λ). pub fn lame_lambda(&self) -> f64 { let e = self.youngs_modulus; let nu = self.poissons_ratio; e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu)) } /// Calculate second Lamé parameter (μ), same as shear modulus. pub fn lame_mu(&self) -> f64 { self.shear_modulus() } /// Calculate P-wave modulus (M = K + 4G/3). pub fn p_wave_modulus(&self) -> f64 { self.bulk_modulus() + 4.0 * self.shear_modulus() / 3.0 } /// Calculate P-wave velocity. pub fn p_wave_velocity(&self) -> f64 { (self.p_wave_modulus() / self.density).sqrt() } /// Calculate S-wave velocity. pub fn s_wave_velocity(&self) -> f64 { (self.shear_modulus() / self.density).sqrt() } } impl Default for LinearElastic { fn default() -> Self { // Typical soft tissue properties Self { youngs_modulus: 3000.0, // 3 kPa poissons_ratio: 0.49, // Nearly incompressible density: 1000.0, // ~water density } } } /// Common material presets impl LinearElastic { /// Brain tissue (approximate) pub fn brain_tissue() -> Self { Self::new(3000.0, 0.49, 1040.0) } /// Cortical bone pub fn cortical_bone() -> Self { Self::new(17e9, 0.3, 1900.0) } /// Cancellous bone pub fn cancellous_bone() -> Self { Self::new(1e9, 0.3, 600.0) } /// Soft tissue (general) pub fn soft_tissue() -> Self { Self::new(5000.0, 0.49, 1000.0) } /// Steel pub fn steel() -> Self { Self::new(200e9, 0.3, 7850.0) } /// Aluminum pub fn aluminum() -> Self { Self::new(70e9, 0.33, 2700.0) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_elastic_creation() { let mat = LinearElastic::new(200e9, 0.3, 7850.0); assert_eq!(mat.youngs_modulus, 200e9); assert_eq!(mat.poissons_ratio, 0.3); assert_eq!(mat.density, 7850.0); } #[test] fn test_shear_modulus() { let mat = LinearElastic::new(200e9, 0.3, 7850.0); let g = mat.shear_modulus(); // G = E / (2 * (1 + ν)) = 200e9 / (2 * 1.3) ≈ 76.92e9 assert!((g - 76.92307692307693e9).abs() < 1e6); } #[test] fn test_bulk_modulus() { let mat = LinearElastic::new(200e9, 0.3, 7850.0); let k = mat.bulk_modulus(); // K = E / (3 * (1 - 2ν)) = 200e9 / (3 * 0.4) ≈ 166.67e9 assert!((k - 166.66666666666666e9).abs() < 1e6); } #[test] fn test_from_shear_bulk() { let g = 76.92307692307693e9; let k = 166.66666666666666e9; let mat = LinearElastic::from_shear_bulk(g, k, 7850.0); assert!((mat.youngs_modulus - 200e9).abs() < 1e6); assert!((mat.poissons_ratio - 0.3).abs() < 0.001); } #[test] fn test_lame_parameters() { let mat = LinearElastic::new(200e9, 0.3, 7850.0); let lambda = mat.lame_lambda(); let mu = mat.lame_mu(); // Verify round-trip let mat2 = LinearElastic::from_lame(lambda, mu, 7850.0); assert!((mat2.youngs_modulus - mat.youngs_modulus).abs() < 1e3); assert!((mat2.poissons_ratio - mat.poissons_ratio).abs() < 0.0001); } }