pub type Stress6 = [f64; 6];
pub trait ConstitutiveModel {
fn stress(&self, strain: Stress6) -> Stress6;
fn tangent_stiffness(&self, strain: Stress6) -> [f64; 36];
}
#[derive(Debug, Clone)]
pub struct LinearElasticMaterial {
pub young_modulus: f64,
pub poisson_ratio: f64,
}
impl LinearElasticMaterial {
pub fn new(young_modulus: f64, poisson_ratio: f64) -> Self {
Self {
young_modulus,
poisson_ratio,
}
}
pub fn constitutive_matrix(&self) -> [[f64; 6]; 6] {
let e = self.young_modulus;
let nu = self.poisson_ratio;
let factor = e / ((1.0 + nu) * (1.0 - 2.0 * nu));
let a = 1.0 - nu;
let g = (1.0 - 2.0 * nu) / 2.0;
let mut d = [[0.0f64; 6]; 6];
d[0][0] = factor * a;
d[0][1] = factor * nu;
d[0][2] = factor * nu;
d[1][0] = factor * nu;
d[1][1] = factor * a;
d[1][2] = factor * nu;
d[2][0] = factor * nu;
d[2][1] = factor * nu;
d[2][2] = factor * a;
d[3][3] = factor * g;
d[4][4] = factor * g;
d[5][5] = factor * g;
d
}
pub fn shear_modulus(&self) -> f64 {
self.young_modulus / (2.0 * (1.0 + self.poisson_ratio))
}
pub fn bulk_modulus(&self) -> f64 {
self.young_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
}
}
#[derive(Debug, Clone)]
pub struct LinearElastic {
pub youngs_modulus: f64,
pub poisson_ratio: f64,
}
impl LinearElastic {
pub fn new(youngs_modulus: f64, poisson_ratio: f64) -> Self {
Self {
youngs_modulus,
poisson_ratio,
}
}
pub fn lambda(&self) -> f64 {
let e = self.youngs_modulus;
let nu = self.poisson_ratio;
e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu))
}
pub fn mu(&self) -> f64 {
self.youngs_modulus / (2.0 * (1.0 + self.poisson_ratio))
}
pub fn isotropic_stiffness_voigt(&self) -> [f64; 36] {
let lambda = self.lambda();
let mu = self.mu();
let mut c = [0.0f64; 36];
c[0] = lambda + 2.0 * mu; c[1] = lambda; c[2] = lambda; c[6] = lambda; c[7] = lambda + 2.0 * mu; c[8] = lambda; c[12] = lambda; c[13] = lambda; c[14] = lambda + 2.0 * mu; c[21] = mu; c[28] = mu; c[35] = mu; c
}
pub fn stress_from_strain(&self, strain: Stress6) -> Stress6 {
let c = self.isotropic_stiffness_voigt();
let mut s = [0.0f64; 6];
for i in 0..6 {
for j in 0..6 {
s[i] += c[i * 6 + j] * strain[j];
}
}
s
}
}
impl ConstitutiveModel for LinearElastic {
fn stress(&self, strain: Stress6) -> Stress6 {
self.stress_from_strain(strain)
}
fn tangent_stiffness(&self, _strain: Stress6) -> [f64; 36] {
self.isotropic_stiffness_voigt()
}
}
#[derive(Debug, Clone)]
pub struct NeoHookean {
pub mu: f64,
pub kappa: f64,
}
impl NeoHookean {
pub fn new(mu: f64, kappa: f64) -> Self {
Self { mu, kappa }
}
pub fn strain_energy_density(&self, f: [[f64; 3]; 3]) -> f64 {
let j = det3(f);
let c = ftf(f); let i1 = c[0][0] + c[1][1] + c[2][2]; let ln_j = j.ln();
self.mu / 2.0 * (i1 - 3.0) - self.mu * ln_j + self.kappa / 4.0 * (j * j - 1.0 - 2.0 * ln_j)
}
pub fn pk2_stress(&self, f: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
let j = det3(f);
let c = ftf(f);
let c_inv = inv3(c);
let coeff = self.kappa / 2.0 * (j * j - 1.0);
let mut s = [[0.0f64; 3]; 3];
for i in 0..3 {
for k in 0..3 {
let delta_ik = if i == k { 1.0 } else { 0.0 };
s[i][k] = self.mu * (delta_ik - c_inv[i][k]) + coeff * c_inv[i][k];
}
}
s
}
}
impl ConstitutiveModel for NeoHookean {
fn stress(&self, strain: Stress6) -> Stress6 {
let f = voigt_strain_to_f(strain);
let s = self.pk2_stress(f);
[s[0][0], s[1][1], s[2][2], s[0][1], s[1][2], s[0][2]]
}
fn tangent_stiffness(&self, _strain: Stress6) -> [f64; 36] {
let le = LinearElastic::new(
self.mu * (3.0 * self.kappa + self.mu) / (self.kappa + self.mu), (3.0 * self.kappa - 2.0 * self.mu) / (2.0 * (3.0 * self.kappa + self.mu)), );
le.isotropic_stiffness_voigt()
}
}
#[derive(Debug, Clone)]
pub struct MooneyRivlin {
pub c10: f64,
pub c01: f64,
}
impl MooneyRivlin {
pub fn new(c10: f64, c01: f64) -> Self {
Self { c10, c01 }
}
pub fn strain_energy_density(&self, i1: f64, i2: f64) -> f64 {
self.c10 * (i1 - 3.0) + self.c01 * (i2 - 3.0)
}
pub fn invariants(f: [[f64; 3]; 3]) -> (f64, f64) {
let c = ftf(f);
let i1 = c[0][0] + c[1][1] + c[2][2];
let mut tr_c2 = 0.0;
for (i, c_row) in c.iter().enumerate() {
for (j, &c_ij) in c_row.iter().enumerate() {
tr_c2 += c_ij * c[j][i];
}
}
let i2 = (i1 * i1 - tr_c2) / 2.0;
(i1, i2)
}
}
impl ConstitutiveModel for MooneyRivlin {
fn stress(&self, strain: Stress6) -> Stress6 {
let f = voigt_strain_to_f(strain);
let (i1, i2) = MooneyRivlin::invariants(f);
let c = ftf(f);
let c2 = mat3_mul(c, c);
let dw_di1 = self.c10;
let dw_di2 = self.c01;
let mut s = [[0.0f64; 3]; 3];
for i in 0..3 {
for k in 0..3 {
let delta = if i == k { 1.0 } else { 0.0 };
s[i][k] = 2.0 * (dw_di1 * delta + dw_di2 * (i1 * delta - c[i][k]));
}
}
let _ = (i2, c2);
[s[0][0], s[1][1], s[2][2], s[0][1], s[1][2], s[0][2]]
}
fn tangent_stiffness(&self, _strain: Stress6) -> [f64; 36] {
let mu = 2.0 * (self.c10 + self.c01);
let le = LinearElastic::new(3.0 * mu, 0.5 - 1e-9); le.isotropic_stiffness_voigt()
}
}
#[derive(Debug, Clone)]
pub struct J2Plasticity {
pub e: f64,
pub nu: f64,
pub yield_stress: f64,
pub hardening: f64,
}
impl J2Plasticity {
pub fn new(e: f64, nu: f64, yield_stress: f64, hardening: f64) -> Self {
Self {
e,
nu,
yield_stress,
hardening,
}
}
pub fn yield_function(&self, stress_voigt: Stress6, plastic_strain: f64) -> f64 {
let s_eq = von_mises_stress(stress_voigt);
s_eq - (self.yield_stress + self.hardening * plastic_strain)
}
pub fn return_mapping(&self, trial_stress: Stress6, plastic_strain: f64) -> (Stress6, f64) {
let mu = self.e / (2.0 * (1.0 + self.nu));
let f_trial = self.yield_function(trial_stress, plastic_strain);
if f_trial <= 0.0 {
return (trial_stress, plastic_strain);
}
let d_gamma = f_trial / (3.0 * mu + self.hardening);
let s_eq_trial = von_mises_stress(trial_stress);
let scale = if s_eq_trial > 1e-30 {
1.0 - d_gamma * 3.0 * mu / s_eq_trial
} else {
1.0
};
let p = hydrostatic_stress(trial_stress);
let dev = deviatoric_voigt(trial_stress);
let corrected = [
p + scale * dev[0],
p + scale * dev[1],
p + scale * dev[2],
scale * trial_stress[3],
scale * trial_stress[4],
scale * trial_stress[5],
];
(corrected, plastic_strain + d_gamma)
}
}
impl ConstitutiveModel for J2Plasticity {
fn stress(&self, strain: Stress6) -> Stress6 {
let le = LinearElastic::new(self.e, self.nu);
let trial = le.stress(strain);
let (s, _) = self.return_mapping(trial, 0.0);
s
}
fn tangent_stiffness(&self, _strain: Stress6) -> [f64; 36] {
let le = LinearElastic::new(self.e, self.nu);
le.isotropic_stiffness_voigt()
}
}
pub fn von_mises_stress(s: Stress6) -> f64 {
let dev_sq = (s[0] - s[1]).powi(2) + (s[1] - s[2]).powi(2) + (s[2] - s[0]).powi(2);
let shear_sq = s[3].powi(2) + s[4].powi(2) + s[5].powi(2);
((dev_sq / 2.0 + 3.0 * shear_sq).max(0.0)).sqrt()
}
pub fn hydrostatic_stress(s: Stress6) -> f64 {
(s[0] + s[1] + s[2]) / 3.0
}
pub fn deviatoric_voigt(s: Stress6) -> Stress6 {
let p = hydrostatic_stress(s);
[s[0] - p, s[1] - p, s[2] - p, s[3], s[4], s[5]]
}
#[inline]
fn det3(m: [[f64; 3]; 3]) -> f64 {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
#[inline]
fn ftf(f: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut c = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for f_k in &f {
c[i][j] += f_k[i] * f_k[j];
}
}
}
c
}
fn mat3_mul(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut c = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
fn inv3(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
let d = det3(m);
assert!(d.abs() > 1e-30, "matrix is singular");
let inv_d = 1.0 / d;
[
[
(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_d,
(m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_d,
(m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_d,
],
[
(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_d,
(m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_d,
(m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_d,
],
[
(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_d,
(m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_d,
(m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_d,
],
]
}
fn voigt_strain_to_f(strain: Stress6) -> [[f64; 3]; 3] {
[
[1.0 + strain[0], strain[3] / 2.0, strain[5] / 2.0],
[strain[3] / 2.0, 1.0 + strain[1], strain[4] / 2.0],
[strain[5] / 2.0, strain[4] / 2.0, 1.0 + strain[2]],
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constitutive_matrix_steel() {
let mat = LinearElasticMaterial::new(200.0e9, 0.3);
let d = mat.constitutive_matrix();
let factor = 200.0e9 / ((1.0 + 0.3) * (1.0 - 0.6));
let expected_d00 = factor * 0.7;
let expected_d01 = factor * 0.3;
let expected_d33 = factor * 0.2;
assert!(
(d[0][0] - expected_d00).abs() / expected_d00.abs() < 1e-12,
"D[0][0] = {} expected {expected_d00}",
d[0][0]
);
assert!(
(d[0][1] - expected_d01).abs() / expected_d01.abs() < 1e-12,
"D[0][1] = {} expected {expected_d01}",
d[0][1]
);
assert!(
(d[3][3] - expected_d33).abs() / expected_d33.abs() < 1e-12,
"D[3][3] = {} expected {expected_d33}",
d[3][3]
);
for (i, row) in d.iter().enumerate() {
for (j, &v) in row.iter().enumerate() {
assert!((v - d[j][i]).abs() < 1e-6, "D not symmetric at ({i},{j})");
}
}
assert_eq!(d[0][3], 0.0);
assert_eq!(d[3][0], 0.0);
}
#[test]
fn test_shear_and_bulk_modulus() {
let mat = LinearElasticMaterial::new(200.0e9, 0.3);
let g = mat.shear_modulus();
let k = mat.bulk_modulus();
let expected_g = 200.0e9 / (2.0 * 1.3);
let expected_k = 200.0e9 / (3.0 * 0.4);
assert!((g - expected_g).abs() / expected_g < 1e-12);
assert!((k - expected_k).abs() / expected_k < 1e-12);
}
#[test]
fn test_isotropic_stiffness_symmetry() {
let mat = LinearElastic::new(210.0e9, 0.3);
let c = mat.isotropic_stiffness_voigt();
for i in 0..6 {
for j in 0..6 {
assert!(
(c[i * 6 + j] - c[j * 6 + i]).abs() < 1.0,
"C not symmetric at ({i},{j}): {} vs {}",
c[i * 6 + j],
c[j * 6 + i]
);
}
}
}
#[test]
fn test_pure_hydrostatic_gives_diagonal_stress() {
let mat = LinearElastic::new(210.0e9, 0.3);
let eps = 1e-3;
let strain = [eps, eps, eps, 0.0, 0.0, 0.0];
let stress = mat.stress(strain);
assert!(stress[3].abs() < 1.0, "σ12 should be ~0, got {}", stress[3]);
assert!(stress[4].abs() < 1.0, "σ23 should be ~0, got {}", stress[4]);
assert!(stress[5].abs() < 1.0, "σ13 should be ~0, got {}", stress[5]);
assert!(
(stress[0] - stress[1]).abs() < 1.0,
"σ11 != σ22 in hydrostatic: {} vs {}",
stress[0],
stress[1]
);
assert!(
(stress[1] - stress[2]).abs() < 1.0,
"σ22 != σ33 in hydrostatic: {} vs {}",
stress[1],
stress[2]
);
}
#[test]
fn test_lame_parameters() {
let e = 200.0e9;
let nu = 0.3;
let mat = LinearElastic::new(e, nu);
let expected_lambda = e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu));
let expected_mu = e / (2.0 * (1.0 + nu));
assert!((mat.lambda() - expected_lambda).abs() / expected_lambda < 1e-12);
assert!((mat.mu() - expected_mu).abs() / expected_mu < 1e-12);
}
#[test]
fn test_neohookean_undeformed_zero_pk2() {
let mat = NeoHookean::new(1.0e6, 3.0e6);
let f_identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let s = mat.pk2_stress(f_identity);
for (i, row) in s.iter().enumerate() {
for (k, &v) in row.iter().enumerate() {
assert!(
v.abs() < 1e-6,
"PK2 at identity should be zero, S[{i}][{k}] = {v}"
);
}
}
}
#[test]
fn test_neohookean_energy_positive_for_stretch() {
let mat = NeoHookean::new(1.0e6, 3.0e6);
let f_stretch = [[1.1, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let w = mat.strain_energy_density(f_stretch);
assert!(
w > 0.0,
"strain energy density should be positive for stretch, got {w}"
);
}
#[test]
fn test_mooney_rivlin_energy_positive() {
let mat = MooneyRivlin::new(0.5e6, 0.1e6);
let w = mat.strain_energy_density(3.5, 3.2);
assert!(w > 0.0, "Mooney-Rivlin energy should be positive, got {w}");
}
#[test]
fn test_mooney_rivlin_energy_zero_at_reference() {
let mat = MooneyRivlin::new(0.5e6, 0.1e6);
let w = mat.strain_energy_density(3.0, 3.0);
assert!(
w.abs() < 1e-30,
"W should be 0 at reference config, got {w}"
);
}
#[test]
fn test_j2_yield_at_correct_stress() {
let yield_stress = 250.0e6; let mat = J2Plasticity::new(200.0e9, 0.3, yield_stress, 1.0e9);
let stress_at_yield = [yield_stress, 0.0, 0.0, 0.0, 0.0, 0.0];
let f = mat.yield_function(stress_at_yield, 0.0);
assert!(
f.abs() < 1e-3,
"yield function should be ~0 at σ=σ_y, got {f}"
);
let stress_below = [0.9 * yield_stress, 0.0, 0.0, 0.0, 0.0, 0.0];
let f_below = mat.yield_function(stress_below, 0.0);
assert!(f_below < 0.0, "should be elastic below yield, f={f_below}");
}
#[test]
fn test_j2_return_mapping_elastic() {
let mat = J2Plasticity::new(200.0e9, 0.3, 250.0e6, 1.0e9);
let trial = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0]; let (corrected, ep) = mat.return_mapping(trial, 0.0);
assert_eq!(ep, 0.0);
for i in 0..6 {
assert!((corrected[i] - trial[i]).abs() < 1.0);
}
}
#[test]
fn test_j2_return_mapping_plastic() {
let yield_stress = 250.0e6;
let mat = J2Plasticity::new(200.0e9, 0.3, yield_stress, 0.0); let trial = [400.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let (corrected, ep) = mat.return_mapping(trial, 0.0);
let s_eq = von_mises_stress(corrected);
assert!(
(s_eq - yield_stress).abs() / yield_stress < 1e-6,
"after return mapping σ_eq should equal σ_y: {} vs {}",
s_eq,
yield_stress
);
assert!(ep > 0.0, "plastic strain should be positive after yielding");
}
#[test]
fn test_hydrostatic_and_deviatoric() {
let s = [100.0, 200.0, 300.0, 50.0, 30.0, 20.0];
let p = hydrostatic_stress(s);
assert!((p - 200.0).abs() < 1e-10);
let dev = deviatoric_voigt(s);
let trace = dev[0] + dev[1] + dev[2];
assert!(
trace.abs() < 1e-10,
"deviatoric trace should be 0, got {trace}"
);
}
#[test]
fn test_von_mises_hydrostatic_zero() {
let s = [100.0, 100.0, 100.0, 0.0, 0.0, 0.0];
let vm = von_mises_stress(s);
assert!(
vm.abs() < 1e-8,
"hydrostatic stress has zero von Mises, got {vm}"
);
}
}
#[derive(Debug, Clone)]
pub struct ThermoElastic {
pub youngs_modulus: f64,
pub poisson_ratio: f64,
pub alpha: f64,
pub t_ref: f64,
}
impl ThermoElastic {
pub fn new(youngs_modulus: f64, poisson_ratio: f64, alpha: f64, t_ref: f64) -> Self {
Self {
youngs_modulus,
poisson_ratio,
alpha,
t_ref,
}
}
pub fn thermal_strain(&self, temperature: f64) -> Stress6 {
let dt = temperature - self.t_ref;
let eps_th = self.alpha * dt;
[eps_th, eps_th, eps_th, 0.0, 0.0, 0.0]
}
pub fn stress(&self, strain: Stress6, temperature: f64) -> Stress6 {
let eps_th = self.thermal_strain(temperature);
let eps_mech = [
strain[0] - eps_th[0],
strain[1] - eps_th[1],
strain[2] - eps_th[2],
strain[3],
strain[4],
strain[5],
];
let le = LinearElastic::new(self.youngs_modulus, self.poisson_ratio);
le.stress(eps_mech)
}
pub fn stiffness(&self) -> [f64; 36] {
LinearElastic::new(self.youngs_modulus, self.poisson_ratio).isotropic_stiffness_voigt()
}
pub fn thermal_stress_vector(&self, temperature: f64) -> Stress6 {
let eps_th = self.thermal_strain(temperature);
let le = LinearElastic::new(self.youngs_modulus, self.poisson_ratio);
le.stress(eps_th)
}
}
#[derive(Debug, Clone)]
pub struct OrthotropicElastic {
pub e: [f64; 3],
pub nu: [f64; 3],
pub g: [f64; 3],
}
impl OrthotropicElastic {
pub fn new(e: [f64; 3], nu: [f64; 3], g: [f64; 3]) -> Self {
Self { e, nu, g }
}
pub fn from_isotropic(youngs: f64, poisson: f64) -> Self {
let g_val = youngs / (2.0 * (1.0 + poisson));
Self {
e: [youngs; 3],
nu: [poisson; 3],
g: [g_val; 3],
}
}
pub fn compliance_matrix(&self) -> [f64; 36] {
let [e1, e2, e3] = self.e;
let [nu12, nu13, nu23] = self.nu;
let nu21 = nu12 * e2 / e1.max(f64::EPSILON);
let nu31 = nu13 * e3 / e1.max(f64::EPSILON);
let nu32 = nu23 * e3 / e2.max(f64::EPSILON);
let [g12, g13, g23] = self.g;
let mut s = [0.0_f64; 36];
s[0] = 1.0 / e1;
s[1] = -nu21 / e2;
s[2] = -nu31 / e3;
s[6] = -nu12 / e1;
s[7] = 1.0 / e2;
s[8] = -nu32 / e3;
s[12] = -nu13 / e1;
s[13] = -nu23 / e2;
s[14] = 1.0 / e3;
s[21] = 1.0 / g12;
s[28] = 1.0 / g23;
s[35] = 1.0 / g13;
s
}
pub fn stress_approx(&self, strain: Stress6) -> Stress6 {
let [e1, e2, e3] = self.e;
let [g12, g13, g23] = self.g;
[
e1 * strain[0],
e2 * strain[1],
e3 * strain[2],
g12 * strain[3],
g23 * strain[4],
g13 * strain[5],
]
}
pub fn is_stable(&self) -> bool {
let [e1, e2, e3] = self.e;
let [nu12, nu13, nu23] = self.nu;
let [g12, g13, g23] = self.g;
if e1 <= 0.0 || e2 <= 0.0 || e3 <= 0.0 {
return false;
}
if g12 <= 0.0 || g13 <= 0.0 || g23 <= 0.0 {
return false;
}
let check12 = nu12.abs() < (e1 / e2).sqrt();
let check13 = nu13.abs() < (e1 / e3).sqrt();
let check23 = nu23.abs() < (e2 / e3).sqrt();
check12 && check13 && check23
}
}
#[derive(Debug, Clone)]
pub struct PerzynaViscoplasticity {
pub e: f64,
pub nu: f64,
pub yield_stress: f64,
pub hardening: f64,
pub eta: f64,
pub exponent: f64,
}
impl PerzynaViscoplasticity {
pub fn new(
e: f64,
nu: f64,
yield_stress: f64,
hardening: f64,
eta: f64,
exponent: f64,
) -> Self {
Self {
e,
nu,
yield_stress,
hardening,
eta,
exponent,
}
}
pub fn overstress(&self, stress_voigt: Stress6, plastic_strain: f64) -> f64 {
let s_eq = von_mises_stress(stress_voigt);
let sigma_y = self.yield_stress + self.hardening * plastic_strain;
((s_eq - sigma_y) / sigma_y.max(f64::EPSILON)).max(0.0)
}
pub fn viscoplastic_strain_rate(&self, stress_voigt: Stress6, plastic_strain: f64) -> f64 {
let phi = self.overstress(stress_voigt, plastic_strain);
if phi < f64::EPSILON {
return 0.0;
}
phi.powf(self.exponent) / self.eta.max(f64::EPSILON)
}
pub fn explicit_stress_update(
&self,
strain: Stress6,
plastic_strain: f64,
dt: f64,
) -> (Stress6, f64) {
let le = LinearElastic::new(self.e, self.nu);
let trial = le.stress(strain);
let dg_dt = self.viscoplastic_strain_rate(trial, plastic_strain);
let d_gamma = dg_dt * dt;
let new_ep = plastic_strain + d_gamma;
let s_eq = von_mises_stress(trial);
if s_eq < f64::EPSILON {
return (trial, new_ep);
}
let scale = 1.0 - d_gamma * 3.0 * le.mu() / s_eq.max(f64::EPSILON);
let p = hydrostatic_stress(trial);
let dev = deviatoric_voigt(trial);
let corrected = [
p + scale * dev[0],
p + scale * dev[1],
p + scale * dev[2],
scale * trial[3],
scale * trial[4],
scale * trial[5],
];
(corrected, new_ep)
}
}
pub fn norton_creep_rate(stress_voigt: Stress6, a_coeff: f64, n_exp: f64) -> f64 {
let s_eq = von_mises_stress(stress_voigt);
a_coeff * s_eq.powf(n_exp)
}
pub fn norton_creep_increment(stress_voigt: Stress6, a_coeff: f64, n_exp: f64, dt: f64) -> f64 {
norton_creep_rate(stress_voigt, a_coeff, n_exp) * dt
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelId {
LinearElasticModel,
NeoHookeanModel,
MooneyRivlinModel,
J2PlasticityModel,
ThermoElasticModel,
}
#[derive(Debug, Clone)]
pub struct ModelDescriptor {
pub id: ModelId,
pub name: String,
pub n_state_vars: usize,
}
impl ModelDescriptor {
pub fn linear_elastic() -> Self {
Self {
id: ModelId::LinearElasticModel,
name: "Linear Elastic".into(),
n_state_vars: 0,
}
}
pub fn j2_plasticity() -> Self {
Self {
id: ModelId::J2PlasticityModel,
name: "J2 Plasticity".into(),
n_state_vars: 1, }
}
pub fn thermoelastic() -> Self {
Self {
id: ModelId::ThermoElasticModel,
name: "Thermoelastic".into(),
n_state_vars: 0,
}
}
}
pub fn strain_energy_density(stress: Stress6, strain: Stress6) -> f64 {
0.5 * (stress[0] * strain[0]
+ stress[1] * strain[1]
+ stress[2] * strain[2]
+ stress[3] * strain[3]
+ stress[4] * strain[4]
+ stress[5] * strain[5])
}
pub fn effective_modulus_uniaxial(strain1: f64, stress1: f64, strain2: f64, stress2: f64) -> f64 {
let d_strain = strain2 - strain1;
if d_strain.abs() < f64::EPSILON {
return f64::INFINITY;
}
(stress2 - stress1) / d_strain
}
pub fn stress_j2_invariant(s: Stress6) -> f64 {
let dev = deviatoric_voigt(s);
0.5 * (dev[0] * dev[0]
+ dev[1] * dev[1]
+ dev[2] * dev[2]
+ 2.0 * dev[3] * dev[3]
+ 2.0 * dev[4] * dev[4]
+ 2.0 * dev[5] * dev[5])
}
pub fn lode_angle(s: Stress6) -> f64 {
let j2 = stress_j2_invariant(s);
if j2 < f64::EPSILON {
return 0.0;
}
let dev = deviatoric_voigt(s);
let j3 = dev[0] * (dev[1] * dev[2] - dev[4] * dev[4])
- dev[3] * (dev[3] * dev[2] - dev[4] * dev[5])
+ dev[5] * (dev[3] * dev[4] - dev[1] * dev[5]);
let ratio = (3.0 * 3.0_f64.sqrt() / 2.0) * j3 / j2.powf(1.5);
let ratio_clamped = ratio.clamp(-1.0, 1.0);
ratio_clamped.acos() / 3.0
}
#[cfg(test)]
mod tests_extended {
use super::*;
#[test]
fn thermoelastic_no_thermal_stress_at_ref() {
let mat = ThermoElastic::new(200.0e9, 0.3, 12e-6, 293.0);
let strain = [0.0; 6];
let stress = mat.stress(strain, 293.0); for &si in &stress {
assert!(si.abs() < 1.0, "no thermal stress at T_ref, got {si}");
}
}
#[test]
fn thermoelastic_thermal_strain_scales_with_dt() {
let mat = ThermoElastic::new(200.0e9, 0.3, 12e-6, 293.0);
let eps1 = mat.thermal_strain(393.0); let eps2 = mat.thermal_strain(493.0); assert!(
(eps2[0] / eps1[0] - 2.0).abs() < 1e-10,
"thermal strain ratio: {}",
eps2[0] / eps1[0]
);
}
#[test]
fn thermoelastic_thermal_strain_isotropic() {
let mat = ThermoElastic::new(200.0e9, 0.3, 12e-6, 293.0);
let eps = mat.thermal_strain(393.0);
assert!((eps[0] - eps[1]).abs() < 1e-20);
assert!((eps[1] - eps[2]).abs() < 1e-20);
assert!(eps[3].abs() < 1e-20);
assert!(eps[4].abs() < 1e-20);
assert!(eps[5].abs() < 1e-20);
}
#[test]
fn thermoelastic_positive_thermal_stress_for_heating() {
let mat = ThermoElastic::new(200.0e9, 0.3, 12e-6, 293.0);
let strain = [0.0; 6]; let stress = mat.stress(strain, 393.0);
assert!(
stress[0] < 0.0,
"constrained thermal expansion: sigma_11 = {}",
stress[0]
);
}
#[test]
fn thermoelastic_stiffness_matches_linear_elastic() {
let mat = ThermoElastic::new(200.0e9, 0.3, 12e-6, 293.0);
let le = LinearElastic::new(200.0e9, 0.3);
let c_thermo = mat.stiffness();
let c_le = le.isotropic_stiffness_voigt();
for i in 0..36 {
assert!(
(c_thermo[i] - c_le[i]).abs() < 1.0,
"stiffness mismatch at [{i}]"
);
}
}
#[test]
fn orthotropic_from_isotropic_stable() {
let mat = OrthotropicElastic::from_isotropic(200.0e9, 0.3);
assert!(mat.is_stable(), "isotropic material must satisfy stability");
}
#[test]
fn orthotropic_compliance_diagonal_nonzero() {
let mat = OrthotropicElastic::from_isotropic(200.0e9, 0.3);
let s = mat.compliance_matrix();
assert!(s[0] > 0.0, "S11 > 0");
assert!(s[7] > 0.0, "S22 > 0");
assert!(s[14] > 0.0, "S33 > 0");
assert!(s[21] > 0.0, "S44 > 0");
}
#[test]
fn orthotropic_stress_approx_uniaxial() {
let mat = OrthotropicElastic::from_isotropic(200.0e9, 0.3);
let strain = [1e-3, 0.0, 0.0, 0.0, 0.0, 0.0];
let stress = mat.stress_approx(strain);
assert!(
(stress[0] - 200.0e9 * 1e-3).abs() < 1.0,
"sigma_11 = {}",
stress[0]
);
}
#[test]
fn orthotropic_from_isotropic_compliance_inverse_consistency() {
let mat = OrthotropicElastic::from_isotropic(200.0e9, 0.3);
let s = mat.compliance_matrix();
let expected_s11 = 1.0 / 200.0e9;
assert!(
(s[0] - expected_s11).abs() / expected_s11 < 1e-10,
"S11 = {}",
s[0]
);
}
#[test]
fn perzyna_no_overstress_below_yield() {
let mat = PerzynaViscoplasticity::new(200.0e9, 0.3, 250.0e6, 0.0, 1e-3, 1.0);
let stress = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0]; let phi = mat.overstress(stress, 0.0);
assert_eq!(phi, 0.0, "no overstress below yield: phi = {phi}");
}
#[test]
fn perzyna_overstress_above_yield() {
let mat = PerzynaViscoplasticity::new(200.0e9, 0.3, 250.0e6, 0.0, 1e-3, 1.0);
let stress = [400.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let phi = mat.overstress(stress, 0.0);
assert!(phi > 0.0, "overstress above yield: phi = {phi}");
}
#[test]
fn perzyna_viscoplastic_rate_zero_at_yield() {
let mat = PerzynaViscoplasticity::new(200.0e9, 0.3, 250.0e6, 0.0, 1e-3, 1.0);
let stress = [250.0e6, 0.0, 0.0, 0.0, 0.0, 0.0]; let rate = mat.viscoplastic_strain_rate(stress, 0.0);
assert!(rate.abs() < 1e-10, "no rate at yield surface: {rate}");
}
#[test]
fn perzyna_explicit_update_plastic_strain_increases() {
let mat = PerzynaViscoplasticity::new(200.0e9, 0.3, 250.0e6, 0.0, 1e-3, 1.0);
let strain = [2e-3, 0.0, 0.0, 0.0, 0.0, 0.0]; let (_, ep) = mat.explicit_stress_update(strain, 0.0, 0.001);
assert!(ep >= 0.0, "plastic strain must be non-negative: {ep}");
}
#[test]
fn norton_creep_zero_stress_zero_rate() {
let rate = norton_creep_rate([0.0; 6], 1e-15, 3.0);
assert_eq!(rate, 0.0, "zero stress → zero creep rate");
}
#[test]
fn norton_creep_rate_positive_for_nonzero_stress() {
let stress = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let rate = norton_creep_rate(stress, 1e-20, 3.0);
assert!(rate > 0.0 && rate.is_finite(), "creep rate = {rate}");
}
#[test]
fn norton_creep_increment_proportional_to_dt() {
let stress = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let de1 = norton_creep_increment(stress, 1e-20, 3.0, 1.0);
let de2 = norton_creep_increment(stress, 1e-20, 3.0, 2.0);
assert!(
(de2 / de1 - 2.0).abs() < 1e-10,
"increment ratio: {}",
de2 / de1
);
}
#[test]
fn model_descriptor_names_unique() {
let le = ModelDescriptor::linear_elastic();
let j2 = ModelDescriptor::j2_plasticity();
let te = ModelDescriptor::thermoelastic();
assert_ne!(le.name, j2.name);
assert_ne!(j2.name, te.name);
assert_ne!(le.name, te.name);
}
#[test]
fn model_descriptor_state_vars() {
let le = ModelDescriptor::linear_elastic();
let j2 = ModelDescriptor::j2_plasticity();
assert_eq!(le.n_state_vars, 0);
assert_eq!(j2.n_state_vars, 1);
}
#[test]
fn model_descriptor_ids() {
assert_eq!(
ModelDescriptor::linear_elastic().id,
ModelId::LinearElasticModel
);
assert_eq!(
ModelDescriptor::j2_plasticity().id,
ModelId::J2PlasticityModel
);
assert_eq!(
ModelDescriptor::thermoelastic().id,
ModelId::ThermoElasticModel
);
}
#[test]
fn strain_energy_positive_uniaxial() {
let e = 200.0e9;
let eps = 1e-3;
let sigma = e * eps;
let stress = [sigma, 0.0, 0.0, 0.0, 0.0, 0.0];
let strain = [eps, 0.0, 0.0, 0.0, 0.0, 0.0];
let w = strain_energy_density(stress, strain);
let expected = 0.5 * sigma * eps;
assert!(
(w - expected).abs() / expected < 1e-10,
"W = {w}, expected {expected}"
);
}
#[test]
fn strain_energy_zero_for_zero_strain() {
let w = strain_energy_density([0.0; 6], [0.0; 6]);
assert_eq!(w, 0.0);
}
#[test]
fn effective_modulus_linear_material() {
let e = effective_modulus_uniaxial(0.0, 0.0, 1e-3, 200.0e6);
assert!((e - 200.0e9).abs() / 200.0e9 < 1e-10, "E_eff = {e}");
}
#[test]
fn effective_modulus_infinity_for_zero_strain_diff() {
let e = effective_modulus_uniaxial(0.001, 100.0e6, 0.001, 200.0e6);
assert!(e.is_infinite(), "E_eff should be infinite: {e}");
}
#[test]
fn j2_invariant_uniaxial_tension() {
let sigma = 300.0e6;
let s = [sigma, 0.0, 0.0, 0.0, 0.0, 0.0];
let j2 = stress_j2_invariant(s);
let expected = sigma * sigma / 3.0;
assert!(
(j2 - expected).abs() / expected < 1e-10,
"J2 = {j2}, expected {expected}"
);
}
#[test]
fn j2_invariant_hydrostatic_zero() {
let s = [100.0e6, 100.0e6, 100.0e6, 0.0, 0.0, 0.0];
let j2 = stress_j2_invariant(s);
assert!(j2.abs() < 1.0, "hydrostatic: J2 = {j2}");
}
#[test]
fn lode_angle_hydrostatic_zero() {
let s = [100.0e6, 100.0e6, 100.0e6, 0.0, 0.0, 0.0];
let theta = lode_angle(s);
assert!(theta.abs() < 1e-10, "hydrostatic Lode angle = {theta}");
}
#[test]
fn lode_angle_in_valid_range() {
let s = [300.0e6, 100.0e6, 50.0e6, 20.0e6, 10.0e6, 5.0e6];
let theta = lode_angle(s);
let pi_over_3 = std::f64::consts::PI / 3.0;
assert!(
(0.0..=pi_over_3 + 1e-10).contains(&theta),
"Lode angle out of range: {theta}"
);
}
}