Skip to main content

solid_mechanics/
tensor.rs

1//! Stress and strain tensors for continuum mechanics.
2//!
3//! Provides Cauchy stress tensor and infinitesimal strain tensor representations
4//! using 3×3 symmetric matrices from nalgebra.
5
6use nalgebra::{Matrix3, Vector3};
7use serde::{Deserialize, Serialize};
8
9/// Cauchy stress tensor — a symmetric 3×3 tensor representing the state of stress at a point.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct StressTensor {
12    /// The 3×3 symmetric stress matrix [σ_ij]
13    pub matrix: Matrix3<f64>,
14}
15
16impl StressTensor {
17    /// Create a new stress tensor from a 3×3 matrix.
18    /// The matrix is symmetrized by averaging off-diagonal components.
19    pub fn new(matrix: Matrix3<f64>) -> Self {
20        let sym = (matrix + matrix.transpose()) * 0.5;
21        Self { matrix: sym }
22    }
23
24    /// Create a stress tensor from individual components.
25    /// σ = [[σ_xx, τ_xy, τ_xz], [τ_xy, σ_yy, τ_yz], [τ_xz, τ_yz, σ_zz]]
26    pub fn from_components(
27        sxx: f64, syy: f64, szz: f64,
28        txy: f64, txz: f64, tyz: f64,
29    ) -> Self {
30        Self {
31            matrix: Matrix3::new(
32                sxx, txy, txz,
33                txy, syy, tyz,
34                txz, tyz, szz,
35            ),
36        }
37    }
38
39    /// Hydrostatic (mean) stress: σ_m = (σ_xx + σ_yy + σ_zz) / 3
40    pub fn hydrostatic(&self) -> f64 {
41        self.matrix.trace() / 3.0
42    }
43
44    /// Deviatoric stress tensor: s_ij = σ_ij - σ_m * δ_ij
45    pub fn deviatoric(&self) -> StressTensor {
46        let p = self.hydrostatic();
47        let dev = self.matrix - p * Matrix3::identity();
48        StressTensor { matrix: dev }
49    }
50
51    /// Von Mises equivalent stress.
52    pub fn von_mises(&self) -> f64 {
53        let s = self.deviatoric().matrix;
54        ((3.0 / 2.0) * (
55            s[(0, 0)] * s[(0, 0)] + s[(1, 1)] * s[(1, 1)] + s[(2, 2)] * s[(2, 2)]
56            + 2.0 * (s[(0, 1)] * s[(0, 1)] + s[(0, 2)] * s[(0, 2)] + s[(1, 2)] * s[(1, 2)])
57        )).sqrt()
58    }
59
60    /// First invariant (trace): I₁ = σ_xx + σ_yy + σ_zz
61    pub fn first_invariant(&self) -> f64 {
62        self.matrix.trace()
63    }
64
65    /// Second invariant: I₂ = σ_xx*σ_yy + σ_yy*σ_zz + σ_zz*σ_xx - τ_xy² - τ_yz² - τ_xz²
66    pub fn second_invariant(&self) -> f64 {
67        let m = &self.matrix;
68        m[(0, 0)] * m[(1, 1)] + m[(1, 1)] * m[(2, 2)] + m[(2, 2)] * m[(0, 0)]
69            - m[(0, 1)] * m[(0, 1)] - m[(1, 2)] * m[(1, 2)] - m[(0, 2)] * m[(0, 2)]
70    }
71
72    /// Third invariant (determinant): I₃ = det(σ)
73    pub fn third_invariant(&self) -> f64 {
74        self.matrix.determinant()
75    }
76
77    /// Principal stresses (eigenvalues of the stress tensor), returned in descending order.
78    pub fn principal_stresses(&self) -> Vector3<f64> {
79        let eig = self.matrix.symmetric_eigen();
80        let mut vals = eig.eigenvalues.data.0[0];
81        vals.sort_by(|a, b| b.partial_cmp(a).unwrap());
82        Vector3::new(vals[0], vals[1], vals[2])
83    }
84
85    /// Maximum shear stress: τ_max = (σ₁ - σ₃) / 2
86    pub fn max_shear(&self) -> f64 {
87        let p = self.principal_stresses();
88        (p[0] - p[2]) / 2.0
89    }
90
91    /// Traction vector on a plane with normal n: t = σ · n
92    pub fn traction(&self, normal: &Vector3<f64>) -> Vector3<f64> {
93        self.matrix * normal
94    }
95
96    /// Normal stress on a plane: σ_n = n · (σ · n)
97    pub fn normal_stress_on_plane(&self, normal: &Vector3<f64>) -> f64 {
98        let t = self.traction(normal);
99        normal.dot(&t)
100    }
101
102    /// Shear stress on a plane: τ = |t - σ_n · n|
103    pub fn shear_stress_on_plane(&self, normal: &Vector3<f64>) -> f64 {
104        let t = self.traction(normal);
105        let sn = self.normal_stress_on_plane(normal);
106        let tau_vec = t - sn * normal;
107        tau_vec.norm()
108    }
109
110    /// Create a uniaxial stress state (σ_xx = σ, all others zero).
111    pub fn uniaxial(sigma: f64) -> Self {
112        Self::from_components(sigma, 0.0, 0.0, 0.0, 0.0, 0.0)
113    }
114
115    /// Create a biaxial stress state.
116    pub fn biaxial(sxx: f64, syy: f64) -> Self {
117        Self::from_components(sxx, syy, 0.0, 0.0, 0.0, 0.0)
118    }
119
120    /// Create a pure shear stress state.
121    pub fn pure_shear(txy: f64) -> Self {
122        Self::from_components(0.0, 0.0, 0.0, txy, 0.0, 0.0)
123    }
124}
125
126impl std::ops::Add for StressTensor {
127    type Output = StressTensor;
128    fn add(self, rhs: StressTensor) -> Self::Output {
129        StressTensor { matrix: self.matrix + rhs.matrix }
130    }
131}
132
133impl std::ops::Mul<f64> for StressTensor {
134    type Output = StressTensor;
135    fn mul(self, scalar: f64) -> Self::Output {
136        StressTensor { matrix: self.matrix * scalar }
137    }
138}
139
140/// Infinitesimal strain tensor — symmetric 3×3 tensor for small deformations.
141/// ε_ij = 0.5 * (∂u_i/∂x_j + ∂u_j/∂x_i)
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct StrainTensor {
144    pub matrix: Matrix3<f64>,
145}
146
147impl StrainTensor {
148    /// Create from a 3×3 matrix (symmetrized).
149    pub fn new(matrix: Matrix3<f64>) -> Self {
150        let sym = (matrix + matrix.transpose()) * 0.5;
151        Self { matrix: sym }
152    }
153
154    /// Create from individual components.
155    pub fn from_components(
156        exx: f64, eyy: f64, ezz: f64,
157        exy: f64, exz: f64, eyz: f64,
158    ) -> Self {
159        // Engineering shear strain: γ = 2ε
160        Self {
161            matrix: Matrix3::new(
162                exx, exy, exz,
163                exy, eyy, eyz,
164                exz, eyz, ezz,
165            ),
166        }
167    }
168
169    /// Create from engineering shear strains (γ_xy = 2ε_xy, etc.).
170    pub fn from_engineering(
171        exx: f64, eyy: f64, ezz: f64,
172        gamma_xy: f64, gamma_xz: f64, gamma_yz: f64,
173    ) -> Self {
174        Self::from_components(exx, eyy, ezz, gamma_xy / 2.0, gamma_xz / 2.0, gamma_yz / 2.0)
175    }
176
177    /// Volumetric strain: ε_v = ε_xx + ε_yy + ε_zz
178    pub fn volumetric(&self) -> f64 {
179        self.matrix.trace()
180    }
181
182    /// Deviatoric strain tensor.
183    pub fn deviatoric(&self) -> StrainTensor {
184        let ev = self.volumetric() / 3.0;
185        let dev = self.matrix - ev * Matrix3::identity();
186        StrainTensor { matrix: dev }
187    }
188
189    /// Principal strains (eigenvalues), descending order.
190    pub fn principal_strains(&self) -> Vector3<f64> {
191        let eig = self.matrix.symmetric_eigen();
192        let mut vals = eig.eigenvalues.data.0[0];
193        vals.sort_by(|a, b| b.partial_cmp(a).unwrap());
194        Vector3::new(vals[0], vals[1], vals[2])
195    }
196
197    /// Maximum engineering shear strain.
198    pub fn max_shear_strain(&self) -> f64 {
199        let p = self.principal_strains();
200        p[0] - p[2]
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use approx::assert_relative_eq;
208
209    #[test]
210    fn test_stress_tensor_symmetry() {
211        let s = StressTensor::new(Matrix3::new(
212            10.0, 5.0, 3.0,
213            4.0, 20.0, 6.0,
214            7.0, 8.0, 30.0,
215        ));
216        assert_relative_eq!(s.matrix[(0, 1)], s.matrix[(1, 0)]);
217        assert_relative_eq!(s.matrix[(0, 2)], s.matrix[(2, 0)]);
218        assert_relative_eq!(s.matrix[(1, 2)], s.matrix[(2, 1)]);
219    }
220
221    #[test]
222    fn test_hydrostatic_stress() {
223        let s = StressTensor::from_components(30.0, 30.0, 30.0, 0.0, 0.0, 0.0);
224        assert_relative_eq!(s.hydrostatic(), 30.0);
225    }
226
227    #[test]
228    fn test_deviatoric_trace_zero() {
229        let s = StressTensor::from_components(10.0, 20.0, 30.0, 5.0, 0.0, 0.0);
230        let dev = s.deviatoric();
231        assert_relative_eq!(dev.matrix.trace(), 0.0, epsilon = 1e-10);
232    }
233
234    #[test]
235    fn test_principal_stresses_ordering() {
236        let s = StressTensor::from_components(30.0, 10.0, 20.0, 0.0, 0.0, 0.0);
237        let p = s.principal_stresses();
238        assert!(p[0] >= p[1]);
239        assert!(p[1] >= p[2]);
240        assert_relative_eq!(p[0], 30.0);
241        assert_relative_eq!(p[1], 20.0);
242        assert_relative_eq!(p[2], 10.0);
243    }
244
245    #[test]
246    fn test_max_shear() {
247        let s = StressTensor::from_components(100.0, 0.0, -50.0, 0.0, 0.0, 0.0);
248        assert_relative_eq!(s.max_shear(), 75.0);
249    }
250
251    #[test]
252    fn test_uniaxial_von_mises() {
253        let s = StressTensor::uniaxial(100.0);
254        assert_relative_eq!(s.von_mises(), 100.0);
255    }
256
257    #[test]
258    fn test_strain_volumetric() {
259        let e = StrainTensor::from_components(0.001, 0.002, 0.003, 0.0, 0.0, 0.0);
260        assert_relative_eq!(e.volumetric(), 0.006);
261    }
262
263    #[test]
264    fn test_strain_deviatoric_trace_zero() {
265        let e = StrainTensor::from_components(0.001, 0.002, 0.003, 0.0, 0.0, 0.0);
266        let dev = e.deviatoric();
267        assert_relative_eq!(dev.matrix.trace(), 0.0, epsilon = 1e-12);
268    }
269
270    #[test]
271    fn test_traction_vector() {
272        let s = StressTensor::from_components(10.0, 0.0, 0.0, 5.0, 0.0, 0.0);
273        let n = Vector3::new(1.0, 0.0, 0.0);
274        let t = s.traction(&n);
275        assert_relative_eq!(t[0], 10.0);
276        assert_relative_eq!(t[1], 5.0);
277    }
278}