#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct IsotropicHardening {
pub sigma_y0: f64,
pub hardening_modulus: f64,
}
#[allow(dead_code)]
impl IsotropicHardening {
pub fn new(sigma_y0: f64, hardening_modulus: f64) -> Self {
Self {
sigma_y0,
hardening_modulus,
}
}
pub fn yield_stress(&self, eps_p: f64) -> f64 {
self.sigma_y0 + self.hardening_modulus * eps_p
}
pub fn d_yield_stress(&self) -> f64 {
self.hardening_modulus
}
pub fn elastic_perfectly_plastic(sigma_y0: f64) -> Self {
Self::new(sigma_y0, 0.0)
}
pub fn mild_steel() -> Self {
Self::new(250.0e6, 2.0e9)
}
pub fn aluminium_alloy() -> Self {
Self::new(270.0e6, 1.5e9)
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct VoceHardening {
pub sigma_y0: f64,
pub sigma_inf: f64,
pub b: f64,
}
#[allow(dead_code)]
impl VoceHardening {
pub fn new(sigma_y0: f64, sigma_inf: f64, b: f64) -> Self {
Self {
sigma_y0,
sigma_inf,
b,
}
}
pub fn yield_stress(&self, eps_p: f64) -> f64 {
self.sigma_inf - (self.sigma_inf - self.sigma_y0) * (-self.b * eps_p).exp()
}
pub fn d_yield_stress(&self, eps_p: f64) -> f64 {
self.b * (self.sigma_inf - self.sigma_y0) * (-self.b * eps_p).exp()
}
pub fn copper_like() -> Self {
Self::new(50.0e6, 300.0e6, 10.0)
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct PragerKinematic {
pub sigma_y0: f64,
pub c_kinematic: f64,
}
#[allow(dead_code)]
impl PragerKinematic {
pub fn new(sigma_y0: f64, c_kinematic: f64) -> Self {
Self {
sigma_y0,
c_kinematic,
}
}
pub fn backstress_increment(&self, delta_ep: &[f64; 6]) -> [f64; 6] {
let c = self.c_kinematic;
[
c * delta_ep[0],
c * delta_ep[1],
c * delta_ep[2],
c * delta_ep[3],
c * delta_ep[4],
c * delta_ep[5],
]
}
pub fn effective_stress(stress: &[f64; 6], backstress: &[f64; 6]) -> [f64; 6] {
[
stress[0] - backstress[0],
stress[1] - backstress[1],
stress[2] - backstress[2],
stress[3] - backstress[3],
stress[4] - backstress[4],
stress[5] - backstress[5],
]
}
pub fn von_mises_norm(s: &[f64; 6]) -> f64 {
let mean = (s[0] + s[1] + s[2]) / 3.0;
let dev = [s[0] - mean, s[1] - mean, s[2] - mean];
let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
+ s[3].powi(2)
+ s[4].powi(2)
+ s[5].powi(2);
(3.0 * j2).sqrt() }
pub fn yield_function(&self, stress: &[f64; 6], backstress: &[f64; 6]) -> f64 {
let s_eff = Self::effective_stress(stress, backstress);
Self::von_mises_norm(&s_eff) - self.sigma_y0
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct Chaboche {
pub sigma_y0: f64,
pub q: f64,
pub b_iso: f64,
pub c_kin: f64,
pub gamma_kin: f64,
}
#[allow(dead_code)]
impl Chaboche {
pub fn new(sigma_y0: f64, q: f64, b_iso: f64, c_kin: f64, gamma_kin: f64) -> Self {
Self {
sigma_y0,
q,
b_iso,
c_kin,
gamma_kin,
}
}
pub fn steel_default() -> Self {
Self::new(
250.0e6, 80.0e6, 15.0, 50.0e9, 500.0, )
}
pub fn isotropic_stress(&self, p: f64) -> f64 {
self.q * (1.0 - (-self.b_iso * p).exp())
}
pub fn yield_stress(&self, p: f64) -> f64 {
self.sigma_y0 + self.isotropic_stress(p)
}
pub fn backstress_increment(
&self,
flow_dir: &[f64; 6],
alpha: &[f64; 6],
delta_p: f64,
) -> [f64; 6] {
let mut da = [0.0f64; 6];
for i in 0..6 {
da[i] = (self.c_kin * flow_dir[i] - self.gamma_kin * alpha[i]) * delta_p;
}
da
}
pub fn backstress_norm(alpha: &[f64; 6]) -> f64 {
let sum = alpha[0].powi(2)
+ alpha[1].powi(2)
+ alpha[2].powi(2)
+ 2.0 * (alpha[3].powi(2) + alpha[4].powi(2) + alpha[5].powi(2));
(2.0 / 3.0 * sum).sqrt()
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct J2ReturnMapping {
pub shear_modulus: f64,
pub bulk_modulus: f64,
pub sigma_y0: f64,
pub hardening_modulus: f64,
}
#[allow(dead_code)]
impl J2ReturnMapping {
pub fn new(
shear_modulus: f64,
bulk_modulus: f64,
sigma_y0: f64,
hardening_modulus: f64,
) -> Self {
Self {
shear_modulus,
bulk_modulus,
sigma_y0,
hardening_modulus,
}
}
#[allow(non_snake_case)]
pub fn from_young_poisson(E: f64, nu: f64, sigma_y0: f64, h: f64) -> Self {
let g = E / (2.0 * (1.0 + nu));
let k = E / (3.0 * (1.0 - 2.0 * nu));
Self::new(g, k, sigma_y0, h)
}
pub fn von_mises(stress: &[f64; 6]) -> f64 {
let mean = (stress[0] + stress[1] + stress[2]) / 3.0;
let dev = [stress[0] - mean, stress[1] - mean, stress[2] - mean];
let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
+ stress[3].powi(2)
+ stress[4].powi(2)
+ stress[5].powi(2);
(3.0 * j2).sqrt()
}
pub fn return_map(&self, trial_stress: &[f64; 6], eps_p_bar: f64) -> ([f64; 6], [f64; 6], f64) {
let sigma_y = self.sigma_y0 + self.hardening_modulus * eps_p_bar;
let vm = Self::von_mises(trial_stress);
if vm <= sigma_y + f64::EPSILON * sigma_y {
return (*trial_stress, [0.0; 6], 0.0);
}
let delta_gamma = (vm - sigma_y) / (3.0 * self.shear_modulus + self.hardening_modulus);
let p = (trial_stress[0] + trial_stress[1] + trial_stress[2]) / 3.0;
let dev_trial = [
trial_stress[0] - p,
trial_stress[1] - p,
trial_stress[2] - p,
trial_stress[3],
trial_stress[4],
trial_stress[5],
];
let scale = 1.0 - 3.0 * self.shear_modulus * delta_gamma / vm;
let updated_stress = [
p + dev_trial[0] * scale,
p + dev_trial[1] * scale,
p + dev_trial[2] * scale,
dev_trial[3] * scale,
dev_trial[4] * scale,
dev_trial[5] * scale,
];
let n_hat = [
dev_trial[0] / vm * 3.0 / 2.0,
dev_trial[1] / vm * 3.0 / 2.0,
dev_trial[2] / vm * 3.0 / 2.0,
dev_trial[3] / vm * 3.0 / 2.0,
dev_trial[4] / vm * 3.0 / 2.0,
dev_trial[5] / vm * 3.0 / 2.0,
];
let delta_eps_p = [
delta_gamma * n_hat[0],
delta_gamma * n_hat[1],
delta_gamma * n_hat[2],
delta_gamma * n_hat[3],
delta_gamma * n_hat[4],
delta_gamma * n_hat[5],
];
(updated_stress, delta_eps_p, delta_gamma)
}
pub fn consistent_tangent_uniaxial(&self) -> f64 {
let e = 9.0 * self.bulk_modulus * self.shear_modulus
/ (3.0 * self.bulk_modulus + self.shear_modulus);
e * self.hardening_modulus / (e + self.hardening_modulus)
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct DruckerPragerModel {
pub friction_angle: f64,
pub cohesion: f64,
pub shear_modulus: f64,
pub bulk_modulus: f64,
}
#[allow(dead_code)]
impl DruckerPragerModel {
pub fn new(friction_angle: f64, cohesion: f64, shear_modulus: f64, bulk_modulus: f64) -> Self {
Self {
friction_angle,
cohesion,
shear_modulus,
bulk_modulus,
}
}
#[allow(non_snake_case, clippy::too_many_arguments)]
pub fn from_mohr_coulomb_degrees(phi_deg: f64, cohesion: f64, E: f64, nu: f64) -> Self {
let phi = phi_deg.to_radians();
let g = E / (2.0 * (1.0 + nu));
let k = E / (3.0 * (1.0 - 2.0 * nu));
Self::new(phi, cohesion, g, k)
}
pub fn alpha_dp(&self) -> f64 {
let sin_phi = self.friction_angle.sin();
2.0 * sin_phi / (3.0_f64.sqrt() * (3.0 - sin_phi))
}
pub fn k_dp(&self) -> f64 {
let sin_phi = self.friction_angle.sin();
let cos_phi = self.friction_angle.cos();
6.0 * self.cohesion * cos_phi / (3.0_f64.sqrt() * (3.0 - sin_phi))
}
fn j2(stress: &[f64; 6]) -> f64 {
let mean = (stress[0] + stress[1] + stress[2]) / 3.0;
let d = [stress[0] - mean, stress[1] - mean, stress[2] - mean];
0.5 * (d[0].powi(2) + d[1].powi(2) + d[2].powi(2))
+ stress[3].powi(2)
+ stress[4].powi(2)
+ stress[5].powi(2)
}
pub fn yield_function(&self, stress: &[f64; 6]) -> f64 {
let i1 = stress[0] + stress[1] + stress[2];
let sqrt_j2 = Self::j2(stress).sqrt();
sqrt_j2 + self.alpha_dp() * i1 - self.k_dp()
}
pub fn is_elastic(&self, stress: &[f64; 6]) -> bool {
self.yield_function(stress) <= 0.0
}
pub fn uniaxial_tensile_strength(&self) -> f64 {
let sin_phi = self.friction_angle.sin();
let cos_phi = self.friction_angle.cos();
2.0 * self.cohesion * cos_phi / (1.0 + sin_phi)
}
pub fn uniaxial_compressive_strength(&self) -> f64 {
let sin_phi = self.friction_angle.sin();
let cos_phi = self.friction_angle.cos();
2.0 * self.cohesion * cos_phi / (1.0 - sin_phi)
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct ModifiedCamClay {
pub m: f64,
pub kappa: f64,
pub lambda: f64,
pub e0: f64,
pub poisson_ratio: f64,
pub pc0: f64,
}
#[allow(dead_code)]
impl ModifiedCamClay {
#[allow(clippy::too_many_arguments)]
pub fn new(m: f64, kappa: f64, lambda: f64, e0: f64, poisson_ratio: f64, pc0: f64) -> Self {
Self {
m,
kappa,
lambda,
e0,
poisson_ratio,
pc0,
}
}
pub fn soft_clay() -> Self {
Self::new(0.9, 0.05, 0.2, 1.5, 0.3, 100.0e3)
}
pub fn bulk_modulus(&self, p_prime: f64) -> f64 {
(1.0 + self.e0) * p_prime / self.kappa
}
pub fn shear_modulus(&self, p_prime: f64) -> f64 {
let k = self.bulk_modulus(p_prime);
3.0 * k * (1.0 - 2.0 * self.poisson_ratio) / (2.0 * (1.0 + self.poisson_ratio))
}
pub fn mean_stress(stress: &[f64; 6]) -> f64 {
(stress[0] + stress[1] + stress[2]) / 3.0
}
pub fn deviatoric_stress(stress: &[f64; 6]) -> f64 {
let p = Self::mean_stress(stress);
let dev = [stress[0] - p, stress[1] - p, stress[2] - p];
let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
+ stress[3].powi(2)
+ stress[4].powi(2)
+ stress[5].powi(2);
(3.0 * j2).sqrt()
}
pub fn yield_function(&self, stress: &[f64; 6], pc: f64) -> f64 {
let p = Self::mean_stress(stress);
let q = Self::deviatoric_stress(stress);
q * q / (self.m * self.m) + p * (p - pc)
}
pub fn critical_state_pressure(&self, pc: f64) -> f64 {
pc / 2.0
}
pub fn update_preconsolidation_pressure(&self, pc: f64, delta_eps_v_p: f64) -> f64 {
pc * ((1.0 + self.e0) * delta_eps_v_p / (self.lambda - self.kappa)).exp()
}
pub fn compression_index(&self) -> f64 {
self.lambda * 10.0_f64.ln()
}
}
#[allow(dead_code)]
pub fn plastic_dissipation(stress: &[f64; 6], delta_eps_p: &[f64; 6]) -> f64 {
let normal =
stress[0] * delta_eps_p[0] + stress[1] * delta_eps_p[1] + stress[2] * delta_eps_p[2];
let shear = 2.0
* (stress[3] * delta_eps_p[3] + stress[4] * delta_eps_p[4] + stress[5] * delta_eps_p[5]);
normal + shear
}
#[allow(dead_code)]
pub fn cumulative_dissipation(
stresses: &[[f64; 6]],
plastic_strain_increments: &[[f64; 6]],
) -> f64 {
stresses
.iter()
.zip(plastic_strain_increments.iter())
.map(|(s, dep)| plastic_dissipation(s, dep))
.sum()
}
#[allow(dead_code)]
pub fn limit_load_factor_lower_bound(
elastic_stresses_voigt: &[[f64; 6]],
yield_stress: f64,
) -> f64 {
if elastic_stresses_voigt.is_empty() || yield_stress <= 0.0 {
return 0.0;
}
let max_vm = elastic_stresses_voigt
.iter()
.map(von_mises_stress_voigt)
.fold(0.0_f64, f64::max);
if max_vm < f64::EPSILON {
return f64::INFINITY;
}
yield_stress / max_vm
}
#[allow(dead_code)]
pub fn von_mises_stress_voigt(s: &[f64; 6]) -> f64 {
let (sxx, syy, szz, sxy, syz, sxz) = (s[0], s[1], s[2], s[3], s[4], s[5]);
let term1 = (sxx - syy).powi(2) + (syy - szz).powi(2) + (szz - sxx).powi(2);
let term2 = 6.0 * (sxy.powi(2) + syz.powi(2) + sxz.powi(2));
((term1 + term2) / 2.0).sqrt()
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct RambergOsgood {
pub young_modulus: f64,
pub sigma_ref: f64,
pub n: f64,
pub alpha: f64,
}
#[allow(dead_code)]
impl RambergOsgood {
pub fn new(young_modulus: f64, sigma_ref: f64, n: f64, alpha: f64) -> Self {
Self {
young_modulus,
sigma_ref,
n,
alpha,
}
}
pub fn total_strain(&self, sigma: f64) -> f64 {
sigma / self.young_modulus
+ self.alpha * (sigma / self.sigma_ref).powf(self.n) * self.sigma_ref
/ self.young_modulus
}
pub fn plastic_strain(&self, sigma: f64) -> f64 {
self.total_strain(sigma) - sigma / self.young_modulus
}
pub fn secant_modulus(&self, sigma: f64) -> f64 {
let eps = self.total_strain(sigma);
if eps.abs() < f64::EPSILON {
return self.young_modulus;
}
sigma / eps
}
pub fn tangent_modulus(&self, sigma: f64) -> f64 {
let d_eps_d_sigma = 1.0 / self.young_modulus
+ self.alpha * self.n / self.sigma_ref * (sigma / self.sigma_ref).powf(self.n - 1.0);
if d_eps_d_sigma.abs() < f64::EPSILON {
return self.young_modulus;
}
1.0 / d_eps_d_sigma
}
}
#[allow(dead_code)]
pub struct J2ConsistentTangent {
pub shear_modulus: f64,
pub bulk_modulus: f64,
pub hardening_modulus: f64,
}
#[allow(dead_code)]
impl J2ConsistentTangent {
pub fn new(shear_modulus: f64, bulk_modulus: f64, hardening_modulus: f64) -> Self {
Self {
shear_modulus,
bulk_modulus,
hardening_modulus,
}
}
#[allow(non_snake_case)]
pub fn from_young_poisson(E: f64, nu: f64, hardening_modulus: f64) -> Self {
let g = E / (2.0 * (1.0 + nu));
let k = E / (3.0 * (1.0 - 2.0 * nu));
Self::new(g, k, hardening_modulus)
}
pub fn elastic_stiffness(&self) -> [f64; 36] {
let g = self.shear_modulus;
let k = self.bulk_modulus;
let lam = k - 2.0 / 3.0 * g; let mut c = [0.0_f64; 36];
for i in 0..3 {
for j in 0..3 {
c[i * 6 + j] = lam;
}
c[i * 6 + i] += 2.0 * g;
}
c[3 * 6 + 3] = g;
c[4 * 6 + 4] = g;
c[5 * 6 + 5] = g;
c
}
pub fn compute_consistent_tangent(
&self,
trial_stress: &[f64; 6],
delta_gamma: f64,
) -> [f64; 36] {
let g = self.shear_modulus;
let h = self.hardening_modulus;
let p = (trial_stress[0] + trial_stress[1] + trial_stress[2]) / 3.0;
let dev = [
trial_stress[0] - p,
trial_stress[1] - p,
trial_stress[2] - p,
trial_stress[3],
trial_stress[4],
trial_stress[5],
];
let q_tr = {
let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
+ dev[3].powi(2)
+ dev[4].powi(2)
+ dev[5].powi(2);
(3.0 * j2).sqrt()
};
if delta_gamma < f64::EPSILON || q_tr < f64::EPSILON {
return self.elastic_stiffness();
}
let scale = if q_tr > 1e-30 { 1.0 / q_tr } else { 0.0 };
let n: [f64; 6] = [
dev[0] * scale,
dev[1] * scale,
dev[2] * scale,
dev[3] * scale,
dev[4] * scale,
dev[5] * scale,
];
let mut c = self.elastic_stiffness();
let coeff1 = 6.0 * g * g / (3.0 * g + h);
for i in 0..6 {
for j in 0..6 {
c[i * 6 + j] -= coeff1 * n[i] * n[j];
}
}
let coeff2 = 2.0 * g * delta_gamma / q_tr;
let i_dev_diag = [2.0 / 3.0, 2.0 / 3.0, 2.0 / 3.0, 1.0, 1.0, 1.0];
for i in 0..3 {
for j in 0..3 {
let i_dev_ij = if i == j { i_dev_diag[i] } else { -1.0 / 3.0 };
c[i * 6 + j] -= coeff2 * (i_dev_ij - n[i] * n[j]);
}
}
for k in 3..6 {
c[k * 6 + k] -= coeff2 * (i_dev_diag[k] - n[k] * n[k]);
}
c
}
}
#[allow(dead_code)]
pub struct DruckerPragerApexReturn {
pub alpha: f64,
pub k_dp: f64,
pub bulk_modulus: f64,
pub shear_modulus: f64,
}
#[allow(dead_code)]
impl DruckerPragerApexReturn {
pub fn new(alpha: f64, k_dp: f64, bulk_modulus: f64, shear_modulus: f64) -> Self {
Self {
alpha,
k_dp,
bulk_modulus,
shear_modulus,
}
}
pub fn from_dp_model(dp: &DruckerPragerModel) -> Self {
Self::new(dp.alpha_dp(), dp.k_dp(), dp.bulk_modulus, dp.shear_modulus)
}
pub fn apex_pressure(&self) -> f64 {
if self.alpha.abs() < f64::EPSILON {
return 0.0;
}
self.k_dp / (3.0 * self.alpha)
}
pub fn needs_apex_return(&self, trial_stress: &[f64; 6]) -> bool {
let p = (trial_stress[0] + trial_stress[1] + trial_stress[2]) / 3.0;
p < self.apex_pressure()
}
pub fn compute_apex_return(&self, _trial_stress: &[f64; 6]) -> [f64; 6] {
let p_apex = self.apex_pressure();
[p_apex, p_apex, p_apex, 0.0, 0.0, 0.0]
}
pub fn compute_apex_multiplier(&self, trial_stress: &[f64; 6]) -> f64 {
let i1 = trial_stress[0] + trial_stress[1] + trial_stress[2];
let p = i1 / 3.0;
let dev = [
trial_stress[0] - p,
trial_stress[1] - p,
trial_stress[2] - p,
];
let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
+ trial_stress[3].powi(2)
+ trial_stress[4].powi(2)
+ trial_stress[5].powi(2);
let sqrt_j2 = j2.sqrt();
let f_tr = sqrt_j2 + self.alpha * i1 - self.k_dp;
let denom = 3.0 * self.alpha * self.bulk_modulus + self.shear_modulus;
if denom.abs() < f64::EPSILON {
return 0.0;
}
f_tr / denom
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct CamClayPreconsolidation {
pub cc: f64,
pub cs: f64,
pub e0: f64,
pub pc0: f64,
}
#[allow(dead_code)]
impl CamClayPreconsolidation {
pub fn new(cc: f64, cs: f64, e0: f64, pc0: f64) -> Self {
Self { cc, cs, e0, pc0 }
}
pub fn soft_clay() -> Self {
Self::new(0.5, 0.05, 1.2, 100.0e3)
}
pub fn lambda(&self) -> f64 {
self.cc / 10.0_f64.ln()
}
pub fn kappa(&self) -> f64 {
self.cs / 10.0_f64.ln()
}
pub fn compute_preconsolidation(&self, pc_old: f64, delta_eps_v_p: f64) -> f64 {
let lambda = self.lambda();
let kappa = self.kappa();
if (lambda - kappa).abs() < f64::EPSILON {
return pc_old;
}
pc_old * ((1.0 + self.e0) * delta_eps_v_p / (lambda - kappa)).exp()
}
pub fn void_ratio_ncl(&self, p: f64) -> f64 {
self.e0 - self.cc * (p / self.pc0).log10()
}
pub fn void_ratio_swelling(&self, p: f64, p0: f64, e_curr: f64) -> f64 {
e_curr - self.cs * (p / p0).log10()
}
pub fn ocr(&self, p_prime: f64, pc: f64) -> f64 {
if p_prime.abs() < f64::EPSILON {
return 1.0;
}
pc / p_prime
}
pub fn critical_state_pressure(&self, pc: f64) -> f64 {
pc / 2.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_isotropic_hardening_at_zero_strain() {
let h = IsotropicHardening::mild_steel();
let sy = h.yield_stress(0.0);
assert!((sy - 250.0e6).abs() < 1.0, "σ_y(0) = {sy}");
}
#[test]
fn test_isotropic_hardening_increases_with_strain() {
let h = IsotropicHardening::mild_steel();
let sy0 = h.yield_stress(0.0);
let sy1 = h.yield_stress(0.01);
assert!(
sy1 > sy0,
"yield stress should increase with plastic strain"
);
}
#[test]
fn test_isotropic_hardening_linear_rate() {
let h = IsotropicHardening::new(200.0e6, 1.0e9);
let eps_p = 0.005;
let sy = h.yield_stress(eps_p);
let expected = 200.0e6 + 1.0e9 * eps_p;
assert!(
(sy - expected).abs() < 1.0,
"linear hardening: {sy} vs {expected}"
);
}
#[test]
fn test_isotropic_hardening_derivative_is_h() {
let h = IsotropicHardening::new(200.0e6, 5.0e9);
assert!((h.d_yield_stress() - 5.0e9).abs() < 1.0);
}
#[test]
fn test_elastic_perfectly_plastic_constant_yield() {
let h = IsotropicHardening::elastic_perfectly_plastic(300.0e6);
assert!((h.yield_stress(0.0) - h.yield_stress(1.0)).abs() < 1.0);
}
#[test]
fn test_voce_at_zero_strain() {
let v = VoceHardening::copper_like();
let sy = v.yield_stress(0.0);
assert!((sy - 50.0e6).abs() < 1.0, "Voce σ_y(0) = {sy}");
}
#[test]
fn test_voce_saturates_at_large_strain() {
let v = VoceHardening::copper_like();
let sy = v.yield_stress(100.0);
assert!(
(sy - v.sigma_inf).abs() / v.sigma_inf < 0.001,
"Voce should saturate: {sy}"
);
}
#[test]
fn test_voce_derivative_positive_at_small_strain() {
let v = VoceHardening::new(100.0e6, 400.0e6, 5.0);
assert!(v.d_yield_stress(0.01) > 0.0, "dσ_y/dε_p should be positive");
}
#[test]
fn test_voce_derivative_decreases_with_strain() {
let v = VoceHardening::new(100.0e6, 400.0e6, 5.0);
let d1 = v.d_yield_stress(0.0);
let d2 = v.d_yield_stress(0.1);
assert!(d2 < d1, "hardening rate should decrease with strain");
}
#[test]
fn test_prager_backstress_increment_direction() {
let pk = PragerKinematic::new(250.0e6, 10.0e9);
let dep = [0.001, -0.0005, -0.0005, 0.0, 0.0, 0.0];
let da = pk.backstress_increment(&dep);
assert!((da[0] - 10.0e9 * 0.001).abs() < 1.0, "Δα_xx");
}
#[test]
fn test_prager_von_mises_norm_uniaxial() {
let s = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let vm = PragerKinematic::von_mises_norm(&s);
assert!((vm - 300.0e6).abs() < 1.0, "VM norm for uniaxial: {vm}");
}
#[test]
fn test_prager_yield_function_below_yield() {
let pk = PragerKinematic::new(300.0e6, 10.0e9);
let stress = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let backstress = [0.0; 6];
let f = pk.yield_function(&stress, &backstress);
assert!(f < 0.0, "Below yield: f = {f}");
}
#[test]
fn test_prager_effective_stress_with_backstress() {
let stress = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let backstress = [50.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let s_eff = PragerKinematic::effective_stress(&stress, &backstress);
assert!((s_eff[0] - 250.0e6).abs() < 1.0, "σ_eff_xx = {}", s_eff[0]);
}
#[test]
fn test_chaboche_isotropic_stress_zero_at_zero_strain() {
let c = Chaboche::steel_default();
assert!(c.isotropic_stress(0.0).abs() < 1.0);
}
#[test]
fn test_chaboche_isotropic_stress_saturates() {
let c = Chaboche::steel_default();
let r_large = c.isotropic_stress(1000.0);
assert!(
(r_large - c.q).abs() / c.q < 0.001,
"Chaboche R should saturate: {r_large}"
);
}
#[test]
fn test_chaboche_yield_stress_increases_with_p() {
let c = Chaboche::steel_default();
let sy0 = c.yield_stress(0.0);
let sy1 = c.yield_stress(0.01);
assert!(sy1 > sy0, "yield stress should increase with p");
}
#[test]
fn test_chaboche_backstress_norm_positive() {
let alpha = [50.0e6, -20.0e6, -30.0e6, 5.0e6, 0.0, 0.0];
let norm = Chaboche::backstress_norm(&alpha);
assert!(norm > 0.0, "backstress norm should be positive: {norm}");
}
#[test]
fn test_chaboche_backstress_increment_sign() {
let c = Chaboche::steel_default();
let flow_dir = [1.0, -0.5, -0.5, 0.0, 0.0, 0.0];
let alpha = [0.0; 6];
let da = c.backstress_increment(&flow_dir, &alpha, 0.001);
assert!(
da[0] > 0.0,
"backstress increment xx should be positive: {}",
da[0]
);
}
#[test]
fn test_j2_return_map_elastic() {
let rm = J2ReturnMapping::from_young_poisson(200.0e9, 0.3, 250.0e6, 2.0e9);
let trial = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let (stress, _dep, delta_gamma) = rm.return_map(&trial, 0.0);
assert!(
delta_gamma < f64::EPSILON,
"Should be elastic: Δγ={delta_gamma}"
);
assert!(
(stress[0] - 100.0e6).abs() < 1.0,
"stress unchanged: {}",
stress[0]
);
}
#[test]
fn test_j2_return_map_plastic() {
let rm = J2ReturnMapping::from_young_poisson(200.0e9, 0.3, 250.0e6, 2.0e9);
let trial = [500.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let (stress, _dep, delta_gamma) = rm.return_map(&trial, 0.0);
assert!(delta_gamma > 0.0, "Should yield: Δγ={delta_gamma}");
let vm = J2ReturnMapping::von_mises(&stress);
let sy_updated = 250.0e6 + 2.0e9 * delta_gamma;
assert!(
(vm - sy_updated).abs() / sy_updated < 0.001,
"VM after return: {vm} vs σ_y: {sy_updated}"
);
}
#[test]
fn test_j2_von_mises_uniaxial() {
let s = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let vm = J2ReturnMapping::von_mises(&s);
assert!((vm - 300.0e6).abs() < 1.0, "VM uniaxial: {vm}");
}
#[test]
fn test_j2_consistent_tangent_less_than_elastic() {
let rm = J2ReturnMapping::from_young_poisson(200.0e9, 0.3, 250.0e6, 2.0e9);
let c_ep = rm.consistent_tangent_uniaxial();
let e =
9.0 * rm.bulk_modulus * rm.shear_modulus / (3.0 * rm.bulk_modulus + rm.shear_modulus);
assert!(c_ep < e, "C_ep should be < E");
assert!(c_ep > 0.0, "C_ep should be positive");
}
#[test]
fn test_drucker_prager_cohesion_only_yield_stress() {
let dp = DruckerPragerModel::from_mohr_coulomb_degrees(0.0, 1.0e6, 30.0e9, 0.3);
let k = dp.k_dp();
let expected = 2.0 * 1.0e6 / 3.0_f64.sqrt();
assert!(
(k - expected).abs() / expected < 0.01,
"k_dp = {k} vs {expected}"
);
}
#[test]
fn test_drucker_prager_hydrostatic_elastic() {
let dp = DruckerPragerModel::from_mohr_coulomb_degrees(30.0, 1.0e6, 30.0e9, 0.3);
let stress = [-10.0e6, -10.0e6, -10.0e6, 0.0, 0.0, 0.0];
assert!(
dp.is_elastic(&stress),
"Deep hydrostatic compression should be elastic"
);
}
#[test]
fn test_drucker_prager_tensile_strength_positive() {
let dp = DruckerPragerModel::from_mohr_coulomb_degrees(30.0, 500.0e3, 10.0e9, 0.3);
let st = dp.uniaxial_tensile_strength();
assert!(st > 0.0, "tensile strength should be positive: {st}");
}
#[test]
fn test_drucker_prager_compressive_strength_gt_tensile() {
let dp = DruckerPragerModel::from_mohr_coulomb_degrees(30.0, 500.0e3, 10.0e9, 0.3);
let st = dp.uniaxial_tensile_strength();
let sc = dp.uniaxial_compressive_strength();
assert!(sc > st, "compressive > tensile for φ>0: sc={sc}, st={st}");
}
#[test]
fn test_mcc_bulk_modulus_increases_with_pressure() {
let mcc = ModifiedCamClay::soft_clay();
let k1 = mcc.bulk_modulus(50.0e3);
let k2 = mcc.bulk_modulus(100.0e3);
assert!(k2 > k1, "K should increase with p'");
}
#[test]
fn test_mcc_yield_function_on_yield_surface() {
let mcc = ModifiedCamClay::soft_clay();
let pc = mcc.pc0;
let stress_apex = [pc, pc, pc, 0.0, 0.0, 0.0];
let f = mcc.yield_function(&stress_apex, pc);
assert!(
f.abs() < 1.0,
"MCC yield f at right apex should be ~0, got {f}"
);
}
#[test]
fn test_mcc_hardening_increases_pc_under_compression() {
let mcc = ModifiedCamClay::soft_clay();
let pc_new = mcc.update_preconsolidation_pressure(mcc.pc0, 0.01);
assert!(
pc_new > mcc.pc0,
"pc should increase under plastic volumetric compression"
);
}
#[test]
fn test_mcc_compression_index_positive() {
let mcc = ModifiedCamClay::soft_clay();
assert!(mcc.compression_index() > 0.0);
}
#[test]
fn test_mcc_critical_state_pressure_half_pc() {
let mcc = ModifiedCamClay::soft_clay();
let p_cs = mcc.critical_state_pressure(mcc.pc0);
assert!((p_cs - mcc.pc0 / 2.0).abs() < 1e-6);
}
#[test]
fn test_plastic_dissipation_positive() {
let stress = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let dep = [0.001, -0.0005, -0.0005, 0.0, 0.0, 0.0];
let d = plastic_dissipation(&stress, &dep);
assert!(d > 0.0, "plastic dissipation should be positive: {d}");
}
#[test]
fn test_plastic_dissipation_zero_for_zero_strain() {
let stress = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let dep = [0.0; 6];
let d = plastic_dissipation(&stress, &dep);
assert!(d.abs() < 1e-10, "zero strain → zero dissipation: {d}");
}
#[test]
fn test_cumulative_dissipation() {
let stresses = vec![
[300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64],
[310.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64],
];
let deps = vec![
[0.001, -0.0005, -0.0005, 0.0, 0.0, 0.0_f64],
[0.001, -0.0005, -0.0005, 0.0, 0.0, 0.0_f64],
];
let d_total = cumulative_dissipation(&stresses, &deps);
assert!(d_total > 0.0, "cumulative dissipation should be positive");
}
#[test]
fn test_limit_load_factor_below_yield() {
let stresses = vec![[100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64]];
let f = limit_load_factor_lower_bound(&stresses, 250.0e6);
assert!((f - 2.5).abs() < 0.01, "limit load factor: {f}");
}
#[test]
fn test_limit_load_factor_multiple_elements() {
let stresses = vec![
[100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64],
[200.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64], ];
let f = limit_load_factor_lower_bound(&stresses, 250.0e6);
assert!((f - 1.25).abs() < 0.01, "limit load factor: {f}");
}
#[test]
fn test_limit_load_empty_returns_zero() {
let f = limit_load_factor_lower_bound(&[], 250.0e6);
assert_eq!(f, 0.0);
}
#[test]
fn test_von_mises_voigt_uniaxial() {
let s = [200.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
let vm = von_mises_stress_voigt(&s);
assert!((vm - 200.0e6).abs() < 1.0, "VM uniaxial: {vm}");
}
#[test]
fn test_von_mises_voigt_hydrostatic_zero() {
let s = [100.0e6, 100.0e6, 100.0e6, 0.0, 0.0, 0.0];
let vm = von_mises_stress_voigt(&s);
assert!(vm.abs() < 1.0, "VM hydrostatic should be ~0: {vm}");
}
#[test]
fn test_ramberg_osgood_elastic_at_small_stress() {
let ro = RambergOsgood::new(200.0e9, 400.0e6, 5.0, 3.0 / 7.0);
let sigma = 1.0e6; let eps = ro.total_strain(sigma);
let eps_elastic = sigma / ro.young_modulus;
assert!(
(eps - eps_elastic).abs() / eps_elastic < 0.01,
"small stress should be nearly elastic"
);
}
#[test]
fn test_ramberg_osgood_plastic_strain_zero_at_zero_stress() {
let ro = RambergOsgood::new(200.0e9, 400.0e6, 5.0, 3.0 / 7.0);
assert!(ro.plastic_strain(0.0).abs() < f64::EPSILON);
}
#[test]
fn test_ramberg_osgood_tangent_modulus_less_than_elastic() {
let ro = RambergOsgood::new(200.0e9, 400.0e6, 5.0, 3.0 / 7.0);
let et = ro.tangent_modulus(ro.sigma_ref);
assert!(
et < ro.young_modulus,
"tangent should be < E at reference stress"
);
assert!(et > 0.0, "tangent should be positive");
}
#[test]
fn test_ramberg_osgood_secant_less_than_elastic() {
let ro = RambergOsgood::new(200.0e9, 400.0e6, 5.0, 3.0 / 7.0);
let es = ro.secant_modulus(ro.sigma_ref);
assert!(
es < ro.young_modulus,
"secant modulus at ref stress should be < E"
);
}
#[test]
fn test_j2_consistent_tangent_elastic_step() {
let ct = J2ConsistentTangent::from_young_poisson(200.0e9, 0.3, 2.0e9);
let trial = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64];
let c = ct.compute_consistent_tangent(&trial, 0.0);
let ce = ct.elastic_stiffness();
for i in 0..36 {
assert!(
(c[i] - ce[i]).abs() < 1.0,
"Elastic tangent mismatch at {i}: {} vs {}",
c[i],
ce[i]
);
}
}
#[test]
fn test_j2_consistent_tangent_c11() {
let e = 200.0e9_f64;
let nu = 0.3_f64;
let ct = J2ConsistentTangent::from_young_poisson(e, nu, 2.0e9);
let c = ct.elastic_stiffness();
let k = e / (3.0 * (1.0 - 2.0 * nu));
let g = e / (2.0 * (1.0 + nu));
let c11 = k + 4.0 / 3.0 * g;
assert!((c[0] - c11).abs() / c11 < 1e-8, "C11: {} vs {}", c[0], c11);
}
#[test]
fn test_j2_consistent_tangent_c12() {
let e = 200.0e9_f64;
let nu = 0.3_f64;
let ct = J2ConsistentTangent::from_young_poisson(e, nu, 2.0e9);
let c = ct.elastic_stiffness();
let k = e / (3.0 * (1.0 - 2.0 * nu));
let g = e / (2.0 * (1.0 + nu));
let c12 = k - 2.0 / 3.0 * g;
assert!(
(c[1] - c12).abs() / c12.abs() < 1e-8,
"C12: {} vs {}",
c[1],
c12
);
}
#[test]
fn test_j2_consistent_tangent_plastic_step_differs() {
let ct = J2ConsistentTangent::from_young_poisson(200.0e9, 0.3, 2.0e9);
let trial = [500.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64];
let delta_gamma = 1.0e-4;
let c_ep = ct.compute_consistent_tangent(&trial, delta_gamma);
let c_e = ct.elastic_stiffness();
let diff: f64 = c_ep
.iter()
.zip(c_e.iter())
.map(|(a, b)| (a - b).abs())
.sum();
assert!(
diff > 1.0,
"Plastic tangent should differ from elastic: total diff = {diff}"
);
}
#[test]
fn test_dp_apex_pressure() {
let phi = 30.0_f64.to_radians();
let cohesion = 50.0e3_f64;
let e = 50.0e6_f64;
let nu = 0.3_f64;
let dp = DruckerPragerModel::new(
phi,
cohesion,
e / (2.0 * (1.0 + nu)),
e / (3.0 * (1.0 - 2.0 * nu)),
);
let apr = DruckerPragerApexReturn::from_dp_model(&dp);
let p_apex = apr.apex_pressure();
let expected = dp.k_dp() / (3.0 * dp.alpha_dp());
assert!(
(p_apex - expected).abs() / expected < 1e-10,
"Apex pressure: {} vs {}",
p_apex,
expected
);
}
#[test]
fn test_dp_apex_return_zero_deviatoric() {
let apr = DruckerPragerApexReturn::new(0.2, 100.0e3, 30.0e6, 15.0e6);
let trial = [-200.0e3, -100.0e3, -50.0e3, 10.0e3, 0.0, 0.0];
let s_apex = apr.compute_apex_return(&trial);
assert!(
(s_apex[0] - s_apex[1]).abs() < f64::EPSILON,
"Apex return should be hydrostatic"
);
assert!(
(s_apex[1] - s_apex[2]).abs() < f64::EPSILON,
"Apex return should be hydrostatic"
);
assert_eq!(s_apex[3], 0.0);
}
#[test]
fn test_cam_clay_preconsolidation_increases_under_compression() {
let cc = CamClayPreconsolidation::soft_clay();
let pc_new = cc.compute_preconsolidation(cc.pc0, 0.01);
assert!(
pc_new > cc.pc0,
"Preconsolidation should increase under compression"
);
}
#[test]
fn test_cam_clay_preconsolidation_zero_strain_unchanged() {
let cc = CamClayPreconsolidation::soft_clay();
let pc_new = cc.compute_preconsolidation(cc.pc0, 0.0);
assert!(
(pc_new - cc.pc0).abs() / cc.pc0 < 1e-12,
"Zero plastic strain: pc should not change: {} vs {}",
pc_new,
cc.pc0
);
}
#[test]
fn test_cam_clay_ocr_normally_consolidated() {
let cc = CamClayPreconsolidation::soft_clay();
let ocr = cc.ocr(cc.pc0, cc.pc0);
assert!(
(ocr - 1.0).abs() < f64::EPSILON,
"OCR should be 1 at p' = pc: {ocr}"
);
}
#[test]
fn test_cam_clay_critical_state_pressure() {
let cc = CamClayPreconsolidation::soft_clay();
let pcs = cc.critical_state_pressure(cc.pc0);
assert!(
(pcs - cc.pc0 / 2.0).abs() < f64::EPSILON,
"p'_cs should be pc/2: {pcs}"
);
}
}