//! Kelvin-Maxwell viscoelastic material model. //! //! This is the standard viscoelastic material model used by LS-DYNA //! (MAT_KELVIN-MAXWELL_VISCOELASTIC, MAT_076). use crate::viscoelastic::prony::PronyCoefficients; use serde::{Deserialize, Serialize}; /// Kelvin-Maxwell viscoelastic material for LS-DYNA. /// /// This represents a first-order Kelvin-Maxwell model with: /// - Bulk modulus (K) for volumetric response /// - Long-term shear modulus (G0) /// - Short-term shear modulus (Gi) /// - Decay constant (βi) /// /// The shear relaxation function is: /// G(t) = G0 + Gi * exp(-βi * t) #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct KelvinMaxwell { /// Density (RO) in kg/m³ pub density: f64, /// Bulk modulus (K) in Pa pub bulk_modulus: f64, /// Long-term shear modulus (G0) in Pa pub g0: f64, /// Short-term shear modulus (GI) in Pa pub gi: f64, /// Decay constant (BETAI) in 1/s pub beta_i: f64, } impl KelvinMaxwell { /// Create a new Kelvin-Maxwell material. /// /// # Arguments /// * `density` - Density in kg/m³ /// * `bulk_modulus` - Bulk modulus (K) in Pa /// * `g0` - Long-term shear modulus in Pa /// * `gi` - Short-term shear modulus in Pa /// * `beta_i` - Decay constant in 1/s pub fn new(density: f64, bulk_modulus: f64, g0: f64, gi: f64, beta_i: f64) -> Self { Self { density, bulk_modulus, g0, gi, beta_i, } } /// Create from Prony series coefficients. /// /// # Arguments /// * `prony` - Prony series coefficients /// * `density` - Material density in kg/m³ /// * `bulk_modulus` - Bulk modulus in Pa (or computed from Poisson's ratio) pub fn from_prony(prony: &PronyCoefficients, density: f64, bulk_modulus: f64) -> Self { Self { density, bulk_modulus, g0: prony.ginf, gi: prony.g1, beta_i: 1.0 / prony.tau, } } /// Create from Prony coefficients with Poisson's ratio. /// /// The bulk modulus is calculated assuming the instantaneous response. pub fn from_prony_with_poisson(prony: &PronyCoefficients, density: f64, nu: f64) -> Self { // G_instantaneous = G∞ + G1 let g_inst = prony.g0(); // E = 2G(1 + ν) let e = 2.0 * g_inst * (1.0 + nu); // K = E / (3(1 - 2ν)) let k = e / (3.0 * (1.0 - 2.0 * nu)); Self::from_prony(prony, density, k) } /// Convert to Prony series coefficients. pub fn to_prony(&self) -> PronyCoefficients { PronyCoefficients { ginf: self.g0, g1: self.gi, tau: 1.0 / self.beta_i, } } /// Calculate the instantaneous shear modulus: G_inst = G0 + Gi pub fn g_instantaneous(&self) -> f64 { self.g0 + self.gi } /// Calculate shear modulus at time t. pub fn g_at_time(&self, t: f64) -> f64 { self.g0 + self.gi * (-self.beta_i * t).exp() } /// Calculate relaxation time τ = 1/β. pub fn relaxation_time(&self) -> f64 { 1.0 / self.beta_i } /// Calculate complex shear modulus at angular frequency ω. pub fn complex_modulus(&self, omega: f64) -> (f64, f64) { self.to_prony().complex_modulus(omega) } /// Estimate Poisson's ratio from bulk and shear moduli. /// /// Uses the instantaneous shear modulus. pub fn poissons_ratio(&self) -> f64 { let g = self.g_instantaneous(); let k = self.bulk_modulus; (3.0 * k - 2.0 * g) / (6.0 * k + 2.0 * g) } /// Calculate Young's modulus from bulk and shear moduli. pub fn youngs_modulus(&self) -> f64 { let g = self.g_instantaneous(); let k = self.bulk_modulus; 9.0 * k * g / (3.0 * k + g) } } impl Default for KelvinMaxwell { fn default() -> Self { // Typical brain tissue values Self { density: 1040.0, bulk_modulus: 2.19e9, // Nearly incompressible g0: 1000.0, gi: 2000.0, beta_i: 100.0, } } } /// LS-DYNA material card representation. /// /// This generates the format needed for *MAT_KELVIN-MAXWELL_VISCOELASTIC. impl KelvinMaxwell { /// Generate LS-DYNA material card lines. /// /// Returns the card data as would appear in a .k file. pub fn to_lsdyna_card(&self, mid: u32) -> String { // Card 1: MID, RO, K, N // Card 2: GI, BETAI (repeated for each term) let n = 1; // Number of terms (we only support 1-term) format!( "*MAT_KELVIN-MAXWELL_VISCOELASTIC\n\ ${:>9},{:>9},{:>9},{:>9},{:>9},{:>9},{:>9},{:>9}\n\ {:>10},{:>10.4E},{:>10.4E},{:>10}\n\ {:>10.4E},{:>10.4E}\n\ {:>10.4E},{:>10.4E}", "MID", "RO", "BULK", "G0", "", "", "", "", mid, self.density, self.bulk_modulus, self.g0, self.gi, self.beta_i, 0.0, 0.0 // Extra terms (not used) ) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_kelvin_maxwell_creation() { let mat = KelvinMaxwell::new(1040.0, 2.19e9, 1000.0, 2000.0, 100.0); assert_eq!(mat.density, 1040.0); assert_eq!(mat.g0, 1000.0); assert_eq!(mat.gi, 2000.0); assert_eq!(mat.beta_i, 100.0); } #[test] fn test_from_prony() { let prony = PronyCoefficients::new(1000.0, 2000.0, 0.01); let mat = KelvinMaxwell::from_prony(&prony, 1040.0, 2.19e9); assert_eq!(mat.g0, prony.ginf); assert_eq!(mat.gi, prony.g1); assert!((mat.beta_i - 100.0).abs() < 1e-10); // 1/0.01 = 100 } #[test] fn test_to_prony() { let mat = KelvinMaxwell::new(1040.0, 2.19e9, 1000.0, 2000.0, 100.0); let prony = mat.to_prony(); assert_eq!(prony.ginf, 1000.0); assert_eq!(prony.g1, 2000.0); assert!((prony.tau - 0.01).abs() < 1e-10); } #[test] fn test_g_at_time() { let mat = KelvinMaxwell::new(1040.0, 2.19e9, 1000.0, 2000.0, 100.0); // At t=0, G = G0 + Gi = 3000 assert!((mat.g_at_time(0.0) - 3000.0).abs() < 1e-10); // At t->∞, G = G0 = 1000 assert!((mat.g_at_time(1.0) - 1000.0).abs() < 1.0); } #[test] fn test_instantaneous_modulus() { let mat = KelvinMaxwell::new(1040.0, 2.19e9, 1000.0, 2000.0, 100.0); assert_eq!(mat.g_instantaneous(), 3000.0); } }