1#![allow(dead_code)]
11#![allow(clippy::too_many_arguments)]
12
13use std::f64::consts::PI;
14
15#[derive(Debug, Clone, PartialEq)]
21pub struct Fiber {
22 pub youngs_modulus: f64,
24 pub poissons_ratio: f64,
26 pub density: f64,
28 pub tensile_strength: f64,
30 pub diameter_um: f64,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum FiberType {
41 Carbon,
43 Glass,
45 Kevlar,
47 Basalt,
49 Steel,
51}
52
53pub fn fiber_preset(ft: FiberType) -> Fiber {
62 match ft {
63 FiberType::Carbon => Fiber {
64 youngs_modulus: 230.0e9,
65 poissons_ratio: 0.20,
66 density: 1750.0,
67 tensile_strength: 3500.0e6,
68 diameter_um: 7.0,
69 },
70 FiberType::Glass => Fiber {
71 youngs_modulus: 72.0e9,
72 poissons_ratio: 0.22,
73 density: 2540.0,
74 tensile_strength: 3400.0e6,
75 diameter_um: 10.0,
76 },
77 FiberType::Kevlar => Fiber {
78 youngs_modulus: 125.0e9,
79 poissons_ratio: 0.36,
80 density: 1440.0,
81 tensile_strength: 3620.0e6,
82 diameter_um: 12.0,
83 },
84 FiberType::Basalt => Fiber {
85 youngs_modulus: 89.0e9,
86 poissons_ratio: 0.26,
87 density: 2650.0,
88 tensile_strength: 4840.0e6,
89 diameter_um: 15.0,
90 },
91 FiberType::Steel => Fiber {
92 youngs_modulus: 200.0e9,
93 poissons_ratio: 0.30,
94 density: 7800.0,
95 tensile_strength: 1200.0e6,
96 diameter_um: 200.0,
97 },
98 }
99}
100
101pub fn rule_of_mixtures_longitudinal(e_fiber: f64, e_matrix: f64, v_f: f64) -> f64 {
112 let v_m = 1.0 - v_f;
113 v_f * e_fiber + v_m * e_matrix
114}
115
116pub fn rule_of_mixtures_transverse(e_fiber: f64, e_matrix: f64, v_f: f64) -> f64 {
125 halpin_tsai_transverse(e_fiber, e_matrix, v_f, 2.0)
126}
127
128pub fn halpin_tsai_transverse(e_fiber: f64, e_matrix: f64, v_f: f64, xi: f64) -> f64 {
142 if e_matrix < 1e-30 {
143 return 0.0;
144 }
145 let ratio = e_fiber / e_matrix;
146 let eta = (ratio - 1.0) / (ratio + xi);
147 let denom = 1.0 - eta * v_f;
148 if denom.abs() < 1e-30 {
149 return e_matrix;
150 }
151 e_matrix * (1.0 + xi * eta * v_f) / denom
152}
153
154pub fn clt_in_plane_stiffness(plies: &[(f64, f64, f64, f64, f64, f64)]) -> [[f64; 3]; 3] {
170 let mut a = [[0.0f64; 3]; 3];
171 for &(e1, e2, g12, nu12, theta_deg, t) in plies {
172 let q = ply_reduced_stiffness(e1, e2, g12, nu12);
173 let qbar = transform_stiffness(q, theta_deg);
174 for i in 0..3 {
175 for j in 0..3 {
176 a[i][j] += qbar[i][j] * t;
177 }
178 }
179 }
180 a
181}
182
183fn ply_reduced_stiffness(e1: f64, e2: f64, g12: f64, nu12: f64) -> [[f64; 3]; 3] {
185 let nu21 = nu12 * e2 / e1.max(1e-30);
186 let delta = 1.0 - nu12 * nu21;
187 if delta.abs() < 1e-30 {
188 return [[0.0; 3]; 3];
189 }
190 let q11 = e1 / delta;
191 let q22 = e2 / delta;
192 let q12 = nu12 * e2 / delta;
193 let q66 = g12;
194 [[q11, q12, 0.0], [q12, q22, 0.0], [0.0, 0.0, q66]]
195}
196
197fn transform_stiffness(q: [[f64; 3]; 3], theta_deg: f64) -> [[f64; 3]; 3] {
199 let th = theta_deg.to_radians();
200 let c = th.cos();
201 let s = th.sin();
202 let c2 = c * c;
203 let s2 = s * s;
204 let _cs = c * s;
205 let c4 = c2 * c2;
206 let s4 = s2 * s2;
207 let c2s2 = c2 * s2;
208 let q11 = q[0][0];
209 let q12 = q[0][1];
210 let q22 = q[1][1];
211 let q66 = q[2][2];
212 let qb11 = q11 * c4 + 2.0 * (q12 + 2.0 * q66) * c2s2 + q22 * s4;
213 let qb12 = (q11 + q22 - 4.0 * q66) * c2s2 + q12 * (c4 + s4);
214 let qb22 = q11 * s4 + 2.0 * (q12 + 2.0 * q66) * c2s2 + q22 * c4;
215 let qb16 = (q11 - q12 - 2.0 * q66) * c * c2 * s - (q22 - q12 - 2.0 * q66) * s * s2 * c;
216 let qb26 = (q11 - q12 - 2.0 * q66) * c * s * s2 - (q22 - q12 - 2.0 * q66) * s * c * c2;
217 let qb66 = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * c2s2 + q66 * (c4 + s4);
218 [[qb11, qb12, qb16], [qb12, qb22, qb26], [qb16, qb26, qb66]]
219}
220
221pub fn micromechanics_shear(g_fiber: f64, g_matrix: f64, v_f: f64) -> f64 {
232 if g_matrix < 1e-30 {
233 return 0.0;
234 }
235 let ratio = g_fiber / g_matrix;
236 let eta = (ratio - 1.0) / (ratio + 1.0);
237 let denom = 1.0 - eta * v_f;
238 if denom.abs() < 1e-30 {
239 return g_matrix;
240 }
241 g_matrix * (1.0 + eta * v_f) / denom
242}
243
244pub fn critical_fiber_length(tensile_strength: f64, interfacial_shear: f64, diameter: f64) -> f64 {
257 if interfacial_shear.abs() < 1e-30 {
258 return f64::INFINITY;
259 }
260 tensile_strength * diameter / (2.0 * interfacial_shear)
261}
262
263pub fn fiber_pullout_energy(fiber: &Fiber, bond_length: f64, tau_i: f64) -> f64 {
276 let d = fiber.diameter_um * 1.0e-6; PI * d * bond_length * bond_length * tau_i / 2.0
278}
279
280pub fn winding_angle_effect(e1: f64, e2: f64, g12: f64, nu12: f64, theta_deg: f64) -> f64 {
292 let th = theta_deg.to_radians();
293 let c = th.cos();
294 let s = th.sin();
295 let c2 = c * c;
296 let s2 = s * s;
297 let inv = c2 * c2 / e1.max(1e-30)
298 + s2 * s2 / e2.max(1e-30)
299 + (1.0 / g12.max(1e-30) - 2.0 * nu12 / e1.max(1e-30)) * s2 * c2;
300 if inv.abs() < 1e-30 { 0.0 } else { 1.0 / inv }
301}
302
303#[cfg(test)]
308mod tests {
309 use super::*;
310
311 const EPS: f64 = 1e-6;
312
313 #[test]
315 fn test_carbon_stiffer_than_glass() {
316 let c = fiber_preset(FiberType::Carbon);
317 let g = fiber_preset(FiberType::Glass);
318 assert!(c.youngs_modulus > g.youngs_modulus);
319 }
320
321 #[test]
323 fn test_all_densities_positive() {
324 for ft in [
325 FiberType::Carbon,
326 FiberType::Glass,
327 FiberType::Kevlar,
328 FiberType::Basalt,
329 FiberType::Steel,
330 ] {
331 assert!(fiber_preset(ft).density > 0.0);
332 }
333 }
334
335 #[test]
337 fn test_all_diameters_positive() {
338 for ft in [
339 FiberType::Carbon,
340 FiberType::Glass,
341 FiberType::Kevlar,
342 FiberType::Basalt,
343 FiberType::Steel,
344 ] {
345 assert!(fiber_preset(ft).diameter_um > 0.0);
346 }
347 }
348
349 #[test]
351 fn test_rom_long_vf0() {
352 let e = rule_of_mixtures_longitudinal(200e9, 3e9, 0.0);
353 assert!((e - 3e9).abs() < EPS * 3e9);
354 }
355
356 #[test]
358 fn test_rom_long_vf1() {
359 let e = rule_of_mixtures_longitudinal(200e9, 3e9, 1.0);
360 assert!((e - 200e9).abs() < EPS * 200e9);
361 }
362
363 #[test]
365 fn test_rom_long_bounds() {
366 let e = rule_of_mixtures_longitudinal(230e9, 4e9, 0.6);
367 assert!((4e9..=230e9).contains(&e));
368 }
369
370 #[test]
372 fn test_rom_trans_vf0() {
373 let e = rule_of_mixtures_transverse(200e9, 3e9, 0.0);
374 assert!((e - 3e9).abs() < EPS * 3e9);
375 }
376
377 #[test]
379 fn test_rom_trans_bounds() {
380 let e = rule_of_mixtures_transverse(200e9, 4e9, 0.5);
381 assert!((4e9..=200e9).contains(&e));
382 }
383
384 #[test]
386 fn test_halpin_tsai_xi0() {
387 let e = halpin_tsai_transverse(100e9, 4e9, 0.0, 2.0);
390 assert!((e - 4e9).abs() < EPS * 4e9);
391 }
392
393 #[test]
395 fn test_halpin_tsai_higher_xi() {
396 let e1 = halpin_tsai_transverse(200e9, 4e9, 0.6, 1.0);
397 let e2 = halpin_tsai_transverse(200e9, 4e9, 0.6, 4.0);
398 assert!(e2 > e1, "Higher xi should give higher transverse modulus");
399 }
400
401 #[test]
403 fn test_clt_single_ply_0deg() {
404 let e1 = 140e9f64;
405 let e2 = 10e9f64;
406 let g12 = 5e9f64;
407 let nu12 = 0.3f64;
408 let t = 0.001f64;
409 let a = clt_in_plane_stiffness(&[(e1, e2, g12, nu12, 0.0, t)]);
410 let nu21 = nu12 * e2 / e1;
411 let delta = 1.0 - nu12 * nu21;
412 let q11 = e1 / delta;
413 assert!(
415 (a[0][0] - q11 * t).abs() < 1e3,
416 "A11={} should match Q11*t={}",
417 a[0][0],
418 q11 * t
419 );
420 }
421
422 #[test]
424 fn test_clt_crossply_a16_zero() {
425 let ply = (140e9, 10e9, 5e9, 0.3, 0.0, 0.001);
426 let ply90 = (140e9, 10e9, 5e9, 0.3, 90.0, 0.001);
427 let a = clt_in_plane_stiffness(&[ply, ply90, ply90, ply]);
428 assert!(a[0][2].abs() < 1e3, "Cross-ply A16 should be ≈ 0");
429 }
430
431 #[test]
433 fn test_shear_vf0() {
434 let g = micromechanics_shear(30e9, 1.5e9, 0.0);
435 assert!((g - 1.5e9).abs() < EPS * 1.5e9);
436 }
437
438 #[test]
440 fn test_shear_higher_than_matrix() {
441 let g = micromechanics_shear(30e9, 1.5e9, 0.5);
442 assert!(g > 1.5e9);
443 }
444
445 #[test]
447 fn test_critical_length_formula() {
448 let sigma = 3500e6f64;
449 let d = 7e-6f64;
450 let tau = 35e6f64;
451 let lc = critical_fiber_length(sigma, tau, d);
452 let expected = sigma * d / (2.0 * tau);
453 assert!((lc - expected).abs() < EPS * expected);
454 }
455
456 #[test]
458 fn test_critical_length_zero_tau() {
459 assert!(critical_fiber_length(1000.0, 0.0, 1e-6).is_infinite());
460 }
461
462 #[test]
464 fn test_critical_length_larger_diameter() {
465 let lc1 = critical_fiber_length(3500e6, 35e6, 7e-6);
466 let lc2 = critical_fiber_length(3500e6, 35e6, 14e-6);
467 assert!((lc2 - 2.0 * lc1).abs() < EPS * lc1);
468 }
469
470 #[test]
472 fn test_pullout_energy_positive() {
473 let f = fiber_preset(FiberType::Carbon);
474 let e = fiber_pullout_energy(&f, 1e-3, 35e6);
475 assert!(e > 0.0);
476 }
477
478 #[test]
480 fn test_pullout_energy_quadratic_length() {
481 let f = fiber_preset(FiberType::Carbon);
482 let e1 = fiber_pullout_energy(&f, 1e-3, 35e6);
483 let e2 = fiber_pullout_energy(&f, 2e-3, 35e6);
484 assert!((e2 - 4.0 * e1).abs() < EPS * e1 * 4.0);
485 }
486
487 #[test]
489 fn test_pullout_energy_linear_tau() {
490 let f = fiber_preset(FiberType::Carbon);
491 let e1 = fiber_pullout_energy(&f, 1e-3, 10e6);
492 let e2 = fiber_pullout_energy(&f, 1e-3, 20e6);
493 assert!((e2 - 2.0 * e1).abs() < EPS * e1 * 2.0);
494 }
495
496 #[test]
498 fn test_winding_0deg_gives_e1() {
499 let e1 = 140e9f64;
500 let e2 = 10e9f64;
501 let g12 = 5e9f64;
502 let nu12 = 0.3f64;
503 let ex = winding_angle_effect(e1, e2, g12, nu12, 0.0);
504 assert!((ex - e1).abs() < 1e3, "At 0° Ex should = E1");
505 }
506
507 #[test]
509 fn test_winding_90deg_gives_e2() {
510 let e1 = 140e9f64;
511 let e2 = 10e9f64;
512 let g12 = 5e9f64;
513 let nu12 = 0.3f64;
514 let ex = winding_angle_effect(e1, e2, g12, nu12, 90.0);
515 assert!((ex - e2).abs() < 1e3, "At 90° Ex should = E2");
516 }
517
518 #[test]
520 fn test_winding_45deg_bounds() {
521 let e1 = 140e9f64;
522 let e2 = 10e9f64;
523 let ex = winding_angle_effect(e1, e2, 5e9, 0.3, 45.0);
524 assert!(ex >= e2 && ex <= e1);
525 }
526
527 #[test]
529 fn test_winding_monotone() {
530 let e1 = 140e9f64;
531 let e2 = 10e9f64;
532 let g12 = 5e9f64;
533 let nu12 = 0.3f64;
534 let mut prev = winding_angle_effect(e1, e2, g12, nu12, 0.0);
535 for deg in (5u32..=90).step_by(5) {
536 let cur = winding_angle_effect(e1, e2, g12, nu12, deg as f64);
537 assert!(
538 cur <= prev + 1e6,
539 "Ex should decrease: theta={deg} cur={cur} prev={prev}"
540 );
541 prev = cur;
542 }
543 }
544
545 #[test]
547 fn test_rom_long_midpoint() {
548 let e_f = 200e9f64;
549 let e_m = 4e9f64;
550 let e = rule_of_mixtures_longitudinal(e_f, e_m, 0.5);
551 let expected = 0.5 * e_f + 0.5 * e_m;
552 assert!((e - expected).abs() < EPS * expected);
553 }
554
555 #[test]
557 fn test_halpin_tsai_realistic() {
558 let e2 = halpin_tsai_transverse(230e9, 4e9, 0.6, 2.0);
559 assert!(e2 > 4e9 && e2 < 50e9);
561 }
562
563 #[test]
565 fn test_shear_zero_matrix() {
566 let g = micromechanics_shear(30e9, 0.0, 0.5);
567 assert_eq!(g, 0.0);
568 }
569
570 #[test]
572 fn test_clt_a11_positive() {
573 let plies = vec![
574 (140e9, 10e9, 5e9, 0.3, 0.0, 0.001),
575 (140e9, 10e9, 5e9, 0.3, 90.0, 0.001),
576 ];
577 let a = clt_in_plane_stiffness(&plies);
578 assert!(a[0][0] > 0.0 && a[1][1] > 0.0 && a[2][2] > 0.0);
579 }
580
581 #[test]
583 fn test_pullout_energy_zero_tau() {
584 let f = fiber_preset(FiberType::Glass);
585 assert_eq!(fiber_pullout_energy(&f, 1e-3, 0.0), 0.0);
586 }
587
588 #[test]
590 fn test_basalt_stronger_than_steel() {
591 let b = fiber_preset(FiberType::Basalt);
592 let s = fiber_preset(FiberType::Steel);
593 assert!(b.tensile_strength > s.tensile_strength);
594 }
595
596 #[test]
598 fn test_shear_vf1_saturates() {
599 let g = micromechanics_shear(30e9, 1.5e9, 1.0);
600 assert!(g > 1.5e9);
603 }
604
605 #[test]
607 fn test_halpin_tsai_zero_matrix() {
608 let e = halpin_tsai_transverse(200e9, 0.0, 0.5, 2.0);
609 assert_eq!(e, 0.0);
610 }
611
612 #[test]
614 fn test_critical_length_proportional_strength() {
615 let lc1 = critical_fiber_length(1000e6, 50e6, 10e-6);
616 let lc2 = critical_fiber_length(2000e6, 50e6, 10e-6);
617 assert!((lc2 - 2.0 * lc1).abs() < EPS * lc1 * 2.0);
618 }
619
620 #[test]
622 fn test_clt_empty_plies() {
623 let a = clt_in_plane_stiffness(&[]);
624 for row in &a {
625 for val in row {
626 assert_eq!(*val, 0.0);
627 }
628 }
629 }
630
631 #[test]
633 fn test_winding_glass_0deg() {
634 let gf = fiber_preset(FiberType::Glass);
635 let ex = winding_angle_effect(gf.youngs_modulus, 10e9, 3e9, 0.25, 0.0);
637 assert!((ex - gf.youngs_modulus).abs() < 1e4);
638 }
639}