1#![allow(dead_code)]
11
12use std::f64::consts::PI;
13
14fn mat3_zero() -> [[f64; 3]; 3] {
19 [[0.0; 3]; 3]
20}
21
22fn mat3_add_scaled(acc: &mut [[f64; 3]; 3], m: &[[f64; 3]; 3], scale: f64) {
23 for i in 0..3 {
24 for j in 0..3 {
25 acc[i][j] += m[i][j] * scale;
26 }
27 }
28}
29
30fn mat3_inv(m: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
32 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
33 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
34 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
35 if det.abs() < 1e-300 {
36 return None;
37 }
38 let inv_det = 1.0 / det;
39 Some([
40 [
41 (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
42 (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
43 (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
44 ],
45 [
46 (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
47 (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
48 (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
49 ],
50 [
51 (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
52 (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
53 (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
54 ],
55 ])
56}
57
58#[derive(Debug, Clone, Copy)]
64pub struct IsotropicConstituent {
65 pub youngs_modulus: f64,
67 pub poisson_ratio: f64,
69 pub density: f64,
71 pub volume_fraction: f64,
73}
74
75impl IsotropicConstituent {
76 pub fn new(
78 youngs_modulus: f64,
79 poisson_ratio: f64,
80 density: f64,
81 volume_fraction: f64,
82 ) -> Self {
83 Self {
84 youngs_modulus,
85 poisson_ratio,
86 density,
87 volume_fraction,
88 }
89 }
90
91 pub fn shear_modulus(&self) -> f64 {
93 self.youngs_modulus / (2.0 * (1.0 + self.poisson_ratio))
94 }
95
96 pub fn bulk_modulus(&self) -> f64 {
98 self.youngs_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
99 }
100}
101
102pub fn voigt_modulus(constituents: &[IsotropicConstituent]) -> f64 {
108 constituents
109 .iter()
110 .map(|c| c.volume_fraction * c.youngs_modulus)
111 .sum()
112}
113
114pub fn reuss_modulus(constituents: &[IsotropicConstituent]) -> f64 {
116 let inv_e: f64 = constituents
117 .iter()
118 .map(|c| c.volume_fraction / c.youngs_modulus)
119 .sum();
120 1.0 / inv_e
121}
122
123pub fn voigt_density(constituents: &[IsotropicConstituent]) -> f64 {
125 constituents
126 .iter()
127 .map(|c| c.volume_fraction * c.density)
128 .sum()
129}
130
131pub fn voigt_poisson(constituents: &[IsotropicConstituent]) -> f64 {
133 constituents
134 .iter()
135 .map(|c| c.volume_fraction * c.poisson_ratio)
136 .sum()
137}
138
139pub fn hashin_shtrikman_upper(matrix: &IsotropicConstituent, fiber: &IsotropicConstituent) -> f64 {
143 let (ref_phase, other) = if fiber.bulk_modulus() >= matrix.bulk_modulus() {
145 (fiber, matrix)
146 } else {
147 (matrix, fiber)
148 };
149 let k_ref = ref_phase.bulk_modulus();
150 let g_ref = ref_phase.shear_modulus();
151 let k_other = other.bulk_modulus();
152 let v_other = other.volume_fraction;
153
154 let v_ref = 1.0 - v_other;
156 let denom = 1.0 / (k_other - k_ref) + 3.0 * v_ref / (3.0 * k_ref + 4.0 * g_ref);
157 k_ref + v_other / denom
158}
159
160pub fn hashin_shtrikman_lower(matrix: &IsotropicConstituent, fiber: &IsotropicConstituent) -> f64 {
164 let (ref_phase, other) = if matrix.bulk_modulus() <= fiber.bulk_modulus() {
166 (matrix, fiber)
167 } else {
168 (fiber, matrix)
169 };
170 let k_ref = ref_phase.bulk_modulus();
171 let g_ref = ref_phase.shear_modulus();
172 let k_other = other.bulk_modulus();
173 let v_other = other.volume_fraction;
174
175 let v_ref = 1.0 - v_other;
176 let denom = 1.0 / (k_other - k_ref) + 3.0 * v_ref / (3.0 * k_ref + 4.0 * g_ref);
177 k_ref + v_other / denom
178}
179
180#[derive(Debug, Clone, Copy)]
186pub struct HalpinTsai {
187 pub fiber: IsotropicConstituent,
189 pub matrix: IsotropicConstituent,
191 pub aspect_ratio: f64,
193}
194
195impl HalpinTsai {
196 pub fn new(
198 fiber: IsotropicConstituent,
199 matrix: IsotropicConstituent,
200 aspect_ratio: f64,
201 ) -> Self {
202 Self {
203 fiber,
204 matrix,
205 aspect_ratio,
206 }
207 }
208
209 pub fn xi_longitudinal(&self) -> f64 {
211 2.0 * self.aspect_ratio
212 }
213
214 pub fn xi_transverse(&self) -> f64 {
216 2.0
217 }
218
219 pub fn eta(property_ratio: f64, xi: f64) -> f64 {
221 (property_ratio - 1.0) / (property_ratio + xi)
222 }
223
224 pub fn longitudinal_modulus(&self) -> f64 {
226 let v_f = self.fiber.volume_fraction;
227 let xi = self.xi_longitudinal();
228 let ratio = self.fiber.youngs_modulus / self.matrix.youngs_modulus;
229 let eta = Self::eta(ratio, xi);
230 self.matrix.youngs_modulus * (1.0 + xi * eta * v_f) / (1.0 - eta * v_f)
231 }
232
233 pub fn transverse_modulus(&self) -> f64 {
235 let v_f = self.fiber.volume_fraction;
236 let xi = self.xi_transverse();
237 let ratio = self.fiber.youngs_modulus / self.matrix.youngs_modulus;
238 let eta = Self::eta(ratio, xi);
239 self.matrix.youngs_modulus * (1.0 + xi * eta * v_f) / (1.0 - eta * v_f)
240 }
241
242 pub fn shear_modulus_12(&self) -> f64 {
244 let v_f = self.fiber.volume_fraction;
245 let xi = 1.0;
246 let ratio = self.fiber.shear_modulus() / self.matrix.shear_modulus();
247 let eta = Self::eta(ratio, xi);
248 self.matrix.shear_modulus() * (1.0 + xi * eta * v_f) / (1.0 - eta * v_f)
249 }
250
251 pub fn poisson_12(&self) -> f64 {
253 let v_f = self.fiber.volume_fraction;
254 let v_m = self.matrix.volume_fraction;
255 v_f * self.fiber.poisson_ratio + v_m * self.matrix.poisson_ratio
256 }
257
258 pub fn poisson_21(&self) -> f64 {
260 self.poisson_12() * self.transverse_modulus() / self.longitudinal_modulus()
261 }
262}
263
264#[derive(Debug, Clone, Copy)]
270pub struct OrthotropicPly {
271 pub e11: f64,
273 pub e22: f64,
275 pub g12: f64,
277 pub nu12: f64,
279 pub thickness: f64,
281}
282
283impl OrthotropicPly {
284 pub fn new(e11: f64, e22: f64, g12: f64, nu12: f64, thickness: f64) -> Self {
286 Self {
287 e11,
288 e22,
289 g12,
290 nu12,
291 thickness,
292 }
293 }
294
295 pub fn nu21(&self) -> f64 {
297 self.nu12 * self.e22 / self.e11
298 }
299
300 pub fn q_matrix(&self) -> [[f64; 3]; 3] {
309 let nu21 = self.nu21();
310 let denom = 1.0 - self.nu12 * nu21;
311 let q11 = self.e11 / denom;
312 let q22 = self.e22 / denom;
313 let q12 = self.nu12 * self.e22 / denom;
314 let q66 = self.g12;
315 [[q11, q12, 0.0], [q12, q22, 0.0], [0.0, 0.0, q66]]
316 }
317
318 pub fn q_bar_matrix(&self, angle_deg: f64) -> [[f64; 3]; 3] {
320 let theta = angle_deg * PI / 180.0;
321 let c = theta.cos();
322 let s = theta.sin();
323 let c2 = c * c;
324 let s2 = s * s;
325 let c4 = c2 * c2;
326 let s4 = s2 * s2;
327 let c2s2 = c2 * s2;
328
329 let q = self.q_matrix();
330 let q11 = q[0][0];
331 let q12 = q[0][1];
332 let q22 = q[1][1];
333 let q66 = q[2][2];
334
335 let qb11 = q11 * c4 + 2.0 * (q12 + 2.0 * q66) * c2s2 + q22 * s4;
336 let qb12 = (q11 + q22 - 4.0 * q66) * c2s2 + q12 * (c4 + s4);
337 let qb22 = q11 * s4 + 2.0 * (q12 + 2.0 * q66) * c2s2 + q22 * c4;
338 let qb16 = (q11 - q12 - 2.0 * q66) * c2 * c * s - (q22 - q12 - 2.0 * q66) * s2 * c * s;
339 let qb26 = (q11 - q12 - 2.0 * q66) * s2 * c * s - (q22 - q12 - 2.0 * q66) * c2 * c * s;
340 let qb66 = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * c2s2 + q66 * (c4 + s4);
341
342 [[qb11, qb12, qb16], [qb12, qb22, qb26], [qb16, qb26, qb66]]
343 }
344}
345
346#[derive(Debug, Clone)]
354pub struct Laminate {
355 pub plies: Vec<(OrthotropicPly, f64)>,
357}
358
359impl Laminate {
360 pub fn new() -> Self {
362 Self { plies: Vec::new() }
363 }
364
365 pub fn add_ply(&mut self, ply: OrthotropicPly, angle_deg: f64) {
367 self.plies.push((ply, angle_deg));
368 }
369
370 pub fn total_thickness(&self) -> f64 {
372 self.plies.iter().map(|(p, _)| p.thickness).sum()
373 }
374
375 fn ply_z_coords(&self) -> Vec<f64> {
377 let h = self.total_thickness();
378 let mut z = Vec::with_capacity(self.plies.len() + 1);
379 z.push(-h / 2.0);
380 for (ply, _) in &self.plies {
381 let last = *z.last().expect("collection should not be empty");
382 z.push(last + ply.thickness);
383 }
384 z
385 }
386
387 pub fn a_matrix(&self) -> [[f64; 3]; 3] {
389 let mut a = mat3_zero();
390 for (ply, angle) in &self.plies {
391 mat3_add_scaled(&mut a, &ply.q_bar_matrix(*angle), ply.thickness);
392 }
393 a
394 }
395
396 pub fn b_matrix(&self) -> [[f64; 3]; 3] {
398 let z = self.ply_z_coords();
399 let mut b = mat3_zero();
400 for (k, (ply, angle)) in self.plies.iter().enumerate() {
401 let scale = 0.5 * (z[k + 1].powi(2) - z[k].powi(2));
402 mat3_add_scaled(&mut b, &ply.q_bar_matrix(*angle), scale);
403 }
404 b
405 }
406
407 pub fn d_matrix(&self) -> [[f64; 3]; 3] {
409 let z = self.ply_z_coords();
410 let mut d = mat3_zero();
411 for (k, (ply, angle)) in self.plies.iter().enumerate() {
412 let scale = (z[k + 1].powi(3) - z[k].powi(3)) / 3.0;
413 mat3_add_scaled(&mut d, &ply.q_bar_matrix(*angle), scale);
414 }
415 d
416 }
417
418 pub fn effective_modulus_x(&self) -> f64 {
420 let h = self.total_thickness();
421 if let Some(a_inv) = mat3_inv(&self.a_matrix()) {
422 1.0 / (a_inv[0][0] * h)
423 } else {
424 0.0
425 }
426 }
427
428 pub fn effective_modulus_y(&self) -> f64 {
430 let h = self.total_thickness();
431 if let Some(a_inv) = mat3_inv(&self.a_matrix()) {
432 1.0 / (a_inv[1][1] * h)
433 } else {
434 0.0
435 }
436 }
437
438 pub fn effective_shear_modulus(&self) -> f64 {
440 let h = self.total_thickness();
441 if let Some(a_inv) = mat3_inv(&self.a_matrix()) {
442 1.0 / (a_inv[2][2] * h)
443 } else {
444 0.0
445 }
446 }
447
448 pub fn is_symmetric(&self) -> bool {
450 let b = self.b_matrix();
451 let a = self.a_matrix();
452 let a_norm = a[0][0].abs().max(a[1][1].abs()).max(a[2][2].abs());
454 let tol = 1.0e-6 * a_norm;
455 b.iter().flatten().all(|v| v.abs() < tol)
456 }
457}
458
459impl Default for Laminate {
460 fn default() -> Self {
461 Self::new()
462 }
463}
464
465#[derive(Debug, Clone, Copy)]
473pub struct MoriTanaka {
474 pub matrix: IsotropicConstituent,
476 pub inclusion: IsotropicConstituent,
478}
479
480impl MoriTanaka {
481 pub fn new(matrix: IsotropicConstituent, inclusion: IsotropicConstituent) -> Self {
483 Self { matrix, inclusion }
484 }
485
486 pub fn effective_bulk_modulus(&self) -> f64 {
490 let k_m = self.matrix.bulk_modulus();
491 let g_m = self.matrix.shear_modulus();
492 let k_i = self.inclusion.bulk_modulus();
493 let v_i = self.inclusion.volume_fraction;
494
495 let alpha = k_m + 4.0 * g_m / 3.0;
496 k_m + v_i * (k_i - k_m) / (1.0 + (1.0 - v_i) * (k_i - k_m) / alpha)
497 }
498
499 pub fn effective_shear_modulus(&self) -> f64 {
504 let k_m = self.matrix.bulk_modulus();
505 let g_m = self.matrix.shear_modulus();
506 let g_i = self.inclusion.shear_modulus();
507 let v_i = self.inclusion.volume_fraction;
508
509 let beta = g_m * (9.0 * k_m + 8.0 * g_m) / (6.0 * (k_m + 2.0 * g_m));
510 g_m + v_i * (g_i - g_m) / (1.0 + (1.0 - v_i) * (g_i - g_m) / beta)
511 }
512
513 pub fn effective_youngs_modulus(&self) -> f64 {
517 let k = self.effective_bulk_modulus();
518 let g = self.effective_shear_modulus();
519 9.0 * k * g / (3.0 * k + g)
520 }
521}
522
523#[derive(Debug, Clone, Copy)]
529pub struct PlyStrength {
530 pub xt: f64,
532 pub xc: f64,
534 pub yt: f64,
536 pub yc: f64,
538 pub s12: f64,
540}
541
542impl PlyStrength {
543 pub fn new(xt: f64, xc: f64, yt: f64, yc: f64, s12: f64) -> Self {
545 Self {
546 xt,
547 xc,
548 yt,
549 yc,
550 s12,
551 }
552 }
553
554 pub fn cfrp_typical() -> Self {
556 Self {
557 xt: 1500.0e6,
558 xc: 1200.0e6,
559 yt: 50.0e6,
560 yc: 250.0e6,
561 s12: 70.0e6,
562 }
563 }
564
565 pub fn gfrp_typical() -> Self {
567 Self {
568 xt: 1000.0e6,
569 xc: 600.0e6,
570 yt: 30.0e6,
571 yc: 110.0e6,
572 s12: 55.0e6,
573 }
574 }
575}
576
577#[derive(Debug, Clone, Copy)]
583pub struct PlyFailureResult {
584 pub fi_fiber: f64,
586 pub fi_matrix: f64,
588 pub fi_shear: f64,
590 pub failed: bool,
592}
593
594pub fn max_stress_failure(sigma: [f64; 3], strength: &PlyStrength) -> PlyFailureResult {
598 let s1 = sigma[0];
599 let s2 = sigma[1];
600 let s12 = sigma[2];
601
602 let fi_fiber = if s1 >= 0.0 {
603 s1 / strength.xt
604 } else {
605 (-s1) / strength.xc
606 };
607
608 let fi_matrix = if s2 >= 0.0 {
609 s2 / strength.yt
610 } else {
611 (-s2) / strength.yc
612 };
613
614 let fi_shear = s12.abs() / strength.s12;
615
616 PlyFailureResult {
617 fi_fiber,
618 fi_matrix,
619 fi_shear,
620 failed: fi_fiber >= 1.0 || fi_matrix >= 1.0 || fi_shear >= 1.0,
621 }
622}
623
624pub fn tsai_wu_failure(sigma: [f64; 3], strength: &PlyStrength) -> f64 {
632 let s1 = sigma[0];
633 let s2 = sigma[1];
634 let s12 = sigma[2];
635
636 let f1 = 1.0 / strength.xt - 1.0 / strength.xc;
637 let f2 = 1.0 / strength.yt - 1.0 / strength.yc;
638 let f11 = 1.0 / (strength.xt * strength.xc);
639 let f22 = 1.0 / (strength.yt * strength.yc);
640 let f66 = 1.0 / (strength.s12 * strength.s12);
641 let f12 = -0.5 * (f11 * f22).sqrt();
643
644 f1 * s1 + f2 * s2 + f11 * s1 * s1 + f22 * s2 * s2 + f66 * s12 * s12 + 2.0 * f12 * s1 * s2
645}
646
647#[derive(Debug, Clone, Copy)]
653pub struct HashinResult {
654 pub fiber_tension: f64,
656 pub fiber_compression: f64,
658 pub matrix_tension: f64,
660 pub matrix_compression: f64,
662 pub failed: bool,
664}
665
666pub fn hashin_failure(sigma: [f64; 3], strength: &PlyStrength) -> HashinResult {
670 let s1 = sigma[0];
671 let s2 = sigma[1];
672 let s12 = sigma[2];
673
674 let fiber_tension = if s1 >= 0.0 {
676 (s1 / strength.xt).powi(2) + (s12 / strength.s12).powi(2)
677 } else {
678 0.0
679 };
680
681 let fiber_compression = if s1 < 0.0 {
683 (-s1 / strength.xc).powi(2)
684 } else {
685 0.0
686 };
687
688 let matrix_tension = if s2 >= 0.0 {
690 (s2 / strength.yt).powi(2) + (s12 / strength.s12).powi(2)
691 } else {
692 0.0
693 };
694
695 let matrix_compression = if s2 < 0.0 {
697 (s2 / (2.0 * strength.s12)).powi(2)
698 + ((strength.yc / (2.0 * strength.s12)).powi(2) - 1.0) * s2 / strength.yc
699 + (s12 / strength.s12).powi(2)
700 } else {
701 0.0
702 };
703
704 let failed = fiber_tension >= 1.0
705 || fiber_compression >= 1.0
706 || matrix_tension >= 1.0
707 || matrix_compression >= 1.0;
708
709 HashinResult {
710 fiber_tension,
711 fiber_compression,
712 matrix_tension,
713 matrix_compression,
714 failed,
715 }
716}
717
718#[derive(Debug, Clone, Copy)]
724pub struct PuckResult {
725 pub iff_effort: f64,
727 pub ff_effort: f64,
729 pub failed: bool,
731}
732
733pub fn puck_failure(sigma: [f64; 3], strength: &PlyStrength) -> PuckResult {
737 let s1 = sigma[0];
738 let s2 = sigma[1];
739 let s12 = sigma[2];
740
741 let ff_effort = if s1 >= 0.0 {
743 s1 / strength.xt
744 } else {
745 (-s1) / strength.xc
746 };
747
748 let iff_effort = if s2 >= 0.0 {
750 ((s2 / strength.yt).powi(2) + (s12 / strength.s12).powi(2)).sqrt()
752 } else {
753 let p_perp_minus = 0.25; let tau_eff = (s12 * s12 + (p_perp_minus * s2).powi(2)).sqrt();
756 tau_eff / (strength.s12 - p_perp_minus * s2)
757 };
758
759 PuckResult {
760 iff_effort,
761 ff_effort,
762 failed: iff_effort >= 1.0 || ff_effort >= 1.0,
763 }
764}
765
766#[derive(Debug, Clone, Copy)]
772pub struct PlyStress {
773 pub ply_index: usize,
775 pub angle_deg: f64,
777 pub stress: [f64; 3],
779 pub strain: [f64; 3],
781 pub z_mid: f64,
783}
784
785fn transform_strain_to_material(global_strain: [f64; 3], angle_deg: f64) -> [f64; 3] {
790 let theta = angle_deg * PI / 180.0;
791 let c = theta.cos();
792 let s = theta.sin();
793 let c2 = c * c;
794 let s2 = s * s;
795 let cs = c * s;
796
797 let ex = global_strain[0];
798 let ey = global_strain[1];
799 let gxy = global_strain[2];
800
801 [
802 c2 * ex + s2 * ey + cs * gxy,
803 s2 * ex + c2 * ey - cs * gxy,
804 -2.0 * cs * ex + 2.0 * cs * ey + (c2 - s2) * gxy,
805 ]
806}
807
808#[allow(dead_code)]
813pub fn ply_by_ply_stress(
814 laminate: &Laminate,
815 midplane_strain: [f64; 3],
816 curvature: [f64; 3],
817) -> Vec<PlyStress> {
818 let z_coords = laminate.ply_z_coords();
819 let mut results = Vec::with_capacity(laminate.plies.len());
820
821 for (k, (ply, angle)) in laminate.plies.iter().enumerate() {
822 let z_mid = (z_coords[k] + z_coords[k + 1]) / 2.0;
823
824 let global_strain = [
826 midplane_strain[0] + z_mid * curvature[0],
827 midplane_strain[1] + z_mid * curvature[1],
828 midplane_strain[2] + z_mid * curvature[2],
829 ];
830
831 let mat_strain = transform_strain_to_material(global_strain, *angle);
833
834 let q = ply.q_matrix();
836 let stress = [
837 q[0][0] * mat_strain[0] + q[0][1] * mat_strain[1],
838 q[0][1] * mat_strain[0] + q[1][1] * mat_strain[1],
839 q[2][2] * mat_strain[2],
840 ];
841
842 results.push(PlyStress {
843 ply_index: k,
844 angle_deg: *angle,
845 stress,
846 strain: mat_strain,
847 z_mid,
848 });
849 }
850
851 results
852}
853
854#[allow(dead_code)]
860#[derive(Debug, Clone)]
861pub struct ProgressiveFailureResult {
862 pub first_ply_failure_load: f64,
864 pub first_failed_ply: usize,
866 pub ultimate_load: f64,
868 pub n_failed_plies: usize,
870}
871
872#[allow(dead_code)]
877pub fn progressive_failure_analysis(
878 laminate: &Laminate,
879 base_strain: [f64; 3],
880 strength: &PlyStrength,
881 max_load_factor: f64,
882 n_steps: usize,
883) -> ProgressiveFailureResult {
884 let n_plies = laminate.plies.len();
885 let mut failed = vec![false; n_plies];
886 let mut first_failure_load = max_load_factor;
887 let mut first_failed_ply = 0;
888 let mut ultimate_load = max_load_factor;
889 let mut n_failed = 0;
890 let mut first_found = false;
891
892 let curvature = [0.0, 0.0, 0.0];
893
894 for step in 0..=n_steps {
895 let lf = max_load_factor * (step as f64) / (n_steps as f64);
896 let strain = [
897 base_strain[0] * lf,
898 base_strain[1] * lf,
899 base_strain[2] * lf,
900 ];
901
902 let ply_stresses = ply_by_ply_stress(laminate, strain, curvature);
903
904 let mut all_failed = true;
905 for ps in &ply_stresses {
906 if failed[ps.ply_index] {
907 continue;
908 }
909 let result = max_stress_failure(ps.stress, strength);
910 if result.failed {
911 failed[ps.ply_index] = true;
912 n_failed += 1;
913 if !first_found {
914 first_failure_load = lf;
915 first_failed_ply = ps.ply_index;
916 first_found = true;
917 }
918 }
919 if !failed[ps.ply_index] {
920 all_failed = false;
921 }
922 }
923
924 if all_failed && n_plies > 0 {
925 ultimate_load = lf;
926 break;
927 }
928 }
929
930 ProgressiveFailureResult {
931 first_ply_failure_load: first_failure_load,
932 first_failed_ply,
933 ultimate_load,
934 n_failed_plies: n_failed,
935 }
936}
937
938#[allow(dead_code)]
947pub fn thermal_strain(alpha: [f64; 2], delta_t: f64) -> [f64; 3] {
948 [alpha[0] * delta_t, alpha[1] * delta_t, 0.0]
949}
950
951#[allow(dead_code)]
959pub fn thermal_residual_stresses(
960 laminate: &Laminate,
961 alpha: [f64; 2],
962 delta_t: f64,
963) -> Vec<[f64; 3]> {
964 let a_mat = laminate.a_matrix();
966 let a_inv = match mat3_inv(&a_mat) {
967 Some(inv) => inv,
968 None => return vec![[0.0; 3]; laminate.plies.len()],
969 };
970
971 let mut n_thermal = [0.0; 3];
973 for (ply, angle) in &laminate.plies {
974 let theta = angle * PI / 180.0;
975 let c = theta.cos();
976 let s = theta.sin();
977 let c2 = c * c;
978 let s2 = s * s;
979 let cs = c * s;
980
981 let alpha_x = c2 * alpha[0] + s2 * alpha[1];
983 let alpha_y = s2 * alpha[0] + c2 * alpha[1];
984 let alpha_xy = 2.0 * cs * (alpha[0] - alpha[1]);
985
986 let qbar = ply.q_bar_matrix(*angle);
987 let t = ply.thickness;
988
989 n_thermal[0] +=
990 (qbar[0][0] * alpha_x + qbar[0][1] * alpha_y + qbar[0][2] * alpha_xy) * delta_t * t;
991 n_thermal[1] +=
992 (qbar[1][0] * alpha_x + qbar[1][1] * alpha_y + qbar[1][2] * alpha_xy) * delta_t * t;
993 n_thermal[2] +=
994 (qbar[2][0] * alpha_x + qbar[2][1] * alpha_y + qbar[2][2] * alpha_xy) * delta_t * t;
995 }
996
997 let eps0 = [
999 a_inv[0][0] * n_thermal[0] + a_inv[0][1] * n_thermal[1] + a_inv[0][2] * n_thermal[2],
1000 a_inv[1][0] * n_thermal[0] + a_inv[1][1] * n_thermal[1] + a_inv[1][2] * n_thermal[2],
1001 a_inv[2][0] * n_thermal[0] + a_inv[2][1] * n_thermal[1] + a_inv[2][2] * n_thermal[2],
1002 ];
1003
1004 let mut results = Vec::with_capacity(laminate.plies.len());
1006 for (ply, angle) in &laminate.plies {
1007 let theta = angle * PI / 180.0;
1008 let c = theta.cos();
1009 let s = theta.sin();
1010 let c2 = c * c;
1011 let s2 = s * s;
1012 let cs = c * s;
1013
1014 let alpha_x = c2 * alpha[0] + s2 * alpha[1];
1016 let alpha_y = s2 * alpha[0] + c2 * alpha[1];
1017 let alpha_xy = 2.0 * cs * (alpha[0] - alpha[1]);
1018
1019 let mech_strain_global = [
1021 eps0[0] - alpha_x * delta_t,
1022 eps0[1] - alpha_y * delta_t,
1023 eps0[2] - alpha_xy * delta_t,
1024 ];
1025
1026 let mat_strain = transform_strain_to_material(mech_strain_global, *angle);
1028
1029 let q = ply.q_matrix();
1031 let stress = [
1032 q[0][0] * mat_strain[0] + q[0][1] * mat_strain[1],
1033 q[0][1] * mat_strain[0] + q[1][1] * mat_strain[1],
1034 q[2][2] * mat_strain[2],
1035 ];
1036
1037 results.push(stress);
1038 }
1039
1040 results
1041}
1042
1043#[allow(dead_code)]
1052pub fn interlaminar_shear_estimate(
1053 laminate: &Laminate,
1054 midplane_strain: [f64; 3],
1055 curvature: [f64; 3],
1056) -> Vec<f64> {
1057 let z_coords = laminate.ply_z_coords();
1058 let n = laminate.plies.len();
1059 if n < 2 {
1060 return Vec::new();
1061 }
1062
1063 let mut shear_stresses = Vec::with_capacity(n - 1);
1064
1065 let mut sum_sx = 0.0;
1067
1068 for k in 0..n - 1 {
1069 let z_bot = z_coords[k];
1070 let z_top = z_coords[k + 1];
1071 let (ply, angle) = &laminate.plies[k];
1072 let qbar = ply.q_bar_matrix(*angle);
1073
1074 let dz = z_top - z_bot;
1078 let z_avg = (z_bot + z_top) / 2.0;
1079
1080 let eps_x_avg = midplane_strain[0] + z_avg * curvature[0];
1081 let eps_y_avg = midplane_strain[1] + z_avg * curvature[1];
1082 let gam_avg = midplane_strain[2] + z_avg * curvature[2];
1083
1084 let sigma_x = qbar[0][0] * eps_x_avg + qbar[0][1] * eps_y_avg + qbar[0][2] * gam_avg;
1085 sum_sx += sigma_x * dz;
1086
1087 let total_h = laminate.total_thickness();
1090 shear_stresses.push(sum_sx / total_h);
1091 }
1092
1093 shear_stresses
1094}
1095
1096#[cfg(test)]
1101mod tests {
1102 use super::*;
1103
1104 const TOL: f64 = 1.0e-6;
1105
1106 #[test]
1107 fn test_voigt_reuss_ordering() {
1108 let c1 = IsotropicConstituent::new(70.0e9, 0.33, 2700.0, 0.3);
1109 let c2 = IsotropicConstituent::new(400.0e9, 0.25, 3200.0, 0.7);
1110 let constituents = [c1, c2];
1111 let ev = voigt_modulus(&constituents);
1112 let er = reuss_modulus(&constituents);
1113 assert!(ev > er, "Voigt ({ev}) should be > Reuss ({er})");
1114 }
1115
1116 #[test]
1117 fn test_voigt_density_weighted_average() {
1118 let c1 = IsotropicConstituent::new(70.0e9, 0.33, 2700.0, 0.4);
1119 let c2 = IsotropicConstituent::new(200.0e9, 0.30, 7800.0, 0.6);
1120 let constituents = [c1, c2];
1121 let rho = voigt_density(&constituents);
1122 let expected = 0.4 * 2700.0 + 0.6 * 7800.0;
1123 assert!(
1124 (rho - expected).abs() < TOL,
1125 "density = {rho} expected {expected}"
1126 );
1127 }
1128
1129 #[test]
1130 fn test_halpin_tsai_longitudinal_between_fiber_matrix() {
1131 let fiber = IsotropicConstituent::new(230.0e9, 0.25, 1800.0, 0.6);
1132 let matrix = IsotropicConstituent::new(3.5e9, 0.35, 1200.0, 0.4);
1133 let ht = HalpinTsai::new(fiber, matrix, 50.0);
1134 let e11 = ht.longitudinal_modulus();
1135 assert!(
1136 e11 > matrix.youngs_modulus && e11 < fiber.youngs_modulus,
1137 "E11 = {e11} not between matrix ({}) and fiber ({})",
1138 matrix.youngs_modulus,
1139 fiber.youngs_modulus
1140 );
1141 }
1142
1143 #[test]
1144 fn test_orthotropic_ply_q_matrix_symmetry() {
1145 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1146 let q = ply.q_matrix();
1147 assert!(
1148 (q[0][1] - q[1][0]).abs() < TOL,
1149 "Q[0][1]={} != Q[1][0]={}",
1150 q[0][1],
1151 q[1][0]
1152 );
1153 }
1154
1155 #[test]
1156 fn test_symmetric_crossply_b_near_zero() {
1157 let t = 0.125e-3_f64;
1158 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, t);
1159 let mut lam = Laminate::new();
1160 lam.add_ply(ply, 0.0);
1161 lam.add_ply(ply, 90.0);
1162 lam.add_ply(ply, 0.0);
1163 assert!(
1164 lam.is_symmetric(),
1165 "Symmetric [0/90/0] laminate should have B ~ 0"
1166 );
1167 }
1168
1169 #[test]
1170 fn test_laminate_single_0deg_effective_modulus_x() {
1171 let e11 = 140.0e9_f64;
1172 let e22 = 10.0e9_f64;
1173 let g12 = 5.0e9_f64;
1174 let nu12 = 0.3_f64;
1175 let t = 1.0e-3_f64;
1176 let ply = OrthotropicPly::new(e11, e22, g12, nu12, t);
1177 let mut lam = Laminate::new();
1178 lam.add_ply(ply, 0.0);
1179 let ex = lam.effective_modulus_x();
1180 assert!(
1181 (ex - e11).abs() / e11 < 0.05,
1182 "effective_modulus_x = {ex}, E11 = {e11}"
1183 );
1184 }
1185
1186 #[test]
1187 fn test_mori_tanaka_modulus_between_phases() {
1188 let matrix = IsotropicConstituent::new(3.5e9, 0.35, 1200.0, 0.7);
1189 let inclusion = IsotropicConstituent::new(70.0e9, 0.22, 2700.0, 0.3);
1190 let mt = MoriTanaka::new(matrix, inclusion);
1191 let e_eff = mt.effective_youngs_modulus();
1192 assert!(
1193 e_eff > matrix.youngs_modulus && e_eff < inclusion.youngs_modulus,
1194 "E_eff = {e_eff} not between matrix ({}) and inclusion ({})",
1195 matrix.youngs_modulus,
1196 inclusion.youngs_modulus
1197 );
1198 }
1199
1200 #[test]
1201 fn test_halpin_tsai_poisson_12() {
1202 let fiber = IsotropicConstituent::new(230.0e9, 0.25, 1800.0, 0.6);
1203 let matrix = IsotropicConstituent::new(3.5e9, 0.35, 1200.0, 0.4);
1204 let ht = HalpinTsai::new(fiber, matrix, 50.0);
1205 let nu12 = ht.poisson_12();
1206 let expected = 0.6 * 0.25 + 0.4 * 0.35;
1207 assert!(
1208 (nu12 - expected).abs() < TOL,
1209 "nu_12 = {nu12} expected {expected}"
1210 );
1211 }
1212
1213 #[test]
1216 fn test_max_stress_no_failure() {
1217 let s = PlyStrength::cfrp_typical();
1218 let sigma = [100.0e6, 10.0e6, 5.0e6];
1219 let r = max_stress_failure(sigma, &s);
1220 assert!(!r.failed);
1221 assert!(r.fi_fiber < 1.0);
1222 assert!(r.fi_matrix < 1.0);
1223 assert!(r.fi_shear < 1.0);
1224 }
1225
1226 #[test]
1227 fn test_max_stress_fiber_tension_failure() {
1228 let s = PlyStrength::cfrp_typical();
1229 let sigma = [2000.0e6, 0.0, 0.0]; let r = max_stress_failure(sigma, &s);
1231 assert!(r.failed);
1232 assert!(r.fi_fiber >= 1.0);
1233 }
1234
1235 #[test]
1236 fn test_max_stress_fiber_compression_failure() {
1237 let s = PlyStrength::cfrp_typical();
1238 let sigma = [-1500.0e6, 0.0, 0.0]; let r = max_stress_failure(sigma, &s);
1240 assert!(r.failed);
1241 assert!(r.fi_fiber >= 1.0);
1242 }
1243
1244 #[test]
1245 fn test_max_stress_matrix_tension_failure() {
1246 let s = PlyStrength::cfrp_typical();
1247 let sigma = [0.0, 60.0e6, 0.0]; let r = max_stress_failure(sigma, &s);
1249 assert!(r.failed);
1250 assert!(r.fi_matrix >= 1.0);
1251 }
1252
1253 #[test]
1254 fn test_max_stress_shear_failure() {
1255 let s = PlyStrength::cfrp_typical();
1256 let sigma = [0.0, 0.0, 80.0e6]; let r = max_stress_failure(sigma, &s);
1258 assert!(r.failed);
1259 assert!(r.fi_shear >= 1.0);
1260 }
1261
1262 #[test]
1265 fn test_tsai_wu_no_failure() {
1266 let s = PlyStrength::cfrp_typical();
1267 let sigma = [100.0e6, 5.0e6, 5.0e6];
1268 let fi = tsai_wu_failure(sigma, &s);
1269 assert!(fi < 1.0, "Tsai-Wu FI = {fi} should be < 1");
1270 }
1271
1272 #[test]
1273 fn test_tsai_wu_failure() {
1274 let s = PlyStrength::cfrp_typical();
1275 let sigma = [0.0, 60.0e6, 0.0]; let fi = tsai_wu_failure(sigma, &s);
1277 assert!(fi >= 1.0, "Tsai-Wu FI = {fi} should be >= 1");
1278 }
1279
1280 #[test]
1281 fn test_tsai_wu_zero_stress() {
1282 let s = PlyStrength::cfrp_typical();
1283 let sigma = [0.0, 0.0, 0.0];
1284 let fi = tsai_wu_failure(sigma, &s);
1285 assert!(
1286 fi.abs() < 1e-10,
1287 "zero stress should give zero FI, got {fi}"
1288 );
1289 }
1290
1291 #[test]
1294 fn test_hashin_no_failure() {
1295 let s = PlyStrength::cfrp_typical();
1296 let sigma = [100.0e6, 5.0e6, 5.0e6];
1297 let r = hashin_failure(sigma, &s);
1298 assert!(!r.failed);
1299 }
1300
1301 #[test]
1302 fn test_hashin_fiber_tension() {
1303 let s = PlyStrength::cfrp_typical();
1304 let sigma = [2000.0e6, 0.0, 0.0];
1305 let r = hashin_failure(sigma, &s);
1306 assert!(r.failed);
1307 assert!(r.fiber_tension >= 1.0);
1308 }
1309
1310 #[test]
1311 fn test_hashin_fiber_compression() {
1312 let s = PlyStrength::cfrp_typical();
1313 let sigma = [-1500.0e6, 0.0, 0.0];
1314 let r = hashin_failure(sigma, &s);
1315 assert!(r.failed);
1316 assert!(r.fiber_compression >= 1.0);
1317 }
1318
1319 #[test]
1320 fn test_hashin_matrix_tension() {
1321 let s = PlyStrength::cfrp_typical();
1322 let sigma = [0.0, 60.0e6, 0.0];
1323 let r = hashin_failure(sigma, &s);
1324 assert!(r.failed);
1325 assert!(r.matrix_tension >= 1.0);
1326 }
1327
1328 #[test]
1329 fn test_hashin_matrix_compression() {
1330 let s = PlyStrength::cfrp_typical();
1331 let sigma = [0.0, -300.0e6, 0.0];
1332 let r = hashin_failure(sigma, &s);
1333 assert!(r.failed);
1334 assert!(r.matrix_compression >= 1.0);
1335 }
1336
1337 #[test]
1340 fn test_puck_no_failure() {
1341 let s = PlyStrength::cfrp_typical();
1342 let sigma = [100.0e6, 5.0e6, 5.0e6];
1343 let r = puck_failure(sigma, &s);
1344 assert!(!r.failed);
1345 }
1346
1347 #[test]
1348 fn test_puck_fiber_failure() {
1349 let s = PlyStrength::cfrp_typical();
1350 let sigma = [2000.0e6, 0.0, 0.0];
1351 let r = puck_failure(sigma, &s);
1352 assert!(r.failed);
1353 assert!(r.ff_effort >= 1.0);
1354 }
1355
1356 #[test]
1357 fn test_puck_iff_tension() {
1358 let s = PlyStrength::cfrp_typical();
1359 let sigma = [0.0, 60.0e6, 0.0];
1360 let r = puck_failure(sigma, &s);
1361 assert!(r.failed);
1362 assert!(r.iff_effort >= 1.0);
1363 }
1364
1365 #[test]
1366 fn test_puck_iff_compression() {
1367 let s = PlyStrength::cfrp_typical();
1368 let sigma = [0.0, -300.0e6, 50.0e6];
1369 let r = puck_failure(sigma, &s);
1370 assert!(r.iff_effort > 0.0);
1372 }
1373
1374 #[test]
1377 fn test_ply_by_ply_stress_single_ply() {
1378 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1379 let mut lam = Laminate::new();
1380 lam.add_ply(ply, 0.0);
1381
1382 let strain = [0.001, 0.0, 0.0];
1383 let curv = [0.0, 0.0, 0.0];
1384 let results = ply_by_ply_stress(&lam, strain, curv);
1385
1386 assert_eq!(results.len(), 1);
1387 assert!(
1388 results[0].stress[0] > 0.0,
1389 "tensile stress in fiber direction"
1390 );
1391 }
1392
1393 #[test]
1394 fn test_ply_by_ply_stress_symmetric_laminate() {
1395 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1396 let mut lam = Laminate::new();
1397 lam.add_ply(ply, 0.0);
1398 lam.add_ply(ply, 90.0);
1399 lam.add_ply(ply, 0.0);
1400
1401 let strain = [0.001, 0.0, 0.0];
1402 let curv = [0.0, 0.0, 0.0];
1403 let results = ply_by_ply_stress(&lam, strain, curv);
1404
1405 assert_eq!(results.len(), 3);
1406 assert!(results[0].stress[0] > 0.0);
1408 assert!(results[2].stress[0] > 0.0);
1409 }
1410
1411 #[test]
1412 fn test_ply_by_ply_stress_with_curvature() {
1413 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1414 let mut lam = Laminate::new();
1415 lam.add_ply(ply, 0.0);
1416 lam.add_ply(ply, 0.0);
1417
1418 let strain = [0.0, 0.0, 0.0];
1419 let curv = [10.0, 0.0, 0.0]; let results = ply_by_ply_stress(&lam, strain, curv);
1421
1422 let s_top = results[1].stress[0];
1424 let s_bot = results[0].stress[0];
1425 assert!(
1426 s_top * s_bot < 0.0 || (s_top.abs() < 1e-3 && s_bot.abs() < 1e-3),
1427 "bending should cause opposite signs: top={s_top}, bot={s_bot}"
1428 );
1429 }
1430
1431 #[test]
1434 fn test_progressive_failure_returns_result() {
1435 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1436 let mut lam = Laminate::new();
1437 lam.add_ply(ply, 0.0);
1438 lam.add_ply(ply, 90.0);
1439 lam.add_ply(ply, 0.0);
1440
1441 let s = PlyStrength::cfrp_typical();
1442 let base_strain = [0.01, 0.0, 0.0]; let result = progressive_failure_analysis(&lam, base_strain, &s, 2.0, 100);
1445 assert!(result.first_ply_failure_load > 0.0);
1446 assert!(result.first_ply_failure_load <= result.ultimate_load);
1447 assert!(result.n_failed_plies > 0);
1448 }
1449
1450 #[test]
1451 fn test_progressive_failure_90deg_fails_first() {
1452 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1453 let mut lam = Laminate::new();
1454 lam.add_ply(ply, 0.0);
1455 lam.add_ply(ply, 90.0); lam.add_ply(ply, 0.0);
1457
1458 let s = PlyStrength::cfrp_typical();
1459 let base_strain = [0.01, 0.0, 0.0];
1460
1461 let result = progressive_failure_analysis(&lam, base_strain, &s, 2.0, 200);
1462 assert_eq!(
1464 result.first_failed_ply, 1,
1465 "90-degree ply should fail first"
1466 );
1467 }
1468
1469 #[test]
1472 fn test_thermal_strain() {
1473 let alpha = [1.0e-6, 30.0e-6]; let dt = -150.0; let eps = thermal_strain(alpha, dt);
1476 assert!((eps[0] - (-150.0e-6)).abs() < 1e-12);
1477 assert!((eps[1] - (-4500.0e-6)).abs() < 1e-12);
1478 assert!(eps[2].abs() < 1e-15);
1479 }
1480
1481 #[test]
1482 fn test_thermal_residual_single_ply_zero() {
1483 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1485 let mut lam = Laminate::new();
1486 lam.add_ply(ply, 0.0);
1487
1488 let alpha = [1.0e-6, 30.0e-6];
1489 let residuals = thermal_residual_stresses(&lam, alpha, -150.0);
1490 assert_eq!(residuals.len(), 1);
1491 for s in &residuals[0] {
1493 assert!(s.abs() < 1.0, "single ply residual should be ~0, got {s}");
1494 }
1495 }
1496
1497 #[test]
1498 fn test_thermal_residual_crossply_has_stress() {
1499 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1501 let mut lam = Laminate::new();
1502 lam.add_ply(ply, 0.0);
1503 lam.add_ply(ply, 90.0);
1504
1505 let alpha = [1.0e-6, 30.0e-6];
1506 let residuals = thermal_residual_stresses(&lam, alpha, -150.0);
1507 assert_eq!(residuals.len(), 2);
1508 let has_nonzero = residuals
1510 .iter()
1511 .any(|s| s[0].abs() > 1.0 || s[1].abs() > 1.0);
1512 assert!(has_nonzero, "crossply should have residual stresses");
1513 }
1514
1515 #[test]
1518 fn test_interlaminar_shear_single_ply() {
1519 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1520 let mut lam = Laminate::new();
1521 lam.add_ply(ply, 0.0);
1522
1523 let strain = [0.001, 0.0, 0.0];
1524 let curv = [0.0, 0.0, 0.0];
1525 let shear = interlaminar_shear_estimate(&lam, strain, curv);
1526 assert!(shear.is_empty(), "single ply has no interfaces");
1527 }
1528
1529 #[test]
1530 fn test_interlaminar_shear_two_plies() {
1531 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1532 let mut lam = Laminate::new();
1533 lam.add_ply(ply, 0.0);
1534 lam.add_ply(ply, 90.0);
1535
1536 let strain = [0.001, 0.0, 0.0];
1537 let curv = [0.0, 0.0, 0.0];
1538 let shear = interlaminar_shear_estimate(&lam, strain, curv);
1539 assert_eq!(shear.len(), 1);
1540 }
1541
1542 #[test]
1543 fn test_interlaminar_shear_three_plies() {
1544 let ply = OrthotropicPly::new(140.0e9, 10.0e9, 5.0e9, 0.3, 0.125e-3);
1545 let mut lam = Laminate::new();
1546 lam.add_ply(ply, 0.0);
1547 lam.add_ply(ply, 90.0);
1548 lam.add_ply(ply, 0.0);
1549
1550 let strain = [0.001, 0.0, 0.0];
1551 let curv = [10.0, 0.0, 0.0]; let shear = interlaminar_shear_estimate(&lam, strain, curv);
1553 assert_eq!(shear.len(), 2);
1554 }
1555
1556 #[test]
1559 fn test_ply_strength_cfrp() {
1560 let s = PlyStrength::cfrp_typical();
1561 assert!(s.xt > s.yt, "fiber strength should exceed transverse");
1562 assert!(
1563 s.xc > s.yc,
1564 "fiber compression should exceed transverse compression"
1565 );
1566 }
1567
1568 #[test]
1569 fn test_ply_strength_gfrp() {
1570 let s = PlyStrength::gfrp_typical();
1571 assert!(s.xt > 0.0);
1572 assert!(s.s12 > 0.0);
1573 }
1574
1575 #[test]
1576 fn test_ply_strength_new() {
1577 let s = PlyStrength::new(100.0, 80.0, 10.0, 50.0, 20.0);
1578 assert_eq!(s.xt, 100.0);
1579 assert_eq!(s.yc, 50.0);
1580 }
1581
1582 #[test]
1585 fn test_transform_strain_zero_angle() {
1586 let strain = [0.001, 0.002, 0.0005];
1587 let transformed = transform_strain_to_material(strain, 0.0);
1588 for i in 0..3 {
1589 assert!((transformed[i] - strain[i]).abs() < 1e-12);
1590 }
1591 }
1592
1593 #[test]
1594 fn test_transform_strain_90deg() {
1595 let strain = [0.001, 0.002, 0.0];
1596 let transformed = transform_strain_to_material(strain, 90.0);
1597 assert!((transformed[0] - 0.002).abs() < 1e-10);
1599 assert!((transformed[1] - 0.001).abs() < 1e-10);
1600 }
1601}