Skip to main content

oxiphysics_materials/
combination.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Rules for combining material properties in contact pairs and composite materials.
5//!
6//! Provides combining rules for friction, restitution, and mechanical
7//! properties (Young's modulus, Poisson's ratio) for contact between two
8//! different materials. Also includes Voigt/Reuss/Hill averaging,
9//! rule of mixtures, laminate theory (CLT) basics, Halpin-Tsai model,
10//! and effective thermal expansion for composite materials.
11
12// ─── Friction combining rules ────────────────────────────────────────────────
13
14/// Rule for combining friction coefficients of two materials in contact.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum FrictionCombineRule {
17    /// Arithmetic average: (f1 + f2) / 2
18    Average,
19    /// Minimum of the two values
20    Min,
21    /// Maximum of the two values
22    Max,
23    /// Product: f1 * f2
24    Multiply,
25    /// Geometric mean: sqrt(f1 * f2)
26    GeometricMean,
27    /// Harmonic mean: 2*f1*f2 / (f1 + f2)
28    HarmonicMean,
29}
30
31/// Rule for combining restitution coefficients of two materials in contact.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum RestitutionCombineRule {
34    /// Arithmetic average: (r1 + r2) / 2
35    Average,
36    /// Minimum of the two values
37    Min,
38    /// Maximum of the two values
39    Max,
40    /// Product: r1 * r2
41    Multiply,
42    /// Geometric mean: sqrt(r1 * r2)
43    GeometricMean,
44    /// Harmonic mean: 2*r1*r2 / (r1 + r2)
45    HarmonicMean,
46}
47
48/// Rule for combining Young's moduli for Hertzian contact.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ModulusCombineRule {
51    /// Harmonic mean (correct for Hertz contact): 2*E1*E2 / (E1 + E2)
52    HertzHarmonic,
53    /// Voigt upper bound: arithmetic average
54    Voigt,
55    /// Reuss lower bound: harmonic mean
56    Reuss,
57    /// Geometric mean: sqrt(E1 * E2)
58    GeometricMean,
59}
60
61// ─── Combining functions ─────────────────────────────────────────────────────
62
63/// Combine friction coefficients of two materials using the given rule.
64pub fn combine_friction(f1: f64, f2: f64, rule: FrictionCombineRule) -> f64 {
65    match rule {
66        FrictionCombineRule::Average => (f1 + f2) * 0.5,
67        FrictionCombineRule::Min => f1.min(f2),
68        FrictionCombineRule::Max => f1.max(f2),
69        FrictionCombineRule::Multiply => f1 * f2,
70        FrictionCombineRule::GeometricMean => (f1 * f2).sqrt(),
71        FrictionCombineRule::HarmonicMean => {
72            let s = f1 + f2;
73            if s.abs() < f64::EPSILON {
74                0.0
75            } else {
76                2.0 * f1 * f2 / s
77            }
78        }
79    }
80}
81
82/// Combine restitution coefficients of two materials using the given rule.
83pub fn combine_restitution(r1: f64, r2: f64, rule: RestitutionCombineRule) -> f64 {
84    match rule {
85        RestitutionCombineRule::Average => (r1 + r2) * 0.5,
86        RestitutionCombineRule::Min => r1.min(r2),
87        RestitutionCombineRule::Max => r1.max(r2),
88        RestitutionCombineRule::Multiply => r1 * r2,
89        RestitutionCombineRule::GeometricMean => (r1 * r2).sqrt(),
90        RestitutionCombineRule::HarmonicMean => {
91            let s = r1 + r2;
92            if s.abs() < f64::EPSILON {
93                0.0
94            } else {
95                2.0 * r1 * r2 / s
96            }
97        }
98    }
99}
100
101/// Combine Young's moduli for contact mechanics.
102pub fn combine_modulus(e1: f64, e2: f64, rule: ModulusCombineRule) -> f64 {
103    match rule {
104        ModulusCombineRule::HertzHarmonic => {
105            let s = e1 + e2;
106            if s.abs() < f64::EPSILON {
107                0.0
108            } else {
109                2.0 * e1 * e2 / s
110            }
111        }
112        ModulusCombineRule::Voigt => (e1 + e2) * 0.5,
113        ModulusCombineRule::Reuss => {
114            let s = e1 + e2;
115            if s.abs() < f64::EPSILON {
116                0.0
117            } else {
118                2.0 * e1 * e2 / s
119            }
120        }
121        ModulusCombineRule::GeometricMean => (e1 * e2).sqrt(),
122    }
123}
124
125// ─── Hertz contact helpers ────────────────────────────────────────────────────
126
127/// Effective Young's modulus for Hertz contact between two elastic bodies.
128///
129/// 1/E* = (1-ν1²)/E1 + (1-ν2²)/E2
130pub fn hertz_effective_modulus(e1: f64, nu1: f64, e2: f64, nu2: f64) -> f64 {
131    let inv = (1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2;
132    if inv.abs() < f64::EPSILON {
133        0.0
134    } else {
135        1.0 / inv
136    }
137}
138
139/// Effective radius for Hertz contact between two spheres.
140///
141/// 1/R* = 1/R1 + 1/R2
142pub fn hertz_effective_radius(r1: f64, r2: f64) -> f64 {
143    let inv = 1.0 / r1 + 1.0 / r2;
144    if inv.abs() < f64::EPSILON {
145        0.0
146    } else {
147        1.0 / inv
148    }
149}
150
151/// Hertz contact force for two elastic spheres.
152///
153/// F = (4/3) * E* * sqrt(R*) * δ^(3/2)
154///
155/// where δ is the penetration depth.
156pub fn hertz_contact_force(e_star: f64, r_star: f64, penetration: f64) -> f64 {
157    if penetration <= 0.0 {
158        return 0.0;
159    }
160    (4.0 / 3.0) * e_star * r_star.sqrt() * penetration.powf(1.5)
161}
162
163// ─── Contact material pair ────────────────────────────────────────────────────
164
165/// Combined contact properties for a pair of materials.
166#[derive(Debug, Clone)]
167pub struct ContactMaterialPair {
168    /// Combined friction coefficient.
169    pub friction: f64,
170    /// Combined restitution coefficient.
171    pub restitution: f64,
172    /// Effective Young's modulus for contact stiffness (Pa).
173    pub effective_modulus: f64,
174    /// Contact damping coefficient (N·s/m).
175    pub damping: f64,
176}
177
178impl ContactMaterialPair {
179    /// Compute combined contact properties from two materials' properties.
180    ///
181    /// Uses geometric mean for friction, minimum for restitution, and
182    /// Hertz formula for effective modulus.
183    #[allow(clippy::too_many_arguments)]
184    pub fn from_materials(
185        friction1: f64,
186        restitution1: f64,
187        young1: f64,
188        poisson1: f64,
189        friction2: f64,
190        restitution2: f64,
191        young2: f64,
192        poisson2: f64,
193    ) -> Self {
194        Self {
195            friction: combine_friction(friction1, friction2, FrictionCombineRule::GeometricMean),
196            restitution: combine_restitution(
197                restitution1,
198                restitution2,
199                RestitutionCombineRule::Min,
200            ),
201            effective_modulus: hertz_effective_modulus(young1, poisson1, young2, poisson2),
202            damping: 0.0,
203        }
204    }
205}
206
207// ─── Thermal contact resistance ───────────────────────────────────────────────
208
209/// Thermal contact resistance at an interface between two materials.
210///
211/// R_contact = R1 + R_gap + R2
212#[derive(Debug, Clone, Copy)]
213pub struct ThermalContactResistance {
214    /// Thermal conductivity of material 1 (W/(m·K)).
215    pub k1: f64,
216    /// Thermal conductivity of material 2 (W/(m·K)).
217    pub k2: f64,
218    /// Gap conductance (W/(m²·K)) for the interfacial gap.
219    pub gap_conductance: f64,
220}
221
222impl ThermalContactResistance {
223    /// Create a thermal contact resistance model.
224    pub fn new(k1: f64, k2: f64, gap_conductance: f64) -> Self {
225        Self {
226            k1,
227            k2,
228            gap_conductance,
229        }
230    }
231
232    /// Combined thermal conductance at the interface (W/(m²·K)).
233    ///
234    /// Uses harmonic mean of the two conductivities plus the gap conductance.
235    pub fn combined_conductance(&self) -> f64 {
236        let harmonic_k = if (self.k1 + self.k2).abs() < f64::EPSILON {
237            0.0
238        } else {
239            2.0 * self.k1 * self.k2 / (self.k1 + self.k2)
240        };
241        harmonic_k + self.gap_conductance
242    }
243
244    /// Heat flux (W/m²) given temperature difference (K).
245    pub fn heat_flux(&self, delta_t: f64) -> f64 {
246        self.combined_conductance() * delta_t
247    }
248}
249
250// ─── Diffusion combining ──────────────────────────────────────────────────────
251
252/// Effective diffusion coefficient for a two-phase mixture (Maxwell model).
253///
254/// For spherical inclusions of phase 2 in matrix phase 1:
255/// D_eff = D1 * (D2 + 2*D1 - 2*phi2*(D1-D2)) / (D2 + 2*D1 + phi2*(D1-D2))
256pub fn maxwell_diffusivity(d1: f64, d2: f64, volume_fraction2: f64) -> f64 {
257    let phi = volume_fraction2.clamp(0.0, 1.0);
258    let num = d2 + 2.0 * d1 - 2.0 * phi * (d1 - d2);
259    let den = d2 + 2.0 * d1 + phi * (d1 - d2);
260    if den.abs() < f64::EPSILON {
261        d1
262    } else {
263        d1 * num / den
264    }
265}
266
267// ─── Voigt/Reuss/Hill averaging for composite materials ─────────────────────
268
269/// Voigt (upper bound) average of a composite property.
270///
271/// P_Voigt = Σ fᵢ * Pᵢ
272///
273/// where fᵢ are volume fractions and Pᵢ are phase properties.
274/// This is the iso-strain (parallel) bound.
275#[allow(dead_code)]
276pub fn voigt_average(volume_fractions: &[f64], properties: &[f64]) -> f64 {
277    volume_fractions
278        .iter()
279        .zip(properties.iter())
280        .map(|(&f, &p)| f * p)
281        .sum()
282}
283
284/// Reuss (lower bound) average of a composite property.
285///
286/// 1/P_Reuss = Σ fᵢ / Pᵢ
287///
288/// where fᵢ are volume fractions and Pᵢ are phase properties.
289/// This is the iso-stress (series) bound.
290#[allow(dead_code)]
291pub fn reuss_average(volume_fractions: &[f64], properties: &[f64]) -> f64 {
292    let inv_sum: f64 = volume_fractions
293        .iter()
294        .zip(properties.iter())
295        .map(|(&f, &p)| if p.abs() < f64::EPSILON { 0.0 } else { f / p })
296        .sum();
297    if inv_sum.abs() < f64::EPSILON {
298        0.0
299    } else {
300        1.0 / inv_sum
301    }
302}
303
304/// Hill (Voigt-Reuss-Hill) average of a composite property.
305///
306/// P_Hill = (P_Voigt + P_Reuss) / 2
307///
308/// Provides a practical estimate between the upper and lower bounds.
309#[allow(dead_code)]
310pub fn hill_average(volume_fractions: &[f64], properties: &[f64]) -> f64 {
311    let v = voigt_average(volume_fractions, properties);
312    let r = reuss_average(volume_fractions, properties);
313    0.5 * (v + r)
314}
315
316// ─── Rule of mixtures for fiber composites ──────────────────────────────────
317
318/// Longitudinal modulus of a unidirectional fiber composite via the rule of mixtures.
319///
320/// E_1 = V_f * E_f + (1 - V_f) * E_m
321///
322/// where V_f is fiber volume fraction, E_f is fiber modulus, E_m is matrix modulus.
323#[allow(dead_code)]
324pub fn rule_of_mixtures_longitudinal(vf: f64, e_fiber: f64, e_matrix: f64) -> f64 {
325    let vf = vf.clamp(0.0, 1.0);
326    vf * e_fiber + (1.0 - vf) * e_matrix
327}
328
329/// Transverse modulus of a unidirectional fiber composite via the inverse rule of mixtures.
330///
331/// 1/E_2 = V_f / E_f + (1 - V_f) / E_m
332#[allow(dead_code)]
333pub fn rule_of_mixtures_transverse(vf: f64, e_fiber: f64, e_matrix: f64) -> f64 {
334    let vf = vf.clamp(0.0, 1.0);
335    let inv = vf / e_fiber + (1.0 - vf) / e_matrix;
336    if inv.abs() < f64::EPSILON {
337        0.0
338    } else {
339        1.0 / inv
340    }
341}
342
343/// Longitudinal Poisson's ratio of a unidirectional fiber composite.
344///
345/// ν_12 = V_f * ν_f + (1 - V_f) * ν_m
346#[allow(dead_code)]
347pub fn rule_of_mixtures_poisson(vf: f64, nu_fiber: f64, nu_matrix: f64) -> f64 {
348    let vf = vf.clamp(0.0, 1.0);
349    vf * nu_fiber + (1.0 - vf) * nu_matrix
350}
351
352/// In-plane shear modulus of a unidirectional fiber composite (inverse rule).
353///
354/// 1/G_12 = V_f / G_f + (1 - V_f) / G_m
355#[allow(dead_code)]
356pub fn rule_of_mixtures_shear(vf: f64, g_fiber: f64, g_matrix: f64) -> f64 {
357    let vf = vf.clamp(0.0, 1.0);
358    let inv = vf / g_fiber + (1.0 - vf) / g_matrix;
359    if inv.abs() < f64::EPSILON {
360        0.0
361    } else {
362        1.0 / inv
363    }
364}
365
366/// Composite density via rule of mixtures.
367///
368/// ρ_c = V_f * ρ_f + (1 - V_f) * ρ_m
369#[allow(dead_code)]
370pub fn rule_of_mixtures_density(vf: f64, rho_fiber: f64, rho_matrix: f64) -> f64 {
371    let vf = vf.clamp(0.0, 1.0);
372    vf * rho_fiber + (1.0 - vf) * rho_matrix
373}
374
375// ─── Halpin-Tsai model ──────────────────────────────────────────────────────
376
377/// Halpin-Tsai model for transverse modulus of a fiber composite.
378///
379/// E_2 = E_m * (1 + ξ * η * V_f) / (1 - η * V_f)
380///
381/// where η = (E_f/E_m - 1) / (E_f/E_m + ξ)
382/// and ξ is a shape/packing factor (typically 1 or 2 for transverse modulus).
383#[allow(dead_code)]
384pub fn halpin_tsai_modulus(vf: f64, e_fiber: f64, e_matrix: f64, xi: f64) -> f64 {
385    let vf = vf.clamp(0.0, 1.0);
386    if e_matrix.abs() < f64::EPSILON {
387        return 0.0;
388    }
389    let ratio = e_fiber / e_matrix;
390    let eta = (ratio - 1.0) / (ratio + xi);
391    e_matrix * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
392}
393
394/// Halpin-Tsai model for shear modulus of a fiber composite.
395///
396/// G_12 = G_m * (1 + ξ * η * V_f) / (1 - η * V_f)
397///
398/// where η = (G_f/G_m - 1) / (G_f/G_m + ξ)
399/// and ξ is typically 1 for shear modulus.
400#[allow(dead_code)]
401pub fn halpin_tsai_shear(vf: f64, g_fiber: f64, g_matrix: f64, xi: f64) -> f64 {
402    let vf = vf.clamp(0.0, 1.0);
403    if g_matrix.abs() < f64::EPSILON {
404        return 0.0;
405    }
406    let ratio = g_fiber / g_matrix;
407    let eta = (ratio - 1.0) / (ratio + xi);
408    g_matrix * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
409}
410
411// ─── Effective thermal expansion ─────────────────────────────────────────────
412
413/// Effective longitudinal coefficient of thermal expansion (CTE)
414/// for a unidirectional composite using Schapery's formula.
415///
416/// α_1 = (V_f * E_f * α_f + V_m * E_m * α_m) / (V_f * E_f + V_m * E_m)
417#[allow(dead_code)]
418pub fn effective_cte_longitudinal(
419    vf: f64,
420    e_fiber: f64,
421    alpha_fiber: f64,
422    e_matrix: f64,
423    alpha_matrix: f64,
424) -> f64 {
425    let vf = vf.clamp(0.0, 1.0);
426    let vm = 1.0 - vf;
427    let num = vf * e_fiber * alpha_fiber + vm * e_matrix * alpha_matrix;
428    let den = vf * e_fiber + vm * e_matrix;
429    if den.abs() < f64::EPSILON {
430        0.0
431    } else {
432        num / den
433    }
434}
435
436/// Effective transverse CTE for a unidirectional composite (Schapery).
437///
438/// α_2 = (1 + ν_f) * V_f * α_f + (1 + ν_m) * V_m * α_m - α_1 * ν_12
439///
440/// where ν_12 is the composite Poisson's ratio.
441#[allow(dead_code)]
442#[allow(clippy::too_many_arguments)]
443pub fn effective_cte_transverse(
444    vf: f64,
445    alpha_fiber: f64,
446    nu_fiber: f64,
447    alpha_matrix: f64,
448    nu_matrix: f64,
449    alpha_1: f64,
450    nu_12: f64,
451) -> f64 {
452    let vf = vf.clamp(0.0, 1.0);
453    let vm = 1.0 - vf;
454    (1.0 + nu_fiber) * vf * alpha_fiber + (1.0 + nu_matrix) * vm * alpha_matrix - alpha_1 * nu_12
455}
456
457/// Effective bulk CTE for a particulate composite using Turner's model.
458///
459/// α_eff = (V_1 * K_1 * α_1 + V_2 * K_2 * α_2) / (V_1 * K_1 + V_2 * K_2)
460///
461/// where K is the bulk modulus and V is volume fraction.
462#[allow(dead_code)]
463pub fn turner_cte(v1: f64, k1: f64, alpha1: f64, k2: f64, alpha2: f64) -> f64 {
464    let v1 = v1.clamp(0.0, 1.0);
465    let v2 = 1.0 - v1;
466    let num = v1 * k1 * alpha1 + v2 * k2 * alpha2;
467    let den = v1 * k1 + v2 * k2;
468    if den.abs() < f64::EPSILON {
469        0.0
470    } else {
471        num / den
472    }
473}
474
475// ─── Classical Laminate Theory (CLT) basics ─────────────────────────────────
476
477/// Reduced stiffness matrix Q for an orthotropic lamina (plane stress).
478///
479/// Returns the 3x3 Q matrix as a flat \[f64; 9\] in row-major order:
480/// \[Q11, Q12, 0, Q12, Q22, 0, 0, 0, Q66\]
481///
482/// Q11 = E1 / (1 - ν12*ν21)
483/// Q22 = E2 / (1 - ν12*ν21)
484/// Q12 = ν12 * E2 / (1 - ν12*ν21)
485/// Q66 = G12
486#[allow(dead_code)]
487pub fn lamina_stiffness_matrix(e1: f64, e2: f64, nu12: f64, g12: f64) -> [f64; 9] {
488    let nu21 = nu12 * e2 / e1;
489    let denom = 1.0 - nu12 * nu21;
490    let q11 = e1 / denom;
491    let q22 = e2 / denom;
492    let q12 = nu12 * e2 / denom;
493    let q66 = g12;
494    [q11, q12, 0.0, q12, q22, 0.0, 0.0, 0.0, q66]
495}
496
497/// Transform a reduced stiffness matrix Q to an arbitrary angle θ (radians).
498///
499/// Returns the transformed Q-bar matrix as \[f64; 9\] in row-major order.
500/// Uses standard CLT transformation with m = cos(θ), n = sin(θ).
501#[allow(dead_code)]
502pub fn transform_stiffness(q: &[f64; 9], theta: f64) -> [f64; 9] {
503    let m = theta.cos();
504    let n = theta.sin();
505    let m2 = m * m;
506    let n2 = n * n;
507    let m4 = m2 * m2;
508    let n4 = n2 * n2;
509    let mn = m * n;
510    let m2n2 = m2 * n2;
511
512    let q11 = q[0];
513    let q12 = q[1];
514    let q22 = q[4];
515    let q66 = q[8];
516
517    let qb11 = q11 * m4 + 2.0 * (q12 + 2.0 * q66) * m2n2 + q22 * n4;
518    let qb22 = q11 * n4 + 2.0 * (q12 + 2.0 * q66) * m2n2 + q22 * m4;
519    let qb12 = (q11 + q22 - 4.0 * q66) * m2n2 + q12 * (m4 + n4);
520    let qb66 = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * m2n2 + q66 * (m4 + n4);
521    let qb16 = (q11 - q12 - 2.0 * q66) * m2 * mn + (q12 - q22 + 2.0 * q66) * n2 * mn;
522    let qb26 = (q11 - q12 - 2.0 * q66) * n2 * mn + (q12 - q22 + 2.0 * q66) * m2 * mn;
523
524    [qb11, qb12, qb16, qb12, qb22, qb26, qb16, qb26, qb66]
525}
526
527/// A single lamina (ply) in a laminate.
528#[derive(Debug, Clone)]
529pub struct Lamina {
530    /// Reduced stiffness matrix Q (3x3 row-major).
531    pub q: [f64; 9],
532    /// Ply orientation angle (radians).
533    pub theta: f64,
534    /// Ply thickness (m).
535    pub thickness: f64,
536}
537
538/// Compute the ABD stiffness matrices for a symmetric or general laminate.
539///
540/// Returns (A, B, D) as three \[f64; 9\] arrays in row-major order.
541///
542/// A_ij = Σ Q̄_ij_k * (z_k - z_{k-1})
543/// B_ij = (1/2) Σ Q̄_ij_k * (z_k² - z_{k-1}²)
544/// D_ij = (1/3) Σ Q̄_ij_k * (z_k³ - z_{k-1}³)
545///
546/// where z is measured from the laminate midplane.
547#[allow(dead_code)]
548pub fn laminate_abd(plies: &[Lamina]) -> ([f64; 9], [f64; 9], [f64; 9]) {
549    let total_thickness: f64 = plies.iter().map(|p| p.thickness).sum();
550    let mut z_bot = -total_thickness / 2.0;
551
552    let mut a_mat = [0.0f64; 9];
553    let mut b_mat = [0.0f64; 9];
554    let mut d_mat = [0.0f64; 9];
555
556    for ply in plies {
557        let z_top = z_bot + ply.thickness;
558        let qbar = transform_stiffness(&ply.q, ply.theta);
559
560        for i in 0..9 {
561            a_mat[i] += qbar[i] * (z_top - z_bot);
562            b_mat[i] += 0.5 * qbar[i] * (z_top * z_top - z_bot * z_bot);
563            d_mat[i] += (1.0 / 3.0) * qbar[i] * (z_top.powi(3) - z_bot.powi(3));
564        }
565
566        z_bot = z_top;
567    }
568
569    (a_mat, b_mat, d_mat)
570}
571
572/// Compute effective in-plane engineering constants from the A matrix of a laminate.
573///
574/// Returns (E_x, E_y, G_xy, nu_xy) assuming a symmetric laminate (B=0).
575#[allow(dead_code)]
576pub fn laminate_engineering_constants(
577    a_mat: &[f64; 9],
578    total_thickness: f64,
579) -> (f64, f64, f64, f64) {
580    // Normalized stiffness: a_ij = A_ij / h
581    let a11 = a_mat[0] / total_thickness;
582    let a12 = a_mat[1] / total_thickness;
583    let a22 = a_mat[4] / total_thickness;
584    let a66 = a_mat[8] / total_thickness;
585
586    let det = a11 * a22 - a12 * a12;
587    if det.abs() < f64::EPSILON {
588        return (0.0, 0.0, 0.0, 0.0);
589    }
590
591    let ex = det / a22;
592    let ey = det / a11;
593    let nu_xy = a12 / a22;
594    let gxy = a66;
595
596    (ex, ey, gxy, nu_xy)
597}
598
599// ─── Hashin-Shtrikman bounds ────────────────────────────────────────────────
600
601/// Hashin-Shtrikman upper bound for bulk modulus of a two-phase composite.
602///
603/// K_upper = K_2 + V_1 / (1/(K_1-K_2) + 3*V_2/(3*K_2+4*G_2))
604///
605/// Assumes K_2 > K_1 (phase 2 is the stiffer phase).
606#[allow(dead_code)]
607pub fn hashin_shtrikman_bulk_upper(v1: f64, k1: f64, k2: f64, g2: f64) -> f64 {
608    let v1 = v1.clamp(0.0, 1.0);
609    let v2 = 1.0 - v1;
610    let dk = k1 - k2;
611    if dk.abs() < f64::EPSILON {
612        return k1;
613    }
614    let inv = 1.0 / dk + 3.0 * v2 / (3.0 * k2 + 4.0 * g2);
615    if inv.abs() < f64::EPSILON {
616        k2
617    } else {
618        k2 + v1 / inv
619    }
620}
621
622/// Hashin-Shtrikman lower bound for bulk modulus of a two-phase composite.
623///
624/// K_lower = K_1 + V_2 / (1/(K_2-K_1) + 3*V_1/(3*K_1+4*G_1))
625///
626/// Assumes K_1 < K_2 (phase 1 is the softer phase).
627#[allow(dead_code)]
628pub fn hashin_shtrikman_bulk_lower(v1: f64, k1: f64, g1: f64, k2: f64) -> f64 {
629    let v1 = v1.clamp(0.0, 1.0);
630    let v2 = 1.0 - v1;
631    let dk = k2 - k1;
632    if dk.abs() < f64::EPSILON {
633        return k1;
634    }
635    let inv = 1.0 / dk + 3.0 * v1 / (3.0 * k1 + 4.0 * g1);
636    if inv.abs() < f64::EPSILON {
637        k1
638    } else {
639        k1 + v2 / inv
640    }
641}
642
643// ─── Tests ────────────────────────────────────────────────────────────────────
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648
649    use crate::combination::halpin_tsai_modulus;
650
651    #[test]
652    fn test_combine_friction_average() {
653        let r = combine_friction(0.4, 0.6, FrictionCombineRule::Average);
654        assert!((r - 0.5).abs() < 1e-10);
655    }
656
657    #[test]
658    fn test_combine_friction_geometric() {
659        let r = combine_friction(0.25, 1.0, FrictionCombineRule::GeometricMean);
660        assert!((r - 0.5).abs() < 1e-10);
661    }
662
663    #[test]
664    fn test_combine_friction_harmonic() {
665        let r = combine_friction(1.0, 1.0, FrictionCombineRule::HarmonicMean);
666        assert!((r - 1.0).abs() < 1e-10);
667    }
668
669    #[test]
670    fn test_combine_friction_min() {
671        assert!((combine_friction(0.4, 0.6, FrictionCombineRule::Min) - 0.4).abs() < 1e-10);
672    }
673
674    #[test]
675    fn test_combine_friction_max() {
676        assert!((combine_friction(0.4, 0.6, FrictionCombineRule::Max) - 0.6).abs() < 1e-10);
677    }
678
679    #[test]
680    fn test_combine_friction_multiply() {
681        assert!((combine_friction(0.4, 0.6, FrictionCombineRule::Multiply) - 0.24).abs() < 1e-10);
682    }
683
684    #[test]
685    fn test_combine_restitution_rules() {
686        assert!(
687            (combine_restitution(0.3, 0.7, RestitutionCombineRule::Average) - 0.5).abs() < 1e-10
688        );
689        assert!((combine_restitution(0.3, 0.7, RestitutionCombineRule::Min) - 0.3).abs() < 1e-10);
690        assert!((combine_restitution(0.3, 0.7, RestitutionCombineRule::Max) - 0.7).abs() < 1e-10);
691        assert!(
692            (combine_restitution(0.3, 0.7, RestitutionCombineRule::Multiply) - 0.21).abs() < 1e-10
693        );
694    }
695
696    #[test]
697    fn test_hertz_effective_modulus_equal_materials() {
698        let e = 200e9_f64;
699        let nu = 0.3_f64;
700        let e_star = hertz_effective_modulus(e, nu, e, nu);
701        let expected = e / (2.0 * (1.0 - nu * nu));
702        assert!((e_star - expected).abs() / expected < 1e-10);
703    }
704
705    #[test]
706    fn test_hertz_contact_force_zero_penetration() {
707        assert_eq!(hertz_contact_force(200e9, 0.01, 0.0), 0.0);
708        assert_eq!(hertz_contact_force(200e9, 0.01, -0.001), 0.0);
709    }
710
711    #[test]
712    fn test_hertz_effective_radius() {
713        let r = hertz_effective_radius(1.0, 1.0);
714        assert!((r - 0.5).abs() < 1e-10);
715    }
716
717    #[test]
718    fn test_thermal_contact_heat_flux() {
719        let tcr = ThermalContactResistance::new(50.0, 50.0, 0.0);
720        let cond = tcr.combined_conductance();
721        assert!((cond - 50.0).abs() < 1e-6);
722        let flux = tcr.heat_flux(10.0);
723        assert!((flux - 500.0).abs() < 1e-6);
724    }
725
726    #[test]
727    fn test_maxwell_diffusivity_pure_phase() {
728        let d = maxwell_diffusivity(1.0, 5.0, 0.0);
729        assert!((d - 1.0).abs() < 1e-10);
730        let d2 = maxwell_diffusivity(1.0, 5.0, 1.0);
731        assert!((d2 - 5.0).abs() < 1e-6);
732    }
733
734    #[test]
735    fn test_contact_material_pair() {
736        let pair = ContactMaterialPair::from_materials(0.5, 0.4, 200e9, 0.3, 0.7, 0.8, 70e9, 0.33);
737        assert!(pair.friction > 0.0);
738        assert!(pair.restitution <= 0.4);
739        assert!(pair.effective_modulus > 0.0);
740    }
741
742    // ─── Voigt/Reuss/Hill tests ─────────────────────────────────────────
743
744    #[test]
745    fn test_voigt_average_equal_phases() {
746        // Two equal phases → average = that value
747        let vf = [0.5, 0.5];
748        let props = [200.0e9, 200.0e9];
749        let result = voigt_average(&vf, &props);
750        assert!((result - 200.0e9).abs() < 1.0);
751    }
752
753    #[test]
754    fn test_voigt_average_weighted() {
755        // 60% phase1 (100 GPa) + 40% phase2 (200 GPa) = 140 GPa
756        let vf = [0.6, 0.4];
757        let props = [100.0e9, 200.0e9];
758        let result = voigt_average(&vf, &props);
759        assert!((result - 140.0e9).abs() < 1.0);
760    }
761
762    #[test]
763    fn test_reuss_average_equal_phases() {
764        let vf = [0.5, 0.5];
765        let props = [200.0e9, 200.0e9];
766        let result = reuss_average(&vf, &props);
767        assert!((result - 200.0e9).abs() < 1.0);
768    }
769
770    #[test]
771    fn test_reuss_leq_voigt() {
772        // Reuss bound <= Voigt bound always
773        let vf = [0.3, 0.7];
774        let props = [70.0e9, 200.0e9];
775        let v = voigt_average(&vf, &props);
776        let r = reuss_average(&vf, &props);
777        assert!(r <= v + 1e-6, "Reuss {r} should be <= Voigt {v}");
778    }
779
780    #[test]
781    fn test_hill_between_voigt_reuss() {
782        let vf = [0.4, 0.6];
783        let props = [70.0e9, 200.0e9];
784        let v = voigt_average(&vf, &props);
785        let r = reuss_average(&vf, &props);
786        let h = hill_average(&vf, &props);
787        assert!(
788            h >= r - 1e-6 && h <= v + 1e-6,
789            "Hill {h} not between Reuss {r} and Voigt {v}"
790        );
791    }
792
793    #[test]
794    fn test_hill_average_value() {
795        let vf = [0.5, 0.5];
796        let props = [100.0e9, 200.0e9];
797        let h = hill_average(&vf, &props);
798        // Voigt = 150 GPa, Reuss = 2*100*200/(100+200)*1e9 = 133.33 GPa
799        // Hill = (150+133.33)/2 ~ 141.67 GPa
800        assert!((h - 141.666666e9).abs() / h < 1e-4);
801    }
802
803    // ─── Rule of mixtures tests ─────────────────────────────────────────
804
805    #[test]
806    fn test_rom_longitudinal() {
807        // Glass fiber (72 GPa) in epoxy (3.5 GPa), Vf = 0.6
808        let e1 = rule_of_mixtures_longitudinal(0.6, 72.0e9, 3.5e9);
809        let expected = 0.6 * 72.0e9 + 0.4 * 3.5e9;
810        assert!((e1 - expected).abs() < 1.0);
811    }
812
813    #[test]
814    fn test_rom_transverse() {
815        let e2 = rule_of_mixtures_transverse(0.6, 72.0e9, 3.5e9);
816        // Transverse modulus should be dominated by matrix
817        assert!(
818            e2 > 3.5e9,
819            "Transverse modulus should exceed matrix modulus"
820        );
821        assert!(
822            e2 < 72.0e9,
823            "Transverse modulus should be less than fiber modulus"
824        );
825    }
826
827    #[test]
828    fn test_rom_transverse_leq_longitudinal() {
829        let e1 = rule_of_mixtures_longitudinal(0.6, 72.0e9, 3.5e9);
830        let e2 = rule_of_mixtures_transverse(0.6, 72.0e9, 3.5e9);
831        assert!(e2 < e1, "E2 ({e2}) should be less than E1 ({e1})");
832    }
833
834    #[test]
835    fn test_rom_poisson() {
836        let nu = rule_of_mixtures_poisson(0.5, 0.22, 0.35);
837        let expected = 0.5 * 0.22 + 0.5 * 0.35;
838        assert!((nu - expected).abs() < 1e-10);
839    }
840
841    #[test]
842    fn test_rom_shear() {
843        let g = rule_of_mixtures_shear(0.5, 30.0e9, 1.3e9);
844        assert!(g > 1.3e9);
845        assert!(g < 30.0e9);
846    }
847
848    #[test]
849    fn test_rom_density() {
850        // Carbon fiber (1.8 g/cc) + epoxy (1.2 g/cc), Vf = 0.55
851        let rho = rule_of_mixtures_density(0.55, 1800.0, 1200.0);
852        let expected = 0.55 * 1800.0 + 0.45 * 1200.0;
853        assert!((rho - expected).abs() < 1e-6);
854    }
855
856    // ─── Halpin-Tsai tests ──────────────────────────────────────────────
857
858    #[test]
859    fn test_halpin_tsai_pure_matrix() {
860        // Vf=0 → should return matrix modulus
861        let e2 = halpin_tsai_modulus(0.0, 72.0e9, 3.5e9, 2.0);
862        assert!((e2 - 3.5e9).abs() < 1.0);
863    }
864
865    #[test]
866    fn test_halpin_tsai_between_bounds() {
867        // Result should be between Reuss and Voigt bounds
868        let vf = 0.6;
869        let ef = 72.0e9;
870        let em = 3.5e9;
871        let e_ht = halpin_tsai_modulus(vf, ef, em, 2.0);
872        let e_voigt = rule_of_mixtures_longitudinal(vf, ef, em);
873        let e_reuss = rule_of_mixtures_transverse(vf, ef, em);
874        assert!(
875            e_ht >= e_reuss - 1e-6 && e_ht <= e_voigt + 1e-6,
876            "HT {e_ht} not between Reuss {e_reuss} and Voigt {e_voigt}"
877        );
878    }
879
880    #[test]
881    fn test_halpin_tsai_xi_effect() {
882        // Higher ξ → closer to Voigt bound
883        let vf = 0.5;
884        let ef = 72.0e9;
885        let em = 3.5e9;
886        let e_low_xi = halpin_tsai_modulus(vf, ef, em, 1.0);
887        let e_high_xi = halpin_tsai_modulus(vf, ef, em, 10.0);
888        assert!(e_high_xi > e_low_xi, "Higher ξ should give higher modulus");
889    }
890
891    #[test]
892    fn test_halpin_tsai_shear() {
893        let g = halpin_tsai_shear(0.5, 30.0e9, 1.3e9, 1.0);
894        assert!(g > 1.3e9);
895        assert!(g < 30.0e9);
896    }
897
898    // ─── Effective CTE tests ────────────────────────────────────────────
899
900    #[test]
901    fn test_cte_longitudinal_equal_materials() {
902        // Same CTE → effective CTE = that CTE
903        let alpha = effective_cte_longitudinal(0.5, 200.0e9, 12.0e-6, 200.0e9, 12.0e-6);
904        assert!((alpha - 12.0e-6).abs() < 1e-15);
905    }
906
907    #[test]
908    fn test_cte_longitudinal_stiff_fiber_dominates() {
909        // Stiff fiber with low CTE should pull result toward fiber CTE
910        let vf = 0.6;
911        let alpha = effective_cte_longitudinal(vf, 230.0e9, 5.0e-6, 3.5e9, 60.0e-6);
912        // Should be much closer to fiber CTE (5e-6) than matrix CTE (60e-6)
913        assert!(
914            alpha < 20.0e-6,
915            "Expected CTE near fiber value, got {alpha}"
916        );
917    }
918
919    #[test]
920    fn test_turner_cte_equal_phases() {
921        let alpha = turner_cte(0.5, 100.0e9, 10.0e-6, 100.0e9, 10.0e-6);
922        assert!((alpha - 10.0e-6).abs() < 1e-15);
923    }
924
925    #[test]
926    fn test_turner_cte_bulk_weighted() {
927        // Phase with higher bulk modulus dominates
928        let alpha = turner_cte(0.5, 200.0e9, 5.0e-6, 50.0e9, 20.0e-6);
929        // Weighted toward 5e-6 since K1 is much larger
930        assert!(
931            alpha < 12.5e-6,
932            "Turner CTE should be weighted toward stiffer phase"
933        );
934    }
935
936    #[test]
937    fn test_cte_transverse() {
938        let vf = 0.5;
939        let alpha_1 = effective_cte_longitudinal(vf, 230.0e9, 5.0e-6, 3.5e9, 60.0e-6);
940        let nu_12 = rule_of_mixtures_poisson(vf, 0.2, 0.35);
941        let alpha_2 = effective_cte_transverse(vf, 5.0e-6, 0.2, 60.0e-6, 0.35, alpha_1, nu_12);
942        // Transverse CTE should be larger than longitudinal
943        assert!(
944            alpha_2 > alpha_1,
945            "Transverse CTE should exceed longitudinal"
946        );
947    }
948
949    // ─── CLT tests ──────────────────────────────────────────────────────
950
951    #[test]
952    fn test_lamina_stiffness_matrix() {
953        let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
954        // Q11 should be close to E1/(1-nu12*nu21)
955        let nu21 = 0.3 * 10.0e9 / 140.0e9;
956        let denom = 1.0 - 0.3 * nu21;
957        let expected_q11 = 140.0e9 / denom;
958        assert!((q[0] - expected_q11).abs() / expected_q11 < 1e-10);
959        // Off-diagonal terms Q13=Q31=Q23=Q32=0
960        assert_eq!(q[2], 0.0);
961        assert_eq!(q[6], 0.0);
962    }
963
964    #[test]
965    fn test_transform_stiffness_zero_angle() {
966        let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
967        let qbar = transform_stiffness(&q, 0.0);
968        for i in 0..9 {
969            assert!(
970                (qbar[i] - q[i]).abs() < 1.0,
971                "Q-bar[{i}] = {} should match Q[{i}] = {} at θ=0",
972                qbar[i],
973                q[i]
974            );
975        }
976    }
977
978    #[test]
979    fn test_transform_stiffness_90_degrees() {
980        let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
981        let qbar = transform_stiffness(&q, std::f64::consts::FRAC_PI_2);
982        // At 90°, Q̄11 ≈ Q22 and Q̄22 ≈ Q11
983        assert!(
984            (qbar[0] - q[4]).abs() / q[4] < 1e-8,
985            "Q̄11 should ≈ Q22 at 90°"
986        );
987        assert!(
988            (qbar[4] - q[0]).abs() / q[0] < 1e-8,
989            "Q̄22 should ≈ Q11 at 90°"
990        );
991    }
992
993    #[test]
994    #[allow(clippy::needless_range_loop)]
995    fn test_laminate_abd_symmetric() {
996        // Symmetric laminate [0/90]_s should have B=0
997        let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
998        let plies = vec![
999            Lamina {
1000                q,
1001                theta: 0.0,
1002                thickness: 0.125e-3,
1003            },
1004            Lamina {
1005                q,
1006                theta: std::f64::consts::FRAC_PI_2,
1007                thickness: 0.125e-3,
1008            },
1009            Lamina {
1010                q,
1011                theta: std::f64::consts::FRAC_PI_2,
1012                thickness: 0.125e-3,
1013            },
1014            Lamina {
1015                q,
1016                theta: 0.0,
1017                thickness: 0.125e-3,
1018            },
1019        ];
1020        let (_a, b, _d) = laminate_abd(&plies);
1021        for i in 0..9 {
1022            assert!(
1023                b[i].abs() < 1e-3,
1024                "B[{i}]={} should be ~0 for symmetric laminate",
1025                b[i]
1026            );
1027        }
1028    }
1029
1030    #[test]
1031    fn test_laminate_abd_a_positive_diagonal() {
1032        let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
1033        let plies = vec![
1034            Lamina {
1035                q,
1036                theta: 0.0,
1037                thickness: 0.25e-3,
1038            },
1039            Lamina {
1040                q,
1041                theta: std::f64::consts::FRAC_PI_2,
1042                thickness: 0.25e-3,
1043            },
1044        ];
1045        let (a, _b, _d) = laminate_abd(&plies);
1046        assert!(a[0] > 0.0, "A11 should be positive");
1047        assert!(a[4] > 0.0, "A22 should be positive");
1048        assert!(a[8] > 0.0, "A66 should be positive");
1049    }
1050
1051    #[test]
1052    fn test_laminate_engineering_constants() {
1053        let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
1054        let plies = vec![
1055            Lamina {
1056                q,
1057                theta: 0.0,
1058                thickness: 0.125e-3,
1059            },
1060            Lamina {
1061                q,
1062                theta: std::f64::consts::FRAC_PI_2,
1063                thickness: 0.125e-3,
1064            },
1065            Lamina {
1066                q,
1067                theta: std::f64::consts::FRAC_PI_2,
1068                thickness: 0.125e-3,
1069            },
1070            Lamina {
1071                q,
1072                theta: 0.0,
1073                thickness: 0.125e-3,
1074            },
1075        ];
1076        let total_h: f64 = plies.iter().map(|p| p.thickness).sum();
1077        let (a, _b, _d) = laminate_abd(&plies);
1078        let (ex, ey, gxy, _nu_xy) = laminate_engineering_constants(&a, total_h);
1079        assert!(ex > 0.0, "Ex should be positive");
1080        assert!(ey > 0.0, "Ey should be positive");
1081        assert!(gxy > 0.0, "Gxy should be positive");
1082        // [0/90]_s → Ex ≈ Ey due to balanced layup
1083        assert!(
1084            (ex - ey).abs() / ex < 1e-6,
1085            "Ex ({ex}) should ≈ Ey ({ey}) for balanced layup"
1086        );
1087    }
1088
1089    // ─── Hashin-Shtrikman tests ─────────────────────────────────────────
1090
1091    #[test]
1092    fn test_hashin_shtrikman_bounds_ordering() {
1093        // Lower <= Hill <= Upper
1094        let v1 = 0.4;
1095        let k1 = 75.0e9; // aluminum-like
1096        let g1 = 26.0e9;
1097        let k2 = 160.0e9; // steel-like
1098        let g2 = 80.0e9;
1099
1100        let hs_lower = hashin_shtrikman_bulk_lower(v1, k1, g1, k2);
1101        let hs_upper = hashin_shtrikman_bulk_upper(v1, k1, k2, g2);
1102        assert!(
1103            hs_lower <= hs_upper + 1e-6,
1104            "HS lower {hs_lower} should be <= HS upper {hs_upper}"
1105        );
1106    }
1107
1108    #[test]
1109    fn test_hashin_shtrikman_pure_phases() {
1110        // v1=1 → K = K1
1111        let hs = hashin_shtrikman_bulk_lower(1.0, 75.0e9, 26.0e9, 160.0e9);
1112        assert!((hs - 75.0e9).abs() / 75.0e9 < 1e-6);
1113    }
1114}
1115
1116// ─── Multi-scale homogenization ──────────────────────────────────────────────
1117
1118/// Two-scale homogenization result for a periodic composite.
1119#[derive(Debug, Clone)]
1120pub struct HomogenizationResult {
1121    /// Effective stiffness tensor (6×6 Voigt notation, row-major \[f64; 36\]).
1122    pub c_eff: [f64; 36],
1123    /// Effective Young's modulus in the x direction.
1124    pub e_x: f64,
1125    /// Effective Poisson's ratio ν_xy.
1126    pub nu_xy: f64,
1127    /// Effective shear modulus G_xy.
1128    pub g_xy: f64,
1129}
1130
1131/// Simple self-consistent (Eshelby-type) homogenization for a two-phase composite.
1132///
1133/// Uses the Mori-Tanaka (MT) approximation for effective bulk and shear moduli.
1134///
1135/// K_eff = K_m + V_f * (K_f - K_m) / (1 + V_m * (K_f - K_m) / (K_m + 4/3 G_m))
1136/// G_eff = G_m + V_f * (G_f - G_m) / (1 + V_m * (G_f - G_m) * (6*(K_m + 2*G_m)) / (5*G_m*(3*K_m + 4*G_m)))
1137#[allow(dead_code)]
1138pub fn mori_tanaka_homogenization(
1139    vf: f64,
1140    k_fiber: f64,
1141    g_fiber: f64,
1142    k_matrix: f64,
1143    g_matrix: f64,
1144) -> (f64, f64) {
1145    let vf = vf.clamp(0.0, 1.0);
1146    let vm = 1.0 - vf;
1147
1148    // Effective bulk modulus
1149    let dk = k_fiber - k_matrix;
1150    let k_denom = k_matrix + 4.0 / 3.0 * g_matrix;
1151    let k_eff = if k_denom.abs() < f64::EPSILON {
1152        k_matrix
1153    } else {
1154        k_matrix + vf * dk / (1.0 + vm * dk / k_denom)
1155    };
1156
1157    // Effective shear modulus
1158    let dg = g_fiber - g_matrix;
1159    let beta =
1160        6.0 * (k_matrix + 2.0 * g_matrix) / (5.0 * g_matrix * (3.0 * k_matrix + 4.0 * g_matrix));
1161    let g_denom = if (g_matrix * (3.0 * k_matrix + 4.0 * g_matrix)).abs() < f64::EPSILON {
1162        1.0
1163    } else {
1164        1.0 + vm * dg * beta
1165    };
1166    let g_eff = g_matrix + vf * dg / g_denom;
1167
1168    (k_eff, g_eff)
1169}
1170
1171/// Convert bulk modulus K and shear modulus G to engineering constants (E, ν).
1172#[allow(dead_code)]
1173pub fn bulk_shear_to_engineering(k: f64, g: f64) -> (f64, f64) {
1174    let e = 9.0 * k * g / (3.0 * k + g);
1175    let nu = (3.0 * k - 2.0 * g) / (2.0 * (3.0 * k + g));
1176    (e, nu)
1177}
1178
1179/// Build a `HomogenizationResult` from the Mori-Tanaka effective moduli.
1180#[allow(dead_code)]
1181pub fn homogenization_result(
1182    vf: f64,
1183    k_fiber: f64,
1184    g_fiber: f64,
1185    k_matrix: f64,
1186    g_matrix: f64,
1187) -> HomogenizationResult {
1188    let (k_eff, g_eff) = mori_tanaka_homogenization(vf, k_fiber, g_fiber, k_matrix, g_matrix);
1189    let (e_x, nu_xy) = bulk_shear_to_engineering(k_eff, g_eff);
1190
1191    // Build isotropic 6×6 stiffness tensor (Voigt notation)
1192    let lam = k_eff - 2.0 / 3.0 * g_eff; // Lamé λ
1193    let mut c_eff = [0.0f64; 36];
1194    // C_11 = C_22 = C_33 = λ + 2G
1195    c_eff[0] = lam + 2.0 * g_eff;
1196    c_eff[7] = lam + 2.0 * g_eff;
1197    c_eff[14] = lam + 2.0 * g_eff;
1198    // Off-diagonal C_12 = C_13 = C_23 = λ
1199    c_eff[1] = lam;
1200    c_eff[6] = lam;
1201    c_eff[2] = lam;
1202    c_eff[12] = lam;
1203    c_eff[9] = lam;
1204    c_eff[13] = lam;
1205    // Shear C_44 = C_55 = C_66 = G
1206    c_eff[21] = g_eff;
1207    c_eff[28] = g_eff;
1208    c_eff[35] = g_eff;
1209
1210    HomogenizationResult {
1211        c_eff,
1212        e_x,
1213        nu_xy,
1214        g_xy: g_eff,
1215    }
1216}
1217
1218// ─── Random fiber composite models ──────────────────────────────────────────
1219
1220/// Randomly oriented short-fiber composite (Cox-Krenchel model).
1221///
1222/// For randomly oriented (3-D isotropic) short fibers:
1223/// E_composite = η_l * η_o * V_f * E_f + (1 - V_f) * E_m
1224///
1225/// where η_o = 1/5 (random 3-D orientation factor)
1226/// and η_l is the fiber length efficiency factor from Cox shear-lag.
1227///
1228/// η_l = 1 - tanh(β * l/2) / (β * l/2)
1229/// β = sqrt(2*G_m / (E_f * A * ln(R/r)))
1230/// Simplified here to η_l as a direct input.
1231#[allow(dead_code)]
1232pub fn cox_krenchel_modulus(vf: f64, e_fiber: f64, e_matrix: f64, eta_l: f64) -> f64 {
1233    let vf = vf.clamp(0.0, 1.0);
1234    let vm = 1.0 - vf;
1235    let eta_o = 1.0 / 5.0; // 3-D random orientation
1236    eta_l * eta_o * vf * e_fiber + vm * e_matrix
1237}
1238
1239/// Length efficiency factor η_l for the Cox shear-lag model.
1240///
1241/// η_l = 1 - tanh(n * L / 2) / (n * L / 2)
1242///
1243/// where n = sqrt(2 * G_m / (E_f * ln(R / r))) and L is the fiber length,
1244/// r is the fiber radius, R is the mean fibre spacing radius.
1245#[allow(dead_code)]
1246pub fn cox_length_efficiency(_fiber_length: f64, beta_l_over_2: f64) -> f64 {
1247    // beta_l_over_2 = β * L/2, a dimensionless parameter
1248    let x = beta_l_over_2;
1249    if x < 1e-10 {
1250        return 0.0;
1251    }
1252    1.0 - x.tanh() / x
1253}
1254
1255/// Halpin-Tsai model for randomly oriented fibers (2-D in-plane isotropy).
1256///
1257/// E_iso = (3/8) * E_11 + (5/8) * E_22
1258///
1259/// where E_11 and E_22 are the longitudinal and transverse Halpin-Tsai moduli.
1260#[allow(dead_code)]
1261pub fn halpin_tsai_random_2d(vf: f64, e_fiber: f64, e_matrix: f64, xi: f64) -> f64 {
1262    let e11 = rule_of_mixtures_longitudinal(vf, e_fiber, e_matrix);
1263    let e22 = halpin_tsai_modulus(vf, e_fiber, e_matrix, xi);
1264    3.0 / 8.0 * e11 + 5.0 / 8.0 * e22
1265}
1266
1267// ─── Woven composite homogenization ─────────────────────────────────────────
1268
1269/// Mosaic model for a plain-weave composite.
1270///
1271/// Divides the unit cell into undulation and straight regions.
1272/// The effective in-plane modulus is estimated as a Voigt average
1273/// of straight and crimped regions.
1274///
1275/// `vf_warp` and `vf_fill` are volume fractions in warp and fill tows.
1276/// `e_tow` is the effective modulus of a straight tow.
1277/// `e_matrix` is the matrix modulus.
1278/// `crimp_angle` is the crimp half-angle (rad) of the tow undulation.
1279#[allow(dead_code)]
1280pub fn woven_mosaic_modulus(
1281    vf_warp: f64,
1282    vf_fill: f64,
1283    e_tow: f64,
1284    e_matrix: f64,
1285    crimp_angle: f64,
1286) -> f64 {
1287    let vf_warp = vf_warp.clamp(0.0, 1.0);
1288    let vf_fill = vf_fill.clamp(0.0, 1.0);
1289    let vm = (1.0 - vf_warp - vf_fill).max(0.0);
1290
1291    // Crimped tow: modulus reduced by cos^4(θ) (classical off-axis rule)
1292    let e_tow_crimp = e_tow * crimp_angle.cos().powi(4);
1293
1294    // Voigt combination: straight warp + crimped fill + matrix
1295    vf_warp * e_tow + vf_fill * e_tow_crimp + vm * e_matrix
1296}
1297
1298/// Bridging model shear modulus for a plain weave composite.
1299///
1300/// G_eff = G_m * (V_f * G_f + V_m * G_m) / (V_m * G_f + V_f * G_m)
1301/// (analogous to Reuss shear, weighted by bridging)
1302#[allow(dead_code)]
1303pub fn woven_shear_modulus(vf: f64, g_fiber: f64, g_matrix: f64) -> f64 {
1304    let vf = vf.clamp(0.0, 1.0);
1305    let vm = 1.0 - vf;
1306    let num = g_matrix * (vf * g_fiber + vm * g_matrix);
1307    let den = vm * g_fiber + vf * g_matrix;
1308    if den.abs() < f64::EPSILON {
1309        g_matrix
1310    } else {
1311        num / den
1312    }
1313}
1314
1315// ─── Particulate composite ───────────────────────────────────────────────────
1316
1317/// Kerner model for bulk modulus of a particulate composite.
1318///
1319/// K_eff / K_m = \[1 + V_p * (K_p - K_m) / (K_m + 4/3 G_m * (1 - V_p))\]
1320///
1321/// This is equivalent to the dilute Eshelby estimate.
1322#[allow(dead_code)]
1323pub fn kerner_bulk_modulus(vp: f64, k_particle: f64, g_matrix: f64, k_matrix: f64) -> f64 {
1324    let vp = vp.clamp(0.0, 1.0);
1325    let dk = k_particle - k_matrix;
1326    let denom = k_matrix + 4.0 / 3.0 * g_matrix * (1.0 - vp);
1327    if denom.abs() < f64::EPSILON {
1328        k_matrix
1329    } else {
1330        k_matrix + vp * dk * k_matrix / denom
1331    }
1332}
1333
1334/// Nielsen model for tensile modulus of a particulate composite.
1335///
1336/// E_eff = E_m * (1 + A_E * B_E * V_p) / (1 - B_E * ψ * V_p)
1337///
1338/// where B_E = (E_p/E_m - 1) / (E_p/E_m + A_E)
1339/// ψ = 1 + (1 - φ_m) * V_p / φ_m²
1340/// φ_m is the maximum packing fraction (e.g. 0.637 for random packing)
1341/// A_E is the Einstein coefficient (e.g. 2.5 for spheres)
1342#[allow(dead_code)]
1343pub fn nielsen_modulus(vp: f64, e_particle: f64, e_matrix: f64, a_e: f64, phi_max: f64) -> f64 {
1344    let vp = vp.clamp(0.0, phi_max);
1345    if e_matrix.abs() < f64::EPSILON {
1346        return 0.0;
1347    }
1348    let ratio = e_particle / e_matrix;
1349    let b_e = (ratio - 1.0) / (ratio + a_e);
1350    let psi = 1.0 + (1.0 - phi_max) * vp / (phi_max * phi_max);
1351    let denom = 1.0 - b_e * psi * vp;
1352    if denom.abs() < f64::EPSILON {
1353        e_matrix
1354    } else {
1355        e_matrix * (1.0 + a_e * b_e * vp) / denom
1356    }
1357}
1358
1359/// Composite sphere model for the effective bulk modulus (Hashin).
1360///
1361/// K_eff = K_2 + V_1 / (1/(K_1 - K_2) + 3*V_2 / (3*K_2 + 4*G_2))
1362///
1363/// (identical to Mori-Tanaka for spherical inclusions in an isotropic matrix)
1364#[allow(dead_code)]
1365pub fn composite_sphere_bulk(vp: f64, k_inclusion: f64, g_matrix: f64, k_matrix: f64) -> f64 {
1366    let vp = vp.clamp(0.0, 1.0);
1367    let vm = 1.0 - vp;
1368    let dk = k_inclusion - k_matrix;
1369    let inv = 1.0 / dk + 3.0 * vm / (3.0 * k_matrix + 4.0 * g_matrix);
1370    if inv.abs() < f64::EPSILON || dk.abs() < f64::EPSILON {
1371        k_matrix
1372    } else {
1373        k_matrix + vp / inv
1374    }
1375}
1376
1377// ─── Nano-composite effective properties ─────────────────────────────────────
1378
1379/// Interface/interphase correction for nano-composites.
1380///
1381/// For nano-fillers, the interface layer has non-negligible thickness.
1382/// Effective volume fraction including interphase:
1383///
1384/// V_eff = V_f * (1 + t / r)^3
1385///
1386/// where t is interphase thickness and r is particle radius.
1387#[allow(dead_code)]
1388pub fn nano_effective_volume_fraction(
1389    vf: f64,
1390    particle_radius: f64,
1391    interphase_thickness: f64,
1392) -> f64 {
1393    let vf = vf.clamp(0.0, 1.0);
1394    let factor = (1.0 + interphase_thickness / particle_radius).powi(3);
1395    (vf * factor).min(1.0)
1396}
1397
1398/// Nano-composite modulus with interphase using a three-phase model.
1399///
1400/// Treats the composite as matrix + interphase + core particle,
1401/// applying the Mori-Tanaka sequentially:
1402/// 1) Combine core particle + interphase → effective inclusion.
1403/// 2) Combine effective inclusion + matrix → composite modulus.
1404#[allow(dead_code)]
1405pub fn nano_composite_modulus(
1406    vf_core: f64,
1407    e_core: f64,
1408    vf_interphase: f64,
1409    e_interphase: f64,
1410    e_matrix: f64,
1411    xi: f64,
1412) -> f64 {
1413    // Step 1: effective inclusion = core + interphase layer
1414    let vf_total = (vf_core + vf_interphase).min(1.0);
1415    if vf_total < 1e-15 {
1416        return e_matrix;
1417    }
1418    let vf_core_in_inclusion = vf_core / vf_total;
1419    let e_inclusion = halpin_tsai_modulus(vf_core_in_inclusion, e_core, e_interphase, xi);
1420
1421    // Step 2: Halpin-Tsai with effective inclusion
1422    halpin_tsai_modulus(vf_total, e_inclusion, e_matrix, xi)
1423}
1424
1425/// Surface/interface energy contribution to nano-composite elastic modulus.
1426///
1427/// Gurtin-Murdoch surface elasticity correction (simplified):
1428/// ΔK_surface ≈ 2 * K_s / r
1429///
1430/// where K_s is the surface bulk modulus and r is the particle radius.
1431#[allow(dead_code)]
1432pub fn nano_surface_elasticity_correction(k_surface: f64, particle_radius: f64) -> f64 {
1433    if particle_radius < f64::EPSILON {
1434        return 0.0;
1435    }
1436    2.0 * k_surface / particle_radius
1437}
1438
1439// ─── Tests for new combination additions ─────────────────────────────────────
1440
1441#[cfg(test)]
1442mod tests_new_combination {
1443
1444    use crate::combination::bulk_shear_to_engineering;
1445    use crate::combination::composite_sphere_bulk;
1446    use crate::combination::cox_krenchel_modulus;
1447    use crate::combination::cox_length_efficiency;
1448    use crate::combination::halpin_tsai_modulus;
1449    use crate::combination::halpin_tsai_random_2d;
1450    use crate::combination::homogenization_result;
1451    use crate::combination::kerner_bulk_modulus;
1452    use crate::combination::mori_tanaka_homogenization;
1453    use crate::combination::nano_composite_modulus;
1454    use crate::combination::nano_effective_volume_fraction;
1455    use crate::combination::nano_surface_elasticity_correction;
1456    use crate::combination::nielsen_modulus;
1457    use crate::combination::woven_mosaic_modulus;
1458    use crate::combination::woven_shear_modulus;
1459
1460    // --- Mori-Tanaka homogenization ---
1461
1462    #[test]
1463    fn test_mori_tanaka_pure_matrix() {
1464        // Vf = 0 → K_eff = K_matrix, G_eff = G_matrix
1465        let km = 50.0e9;
1466        let gm = 30.0e9;
1467        let (k_eff, g_eff) = mori_tanaka_homogenization(0.0, 100.0e9, 60.0e9, km, gm);
1468        assert!((k_eff - km).abs() / km < 1e-10);
1469        assert!((g_eff - gm).abs() / gm < 1e-10);
1470    }
1471
1472    #[test]
1473    fn test_mori_tanaka_bounds() {
1474        // K_eff should be between K_matrix and K_fiber
1475        let km = 50.0e9;
1476        let gm = 30.0e9;
1477        let kf = 200.0e9;
1478        let gf = 100.0e9;
1479        let (k_eff, g_eff) = mori_tanaka_homogenization(0.5, kf, gf, km, gm);
1480        assert!(k_eff >= km, "K_eff {k_eff} should be >= K_m {km}");
1481        assert!(k_eff <= kf, "K_eff {k_eff} should be <= K_f {kf}");
1482        assert!(g_eff >= gm, "G_eff {g_eff} should be >= G_m {gm}");
1483        assert!(g_eff <= gf, "G_eff {g_eff} should be <= G_f {gf}");
1484    }
1485
1486    #[test]
1487    fn test_bulk_shear_to_engineering() {
1488        // For steel: K ≈ 167 GPa, G ≈ 80 GPa → E ≈ 200 GPa, ν ≈ 0.25
1489        let (e, nu) = bulk_shear_to_engineering(167.0e9, 80.0e9);
1490        assert!(e > 180.0e9 && e < 220.0e9, "E = {e}");
1491        assert!(nu > 0.2 && nu < 0.35, "ν = {nu}");
1492    }
1493
1494    #[test]
1495    fn test_homogenization_result_positive_moduli() {
1496        let res = homogenization_result(0.4, 200.0e9, 80.0e9, 50.0e9, 20.0e9);
1497        assert!(res.e_x > 0.0, "E_x should be positive");
1498        assert!(res.g_xy > 0.0, "G_xy should be positive");
1499        assert!(res.nu_xy > -1.0 && res.nu_xy < 0.5, "ν_xy = {}", res.nu_xy);
1500    }
1501
1502    #[test]
1503    fn test_homogenization_result_stiffness_tensor() {
1504        let res = homogenization_result(0.3, 200.0e9, 80.0e9, 50.0e9, 20.0e9);
1505        // C_11 = C_22 = C_33
1506        assert!((res.c_eff[0] - res.c_eff[7]).abs() < 1.0);
1507        assert!((res.c_eff[0] - res.c_eff[14]).abs() < 1.0);
1508        // C_44 = C_55 = C_66
1509        assert!((res.c_eff[21] - res.c_eff[28]).abs() < 1.0);
1510    }
1511
1512    // --- Random fiber composite ---
1513
1514    #[test]
1515    fn test_cox_krenchel_pure_matrix() {
1516        let e = cox_krenchel_modulus(0.0, 72.0e9, 3.5e9, 1.0);
1517        assert!((e - 3.5e9).abs() < 1.0);
1518    }
1519
1520    #[test]
1521    fn test_cox_krenchel_increases_with_vf() {
1522        let e0 = cox_krenchel_modulus(0.1, 72.0e9, 3.5e9, 0.8);
1523        let e1 = cox_krenchel_modulus(0.4, 72.0e9, 3.5e9, 0.8);
1524        assert!(
1525            e1 > e0,
1526            "modulus should increase with fiber volume fraction"
1527        );
1528    }
1529
1530    #[test]
1531    fn test_cox_length_efficiency_long_fiber() {
1532        // For long fibers (large β*L/2), η_l → 1
1533        let eta = cox_length_efficiency(10.0, 20.0);
1534        assert!(
1535            eta > 0.9,
1536            "length efficiency should be close to 1 for long fibers: {eta}"
1537        );
1538    }
1539
1540    #[test]
1541    fn test_cox_length_efficiency_zero() {
1542        let eta = cox_length_efficiency(0.0, 0.0);
1543        assert!((eta - 0.0).abs() < 1e-10);
1544    }
1545
1546    #[test]
1547    fn test_halpin_tsai_random_2d() {
1548        let e = halpin_tsai_random_2d(0.5, 72.0e9, 3.5e9, 2.0);
1549        assert!(e > 3.5e9, "should exceed matrix modulus");
1550        assert!(e < 72.0e9, "should be less than fiber modulus");
1551    }
1552
1553    // --- Woven composite ---
1554
1555    #[test]
1556    fn test_woven_mosaic_zero_crimp() {
1557        // No crimp: E = Voigt average of warp + fill + matrix
1558        let e = woven_mosaic_modulus(0.3, 0.3, 70.0e9, 3.5e9, 0.0);
1559        let expected = 0.3 * 70.0e9 + 0.3 * 70.0e9 + 0.4 * 3.5e9;
1560        assert!((e - expected).abs() / expected < 1e-10);
1561    }
1562
1563    #[test]
1564    fn test_woven_mosaic_crimp_reduces_modulus() {
1565        let e_no_crimp = woven_mosaic_modulus(0.3, 0.3, 70.0e9, 3.5e9, 0.0);
1566        let e_crimp = woven_mosaic_modulus(0.3, 0.3, 70.0e9, 3.5e9, 0.2);
1567        assert!(e_crimp < e_no_crimp, "crimp should reduce modulus");
1568    }
1569
1570    #[test]
1571    fn test_woven_shear_equal_phases() {
1572        // G_f = G_m → G_eff = G_m
1573        let g = woven_shear_modulus(0.5, 50.0e9, 50.0e9);
1574        assert!((g - 50.0e9).abs() / 50.0e9 < 1e-10);
1575    }
1576
1577    #[test]
1578    fn test_woven_shear_between_phases() {
1579        let gf = 80.0e9;
1580        let gm = 3.0e9;
1581        let g = woven_shear_modulus(0.4, gf, gm);
1582        // The bridging model is a harmonic-type blend
1583        // Result is positive and finite
1584        assert!(g > 0.0, "G_eff {g} should be positive");
1585        assert!(g.is_finite(), "G_eff {g} should be finite");
1586        // For different vf values, result should change
1587        let g2 = woven_shear_modulus(0.6, gf, gm);
1588        assert!((g - g2).abs() > 0.0, "result should vary with vf");
1589    }
1590
1591    // --- Particulate composite ---
1592
1593    #[test]
1594    fn test_kerner_bulk_pure_matrix() {
1595        let km = 50.0e9;
1596        let gm = 30.0e9;
1597        let k = kerner_bulk_modulus(0.0, 200.0e9, gm, km);
1598        // Vp=0: numerator = 0, so k ≈ k_matrix
1599        assert!((k - km).abs() / km < 1e-6);
1600    }
1601
1602    #[test]
1603    fn test_kerner_bulk_above_matrix() {
1604        let k = kerner_bulk_modulus(0.3, 200.0e9, 30.0e9, 50.0e9);
1605        assert!(k > 50.0e9, "Kerner K should exceed matrix K");
1606    }
1607
1608    #[test]
1609    fn test_nielsen_modulus_pure_matrix() {
1610        let e = nielsen_modulus(0.0, 200.0e9, 3.5e9, 2.5, 0.64);
1611        assert!((e - 3.5e9).abs() < 1.0);
1612    }
1613
1614    #[test]
1615    fn test_nielsen_modulus_increases() {
1616        let e0 = nielsen_modulus(0.1, 200.0e9, 3.5e9, 2.5, 0.64);
1617        let e1 = nielsen_modulus(0.3, 200.0e9, 3.5e9, 2.5, 0.64);
1618        assert!(e1 > e0, "modulus should increase with volume fraction");
1619    }
1620
1621    #[test]
1622    fn test_composite_sphere_bulk() {
1623        // Result should be between K_matrix and K_inclusion
1624        let km = 50.0e9;
1625        let ki = 200.0e9;
1626        let gm = 30.0e9;
1627        let k = composite_sphere_bulk(0.4, ki, gm, km);
1628        assert!(k >= km, "K_eff should be >= K_m");
1629        assert!(k <= ki, "K_eff should be <= K_i");
1630    }
1631
1632    // --- Nano-composite ---
1633
1634    #[test]
1635    fn test_nano_effective_vf_no_interphase() {
1636        // t=0 → V_eff = V_f
1637        let veff = nano_effective_volume_fraction(0.3, 10e-9, 0.0);
1638        assert!((veff - 0.3).abs() < 1e-10);
1639    }
1640
1641    #[test]
1642    fn test_nano_effective_vf_grows_with_interphase() {
1643        let vf = 0.1;
1644        let r = 5e-9;
1645        let t = 2e-9;
1646        let veff = nano_effective_volume_fraction(vf, r, t);
1647        assert!(
1648            veff > vf,
1649            "V_eff should exceed V_f with non-zero interphase"
1650        );
1651    }
1652
1653    #[test]
1654    fn test_nano_composite_modulus_no_interphase() {
1655        // V_interphase = 0 → should reduce to Halpin-Tsai
1656        let e = nano_composite_modulus(0.3, 200.0e9, 0.0, 3.5e9, 3.5e9, 2.0);
1657        let e_ht = halpin_tsai_modulus(0.3, 200.0e9, 3.5e9, 2.0);
1658        // Should be close (interphase = matrix)
1659        assert!((e - e_ht).abs() / e_ht < 0.05, "e={e}, e_ht={e_ht}");
1660    }
1661
1662    #[test]
1663    fn test_nano_surface_correction_large_particle() {
1664        // For large particles, surface correction is negligible
1665        let delta_k = nano_surface_elasticity_correction(1e-9, 1e-3);
1666        assert!(
1667            delta_k < 1e-3,
1668            "correction should be tiny for large particles: {delta_k}"
1669        );
1670    }
1671
1672    #[test]
1673    fn test_nano_surface_correction_small_particle() {
1674        // For nano-particles, surface correction is significant
1675        let delta_k = nano_surface_elasticity_correction(1.0, 1e-9);
1676        assert!(
1677            delta_k > 1e8,
1678            "correction should be large for nano-particles: {delta_k}"
1679        );
1680    }
1681
1682    #[test]
1683    fn test_nano_surface_correction_zero_radius() {
1684        let delta_k = nano_surface_elasticity_correction(1.0, 0.0);
1685        assert_eq!(delta_k, 0.0);
1686    }
1687}