#[derive(Debug, Clone, PartialEq)]
pub struct StressState {
pub sigma_x: f64,
pub sigma_y: f64,
pub sigma_z: f64,
pub tau_xy: f64,
pub tau_yz: f64,
pub tau_xz: f64,
}
impl StressState {
pub fn new_2d(sigma_x: f64, sigma_y: f64, tau_xy: f64) -> Self {
Self {
sigma_x,
sigma_y,
sigma_z: 0.0,
tau_xy,
tau_yz: 0.0,
tau_xz: 0.0,
}
}
pub fn new_uniaxial(sigma: f64) -> Self {
Self {
sigma_x: sigma,
sigma_y: 0.0,
sigma_z: 0.0,
tau_xy: 0.0,
tau_yz: 0.0,
tau_xz: 0.0,
}
}
pub fn new_biaxial(sigma_x: f64, sigma_y: f64) -> Self {
Self {
sigma_x,
sigma_y,
sigma_z: 0.0,
tau_xy: 0.0,
tau_yz: 0.0,
tau_xz: 0.0,
}
}
pub fn new_hydrostatic(p: f64) -> Self {
Self {
sigma_x: p,
sigma_y: p,
sigma_z: p,
tau_xy: 0.0,
tau_yz: 0.0,
tau_xz: 0.0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PrincipalStresses {
pub sigma_1: f64,
pub sigma_2: f64,
pub sigma_3: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StressAnalysisResult {
pub principal: PrincipalStresses,
pub von_mises: f64,
pub tresca: f64,
pub hydrostatic: f64,
pub deviatoric_magnitude: f64,
pub safety_factor: f64,
}
pub struct StressAnalyzer;
impl StressAnalyzer {
pub fn analyze(state: &StressState, yield_strength: f64) -> StressAnalysisResult {
let vm = Self::von_mises(state);
let tr = Self::tresca(state);
let hs = Self::hydrostatic_stress(state);
let principal = Self::principal_stresses_3d(state);
let s_x = state.sigma_x - hs;
let s_y = state.sigma_y - hs;
let s_z = state.sigma_z - hs;
let dev_mag = (s_x * s_x
+ s_y * s_y
+ s_z * s_z
+ 2.0 * state.tau_xy * state.tau_xy
+ 2.0 * state.tau_yz * state.tau_yz
+ 2.0 * state.tau_xz * state.tau_xz)
.sqrt();
let safety_factor = if vm == 0.0 {
f64::INFINITY
} else {
yield_strength / vm
};
StressAnalysisResult {
principal,
von_mises: vm,
tresca: tr,
hydrostatic: hs,
deviatoric_magnitude: dev_mag,
safety_factor,
}
}
pub fn von_mises(state: &StressState) -> f64 {
let d1 = state.sigma_x - state.sigma_y;
let d2 = state.sigma_y - state.sigma_z;
let d3 = state.sigma_z - state.sigma_x;
let shear_term = 6.0
* (state.tau_xy * state.tau_xy
+ state.tau_yz * state.tau_yz
+ state.tau_xz * state.tau_xz);
(0.5 * (d1 * d1 + d2 * d2 + d3 * d3 + shear_term)).sqrt()
}
pub fn tresca(state: &StressState) -> f64 {
let p = Self::principal_stresses_3d(state);
p.sigma_1 - p.sigma_3
}
pub fn principal_stresses_2d(sigma_x: f64, sigma_y: f64, tau_xy: f64) -> (f64, f64) {
let avg = (sigma_x + sigma_y) / 2.0;
let r = Self::mohr_circle_radius(sigma_x, sigma_y, tau_xy);
(avg + r, avg - r)
}
pub fn principal_stresses_3d(state: &StressState) -> PrincipalStresses {
let i1 = state.sigma_x + state.sigma_y + state.sigma_z;
let i2 = state.sigma_x * state.sigma_y
+ state.sigma_y * state.sigma_z
+ state.sigma_z * state.sigma_x
- state.tau_xy * state.tau_xy
- state.tau_yz * state.tau_yz
- state.tau_xz * state.tau_xz;
let i3 = state.sigma_x * state.sigma_y * state.sigma_z
+ 2.0 * state.tau_xy * state.tau_yz * state.tau_xz
- state.sigma_x * state.tau_yz * state.tau_yz
- state.sigma_y * state.tau_xz * state.tau_xz
- state.sigma_z * state.tau_xy * state.tau_xy;
let shift = i1 / 3.0;
let p_coeff = i2 - i1 * i1 / 3.0; let q_coeff = -2.0 * i1 * i1 * i1 / 27.0 + i1 * i2 / 3.0 - i3;
let r_sq = (-p_coeff / 3.0).max(0.0);
let r_val = 2.0 * r_sq.sqrt();
let (s1, s2, s3) = if r_val < 1e-14 * (i1.abs() + 1.0) {
(shift, shift, shift)
} else {
let cos_arg = {
let denom = p_coeff * r_val;
if denom.abs() < 1e-30 {
0.0
} else {
(3.0 * q_coeff / denom).clamp(-1.0, 1.0)
}
};
let theta = cos_arg.acos() / 3.0;
let two_pi_3 = 2.0 * std::f64::consts::PI / 3.0;
let t0 = r_val * theta.cos() + shift;
let t1 = r_val * (theta - two_pi_3).cos() + shift;
let t2 = r_val * (theta - 2.0 * two_pi_3).cos() + shift;
(t0, t1, t2)
};
let mut vals = [s1, s2, s3];
vals.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
PrincipalStresses {
sigma_1: vals[0],
sigma_2: vals[1],
sigma_3: vals[2],
}
}
pub fn hydrostatic_stress(state: &StressState) -> f64 {
(state.sigma_x + state.sigma_y + state.sigma_z) / 3.0
}
pub fn is_yielded(state: &StressState, yield_strength: f64) -> bool {
Self::von_mises(state) >= yield_strength
}
pub fn mohr_circle_radius(sigma_x: f64, sigma_y: f64, tau_xy: f64) -> f64 {
let half_diff = (sigma_x - sigma_y) / 2.0;
(half_diff * half_diff + tau_xy * tau_xy).sqrt()
}
pub fn max_shear_stress_2d(sigma_x: f64, sigma_y: f64, tau_xy: f64) -> f64 {
Self::mohr_circle_radius(sigma_x, sigma_y, tau_xy)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn test_new_uniaxial_zero_components() {
let s = StressState::new_uniaxial(100.0);
assert_eq!(s.sigma_x, 100.0);
assert_eq!(s.sigma_y, 0.0);
assert_eq!(s.sigma_z, 0.0);
assert_eq!(s.tau_xy, 0.0);
assert_eq!(s.tau_yz, 0.0);
assert_eq!(s.tau_xz, 0.0);
}
#[test]
fn test_new_2d_zero_z_components() {
let s = StressState::new_2d(50.0, 30.0, 10.0);
assert_eq!(s.sigma_x, 50.0);
assert_eq!(s.sigma_y, 30.0);
assert_eq!(s.sigma_z, 0.0);
assert_eq!(s.tau_xy, 10.0);
assert_eq!(s.tau_yz, 0.0);
assert_eq!(s.tau_xz, 0.0);
}
#[test]
fn test_new_biaxial_zero_shear() {
let s = StressState::new_biaxial(200.0, -50.0);
assert_eq!(s.sigma_x, 200.0);
assert_eq!(s.sigma_y, -50.0);
assert_eq!(s.tau_xy, 0.0);
assert_eq!(s.tau_xz, 0.0);
assert_eq!(s.tau_yz, 0.0);
}
#[test]
fn test_new_hydrostatic_equal_normals() {
let s = StressState::new_hydrostatic(300.0);
assert_eq!(s.sigma_x, 300.0);
assert_eq!(s.sigma_y, 300.0);
assert_eq!(s.sigma_z, 300.0);
assert_eq!(s.tau_xy, 0.0);
}
#[test]
fn test_von_mises_uniaxial_equals_sigma() {
let s = StressState::new_uniaxial(250.0);
let vm = StressAnalyzer::von_mises(&s);
assert!((vm - 250.0).abs() < EPS, "vm={vm}");
}
#[test]
fn test_von_mises_uniaxial_negative_equals_magnitude() {
let s = StressState::new_uniaxial(-300.0);
let vm = StressAnalyzer::von_mises(&s);
assert!((vm - 300.0).abs() < EPS, "vm={vm}");
}
#[test]
fn test_von_mises_hydrostatic_zero() {
let s = StressState::new_hydrostatic(500.0);
let vm = StressAnalyzer::von_mises(&s);
assert!(vm < EPS, "vm should be ~0 for hydrostatic, got {vm}");
}
#[test]
fn test_von_mises_pure_shear() {
let tau = 100.0;
let s = StressState {
sigma_x: 0.0,
sigma_y: 0.0,
sigma_z: 0.0,
tau_xy: tau,
tau_yz: 0.0,
tau_xz: 0.0,
};
let vm = StressAnalyzer::von_mises(&s);
let expected = (3.0_f64).sqrt() * tau;
assert!((vm - expected).abs() < EPS * 100.0, "vm={vm}, expected={expected}");
}
#[test]
fn test_von_mises_biaxial_equal() {
let s = StressState::new_biaxial(100.0, 100.0);
let vm = StressAnalyzer::von_mises(&s);
assert!((vm - 100.0).abs() < EPS * 10.0, "vm should be 100.0, got {vm}");
}
#[test]
fn test_von_mises_biaxial_opposite() {
let sigma = 200.0;
let s = StressState::new_biaxial(sigma, -sigma);
let vm = StressAnalyzer::von_mises(&s);
let expected = (3.0_f64).sqrt() * sigma;
assert!((vm - expected).abs() < EPS * 1000.0, "vm={vm}, expected={expected}");
}
#[test]
fn test_tresca_hydrostatic_zero() {
let s = StressState::new_hydrostatic(400.0);
let tr = StressAnalyzer::tresca(&s);
assert!(tr.abs() < 1e-6, "tresca should be 0, got {tr}");
}
#[test]
fn test_tresca_uniaxial() {
let s = StressState::new_uniaxial(500.0);
let tr = StressAnalyzer::tresca(&s);
assert!((tr - 500.0).abs() < 1e-6, "tresca={tr}");
}
#[test]
fn test_tresca_nonnegative() {
let s = StressState::new_biaxial(300.0, -200.0);
let tr = StressAnalyzer::tresca(&s);
assert!(tr >= 0.0, "Tresca must be non-negative, got {tr}");
}
#[test]
fn test_hydrostatic_uniaxial() {
let s = StressState::new_uniaxial(300.0);
let hs = StressAnalyzer::hydrostatic_stress(&s);
assert!((hs - 100.0).abs() < EPS, "hs={hs}");
}
#[test]
fn test_hydrostatic_symmetric() {
let s = StressState::new_hydrostatic(150.0);
let hs = StressAnalyzer::hydrostatic_stress(&s);
assert!((hs - 150.0).abs() < EPS, "hs={hs}");
}
#[test]
fn test_hydrostatic_zero_for_pure_shear() {
let s = StressState { sigma_x: 0.0, sigma_y: 0.0, sigma_z: 0.0,
tau_xy: 50.0, tau_yz: 0.0, tau_xz: 0.0 };
let hs = StressAnalyzer::hydrostatic_stress(&s);
assert!(hs.abs() < EPS);
}
#[test]
fn test_mohr_radius_pure_normal() {
let r = StressAnalyzer::mohr_circle_radius(100.0, 60.0, 0.0);
assert!((r - 20.0).abs() < EPS);
}
#[test]
fn test_mohr_radius_pure_shear() {
let r = StressAnalyzer::mohr_circle_radius(50.0, 50.0, 30.0);
assert!((r - 30.0).abs() < EPS);
}
#[test]
fn test_mohr_radius_nonnegative() {
let r = StressAnalyzer::mohr_circle_radius(-100.0, 100.0, 50.0);
assert!(r >= 0.0);
}
#[test]
fn test_mohr_radius_pythagorean() {
let r = StressAnalyzer::mohr_circle_radius(130.0, 70.0, 40.0);
assert!((r - 50.0).abs() < EPS);
}
#[test]
fn test_principal_2d_no_shear() {
let (p1, p2) = StressAnalyzer::principal_stresses_2d(100.0, 40.0, 0.0);
assert!((p1 - 100.0).abs() < EPS);
assert!((p2 - 40.0).abs() < EPS);
}
#[test]
fn test_principal_2d_ordering() {
let (p1, p2) = StressAnalyzer::principal_stresses_2d(40.0, 100.0, 0.0);
assert!(p1 >= p2);
}
#[test]
fn test_principal_2d_symmetric() {
let tau = 50.0;
let (p1, p2) = StressAnalyzer::principal_stresses_2d(100.0, 100.0, tau);
assert!((p1 - (100.0 + tau)).abs() < EPS);
assert!((p2 - (100.0 - tau)).abs() < EPS);
}
#[test]
fn test_principal_2d_known_example() {
let (p1, p2) = StressAnalyzer::principal_stresses_2d(80.0, 40.0, 30.0);
let r = (1300.0_f64).sqrt();
assert!((p1 - (60.0 + r)).abs() < EPS * 100.0, "p1={p1}, expected={}", 60.0 + r);
assert!((p2 - (60.0 - r)).abs() < EPS * 100.0, "p2={p2}, expected={}", 60.0 - r);
}
#[test]
fn test_principal_3d_diagonal_ordering() {
let s = StressState {
sigma_x: 300.0, sigma_y: 100.0, sigma_z: 200.0,
tau_xy: 0.0, tau_yz: 0.0, tau_xz: 0.0,
};
let p = StressAnalyzer::principal_stresses_3d(&s);
assert!((p.sigma_1 - 300.0).abs() < 1e-6, "sigma_1={}", p.sigma_1);
assert!((p.sigma_2 - 200.0).abs() < 1e-6, "sigma_2={}", p.sigma_2);
assert!((p.sigma_3 - 100.0).abs() < 1e-6, "sigma_3={}", p.sigma_3);
}
#[test]
fn test_principal_3d_ordering_invariant() {
let s = StressState::new_2d(80.0, 40.0, 30.0);
let p = StressAnalyzer::principal_stresses_3d(&s);
assert!(p.sigma_1 >= p.sigma_2 - EPS);
assert!(p.sigma_2 >= p.sigma_3 - EPS);
}
#[test]
fn test_principal_3d_hydrostatic_equal() {
let s = StressState::new_hydrostatic(200.0);
let p = StressAnalyzer::principal_stresses_3d(&s);
assert!((p.sigma_1 - 200.0).abs() < 1e-6);
assert!((p.sigma_2 - 200.0).abs() < 1e-6);
assert!((p.sigma_3 - 200.0).abs() < 1e-6);
}
#[test]
fn test_principal_3d_uniaxial() {
let s = StressState::new_uniaxial(500.0);
let p = StressAnalyzer::principal_stresses_3d(&s);
assert!((p.sigma_1 - 500.0).abs() < 1e-6, "sigma_1={}", p.sigma_1);
assert!(p.sigma_2.abs() < 1e-6);
assert!(p.sigma_3.abs() < 1e-6);
}
#[test]
fn test_is_yielded_below_threshold() {
let s = StressState::new_uniaxial(100.0);
assert!(!StressAnalyzer::is_yielded(&s, 200.0));
}
#[test]
fn test_is_yielded_at_threshold() {
let s = StressState::new_uniaxial(200.0);
assert!(StressAnalyzer::is_yielded(&s, 200.0));
}
#[test]
fn test_is_yielded_above_threshold() {
let s = StressState::new_uniaxial(300.0);
assert!(StressAnalyzer::is_yielded(&s, 250.0));
}
#[test]
fn test_is_yielded_zero_stress_never_yields() {
let s = StressState::new_uniaxial(0.0);
assert!(!StressAnalyzer::is_yielded(&s, 100.0));
}
#[test]
fn test_safety_factor_uniaxial() {
let s = StressState::new_uniaxial(100.0);
let r = StressAnalyzer::analyze(&s, 400.0);
assert!((r.safety_factor - 4.0).abs() < EPS * 10.0);
}
#[test]
fn test_safety_factor_zero_stress_is_inf() {
let s = StressState::new_uniaxial(0.0);
let r = StressAnalyzer::analyze(&s, 250.0);
assert!(r.safety_factor.is_infinite());
}
#[test]
fn test_safety_factor_equals_one_at_yield() {
let s = StressState::new_uniaxial(300.0);
let r = StressAnalyzer::analyze(&s, 300.0);
assert!((r.safety_factor - 1.0).abs() < EPS * 10.0);
}
#[test]
fn test_analyze_fields_consistent() {
let s = StressState::new_biaxial(100.0, 50.0);
let r = StressAnalyzer::analyze(&s, 300.0);
let vm_direct = StressAnalyzer::von_mises(&s);
assert!((r.von_mises - vm_direct).abs() < EPS);
}
#[test]
fn test_analyze_tresca_nonnegative() {
let s = StressState::new_2d(200.0, -100.0, 75.0);
let r = StressAnalyzer::analyze(&s, 500.0);
assert!(r.tresca >= 0.0);
}
#[test]
fn test_analyze_deviatoric_zero_for_hydrostatic() {
let s = StressState::new_hydrostatic(123.0);
let r = StressAnalyzer::analyze(&s, 500.0);
assert!(r.deviatoric_magnitude < 1e-9, "dev_mag={}", r.deviatoric_magnitude);
}
#[test]
fn test_analyze_hydrostatic_value() {
let s = StressState {
sigma_x: 60.0, sigma_y: 90.0, sigma_z: 120.0,
tau_xy: 0.0, tau_yz: 0.0, tau_xz: 0.0,
};
let r = StressAnalyzer::analyze(&s, 500.0);
assert!((r.hydrostatic - 90.0).abs() < EPS * 10.0);
}
#[test]
fn test_max_shear_2d_equals_radius() {
let r = StressAnalyzer::max_shear_stress_2d(80.0, 40.0, 30.0);
let radius = StressAnalyzer::mohr_circle_radius(80.0, 40.0, 30.0);
assert!((r - radius).abs() < EPS);
}
#[test]
fn test_max_shear_2d_uniaxial() {
let r = StressAnalyzer::max_shear_stress_2d(200.0, 0.0, 0.0);
assert!((r - 100.0).abs() < EPS);
}
#[test]
fn test_von_mises_biaxial_known() {
let s = StressState::new_biaxial(100.0, 50.0);
let vm = StressAnalyzer::von_mises(&s);
let expected = 7500.0_f64.sqrt();
assert!((vm - expected).abs() < 1e-6, "vm={vm}, expected={expected}");
}
#[test]
fn test_safety_below_one_means_yielded() {
let s = StressState::new_uniaxial(500.0);
let r = StressAnalyzer::analyze(&s, 200.0);
assert!(r.safety_factor < 1.0);
assert!(StressAnalyzer::is_yielded(&s, 200.0));
}
#[test]
fn test_principal_3d_sum_equals_trace() {
let s = StressState {
sigma_x: 100.0, sigma_y: 200.0, sigma_z: 300.0,
tau_xy: 50.0, tau_yz: 30.0, tau_xz: 20.0,
};
let p = StressAnalyzer::principal_stresses_3d(&s);
let trace = s.sigma_x + s.sigma_y + s.sigma_z;
let principal_sum = p.sigma_1 + p.sigma_2 + p.sigma_3;
assert!((principal_sum - trace).abs() < 1e-4, "sum={principal_sum}, trace={trace}");
}
#[test]
fn test_analyze_full_3d() {
let s = StressState {
sigma_x: 150.0, sigma_y: 100.0, sigma_z: 50.0,
tau_xy: 40.0, tau_yz: 20.0, tau_xz: 10.0,
};
let result = StressAnalyzer::analyze(&s, 400.0);
assert!(result.von_mises > 0.0);
assert!(result.tresca > 0.0);
assert!(result.safety_factor > 0.0);
assert!(result.principal.sigma_1 >= result.principal.sigma_2 - 1e-6);
assert!(result.principal.sigma_2 >= result.principal.sigma_3 - 1e-6);
}
}