Files
rustytorch/crates/specialized/rtx-materials/src/viscoelastic/kelvin_maxwell.rs
T
2026-03-04 00:08:42 +00:00

232 lines
6.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);
}
}