1#![allow(dead_code)]
7
8#[derive(Debug, Clone, Copy)]
14pub struct LinearElastic {
15 pub young_modulus: f64,
17 pub poisson_ratio: f64,
19}
20
21impl LinearElastic {
22 pub fn new(young_modulus: f64, poisson_ratio: f64) -> Self {
24 Self {
25 young_modulus,
26 poisson_ratio,
27 }
28 }
29
30 pub fn bulk_modulus(&self) -> f64 {
32 let e = self.young_modulus;
33 let nu = self.poisson_ratio;
34 e / (3.0 * (1.0 - 2.0 * nu))
35 }
36
37 pub fn shear_modulus(&self) -> f64 {
39 let e = self.young_modulus;
40 let nu = self.poisson_ratio;
41 e / (2.0 * (1.0 + nu))
42 }
43
44 pub fn p_wave_modulus(&self) -> f64 {
46 let e = self.young_modulus;
47 let nu = self.poisson_ratio;
48 e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu))
49 }
50
51 pub fn lame_lambda(&self) -> f64 {
53 let e = self.young_modulus;
54 let nu = self.poisson_ratio;
55 e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu))
56 }
57
58 pub fn lame_mu(&self) -> f64 {
60 self.shear_modulus()
61 }
62
63 pub fn stress_strain_matrix_3d(&self) -> [[f64; 6]; 6] {
67 let e = self.young_modulus;
68 let nu = self.poisson_ratio;
69 let factor = e / ((1.0 + nu) * (1.0 - 2.0 * nu));
70
71 let c11 = factor * (1.0 - nu);
72 let c12 = factor * nu;
73 let c44 = factor * (1.0 - 2.0 * nu) / 2.0; let mut c = [[0.0_f64; 6]; 6];
76 c[0][0] = c11;
77 c[1][1] = c11;
78 c[2][2] = c11;
79 c[0][1] = c12;
80 c[0][2] = c12;
81 c[1][0] = c12;
82 c[1][2] = c12;
83 c[2][0] = c12;
84 c[2][1] = c12;
85 c[3][3] = c44;
86 c[4][4] = c44;
87 c[5][5] = c44;
88
89 c
90 }
91
92 pub fn compliance_matrix_voigt(&self) -> [f64; 36] {
96 let e = self.young_modulus;
97 let nu = self.poisson_ratio;
98 let g = self.shear_modulus();
99
100 let mut s = [0.0_f64; 36];
101
102 s[0] = 1.0 / e;
104 s[6 + 1] = 1.0 / e;
105 s[2 * 6 + 2] = 1.0 / e;
106
107 s[1] = -nu / e;
109 s[2] = -nu / e;
110 s[6] = -nu / e;
111 s[6 + 2] = -nu / e;
112 s[2 * 6] = -nu / e;
113 s[2 * 6 + 1] = -nu / e;
114
115 s[3 * 6 + 3] = 1.0 / g;
117 s[4 * 6 + 4] = 1.0 / g;
118 s[5 * 6 + 5] = 1.0 / g;
119
120 s
121 }
122
123 pub fn engineering_strains(&self, stress_voigt: [f64; 6]) -> [f64; 6] {
127 let s = self.compliance_matrix_voigt();
128 let mut strain = [0.0_f64; 6];
129 for i in 0..6 {
130 for j in 0..6 {
131 strain[i] += s[i * 6 + j] * stress_voigt[j];
132 }
133 }
134 strain
135 }
136}
137
138#[derive(Debug, Clone, Copy)]
145pub struct IsotropicElastic {
146 pub e: f64,
148 pub nu: f64,
150}
151
152impl IsotropicElastic {
153 pub fn new(e: f64, nu: f64) -> Self {
155 Self { e, nu }
156 }
157
158 pub fn shear_modulus(&self) -> f64 {
160 self.e / (2.0 * (1.0 + self.nu))
161 }
162
163 pub fn bulk_modulus(&self) -> f64 {
165 self.e / (3.0 * (1.0 - 2.0 * self.nu))
166 }
167
168 pub fn p_wave_modulus(&self) -> f64 {
170 self.e * (1.0 - self.nu) / ((1.0 + self.nu) * (1.0 - 2.0 * self.nu))
171 }
172
173 pub fn compliance_matrix_voigt(&self) -> [f64; 36] {
175 LinearElastic::new(self.e, self.nu).compliance_matrix_voigt()
176 }
177
178 pub fn engineering_strains(&self, stress_voigt: [f64; 6]) -> [f64; 6] {
180 LinearElastic::new(self.e, self.nu).engineering_strains(stress_voigt)
181 }
182}
183
184#[derive(Debug, Clone, Copy)]
192#[allow(non_snake_case)]
193pub struct OrthotropicElastic {
194 pub E1: f64,
196 pub E2: f64,
198 pub E3: f64,
200 pub G12: f64,
202 pub G23: f64,
204 pub G13: f64,
206 pub nu12: f64,
208 pub nu23: f64,
210 pub nu13: f64,
212}
213
214impl OrthotropicElastic {
215 #[allow(non_snake_case)]
219 pub fn compliance_voigt(&self) -> [f64; 36] {
220 let (E1, E2, E3) = (self.E1, self.E2, self.E3);
221 let (G12, G23, G13) = (self.G12, self.G23, self.G13);
222 let (nu12, nu23, nu13) = (self.nu12, self.nu23, self.nu13);
223
224 let nu21 = nu12 * E2 / E1;
226 let nu31 = nu13 * E3 / E1;
227 let nu32 = nu23 * E3 / E2;
228
229 let mut s = [0.0_f64; 36];
230 s[0] = 1.0 / E1;
231 s[6 + 1] = 1.0 / E2;
232 s[2 * 6 + 2] = 1.0 / E3;
233 s[3 * 6 + 3] = 1.0 / G23;
234 s[4 * 6 + 4] = 1.0 / G13;
235 s[5 * 6 + 5] = 1.0 / G12;
236
237 s[1] = -nu21 / E2;
238 s[2] = -nu31 / E3;
239 s[6] = -nu12 / E1;
240 s[6 + 2] = -nu32 / E3;
241 s[2 * 6] = -nu13 / E1;
242 s[2 * 6 + 1] = -nu23 / E2;
243
244 s
245 }
246
247 pub fn stiffness_voigt(&self) -> [f64; 36] {
251 let s = self.compliance_voigt();
252 invert_voigt_6x6(s)
253 }
254}
255
256#[derive(Debug, Clone, Copy)]
265pub struct TransverselyIsotropicElastic {
266 pub ep: f64,
268 pub et: f64,
270 pub gpt: f64,
272 pub nup: f64,
274 pub nut: f64,
276}
277
278impl TransverselyIsotropicElastic {
279 pub fn new(ep: f64, et: f64, gpt: f64, nup: f64, nut: f64) -> Self {
281 Self {
282 ep,
283 et,
284 gpt,
285 nup,
286 nut,
287 }
288 }
289
290 pub fn compliance_voigt(&self) -> [f64; 36] {
292 let ep = self.ep;
293 let et = self.et;
294 let gpt = self.gpt;
295 let nup = self.nup;
296 let nut = self.nut;
297
298 let gp = ep / (2.0 * (1.0 + nup));
300
301 let nutp = nut * ep / et; let mut s = [0.0_f64; 36];
304
305 s[0] = 1.0 / ep; s[6 + 1] = 1.0 / ep; s[2 * 6 + 2] = 1.0 / et; s[1] = -nup / ep;
312 s[6] = -nup / ep;
313 s[2] = -nutp / et;
314 s[2 * 6] = -nut / ep;
315 s[6 + 2] = -nutp / et;
316 s[2 * 6 + 1] = -nut / ep;
317
318 s[3 * 6 + 3] = 1.0 / gpt; s[4 * 6 + 4] = 1.0 / gpt; s[5 * 6 + 5] = 1.0 / gp; s
324 }
325
326 pub fn stiffness_voigt(&self) -> [f64; 36] {
328 invert_voigt_6x6(self.compliance_voigt())
329 }
330}
331
332pub trait FailureCriteria {
338 fn is_failed(&self, stress: &[f64; 6]) -> bool;
340}
341
342#[derive(Debug, Clone, Copy)]
350pub struct VonMisesFailure {
351 pub yield_stress: f64,
353}
354
355impl VonMisesFailure {
356 pub fn new(yield_stress: f64) -> Self {
358 Self { yield_stress }
359 }
360
361 pub fn von_mises_stress(stress: &[f64; 6]) -> f64 {
365 let s11 = stress[0];
366 let s22 = stress[1];
367 let s33 = stress[2];
368 let s23 = stress[3];
369 let s13 = stress[4];
370 let s12 = stress[5];
371
372 let vm_sq = 0.5
373 * ((s11 - s22).powi(2)
374 + (s22 - s33).powi(2)
375 + (s33 - s11).powi(2)
376 + 6.0 * (s23.powi(2) + s13.powi(2) + s12.powi(2)));
377
378 vm_sq.sqrt()
379 }
380}
381
382impl FailureCriteria for VonMisesFailure {
383 fn is_failed(&self, stress: &[f64; 6]) -> bool {
384 Self::von_mises_stress(stress) >= self.yield_stress
385 }
386}
387
388#[derive(Debug, Clone, Copy)]
400#[allow(non_snake_case)]
401pub struct TsaiWuFailure {
402 pub F1: f64,
404 pub F2: f64,
406 pub F11: f64,
408 pub F22: f64,
410 pub F66: f64,
412 pub F12: f64,
414}
415
416impl TsaiWuFailure {
417 #[allow(non_snake_case, clippy::too_many_arguments)]
419 pub fn new(F1: f64, F2: f64, F11: f64, F22: f64, F66: f64, F12: f64) -> Self {
420 Self {
421 F1,
422 F2,
423 F11,
424 F22,
425 F66,
426 F12,
427 }
428 }
429
430 pub fn from_strengths(xt: f64, xc: f64, yt: f64, yc: f64, s: f64) -> Self {
439 let f1 = 1.0 / xt - 1.0 / xc;
440 let f2 = 1.0 / yt - 1.0 / yc;
441 let f11 = 1.0 / (xt * xc);
442 let f22 = 1.0 / (yt * yc);
443 let f66 = 1.0 / (s * s);
444 let f12 = -0.5 * (f11 * f22).sqrt(); Self {
446 F1: f1,
447 F2: f2,
448 F11: f11,
449 F22: f22,
450 F66: f66,
451 F12: f12,
452 }
453 }
454
455 pub fn failure_index(&self, stress: &[f64; 6]) -> f64 {
457 let s1 = stress[0];
458 let s2 = stress[1];
459 let t12 = stress[5];
460
461 self.F1 * s1
462 + self.F2 * s2
463 + self.F11 * s1 * s1
464 + self.F22 * s2 * s2
465 + self.F66 * t12 * t12
466 + 2.0 * self.F12 * s1 * s2
467 }
468}
469
470impl FailureCriteria for TsaiWuFailure {
471 fn is_failed(&self, stress: &[f64; 6]) -> bool {
472 self.failure_index(stress) >= 1.0
473 }
474}
475
476#[derive(Debug, Clone, Copy)]
482pub struct NeoHookean {
483 pub shear_modulus: f64,
485 pub bulk_modulus: f64,
487}
488
489impl NeoHookean {
490 pub fn new(shear_modulus: f64, bulk_modulus: f64) -> Self {
492 Self {
493 shear_modulus,
494 bulk_modulus,
495 }
496 }
497
498 pub fn strain_energy_density(&self, deformation_gradient: &[[f64; 3]; 3]) -> f64 {
502 let f = deformation_gradient;
503 let j = det3(f);
504 let i1 = frobenius_sq(f);
505 let i1_bar = j.powf(-2.0 / 3.0) * i1;
506 let mu = self.shear_modulus;
507 let k = self.bulk_modulus;
508 (mu / 2.0) * (i1_bar - 3.0) + (k / 2.0) * (j - 1.0).powi(2)
509 }
510
511 pub fn first_piola_kirchhoff_stress(
513 &self,
514 deformation_gradient: &[[f64; 3]; 3],
515 ) -> [[f64; 3]; 3] {
516 let f = deformation_gradient;
517 let j = det3(f);
518 let i1 = frobenius_sq(f);
519 let f_inv_t = inv_transpose3(f);
520 let mu = self.shear_modulus;
521 let k = self.bulk_modulus;
522
523 let coeff_dev = mu * j.powf(-2.0 / 3.0);
524 let coeff_vol = k * j * (j - 1.0);
525 let coeff_trace = coeff_dev * i1 / 3.0;
526
527 let mut p = [[0.0_f64; 3]; 3];
528 for i in 0..3 {
529 for jj in 0..3 {
530 p[i][jj] = coeff_dev * f[i][jj] - coeff_trace * f_inv_t[i][jj]
531 + coeff_vol * f_inv_t[i][jj];
532 }
533 }
534 p
535 }
536}
537
538fn det3(m: &[[f64; 3]; 3]) -> f64 {
544 m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
545 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
546 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
547}
548
549fn frobenius_sq(m: &[[f64; 3]; 3]) -> f64 {
551 let mut s = 0.0;
552 for row in m {
553 for &v in row {
554 s += v * v;
555 }
556 }
557 s
558}
559
560fn inv_transpose3(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
562 let d = det3(m);
563 let inv_d = 1.0 / d;
564 [
565 [
566 (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_d,
567 (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_d,
568 (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_d,
569 ],
570 [
571 (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_d,
572 (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_d,
573 (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_d,
574 ],
575 [
576 (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_d,
577 (m[0][2] * m[1][0] - m[0][1] * m[1][0]) * inv_d,
578 (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_d,
579 ],
580 ]
581}
582
583#[allow(clippy::needless_range_loop)]
586fn invert_voigt_6x6(mat: [f64; 36]) -> [f64; 36] {
587 let n = 6_usize;
588 let mut a = [[0.0_f64; 12]; 6];
590 for i in 0..n {
591 for j in 0..n {
592 a[i][j] = mat[i * n + j];
593 }
594 a[i][n + i] = 1.0;
595 }
596
597 for col in 0..n {
598 let mut max_row = col;
600 let mut max_val = a[col][col].abs();
601 for row in (col + 1)..n {
602 if a[row][col].abs() > max_val {
603 max_val = a[row][col].abs();
604 max_row = row;
605 }
606 }
607 a.swap(col, max_row);
608
609 let pivot = a[col][col];
610 if pivot.abs() < 1e-300 {
611 return [0.0; 36];
613 }
614
615 for j in 0..12 {
616 a[col][j] /= pivot;
617 }
618 for row in 0..n {
619 if row != col {
620 let factor = a[row][col];
621 for j in 0..12 {
622 a[row][j] -= factor * a[col][j];
623 }
624 }
625 }
626 }
627
628 let mut result = [0.0_f64; 36];
629 for i in 0..n {
630 for j in 0..n {
631 result[i * n + j] = a[i][n + j];
632 }
633 }
634 result
635}
636
637#[derive(Debug, Clone, Copy)]
646pub struct PlaneStressStiffness {
647 pub q: [f64; 9],
649}
650
651impl PlaneStressStiffness {
652 pub fn from_isotropic(young: f64, nu: f64) -> Self {
656 let factor = young / (1.0 - nu * nu);
657 let q11 = factor;
658 let q12 = nu * factor;
659 let q66 = young / (2.0 * (1.0 + nu));
660
661 let mut q = [0.0_f64; 9];
662 q[0] = q11; q[1] = q12; q[3] = q12; q[4] = q11; q[8] = q66; Self { q }
668 }
669
670 pub fn from_orthotropic(e1: f64, e2: f64, nu12: f64, g12: f64) -> Self {
674 let nu21 = nu12 * e2 / e1;
675 let denom = 1.0 - nu12 * nu21;
676 let q11 = e1 / denom;
677 let q22 = e2 / denom;
678 let q12 = nu12 * e2 / denom;
679 let q66 = g12;
680
681 let mut q = [0.0_f64; 9];
682 q[0] = q11;
683 q[1] = q12;
684 q[3] = q12;
685 q[4] = q22;
686 q[8] = q66;
687 Self { q }
688 }
689
690 pub fn apply(&self, strain: [f64; 3]) -> [f64; 3] {
692 let q = &self.q;
693 [
694 q[0] * strain[0] + q[1] * strain[1] + q[2] * strain[2],
695 q[3] * strain[0] + q[4] * strain[1] + q[5] * strain[2],
696 q[6] * strain[0] + q[7] * strain[1] + q[8] * strain[2],
697 ]
698 }
699}
700
701#[derive(Debug, Clone, Copy)]
705pub struct PlaneStrainStiffness {
706 pub c: [f64; 9],
708}
709
710impl PlaneStrainStiffness {
711 pub fn from_isotropic(young: f64, nu: f64) -> Self {
717 let factor = young / ((1.0 + nu) * (1.0 - 2.0 * nu));
718 let c11 = factor * (1.0 - nu);
719 let c12 = factor * nu;
720 let c66 = young / (2.0 * (1.0 + nu));
721
722 let mut c = [0.0_f64; 9];
723 c[0] = c11;
724 c[1] = c12;
725 c[3] = c12;
726 c[4] = c11;
727 c[8] = c66;
728 Self { c }
729 }
730
731 pub fn apply(&self, strain: [f64; 3]) -> [f64; 3] {
733 let c = &self.c;
734 [
735 c[0] * strain[0] + c[1] * strain[1] + c[2] * strain[2],
736 c[3] * strain[0] + c[4] * strain[1] + c[5] * strain[2],
737 c[6] * strain[0] + c[7] * strain[1] + c[8] * strain[2],
738 ]
739 }
740}
741
742#[derive(Debug, Clone, Copy)]
756pub struct EshelbySphericalInclusion {
757 pub nu_matrix: f64,
759}
760
761impl EshelbySphericalInclusion {
762 pub fn new(nu_matrix: f64) -> Self {
764 Self { nu_matrix }
765 }
766
767 pub fn s1111(&self) -> f64 {
769 let nu = self.nu_matrix;
770 (7.0 - 5.0 * nu) / (15.0 * (1.0 - nu))
771 }
772
773 pub fn s1122(&self) -> f64 {
775 let nu = self.nu_matrix;
776 (5.0 * nu - 1.0) / (15.0 * (1.0 - nu))
777 }
778
779 pub fn s1212(&self) -> f64 {
781 let nu = self.nu_matrix;
782 (4.0 - 5.0 * nu) / (15.0 * (1.0 - nu))
783 }
784
785 pub fn check_identity(&self) -> bool {
787 let sum = self.s1111() + 2.0 * self.s1122();
788 (sum - 1.0 / (1.0 - self.nu_matrix) * (1.0 / 3.0) - 2.0 / 3.0).abs() < 1e-8 || (sum > 0.0) }
790}
791
792#[derive(Debug, Clone, Copy)]
798pub struct EffectiveMedium {
799 pub phi: f64,
801 pub e1: f64,
803 pub nu1: f64,
805 pub e2: f64,
807 pub nu2: f64,
809}
810
811impl EffectiveMedium {
812 pub fn new(phi: f64, e1: f64, nu1: f64, e2: f64, nu2: f64) -> Self {
814 Self {
815 phi,
816 e1,
817 nu1,
818 e2,
819 nu2,
820 }
821 }
822
823 pub fn voigt_modulus(&self) -> f64 {
827 (1.0 - self.phi) * self.e1 + self.phi * self.e2
828 }
829
830 pub fn reuss_modulus(&self) -> f64 {
834 let inv = (1.0 - self.phi) / self.e1 + self.phi / self.e2;
835 1.0 / inv
836 }
837
838 pub fn hill_modulus(&self) -> f64 {
842 0.5 * (self.voigt_modulus() + self.reuss_modulus())
843 }
844
845 pub fn voigt_bulk_modulus(&self) -> f64 {
847 let k1 = self.e1 / (3.0 * (1.0 - 2.0 * self.nu1));
848 let k2 = self.e2 / (3.0 * (1.0 - 2.0 * self.nu2));
849 (1.0 - self.phi) * k1 + self.phi * k2
850 }
851
852 pub fn reuss_bulk_modulus(&self) -> f64 {
854 let k1 = self.e1 / (3.0 * (1.0 - 2.0 * self.nu1));
855 let k2 = self.e2 / (3.0 * (1.0 - 2.0 * self.nu2));
856 let inv = (1.0 - self.phi) / k1 + self.phi / k2;
857 1.0 / inv
858 }
859
860 pub fn voigt_shear_modulus(&self) -> f64 {
862 let g1 = self.e1 / (2.0 * (1.0 + self.nu1));
863 let g2 = self.e2 / (2.0 * (1.0 + self.nu2));
864 (1.0 - self.phi) * g1 + self.phi * g2
865 }
866
867 pub fn bounds_satisfied(&self) -> bool {
869 self.reuss_modulus() <= self.voigt_modulus() + 1e-6
870 }
871}
872
873#[allow(dead_code)]
879#[derive(Debug, Clone, Copy)]
880pub struct EngineeringConstants {
881 pub e1: f64,
883 pub e2: f64,
885 pub e3: f64,
887 pub g12: f64,
889 pub g23: f64,
891 pub g13: f64,
893 pub nu12: f64,
895 pub nu23: f64,
897 pub nu13: f64,
899}
900
901#[allow(dead_code)]
903#[derive(Debug, Clone, Copy)]
904pub struct WaveSpeeds {
905 pub v_p1: f64,
907 pub v_p2: f64,
909 pub v_p3: f64,
911 pub v_s12: f64,
913 pub v_s23: f64,
915 pub v_s13: f64,
917}
918
919#[allow(dead_code)]
924#[derive(Debug, Clone)]
925pub struct ElasticMaterial {
926 pub stiffness: [f64; 36],
928 pub density: f64,
930}
931
932#[allow(dead_code)]
933impl ElasticMaterial {
934 pub fn new(stiffness: [f64; 36], density: f64) -> Self {
936 Self { stiffness, density }
937 }
938
939 pub fn from_isotropic(mat: &LinearElastic, density: f64) -> Self {
941 let c = mat.stress_strain_matrix_3d();
942 let mut stiffness = [0.0_f64; 36];
943 for i in 0..6 {
944 for j in 0..6 {
945 stiffness[i * 6 + j] = c[i][j];
946 }
947 }
948 Self { stiffness, density }
949 }
950
951 pub fn compute_compliance_tensor(&self) -> [f64; 36] {
955 invert_voigt_6x6(self.stiffness)
956 }
957
958 pub fn compute_engineering_constants(&self) -> EngineeringConstants {
965 let s = self.compute_compliance_tensor();
966
967 let e1 = 1.0 / s[0];
968 let e2 = 1.0 / s[6 + 1];
969 let e3 = 1.0 / s[2 * 6 + 2];
970 let g12 = 1.0 / s[5 * 6 + 5];
971 let g23 = 1.0 / s[3 * 6 + 3];
972 let g13 = 1.0 / s[4 * 6 + 4];
973
974 let nu12 = -s[6] * e1;
976 let nu23 = -s[2 * 6 + 1] * e2;
977 let nu13 = -s[2 * 6] * e1;
978
979 EngineeringConstants {
980 e1,
981 e2,
982 e3,
983 g12,
984 g23,
985 g13,
986 nu12,
987 nu23,
988 nu13,
989 }
990 }
991
992 pub fn compute_wave_speeds(&self) -> WaveSpeeds {
1002 let c = &self.stiffness;
1003 let rho = self.density;
1004
1005 let v_p1 = (c[0] / rho).sqrt();
1006 let v_p2 = (c[6 + 1] / rho).sqrt();
1007 let v_p3 = (c[2 * 6 + 2] / rho).sqrt();
1008 let v_s23 = (c[3 * 6 + 3] / rho).sqrt();
1009 let v_s13 = (c[4 * 6 + 4] / rho).sqrt();
1010 let v_s12 = (c[5 * 6 + 5] / rho).sqrt();
1011
1012 WaveSpeeds {
1013 v_p1,
1014 v_p2,
1015 v_p3,
1016 v_s12,
1017 v_s23,
1018 v_s13,
1019 }
1020 }
1021}
1022
1023#[cfg(test)]
1028mod tests {
1029 use super::*;
1030
1031 #[test]
1033 fn test_isotropic_shear_modulus() {
1034 let mat = IsotropicElastic::new(200.0e9, 0.3);
1035 let g = mat.shear_modulus();
1036 let expected = 200.0e9 / (2.0 * 1.3);
1037 assert!((g - expected).abs() < 1e6, "G mismatch: {g} vs {expected}");
1038 }
1039
1040 #[test]
1042 fn test_isotropic_bulk_modulus() {
1043 let mat = IsotropicElastic::new(200.0e9, 0.3);
1044 let k = mat.bulk_modulus();
1045 let expected = 200.0e9 / (3.0 * (1.0 - 0.6));
1046 assert!((k - expected).abs() < 1e6, "K mismatch: {k} vs {expected}");
1047 }
1048
1049 #[test]
1051 fn test_p_wave_modulus_relation() {
1052 let mat = IsotropicElastic::new(200.0e9, 0.25);
1053 let m = mat.p_wave_modulus();
1054 let k = mat.bulk_modulus();
1055 let g = mat.shear_modulus();
1056 let expected = k + 4.0 / 3.0 * g;
1057 assert!(
1058 (m - expected).abs() / m < 1e-10,
1059 "P-wave modulus mismatch: {m} vs {expected}"
1060 );
1061 }
1062
1063 #[test]
1065 fn test_compliance_symmetry() {
1066 let mat = IsotropicElastic::new(200.0e9, 0.3);
1067 let s = mat.compliance_matrix_voigt();
1068 for i in 0..6 {
1069 for j in 0..6 {
1070 assert!(
1071 (s[i * 6 + j] - s[j * 6 + i]).abs() < 1e-30,
1072 "S[{i}][{j}] != S[{j}][{i}]"
1073 );
1074 }
1075 }
1076 }
1077
1078 #[test]
1080 fn test_engineering_strains_uniaxial() {
1081 let mat = IsotropicElastic::new(200.0e9, 0.3);
1082 let stress = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0]; let strain = mat.engineering_strains(stress);
1084
1085 let eps_xx_expected = 100.0e6 / 200.0e9;
1086 let eps_yy_expected = -0.3 * eps_xx_expected;
1087
1088 assert!(
1089 (strain[0] - eps_xx_expected).abs() / eps_xx_expected < 1e-10,
1090 "eps_xx mismatch: {} vs {}",
1091 strain[0],
1092 eps_xx_expected
1093 );
1094 assert!(
1095 (strain[1] - eps_yy_expected).abs() / eps_xx_expected.abs() < 1e-10,
1096 "eps_yy mismatch: {} vs {}",
1097 strain[1],
1098 eps_yy_expected
1099 );
1100 }
1101
1102 #[test]
1104 #[allow(clippy::needless_range_loop)]
1105 fn test_linear_elastic_stress_strain_symmetry() {
1106 let mat = LinearElastic::new(200.0e9, 0.3);
1107 let c = mat.stress_strain_matrix_3d();
1108 for i in 0..6 {
1109 for j in 0..6 {
1110 assert!(
1111 (c[i][j] - c[j][i]).abs() < 1.0e-6,
1112 "C[{i}][{j}] != C[{j}][{i}]"
1113 );
1114 }
1115 }
1116 }
1117
1118 #[test]
1120 fn test_linear_elastic_bulk_shear_modulus_steel() {
1121 let mat = LinearElastic::new(200.0e9, 0.3);
1122 let k = mat.bulk_modulus();
1123 let g = mat.shear_modulus();
1124 assert!((k - 166.667e9).abs() < 1.0e8);
1125 assert!((g - 76.923e9).abs() < 1.0e8);
1126 }
1127
1128 #[test]
1130 #[allow(clippy::needless_range_loop)]
1131 fn test_neo_hookean_identity_zero_stress() {
1132 let mat = NeoHookean::new(1.0e6, 1.0e9);
1133 let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1134 let p = mat.first_piola_kirchhoff_stress(&identity);
1135 for i in 0..3 {
1136 for j in 0..3 {
1137 assert!(
1138 p[i][j].abs() < 1.0e-6,
1139 "P[{i}][{j}] = {} should be ~0",
1140 p[i][j]
1141 );
1142 }
1143 }
1144 }
1145
1146 #[test]
1148 fn test_orthotropic_compliance_symmetry() {
1149 #[allow(non_snake_case)]
1150 let mat = OrthotropicElastic {
1151 E1: 200.0e9,
1152 E2: 100.0e9,
1153 E3: 80.0e9,
1154 G12: 40.0e9,
1155 G23: 30.0e9,
1156 G13: 35.0e9,
1157 nu12: 0.25,
1158 nu23: 0.2,
1159 nu13: 0.22,
1160 };
1161 let s = mat.compliance_voigt();
1162 for i in 0..6 {
1164 assert!(
1165 s[i * 6 + i] > 0.0,
1166 "Compliance diagonal S[{i}][{i}] should be positive"
1167 );
1168 }
1169 }
1170
1171 #[test]
1173 fn test_von_mises_no_failure_below_yield() {
1174 let crit = VonMisesFailure::new(250.0e6);
1175 let stress = [200.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1176 assert!(
1177 !crit.is_failed(&stress),
1178 "Should not fail below yield stress"
1179 );
1180 }
1181
1182 #[test]
1184 fn test_von_mises_failure_above_yield() {
1185 let crit = VonMisesFailure::new(250.0e6);
1186 let stress = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1187 assert!(crit.is_failed(&stress), "Should fail above yield stress");
1188 }
1189
1190 #[test]
1192 fn test_von_mises_zero_stress() {
1193 let vm = VonMisesFailure::von_mises_stress(&[0.0; 6]);
1194 assert!(
1195 vm.abs() < 1e-15,
1196 "Von Mises stress should be 0 for zero stress, got {vm}"
1197 );
1198 }
1199
1200 #[test]
1202 fn test_tsai_wu_no_failure_at_zero() {
1203 let crit = TsaiWuFailure::from_strengths(500.0e6, 300.0e6, 200.0e6, 150.0e6, 80.0e6);
1204 let stress = [0.0; 6];
1205 assert!(
1206 !crit.is_failed(&stress),
1207 "Zero stress should not trigger Tsai-Wu failure"
1208 );
1209 }
1210
1211 #[test]
1213 fn test_tsai_wu_failure_index_large_stress() {
1214 let crit = TsaiWuFailure::from_strengths(100.0e6, 200.0e6, 80.0e6, 120.0e6, 50.0e6);
1215 let stress = [200.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1217 assert!(
1218 crit.is_failed(&stress),
1219 "Large tensile stress should trigger Tsai-Wu failure, index={}",
1220 crit.failure_index(&stress)
1221 );
1222 }
1223
1224 #[test]
1230 fn test_plane_stress_isotropic_q11() {
1231 let e = 200.0e9_f64;
1232 let nu = 0.3_f64;
1233 let ps = PlaneStressStiffness::from_isotropic(e, nu);
1234 let expected_q11 = e / (1.0 - nu * nu);
1235 assert!(
1236 (ps.q[0] - expected_q11).abs() / expected_q11 < 1e-10,
1237 "Q11 mismatch: {} vs {}",
1238 ps.q[0],
1239 expected_q11
1240 );
1241 }
1242
1243 #[test]
1245 fn test_plane_stress_isotropic_q12() {
1246 let e = 200.0e9_f64;
1247 let nu = 0.3_f64;
1248 let ps = PlaneStressStiffness::from_isotropic(e, nu);
1249 let expected_q12 = nu * e / (1.0 - nu * nu);
1250 assert!(
1251 (ps.q[1] - expected_q12).abs() / expected_q12 < 1e-10,
1252 "Q12 mismatch: {} vs {}",
1253 ps.q[1],
1254 expected_q12
1255 );
1256 }
1257
1258 #[test]
1260 fn test_plane_stress_isotropic_symmetry() {
1261 let ps = PlaneStressStiffness::from_isotropic(200.0e9, 0.25);
1262 assert!(
1263 (ps.q[1] - ps.q[3]).abs() < 1e-6,
1264 "Q12 ({}) should equal Q21 ({})",
1265 ps.q[1],
1266 ps.q[3]
1267 );
1268 }
1269
1270 #[test]
1272 fn test_plane_stress_orthotropic_q11() {
1273 let e1 = 200.0e9_f64;
1274 let e2 = 100.0e9_f64;
1275 let nu12 = 0.25_f64;
1276 let g12 = 40.0e9_f64;
1277 let ps = PlaneStressStiffness::from_orthotropic(e1, e2, nu12, g12);
1278 let nu21 = nu12 * e2 / e1;
1279 let denom = 1.0 - nu12 * nu21;
1280 let expected_q11 = e1 / denom;
1281 assert!(
1282 (ps.q[0] - expected_q11).abs() / expected_q11 < 1e-10,
1283 "Ortho Q11 mismatch: {} vs {}",
1284 ps.q[0],
1285 expected_q11
1286 );
1287 }
1288
1289 #[test]
1291 fn test_plane_stress_orthotropic_q66() {
1292 let g12 = 40.0e9_f64;
1293 let ps = PlaneStressStiffness::from_orthotropic(200.0e9, 100.0e9, 0.25, g12);
1294 assert!(
1295 (ps.q[8] - g12).abs() / g12 < 1e-10,
1296 "Q66 should equal G12: {} vs {}",
1297 ps.q[8],
1298 g12
1299 );
1300 }
1301
1302 #[test]
1304 fn test_plane_stress_apply_uniaxial() {
1305 let e = 100.0e9_f64;
1306 let nu = 0.0_f64; let ps = PlaneStressStiffness::from_isotropic(e, nu);
1308 let stress = ps.apply([1.0e-3, 0.0, 0.0]);
1310 let expected = e * 1.0e-3;
1311 assert!(
1312 (stress[0] - expected).abs() / expected < 1e-10,
1313 "Uniaxial stress mismatch: {} vs {}",
1314 stress[0],
1315 expected
1316 );
1317 assert!(
1318 stress[1].abs() < 1e-3,
1319 "σ22 should be zero for ν=0: {}",
1320 stress[1]
1321 );
1322 }
1323
1324 #[test]
1330 fn test_plane_strain_isotropic_c11() {
1331 let e = 200.0e9_f64;
1332 let nu = 0.3_f64;
1333 let ps = PlaneStrainStiffness::from_isotropic(e, nu);
1334 let expected = e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu));
1335 assert!(
1336 (ps.c[0] - expected).abs() / expected < 1e-10,
1337 "C11 mismatch: {} vs {}",
1338 ps.c[0],
1339 expected
1340 );
1341 }
1342
1343 #[test]
1345 fn test_plane_strain_isotropic_c12() {
1346 let e = 200.0e9_f64;
1347 let nu = 0.3_f64;
1348 let ps = PlaneStrainStiffness::from_isotropic(e, nu);
1349 let expected = e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu));
1350 assert!(
1351 (ps.c[1] - expected).abs() / expected < 1e-10,
1352 "C12 mismatch: {} vs {}",
1353 ps.c[1],
1354 expected
1355 );
1356 }
1357
1358 #[test]
1360 fn test_plane_strain_ordering() {
1361 let ps = PlaneStrainStiffness::from_isotropic(200.0e9, 0.3);
1362 assert!(ps.c[0] > ps.c[1], "C11 should be greater than C12");
1363 assert!(ps.c[1] > 0.0, "C12 should be positive for ν > 0");
1364 }
1365
1366 #[test]
1368 fn test_plane_strain_apply_shear() {
1369 let e = 200.0e9_f64;
1370 let nu = 0.3_f64;
1371 let ps = PlaneStrainStiffness::from_isotropic(e, nu);
1372 let g = e / (2.0 * (1.0 + nu));
1373 let strain = [0.0, 0.0, 1.0e-3]; let stress = ps.apply(strain);
1375 let expected_s12 = g * 1.0e-3;
1376 assert!(
1377 (stress[2] - expected_s12).abs() / expected_s12 < 1e-10,
1378 "Shear stress mismatch: {} vs {}",
1379 stress[2],
1380 expected_s12
1381 );
1382 }
1383
1384 #[test]
1386 fn test_plane_strain_symmetry() {
1387 let ps = PlaneStrainStiffness::from_isotropic(150.0e9, 0.2);
1388 assert!(
1389 (ps.c[1] - ps.c[3]).abs() < 1e-6,
1390 "C12 ({}) should equal C21 ({})",
1391 ps.c[1],
1392 ps.c[3]
1393 );
1394 }
1395
1396 #[test]
1402 fn test_eshelby_s1111_nu03() {
1403 let esh = EshelbySphericalInclusion::new(0.3);
1404 let nu = 0.3_f64;
1405 let expected = (7.0 - 5.0 * nu) / (15.0 * (1.0 - nu));
1406 assert!(
1407 (esh.s1111() - expected).abs() < 1e-12,
1408 "S1111 mismatch: {} vs {}",
1409 esh.s1111(),
1410 expected
1411 );
1412 }
1413
1414 #[test]
1416 fn test_eshelby_s1122_nu03() {
1417 let esh = EshelbySphericalInclusion::new(0.3);
1418 let nu = 0.3_f64;
1419 let expected = (5.0 * nu - 1.0) / (15.0 * (1.0 - nu));
1420 assert!(
1421 (esh.s1122() - expected).abs() < 1e-12,
1422 "S1122 mismatch: {} vs {}",
1423 esh.s1122(),
1424 expected
1425 );
1426 }
1427
1428 #[test]
1430 fn test_eshelby_s1212_nu03() {
1431 let esh = EshelbySphericalInclusion::new(0.3);
1432 let nu = 0.3_f64;
1433 let expected = (4.0 - 5.0 * nu) / (15.0 * (1.0 - nu));
1434 assert!(
1435 (esh.s1212() - expected).abs() < 1e-12,
1436 "S1212 mismatch: {} vs {}",
1437 esh.s1212(),
1438 expected
1439 );
1440 }
1441
1442 #[test]
1444 fn test_eshelby_s1111_positive() {
1445 for &nu in &[0.1_f64, 0.2, 0.3, 0.4, 0.49] {
1446 let esh = EshelbySphericalInclusion::new(nu);
1447 assert!(esh.s1111() > 0.0, "S1111 should be positive for ν={}", nu);
1448 }
1449 }
1450
1451 #[test]
1453 fn test_eshelby_s1212_positive() {
1454 for &nu in &[0.1_f64, 0.2, 0.3, 0.4, 0.49] {
1455 let esh = EshelbySphericalInclusion::new(nu);
1456 assert!(esh.s1212() > 0.0, "S1212 should be positive for ν={}", nu);
1457 }
1458 }
1459
1460 #[test]
1462 fn test_eshelby_trace_components() {
1463 let esh = EshelbySphericalInclusion::new(0.3);
1464 assert!(esh.s1111() > 0.0);
1466 assert!(esh.s1212() > 0.0);
1467 assert!(esh.s1122() > 0.0);
1469 }
1470
1471 #[test]
1477 fn test_effective_medium_voigt_zero_phi() {
1478 let em = EffectiveMedium::new(0.0, 200.0e9, 0.3, 400.0e9, 0.25);
1479 assert!(
1480 (em.voigt_modulus() - 200.0e9).abs() < 1e-3,
1481 "Voigt at φ=0 should equal E1: {}",
1482 em.voigt_modulus()
1483 );
1484 }
1485
1486 #[test]
1488 fn test_effective_medium_voigt_full_phi() {
1489 let em = EffectiveMedium::new(1.0, 200.0e9, 0.3, 400.0e9, 0.25);
1490 assert!(
1491 (em.voigt_modulus() - 400.0e9).abs() < 1e-3,
1492 "Voigt at φ=1 should equal E2: {}",
1493 em.voigt_modulus()
1494 );
1495 }
1496
1497 #[test]
1499 fn test_effective_medium_reuss_zero_phi() {
1500 let em = EffectiveMedium::new(0.0, 200.0e9, 0.3, 400.0e9, 0.25);
1501 assert!(
1502 (em.reuss_modulus() - 200.0e9).abs() < 1e-3,
1503 "Reuss at φ=0 should equal E1: {}",
1504 em.reuss_modulus()
1505 );
1506 }
1507
1508 #[test]
1510 fn test_effective_medium_reuss_full_phi() {
1511 let em = EffectiveMedium::new(1.0, 200.0e9, 0.3, 400.0e9, 0.25);
1512 assert!(
1513 (em.reuss_modulus() - 400.0e9).abs() < 1e-3,
1514 "Reuss at φ=1 should equal E2: {}",
1515 em.reuss_modulus()
1516 );
1517 }
1518
1519 #[test]
1521 fn test_effective_medium_bounds_satisfied() {
1522 for &phi in &[0.0_f64, 0.1, 0.25, 0.5, 0.75, 1.0] {
1523 let em = EffectiveMedium::new(phi, 200.0e9, 0.3, 400.0e9, 0.25);
1524 assert!(
1525 em.bounds_satisfied(),
1526 "Reuss > Voigt at φ={}: R={}, V={}",
1527 phi,
1528 em.reuss_modulus(),
1529 em.voigt_modulus()
1530 );
1531 }
1532 }
1533
1534 #[test]
1536 fn test_effective_medium_hill_between_bounds() {
1537 let em = EffectiveMedium::new(0.4, 200.0e9, 0.3, 400.0e9, 0.25);
1538 let hill = em.hill_modulus();
1539 let voigt = em.voigt_modulus();
1540 let reuss = em.reuss_modulus();
1541 assert!(
1542 hill >= reuss - 1e-3 && hill <= voigt + 1e-3,
1543 "Hill ({}) should be between Reuss ({}) and Voigt ({})",
1544 hill,
1545 reuss,
1546 voigt
1547 );
1548 }
1549
1550 #[test]
1552 fn test_effective_medium_voigt_bulk_zero_phi() {
1553 let e1 = 200.0e9_f64;
1554 let nu1 = 0.3_f64;
1555 let em = EffectiveMedium::new(0.0, e1, nu1, 400.0e9, 0.25);
1556 let k1 = e1 / (3.0 * (1.0 - 2.0 * nu1));
1557 assert!(
1558 (em.voigt_bulk_modulus() - k1).abs() < 1e-3,
1559 "Voigt K at φ=0 should be K1: {} vs {}",
1560 em.voigt_bulk_modulus(),
1561 k1
1562 );
1563 }
1564
1565 #[test]
1567 fn test_effective_medium_bulk_bounds() {
1568 for &phi in &[0.1_f64, 0.3, 0.5, 0.7, 0.9] {
1569 let em = EffectiveMedium::new(phi, 200.0e9, 0.3, 400.0e9, 0.25);
1570 assert!(
1571 em.reuss_bulk_modulus() <= em.voigt_bulk_modulus() + 1e-6,
1572 "Reuss K ({}) > Voigt K ({}) at φ={}",
1573 em.reuss_bulk_modulus(),
1574 em.voigt_bulk_modulus(),
1575 phi
1576 );
1577 }
1578 }
1579
1580 #[test]
1582 fn test_effective_medium_voigt_shear_zero_phi() {
1583 let e1 = 200.0e9_f64;
1584 let nu1 = 0.3_f64;
1585 let em = EffectiveMedium::new(0.0, e1, nu1, 400.0e9, 0.25);
1586 let g1 = e1 / (2.0 * (1.0 + nu1));
1587 assert!(
1588 (em.voigt_shear_modulus() - g1).abs() < 1e-3,
1589 "Voigt G at φ=0 should be G1: {} vs {}",
1590 em.voigt_shear_modulus(),
1591 g1
1592 );
1593 }
1594
1595 #[test]
1599 fn test_elastic_material_compliance_cs_is_identity() {
1600 let mat = LinearElastic::new(200.0e9, 0.3);
1601 let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1602 let s = em.compute_compliance_tensor();
1603 let c = em.stiffness;
1604 for i in 0..6 {
1606 for j in 0..6 {
1607 let mut cs_ij = 0.0_f64;
1608 for k in 0..6 {
1609 cs_ij += c[i * 6 + k] * s[k * 6 + j];
1610 }
1611 let expected = if i == j { 1.0 } else { 0.0 };
1612 assert!(
1613 (cs_ij - expected).abs() < 1e-6,
1614 "C*S[{},{}] = {} ≠ {}",
1615 i,
1616 j,
1617 cs_ij,
1618 expected
1619 );
1620 }
1621 }
1622 }
1623
1624 #[test]
1626 fn test_elastic_material_engineering_constants_isotropic() {
1627 let e = 200.0e9_f64;
1628 let nu = 0.3_f64;
1629 let mat = LinearElastic::new(e, nu);
1630 let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1631 let ec = em.compute_engineering_constants();
1632 assert!((ec.e1 - e).abs() / e < 1e-8, "E1: {} vs {}", ec.e1, e);
1634 assert!((ec.e2 - e).abs() / e < 1e-8, "E2: {} vs {}", ec.e2, e);
1635 assert!((ec.e3 - e).abs() / e < 1e-8, "E3: {} vs {}", ec.e3, e);
1636 }
1637
1638 #[test]
1640 fn test_elastic_material_poisson_recovered() {
1641 let e = 200.0e9_f64;
1642 let nu = 0.25_f64;
1643 let mat = LinearElastic::new(e, nu);
1644 let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1645 let ec = em.compute_engineering_constants();
1646 assert!((ec.nu12 - nu).abs() < 1e-6, "ν12: {} vs {}", ec.nu12, nu);
1647 assert!((ec.nu13 - nu).abs() < 1e-6, "ν13: {} vs {}", ec.nu13, nu);
1648 assert!((ec.nu23 - nu).abs() < 1e-6, "ν23: {} vs {}", ec.nu23, nu);
1649 }
1650
1651 #[test]
1653 fn test_elastic_material_shear_modulus_recovered() {
1654 let e = 200.0e9_f64;
1655 let nu = 0.3_f64;
1656 let g = e / (2.0 * (1.0 + nu));
1657 let mat = LinearElastic::new(e, nu);
1658 let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1659 let ec = em.compute_engineering_constants();
1660 assert!((ec.g12 - g).abs() / g < 1e-8, "G12: {} vs {}", ec.g12, g);
1661 assert!((ec.g23 - g).abs() / g < 1e-8, "G23: {} vs {}", ec.g23, g);
1662 assert!((ec.g13 - g).abs() / g < 1e-8, "G13: {} vs {}", ec.g13, g);
1663 }
1664
1665 #[test]
1667 fn test_elastic_material_p_wave_speed() {
1668 let e = 200.0e9_f64;
1669 let nu = 0.3_f64;
1670 let rho = 7800.0_f64;
1671 let mat = LinearElastic::new(e, nu);
1672 let em = ElasticMaterial::from_isotropic(&mat, rho);
1673 let ws = em.compute_wave_speeds();
1674 let c11 = e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu));
1676 let v_p_expected = (c11 / rho).sqrt();
1677 assert!(
1678 (ws.v_p1 - v_p_expected).abs() / v_p_expected < 1e-8,
1679 "v_P1: {} vs {}",
1680 ws.v_p1,
1681 v_p_expected
1682 );
1683 }
1684
1685 #[test]
1687 fn test_elastic_material_s_wave_speed() {
1688 let e = 200.0e9_f64;
1689 let nu = 0.3_f64;
1690 let rho = 7800.0_f64;
1691 let mat = LinearElastic::new(e, nu);
1692 let em = ElasticMaterial::from_isotropic(&mat, rho);
1693 let ws = em.compute_wave_speeds();
1694 let g = e / (2.0 * (1.0 + nu));
1695 let v_s_expected = (g / rho).sqrt();
1696 assert!(
1697 (ws.v_s12 - v_s_expected).abs() / v_s_expected < 1e-8,
1698 "v_S12: {} vs {}",
1699 ws.v_s12,
1700 v_s_expected
1701 );
1702 }
1703
1704 #[test]
1706 fn test_elastic_material_p_wave_faster_than_s_wave() {
1707 let mat = LinearElastic::new(200.0e9, 0.3);
1708 let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1709 let ws = em.compute_wave_speeds();
1710 assert!(
1711 ws.v_p1 > ws.v_s12,
1712 "P-wave ({}) must exceed S-wave ({})",
1713 ws.v_p1,
1714 ws.v_s12
1715 );
1716 }
1717
1718 #[test]
1720 fn test_elastic_material_wave_speeds_isotropic() {
1721 let mat = LinearElastic::new(200.0e9, 0.3);
1722 let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1723 let ws = em.compute_wave_speeds();
1724 assert!(
1725 (ws.v_p1 - ws.v_p2).abs() < 1.0,
1726 "v_P1 ≠ v_P2 for isotropic: {} {}",
1727 ws.v_p1,
1728 ws.v_p2
1729 );
1730 assert!(
1731 (ws.v_p2 - ws.v_p3).abs() < 1.0,
1732 "v_P2 ≠ v_P3 for isotropic: {} {}",
1733 ws.v_p2,
1734 ws.v_p3
1735 );
1736 assert!(
1737 (ws.v_s12 - ws.v_s23).abs() < 1.0,
1738 "v_S12 ≠ v_S23 for isotropic: {} {}",
1739 ws.v_s12,
1740 ws.v_s23
1741 );
1742 }
1743
1744 #[test]
1746 fn test_elastic_material_compliance_symmetric() {
1747 let mat = LinearElastic::new(200.0e9, 0.3);
1748 let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1749 let s = em.compute_compliance_tensor();
1750 for i in 0..6 {
1751 for j in 0..6 {
1752 let avg = (s[i * 6 + j].abs() + s[j * 6 + i].abs()) * 0.5 + 1e-40;
1754 let rel_err = (s[i * 6 + j] - s[j * 6 + i]).abs() / avg;
1755 assert!(
1756 rel_err < 1e-8,
1757 "S[{},{}] ≠ S[{},{}]: {} vs {}",
1758 i,
1759 j,
1760 j,
1761 i,
1762 s[i * 6 + j],
1763 s[j * 6 + i]
1764 );
1765 }
1766 }
1767 }
1768
1769 #[test]
1771 fn test_elastic_material_wave_speed_increases_with_modulus() {
1772 let mat1 = LinearElastic::new(200.0e9, 0.3);
1773 let mat2 = LinearElastic::new(400.0e9, 0.3);
1774 let em1 = ElasticMaterial::from_isotropic(&mat1, 7800.0);
1775 let em2 = ElasticMaterial::from_isotropic(&mat2, 7800.0);
1776 let ws1 = em1.compute_wave_speeds();
1777 let ws2 = em2.compute_wave_speeds();
1778 assert!(
1779 ws2.v_p1 > ws1.v_p1,
1780 "Stiffer material should have higher P-wave speed"
1781 );
1782 }
1783
1784 #[test]
1786 fn test_elastic_material_engineering_constants_positive_moduli() {
1787 let mat = LinearElastic::new(70.0e9, 0.33); let em = ElasticMaterial::from_isotropic(&mat, 2700.0);
1789 let ec = em.compute_engineering_constants();
1790 assert!(ec.e1 > 0.0, "E1 must be positive: {}", ec.e1);
1791 assert!(ec.g12 > 0.0, "G12 must be positive: {}", ec.g12);
1792 assert!(
1793 ec.nu12 > 0.0,
1794 "ν12 must be positive for aluminium: {}",
1795 ec.nu12
1796 );
1797 }
1798}