1use std::fmt;
17
18use nalgebra::{SMatrix, SVector};
19
20pub type PlaneVector = SVector<f64, 3>;
22pub type PlaneMatrix = SMatrix<f64, 3, 3>;
24
25#[derive(Debug, Clone, PartialEq)]
26pub enum PlaneMaterial {
27 ElasticMatrix { d: [f64; 6] },
31 Orthotropic {
35 ex: f64,
36 ey: f64,
37 nu_xy: f64,
38 g_xy: f64,
39 angle: f64,
40 },
41 PlaneStress { e: f64, nu: f64 },
43 PlaneStrain { e: f64, nu: f64 },
45 #[cfg(test)]
49 Probe { last: [f64; 3] },
50}
51
52#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum PlaneMaterialError {
55 InvalidModulus,
57 InvalidPoissonRatio,
59 NotPositiveDefinite,
61}
62
63impl PlaneMaterialError {
64 pub fn message(&self) -> &'static str {
65 match self {
66 PlaneMaterialError::InvalidModulus => "a modulus is not finite and positive",
67 PlaneMaterialError::InvalidPoissonRatio => "Poisson ratio is out of range",
68 PlaneMaterialError::NotPositiveDefinite => {
69 "the constitutive matrix is not symmetric positive definite"
70 }
71 }
72 }
73}
74
75impl fmt::Display for PlaneMaterialError {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.write_str(self.message())
78 }
79}
80
81impl std::error::Error for PlaneMaterialError {}
82
83impl PlaneMaterial {
84 pub fn plane_stress(e: f64, nu: f64) -> Result<Self, PlaneMaterialError> {
86 Self::checked(PlaneMaterial::PlaneStress { e, nu })
87 }
88
89 pub fn plane_strain(e: f64, nu: f64) -> Result<Self, PlaneMaterialError> {
91 Self::checked(PlaneMaterial::PlaneStrain { e, nu })
92 }
93
94 pub fn orthotropic(
96 ex: f64,
97 ey: f64,
98 nu_xy: f64,
99 g_xy: f64,
100 angle: f64,
101 ) -> Result<Self, PlaneMaterialError> {
102 Self::checked(PlaneMaterial::Orthotropic {
103 ex,
104 ey,
105 nu_xy,
106 g_xy,
107 angle,
108 })
109 }
110
111 pub fn elastic_matrix(d: [f64; 6]) -> Result<Self, PlaneMaterialError> {
113 Self::checked(PlaneMaterial::ElasticMatrix { d })
114 }
115
116 fn checked(material: PlaneMaterial) -> Result<Self, PlaneMaterialError> {
117 material.validate()?;
118 Ok(material)
119 }
120
121 pub fn validate(&self) -> Result<(), PlaneMaterialError> {
123 let positive = |v: f64| {
124 if v.is_finite() && v > 0.0 {
125 Ok(())
126 } else {
127 Err(PlaneMaterialError::InvalidModulus)
128 }
129 };
130 let poisson = |nu: f64, upper: f64| {
131 if nu.is_finite() && nu > -1.0 && nu < upper {
132 Ok(())
133 } else {
134 Err(PlaneMaterialError::InvalidPoissonRatio)
135 }
136 };
137 match *self {
138 PlaneMaterial::PlaneStress { e, nu } => {
139 positive(e)?;
140 poisson(nu, 1.0)?;
141 }
142 PlaneMaterial::PlaneStrain { e, nu } => {
143 positive(e)?;
144 poisson(nu, 0.5)?;
145 }
146 PlaneMaterial::Orthotropic {
147 ex,
148 ey,
149 nu_xy,
150 g_xy,
151 angle,
152 } => {
153 positive(ex)?;
154 positive(ey)?;
155 positive(g_xy)?;
156 if !nu_xy.is_finite() || !angle.is_finite() || nu_xy * nu_xy >= ex / ey {
157 return Err(PlaneMaterialError::InvalidPoissonRatio);
158 }
159 }
160 PlaneMaterial::ElasticMatrix { d } => {
161 let m = self.d_matrix();
162 if d.iter().any(|v| !v.is_finite()) || m.cholesky().is_none() {
163 return Err(PlaneMaterialError::NotPositiveDefinite);
164 }
165 }
166 #[cfg(test)]
167 PlaneMaterial::Probe { .. } => {}
168 }
169 Ok(())
170 }
171
172 pub fn d_matrix(&self) -> PlaneMatrix {
175 match *self {
176 PlaneMaterial::ElasticMatrix { d } => PlaneMatrix::new(
177 d[0], d[1], d[2], d[1], d[3], d[4], d[2], d[4], d[5],
180 ),
181 PlaneMaterial::PlaneStress { e, nu } => {
182 let c = e / (1.0 - nu * nu);
183 PlaneMatrix::new(
184 c,
185 c * nu,
186 0.0, c * nu,
188 c,
189 0.0, 0.0,
191 0.0,
192 c * (1.0 - nu) / 2.0,
193 )
194 }
195 PlaneMaterial::PlaneStrain { e, nu } => {
196 let c = e / ((1.0 + nu) * (1.0 - 2.0 * nu));
197 PlaneMatrix::new(
198 c * (1.0 - nu),
199 c * nu,
200 0.0, c * nu,
202 c * (1.0 - nu),
203 0.0, 0.0,
205 0.0,
206 c * (1.0 - 2.0 * nu) / 2.0,
207 )
208 }
209 PlaneMaterial::Orthotropic {
210 ex,
211 ey,
212 nu_xy,
213 g_xy,
214 angle,
215 } => {
216 let nu_yx = nu_xy * ey / ex;
217 let k = 1.0 / (1.0 - nu_xy * nu_yx);
218 let local = PlaneMatrix::new(
219 k * ex,
220 k * nu_xy * ey,
221 0.0, k * nu_xy * ey,
223 k * ey,
224 0.0, 0.0,
226 0.0,
227 g_xy,
228 );
229 let (s, c) = angle.sin_cos();
232 let t = PlaneMatrix::new(
233 c * c,
234 s * s,
235 c * s, s * s,
237 c * c,
238 -c * s, -2.0 * c * s,
240 2.0 * c * s,
241 c * c - s * s,
242 );
243 t.transpose() * local * t
244 }
245 #[cfg(test)]
246 PlaneMaterial::Probe { .. } => PlaneMatrix::identity(),
247 }
248 }
249
250 pub fn trial_stress_tangent(&self, strain: &PlaneVector) -> (PlaneVector, PlaneMatrix) {
252 let d = self.d_matrix();
253 #[cfg(test)]
254 if let PlaneMaterial::Probe { last } = *self {
255 return (d * (strain - PlaneVector::from(last)), d);
256 }
257 (d * strain, d)
258 }
259
260 pub fn commit(&self, strain: &PlaneVector) -> Self {
262 #[cfg(test)]
263 if let PlaneMaterial::Probe { .. } = self {
264 return PlaneMaterial::Probe {
265 last: [strain[0], strain[1], strain[2]],
266 };
267 }
268 let _ = strain;
269 self.clone()
270 }
271
272 pub fn is_linear(&self) -> bool {
275 match self {
276 PlaneMaterial::ElasticMatrix { .. }
277 | PlaneMaterial::Orthotropic { .. }
278 | PlaneMaterial::PlaneStress { .. }
279 | PlaneMaterial::PlaneStrain { .. } => true,
280 #[cfg(test)]
281 PlaneMaterial::Probe { .. } => false,
282 }
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 fn close(a: &PlaneMatrix, b: &PlaneMatrix, tol: f64) -> bool {
291 (a - b).abs().max() <= tol * a.abs().max().max(1.0)
292 }
293
294 #[test]
295 fn d_is_symmetric_and_positive_definite_for_every_variant() {
296 let materials = [
297 PlaneMaterial::plane_stress(200e3, 0.3).unwrap(),
298 PlaneMaterial::plane_strain(200e3, 0.3).unwrap(),
299 PlaneMaterial::orthotropic(30e3, 10e3, 0.25, 4e3, 0.6).unwrap(),
300 PlaneMaterial::elastic_matrix([4.0, 1.0, 0.5, 3.0, 0.2, 2.0]).unwrap(),
301 ];
302 for m in materials {
303 let d = m.d_matrix();
304 assert!(close(&d, &d.transpose(), 1e-14), "{m:?}");
305 assert!(d.cholesky().is_some(), "{m:?}");
306 }
307 }
308
309 #[test]
310 fn plane_stress_with_effective_constants_equals_plane_strain() {
311 let (e, nu) = (200e3, 0.3);
312 let strain = PlaneMaterial::plane_strain(e, nu).unwrap();
313 let (e_eff, nu_eff) = (e / (1.0 - nu * nu), nu / (1.0 - nu));
314 let stress = PlaneMaterial::plane_stress(e_eff, nu_eff).unwrap();
315 assert!(close(&strain.d_matrix(), &stress.d_matrix(), 1e-13));
316 }
317
318 #[test]
319 fn orthotropic_with_equal_moduli_is_isotropic_for_any_angle() {
320 let (e, nu) = (100.0, 0.25);
321 let iso = PlaneMaterial::plane_stress(e, nu).unwrap().d_matrix();
322 for angle in [0.0, 0.3, 1.1, -2.0] {
323 let ortho =
324 PlaneMaterial::orthotropic(e, e, nu, e / (2.0 * (1.0 + nu)), angle).unwrap();
325 assert!(close(&ortho.d_matrix(), &iso, 1e-13), "angle {angle}");
326 }
327 }
328
329 #[test]
330 fn rotating_by_ninety_degrees_swaps_the_axes() {
331 let (ex, ey, nu_xy, g) = (30.0, 10.0, 0.2, 4.0);
332 let d0 = PlaneMaterial::orthotropic(ex, ey, nu_xy, g, 0.0)
333 .unwrap()
334 .d_matrix();
335 let d90 = PlaneMaterial::orthotropic(ex, ey, nu_xy, g, std::f64::consts::FRAC_PI_2)
336 .unwrap()
337 .d_matrix();
338 assert!((d0[(0, 0)] - d90[(1, 1)]).abs() < 1e-12);
339 assert!((d0[(1, 1)] - d90[(0, 0)]).abs() < 1e-12);
340 assert!((d0[(0, 1)] - d90[(0, 1)]).abs() < 1e-12);
341 assert!((d0[(2, 2)] - d90[(2, 2)]).abs() < 1e-12);
342 assert!(d90[(0, 2)].abs() < 1e-12 && d90[(1, 2)].abs() < 1e-12);
343 }
344
345 #[test]
346 fn rotation_by_pi_over_four_couples_normal_and_shear() {
347 let d = PlaneMaterial::orthotropic(100.0, 5.0, 0.3, 4.0, std::f64::consts::FRAC_PI_4)
350 .unwrap()
351 .d_matrix();
352 assert!((d[(0, 0)] - d[(1, 1)]).abs() < 1e-12);
353 assert!(d[(0, 2)].abs() > 1.0);
354 }
355
356 #[test]
357 fn elastic_matrix_packing_matches_the_equivalent_isotropic_d() {
358 let iso = PlaneMaterial::plane_stress(100.0, 0.25).unwrap().d_matrix();
359 let packed = [
360 iso[(0, 0)],
361 iso[(0, 1)],
362 iso[(0, 2)],
363 iso[(1, 1)],
364 iso[(1, 2)],
365 iso[(2, 2)],
366 ];
367 let m = PlaneMaterial::elastic_matrix(packed).unwrap();
368 assert!(close(&m.d_matrix(), &iso, 1e-15));
369 let strain = PlaneVector::new(1e-3, -2e-3, 3e-3);
370 assert!((m.trial_stress_tangent(&strain).0 - iso * strain).norm() < 1e-12);
371 }
372
373 #[test]
374 fn invalid_constants_are_errors_not_panics() {
375 assert_eq!(
376 PlaneMaterial::plane_stress(0.0, 0.3),
377 Err(PlaneMaterialError::InvalidModulus)
378 );
379 assert_eq!(
380 PlaneMaterial::plane_stress(-1.0, 0.3),
381 Err(PlaneMaterialError::InvalidModulus)
382 );
383 assert_eq!(
384 PlaneMaterial::plane_stress(f64::NAN, 0.3),
385 Err(PlaneMaterialError::InvalidModulus)
386 );
387 assert_eq!(
388 PlaneMaterial::plane_stress(1.0, 1.0),
389 Err(PlaneMaterialError::InvalidPoissonRatio)
390 );
391 assert_eq!(
392 PlaneMaterial::plane_strain(1.0, 0.5),
393 Err(PlaneMaterialError::InvalidPoissonRatio)
394 );
395 assert_eq!(
396 PlaneMaterial::plane_strain(1.0, -1.0),
397 Err(PlaneMaterialError::InvalidPoissonRatio)
398 );
399 assert_eq!(
400 PlaneMaterial::orthotropic(1.0, 10.0, 0.5, 1.0, 0.0),
401 Err(PlaneMaterialError::InvalidPoissonRatio)
402 );
403 assert_eq!(
404 PlaneMaterial::orthotropic(1.0, 1.0, 0.0, 0.0, 0.0),
405 Err(PlaneMaterialError::InvalidModulus)
406 );
407 assert_eq!(
408 PlaneMaterial::elastic_matrix([1.0, 2.0, 0.0, 1.0, 0.0, 1.0]),
409 Err(PlaneMaterialError::NotPositiveDefinite)
410 );
411 assert_eq!(
412 PlaneMaterial::elastic_matrix([f64::NAN, 0.0, 0.0, 1.0, 0.0, 1.0]),
413 Err(PlaneMaterialError::NotPositiveDefinite)
414 );
415 assert!(PlaneMaterial::PlaneStress { e: -1.0, nu: 0.0 }
417 .validate()
418 .is_err());
419 }
420
421 #[test]
422 fn elastic_variants_are_linear_and_commit_changes_nothing() {
423 let m = PlaneMaterial::plane_stress(10.0, 0.2).unwrap();
424 assert!(m.is_linear());
425 assert_eq!(m.commit(&PlaneVector::new(1.0, 2.0, 3.0)), m);
426 }
427
428 #[test]
429 fn a_stateful_material_keeps_independent_history_per_copy() {
430 let proto = PlaneMaterial::Probe { last: [0.0; 3] };
431 let (mut a, mut b) = (proto.clone(), proto);
432 a = a.commit(&PlaneVector::new(1.0, 0.0, 0.0));
433 b = b.commit(&PlaneVector::new(0.0, 2.0, 0.0));
434 let probe = PlaneVector::new(1.0, 2.0, 0.0);
435 assert_eq!(
436 a.trial_stress_tangent(&probe).0,
437 PlaneVector::new(0.0, 2.0, 0.0)
438 );
439 assert_eq!(
440 b.trial_stress_tangent(&probe).0,
441 PlaneVector::new(1.0, 0.0, 0.0)
442 );
443 assert!(!a.is_linear());
444 }
445}