Skip to main content

oxiphysics_materials/
composites_advanced.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Advanced composite material models: laminate theory, fiber-matrix, damage.
5//!
6//! Implements Classical Laminate Theory (CLT), Halpin-Tsai micromechanics,
7//! Mori-Tanaka homogenization, failure criteria (Tsai-Wu, Puck, Max Stress),
8//! progressive damage, and delamination models.
9
10#![warn(missing_docs)]
11
12/// Fiber material properties.
13#[derive(Debug, Clone)]
14pub struct FiberProperties {
15    /// Axial (fiber direction) Young's modulus \[Pa\].
16    pub e1: f64,
17    /// Transverse Young's modulus \[Pa\].
18    pub e2: f64,
19    /// In-plane shear modulus \[Pa\].
20    pub g12: f64,
21    /// Major Poisson's ratio.
22    pub nu12: f64,
23    /// Fiber volume fraction (0-1).
24    pub volume_fraction: f64,
25    /// Fiber aspect ratio (length/diameter).
26    pub aspect_ratio: f64,
27}
28
29impl FiberProperties {
30    /// Create new fiber properties.
31    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    /// Create carbon fiber (T300) properties.
50    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    /// Create glass fiber (E-glass) properties.
62    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/// Matrix material properties.
75#[derive(Debug, Clone)]
76pub struct MatrixProperties {
77    /// Young's modulus \[Pa\].
78    pub modulus: f64,
79    /// Poisson's ratio.
80    pub poisson_ratio: f64,
81    /// Tensile yield stress \[Pa\].
82    pub yield_stress: f64,
83    /// Fracture toughness K_Ic \[Pa·√m\].
84    pub fracture_toughness: f64,
85}
86
87impl MatrixProperties {
88    /// Create new matrix properties.
89    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    /// Create epoxy resin matrix properties.
104    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    /// Shear modulus G = E / (2*(1+ν)).
114    pub fn shear_modulus(&self) -> f64 {
115        self.modulus / (2.0 * (1.0 + self.poisson_ratio))
116    }
117}
118
119/// Halpin-Tsai micromechanics model.
120///
121/// Predicts elastic constants of a composite from fiber + matrix properties.
122#[derive(Debug, Clone)]
123pub struct HalpinTsai {
124    /// Fiber properties.
125    pub fiber: FiberProperties,
126    /// Matrix properties.
127    pub matrix: MatrixProperties,
128}
129
130impl HalpinTsai {
131    /// Create a new Halpin-Tsai model.
132    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    /// Longitudinal modulus E11 (rule of mixtures).
144    pub fn e11(&self) -> f64 {
145        self.fiber.e1 * self.vf() + self.matrix.modulus * self.vm()
146    }
147
148    /// Transverse modulus E22 using Halpin-Tsai.
149    pub fn e22(&self) -> f64 {
150        let xi = 2.0; // geometry parameter for circular fibers
151        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    /// In-plane shear modulus G12 using Halpin-Tsai.
158    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    /// Major Poisson's ratio ν12 (rule of mixtures).
167    pub fn nu12(&self) -> f64 {
168        self.fiber.nu12 * self.vf() + self.matrix.poisson_ratio * self.vm()
169    }
170
171    /// Minor Poisson's ratio ν21 = ν12 * E22 / E11.
172    pub fn nu21(&self) -> f64 {
173        self.nu12() * self.e22() / self.e11()
174    }
175
176    /// Voigt upper bound for E11.
177    pub fn voigt_e11(&self) -> f64 {
178        self.fiber.e1 * self.vf() + self.matrix.modulus * self.vm()
179    }
180
181    /// Reuss lower bound for E22.
182    pub fn reuss_e22(&self) -> f64 {
183        1.0 / (self.vf() / self.fiber.e2 + self.vm() / self.matrix.modulus)
184    }
185}
186
187/// Mori-Tanaka homogenization for ellipsoidal inclusions.
188#[derive(Debug, Clone)]
189pub struct MoriTanakaComposite {
190    /// Fiber (inclusion) properties.
191    pub fiber: FiberProperties,
192    /// Matrix properties.
193    pub matrix: MatrixProperties,
194}
195
196impl MoriTanakaComposite {
197    /// Create a new Mori-Tanaka model.
198    pub fn new(fiber: FiberProperties, matrix: MatrixProperties) -> Self {
199        Self { fiber, matrix }
200    }
201
202    /// Effective bulk modulus (simplified spherical inclusion).
203    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        // Eshelby factor for spheres
207        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    /// Effective shear modulus (simplified).
215    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/// Single lamina (ply) in a laminate stack.
227#[derive(Debug, Clone)]
228pub struct Lamina {
229    /// Longitudinal modulus E1 \[Pa\].
230    pub e1: f64,
231    /// Transverse modulus E2 \[Pa\].
232    pub e2: f64,
233    /// In-plane shear modulus G12 \[Pa\].
234    pub g12: f64,
235    /// Major Poisson's ratio.
236    pub nu12: f64,
237    /// Ply thickness \[m\].
238    pub thickness: f64,
239    /// Fiber angle from laminate x-axis \[radians\].
240    pub angle: f64,
241}
242
243impl Lamina {
244    /// Create a new lamina.
245    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    /// Minor Poisson's ratio.
257    pub fn nu21(&self) -> f64 {
258        self.nu12 * self.e2 / self.e1
259    }
260
261    /// Reduced stiffness matrix Q in principal material coordinates \[Q11, Q12, Q22, Q66\].
262    pub fn q_matrix(&self) -> [f64; 4] {
263        let denom = 1.0 - self.nu12 * self.nu21();
264        [
265            self.e1 / denom,             // Q11
266            self.nu12 * self.e2 / denom, // Q12
267            self.e2 / denom,             // Q22
268            self.g12,                    // Q66
269        ]
270    }
271
272    /// Transformed reduced stiffness matrix Q_bar (in laminate coords).
273    /// Returns \[Q11, Q12, Q16, Q22, Q26, Q66\].
274    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/// Classical Laminate Theory stiffness matrices.
295///
296/// Computes \[A|B|D\] matrices for a laminate stack.
297#[derive(Debug, Clone)]
298pub struct ClassicalLaminateTheory {
299    /// Laminae in the stack (bottom to top).
300    pub laminae: Vec<Lamina>,
301}
302
303impl ClassicalLaminateTheory {
304    /// Create CLT from a list of laminae.
305    pub fn new(laminae: Vec<Lamina>) -> Self {
306        Self { laminae }
307    }
308
309    /// Total thickness \[m\].
310    pub fn total_thickness(&self) -> f64 {
311        self.laminae.iter().map(|l| l.thickness).sum()
312    }
313
314    /// Z-coordinates of ply interfaces (bottom to top).
315    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    /// A matrix (extensional stiffness), 3x3 as \[A11, A12, A16, A22, A26, A66\].
326    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    /// B matrix (coupling stiffness) \[B11, B12, B16, B22, B26, B66\].
340    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    /// D matrix (bending stiffness) \[D11, D12, D16, D22, D26, D66\].
354    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    /// Check if laminate is symmetric (B ≈ 0).
368    pub fn is_symmetric(&self) -> bool {
369        let b = self.b_matrix();
370        b.iter().all(|&v| v.abs() < 1e-6)
371    }
372
373    /// Check if laminate is balanced (A16 = A26 ≈ 0).
374    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/// Laminate response under in-plane loads and moments.
381#[derive(Debug, Clone)]
382pub struct LaminateResponse {
383    /// Applied in-plane force resultants \[N/m\]: \[Nx, Ny, Nxy\].
384    pub n: [f64; 3],
385    /// Applied moment resultants \[N\]: \[Mx, My, Mxy\].
386    pub m: [f64; 3],
387    /// Midplane strains \[ε_x0, ε_y0, γ_xy0\].
388    pub midplane_strains: [f64; 3],
389    /// Midplane curvatures \[κ_x, κ_y, κ_xy\].
390    pub curvatures: [f64; 3],
391}
392
393impl LaminateResponse {
394    /// Compute midplane strains for a symmetric laminate (B=0) under pure N.
395    /// Uses a_inv (3x3 inverse A in Voigt notation order \[11,12,22,16,26,66\]).
396    pub fn from_n_symmetric(n: [f64; 3], a: [f64; 6]) -> Self {
397        // Simplified: compute midplane strains from [A]{ε} = {N}
398        // For unidirectional symmetric: approximate 3x3 solve
399        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/// Tsai-Wu quadratic failure criterion.
416#[derive(Debug, Clone)]
417pub struct TsaiWuCriterion {
418    /// Longitudinal tensile strength Xt \[Pa\].
419    pub xt: f64,
420    /// Longitudinal compressive strength Xc \[Pa\].
421    pub xc: f64,
422    /// Transverse tensile strength Yt \[Pa\].
423    pub yt: f64,
424    /// Transverse compressive strength Yc \[Pa\].
425    pub yc: f64,
426    /// In-plane shear strength S12 \[Pa\].
427    pub s12: f64,
428    /// Interaction term F12* (typically -0.5 ≤ F12* ≤ 0).
429    pub f12_star: f64,
430}
431
432impl TsaiWuCriterion {
433    /// Create Tsai-Wu criterion.
434    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    /// F1 coefficient.
446    pub fn f1(&self) -> f64 {
447        1.0 / self.xt - 1.0 / self.xc
448    }
449
450    /// F2 coefficient.
451    pub fn f2(&self) -> f64 {
452        1.0 / self.yt - 1.0 / self.yc
453    }
454
455    /// F11 coefficient.
456    pub fn f11(&self) -> f64 {
457        1.0 / (self.xt * self.xc)
458    }
459
460    /// F22 coefficient.
461    pub fn f22(&self) -> f64 {
462        1.0 / (self.yt * self.yc)
463    }
464
465    /// F66 coefficient.
466    pub fn f66(&self) -> f64 {
467        1.0 / (self.s12 * self.s12)
468    }
469
470    /// F12 interaction coefficient.
471    pub fn f12(&self) -> f64 {
472        self.f12_star / (self.f11() * self.f22()).sqrt()
473    }
474
475    /// Tsai-Wu failure index (FI < 1 = safe).
476    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    /// Check if stress state fails (FI ≥ 1).
486    pub fn fails(&self, sigma1: f64, sigma2: f64, tau12: f64) -> bool {
487        self.failure_index(sigma1, sigma2, tau12) >= 1.0
488    }
489
490    /// Check convexity condition: |F12*| < 1 (equivalently F11*F22 > F12²).
491    ///
492    /// Using the normalized form: convexity holds when `f12_star² < 1`.
493    pub fn is_convex(&self) -> bool {
494        self.f12_star * self.f12_star < 1.0
495    }
496
497    /// Strength ratio R: scale factor to failure. Solves quadratic a*R²+b*R-1=0.
498    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/// Maximum stress failure criterion.
519#[derive(Debug, Clone)]
520pub struct MaxStressCriterion {
521    /// Longitudinal tensile strength Xt \[Pa\].
522    pub xt: f64,
523    /// Longitudinal compressive strength Xc \[Pa\].
524    pub xc: f64,
525    /// Transverse tensile strength Yt \[Pa\].
526    pub yt: f64,
527    /// Transverse compressive strength Yc \[Pa\].
528    pub yc: f64,
529    /// In-plane shear strength S12 \[Pa\].
530    pub s12: f64,
531}
532
533impl MaxStressCriterion {
534    /// Create max-stress criterion.
535    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    /// Check failure.
546    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    /// Maximum stress failure index (max ratio to strength).
566    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/// Puck failure criterion (fiber fracture and inter-fiber fracture).
583#[derive(Debug, Clone)]
584pub struct PuckCriterion {
585    /// Longitudinal tensile strength Xt \[Pa\].
586    pub xt: f64,
587    /// Longitudinal compressive strength Xc \[Pa\].
588    pub xc: f64,
589    /// Transverse tensile strength Yt \[Pa\].
590    pub yt: f64,
591    /// Transverse compressive strength Yc \[Pa\].
592    pub yc: f64,
593    /// Shear strength S12 \[Pa\].
594    pub s12: f64,
595    /// Master fracture plane inclination (typically 53°).
596    pub theta_fp_deg: f64,
597}
598
599impl PuckCriterion {
600    /// Create Puck criterion.
601    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    /// Fiber fracture failure index.
613    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    /// Inter-fiber fracture failure index (mode A: transverse tension).
622    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; // friction coefficient (typical)
627        ((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    /// Inter-fiber fracture failure index (mode B: transverse compression).
634    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/// Progressive damage model (ply-by-ply degradation).
647#[derive(Debug, Clone)]
648pub struct ProgressiveDamage {
649    /// Current stiffness degradation factor per ply (1 = intact).
650    pub degradation: Vec<f64>,
651    /// Failure criterion (Tsai-Wu).
652    pub criterion: TsaiWuCriterion,
653    /// Stiffness reduction factor upon failure.
654    pub reduction_factor: f64,
655}
656
657impl ProgressiveDamage {
658    /// Create progressive damage model for n plies.
659    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    /// Apply load step and degrade failed plies.
668    /// Returns (new_degradation_flags, ply failure count).
669    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    /// Check if all plies have failed (last-ply-failure).
684    pub fn all_plies_failed(&self) -> bool {
685        self.degradation.iter().all(|&d| d < 0.01)
686    }
687
688    /// Number of failed plies.
689    pub fn failed_count(&self) -> usize {
690        self.degradation.iter().filter(|&&d| d < 0.99).count()
691    }
692}
693
694/// Delamination model (fracture mechanics approach).
695#[derive(Debug, Clone)]
696pub struct DelaminationModel {
697    /// Mode-I interlaminar fracture toughness G_Ic \[J/m²\].
698    pub g_ic: f64,
699    /// Mode-II interlaminar fracture toughness G_IIc \[J/m²\].
700    pub g_iic: f64,
701    /// BK criterion exponent.
702    pub bk_exponent: f64,
703}
704
705impl DelaminationModel {
706    /// Create delamination model.
707    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    /// Benzeggagh-Kenane (BK) mixed-mode toughness.
716    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    /// Check if delamination occurs under (G_I, G_II) loading.
726    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    /// Failure index for mixed-mode delamination (quadratic criterion).
733    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/// Woven composite geometry parameters.
739#[derive(Debug, Clone)]
740pub struct WovenComposite {
741    /// Weave style (Plain, Twill, Satin).
742    pub weave_style: WeaveStyle,
743    /// Crimp angle \[degrees\].
744    pub crimp_angle_deg: f64,
745    /// Fiber volume fraction.
746    pub vf: f64,
747    /// Warp/weft ratio.
748    pub warp_weft_ratio: f64,
749}
750
751/// Weave style for woven composites.
752#[derive(Debug, Clone, PartialEq)]
753pub enum WeaveStyle {
754    /// Plain weave (1/1 interlacing).
755    Plain,
756    /// Twill weave (2/2 interlacing).
757    Twill,
758    /// Satin weave (4H, 5H, 8H).
759    Satin {
760        /// Number of harnesses (typically 4, 5, or 8)
761        harness: u32,
762    },
763}
764
765impl WovenComposite {
766    /// Create a plain weave composite.
767    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    /// Effective modulus reduction due to crimp (cos⁴θ approximation).
777    pub fn crimp_modulus_factor(&self) -> f64 {
778        let theta = self.crimp_angle_deg.to_radians();
779        theta.cos().powi(4)
780    }
781
782    /// Approximate in-plane modulus (Rule-of-mixtures with crimp).
783    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    /// Interlacement density (number of crossings per unit cell).
789    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/// Homogenization scheme for composite effective properties.
799#[derive(Debug, Clone)]
800pub struct HomogenizationScheme;
801
802impl HomogenizationScheme {
803    /// Voigt (iso-strain) upper bound for modulus.
804    pub fn voigt(e_fiber: f64, e_matrix: f64, vf: f64) -> f64 {
805        e_fiber * vf + e_matrix * (1.0 - vf)
806    }
807
808    /// Reuss (iso-stress) lower bound for modulus.
809    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    /// Hill average = (Voigt + Reuss) / 2.
814    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    /// Hashin-Shtrikman upper bound (spherical inclusions).
819    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    /// Verify Voigt ≥ Reuss (fundamental bound).
825    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        // E11 is rule of mixtures
857        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        // H-T gives better (higher) E22 than Reuss lower bound
866        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        // nu21/E22 = nu12/E11 (reciprocal relation)
881        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        // nu21 = nu12 * E2/E1
919        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); // Q11_bar ≈ Q11
929        assert!((qb[3] - q[2]).abs() < 1e3); // Q22_bar ≈ Q22
930    }
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); // symmetric about 0
948    }
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        // [0/90/90/0] is symmetric -> B=0
962        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        // [0/+45/-45/90] balanced -> A16=A26≈0
970        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        // Symmetric so B=0; check A16/A26 small
982        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        // Just above Xt should fail
1003        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        // Large transverse tension should give fi > 1
1049        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)); // G_I > G_Ic
1091        assert!(!dm.delamination_occurs(100.0, 0.0)); // G_I < G_Ic
1092    }
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        // Effective should be between matrix and fiber bulk moduli
1188        assert!(k_eff >= km.min(kf) - 1e9);
1189        assert!(k_eff <= km.max(kf) + 1e9);
1190    }
1191}