use std::f64::consts::PI;
#[must_use]
#[inline]
pub fn ki_center_crack_infinite(stress: f64, half_crack_length: f64) -> f64 {
stress * (PI * half_crack_length).sqrt()
}
#[must_use]
pub fn ki_center_crack_finite(stress: f64, half_crack_length: f64, width: f64) -> f64 {
if width.abs() < hisab::EPSILON_F64 {
return 0.0;
}
let ratio = PI * half_crack_length / width;
let correction = if ratio.abs() < hisab::EPSILON_F64 {
1.0
} else {
(ratio).cos().recip().sqrt()
};
stress * (PI * half_crack_length).sqrt() * correction
}
#[must_use]
#[inline]
pub fn ki_edge_crack(stress: f64, crack_length: f64) -> f64 {
1.12 * stress * (PI * crack_length).sqrt()
}
#[must_use]
#[inline]
pub fn ki_penny_crack(stress: f64, crack_radius: f64) -> f64 {
(2.0 / PI) * stress * (PI * crack_radius).sqrt()
}
#[must_use]
pub fn ki_crack_at_hole(stress: f64, crack_length: f64, hole_radius: f64) -> f64 {
if hole_radius.abs() < hisab::EPSILON_F64 {
return ki_edge_crack(stress, crack_length);
}
let ratio = crack_length / hole_radius;
let f_bowie = 0.5 * (3.0 - crack_length / (crack_length + hole_radius)) * (1.0 + ratio).sqrt();
stress * (PI * crack_length).sqrt() * f_bowie
}
#[must_use]
#[inline]
pub fn fracture_check(ki: f64, fracture_toughness: f64) -> bool {
ki >= fracture_toughness
}
#[must_use]
#[inline]
pub fn critical_crack_length(stress: f64, fracture_toughness: f64) -> f64 {
if stress.abs() < hisab::EPSILON_F64 {
return f64::INFINITY;
}
(fracture_toughness / stress).powi(2) / PI
}
#[must_use]
#[inline]
pub fn fracture_stress(half_crack_length: f64, fracture_toughness: f64) -> f64 {
let denom = (PI * half_crack_length).sqrt();
if denom.abs() < hisab::EPSILON_F64 {
return f64::INFINITY;
}
fracture_toughness / denom
}
#[must_use]
#[inline]
pub fn kic_from_energy_release(
youngs_modulus: f64,
poisson_ratio: f64,
critical_energy_release: f64,
) -> f64 {
if youngs_modulus <= 0.0 || critical_energy_release < 0.0 {
return 0.0;
}
let denom = 1.0 - poisson_ratio * poisson_ratio;
if denom.abs() < hisab::EPSILON_F64 {
return 0.0;
}
(youngs_modulus * critical_energy_release / denom).sqrt()
}
#[must_use]
#[inline]
pub fn paris_law_rate(c: f64, m: f64, delta_k: f64) -> f64 {
if delta_k <= 0.0 {
return 0.0;
}
c * delta_k.powf(m)
}
#[must_use]
pub fn paris_law_life(
c: f64,
m: f64,
stress_range: f64,
initial_crack: f64,
final_crack: f64,
n_steps: usize,
) -> f64 {
if n_steps == 0 || initial_crack >= final_crack || stress_range.abs() < hisab::EPSILON_F64 {
return 0.0;
}
let integrand = |a: f64| {
let delta_k = stress_range * (PI * a).sqrt();
let da_dn = paris_law_rate(c, m, delta_k);
if da_dn.abs() < hisab::EPSILON_F64 {
f64::INFINITY
} else {
1.0 / da_dn
}
};
let n = if n_steps.is_multiple_of(2) {
n_steps
} else {
n_steps + 1
};
hisab::calc::integral_simpson(integrand, initial_crack, final_crack, n).unwrap_or(0.0)
}
#[must_use]
#[inline]
pub fn kii_center_crack(shear_stress: f64, half_crack_length: f64) -> f64 {
shear_stress * (PI * half_crack_length).sqrt()
}
#[must_use]
#[inline]
pub fn kii_edge_crack(shear_stress: f64, crack_length: f64) -> f64 {
1.12 * shear_stress * (PI * crack_length).sqrt()
}
#[must_use]
#[inline]
pub fn kiii_through_crack(shear_stress: f64, half_crack_length: f64) -> f64 {
shear_stress * (PI * half_crack_length).sqrt()
}
#[must_use]
#[inline]
pub fn j_integral_from_sifs(
ki: f64,
kii: f64,
kiii: f64,
youngs_modulus: f64,
poisson_ratio: f64,
) -> f64 {
let e_prime = youngs_modulus / (1.0 - poisson_ratio * poisson_ratio);
let g = youngs_modulus / (2.0 * (1.0 + poisson_ratio));
if e_prime.abs() < hisab::EPSILON_F64 || g.abs() < hisab::EPSILON_F64 {
return 0.0;
}
(ki * ki + kii * kii) / e_prime + kiii * kiii / (2.0 * g)
}
#[must_use]
#[inline]
pub fn k_from_j_integral(j: f64, youngs_modulus: f64, poisson_ratio: f64) -> f64 {
if j < 0.0 || youngs_modulus <= 0.0 {
return 0.0;
}
let e_prime = youngs_modulus / (1.0 - poisson_ratio * poisson_ratio);
(j * e_prime).sqrt()
}
#[must_use]
#[inline]
pub fn j_integral_mode_i(ki: f64, youngs_modulus: f64, poisson_ratio: f64) -> f64 {
j_integral_from_sifs(ki, 0.0, 0.0, youngs_modulus, poisson_ratio)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ki_center_crack_basic() {
let ki = ki_center_crack_infinite(100e6, 0.01);
assert!(
(ki - 17.72e6).abs() < 0.1e6,
"KI should be ~17.72 MPa√m, got {}",
ki / 1e6
);
}
#[test]
fn ki_finite_approaches_infinite() {
let ki_inf = ki_center_crack_infinite(100e6, 0.01);
let ki_fin = ki_center_crack_finite(100e6, 0.01, 10.0); assert!(
(ki_inf - ki_fin).abs() / ki_inf < 0.001,
"finite width with large W should match infinite"
);
}
#[test]
fn ki_finite_exceeds_infinite() {
let ki_inf = ki_center_crack_infinite(100e6, 0.01);
let ki_fin = ki_center_crack_finite(100e6, 0.01, 0.05); assert!(ki_fin > ki_inf, "finite width should increase KI");
}
#[test]
fn ki_edge_exceeds_center() {
let ki_center = ki_center_crack_infinite(100e6, 0.01);
let ki_edge = ki_edge_crack(100e6, 0.01);
assert!(ki_edge > ki_center, "edge crack should have higher KI");
}
#[test]
fn ki_penny_less_than_through() {
let ki_through = ki_center_crack_infinite(100e6, 0.01);
let ki_penny = ki_penny_crack(100e6, 0.01);
assert!(ki_penny < ki_through, "penny crack should have lower KI");
}
#[test]
fn ki_zero_crack_is_zero() {
let ki = ki_center_crack_infinite(100e6, 0.0);
assert!(ki.abs() < hisab::EPSILON_F64);
}
#[test]
fn fracture_check_basic() {
let ki = ki_center_crack_infinite(200e6, 0.02); assert!(fracture_check(ki, 40e6), "should fracture when KI > KIc");
assert!(
!fracture_check(ki, 100e6),
"should not fracture when KI < KIc"
);
}
#[test]
fn critical_crack_length_basic() {
let a_cr = critical_crack_length(100e6, 50e6);
assert!(
(a_cr - 0.0796).abs() < 0.001,
"critical crack length should be ~79.6 mm, got {:.1} mm",
a_cr * 1000.0
);
}
#[test]
fn critical_crack_length_zero_stress() {
let a_cr = critical_crack_length(0.0, 50e6);
assert!(a_cr.is_infinite());
}
#[test]
fn fracture_stress_roundtrip() {
let a = 0.01;
let kic = 50e6;
let sigma_cr = fracture_stress(a, kic);
let ki = ki_center_crack_infinite(sigma_cr, a);
assert!(
(ki - kic).abs() < 1e3,
"fracture stress roundtrip: KI should equal KIc"
);
}
#[test]
fn kic_from_energy_release_steel() {
let kic = kic_from_energy_release(200e9, 0.3, 20000.0);
assert!(
kic > 50e6 && kic < 80e6,
"KIc should be ~66 MPa√m, got {}",
kic / 1e6
);
}
#[test]
fn paris_law_rate_basic() {
let da_dn = paris_law_rate(1e-11, 3.0, 20e6); assert!(da_dn > 0.0, "growth rate should be positive");
}
#[test]
fn paris_law_rate_zero_delta_k() {
let da_dn = paris_law_rate(1e-11, 3.0, 0.0);
assert_eq!(da_dn, 0.0);
}
#[test]
fn paris_law_rate_negative_delta_k() {
let da_dn = paris_law_rate(1e-11, 3.0, -10e6);
assert_eq!(da_dn, 0.0, "negative ΔK should give zero growth");
}
#[test]
fn paris_law_life_positive() {
let life = paris_law_life(1e-11, 3.0, 100e6, 0.001, 0.01, 1000);
assert!(life > 0.0, "life should be positive");
assert!(life.is_finite(), "life should be finite");
}
#[test]
fn paris_law_life_longer_crack_fewer_cycles() {
let life_small = paris_law_life(1e-11, 3.0, 100e6, 0.001, 0.05, 1000);
let life_large = paris_law_life(1e-11, 3.0, 100e6, 0.01, 0.05, 1000);
assert!(
life_large < life_small,
"larger initial crack should give fewer cycles"
);
}
#[test]
fn paris_law_life_zero_range() {
let life = paris_law_life(1e-11, 3.0, 0.0, 0.001, 0.01, 1000);
assert_eq!(life, 0.0);
}
#[test]
fn ki_crack_at_hole_basic() {
let ki = ki_crack_at_hole(100e6, 0.005, 0.01);
assert!(ki > 0.0);
let ki_plain = ki_edge_crack(100e6, 0.005);
assert!(
ki > ki_plain,
"crack at hole should have higher KI than plain edge"
);
}
#[test]
fn kii_center_basic() {
let kii = kii_center_crack(100e6, 0.01);
let ki = ki_center_crack_infinite(100e6, 0.01);
assert!(
(kii - ki).abs() < 1.0,
"KII center should equal KI center for same stress"
);
}
#[test]
fn kii_edge_exceeds_center() {
let kii_c = kii_center_crack(100e6, 0.01);
let kii_e = kii_edge_crack(100e6, 0.01);
assert!(kii_e > kii_c, "edge should exceed center (free surface)");
}
#[test]
fn kiii_positive() {
let kiii = kiii_through_crack(100e6, 0.01);
assert!(kiii > 0.0);
}
#[test]
fn j_integral_mode_i_positive() {
let ki = ki_center_crack_infinite(100e6, 0.01);
let j = j_integral_mode_i(ki, 200e9, 0.30);
assert!(j > 0.0, "J should be positive for non-zero KI");
}
#[test]
fn j_integral_roundtrip() {
let ki_original = 50e6; let j = j_integral_mode_i(ki_original, 200e9, 0.30);
let ki_recovered = k_from_j_integral(j, 200e9, 0.30);
assert!(
(ki_recovered - ki_original).abs() < 1e3,
"roundtrip: expected {ki_original}, got {ki_recovered}"
);
}
#[test]
fn j_integral_mixed_mode() {
let ki = 30e6;
let kii = 20e6;
let kiii = 10e6;
let j = j_integral_from_sifs(ki, kii, kiii, 200e9, 0.30);
let j_mode_i = j_integral_mode_i(ki, 200e9, 0.30);
assert!(j > j_mode_i, "mixed-mode J should exceed pure Mode I J");
}
#[test]
fn j_integral_zero_k() {
let j = j_integral_from_sifs(0.0, 0.0, 0.0, 200e9, 0.30);
assert!(
j.abs() < hisab::EPSILON_F64,
"J should be zero for zero SIFs"
);
}
#[test]
fn k_from_j_matches_gi_formula() {
let gic = 5000.0; let k_via_j = k_from_j_integral(gic, 200e9, 0.30);
let k_via_gi = kic_from_energy_release(200e9, 0.30, gic);
assert!(
(k_via_j - k_via_gi).abs() < 1e3,
"k_from_j should match kic_from_energy_release: {} vs {}",
k_via_j / 1e6,
k_via_gi / 1e6
);
}
}