1use std::fmt;
4use std::ops::{Add, Mul, Sub};
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct StrainTensor {
14 pub components: [f64; 6],
15}
16
17impl StrainTensor {
18 #[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 pub const ZERO: Self = Self {
28 components: [0.0; 6],
29 };
30
31 #[must_use]
33 #[inline]
34 pub fn volumetric(&self) -> f64 {
35 self.components[0] + self.components[1] + self.components[2]
36 }
37
38 #[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 #[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 #[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#[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#[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
163pub 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
173pub 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 let s = StrainTensor::new(0.01, -0.003, -0.003, 0.0, 0.0, 0.0);
248 let eff = s.effective_strain();
249 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}