1#[allow(dead_code)]
12#[derive(Debug, Clone, PartialEq)]
13pub struct FiberProperties {
14 pub modulus_longitudinal: f64,
16 pub modulus_transverse: f64,
18 pub strength_longitudinal: f64,
20 pub volume_fraction: f64,
22 pub orientation: f64,
24}
25
26impl FiberProperties {
27 pub fn new(
29 modulus_longitudinal: f64,
30 modulus_transverse: f64,
31 strength_longitudinal: f64,
32 volume_fraction: f64,
33 orientation: f64,
34 ) -> Self {
35 Self {
36 modulus_longitudinal,
37 modulus_transverse,
38 strength_longitudinal,
39 volume_fraction,
40 orientation,
41 }
42 }
43}
44
45#[allow(dead_code)]
47#[derive(Debug, Clone, PartialEq)]
48pub struct MatrixProperties {
49 pub modulus: f64,
51 pub poisson_ratio: f64,
53}
54
55impl MatrixProperties {
56 pub fn new(modulus: f64, poisson_ratio: f64) -> Self {
58 Self {
59 modulus,
60 poisson_ratio,
61 }
62 }
63}
64
65#[allow(dead_code)]
67#[derive(Debug, Clone, PartialEq)]
68pub struct Ply {
69 pub angle: f64,
71 pub thickness: f64,
73 pub e11: f64,
75 pub e22: f64,
77 pub g12: f64,
79 pub nu12: f64,
81}
82
83impl Ply {
84 #[allow(clippy::too_many_arguments)]
86 pub fn new(angle: f64, thickness: f64, e11: f64, e22: f64, g12: f64, nu12: f64) -> Self {
87 Self {
88 angle,
89 thickness,
90 e11,
91 e22,
92 g12,
93 nu12,
94 }
95 }
96}
97
98#[allow(dead_code)]
100#[derive(Debug, Clone)]
101pub struct CompositeLayup {
102 pub plies: Vec<Ply>,
104}
105
106impl CompositeLayup {
107 pub fn new(plies: Vec<Ply>) -> Self {
109 Self { plies }
110 }
111
112 pub fn total_thickness(&self) -> f64 {
114 self.plies.iter().map(|p| p.thickness).sum()
115 }
116}
117
118pub fn rule_of_mixtures_modulus(e_fiber: f64, e_matrix: f64, vf: f64) -> (f64, f64) {
129 let vm = 1.0 - vf;
130 let e_voigt = e_fiber * vf + e_matrix * vm;
131 let e_reuss = 1.0 / (vf / e_fiber + vm / e_matrix);
132 (e_voigt, e_reuss)
133}
134
135pub fn halpin_tsai(
145 fiber: &FiberProperties,
146 matrix: &MatrixProperties,
147 xi_e: f64,
148 xi_g: f64,
149) -> (f64, f64, f64) {
150 let vf = fiber.volume_fraction;
151 let vm = 1.0 - vf;
152
153 let e11 = fiber.modulus_longitudinal * vf + matrix.modulus * vm;
155
156 let eta_e = (fiber.modulus_transverse / matrix.modulus - 1.0)
158 / (fiber.modulus_transverse / matrix.modulus + xi_e);
159 let e22 = matrix.modulus * (1.0 + xi_e * eta_e * vf) / (1.0 - eta_e * vf);
160
161 let g_fiber = fiber.modulus_longitudinal / (2.0 * (1.0 + 0.25)); let g_matrix = matrix.modulus / (2.0 * (1.0 + matrix.poisson_ratio));
164 let eta_g = (g_fiber / g_matrix - 1.0) / (g_fiber / g_matrix + xi_g);
165 let g12 = g_matrix * (1.0 + xi_g * eta_g * vf) / (1.0 - eta_g * vf);
166
167 (e11, e22, g12)
168}
169
170pub fn classical_lamination_theory(layup: &CompositeLayup) -> ([f64; 6], [f64; 6], [f64; 6]) {
180 let mut a = [0.0f64; 6];
181 let mut b = [0.0f64; 6];
182 let mut d = [0.0f64; 6];
183
184 let total_h = layup.total_thickness();
186 let mut z_bottom = -total_h / 2.0;
187
188 for ply in &layup.plies {
189 let z_top = z_bottom + ply.thickness;
190 let z_mid = (z_bottom + z_top) / 2.0;
191
192 let nu21 = ply.nu12 * ply.e22 / ply.e11;
194 let denom = 1.0 - ply.nu12 * nu21;
195 let q11 = ply.e11 / denom;
196 let q22 = ply.e22 / denom;
197 let q12 = ply.nu12 * ply.e22 / denom;
198 let q66 = ply.g12;
199
200 let c = ply.angle.cos();
202 let s = ply.angle.sin();
203 let c2 = c * c;
204 let s2 = s * s;
205 let cs = c * s;
206
207 let qb11 = q11 * c2 * c2 + 2.0 * (q12 + 2.0 * q66) * s2 * c2 + q22 * s2 * s2;
209 let qb12 = (q11 + q22 - 4.0 * q66) * s2 * c2 + q12 * (c2 * c2 + s2 * s2);
210 let qb16 = (q11 - q12 - 2.0 * q66) * cs * c2 - (q22 - q12 - 2.0 * q66) * cs * s2;
211 let qb22 = q11 * s2 * s2 + 2.0 * (q12 + 2.0 * q66) * s2 * c2 + q22 * c2 * c2;
212 let qb26 = (q11 - q12 - 2.0 * q66) * cs * s2 - (q22 - q12 - 2.0 * q66) * cs * c2;
213 let qb66 = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * s2 * c2 + q66 * (c2 * c2 + s2 * s2);
214
215 let qbar = [qb11, qb12, qb16, qb22, qb26, qb66];
216
217 let dz = z_top - z_bottom;
218 let dz2 = (z_top * z_top - z_bottom * z_bottom) / 2.0;
219 let dz3 = (z_top * z_top * z_top - z_bottom * z_bottom * z_bottom) / 3.0;
220
221 for i in 0..6 {
222 a[i] += qbar[i] * dz;
223 b[i] += qbar[i] * dz2;
224 d[i] += qbar[i] * dz3;
225 }
226
227 let _ = z_mid;
229 z_bottom = z_top;
230 }
231
232 (a, b, d)
233}
234
235#[allow(clippy::too_many_arguments)]
247pub fn failure_criterion_tsai_wu(
248 sigma: [f64; 3],
249 f1t: f64,
250 f1c: f64,
251 f2t: f64,
252 f2c: f64,
253 f12: f64,
254) -> f64 {
255 let [s1, s2, t12] = sigma;
256
257 let f1 = 1.0 / f1t - 1.0 / f1c;
259 let f2 = 1.0 / f2t - 1.0 / f2c;
260 let f11 = 1.0 / (f1t * f1c);
261 let f22 = 1.0 / (f2t * f2c);
262 let f66 = 1.0 / (f12 * f12);
263 let f12_int = -0.5 * (f11 * f22).sqrt();
265
266 f1 * s1 + f2 * s2 + f11 * s1 * s1 + f22 * s2 * s2 + f66 * t12 * t12 + 2.0 * f12_int * s1 * s2
267}
268
269pub fn interlaminar_shear(p_max: f64, width: f64, thickness: f64) -> f64 {
278 0.75 * p_max / (width * thickness)
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use std::f64::consts::PI;
286
287 const EPS: f64 = 1e-9;
288
289 #[test]
294 fn test_fiber_props_new() {
295 let f = FiberProperties::new(230e9, 15e9, 3.5e9, 0.6, 0.0);
296 assert_eq!(f.modulus_longitudinal, 230e9);
297 assert_eq!(f.volume_fraction, 0.6);
298 }
299
300 #[test]
301 fn test_matrix_props_new() {
302 let m = MatrixProperties::new(3.5e9, 0.35);
303 assert_eq!(m.modulus, 3.5e9);
304 assert!((m.poisson_ratio - 0.35).abs() < EPS);
305 }
306
307 #[test]
312 fn test_rom_voigt_bounds() {
313 let (ev, er) = rule_of_mixtures_modulus(200e9, 3e9, 0.5);
315 assert!(ev > er, "Voigt {ev} should exceed Reuss {er}");
316 }
317
318 #[test]
319 fn test_rom_pure_fiber() {
320 let (ev, er) = rule_of_mixtures_modulus(200e9, 3e9, 1.0);
322 assert!((ev - 200e9).abs() < 1.0, "Voigt at vf=1 should be E_fiber");
323 assert!((er - 200e9).abs() < 1.0, "Reuss at vf=1 should be E_fiber");
324 }
325
326 #[test]
327 fn test_rom_pure_matrix() {
328 let (ev, er) = rule_of_mixtures_modulus(200e9, 3e9, 0.0);
330 assert!((ev - 3e9).abs() < 1.0);
331 assert!((er - 3e9).abs() < 1.0);
332 }
333
334 #[test]
335 fn test_rom_midpoint_voigt() {
336 let ef = 100.0;
338 let em = 50.0;
339 let (ev, _) = rule_of_mixtures_modulus(ef, em, 0.5);
340 assert!((ev - 75.0).abs() < EPS);
341 }
342
343 #[test]
344 fn test_rom_reuss_harmonic_mean() {
345 let ef = 100.0;
346 let em = 50.0;
347 let vf = 0.4;
348 let (_, er) = rule_of_mixtures_modulus(ef, em, vf);
349 let expected = 1.0 / (vf / ef + (1.0 - vf) / em);
350 assert!((er - expected).abs() < EPS);
351 }
352
353 #[test]
354 fn test_rom_symmetry_voigt() {
355 let (ev1, _) = rule_of_mixtures_modulus(100.0, 100.0, 0.3);
358 let (ev2, _) = rule_of_mixtures_modulus(100.0, 100.0, 0.7);
359 assert!((ev1 - ev2).abs() < EPS, "Degenerate: ev1={ev1}, ev2={ev2}");
360 }
361
362 #[test]
367 fn test_halpin_tsai_e11_rule_of_mixtures() {
368 let fiber = FiberProperties::new(230e9, 15e9, 3.5e9, 0.6, 0.0);
369 let matrix = MatrixProperties::new(3.5e9, 0.35);
370 let (e11, _, _) = halpin_tsai(&fiber, &matrix, 2.0, 1.0);
371 let (ev, _) = rule_of_mixtures_modulus(
372 fiber.modulus_longitudinal,
373 matrix.modulus,
374 fiber.volume_fraction,
375 );
376 assert!(
377 (e11 - ev).abs() < 1.0,
378 "E11 from HT should equal Voigt ROM: e11={e11}, ev={ev}"
379 );
380 }
381
382 #[test]
383 fn test_halpin_tsai_e22_positive() {
384 let fiber = FiberProperties::new(230e9, 15e9, 3.5e9, 0.6, 0.0);
385 let matrix = MatrixProperties::new(3.5e9, 0.35);
386 let (_, e22, _) = halpin_tsai(&fiber, &matrix, 2.0, 1.0);
387 assert!(e22 > 0.0, "E22 should be positive, got {e22}");
388 }
389
390 #[test]
391 fn test_halpin_tsai_g12_positive() {
392 let fiber = FiberProperties::new(230e9, 15e9, 3.5e9, 0.6, 0.0);
393 let matrix = MatrixProperties::new(3.5e9, 0.35);
394 let (_, _, g12) = halpin_tsai(&fiber, &matrix, 2.0, 1.0);
395 assert!(g12 > 0.0, "G12 should be positive, got {g12}");
396 }
397
398 #[test]
399 fn test_halpin_tsai_e22_above_matrix_modulus() {
400 let fiber = FiberProperties::new(230e9, 15e9, 3.5e9, 0.5, 0.0);
402 let matrix = MatrixProperties::new(3.5e9, 0.35);
403 let (_, e22, _) = halpin_tsai(&fiber, &matrix, 2.0, 1.0);
404 assert!(e22 > matrix.modulus, "E22 should exceed matrix modulus");
405 }
406
407 #[test]
412 fn test_clt_a_matrix_positive_diagonal() {
413 let ply_0 = Ply::new(0.0, 0.001, 140e9, 10e9, 5e9, 0.3);
415 let ply_90 = Ply::new(PI / 2.0, 0.001, 140e9, 10e9, 5e9, 0.3);
416 let layup = CompositeLayup::new(vec![
417 ply_0,
418 ply_90.clone(),
419 ply_90,
420 Ply::new(0.0, 0.001, 140e9, 10e9, 5e9, 0.3),
421 ]);
422 let (a, _, _) = classical_lamination_theory(&layup);
423 assert!(a[0] > 0.0, "A11 should be positive, got {}", a[0]);
424 assert!(a[3] > 0.0, "A22 should be positive, got {}", a[3]);
425 }
426
427 #[test]
428 fn test_clt_b_matrix_zero_symmetric() {
429 let ply = Ply::new(0.0, 0.001, 140e9, 10e9, 5e9, 0.3);
431 let ply90 = Ply::new(PI / 2.0, 0.001, 140e9, 10e9, 5e9, 0.3);
432 let layup = CompositeLayup::new(vec![ply.clone(), ply90.clone(), ply90, ply]);
433 let (_, b, _) = classical_lamination_theory(&layup);
434 for (i, &bi) in b.iter().enumerate() {
435 assert!(
436 bi.abs() < 1.0,
437 "B[{i}] = {bi} should be ~0 for symmetric laminate"
438 );
439 }
440 }
441
442 #[test]
443 fn test_clt_single_ply_thickness() {
444 let h = 0.002;
446 let ply = Ply::new(0.0, h, 140e9, 10e9, 5e9, 0.3);
447 let layup = CompositeLayup::new(vec![ply]);
448 let (a, _, _) = classical_lamination_theory(&layup);
449 let nu21 = 0.3 * 10e9 / 140e9;
450 let q11 = 140e9 / (1.0 - 0.3 * nu21);
451 let expected_a11 = q11 * h;
452 let rel_err = (a[0] - expected_a11).abs() / expected_a11;
453 assert!(rel_err < 1e-6, "A11={} expected={expected_a11}", a[0]);
454 }
455
456 #[test]
457 fn test_clt_d_matrix_positive_diagonal() {
458 let ply = Ply::new(0.0, 0.002, 140e9, 10e9, 5e9, 0.3);
459 let layup = CompositeLayup::new(vec![ply]);
460 let (_, _, d) = classical_lamination_theory(&layup);
461 assert!(d[0] > 0.0, "D11 should be positive");
462 assert!(d[3] > 0.0, "D22 should be positive");
463 }
464
465 #[test]
470 fn test_tsai_wu_zero_stress() {
471 let fi = failure_criterion_tsai_wu([0.0, 0.0, 0.0], 1.5e9, 1.0e9, 0.05e9, 0.25e9, 0.07e9);
472 assert!((fi).abs() < EPS, "FI at zero stress should be ~0, got {fi}");
473 }
474
475 #[test]
476 fn test_tsai_wu_uniaxial_at_strength() {
477 let f1t = 1.5e9_f64;
479 let f1c = 1.0e9_f64;
480 let fi = failure_criterion_tsai_wu([f1t, 0.0, 0.0], f1t, f1c, 0.05e9, 0.25e9, 0.07e9);
482 assert!(
484 (fi - 1.0).abs() < 0.1,
485 "FI at longitudinal tensile strength should be ~1, got {fi}"
486 );
487 }
488
489 #[test]
490 fn test_tsai_wu_positive_above_threshold() {
491 let fi0 = failure_criterion_tsai_wu([0.0, 0.0, 0.0], 1.5e9, 1.0e9, 0.05e9, 0.25e9, 0.07e9);
493 let fi1 = failure_criterion_tsai_wu([0.0, 0.0, 1e7], 1.5e9, 1.0e9, 0.05e9, 0.25e9, 0.07e9);
494 assert!(fi1 > fi0, "Non-zero shear should increase FI");
495 }
496
497 #[test]
502 fn test_ilss_formula() {
503 let ilss = interlaminar_shear(1000.0, 0.02, 0.004);
504 let expected = 0.75 * 1000.0 / (0.02 * 0.004);
505 assert!((ilss - expected).abs() < EPS);
506 }
507
508 #[test]
509 fn test_ilss_scales_with_load() {
510 let i1 = interlaminar_shear(500.0, 0.02, 0.004);
511 let i2 = interlaminar_shear(1000.0, 0.02, 0.004);
512 assert!(
513 (i2 - 2.0 * i1).abs() < EPS,
514 "ILSS should double when load doubles"
515 );
516 }
517
518 #[test]
519 fn test_ilss_positive() {
520 let ilss = interlaminar_shear(800.0, 0.015, 0.003);
521 assert!(ilss > 0.0, "ILSS must be positive");
522 }
523
524 #[test]
529 fn test_total_thickness() {
530 let plies = vec![
531 Ply::new(0.0, 0.001, 140e9, 10e9, 5e9, 0.3),
532 Ply::new(PI / 4.0, 0.002, 140e9, 10e9, 5e9, 0.3),
533 ];
534 let layup = CompositeLayup::new(plies);
535 assert!((layup.total_thickness() - 0.003).abs() < EPS);
536 }
537
538 #[test]
539 fn test_ply_new_fields() {
540 let ply = Ply::new(PI / 4.0, 0.001, 140e9, 10e9, 5e9, 0.28);
541 assert!((ply.angle - PI / 4.0).abs() < EPS);
542 assert!((ply.nu12 - 0.28).abs() < EPS);
543 }
544
545 #[test]
550 fn test_rom_modulus_between_bounds() {
551 let (ev, er) = rule_of_mixtures_modulus(200e9, 5e9, 0.3);
552 assert!(ev >= er);
554 assert!(ev >= 5e9);
555 assert!(er >= 5e9);
556 assert!(ev <= 200e9);
557 }
558
559 #[test]
560 fn test_halpin_tsai_varies_with_vf() {
561 let f1 = FiberProperties::new(230e9, 15e9, 3.5e9, 0.3, 0.0);
562 let f2 = FiberProperties::new(230e9, 15e9, 3.5e9, 0.6, 0.0);
563 let matrix = MatrixProperties::new(3.5e9, 0.35);
564 let (e11_low, _, _) = halpin_tsai(&f1, &matrix, 2.0, 1.0);
565 let (e11_high, _, _) = halpin_tsai(&f2, &matrix, 2.0, 1.0);
566 assert!(
567 e11_high > e11_low,
568 "Higher fiber volume fraction should give higher E11"
569 );
570 }
571
572 #[test]
573 fn test_tsai_wu_compressive_stress_increases_fi() {
574 let f1t = 1.5e9_f64;
576 let f1c = 1.0e9_f64;
577 let fi_ten = failure_criterion_tsai_wu([1e8, 0.0, 0.0], f1t, f1c, 0.05e9, 0.25e9, 0.07e9);
578 let fi_comp = failure_criterion_tsai_wu([-1e8, 0.0, 0.0], f1t, f1c, 0.05e9, 0.25e9, 0.07e9);
579 assert!(fi_ten.is_finite());
581 assert!(fi_comp.is_finite());
582 }
583
584 #[test]
585 fn test_clt_quasi_isotropic_a_symmetry() {
586 let h = 0.001;
588 let e11 = 140e9;
589 let e22 = 10e9;
590 let g12 = 5e9;
591 let nu12 = 0.3;
592 let plies = vec![
593 Ply::new(0.0, h, e11, e22, g12, nu12),
594 Ply::new(PI / 3.0, h, e11, e22, g12, nu12),
595 Ply::new(-PI / 3.0, h, e11, e22, g12, nu12),
596 ];
597 let layup = CompositeLayup::new(plies);
598 let (a, _, _) = classical_lamination_theory(&layup);
599 let diff = (a[0] - a[3]).abs() / a[0];
600 assert!(
601 diff < 0.05,
602 "Quasi-isotropic laminate: A11={:.3e} should ≈ A22={:.3e} (rel diff={diff:.4})",
603 a[0],
604 a[3]
605 );
606 }
607
608 #[test]
609 fn test_tsai_wu_transverse_stress() {
610 let f2t = 0.05e9_f64;
612 let fi = failure_criterion_tsai_wu([0.0, f2t, 0.0], 1.5e9, 1.0e9, f2t, 0.25e9, 0.07e9);
613 assert!(
614 (fi - 1.0).abs() < 0.1,
615 "FI at transverse tensile strength should be ~1, got {fi}"
616 );
617 }
618
619 #[test]
620 fn test_halpin_tsai_xi_effect() {
621 let fiber = FiberProperties::new(230e9, 15e9, 3.5e9, 0.5, 0.0);
623 let matrix = MatrixProperties::new(3.5e9, 0.35);
624 let (_, e22_low, _) = halpin_tsai(&fiber, &matrix, 1.0, 1.0);
625 let (_, e22_high, _) = halpin_tsai(&fiber, &matrix, 4.0, 1.0);
626 assert!(e22_high > e22_low, "Higher ξ_E should give higher E22");
627 }
628}