Initial commit
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user