1#![warn(missing_docs)]
11
12#[derive(Debug, Clone)]
14pub struct FiberProperties {
15 pub e1: f64,
17 pub e2: f64,
19 pub g12: f64,
21 pub nu12: f64,
23 pub volume_fraction: f64,
25 pub aspect_ratio: f64,
27}
28
29impl FiberProperties {
30 pub fn new(
32 e1: f64,
33 e2: f64,
34 g12: f64,
35 nu12: f64,
36 volume_fraction: f64,
37 aspect_ratio: f64,
38 ) -> Self {
39 Self {
40 e1,
41 e2,
42 g12,
43 nu12,
44 volume_fraction,
45 aspect_ratio,
46 }
47 }
48
49 pub fn carbon_t300() -> Self {
51 Self {
52 e1: 230e9,
53 e2: 15e9,
54 g12: 15e9,
55 nu12: 0.27,
56 volume_fraction: 0.6,
57 aspect_ratio: 1000.0,
58 }
59 }
60
61 pub fn glass_e() -> Self {
63 Self {
64 e1: 72e9,
65 e2: 72e9,
66 g12: 30e9,
67 nu12: 0.22,
68 volume_fraction: 0.5,
69 aspect_ratio: 500.0,
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct MatrixProperties {
77 pub modulus: f64,
79 pub poisson_ratio: f64,
81 pub yield_stress: f64,
83 pub fracture_toughness: f64,
85}
86
87impl MatrixProperties {
88 pub fn new(
90 modulus: f64,
91 poisson_ratio: f64,
92 yield_stress: f64,
93 fracture_toughness: f64,
94 ) -> Self {
95 Self {
96 modulus,
97 poisson_ratio,
98 yield_stress,
99 fracture_toughness,
100 }
101 }
102
103 pub fn epoxy() -> Self {
105 Self {
106 modulus: 3.5e9,
107 poisson_ratio: 0.35,
108 yield_stress: 80e6,
109 fracture_toughness: 0.5e6,
110 }
111 }
112
113 pub fn shear_modulus(&self) -> f64 {
115 self.modulus / (2.0 * (1.0 + self.poisson_ratio))
116 }
117}
118
119#[derive(Debug, Clone)]
123pub struct HalpinTsai {
124 pub fiber: FiberProperties,
126 pub matrix: MatrixProperties,
128}
129
130impl HalpinTsai {
131 pub fn new(fiber: FiberProperties, matrix: MatrixProperties) -> Self {
133 Self { fiber, matrix }
134 }
135
136 fn vf(&self) -> f64 {
137 self.fiber.volume_fraction
138 }
139 fn vm(&self) -> f64 {
140 1.0 - self.fiber.volume_fraction
141 }
142
143 pub fn e11(&self) -> f64 {
145 self.fiber.e1 * self.vf() + self.matrix.modulus * self.vm()
146 }
147
148 pub fn e22(&self) -> f64 {
150 let xi = 2.0; let ef = self.fiber.e2;
152 let em = self.matrix.modulus;
153 let eta = (ef / em - 1.0) / (ef / em + xi);
154 em * (1.0 + xi * eta * self.vf()) / (1.0 - eta * self.vf())
155 }
156
157 pub fn g12(&self) -> f64 {
159 let xi = 1.0;
160 let gf = self.fiber.g12;
161 let gm = self.matrix.shear_modulus();
162 let eta = (gf / gm - 1.0) / (gf / gm + xi);
163 gm * (1.0 + xi * eta * self.vf()) / (1.0 - eta * self.vf())
164 }
165
166 pub fn nu12(&self) -> f64 {
168 self.fiber.nu12 * self.vf() + self.matrix.poisson_ratio * self.vm()
169 }
170
171 pub fn nu21(&self) -> f64 {
173 self.nu12() * self.e22() / self.e11()
174 }
175
176 pub fn voigt_e11(&self) -> f64 {
178 self.fiber.e1 * self.vf() + self.matrix.modulus * self.vm()
179 }
180
181 pub fn reuss_e22(&self) -> f64 {
183 1.0 / (self.vf() / self.fiber.e2 + self.vm() / self.matrix.modulus)
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct MoriTanakaComposite {
190 pub fiber: FiberProperties,
192 pub matrix: MatrixProperties,
194}
195
196impl MoriTanakaComposite {
197 pub fn new(fiber: FiberProperties, matrix: MatrixProperties) -> Self {
199 Self { fiber, matrix }
200 }
201
202 pub fn effective_bulk_modulus(&self) -> f64 {
204 let km = self.matrix.modulus / (3.0 * (1.0 - 2.0 * self.matrix.poisson_ratio));
205 let gm = self.matrix.shear_modulus();
206 let kf = self.fiber.e1 / (3.0 * (1.0 - 2.0 * self.fiber.nu12));
208 let vf = self.fiber.volume_fraction;
209 let vm = 1.0 - vf;
210 let alpha = 3.0 * km / (3.0 * km + 4.0 * gm);
211 km + vf * (kf - km) / (1.0 + vm * alpha * (kf - km) / km)
212 }
213
214 pub fn effective_shear_modulus(&self) -> f64 {
216 let gm = self.matrix.shear_modulus();
217 let gf = self.fiber.g12;
218 let vf = self.fiber.volume_fraction;
219 let vm = 1.0 - vf;
220 let km = self.matrix.modulus / (3.0 * (1.0 - 2.0 * self.matrix.poisson_ratio));
221 let beta = 6.0 * (km + 2.0 * gm) / (5.0 * (3.0 * km + 4.0 * gm));
222 gm + vf * (gf - gm) / (1.0 + vm * beta * (gf - gm) / gm)
223 }
224}
225
226#[derive(Debug, Clone)]
228pub struct Lamina {
229 pub e1: f64,
231 pub e2: f64,
233 pub g12: f64,
235 pub nu12: f64,
237 pub thickness: f64,
239 pub angle: f64,
241}
242
243impl Lamina {
244 pub fn new(e1: f64, e2: f64, g12: f64, nu12: f64, thickness: f64, angle_deg: f64) -> Self {
246 Self {
247 e1,
248 e2,
249 g12,
250 nu12,
251 thickness,
252 angle: angle_deg.to_radians(),
253 }
254 }
255
256 pub fn nu21(&self) -> f64 {
258 self.nu12 * self.e2 / self.e1
259 }
260
261 pub fn q_matrix(&self) -> [f64; 4] {
263 let denom = 1.0 - self.nu12 * self.nu21();
264 [
265 self.e1 / denom, self.nu12 * self.e2 / denom, self.e2 / denom, self.g12, ]
270 }
271
272 pub fn q_bar(&self) -> [f64; 6] {
275 let q = self.q_matrix();
276 let c = self.angle.cos();
277 let s = self.angle.sin();
278 let c2 = c * c;
279 let s2 = s * s;
280 let c4 = c2 * c2;
281 let s4 = s2 * s2;
282 let c2s2 = c2 * s2;
283 [
284 q[0] * c4 + 2.0 * (q[1] + 2.0 * q[3]) * c2s2 + q[2] * s4,
285 (q[0] + q[2] - 4.0 * q[3]) * c2s2 + q[1] * (c4 + s4),
286 (q[0] - q[1] - 2.0 * q[3]) * c * s * c2 - (q[2] - q[1] - 2.0 * q[3]) * c * s * s2,
287 q[0] * s4 + 2.0 * (q[1] + 2.0 * q[3]) * c2s2 + q[2] * c4,
288 (q[0] - q[1] - 2.0 * q[3]) * c * s * s2 - (q[2] - q[1] - 2.0 * q[3]) * c * s * c2,
289 (q[0] + q[2] - 2.0 * q[1] - 2.0 * q[3]) * c2s2 + q[3] * (c4 + s4),
290 ]
291 }
292}
293
294#[derive(Debug, Clone)]
298pub struct ClassicalLaminateTheory {
299 pub laminae: Vec<Lamina>,
301}
302
303impl ClassicalLaminateTheory {
304 pub fn new(laminae: Vec<Lamina>) -> Self {
306 Self { laminae }
307 }
308
309 pub fn total_thickness(&self) -> f64 {
311 self.laminae.iter().map(|l| l.thickness).sum()
312 }
313
314 pub fn z_coords(&self) -> Vec<f64> {
316 let h = self.total_thickness();
317 let mut z = vec![-h / 2.0];
318 for l in &self.laminae {
319 let top = *z.last().expect("collection should not be empty") + l.thickness;
320 z.push(top);
321 }
322 z
323 }
324
325 pub fn a_matrix(&self) -> [f64; 6] {
327 let z = self.z_coords();
328 let mut a = [0.0f64; 6];
329 for (i, l) in self.laminae.iter().enumerate() {
330 let qb = l.q_bar();
331 let dz = z[i + 1] - z[i];
332 for j in 0..6 {
333 a[j] += qb[j] * dz;
334 }
335 }
336 a
337 }
338
339 pub fn b_matrix(&self) -> [f64; 6] {
341 let z = self.z_coords();
342 let mut b = [0.0f64; 6];
343 for (i, l) in self.laminae.iter().enumerate() {
344 let qb = l.q_bar();
345 let dz2 = (z[i + 1].powi(2) - z[i].powi(2)) / 2.0;
346 for j in 0..6 {
347 b[j] += qb[j] * dz2;
348 }
349 }
350 b
351 }
352
353 pub fn d_matrix(&self) -> [f64; 6] {
355 let z = self.z_coords();
356 let mut d = [0.0f64; 6];
357 for (i, l) in self.laminae.iter().enumerate() {
358 let qb = l.q_bar();
359 let dz3 = (z[i + 1].powi(3) - z[i].powi(3)) / 3.0;
360 for j in 0..6 {
361 d[j] += qb[j] * dz3;
362 }
363 }
364 d
365 }
366
367 pub fn is_symmetric(&self) -> bool {
369 let b = self.b_matrix();
370 b.iter().all(|&v| v.abs() < 1e-6)
371 }
372
373 pub fn is_balanced(&self) -> bool {
375 let a = self.a_matrix();
376 a[2].abs() < 1e-6 && a[4].abs() < 1e-6
377 }
378}
379
380#[derive(Debug, Clone)]
382pub struct LaminateResponse {
383 pub n: [f64; 3],
385 pub m: [f64; 3],
387 pub midplane_strains: [f64; 3],
389 pub curvatures: [f64; 3],
391}
392
393impl LaminateResponse {
394 pub fn from_n_symmetric(n: [f64; 3], a: [f64; 6]) -> Self {
397 let a11 = a[0];
400 let a12 = a[1];
401 let a22 = a[3];
402 let det = a11 * a22 - a12 * a12;
403 let eps_x = (a22 * n[0] - a12 * n[1]) / det;
404 let eps_y = (-a12 * n[0] + a11 * n[1]) / det;
405 let gamma_xy = if a[5] > 0.0 { n[2] / a[5] } else { 0.0 };
406 Self {
407 n,
408 m: [0.0; 3],
409 midplane_strains: [eps_x, eps_y, gamma_xy],
410 curvatures: [0.0; 3],
411 }
412 }
413}
414
415#[derive(Debug, Clone)]
417pub struct TsaiWuCriterion {
418 pub xt: f64,
420 pub xc: f64,
422 pub yt: f64,
424 pub yc: f64,
426 pub s12: f64,
428 pub f12_star: f64,
430}
431
432impl TsaiWuCriterion {
433 pub fn new(xt: f64, xc: f64, yt: f64, yc: f64, s12: f64, f12_star: f64) -> Self {
435 Self {
436 xt,
437 xc,
438 yt,
439 yc,
440 s12,
441 f12_star,
442 }
443 }
444
445 pub fn f1(&self) -> f64 {
447 1.0 / self.xt - 1.0 / self.xc
448 }
449
450 pub fn f2(&self) -> f64 {
452 1.0 / self.yt - 1.0 / self.yc
453 }
454
455 pub fn f11(&self) -> f64 {
457 1.0 / (self.xt * self.xc)
458 }
459
460 pub fn f22(&self) -> f64 {
462 1.0 / (self.yt * self.yc)
463 }
464
465 pub fn f66(&self) -> f64 {
467 1.0 / (self.s12 * self.s12)
468 }
469
470 pub fn f12(&self) -> f64 {
472 self.f12_star / (self.f11() * self.f22()).sqrt()
473 }
474
475 pub fn failure_index(&self, sigma1: f64, sigma2: f64, tau12: f64) -> f64 {
477 self.f1() * sigma1
478 + self.f2() * sigma2
479 + self.f11() * sigma1 * sigma1
480 + self.f22() * sigma2 * sigma2
481 + self.f66() * tau12 * tau12
482 + 2.0 * self.f12() * sigma1 * sigma2
483 }
484
485 pub fn fails(&self, sigma1: f64, sigma2: f64, tau12: f64) -> bool {
487 self.failure_index(sigma1, sigma2, tau12) >= 1.0
488 }
489
490 pub fn is_convex(&self) -> bool {
494 self.f12_star * self.f12_star < 1.0
495 }
496
497 pub fn strength_ratio(&self, sigma1: f64, sigma2: f64, tau12: f64) -> f64 {
499 let a = self.f11() * sigma1 * sigma1
500 + self.f22() * sigma2 * sigma2
501 + self.f66() * tau12 * tau12
502 + 2.0 * self.f12() * sigma1 * sigma2;
503 let b = self.f1() * sigma1 + self.f2() * sigma2;
504 if a < 1e-30 {
505 if b.abs() < 1e-30 {
506 return f64::INFINITY;
507 }
508 return 1.0 / b;
509 }
510 let disc = b * b + 4.0 * a;
511 if disc < 0.0 {
512 return f64::INFINITY;
513 }
514 (-b + disc.sqrt()) / (2.0 * a)
515 }
516}
517
518#[derive(Debug, Clone)]
520pub struct MaxStressCriterion {
521 pub xt: f64,
523 pub xc: f64,
525 pub yt: f64,
527 pub yc: f64,
529 pub s12: f64,
531}
532
533impl MaxStressCriterion {
534 pub fn new(xt: f64, xc: f64, yt: f64, yc: f64, s12: f64) -> Self {
536 Self {
537 xt,
538 xc,
539 yt,
540 yc,
541 s12,
542 }
543 }
544
545 pub fn fails(&self, sigma1: f64, sigma2: f64, tau12: f64) -> bool {
547 if sigma1 > 0.0 && sigma1 >= self.xt {
548 return true;
549 }
550 if sigma1 < 0.0 && sigma1.abs() >= self.xc {
551 return true;
552 }
553 if sigma2 > 0.0 && sigma2 >= self.yt {
554 return true;
555 }
556 if sigma2 < 0.0 && sigma2.abs() >= self.yc {
557 return true;
558 }
559 if tau12.abs() >= self.s12 {
560 return true;
561 }
562 false
563 }
564
565 pub fn failure_index(&self, sigma1: f64, sigma2: f64, tau12: f64) -> f64 {
567 let f1 = if sigma1 > 0.0 {
568 sigma1 / self.xt
569 } else {
570 sigma1.abs() / self.xc
571 };
572 let f2 = if sigma2 > 0.0 {
573 sigma2 / self.yt
574 } else {
575 sigma2.abs() / self.yc
576 };
577 let f6 = tau12.abs() / self.s12;
578 f1.max(f2).max(f6)
579 }
580}
581
582#[derive(Debug, Clone)]
584pub struct PuckCriterion {
585 pub xt: f64,
587 pub xc: f64,
589 pub yt: f64,
591 pub yc: f64,
593 pub s12: f64,
595 pub theta_fp_deg: f64,
597}
598
599impl PuckCriterion {
600 pub fn new(xt: f64, xc: f64, yt: f64, yc: f64, s12: f64) -> Self {
602 Self {
603 xt,
604 xc,
605 yt,
606 yc,
607 s12,
608 theta_fp_deg: 53.0,
609 }
610 }
611
612 pub fn fiber_fracture_index(&self, sigma1: f64) -> f64 {
614 if sigma1 >= 0.0 {
615 sigma1 / self.xt
616 } else {
617 sigma1.abs() / self.xc
618 }
619 }
620
621 pub fn iff_mode_a(&self, sigma2: f64, tau12: f64) -> f64 {
623 if sigma2 < 0.0 {
624 return 0.0;
625 }
626 let p12 = 0.27; ((tau12 / self.s12).powi(2)
628 + (1.0 - p12 * self.yt / self.s12).powi(2) * (sigma2 / self.yt).powi(2))
629 .sqrt()
630 + p12 * sigma2 / self.s12
631 }
632
633 pub fn iff_mode_b(&self, sigma2: f64, tau12: f64) -> f64 {
635 if sigma2 >= 0.0 {
636 return 0.0;
637 }
638 let p12c = 0.32;
639 let ra = self.s12 / (2.0 * p12c);
640 let term1 = (tau12 / (2.0 * (1.0 + p12c) * self.s12 * ra)).powi(2);
641 let term2 = (sigma2 / self.yc).powi(2);
642 (term1 + term2).sqrt() + p12c * sigma2 / self.yc
643 }
644}
645
646#[derive(Debug, Clone)]
648pub struct ProgressiveDamage {
649 pub degradation: Vec<f64>,
651 pub criterion: TsaiWuCriterion,
653 pub reduction_factor: f64,
655}
656
657impl ProgressiveDamage {
658 pub fn new(n_plies: usize, criterion: TsaiWuCriterion, reduction_factor: f64) -> Self {
660 Self {
661 degradation: vec![1.0; n_plies],
662 criterion,
663 reduction_factor,
664 }
665 }
666
667 pub fn apply_load_step(&mut self, stresses: &[[f64; 3]]) -> usize {
670 let mut count = 0;
671 for (i, s) in stresses.iter().enumerate() {
672 if self.degradation[i] > 1e-6 {
673 let fi = self.criterion.failure_index(s[0], s[1], s[2]);
674 if fi >= 1.0 {
675 self.degradation[i] *= self.reduction_factor;
676 count += 1;
677 }
678 }
679 }
680 count
681 }
682
683 pub fn all_plies_failed(&self) -> bool {
685 self.degradation.iter().all(|&d| d < 0.01)
686 }
687
688 pub fn failed_count(&self) -> usize {
690 self.degradation.iter().filter(|&&d| d < 0.99).count()
691 }
692}
693
694#[derive(Debug, Clone)]
696pub struct DelaminationModel {
697 pub g_ic: f64,
699 pub g_iic: f64,
701 pub bk_exponent: f64,
703}
704
705impl DelaminationModel {
706 pub fn new(g_ic: f64, g_iic: f64, bk_exponent: f64) -> Self {
708 Self {
709 g_ic,
710 g_iic,
711 bk_exponent,
712 }
713 }
714
715 pub fn bk_toughness(&self, g_i: f64, g_ii: f64) -> f64 {
717 let g_total = g_i + g_ii;
718 if g_total < 1e-30 {
719 return self.g_ic;
720 }
721 let mode_ratio = g_ii / g_total;
722 self.g_ic + (self.g_iic - self.g_ic) * mode_ratio.powf(self.bk_exponent)
723 }
724
725 pub fn delamination_occurs(&self, g_i: f64, g_ii: f64) -> bool {
727 let g_total = g_i + g_ii;
728 let g_c = self.bk_toughness(g_i, g_ii);
729 g_total >= g_c
730 }
731
732 pub fn failure_index_quadratic(&self, g_i: f64, g_ii: f64) -> f64 {
734 (g_i / self.g_ic).powi(2) + (g_ii / self.g_iic).powi(2)
735 }
736}
737
738#[derive(Debug, Clone)]
740pub struct WovenComposite {
741 pub weave_style: WeaveStyle,
743 pub crimp_angle_deg: f64,
745 pub vf: f64,
747 pub warp_weft_ratio: f64,
749}
750
751#[derive(Debug, Clone, PartialEq)]
753pub enum WeaveStyle {
754 Plain,
756 Twill,
758 Satin {
760 harness: u32,
762 },
763}
764
765impl WovenComposite {
766 pub fn plain(crimp_angle_deg: f64, vf: f64) -> Self {
768 Self {
769 weave_style: WeaveStyle::Plain,
770 crimp_angle_deg,
771 vf,
772 warp_weft_ratio: 1.0,
773 }
774 }
775
776 pub fn crimp_modulus_factor(&self) -> f64 {
778 let theta = self.crimp_angle_deg.to_radians();
779 theta.cos().powi(4)
780 }
781
782 pub fn approximate_modulus(&self, fiber_e: f64, matrix_e: f64) -> f64 {
784 let rom = fiber_e * self.vf + matrix_e * (1.0 - self.vf);
785 rom * self.crimp_modulus_factor()
786 }
787
788 pub fn interlacement_density(&self) -> f64 {
790 match &self.weave_style {
791 WeaveStyle::Plain => 1.0,
792 WeaveStyle::Twill => 0.5,
793 WeaveStyle::Satin { harness } => 1.0 / (*harness as f64),
794 }
795 }
796}
797
798#[derive(Debug, Clone)]
800pub struct HomogenizationScheme;
801
802impl HomogenizationScheme {
803 pub fn voigt(e_fiber: f64, e_matrix: f64, vf: f64) -> f64 {
805 e_fiber * vf + e_matrix * (1.0 - vf)
806 }
807
808 pub fn reuss(e_fiber: f64, e_matrix: f64, vf: f64) -> f64 {
810 1.0 / (vf / e_fiber + (1.0 - vf) / e_matrix)
811 }
812
813 pub fn hill(e_fiber: f64, e_matrix: f64, vf: f64) -> f64 {
815 (Self::voigt(e_fiber, e_matrix, vf) + Self::reuss(e_fiber, e_matrix, vf)) / 2.0
816 }
817
818 pub fn hashin_shtrikman_upper(k1: f64, g1: f64, k2: f64, vf: f64) -> f64 {
820 let vm = 1.0 - vf;
821 k1 + vm / (1.0 / (k2 - k1) + 3.0 * vf / (3.0 * k1 + 4.0 * g1))
822 }
823
824 pub fn verify_bounds(e_fiber: f64, e_matrix: f64, vf: f64) -> bool {
826 Self::voigt(e_fiber, e_matrix, vf) >= Self::reuss(e_fiber, e_matrix, vf)
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 fn carbon_epoxy() -> (FiberProperties, MatrixProperties) {
835 (FiberProperties::carbon_t300(), MatrixProperties::epoxy())
836 }
837
838 #[test]
839 fn test_fiber_properties_carbon() {
840 let f = FiberProperties::carbon_t300();
841 assert!(f.e1 > 200e9);
842 assert!(f.volume_fraction > 0.0 && f.volume_fraction < 1.0);
843 }
844
845 #[test]
846 fn test_matrix_shear_modulus() {
847 let m = MatrixProperties::epoxy();
848 let g = m.shear_modulus();
849 assert!((g - m.modulus / (2.0 * (1.0 + m.poisson_ratio))).abs() < 1.0);
850 }
851
852 #[test]
853 fn test_halpin_tsai_e11_rom() {
854 let (f, m) = carbon_epoxy();
855 let ht = HalpinTsai::new(f.clone(), m.clone());
856 let rom = f.e1 * f.volume_fraction + m.modulus * (1.0 - f.volume_fraction);
858 assert!((ht.e11() - rom).abs() < 1.0);
859 }
860
861 #[test]
862 fn test_halpin_tsai_e22_greater_reuss() {
863 let (f, m) = carbon_epoxy();
864 let ht = HalpinTsai::new(f, m);
865 assert!(ht.e22() >= ht.reuss_e22() - 1e6);
867 }
868
869 #[test]
870 fn test_halpin_tsai_nu12_positive() {
871 let (f, m) = carbon_epoxy();
872 let ht = HalpinTsai::new(f, m);
873 assert!(ht.nu12() > 0.0 && ht.nu12() < 0.5);
874 }
875
876 #[test]
877 fn test_halpin_tsai_nu21_reciprocal() {
878 let (f, m) = carbon_epoxy();
879 let ht = HalpinTsai::new(f, m);
880 let lhs = ht.nu21() / ht.e22();
882 let rhs = ht.nu12() / ht.e11();
883 assert!((lhs - rhs).abs() < 1e-15);
884 }
885
886 #[test]
887 fn test_voigt_reuss_bounds_fiber() {
888 let (f, m) = carbon_epoxy();
889 let ht = HalpinTsai::new(f, m);
890 assert!(ht.voigt_e11() >= ht.reuss_e22());
891 }
892
893 #[test]
894 fn test_mori_tanaka_bulk_positive() {
895 let (f, m) = carbon_epoxy();
896 let mt = MoriTanakaComposite::new(f, m);
897 assert!(mt.effective_bulk_modulus() > 0.0);
898 }
899
900 #[test]
901 fn test_mori_tanaka_shear_positive() {
902 let (f, m) = carbon_epoxy();
903 let mt = MoriTanakaComposite::new(f, m);
904 assert!(mt.effective_shear_modulus() > 0.0);
905 }
906
907 #[test]
908 fn test_lamina_q_matrix_positive_definite() {
909 let l = Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.125e-3, 0.0);
910 let q = l.q_matrix();
911 assert!(q[0] > 0.0 && q[2] > 0.0 && q[3] > 0.0);
912 }
913
914 #[test]
915 fn test_lamina_nu21_reciprocal() {
916 let l = Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.125e-3, 0.0);
917 let nu21 = l.nu21();
918 let expected = 0.28 * 8e9 / 130e9;
920 assert!((nu21 - expected).abs() < 1e-15);
921 }
922
923 #[test]
924 fn test_q_bar_zero_angle_equals_q() {
925 let l = Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.125e-3, 0.0);
926 let q = l.q_matrix();
927 let qb = l.q_bar();
928 assert!((qb[0] - q[0]).abs() < 1e3); assert!((qb[3] - q[2]).abs() < 1e3); }
931
932 #[test]
933 fn test_clt_total_thickness() {
934 let l1 = Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.25e-3, 0.0);
935 let l2 = Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.25e-3, 90.0);
936 let clt = ClassicalLaminateTheory::new(vec![l1, l2]);
937 assert!((clt.total_thickness() - 0.5e-3).abs() < 1e-10);
938 }
939
940 #[test]
941 fn test_clt_z_coords_symmetric() {
942 let l1 = Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.25e-3, 0.0);
943 let l2 = Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.25e-3, 90.0);
944 let clt = ClassicalLaminateTheory::new(vec![l1, l2]);
945 let z = clt.z_coords();
946 assert_eq!(z.len(), 3);
947 assert!((z[0] + z[2]).abs() < 1e-15); }
949
950 #[test]
951 fn test_clt_a_matrix_nonzero() {
952 let l1 = Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.25e-3, 0.0);
953 let l2 = Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.25e-3, 90.0);
954 let clt = ClassicalLaminateTheory::new(vec![l1, l2]);
955 let a = clt.a_matrix();
956 assert!(a[0] > 0.0 && a[3] > 0.0);
957 }
958
959 #[test]
960 fn test_clt_symmetric_laminate_b_zero() {
961 let mk_l = |angle: f64| Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.125e-3, angle);
963 let clt = ClassicalLaminateTheory::new(vec![mk_l(0.0), mk_l(90.0), mk_l(90.0), mk_l(0.0)]);
964 assert!(clt.is_symmetric());
965 }
966
967 #[test]
968 fn test_clt_balanced_laminate() {
969 let mk_l = |angle: f64| Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.125e-3, angle);
971 let clt = ClassicalLaminateTheory::new(vec![
972 mk_l(0.0),
973 mk_l(45.0),
974 mk_l(-45.0),
975 mk_l(90.0),
976 mk_l(90.0),
977 mk_l(-45.0),
978 mk_l(45.0),
979 mk_l(0.0),
980 ]);
981 let a = clt.a_matrix();
983 assert!(a[2].abs() < a[0] * 1e-10);
984 }
985
986 #[test]
987 fn test_tsai_wu_convexity() {
988 let tw = TsaiWuCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6, -0.5);
989 assert!(tw.is_convex());
990 }
991
992 #[test]
993 fn test_tsai_wu_origin_safe() {
994 let tw = TsaiWuCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6, -0.5);
995 assert!(!tw.fails(0.0, 0.0, 0.0));
996 assert!(tw.failure_index(0.0, 0.0, 0.0).abs() < 1e-10);
997 }
998
999 #[test]
1000 fn test_tsai_wu_longitudinal_tensile_failure() {
1001 let tw = TsaiWuCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6, -0.5);
1002 assert!(tw.fails(1600e6, 0.0, 0.0));
1004 }
1005
1006 #[test]
1007 fn test_tsai_wu_strength_ratio_positive() {
1008 let tw = TsaiWuCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6, -0.5);
1009 let r = tw.strength_ratio(500e6, 10e6, 5e6);
1010 assert!(r > 0.0);
1011 }
1012
1013 #[test]
1014 fn test_max_stress_safe_origin() {
1015 let ms = MaxStressCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6);
1016 assert!(!ms.fails(0.0, 0.0, 0.0));
1017 }
1018
1019 #[test]
1020 fn test_max_stress_fiber_failure() {
1021 let ms = MaxStressCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6);
1022 assert!(ms.fails(1600e6, 0.0, 0.0));
1023 }
1024
1025 #[test]
1026 fn test_max_stress_shear_failure() {
1027 let ms = MaxStressCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6);
1028 assert!(ms.fails(0.0, 0.0, 80e6));
1029 }
1030
1031 #[test]
1032 fn test_max_stress_failure_index_range() {
1033 let ms = MaxStressCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6);
1034 let fi = ms.failure_index(500e6, 20e6, 30e6);
1035 assert!(fi > 0.0);
1036 }
1037
1038 #[test]
1039 fn test_puck_fiber_fracture_tension() {
1040 let p = PuckCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6);
1041 let fi = p.fiber_fracture_index(1600e6);
1042 assert!(fi > 1.0);
1043 }
1044
1045 #[test]
1046 fn test_puck_iff_mode_a_transverse_tension() {
1047 let p = PuckCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6);
1048 let fi = p.iff_mode_a(60e6, 0.0);
1050 assert!(fi > 1.0);
1051 }
1052
1053 #[test]
1054 fn test_progressive_damage_count() {
1055 let tw = TsaiWuCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6, -0.5);
1056 let mut pd = ProgressiveDamage::new(3, tw, 0.01);
1057 let stresses = [
1058 [1600e6_f64, 0.0, 0.0],
1059 [100e6, 10e6, 5e6],
1060 [1700e6, 0.0, 0.0],
1061 ];
1062 let failed = pd.apply_load_step(&stresses);
1063 assert_eq!(failed, 2);
1064 }
1065
1066 #[test]
1067 fn test_progressive_damage_not_all_failed() {
1068 let tw = TsaiWuCriterion::new(1500e6, 1200e6, 50e6, 200e6, 70e6, -0.5);
1069 let pd = ProgressiveDamage::new(4, tw, 0.5);
1070 assert!(!pd.all_plies_failed());
1071 }
1072
1073 #[test]
1074 fn test_delamination_bk_toughness_mode1() {
1075 let dm = DelaminationModel::new(200.0, 800.0, 2.284);
1076 let gc = dm.bk_toughness(200.0, 0.0);
1077 assert!((gc - 200.0).abs() < 1e-6);
1078 }
1079
1080 #[test]
1081 fn test_delamination_bk_toughness_mode2() {
1082 let dm = DelaminationModel::new(200.0, 800.0, 2.284);
1083 let gc = dm.bk_toughness(0.0, 800.0);
1084 assert!((gc - 800.0).abs() < 1.0);
1085 }
1086
1087 #[test]
1088 fn test_delamination_failure_detection() {
1089 let dm = DelaminationModel::new(200.0, 800.0, 2.284);
1090 assert!(dm.delamination_occurs(300.0, 0.0)); assert!(!dm.delamination_occurs(100.0, 0.0)); }
1093
1094 #[test]
1095 fn test_woven_composite_crimp_factor_leq1() {
1096 let wc = WovenComposite::plain(5.0, 0.5);
1097 let factor = wc.crimp_modulus_factor();
1098 assert!(factor > 0.0 && factor <= 1.0);
1099 }
1100
1101 #[test]
1102 fn test_woven_composite_modulus_less_than_rom() {
1103 let wc = WovenComposite::plain(5.0, 0.5);
1104 let rom = 200e9 * 0.5 + 3e9 * 0.5;
1105 let woven_mod = wc.approximate_modulus(200e9, 3e9);
1106 assert!(woven_mod < rom);
1107 }
1108
1109 #[test]
1110 fn test_woven_satin_lower_interlacement() {
1111 let plain = WovenComposite::plain(5.0, 0.5);
1112 let satin = WovenComposite {
1113 weave_style: WeaveStyle::Satin { harness: 8 },
1114 crimp_angle_deg: 5.0,
1115 vf: 0.5,
1116 warp_weft_ratio: 1.0,
1117 };
1118 assert!(plain.interlacement_density() > satin.interlacement_density());
1119 }
1120
1121 #[test]
1122 fn test_homogenization_voigt_reuss_bounds() {
1123 let vf = 0.6;
1124 let ef = 230e9;
1125 let em = 3.5e9;
1126 assert!(HomogenizationScheme::verify_bounds(ef, em, vf));
1127 }
1128
1129 #[test]
1130 fn test_homogenization_voigt_gt_hill_gt_reuss() {
1131 let vf = 0.6;
1132 let ef = 230e9;
1133 let em = 3.5e9;
1134 let v = HomogenizationScheme::voigt(ef, em, vf);
1135 let h = HomogenizationScheme::hill(ef, em, vf);
1136 let r = HomogenizationScheme::reuss(ef, em, vf);
1137 assert!(v >= h);
1138 assert!(h >= r);
1139 }
1140
1141 #[test]
1142 fn test_homogenization_vf0_gives_matrix() {
1143 let em = 3.5e9;
1144 let ef = 230e9;
1145 let v = HomogenizationScheme::voigt(ef, em, 0.0);
1146 let r = HomogenizationScheme::reuss(ef, em, 0.0);
1147 assert!((v - em).abs() < 1.0);
1148 assert!((r - em).abs() < 1.0);
1149 }
1150
1151 #[test]
1152 fn test_homogenization_vf1_gives_fiber() {
1153 let em = 3.5e9;
1154 let ef = 230e9;
1155 let v = HomogenizationScheme::voigt(ef, em, 1.0);
1156 let r = HomogenizationScheme::reuss(ef, em, 1.0);
1157 assert!((v - ef).abs() < 1.0);
1158 assert!((r - ef).abs() < 1.0);
1159 }
1160
1161 #[test]
1162 fn test_laminate_response_symmetric() {
1163 let mk_l = |angle: f64| Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.125e-3, angle);
1164 let clt = ClassicalLaminateTheory::new(vec![mk_l(0.0), mk_l(90.0), mk_l(90.0), mk_l(0.0)]);
1165 let a = clt.a_matrix();
1166 let resp = LaminateResponse::from_n_symmetric([1000.0, 0.0, 0.0], a);
1167 assert!(resp.midplane_strains[0].abs() > 0.0);
1168 }
1169
1170 #[test]
1171 fn test_clt_d_matrix_positive_diagonal() {
1172 let mk_l = |angle: f64| Lamina::new(130e9, 8e9, 4.5e9, 0.28, 0.125e-3, angle);
1173 let clt = ClassicalLaminateTheory::new(vec![mk_l(0.0), mk_l(90.0)]);
1174 let d = clt.d_matrix();
1175 assert!(d[0] > 0.0 && d[3] > 0.0 && d[5] > 0.0);
1176 }
1177
1178 #[test]
1179 fn test_mori_tanaka_bounds() {
1180 let (f, m) = carbon_epoxy();
1181 let em = m.modulus;
1182 let ef = f.e1;
1183 let mt = MoriTanakaComposite::new(f, m);
1184 let k_eff = mt.effective_bulk_modulus();
1185 let km = em / (3.0 * (1.0 - 2.0 * 0.35));
1186 let kf = ef / (3.0 * (1.0 - 2.0 * 0.27));
1187 assert!(k_eff >= km.min(kf) - 1e9);
1189 assert!(k_eff <= km.max(kf) + 1e9);
1190 }
1191}