Skip to main content

dravya/
strain.rs

1//! Strain tensors (Voigt notation), engineering/true strain, and effective strain.
2
3use std::fmt;
4use std::ops::{Add, Mul, Sub};
5
6use serde::{Deserialize, Serialize};
7
8/// Symmetric strain tensor (6 independent components, Voigt notation).
9///
10/// Components: [εxx, εyy, εzz, γxy, γyz, γxz] where γ = engineering
11/// shear strain (γ = 2ε_ij for shear components).
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct StrainTensor {
14    pub components: [f64; 6],
15}
16
17impl StrainTensor {
18    /// Create from individual components (engineering shear strain convention).
19    #[must_use]
20    pub fn new(exx: f64, eyy: f64, ezz: f64, gxy: f64, gyz: f64, gxz: f64) -> Self {
21        Self {
22            components: [exx, eyy, ezz, gxy, gyz, gxz],
23        }
24    }
25
26    /// Zero strain state.
27    pub const ZERO: Self = Self {
28        components: [0.0; 6],
29    };
30
31    /// Volumetric strain: ε_vol = εxx + εyy + εzz
32    #[must_use]
33    #[inline]
34    pub fn volumetric(&self) -> f64 {
35        self.components[0] + self.components[1] + self.components[2]
36    }
37
38    /// Deviatoric strain tensor.
39    #[must_use]
40    #[inline]
41    pub fn deviatoric(&self) -> Self {
42        let mean = self.volumetric() / 3.0;
43        Self::new(
44            self.components[0] - mean,
45            self.components[1] - mean,
46            self.components[2] - mean,
47            self.components[3],
48            self.components[4],
49            self.components[5],
50        )
51    }
52
53    /// Von Mises equivalent strain.
54    ///
55    /// ε_eq = sqrt(2/3 * (e_xx² + e_yy² + e_zz² + γxy²/2 + γyz²/2 + γxz²/2))
56    /// where e_ij are deviatoric strain components.
57    #[must_use]
58    #[inline]
59    pub fn effective_strain(&self) -> f64 {
60        let dev = self.deviatoric();
61        let [exx, eyy, ezz, gxy, gyz, gxz] = dev.components;
62        let normal_sum = exx.powi(2) + eyy.powi(2) + ezz.powi(2);
63        let shear_sum = (gxy.powi(2) + gyz.powi(2) + gxz.powi(2)) / 2.0;
64        ((2.0 / 3.0) * (normal_sum + shear_sum)).sqrt()
65    }
66
67    /// Scale all components by a scalar.
68    #[must_use]
69    #[inline]
70    pub fn scale(self, factor: f64) -> Self {
71        Self {
72            components: [
73                self.components[0] * factor,
74                self.components[1] * factor,
75                self.components[2] * factor,
76                self.components[3] * factor,
77                self.components[4] * factor,
78                self.components[5] * factor,
79            ],
80        }
81    }
82}
83
84impl Default for StrainTensor {
85    fn default() -> Self {
86        Self::ZERO
87    }
88}
89
90impl fmt::Display for StrainTensor {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        let [exx, eyy, ezz, gxy, gyz, gxz] = self.components;
93        write!(
94            f,
95            "[εxx={exx:.3e}, εyy={eyy:.3e}, εzz={ezz:.3e}, γxy={gxy:.3e}, γyz={gyz:.3e}, γxz={gxz:.3e}]"
96        )
97    }
98}
99
100impl Add for StrainTensor {
101    type Output = Self;
102    fn add(self, rhs: Self) -> Self {
103        Self {
104            components: [
105                self.components[0] + rhs.components[0],
106                self.components[1] + rhs.components[1],
107                self.components[2] + rhs.components[2],
108                self.components[3] + rhs.components[3],
109                self.components[4] + rhs.components[4],
110                self.components[5] + rhs.components[5],
111            ],
112        }
113    }
114}
115
116impl Sub for StrainTensor {
117    type Output = Self;
118    fn sub(self, rhs: Self) -> Self {
119        Self {
120            components: [
121                self.components[0] - rhs.components[0],
122                self.components[1] - rhs.components[1],
123                self.components[2] - rhs.components[2],
124                self.components[3] - rhs.components[3],
125                self.components[4] - rhs.components[4],
126                self.components[5] - rhs.components[5],
127            ],
128        }
129    }
130}
131
132impl Mul<f64> for StrainTensor {
133    type Output = Self;
134    fn mul(self, rhs: f64) -> Self {
135        self.scale(rhs)
136    }
137}
138
139/// Engineering strain: ε = (L - L0) / L0
140#[must_use]
141#[inline]
142pub fn engineering_strain(original: f64, deformed: f64) -> f64 {
143    if original.abs() < hisab::EPSILON_F64 {
144        tracing::warn!("engineering_strain: zero original length, returning 0.0");
145        return 0.0;
146    }
147    (deformed - original) / original
148}
149
150/// True (logarithmic) strain: ε_true = ln(L / L0)
151#[must_use]
152#[inline]
153pub fn true_strain(original: f64, deformed: f64) -> f64 {
154    if original <= 0.0 || deformed <= 0.0 {
155        tracing::warn!(
156            "true_strain: non-positive input (original={original}, deformed={deformed}), returning 0.0"
157        );
158        return 0.0;
159    }
160    (deformed / original).ln()
161}
162
163/// Checked engineering strain. Returns `Err` for zero original length.
164pub fn try_engineering_strain(original: f64, deformed: f64) -> crate::Result<f64> {
165    if original.abs() < hisab::EPSILON_F64 {
166        return Err(crate::DravyaError::DivisionByZero(
167            "engineering_strain: zero original length",
168        ));
169    }
170    Ok((deformed - original) / original)
171}
172
173/// Checked true strain. Returns `Err` for non-positive inputs.
174pub fn try_true_strain(original: f64, deformed: f64) -> crate::Result<f64> {
175    if original <= 0.0 {
176        return Err(crate::DravyaError::InvalidParameter {
177            name: "original",
178            value: original,
179            reason: "must be positive",
180        });
181    }
182    if deformed <= 0.0 {
183        return Err(crate::DravyaError::InvalidParameter {
184            name: "deformed",
185            value: deformed,
186            reason: "must be positive",
187        });
188    }
189    Ok((deformed / original).ln())
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn engineering_strain_1_percent() {
198        let e = engineering_strain(100.0, 101.0);
199        assert!((e - 0.01).abs() < 1e-10, "1% extension, got {e}");
200    }
201
202    #[test]
203    fn true_strain_1_percent() {
204        let e = true_strain(100.0, 101.0);
205        assert!(
206            (e - 0.00995).abs() < 0.001,
207            "true strain ~0.00995 for 1%, got {e}"
208        );
209    }
210
211    #[test]
212    fn true_strain_close_to_engineering_small() {
213        let eng = engineering_strain(100.0, 100.5);
214        let tru = true_strain(100.0, 100.5);
215        assert!(
216            (eng - tru).abs() < 0.001,
217            "small strains should be nearly equal"
218        );
219    }
220
221    #[test]
222    fn volumetric_strain() {
223        let s = StrainTensor::new(0.01, 0.02, 0.03, 0.0, 0.0, 0.0);
224        assert!((s.volumetric() - 0.06).abs() < hisab::EPSILON_F64);
225    }
226
227    #[test]
228    fn zero_length_safe() {
229        assert_eq!(engineering_strain(0.0, 1.0), 0.0);
230        assert_eq!(true_strain(0.0, 1.0), 0.0);
231    }
232
233    #[test]
234    fn deviatoric_trace_zero() {
235        let s = StrainTensor::new(0.01, 0.02, 0.03, 0.001, 0.002, 0.003);
236        let dev = s.deviatoric();
237        assert!(
238            dev.volumetric().abs() < hisab::EPSILON_F64,
239            "deviatoric volumetric strain should be zero"
240        );
241    }
242
243    #[test]
244    fn effective_strain_uniaxial() {
245        // Uniaxial strain 0.01 with lateral contraction (v=0.3):
246        // exx=0.01, eyy=ezz=-0.003
247        let s = StrainTensor::new(0.01, -0.003, -0.003, 0.0, 0.0, 0.0);
248        let eff = s.effective_strain();
249        // dev = (0.00867, -0.00433, -0.00433, 0, 0, 0)
250        // eff = sqrt(2/3 * (7.51e-5 + 1.88e-5 + 1.88e-5)) ~ 0.00867
251        assert!(
252            (eff - 0.00867).abs() < 0.001,
253            "effective strain should be ~0.00867, got {eff}"
254        );
255    }
256
257    #[test]
258    fn arithmetic_add() {
259        let a = StrainTensor::new(0.01, 0.0, 0.0, 0.0, 0.0, 0.0);
260        let b = StrainTensor::new(0.02, 0.0, 0.0, 0.0, 0.0, 0.0);
261        let c = a + b;
262        assert!((c.components[0] - 0.03).abs() < hisab::EPSILON_F64);
263    }
264
265    #[test]
266    fn arithmetic_scale() {
267        let s = StrainTensor::new(0.01, 0.02, 0.03, 0.0, 0.0, 0.0);
268        let scaled = s * 2.0;
269        assert!((scaled.components[0] - 0.02).abs() < hisab::EPSILON_F64);
270    }
271
272    #[test]
273    fn default_is_zero() {
274        assert_eq!(StrainTensor::default(), StrainTensor::ZERO);
275    }
276
277    #[test]
278    fn display_format() {
279        let s = StrainTensor::new(0.001, 0.0, 0.0, 0.0, 0.0, 0.0);
280        let display = s.to_string();
281        assert!(display.contains("εxx="));
282    }
283
284    #[test]
285    fn serde_roundtrip() {
286        let s = StrainTensor::new(0.01, 0.02, 0.03, 0.001, 0.002, 0.003);
287        let json = serde_json::to_string(&s).unwrap();
288        let back: StrainTensor = serde_json::from_str(&json).unwrap();
289        assert_eq!(s, back);
290    }
291
292    #[test]
293    fn try_engineering_strain_zero_length() {
294        let result = try_engineering_strain(0.0, 1.0);
295        assert!(result.is_err());
296    }
297
298    #[test]
299    fn try_engineering_strain_valid() {
300        let result = try_engineering_strain(100.0, 101.0);
301        assert!(result.is_ok());
302        assert!((result.unwrap() - 0.01).abs() < 1e-10);
303    }
304
305    #[test]
306    fn try_true_strain_negative_original() {
307        let result = try_true_strain(-1.0, 1.0);
308        assert!(result.is_err());
309    }
310
311    #[test]
312    fn try_true_strain_negative_deformed() {
313        let result = try_true_strain(1.0, -1.0);
314        assert!(result.is_err());
315    }
316
317    #[test]
318    fn try_true_strain_valid() {
319        let result = try_true_strain(100.0, 101.0);
320        assert!(result.is_ok());
321    }
322}