Skip to main content

oxiphysics_materials/
fiber_composites.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Fiber-reinforced composite material models.
5//!
6//! Implements micromechanical models for unidirectional and woven
7//! fiber composites including rule-of-mixtures, Halpin–Tsai, and
8//! classical laminate theory (CLT) stiffness assembly.
9
10#![allow(dead_code)]
11#![allow(clippy::too_many_arguments)]
12
13use std::f64::consts::PI;
14
15// ---------------------------------------------------------------------------
16// Fiber struct
17// ---------------------------------------------------------------------------
18
19/// Material properties for a single fiber type.
20#[derive(Debug, Clone, PartialEq)]
21pub struct Fiber {
22    /// Longitudinal Young's modulus of the fiber (Pa).
23    pub youngs_modulus: f64,
24    /// Poisson's ratio of the fiber (dimensionless).
25    pub poissons_ratio: f64,
26    /// Density of the fiber (kg/m³).
27    pub density: f64,
28    /// Tensile strength of the fiber (Pa).
29    pub tensile_strength: f64,
30    /// Nominal fiber diameter in micrometres (µm).
31    pub diameter_um: f64,
32}
33
34// ---------------------------------------------------------------------------
35// FiberType enum
36// ---------------------------------------------------------------------------
37
38/// Common commercial fiber types.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum FiberType {
41    /// High-stiffness / high-strength carbon fiber (T300 grade).
42    Carbon,
43    /// E-glass fiber (economical, good electrical insulation).
44    Glass,
45    /// Kevlar 49 aramid fiber (high toughness, light weight).
46    Kevlar,
47    /// Basalt fiber (natural volcanic origin, corrosion resistant).
48    Basalt,
49    /// High-strength steel fiber (for concrete reinforcement).
50    Steel,
51}
52
53// ---------------------------------------------------------------------------
54// fiber_preset
55// ---------------------------------------------------------------------------
56
57/// Return a [`Fiber`] populated with typical published properties for `ft`.
58///
59/// Sources: Ashby & Jones *Engineering Materials 1*, MatWeb, and fiber
60/// manufacturer data sheets.  Values are representative mid-range figures.
61pub 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
101// ---------------------------------------------------------------------------
102// rule_of_mixtures_longitudinal
103// ---------------------------------------------------------------------------
104
105/// Longitudinal Young's modulus E₁ of a unidirectional ply (Voigt / ROM).
106///
107/// E₁ = v_f · E_f + (1 − v_f) · E_m
108///
109/// where v_f is the fiber volume fraction (0–1), E_f the fiber modulus, and
110/// E_m the matrix modulus.
111pub 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
116// ---------------------------------------------------------------------------
117// rule_of_mixtures_transverse
118// ---------------------------------------------------------------------------
119
120/// Transverse Young's modulus E₂ using the Halpin–Tsai approximation.
121///
122/// This is a convenience wrapper around [`halpin_tsai_transverse`] with the
123/// standard reinforcing factor ξ = 2 (circular fibers in square packing).
124pub 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
128// ---------------------------------------------------------------------------
129// halpin_tsai_transverse
130// ---------------------------------------------------------------------------
131
132/// Transverse Young's modulus E₂ via the Halpin–Tsai model.
133///
134/// ```text
135/// η = (E_f/E_m − 1) / (E_f/E_m + ξ)
136/// E₂ = E_m · (1 + ξ·η·v_f) / (1 − η·v_f)
137/// ```
138///
139/// `xi` (ξ) is the reinforcing factor: 2 for circular cross-sections, larger
140/// for higher aspect-ratio inclusions.
141pub 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
154// ---------------------------------------------------------------------------
155// clt_in_plane_stiffness
156// ---------------------------------------------------------------------------
157
158/// Assemble the CLT in-plane stiffness matrix **A** (N/m) for a laminate.
159///
160/// Each ply is described by the tuple `(E1, E2, G12, nu12, theta_deg, t)`:
161/// - `E1` – longitudinal modulus (Pa)
162/// - `E2` – transverse modulus (Pa)
163/// - `G12` – in-plane shear modulus (Pa)
164/// - `nu12` – major Poisson's ratio
165/// - `theta_deg` – ply angle measured from the laminate reference axis (°)
166/// - `t` – ply thickness (m)
167///
168/// Returns the 3×3 **A** matrix in Voigt notation `[σ_xx, σ_yy, σ_xy]`.
169pub 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
183/// Compute the reduced stiffness matrix **Q** for a ply in its material axes.
184fn 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
197/// Rotate reduced stiffness **Q** from material axes by angle `theta_deg`.
198fn 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
221// ---------------------------------------------------------------------------
222// micromechanics_shear
223// ---------------------------------------------------------------------------
224
225/// In-plane shear modulus G₁₂ via Halpin–Tsai micromechanics.
226///
227/// ```text
228/// η = (G_f/G_m − 1) / (G_f/G_m + 1)
229/// G₁₂ = G_m · (1 + η·v_f) / (1 − η·v_f)
230/// ```
231pub 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
244// ---------------------------------------------------------------------------
245// critical_fiber_length
246// ---------------------------------------------------------------------------
247
248/// Critical fiber length l_c for effective load transfer (Kelly–Tyson).
249///
250/// ```text
251/// l_c = σ_f · d / (2 τ_i)
252/// ```
253///
254/// where `tensile_strength` = σ_f (Pa), `diameter` = d (m), and
255/// `interfacial_shear` = τ_i (Pa).
256pub 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
263// ---------------------------------------------------------------------------
264// fiber_pullout_energy
265// ---------------------------------------------------------------------------
266
267/// Fiber pull-out fracture energy per unit area (J/m²).
268///
269/// ```text
270/// G_pullout = π · d · l² · τ_i / 2
271/// ```
272///
273/// where `d` = fiber diameter (m), `bond_length` = l (m), and
274/// `tau_i` = interfacial shear strength (Pa).
275pub fn fiber_pullout_energy(fiber: &Fiber, bond_length: f64, tau_i: f64) -> f64 {
276    let d = fiber.diameter_um * 1.0e-6; // convert µm → m
277    PI * d * bond_length * bond_length * tau_i / 2.0
278}
279
280// ---------------------------------------------------------------------------
281// winding_angle_effect
282// ---------------------------------------------------------------------------
283
284/// Effective longitudinal modulus E_x for a ply rotated by `theta_deg`.
285///
286/// Uses the exact off-axis transformation:
287///
288/// ```text
289/// 1/E_x = cos⁴θ/E₁ + sin⁴θ/E₂ + (1/G₁₂ − 2ν₁₂/E₁)·sin²θ·cos²θ
290/// ```
291pub 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// ---------------------------------------------------------------------------
304// Tests
305// ---------------------------------------------------------------------------
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    const EPS: f64 = 1e-6;
312
313    // 1. fiber_preset: Carbon fiber has E > Glass.
314    #[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    // 2. fiber_preset: all densities are positive.
322    #[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    // 3. fiber_preset: all diameters > 0.
336    #[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    // 4. rule_of_mixtures_longitudinal: vf=0 gives e_matrix.
350    #[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    // 5. rule_of_mixtures_longitudinal: vf=1 gives e_fiber.
357    #[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    // 6. rule_of_mixtures_longitudinal: result is between e_m and e_f.
364    #[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    // 7. rule_of_mixtures_transverse: vf=0 gives e_matrix.
371    #[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    // 8. rule_of_mixtures_transverse: result is bounded by matrix and fiber.
378    #[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    // 9. halpin_tsai_transverse: xi=0 → E2 = E_m (no reinforcement).
385    #[test]
386    fn test_halpin_tsai_xi0() {
387        // xi=0 → eta = (r-1)/(r+0) = 1 for large r
388        // check vf=0 at xi=2 still gives e_matrix
389        let e = halpin_tsai_transverse(100e9, 4e9, 0.0, 2.0);
390        assert!((e - 4e9).abs() < EPS * 4e9);
391    }
392
393    // 10. halpin_tsai_transverse: higher xi → higher E2.
394    #[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    // 11. clt_in_plane_stiffness: single 0° ply gives A11 ≈ Q11*t.
402    #[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        // A11 should equal Q11 * t
414        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    // 12. clt_in_plane_stiffness: symmetric cross-ply gives A16≈0.
423    #[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    // 13. micromechanics_shear: vf=0 gives g_matrix.
432    #[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    // 14. micromechanics_shear: result > g_matrix for positive vf.
439    #[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    // 15. critical_fiber_length: l_c = sigma * d / (2 * tau).
446    #[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    // 16. critical_fiber_length: zero tau returns infinity.
457    #[test]
458    fn test_critical_length_zero_tau() {
459        assert!(critical_fiber_length(1000.0, 0.0, 1e-6).is_infinite());
460    }
461
462    // 17. critical_fiber_length: larger diameter → longer l_c.
463    #[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    // 18. fiber_pullout_energy: energy is positive.
471    #[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    // 19. fiber_pullout_energy: scales quadratically with bond_length.
479    #[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    // 20. fiber_pullout_energy: scales linearly with tau_i.
488    #[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    // 21. winding_angle_effect: theta=0 gives E1.
497    #[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    // 22. winding_angle_effect: theta=90 gives E2.
508    #[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    // 23. winding_angle_effect: intermediate angle gives modulus between E1 and E2.
519    #[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    // 24. winding_angle_effect: modulus decreases monotonically from 0 to 90.
528    #[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    // 25. rule_of_mixtures_longitudinal: linear interpolation at vf=0.5.
546    #[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    // 26. halpin_tsai_transverse: xi=2 at vf=0.6 gives reasonable E2.
556    #[test]
557    fn test_halpin_tsai_realistic() {
558        let e2 = halpin_tsai_transverse(230e9, 4e9, 0.6, 2.0);
559        // Should be well above matrix modulus but below fiber modulus.
560        assert!(e2 > 4e9 && e2 < 50e9);
561    }
562
563    // 27. micromechanics_shear: zero matrix modulus returns 0.
564    #[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    // 28. clt_in_plane_stiffness: A matrix is positive semi-definite (A11 > 0).
571    #[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    // 29. fiber_pullout_energy: zero tau gives zero energy.
582    #[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    // 30. fiber_preset: Basalt tensile strength > Steel.
589    #[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    // 31. micromechanics_shear: vf=1 approaches g_fiber (Halpin–Tsai saturates).
597    #[test]
598    fn test_shear_vf1_saturates() {
599        let g = micromechanics_shear(30e9, 1.5e9, 1.0);
600        // eta * 1 = (r-1)/(r+1); numerator = 1 + eta, denom = 1 - eta
601        // Should be significantly above matrix.
602        assert!(g > 1.5e9);
603    }
604
605    // 32. halpin_tsai_transverse: matrix with zero modulus returns 0.
606    #[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    // 33. critical_fiber_length: proportional to tensile strength.
613    #[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    // 34. clt_in_plane_stiffness: empty ply list gives zero matrix.
621    #[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    // 35. winding_angle_effect: Glass fiber at 0 deg gives ~72 GPa.
632    #[test]
633    fn test_winding_glass_0deg() {
634        let gf = fiber_preset(FiberType::Glass);
635        // E_m assumed 4 GPa, G12 = 2 GPa, nu12 = 0.25
636        let ex = winding_angle_effect(gf.youngs_modulus, 10e9, 3e9, 0.25, 0.0);
637        assert!((ex - gf.youngs_modulus).abs() < 1e4);
638    }
639}