Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -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);
}
}