use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Lamina {
pub e1: f64,
pub e2: f64,
pub g12: f64,
pub nu12: f64,
pub xt: f64,
pub xc: f64,
pub yt: f64,
pub yc: f64,
pub s12: f64,
}
impl Lamina {
#[must_use]
#[inline]
pub fn nu21(&self) -> f64 {
if self.e1.abs() < hisab::EPSILON_F64 {
return 0.0;
}
self.nu12 * self.e2 / self.e1
}
#[must_use]
pub fn stiffness_matrix(&self) -> [[f64; 3]; 3] {
let nu21 = self.nu21();
let denom = 1.0 - self.nu12 * nu21;
if denom.abs() < hisab::EPSILON_F64 {
return [[0.0; 3]; 3];
}
let q11 = self.e1 / denom;
let q22 = self.e2 / denom;
let q12 = self.nu12 * self.e2 / denom;
let q66 = self.g12;
[[q11, q12, 0.0], [q12, q22, 0.0], [0.0, 0.0, q66]]
}
#[must_use]
pub fn transformed_stiffness(&self, theta: f64) -> [[f64; 3]; 3] {
let q = self.stiffness_matrix();
let c = theta.cos();
let s = theta.sin();
let c2 = c * c;
let s2 = s * s;
let cs = c * s;
let c4 = c2 * c2;
let s4 = s2 * s2;
let q11 = q[0][0];
let q12 = q[0][1];
let q22 = q[1][1];
let q66 = q[2][2];
let qb11 = q11 * c4 + 2.0 * (q12 + 2.0 * q66) * c2 * s2 + q22 * s4;
let qb22 = q11 * s4 + 2.0 * (q12 + 2.0 * q66) * c2 * s2 + q22 * c4;
let qb12 = (q11 + q22 - 4.0 * q66) * c2 * s2 + q12 * (c4 + s4);
let qb66 = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * c2 * s2 + q66 * (c4 + s4);
let qb16 = (q11 - q12 - 2.0 * q66) * c2 * cs - (q22 - q12 - 2.0 * q66) * s2 * cs;
let qb26 = (q11 - q12 - 2.0 * q66) * cs * s2 - (q22 - q12 - 2.0 * q66) * c2 * cs;
[[qb11, qb12, qb16], [qb12, qb22, qb26], [qb16, qb26, qb66]]
}
#[must_use]
pub fn carbon_epoxy() -> Self {
Self {
e1: 138e9,
e2: 11e9,
g12: 5.5e9,
nu12: 0.28,
xt: 1500e6,
xc: 1200e6,
yt: 50e6,
yc: 250e6,
s12: 70e6,
}
}
#[must_use]
pub fn glass_epoxy() -> Self {
Self {
e1: 39e9,
e2: 8.6e9,
g12: 3.8e9,
nu12: 0.28,
xt: 1080e6,
xc: 620e6,
yt: 39e6,
yc: 128e6,
s12: 89e6,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Ply {
pub lamina: Lamina,
pub angle: f64,
pub thickness: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AbdMatrix {
pub a: [[f64; 3]; 3],
pub b: [[f64; 3]; 3],
pub d: [[f64; 3]; 3],
}
#[must_use]
pub fn abd_matrix(plies: &[Ply]) -> AbdMatrix {
let mut a = [[0.0; 3]; 3];
let mut b = [[0.0; 3]; 3];
let mut d = [[0.0; 3]; 3];
let total_thickness: f64 = plies.iter().map(|p| p.thickness).sum();
let mut z_bot = -total_thickness / 2.0;
for ply in plies {
let z_top = z_bot + ply.thickness;
let qbar = ply.lamina.transformed_stiffness(ply.angle);
let dz = z_top - z_bot;
let dz2 = z_top * z_top - z_bot * z_bot;
let dz3 = z_top * z_top * z_top - z_bot * z_bot * z_bot;
for i in 0..3 {
for j in 0..3 {
a[i][j] += qbar[i][j] * dz;
b[i][j] += qbar[i][j] * dz2 / 2.0;
d[i][j] += qbar[i][j] * dz3 / 3.0;
}
}
z_bot = z_top;
}
AbdMatrix { a, b, d }
}
#[must_use]
pub fn abd_inverse(abd: &AbdMatrix) -> Option<AbdMatrix> {
let mut full: Vec<Vec<f64>> = vec![vec![0.0; 6]; 6];
for i in 0..3 {
for j in 0..3 {
full[i][j] = abd.a[i][j];
full[i][j + 3] = abd.b[i][j];
full[i + 3][j] = abd.b[i][j];
full[i + 3][j + 3] = abd.d[i][j];
}
}
let inv = hisab::num::matrix_inverse(&full).ok()?;
let mut a = [[0.0; 3]; 3];
let mut b = [[0.0; 3]; 3];
let mut d = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
a[i][j] = inv[i][j];
b[i][j] = inv[i][j + 3];
d[i][j] = inv[i + 3][j + 3];
}
}
Some(AbdMatrix { a, b, d })
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct PlyStress {
pub sigma1: f64,
pub sigma2: f64,
pub tau12: f64,
}
#[must_use]
#[inline]
pub fn max_stress_failure_index(stress: &PlyStress, lamina: &Lamina) -> f64 {
if lamina.xt.abs() < hisab::EPSILON_F64
|| lamina.xc.abs() < hisab::EPSILON_F64
|| lamina.yt.abs() < hisab::EPSILON_F64
|| lamina.yc.abs() < hisab::EPSILON_F64
|| lamina.s12.abs() < hisab::EPSILON_F64
{
return f64::INFINITY;
}
let f1 = if stress.sigma1 >= 0.0 {
stress.sigma1 / lamina.xt
} else {
stress.sigma1.abs() / lamina.xc
};
let f2 = if stress.sigma2 >= 0.0 {
stress.sigma2 / lamina.yt
} else {
stress.sigma2.abs() / lamina.yc
};
let f12 = stress.tau12.abs() / lamina.s12;
f1.max(f2).max(f12)
}
#[must_use]
#[inline]
pub fn tsai_hill_failure_index(stress: &PlyStress, lamina: &Lamina) -> f64 {
let x = if stress.sigma1 >= 0.0 {
lamina.xt
} else {
lamina.xc
};
let y = if stress.sigma2 >= 0.0 {
lamina.yt
} else {
lamina.yc
};
if x.abs() < hisab::EPSILON_F64
|| y.abs() < hisab::EPSILON_F64
|| lamina.s12.abs() < hisab::EPSILON_F64
{
return f64::INFINITY;
}
let s1 = stress.sigma1;
let s2 = stress.sigma2;
let t = stress.tau12;
(s1 / x).powi(2) - (s1 * s2) / (x * x) + (s2 / y).powi(2) + (t / lamina.s12).powi(2)
}
#[must_use]
#[inline]
pub fn tsai_wu_failure_index(stress: &PlyStress, lamina: &Lamina) -> f64 {
tsai_wu_failure_index_custom(stress, lamina, -0.5)
}
#[must_use]
pub fn tsai_wu_failure_index_custom(stress: &PlyStress, lamina: &Lamina, f_star: f64) -> f64 {
if lamina.xt.abs() < hisab::EPSILON_F64
|| lamina.xc.abs() < hisab::EPSILON_F64
|| lamina.yt.abs() < hisab::EPSILON_F64
|| lamina.yc.abs() < hisab::EPSILON_F64
|| lamina.s12.abs() < hisab::EPSILON_F64
{
return f64::INFINITY;
}
let f1 = 1.0 / lamina.xt - 1.0 / lamina.xc;
let f2 = 1.0 / lamina.yt - 1.0 / lamina.yc;
let f11 = 1.0 / (lamina.xt * lamina.xc);
let f22 = 1.0 / (lamina.yt * lamina.yc);
let f66 = 1.0 / (lamina.s12 * lamina.s12);
let f12 = f_star * (f11 * f22).sqrt();
let s1 = stress.sigma1;
let s2 = stress.sigma2;
let t = stress.tau12;
f1 * s1 + f2 * s2 + f11 * s1 * s1 + f22 * s2 * s2 + f66 * t * t + 2.0 * f12 * s1 * s2
}
#[must_use]
pub fn transform_stress_to_material(
sigma_x: f64,
sigma_y: f64,
tau_xy: f64,
theta: f64,
) -> PlyStress {
let c = theta.cos();
let s = theta.sin();
let c2 = c * c;
let s2 = s * s;
let cs = c * s;
PlyStress {
sigma1: c2 * sigma_x + s2 * sigma_y + 2.0 * cs * tau_xy,
sigma2: s2 * sigma_x + c2 * sigma_y - 2.0 * cs * tau_xy,
tau12: -cs * sigma_x + cs * sigma_y + (c2 - s2) * tau_xy,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct StrainAllowables {
pub eps1t: f64,
pub eps1c: f64,
pub eps2t: f64,
pub eps2c: f64,
pub gamma12_max: f64,
}
#[must_use]
#[inline]
pub fn max_strain_failure_index(
eps1: f64,
eps2: f64,
gamma12: f64,
allow: &StrainAllowables,
) -> f64 {
let f1 = if eps1 >= 0.0 {
if allow.eps1t.abs() < hisab::EPSILON_F64 {
f64::INFINITY
} else {
eps1 / allow.eps1t
}
} else if allow.eps1c.abs() < hisab::EPSILON_F64 {
f64::INFINITY
} else {
eps1.abs() / allow.eps1c
};
let f2 = if eps2 >= 0.0 {
if allow.eps2t.abs() < hisab::EPSILON_F64 {
f64::INFINITY
} else {
eps2 / allow.eps2t
}
} else if allow.eps2c.abs() < hisab::EPSILON_F64 {
f64::INFINITY
} else {
eps2.abs() / allow.eps2c
};
let f12 = if allow.gamma12_max.abs() < hisab::EPSILON_F64 {
f64::INFINITY
} else {
gamma12.abs() / allow.gamma12_max
};
f1.max(f2).max(f12)
}
#[must_use]
pub fn hashin_failure(stress: &PlyStress, lamina: &Lamina) -> HashinResult {
let s1 = stress.sigma1;
let s2 = stress.sigma2;
let t12 = stress.tau12;
let fiber_tension = if s1 >= 0.0 {
(s1 / lamina.xt).powi(2) + (t12 / lamina.s12).powi(2)
} else {
0.0
};
let fiber_compression = if s1 < 0.0 {
(s1 / lamina.xc).powi(2)
} else {
0.0
};
let matrix_tension = if s2 >= 0.0 {
(s2 / lamina.yt).powi(2) + (t12 / lamina.s12).powi(2)
} else {
0.0
};
let matrix_compression = if s2 < 0.0 {
(s2 / lamina.yc).powi(2) + (t12 / lamina.s12).powi(2)
} else {
0.0
};
HashinResult {
fiber_tension,
fiber_compression,
matrix_tension,
matrix_compression,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HashinResult {
pub fiber_tension: f64,
pub fiber_compression: f64,
pub matrix_tension: f64,
pub matrix_compression: f64,
}
impl HashinResult {
#[must_use]
#[inline]
pub fn max_index(&self) -> f64 {
self.fiber_tension
.max(self.fiber_compression)
.max(self.matrix_tension)
.max(self.matrix_compression)
}
#[must_use]
#[inline]
pub fn is_failed(&self) -> bool {
self.max_index() >= 1.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct DegradationFactors {
pub fiber: f64,
pub matrix: f64,
}
impl Default for DegradationFactors {
fn default() -> Self {
Self {
fiber: 0.07,
matrix: 0.14,
}
}
}
#[must_use]
pub fn progressive_failure(
plies: &[Ply],
laminate_stress: [f64; 3],
degradation: &DegradationFactors,
max_iterations: usize,
) -> (Vec<Ply>, usize) {
let mut current_plies: Vec<Ply> = plies.to_vec();
let mut failed: Vec<bool> = vec![false; plies.len()];
for iteration in 1..=max_iterations {
let abd = abd_matrix(¤t_plies);
let a_inv = invert_3x3(&abd.a);
let a_inv = match a_inv {
Some(inv) => inv,
None => return (current_plies, iteration),
};
let eps0 = mat3_vec3_mul(&a_inv, &laminate_stress);
let mut new_failure = false;
for (i, ply) in current_plies.iter_mut().enumerate() {
if failed[i] {
continue;
}
let qbar = ply.lamina.transformed_stiffness(ply.angle);
let sig_x = qbar[0][0] * eps0[0] + qbar[0][1] * eps0[1] + qbar[0][2] * eps0[2];
let sig_y = qbar[1][0] * eps0[0] + qbar[1][1] * eps0[1] + qbar[1][2] * eps0[2];
let tau_xy = qbar[2][0] * eps0[0] + qbar[2][1] * eps0[1] + qbar[2][2] * eps0[2];
let ps = transform_stress_to_material(sig_x, sig_y, tau_xy, ply.angle);
let hashin = hashin_failure(&ps, &ply.lamina);
if hashin.is_failed() {
failed[i] = true;
new_failure = true;
if hashin.fiber_tension >= 1.0 || hashin.fiber_compression >= 1.0 {
ply.lamina.e1 *= degradation.fiber;
ply.lamina.e2 *= degradation.fiber;
ply.lamina.g12 *= degradation.fiber;
}
if hashin.matrix_tension >= 1.0 || hashin.matrix_compression >= 1.0 {
ply.lamina.e2 *= degradation.matrix;
ply.lamina.g12 *= degradation.matrix;
}
}
}
if !new_failure {
return (current_plies, iteration);
}
}
(current_plies, max_iterations)
}
fn invert_3x3(m: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
if det.abs() < hisab::EPSILON_F64 {
return None;
}
let inv_det = 1.0 / det;
Some([
[
(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
(m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
(m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
],
[
(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
(m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
(m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
],
[
(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
(m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
(m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
],
])
}
fn mat3_vec3_mul(m: &[[f64; 3]; 3], v: &[f64; 3]) -> [f64; 3] {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn carbon() -> Lamina {
Lamina::carbon_epoxy()
}
#[test]
fn q_matrix_symmetry() {
let q = carbon().stiffness_matrix();
assert!((q[0][1] - q[1][0]).abs() < 1.0, "Q should be symmetric");
}
#[test]
fn q_matrix_diagonal_positive() {
let q = carbon().stiffness_matrix();
for (i, row) in q.iter().enumerate() {
assert!(row[i] > 0.0, "Q[{i}][{i}] should be positive");
}
}
#[test]
fn q11_dominated_by_e1() {
let l = carbon();
let q = l.stiffness_matrix();
assert!(
(q[0][0] - l.e1).abs() / l.e1 < 0.05,
"Q11 should be close to E1"
);
}
#[test]
fn nu21_reciprocity() {
let l = carbon();
let lhs = l.nu21() / l.e2;
let rhs = l.nu12 / l.e1;
assert!(
(lhs - rhs).abs() < hisab::EPSILON_F64,
"reciprocity: v21/E2 = v12/E1"
);
}
#[test]
fn qbar_at_zero_equals_q() {
let l = carbon();
let q = l.stiffness_matrix();
let qbar = l.transformed_stiffness(0.0);
for i in 0..3 {
for j in 0..3 {
assert!(
(q[i][j] - qbar[i][j]).abs() < 1.0,
"Q-bar at 0° should equal Q at [{i}][{j}]"
);
}
}
}
#[test]
fn qbar_at_90_swaps_axes() {
let l = carbon();
let q = l.stiffness_matrix();
let qbar = l.transformed_stiffness(PI / 2.0);
assert!(
(qbar[0][0] - q[1][1]).abs() < 1e3,
"Q-bar_11 at 90° should ≈ Q22"
);
assert!(
(qbar[1][1] - q[0][0]).abs() < 1e3,
"Q-bar_22 at 90° should ≈ Q11"
);
}
#[test]
fn qbar_symmetric() {
let qbar = carbon().transformed_stiffness(PI / 4.0);
for (i, row) in qbar.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
assert!(
(val - qbar[j][i]).abs() < 1.0,
"Q-bar should be symmetric at [{i}][{j}]"
);
}
}
}
#[test]
fn qbar_45_has_coupling() {
let qbar = carbon().transformed_stiffness(PI / 4.0);
assert!(
qbar[0][2].abs() > 1e6,
"Q-bar_16 at 45° should be significant"
);
}
#[test]
fn abd_symmetric_laminate_zero_b() {
let l = carbon();
let plies = vec![
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
];
let abd = abd_matrix(&plies);
for (i, row) in abd.b.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
assert!(
val.abs() < 1e-3,
"B[{i}][{j}] should be ~0 for symmetric layup, got {val}"
);
}
}
}
#[test]
fn abd_a_matrix_positive_diagonal() {
let l = carbon();
let plies = vec![
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 4.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: -PI / 4.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
];
let abd = abd_matrix(&plies);
for (i, row) in abd.a.iter().enumerate() {
assert!(row[i] > 0.0, "A[{i}][{i}] should be positive");
}
}
#[test]
fn abd_d_matrix_positive_diagonal() {
let l = carbon();
let plies = vec![
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
];
let abd = abd_matrix(&plies);
for (i, row) in abd.d.iter().enumerate() {
assert!(row[i] > 0.0, "D[{i}][{i}] should be positive");
}
}
#[test]
fn abd_single_ply() {
let l = carbon();
let plies = vec![Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.001,
}];
let abd = abd_matrix(&plies);
let q = l.stiffness_matrix();
assert!(
(abd.a[0][0] - q[0][0] * 0.001).abs() < 1e3,
"A11 should equal Q11*t for single ply"
);
}
#[test]
fn abd_thicker_laminate_stiffer() {
let l = carbon();
let thin = vec![Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.001,
}];
let thick = vec![Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.002,
}];
let abd_thin = abd_matrix(&thin);
let abd_thick = abd_matrix(&thick);
assert!(abd_thick.a[0][0] > abd_thin.a[0][0]);
}
#[test]
fn max_stress_no_failure() {
let l = carbon();
let s = PlyStress {
sigma1: 100e6,
sigma2: 10e6,
tau12: 5e6,
};
let fi = max_stress_failure_index(&s, &l);
assert!(fi < 1.0, "should not fail at low stress, got {fi}");
}
#[test]
fn max_stress_fiber_failure() {
let l = carbon();
let s = PlyStress {
sigma1: 2000e6,
sigma2: 0.0,
tau12: 0.0,
};
let fi = max_stress_failure_index(&s, &l);
assert!(fi >= 1.0, "should fail in fiber tension");
}
#[test]
fn max_stress_matrix_failure() {
let l = carbon();
let s = PlyStress {
sigma1: 0.0,
sigma2: 100e6,
tau12: 0.0,
};
let fi = max_stress_failure_index(&s, &l);
assert!(fi >= 1.0, "should fail in matrix tension (Yt=50 MPa)");
}
#[test]
fn tsai_hill_no_failure() {
let l = carbon();
let s = PlyStress {
sigma1: 100e6,
sigma2: 10e6,
tau12: 5e6,
};
let fi = tsai_hill_failure_index(&s, &l);
assert!(fi < 1.0, "should not fail, got {fi}");
}
#[test]
fn tsai_hill_fiber_failure() {
let l = carbon();
let s = PlyStress {
sigma1: 1600e6,
sigma2: 0.0,
tau12: 0.0,
};
let fi = tsai_hill_failure_index(&s, &l);
assert!(fi >= 1.0, "should fail in fiber direction");
}
#[test]
fn tsai_wu_no_failure() {
let l = carbon();
let s = PlyStress {
sigma1: 100e6,
sigma2: 10e6,
tau12: 5e6,
};
let fi = tsai_wu_failure_index(&s, &l);
assert!(fi < 1.0, "should not fail, got {fi}");
}
#[test]
fn tsai_wu_failure() {
let l = carbon();
let s = PlyStress {
sigma1: 1600e6,
sigma2: 0.0,
tau12: 0.0,
};
let fi = tsai_wu_failure_index(&s, &l);
assert!(fi >= 1.0, "should fail");
}
#[test]
fn tsai_wu_accounts_for_sign() {
let l = carbon();
let tension = PlyStress {
sigma1: 0.0,
sigma2: 40e6,
tau12: 0.0,
};
let compression = PlyStress {
sigma1: 0.0,
sigma2: -40e6,
tau12: 0.0,
};
let fi_t = tsai_wu_failure_index(&tension, &l);
let fi_c = tsai_wu_failure_index(&compression, &l);
assert!(
fi_t > fi_c,
"tension should be closer to failure than compression (Yt < Yc)"
);
}
#[test]
fn transform_zero_angle_identity() {
let ps = transform_stress_to_material(100e6, 50e6, 20e6, 0.0);
assert!((ps.sigma1 - 100e6).abs() < 1.0);
assert!((ps.sigma2 - 50e6).abs() < 1.0);
assert!((ps.tau12 - 20e6).abs() < 1.0);
}
#[test]
fn transform_90_swaps() {
let ps = transform_stress_to_material(100e6, 50e6, 0.0, PI / 2.0);
assert!((ps.sigma1 - 50e6).abs() < 1e3, "σ1 at 90° should be σy");
assert!((ps.sigma2 - 100e6).abs() < 1e3, "σ2 at 90° should be σx");
}
#[test]
fn transform_preserves_invariant() {
let ps = transform_stress_to_material(100e6, 50e6, 30e6, PI / 6.0);
let sum_material = ps.sigma1 + ps.sigma2;
let sum_laminate = 100e6 + 50e6;
assert!(
(sum_material - sum_laminate).abs() < 1e3,
"stress invariant should be preserved"
);
}
#[test]
fn glass_epoxy_preset() {
let g = Lamina::glass_epoxy();
assert!(g.e1 > g.e2);
assert!(g.xt > 0.0);
assert!(g.s12 > 0.0);
}
#[test]
fn abd_inverse_roundtrip() {
let l = carbon();
let plies = vec![
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
];
let abd = abd_matrix(&plies);
let inv = abd_inverse(&abd).expect("should be invertible");
for i in 0..3 {
for j in 0..3 {
let mut product = 0.0;
for k in 0..3 {
product += abd.a[i][k] * inv.a[k][j];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(product - expected).abs() < 0.01,
"A*a_inv should be identity at [{i}][{j}], got {product}"
);
}
}
}
#[test]
fn max_strain_no_failure() {
let allow = StrainAllowables {
eps1t: 0.01,
eps1c: 0.008,
eps2t: 0.005,
eps2c: 0.02,
gamma12_max: 0.02,
};
let fi = max_strain_failure_index(0.001, 0.0005, 0.001, &allow);
assert!(fi < 1.0, "should not fail at low strain, got {fi}");
}
#[test]
fn max_strain_fiber_failure() {
let allow = StrainAllowables {
eps1t: 0.01,
eps1c: 0.008,
eps2t: 0.005,
eps2c: 0.02,
gamma12_max: 0.02,
};
let fi = max_strain_failure_index(0.02, 0.0, 0.0, &allow);
assert!(fi >= 1.0, "should fail in fiber tension");
}
#[test]
fn hashin_no_failure() {
let l = carbon();
let s = PlyStress {
sigma1: 100e6,
sigma2: 10e6,
tau12: 5e6,
};
let h = hashin_failure(&s, &l);
assert!(!h.is_failed(), "should not fail at low stress");
}
#[test]
fn hashin_fiber_tension() {
let l = carbon();
let s = PlyStress {
sigma1: 2000e6,
sigma2: 0.0,
tau12: 0.0,
};
let h = hashin_failure(&s, &l);
assert!(h.fiber_tension >= 1.0, "should fail in fiber tension");
assert!(
h.fiber_compression < hisab::EPSILON_F64,
"no compression failure"
);
}
#[test]
fn hashin_matrix_tension() {
let l = carbon();
let s = PlyStress {
sigma1: 0.0,
sigma2: 100e6,
tau12: 0.0,
};
let h = hashin_failure(&s, &l);
assert!(
h.matrix_tension >= 1.0,
"should fail in matrix tension (Yt=50 MPa)"
);
}
#[test]
fn hashin_fiber_compression() {
let l = carbon();
let s = PlyStress {
sigma1: -1500e6,
sigma2: 0.0,
tau12: 0.0,
};
let h = hashin_failure(&s, &l);
assert!(
h.fiber_compression >= 1.0,
"should fail in fiber compression (Xc=1200 MPa)"
);
}
#[test]
fn hashin_distinguishes_modes() {
let l = carbon();
let s = PlyStress {
sigma1: 0.0,
sigma2: 100e6,
tau12: 0.0,
};
let h = hashin_failure(&s, &l);
assert!(h.matrix_tension > h.fiber_tension);
assert!(h.matrix_tension > h.fiber_compression);
}
#[test]
fn progressive_failure_low_load_no_damage() {
let l = carbon();
let plies = vec![
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
];
let deg = DegradationFactors::default();
let (result, iters) = progressive_failure(&plies, [1000.0, 500.0, 0.0], °, 10);
assert_eq!(iters, 1, "should converge in 1 iteration (no failures)");
assert!(
(result[0].lamina.e1 - l.e1).abs() < 1.0,
"no degradation expected"
);
}
#[test]
fn progressive_failure_high_load_degrades() {
let l = carbon();
let plies = vec![
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: 0.0,
thickness: 0.000125,
},
Ply {
lamina: l.clone(),
angle: PI / 2.0,
thickness: 0.000125,
},
];
let deg = DegradationFactors::default();
let (result, _) = progressive_failure(&plies, [500_000.0, 0.0, 0.0], °, 10);
assert!(
result[0].lamina.e2 < l.e2,
"90° ply should have degraded E2"
);
}
#[test]
fn tsai_wu_custom_f_star_zero_more_conservative() {
let l = carbon();
let s = PlyStress {
sigma1: 500e6,
sigma2: 20e6,
tau12: 30e6,
};
let fi_default = tsai_wu_failure_index(&s, &l);
let fi_zero = tsai_wu_failure_index_custom(&s, &l, 0.0);
assert!(
(fi_default - fi_zero).abs() > hisab::EPSILON_F64,
"different f* should give different results"
);
}
}