#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrictionCombineRule {
Average,
Min,
Max,
Multiply,
GeometricMean,
HarmonicMean,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestitutionCombineRule {
Average,
Min,
Max,
Multiply,
GeometricMean,
HarmonicMean,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModulusCombineRule {
HertzHarmonic,
Voigt,
Reuss,
GeometricMean,
}
pub fn combine_friction(f1: f64, f2: f64, rule: FrictionCombineRule) -> f64 {
match rule {
FrictionCombineRule::Average => (f1 + f2) * 0.5,
FrictionCombineRule::Min => f1.min(f2),
FrictionCombineRule::Max => f1.max(f2),
FrictionCombineRule::Multiply => f1 * f2,
FrictionCombineRule::GeometricMean => (f1 * f2).sqrt(),
FrictionCombineRule::HarmonicMean => {
let s = f1 + f2;
if s.abs() < f64::EPSILON {
0.0
} else {
2.0 * f1 * f2 / s
}
}
}
}
pub fn combine_restitution(r1: f64, r2: f64, rule: RestitutionCombineRule) -> f64 {
match rule {
RestitutionCombineRule::Average => (r1 + r2) * 0.5,
RestitutionCombineRule::Min => r1.min(r2),
RestitutionCombineRule::Max => r1.max(r2),
RestitutionCombineRule::Multiply => r1 * r2,
RestitutionCombineRule::GeometricMean => (r1 * r2).sqrt(),
RestitutionCombineRule::HarmonicMean => {
let s = r1 + r2;
if s.abs() < f64::EPSILON {
0.0
} else {
2.0 * r1 * r2 / s
}
}
}
}
pub fn combine_modulus(e1: f64, e2: f64, rule: ModulusCombineRule) -> f64 {
match rule {
ModulusCombineRule::HertzHarmonic => {
let s = e1 + e2;
if s.abs() < f64::EPSILON {
0.0
} else {
2.0 * e1 * e2 / s
}
}
ModulusCombineRule::Voigt => (e1 + e2) * 0.5,
ModulusCombineRule::Reuss => {
let s = e1 + e2;
if s.abs() < f64::EPSILON {
0.0
} else {
2.0 * e1 * e2 / s
}
}
ModulusCombineRule::GeometricMean => (e1 * e2).sqrt(),
}
}
pub fn hertz_effective_modulus(e1: f64, nu1: f64, e2: f64, nu2: f64) -> f64 {
let inv = (1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2;
if inv.abs() < f64::EPSILON {
0.0
} else {
1.0 / inv
}
}
pub fn hertz_effective_radius(r1: f64, r2: f64) -> f64 {
let inv = 1.0 / r1 + 1.0 / r2;
if inv.abs() < f64::EPSILON {
0.0
} else {
1.0 / inv
}
}
pub fn hertz_contact_force(e_star: f64, r_star: f64, penetration: f64) -> f64 {
if penetration <= 0.0 {
return 0.0;
}
(4.0 / 3.0) * e_star * r_star.sqrt() * penetration.powf(1.5)
}
#[derive(Debug, Clone)]
pub struct ContactMaterialPair {
pub friction: f64,
pub restitution: f64,
pub effective_modulus: f64,
pub damping: f64,
}
impl ContactMaterialPair {
#[allow(clippy::too_many_arguments)]
pub fn from_materials(
friction1: f64,
restitution1: f64,
young1: f64,
poisson1: f64,
friction2: f64,
restitution2: f64,
young2: f64,
poisson2: f64,
) -> Self {
Self {
friction: combine_friction(friction1, friction2, FrictionCombineRule::GeometricMean),
restitution: combine_restitution(
restitution1,
restitution2,
RestitutionCombineRule::Min,
),
effective_modulus: hertz_effective_modulus(young1, poisson1, young2, poisson2),
damping: 0.0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ThermalContactResistance {
pub k1: f64,
pub k2: f64,
pub gap_conductance: f64,
}
impl ThermalContactResistance {
pub fn new(k1: f64, k2: f64, gap_conductance: f64) -> Self {
Self {
k1,
k2,
gap_conductance,
}
}
pub fn combined_conductance(&self) -> f64 {
let harmonic_k = if (self.k1 + self.k2).abs() < f64::EPSILON {
0.0
} else {
2.0 * self.k1 * self.k2 / (self.k1 + self.k2)
};
harmonic_k + self.gap_conductance
}
pub fn heat_flux(&self, delta_t: f64) -> f64 {
self.combined_conductance() * delta_t
}
}
pub fn maxwell_diffusivity(d1: f64, d2: f64, volume_fraction2: f64) -> f64 {
let phi = volume_fraction2.clamp(0.0, 1.0);
let num = d2 + 2.0 * d1 - 2.0 * phi * (d1 - d2);
let den = d2 + 2.0 * d1 + phi * (d1 - d2);
if den.abs() < f64::EPSILON {
d1
} else {
d1 * num / den
}
}
#[allow(dead_code)]
pub fn voigt_average(volume_fractions: &[f64], properties: &[f64]) -> f64 {
volume_fractions
.iter()
.zip(properties.iter())
.map(|(&f, &p)| f * p)
.sum()
}
#[allow(dead_code)]
pub fn reuss_average(volume_fractions: &[f64], properties: &[f64]) -> f64 {
let inv_sum: f64 = volume_fractions
.iter()
.zip(properties.iter())
.map(|(&f, &p)| if p.abs() < f64::EPSILON { 0.0 } else { f / p })
.sum();
if inv_sum.abs() < f64::EPSILON {
0.0
} else {
1.0 / inv_sum
}
}
#[allow(dead_code)]
pub fn hill_average(volume_fractions: &[f64], properties: &[f64]) -> f64 {
let v = voigt_average(volume_fractions, properties);
let r = reuss_average(volume_fractions, properties);
0.5 * (v + r)
}
#[allow(dead_code)]
pub fn rule_of_mixtures_longitudinal(vf: f64, e_fiber: f64, e_matrix: f64) -> f64 {
let vf = vf.clamp(0.0, 1.0);
vf * e_fiber + (1.0 - vf) * e_matrix
}
#[allow(dead_code)]
pub fn rule_of_mixtures_transverse(vf: f64, e_fiber: f64, e_matrix: f64) -> f64 {
let vf = vf.clamp(0.0, 1.0);
let inv = vf / e_fiber + (1.0 - vf) / e_matrix;
if inv.abs() < f64::EPSILON {
0.0
} else {
1.0 / inv
}
}
#[allow(dead_code)]
pub fn rule_of_mixtures_poisson(vf: f64, nu_fiber: f64, nu_matrix: f64) -> f64 {
let vf = vf.clamp(0.0, 1.0);
vf * nu_fiber + (1.0 - vf) * nu_matrix
}
#[allow(dead_code)]
pub fn rule_of_mixtures_shear(vf: f64, g_fiber: f64, g_matrix: f64) -> f64 {
let vf = vf.clamp(0.0, 1.0);
let inv = vf / g_fiber + (1.0 - vf) / g_matrix;
if inv.abs() < f64::EPSILON {
0.0
} else {
1.0 / inv
}
}
#[allow(dead_code)]
pub fn rule_of_mixtures_density(vf: f64, rho_fiber: f64, rho_matrix: f64) -> f64 {
let vf = vf.clamp(0.0, 1.0);
vf * rho_fiber + (1.0 - vf) * rho_matrix
}
#[allow(dead_code)]
pub fn halpin_tsai_modulus(vf: f64, e_fiber: f64, e_matrix: f64, xi: f64) -> f64 {
let vf = vf.clamp(0.0, 1.0);
if e_matrix.abs() < f64::EPSILON {
return 0.0;
}
let ratio = e_fiber / e_matrix;
let eta = (ratio - 1.0) / (ratio + xi);
e_matrix * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
}
#[allow(dead_code)]
pub fn halpin_tsai_shear(vf: f64, g_fiber: f64, g_matrix: f64, xi: f64) -> f64 {
let vf = vf.clamp(0.0, 1.0);
if g_matrix.abs() < f64::EPSILON {
return 0.0;
}
let ratio = g_fiber / g_matrix;
let eta = (ratio - 1.0) / (ratio + xi);
g_matrix * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
}
#[allow(dead_code)]
pub fn effective_cte_longitudinal(
vf: f64,
e_fiber: f64,
alpha_fiber: f64,
e_matrix: f64,
alpha_matrix: f64,
) -> f64 {
let vf = vf.clamp(0.0, 1.0);
let vm = 1.0 - vf;
let num = vf * e_fiber * alpha_fiber + vm * e_matrix * alpha_matrix;
let den = vf * e_fiber + vm * e_matrix;
if den.abs() < f64::EPSILON {
0.0
} else {
num / den
}
}
#[allow(dead_code)]
#[allow(clippy::too_many_arguments)]
pub fn effective_cte_transverse(
vf: f64,
alpha_fiber: f64,
nu_fiber: f64,
alpha_matrix: f64,
nu_matrix: f64,
alpha_1: f64,
nu_12: f64,
) -> f64 {
let vf = vf.clamp(0.0, 1.0);
let vm = 1.0 - vf;
(1.0 + nu_fiber) * vf * alpha_fiber + (1.0 + nu_matrix) * vm * alpha_matrix - alpha_1 * nu_12
}
#[allow(dead_code)]
pub fn turner_cte(v1: f64, k1: f64, alpha1: f64, k2: f64, alpha2: f64) -> f64 {
let v1 = v1.clamp(0.0, 1.0);
let v2 = 1.0 - v1;
let num = v1 * k1 * alpha1 + v2 * k2 * alpha2;
let den = v1 * k1 + v2 * k2;
if den.abs() < f64::EPSILON {
0.0
} else {
num / den
}
}
#[allow(dead_code)]
pub fn lamina_stiffness_matrix(e1: f64, e2: f64, nu12: f64, g12: f64) -> [f64; 9] {
let nu21 = nu12 * e2 / e1;
let denom = 1.0 - nu12 * nu21;
let q11 = e1 / denom;
let q22 = e2 / denom;
let q12 = nu12 * e2 / denom;
let q66 = g12;
[q11, q12, 0.0, q12, q22, 0.0, 0.0, 0.0, q66]
}
#[allow(dead_code)]
pub fn transform_stiffness(q: &[f64; 9], theta: f64) -> [f64; 9] {
let m = theta.cos();
let n = theta.sin();
let m2 = m * m;
let n2 = n * n;
let m4 = m2 * m2;
let n4 = n2 * n2;
let mn = m * n;
let m2n2 = m2 * n2;
let q11 = q[0];
let q12 = q[1];
let q22 = q[4];
let q66 = q[8];
let qb11 = q11 * m4 + 2.0 * (q12 + 2.0 * q66) * m2n2 + q22 * n4;
let qb22 = q11 * n4 + 2.0 * (q12 + 2.0 * q66) * m2n2 + q22 * m4;
let qb12 = (q11 + q22 - 4.0 * q66) * m2n2 + q12 * (m4 + n4);
let qb66 = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * m2n2 + q66 * (m4 + n4);
let qb16 = (q11 - q12 - 2.0 * q66) * m2 * mn + (q12 - q22 + 2.0 * q66) * n2 * mn;
let qb26 = (q11 - q12 - 2.0 * q66) * n2 * mn + (q12 - q22 + 2.0 * q66) * m2 * mn;
[qb11, qb12, qb16, qb12, qb22, qb26, qb16, qb26, qb66]
}
#[derive(Debug, Clone)]
pub struct Lamina {
pub q: [f64; 9],
pub theta: f64,
pub thickness: f64,
}
#[allow(dead_code)]
pub fn laminate_abd(plies: &[Lamina]) -> ([f64; 9], [f64; 9], [f64; 9]) {
let total_thickness: f64 = plies.iter().map(|p| p.thickness).sum();
let mut z_bot = -total_thickness / 2.0;
let mut a_mat = [0.0f64; 9];
let mut b_mat = [0.0f64; 9];
let mut d_mat = [0.0f64; 9];
for ply in plies {
let z_top = z_bot + ply.thickness;
let qbar = transform_stiffness(&ply.q, ply.theta);
for i in 0..9 {
a_mat[i] += qbar[i] * (z_top - z_bot);
b_mat[i] += 0.5 * qbar[i] * (z_top * z_top - z_bot * z_bot);
d_mat[i] += (1.0 / 3.0) * qbar[i] * (z_top.powi(3) - z_bot.powi(3));
}
z_bot = z_top;
}
(a_mat, b_mat, d_mat)
}
#[allow(dead_code)]
pub fn laminate_engineering_constants(
a_mat: &[f64; 9],
total_thickness: f64,
) -> (f64, f64, f64, f64) {
let a11 = a_mat[0] / total_thickness;
let a12 = a_mat[1] / total_thickness;
let a22 = a_mat[4] / total_thickness;
let a66 = a_mat[8] / total_thickness;
let det = a11 * a22 - a12 * a12;
if det.abs() < f64::EPSILON {
return (0.0, 0.0, 0.0, 0.0);
}
let ex = det / a22;
let ey = det / a11;
let nu_xy = a12 / a22;
let gxy = a66;
(ex, ey, gxy, nu_xy)
}
#[allow(dead_code)]
pub fn hashin_shtrikman_bulk_upper(v1: f64, k1: f64, k2: f64, g2: f64) -> f64 {
let v1 = v1.clamp(0.0, 1.0);
let v2 = 1.0 - v1;
let dk = k1 - k2;
if dk.abs() < f64::EPSILON {
return k1;
}
let inv = 1.0 / dk + 3.0 * v2 / (3.0 * k2 + 4.0 * g2);
if inv.abs() < f64::EPSILON {
k2
} else {
k2 + v1 / inv
}
}
#[allow(dead_code)]
pub fn hashin_shtrikman_bulk_lower(v1: f64, k1: f64, g1: f64, k2: f64) -> f64 {
let v1 = v1.clamp(0.0, 1.0);
let v2 = 1.0 - v1;
let dk = k2 - k1;
if dk.abs() < f64::EPSILON {
return k1;
}
let inv = 1.0 / dk + 3.0 * v1 / (3.0 * k1 + 4.0 * g1);
if inv.abs() < f64::EPSILON {
k1
} else {
k1 + v2 / inv
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::combination::halpin_tsai_modulus;
#[test]
fn test_combine_friction_average() {
let r = combine_friction(0.4, 0.6, FrictionCombineRule::Average);
assert!((r - 0.5).abs() < 1e-10);
}
#[test]
fn test_combine_friction_geometric() {
let r = combine_friction(0.25, 1.0, FrictionCombineRule::GeometricMean);
assert!((r - 0.5).abs() < 1e-10);
}
#[test]
fn test_combine_friction_harmonic() {
let r = combine_friction(1.0, 1.0, FrictionCombineRule::HarmonicMean);
assert!((r - 1.0).abs() < 1e-10);
}
#[test]
fn test_combine_friction_min() {
assert!((combine_friction(0.4, 0.6, FrictionCombineRule::Min) - 0.4).abs() < 1e-10);
}
#[test]
fn test_combine_friction_max() {
assert!((combine_friction(0.4, 0.6, FrictionCombineRule::Max) - 0.6).abs() < 1e-10);
}
#[test]
fn test_combine_friction_multiply() {
assert!((combine_friction(0.4, 0.6, FrictionCombineRule::Multiply) - 0.24).abs() < 1e-10);
}
#[test]
fn test_combine_restitution_rules() {
assert!(
(combine_restitution(0.3, 0.7, RestitutionCombineRule::Average) - 0.5).abs() < 1e-10
);
assert!((combine_restitution(0.3, 0.7, RestitutionCombineRule::Min) - 0.3).abs() < 1e-10);
assert!((combine_restitution(0.3, 0.7, RestitutionCombineRule::Max) - 0.7).abs() < 1e-10);
assert!(
(combine_restitution(0.3, 0.7, RestitutionCombineRule::Multiply) - 0.21).abs() < 1e-10
);
}
#[test]
fn test_hertz_effective_modulus_equal_materials() {
let e = 200e9_f64;
let nu = 0.3_f64;
let e_star = hertz_effective_modulus(e, nu, e, nu);
let expected = e / (2.0 * (1.0 - nu * nu));
assert!((e_star - expected).abs() / expected < 1e-10);
}
#[test]
fn test_hertz_contact_force_zero_penetration() {
assert_eq!(hertz_contact_force(200e9, 0.01, 0.0), 0.0);
assert_eq!(hertz_contact_force(200e9, 0.01, -0.001), 0.0);
}
#[test]
fn test_hertz_effective_radius() {
let r = hertz_effective_radius(1.0, 1.0);
assert!((r - 0.5).abs() < 1e-10);
}
#[test]
fn test_thermal_contact_heat_flux() {
let tcr = ThermalContactResistance::new(50.0, 50.0, 0.0);
let cond = tcr.combined_conductance();
assert!((cond - 50.0).abs() < 1e-6);
let flux = tcr.heat_flux(10.0);
assert!((flux - 500.0).abs() < 1e-6);
}
#[test]
fn test_maxwell_diffusivity_pure_phase() {
let d = maxwell_diffusivity(1.0, 5.0, 0.0);
assert!((d - 1.0).abs() < 1e-10);
let d2 = maxwell_diffusivity(1.0, 5.0, 1.0);
assert!((d2 - 5.0).abs() < 1e-6);
}
#[test]
fn test_contact_material_pair() {
let pair = ContactMaterialPair::from_materials(0.5, 0.4, 200e9, 0.3, 0.7, 0.8, 70e9, 0.33);
assert!(pair.friction > 0.0);
assert!(pair.restitution <= 0.4);
assert!(pair.effective_modulus > 0.0);
}
#[test]
fn test_voigt_average_equal_phases() {
let vf = [0.5, 0.5];
let props = [200.0e9, 200.0e9];
let result = voigt_average(&vf, &props);
assert!((result - 200.0e9).abs() < 1.0);
}
#[test]
fn test_voigt_average_weighted() {
let vf = [0.6, 0.4];
let props = [100.0e9, 200.0e9];
let result = voigt_average(&vf, &props);
assert!((result - 140.0e9).abs() < 1.0);
}
#[test]
fn test_reuss_average_equal_phases() {
let vf = [0.5, 0.5];
let props = [200.0e9, 200.0e9];
let result = reuss_average(&vf, &props);
assert!((result - 200.0e9).abs() < 1.0);
}
#[test]
fn test_reuss_leq_voigt() {
let vf = [0.3, 0.7];
let props = [70.0e9, 200.0e9];
let v = voigt_average(&vf, &props);
let r = reuss_average(&vf, &props);
assert!(r <= v + 1e-6, "Reuss {r} should be <= Voigt {v}");
}
#[test]
fn test_hill_between_voigt_reuss() {
let vf = [0.4, 0.6];
let props = [70.0e9, 200.0e9];
let v = voigt_average(&vf, &props);
let r = reuss_average(&vf, &props);
let h = hill_average(&vf, &props);
assert!(
h >= r - 1e-6 && h <= v + 1e-6,
"Hill {h} not between Reuss {r} and Voigt {v}"
);
}
#[test]
fn test_hill_average_value() {
let vf = [0.5, 0.5];
let props = [100.0e9, 200.0e9];
let h = hill_average(&vf, &props);
assert!((h - 141.666666e9).abs() / h < 1e-4);
}
#[test]
fn test_rom_longitudinal() {
let e1 = rule_of_mixtures_longitudinal(0.6, 72.0e9, 3.5e9);
let expected = 0.6 * 72.0e9 + 0.4 * 3.5e9;
assert!((e1 - expected).abs() < 1.0);
}
#[test]
fn test_rom_transverse() {
let e2 = rule_of_mixtures_transverse(0.6, 72.0e9, 3.5e9);
assert!(
e2 > 3.5e9,
"Transverse modulus should exceed matrix modulus"
);
assert!(
e2 < 72.0e9,
"Transverse modulus should be less than fiber modulus"
);
}
#[test]
fn test_rom_transverse_leq_longitudinal() {
let e1 = rule_of_mixtures_longitudinal(0.6, 72.0e9, 3.5e9);
let e2 = rule_of_mixtures_transverse(0.6, 72.0e9, 3.5e9);
assert!(e2 < e1, "E2 ({e2}) should be less than E1 ({e1})");
}
#[test]
fn test_rom_poisson() {
let nu = rule_of_mixtures_poisson(0.5, 0.22, 0.35);
let expected = 0.5 * 0.22 + 0.5 * 0.35;
assert!((nu - expected).abs() < 1e-10);
}
#[test]
fn test_rom_shear() {
let g = rule_of_mixtures_shear(0.5, 30.0e9, 1.3e9);
assert!(g > 1.3e9);
assert!(g < 30.0e9);
}
#[test]
fn test_rom_density() {
let rho = rule_of_mixtures_density(0.55, 1800.0, 1200.0);
let expected = 0.55 * 1800.0 + 0.45 * 1200.0;
assert!((rho - expected).abs() < 1e-6);
}
#[test]
fn test_halpin_tsai_pure_matrix() {
let e2 = halpin_tsai_modulus(0.0, 72.0e9, 3.5e9, 2.0);
assert!((e2 - 3.5e9).abs() < 1.0);
}
#[test]
fn test_halpin_tsai_between_bounds() {
let vf = 0.6;
let ef = 72.0e9;
let em = 3.5e9;
let e_ht = halpin_tsai_modulus(vf, ef, em, 2.0);
let e_voigt = rule_of_mixtures_longitudinal(vf, ef, em);
let e_reuss = rule_of_mixtures_transverse(vf, ef, em);
assert!(
e_ht >= e_reuss - 1e-6 && e_ht <= e_voigt + 1e-6,
"HT {e_ht} not between Reuss {e_reuss} and Voigt {e_voigt}"
);
}
#[test]
fn test_halpin_tsai_xi_effect() {
let vf = 0.5;
let ef = 72.0e9;
let em = 3.5e9;
let e_low_xi = halpin_tsai_modulus(vf, ef, em, 1.0);
let e_high_xi = halpin_tsai_modulus(vf, ef, em, 10.0);
assert!(e_high_xi > e_low_xi, "Higher ξ should give higher modulus");
}
#[test]
fn test_halpin_tsai_shear() {
let g = halpin_tsai_shear(0.5, 30.0e9, 1.3e9, 1.0);
assert!(g > 1.3e9);
assert!(g < 30.0e9);
}
#[test]
fn test_cte_longitudinal_equal_materials() {
let alpha = effective_cte_longitudinal(0.5, 200.0e9, 12.0e-6, 200.0e9, 12.0e-6);
assert!((alpha - 12.0e-6).abs() < 1e-15);
}
#[test]
fn test_cte_longitudinal_stiff_fiber_dominates() {
let vf = 0.6;
let alpha = effective_cte_longitudinal(vf, 230.0e9, 5.0e-6, 3.5e9, 60.0e-6);
assert!(
alpha < 20.0e-6,
"Expected CTE near fiber value, got {alpha}"
);
}
#[test]
fn test_turner_cte_equal_phases() {
let alpha = turner_cte(0.5, 100.0e9, 10.0e-6, 100.0e9, 10.0e-6);
assert!((alpha - 10.0e-6).abs() < 1e-15);
}
#[test]
fn test_turner_cte_bulk_weighted() {
let alpha = turner_cte(0.5, 200.0e9, 5.0e-6, 50.0e9, 20.0e-6);
assert!(
alpha < 12.5e-6,
"Turner CTE should be weighted toward stiffer phase"
);
}
#[test]
fn test_cte_transverse() {
let vf = 0.5;
let alpha_1 = effective_cte_longitudinal(vf, 230.0e9, 5.0e-6, 3.5e9, 60.0e-6);
let nu_12 = rule_of_mixtures_poisson(vf, 0.2, 0.35);
let alpha_2 = effective_cte_transverse(vf, 5.0e-6, 0.2, 60.0e-6, 0.35, alpha_1, nu_12);
assert!(
alpha_2 > alpha_1,
"Transverse CTE should exceed longitudinal"
);
}
#[test]
fn test_lamina_stiffness_matrix() {
let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
let nu21 = 0.3 * 10.0e9 / 140.0e9;
let denom = 1.0 - 0.3 * nu21;
let expected_q11 = 140.0e9 / denom;
assert!((q[0] - expected_q11).abs() / expected_q11 < 1e-10);
assert_eq!(q[2], 0.0);
assert_eq!(q[6], 0.0);
}
#[test]
fn test_transform_stiffness_zero_angle() {
let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
let qbar = transform_stiffness(&q, 0.0);
for i in 0..9 {
assert!(
(qbar[i] - q[i]).abs() < 1.0,
"Q-bar[{i}] = {} should match Q[{i}] = {} at θ=0",
qbar[i],
q[i]
);
}
}
#[test]
fn test_transform_stiffness_90_degrees() {
let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
let qbar = transform_stiffness(&q, std::f64::consts::FRAC_PI_2);
assert!(
(qbar[0] - q[4]).abs() / q[4] < 1e-8,
"Q̄11 should ≈ Q22 at 90°"
);
assert!(
(qbar[4] - q[0]).abs() / q[0] < 1e-8,
"Q̄22 should ≈ Q11 at 90°"
);
}
#[test]
#[allow(clippy::needless_range_loop)]
fn test_laminate_abd_symmetric() {
let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
let plies = vec![
Lamina {
q,
theta: 0.0,
thickness: 0.125e-3,
},
Lamina {
q,
theta: std::f64::consts::FRAC_PI_2,
thickness: 0.125e-3,
},
Lamina {
q,
theta: std::f64::consts::FRAC_PI_2,
thickness: 0.125e-3,
},
Lamina {
q,
theta: 0.0,
thickness: 0.125e-3,
},
];
let (_a, b, _d) = laminate_abd(&plies);
for i in 0..9 {
assert!(
b[i].abs() < 1e-3,
"B[{i}]={} should be ~0 for symmetric laminate",
b[i]
);
}
}
#[test]
fn test_laminate_abd_a_positive_diagonal() {
let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
let plies = vec![
Lamina {
q,
theta: 0.0,
thickness: 0.25e-3,
},
Lamina {
q,
theta: std::f64::consts::FRAC_PI_2,
thickness: 0.25e-3,
},
];
let (a, _b, _d) = laminate_abd(&plies);
assert!(a[0] > 0.0, "A11 should be positive");
assert!(a[4] > 0.0, "A22 should be positive");
assert!(a[8] > 0.0, "A66 should be positive");
}
#[test]
fn test_laminate_engineering_constants() {
let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
let plies = vec![
Lamina {
q,
theta: 0.0,
thickness: 0.125e-3,
},
Lamina {
q,
theta: std::f64::consts::FRAC_PI_2,
thickness: 0.125e-3,
},
Lamina {
q,
theta: std::f64::consts::FRAC_PI_2,
thickness: 0.125e-3,
},
Lamina {
q,
theta: 0.0,
thickness: 0.125e-3,
},
];
let total_h: f64 = plies.iter().map(|p| p.thickness).sum();
let (a, _b, _d) = laminate_abd(&plies);
let (ex, ey, gxy, _nu_xy) = laminate_engineering_constants(&a, total_h);
assert!(ex > 0.0, "Ex should be positive");
assert!(ey > 0.0, "Ey should be positive");
assert!(gxy > 0.0, "Gxy should be positive");
assert!(
(ex - ey).abs() / ex < 1e-6,
"Ex ({ex}) should ≈ Ey ({ey}) for balanced layup"
);
}
#[test]
fn test_hashin_shtrikman_bounds_ordering() {
let v1 = 0.4;
let k1 = 75.0e9; let g1 = 26.0e9;
let k2 = 160.0e9; let g2 = 80.0e9;
let hs_lower = hashin_shtrikman_bulk_lower(v1, k1, g1, k2);
let hs_upper = hashin_shtrikman_bulk_upper(v1, k1, k2, g2);
assert!(
hs_lower <= hs_upper + 1e-6,
"HS lower {hs_lower} should be <= HS upper {hs_upper}"
);
}
#[test]
fn test_hashin_shtrikman_pure_phases() {
let hs = hashin_shtrikman_bulk_lower(1.0, 75.0e9, 26.0e9, 160.0e9);
assert!((hs - 75.0e9).abs() / 75.0e9 < 1e-6);
}
}
#[derive(Debug, Clone)]
pub struct HomogenizationResult {
pub c_eff: [f64; 36],
pub e_x: f64,
pub nu_xy: f64,
pub g_xy: f64,
}
#[allow(dead_code)]
pub fn mori_tanaka_homogenization(
vf: f64,
k_fiber: f64,
g_fiber: f64,
k_matrix: f64,
g_matrix: f64,
) -> (f64, f64) {
let vf = vf.clamp(0.0, 1.0);
let vm = 1.0 - vf;
let dk = k_fiber - k_matrix;
let k_denom = k_matrix + 4.0 / 3.0 * g_matrix;
let k_eff = if k_denom.abs() < f64::EPSILON {
k_matrix
} else {
k_matrix + vf * dk / (1.0 + vm * dk / k_denom)
};
let dg = g_fiber - g_matrix;
let beta =
6.0 * (k_matrix + 2.0 * g_matrix) / (5.0 * g_matrix * (3.0 * k_matrix + 4.0 * g_matrix));
let g_denom = if (g_matrix * (3.0 * k_matrix + 4.0 * g_matrix)).abs() < f64::EPSILON {
1.0
} else {
1.0 + vm * dg * beta
};
let g_eff = g_matrix + vf * dg / g_denom;
(k_eff, g_eff)
}
#[allow(dead_code)]
pub fn bulk_shear_to_engineering(k: f64, g: f64) -> (f64, f64) {
let e = 9.0 * k * g / (3.0 * k + g);
let nu = (3.0 * k - 2.0 * g) / (2.0 * (3.0 * k + g));
(e, nu)
}
#[allow(dead_code)]
pub fn homogenization_result(
vf: f64,
k_fiber: f64,
g_fiber: f64,
k_matrix: f64,
g_matrix: f64,
) -> HomogenizationResult {
let (k_eff, g_eff) = mori_tanaka_homogenization(vf, k_fiber, g_fiber, k_matrix, g_matrix);
let (e_x, nu_xy) = bulk_shear_to_engineering(k_eff, g_eff);
let lam = k_eff - 2.0 / 3.0 * g_eff; let mut c_eff = [0.0f64; 36];
c_eff[0] = lam + 2.0 * g_eff;
c_eff[7] = lam + 2.0 * g_eff;
c_eff[14] = lam + 2.0 * g_eff;
c_eff[1] = lam;
c_eff[6] = lam;
c_eff[2] = lam;
c_eff[12] = lam;
c_eff[9] = lam;
c_eff[13] = lam;
c_eff[21] = g_eff;
c_eff[28] = g_eff;
c_eff[35] = g_eff;
HomogenizationResult {
c_eff,
e_x,
nu_xy,
g_xy: g_eff,
}
}
#[allow(dead_code)]
pub fn cox_krenchel_modulus(vf: f64, e_fiber: f64, e_matrix: f64, eta_l: f64) -> f64 {
let vf = vf.clamp(0.0, 1.0);
let vm = 1.0 - vf;
let eta_o = 1.0 / 5.0; eta_l * eta_o * vf * e_fiber + vm * e_matrix
}
#[allow(dead_code)]
pub fn cox_length_efficiency(_fiber_length: f64, beta_l_over_2: f64) -> f64 {
let x = beta_l_over_2;
if x < 1e-10 {
return 0.0;
}
1.0 - x.tanh() / x
}
#[allow(dead_code)]
pub fn halpin_tsai_random_2d(vf: f64, e_fiber: f64, e_matrix: f64, xi: f64) -> f64 {
let e11 = rule_of_mixtures_longitudinal(vf, e_fiber, e_matrix);
let e22 = halpin_tsai_modulus(vf, e_fiber, e_matrix, xi);
3.0 / 8.0 * e11 + 5.0 / 8.0 * e22
}
#[allow(dead_code)]
pub fn woven_mosaic_modulus(
vf_warp: f64,
vf_fill: f64,
e_tow: f64,
e_matrix: f64,
crimp_angle: f64,
) -> f64 {
let vf_warp = vf_warp.clamp(0.0, 1.0);
let vf_fill = vf_fill.clamp(0.0, 1.0);
let vm = (1.0 - vf_warp - vf_fill).max(0.0);
let e_tow_crimp = e_tow * crimp_angle.cos().powi(4);
vf_warp * e_tow + vf_fill * e_tow_crimp + vm * e_matrix
}
#[allow(dead_code)]
pub fn woven_shear_modulus(vf: f64, g_fiber: f64, g_matrix: f64) -> f64 {
let vf = vf.clamp(0.0, 1.0);
let vm = 1.0 - vf;
let num = g_matrix * (vf * g_fiber + vm * g_matrix);
let den = vm * g_fiber + vf * g_matrix;
if den.abs() < f64::EPSILON {
g_matrix
} else {
num / den
}
}
#[allow(dead_code)]
pub fn kerner_bulk_modulus(vp: f64, k_particle: f64, g_matrix: f64, k_matrix: f64) -> f64 {
let vp = vp.clamp(0.0, 1.0);
let dk = k_particle - k_matrix;
let denom = k_matrix + 4.0 / 3.0 * g_matrix * (1.0 - vp);
if denom.abs() < f64::EPSILON {
k_matrix
} else {
k_matrix + vp * dk * k_matrix / denom
}
}
#[allow(dead_code)]
pub fn nielsen_modulus(vp: f64, e_particle: f64, e_matrix: f64, a_e: f64, phi_max: f64) -> f64 {
let vp = vp.clamp(0.0, phi_max);
if e_matrix.abs() < f64::EPSILON {
return 0.0;
}
let ratio = e_particle / e_matrix;
let b_e = (ratio - 1.0) / (ratio + a_e);
let psi = 1.0 + (1.0 - phi_max) * vp / (phi_max * phi_max);
let denom = 1.0 - b_e * psi * vp;
if denom.abs() < f64::EPSILON {
e_matrix
} else {
e_matrix * (1.0 + a_e * b_e * vp) / denom
}
}
#[allow(dead_code)]
pub fn composite_sphere_bulk(vp: f64, k_inclusion: f64, g_matrix: f64, k_matrix: f64) -> f64 {
let vp = vp.clamp(0.0, 1.0);
let vm = 1.0 - vp;
let dk = k_inclusion - k_matrix;
let inv = 1.0 / dk + 3.0 * vm / (3.0 * k_matrix + 4.0 * g_matrix);
if inv.abs() < f64::EPSILON || dk.abs() < f64::EPSILON {
k_matrix
} else {
k_matrix + vp / inv
}
}
#[allow(dead_code)]
pub fn nano_effective_volume_fraction(
vf: f64,
particle_radius: f64,
interphase_thickness: f64,
) -> f64 {
let vf = vf.clamp(0.0, 1.0);
let factor = (1.0 + interphase_thickness / particle_radius).powi(3);
(vf * factor).min(1.0)
}
#[allow(dead_code)]
pub fn nano_composite_modulus(
vf_core: f64,
e_core: f64,
vf_interphase: f64,
e_interphase: f64,
e_matrix: f64,
xi: f64,
) -> f64 {
let vf_total = (vf_core + vf_interphase).min(1.0);
if vf_total < 1e-15 {
return e_matrix;
}
let vf_core_in_inclusion = vf_core / vf_total;
let e_inclusion = halpin_tsai_modulus(vf_core_in_inclusion, e_core, e_interphase, xi);
halpin_tsai_modulus(vf_total, e_inclusion, e_matrix, xi)
}
#[allow(dead_code)]
pub fn nano_surface_elasticity_correction(k_surface: f64, particle_radius: f64) -> f64 {
if particle_radius < f64::EPSILON {
return 0.0;
}
2.0 * k_surface / particle_radius
}
#[cfg(test)]
mod tests_new_combination {
use crate::combination::bulk_shear_to_engineering;
use crate::combination::composite_sphere_bulk;
use crate::combination::cox_krenchel_modulus;
use crate::combination::cox_length_efficiency;
use crate::combination::halpin_tsai_modulus;
use crate::combination::halpin_tsai_random_2d;
use crate::combination::homogenization_result;
use crate::combination::kerner_bulk_modulus;
use crate::combination::mori_tanaka_homogenization;
use crate::combination::nano_composite_modulus;
use crate::combination::nano_effective_volume_fraction;
use crate::combination::nano_surface_elasticity_correction;
use crate::combination::nielsen_modulus;
use crate::combination::woven_mosaic_modulus;
use crate::combination::woven_shear_modulus;
#[test]
fn test_mori_tanaka_pure_matrix() {
let km = 50.0e9;
let gm = 30.0e9;
let (k_eff, g_eff) = mori_tanaka_homogenization(0.0, 100.0e9, 60.0e9, km, gm);
assert!((k_eff - km).abs() / km < 1e-10);
assert!((g_eff - gm).abs() / gm < 1e-10);
}
#[test]
fn test_mori_tanaka_bounds() {
let km = 50.0e9;
let gm = 30.0e9;
let kf = 200.0e9;
let gf = 100.0e9;
let (k_eff, g_eff) = mori_tanaka_homogenization(0.5, kf, gf, km, gm);
assert!(k_eff >= km, "K_eff {k_eff} should be >= K_m {km}");
assert!(k_eff <= kf, "K_eff {k_eff} should be <= K_f {kf}");
assert!(g_eff >= gm, "G_eff {g_eff} should be >= G_m {gm}");
assert!(g_eff <= gf, "G_eff {g_eff} should be <= G_f {gf}");
}
#[test]
fn test_bulk_shear_to_engineering() {
let (e, nu) = bulk_shear_to_engineering(167.0e9, 80.0e9);
assert!(e > 180.0e9 && e < 220.0e9, "E = {e}");
assert!(nu > 0.2 && nu < 0.35, "ν = {nu}");
}
#[test]
fn test_homogenization_result_positive_moduli() {
let res = homogenization_result(0.4, 200.0e9, 80.0e9, 50.0e9, 20.0e9);
assert!(res.e_x > 0.0, "E_x should be positive");
assert!(res.g_xy > 0.0, "G_xy should be positive");
assert!(res.nu_xy > -1.0 && res.nu_xy < 0.5, "ν_xy = {}", res.nu_xy);
}
#[test]
fn test_homogenization_result_stiffness_tensor() {
let res = homogenization_result(0.3, 200.0e9, 80.0e9, 50.0e9, 20.0e9);
assert!((res.c_eff[0] - res.c_eff[7]).abs() < 1.0);
assert!((res.c_eff[0] - res.c_eff[14]).abs() < 1.0);
assert!((res.c_eff[21] - res.c_eff[28]).abs() < 1.0);
}
#[test]
fn test_cox_krenchel_pure_matrix() {
let e = cox_krenchel_modulus(0.0, 72.0e9, 3.5e9, 1.0);
assert!((e - 3.5e9).abs() < 1.0);
}
#[test]
fn test_cox_krenchel_increases_with_vf() {
let e0 = cox_krenchel_modulus(0.1, 72.0e9, 3.5e9, 0.8);
let e1 = cox_krenchel_modulus(0.4, 72.0e9, 3.5e9, 0.8);
assert!(
e1 > e0,
"modulus should increase with fiber volume fraction"
);
}
#[test]
fn test_cox_length_efficiency_long_fiber() {
let eta = cox_length_efficiency(10.0, 20.0);
assert!(
eta > 0.9,
"length efficiency should be close to 1 for long fibers: {eta}"
);
}
#[test]
fn test_cox_length_efficiency_zero() {
let eta = cox_length_efficiency(0.0, 0.0);
assert!((eta - 0.0).abs() < 1e-10);
}
#[test]
fn test_halpin_tsai_random_2d() {
let e = halpin_tsai_random_2d(0.5, 72.0e9, 3.5e9, 2.0);
assert!(e > 3.5e9, "should exceed matrix modulus");
assert!(e < 72.0e9, "should be less than fiber modulus");
}
#[test]
fn test_woven_mosaic_zero_crimp() {
let e = woven_mosaic_modulus(0.3, 0.3, 70.0e9, 3.5e9, 0.0);
let expected = 0.3 * 70.0e9 + 0.3 * 70.0e9 + 0.4 * 3.5e9;
assert!((e - expected).abs() / expected < 1e-10);
}
#[test]
fn test_woven_mosaic_crimp_reduces_modulus() {
let e_no_crimp = woven_mosaic_modulus(0.3, 0.3, 70.0e9, 3.5e9, 0.0);
let e_crimp = woven_mosaic_modulus(0.3, 0.3, 70.0e9, 3.5e9, 0.2);
assert!(e_crimp < e_no_crimp, "crimp should reduce modulus");
}
#[test]
fn test_woven_shear_equal_phases() {
let g = woven_shear_modulus(0.5, 50.0e9, 50.0e9);
assert!((g - 50.0e9).abs() / 50.0e9 < 1e-10);
}
#[test]
fn test_woven_shear_between_phases() {
let gf = 80.0e9;
let gm = 3.0e9;
let g = woven_shear_modulus(0.4, gf, gm);
assert!(g > 0.0, "G_eff {g} should be positive");
assert!(g.is_finite(), "G_eff {g} should be finite");
let g2 = woven_shear_modulus(0.6, gf, gm);
assert!((g - g2).abs() > 0.0, "result should vary with vf");
}
#[test]
fn test_kerner_bulk_pure_matrix() {
let km = 50.0e9;
let gm = 30.0e9;
let k = kerner_bulk_modulus(0.0, 200.0e9, gm, km);
assert!((k - km).abs() / km < 1e-6);
}
#[test]
fn test_kerner_bulk_above_matrix() {
let k = kerner_bulk_modulus(0.3, 200.0e9, 30.0e9, 50.0e9);
assert!(k > 50.0e9, "Kerner K should exceed matrix K");
}
#[test]
fn test_nielsen_modulus_pure_matrix() {
let e = nielsen_modulus(0.0, 200.0e9, 3.5e9, 2.5, 0.64);
assert!((e - 3.5e9).abs() < 1.0);
}
#[test]
fn test_nielsen_modulus_increases() {
let e0 = nielsen_modulus(0.1, 200.0e9, 3.5e9, 2.5, 0.64);
let e1 = nielsen_modulus(0.3, 200.0e9, 3.5e9, 2.5, 0.64);
assert!(e1 > e0, "modulus should increase with volume fraction");
}
#[test]
fn test_composite_sphere_bulk() {
let km = 50.0e9;
let ki = 200.0e9;
let gm = 30.0e9;
let k = composite_sphere_bulk(0.4, ki, gm, km);
assert!(k >= km, "K_eff should be >= K_m");
assert!(k <= ki, "K_eff should be <= K_i");
}
#[test]
fn test_nano_effective_vf_no_interphase() {
let veff = nano_effective_volume_fraction(0.3, 10e-9, 0.0);
assert!((veff - 0.3).abs() < 1e-10);
}
#[test]
fn test_nano_effective_vf_grows_with_interphase() {
let vf = 0.1;
let r = 5e-9;
let t = 2e-9;
let veff = nano_effective_volume_fraction(vf, r, t);
assert!(
veff > vf,
"V_eff should exceed V_f with non-zero interphase"
);
}
#[test]
fn test_nano_composite_modulus_no_interphase() {
let e = nano_composite_modulus(0.3, 200.0e9, 0.0, 3.5e9, 3.5e9, 2.0);
let e_ht = halpin_tsai_modulus(0.3, 200.0e9, 3.5e9, 2.0);
assert!((e - e_ht).abs() / e_ht < 0.05, "e={e}, e_ht={e_ht}");
}
#[test]
fn test_nano_surface_correction_large_particle() {
let delta_k = nano_surface_elasticity_correction(1e-9, 1e-3);
assert!(
delta_k < 1e-3,
"correction should be tiny for large particles: {delta_k}"
);
}
#[test]
fn test_nano_surface_correction_small_particle() {
let delta_k = nano_surface_elasticity_correction(1.0, 1e-9);
assert!(
delta_k > 1e8,
"correction should be large for nano-particles: {delta_k}"
);
}
#[test]
fn test_nano_surface_correction_zero_radius() {
let delta_k = nano_surface_elasticity_correction(1.0, 0.0);
assert_eq!(delta_k, 0.0);
}
}