#[derive(Debug, Clone, Copy)]
pub struct BilinearCzmParams {
pub penalty_stiffness: f64,
pub traction_max: f64,
pub delta_failure: f64,
}
impl BilinearCzmParams {
pub fn new(penalty_stiffness: f64, traction_max: f64, delta_failure: f64) -> Self {
Self {
penalty_stiffness,
traction_max,
delta_failure,
}
}
pub fn delta_onset(&self) -> f64 {
self.traction_max / self.penalty_stiffness.max(f64::EPSILON)
}
pub fn fracture_toughness(&self) -> f64 {
0.5 * self.traction_max * self.delta_failure
}
pub fn traction(&self, delta: f64) -> f64 {
if delta <= 0.0 {
return 0.0;
}
let d0 = self.delta_onset();
if delta < d0 {
self.penalty_stiffness * delta
} else if delta < self.delta_failure {
let df = self.delta_failure;
self.traction_max * (df - delta) / (df - d0).max(f64::EPSILON)
} else {
0.0
}
}
pub fn secant_stiffness(&self, delta: f64) -> f64 {
if delta <= f64::EPSILON {
return self.penalty_stiffness;
}
self.traction(delta) / delta
}
pub fn damage(&self, delta_max: f64) -> f64 {
let d0 = self.delta_onset();
let df = self.delta_failure;
if delta_max <= d0 {
0.0
} else if delta_max >= df {
1.0
} else {
(df * (delta_max - d0)) / (delta_max * (df - d0).max(f64::EPSILON))
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PprParams {
pub gamma_n: f64,
pub gamma_t: f64,
pub alpha: f64,
pub beta: f64,
pub delta_n: f64,
pub delta_t: f64,
pub lambda_n: f64,
pub lambda_t: f64,
}
impl PprParams {
pub fn new(
gamma_n: f64,
gamma_t: f64,
alpha: f64,
beta: f64,
delta_n: f64,
delta_t: f64,
lambda_n: f64,
lambda_t: f64,
) -> Self {
Self {
gamma_n,
gamma_t,
alpha,
beta,
delta_n,
delta_t,
lambda_n,
lambda_t,
}
}
pub fn delta_n_peak(&self) -> f64 {
self.lambda_n * self.delta_n
}
pub fn delta_t_peak(&self) -> f64 {
self.lambda_t * self.delta_t
}
pub fn sigma_max(&self) -> f64 {
let dn = self.delta_n.max(f64::EPSILON);
let lam = self.lambda_n.clamp(f64::EPSILON, 1.0 - f64::EPSILON);
self.gamma_n / dn * self.alpha * (1.0 - lam).powf(self.alpha - 1.0) / lam.powf(self.alpha)
}
pub fn normal_traction(&self, delta_n: f64) -> f64 {
if delta_n <= 0.0 || delta_n >= self.delta_n {
return 0.0;
}
let s = delta_n / self.delta_n;
let sp = self.lambda_n.clamp(f64::EPSILON, 1.0 - f64::EPSILON);
let t_max = self.sigma_max();
let numerator = (s / sp).powf(self.alpha - 1.0) * (1.0 - s).powf(self.alpha);
let denom = (1.0 - sp).powf(self.alpha);
t_max * numerator / denom.max(f64::EPSILON)
}
pub fn mode_i_energy_numerical(&self) -> f64 {
let n = 200usize;
let dn_max = self.delta_n;
let h = dn_max / n as f64;
let mut sum = 0.0;
for i in 0..n {
let d = (i as f64 + 0.5) * h;
sum += self.normal_traction(d) * h;
}
sum
}
}
#[derive(Debug, Clone)]
pub struct CohesiveDamageState {
pub damage: f64,
pub delta_max: f64,
pub failed: bool,
}
impl CohesiveDamageState {
pub fn new() -> Self {
Self {
damage: 0.0,
delta_max: 0.0,
failed: false,
}
}
pub fn update(&mut self, delta: f64, params: &BilinearCzmParams) -> f64 {
if self.failed {
return 1.0;
}
if delta > self.delta_max {
self.delta_max = delta;
}
self.damage = params.damage(self.delta_max);
if self.damage >= 1.0 {
self.damage = 1.0;
self.failed = true;
}
self.damage
}
pub fn traction(&self, delta: f64, params: &BilinearCzmParams) -> f64 {
if self.failed || delta <= 0.0 {
return 0.0;
}
(1.0 - self.damage) * params.penalty_stiffness * delta
}
}
#[derive(Debug, Clone)]
pub struct CohesiveInterface {
pub nodes_a: Vec<usize>,
pub nodes_b: Vec<usize>,
pub params: BilinearCzmParams,
pub damage_states: Vec<CohesiveDamageState>,
}
impl CohesiveInterface {
pub fn new(nodes_a: Vec<usize>, nodes_b: Vec<usize>, params: BilinearCzmParams) -> Self {
let n_ip = nodes_a.len();
Self {
nodes_a,
nodes_b,
params,
damage_states: vec![CohesiveDamageState::new(); n_ip],
}
}
pub fn n_nodes(&self) -> usize {
self.nodes_a.len()
}
pub fn is_fully_failed(&self) -> bool {
self.damage_states.iter().all(|s| s.failed)
}
pub fn max_damage(&self) -> f64 {
self.damage_states
.iter()
.map(|s| s.damage)
.fold(0.0_f64, f64::max)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ThermalCzmParams {
pub base: BilinearCzmParams,
pub t_ref: f64,
pub softening_exponent: f64,
pub t_melt: f64,
}
impl ThermalCzmParams {
pub fn new(base: BilinearCzmParams, t_ref: f64, softening_exponent: f64, t_melt: f64) -> Self {
Self {
base,
t_ref,
softening_exponent,
t_melt,
}
}
pub fn thermal_factor(&self, temperature: f64) -> f64 {
let dt = temperature - self.t_ref;
if dt <= 0.0 {
return 1.0;
}
let ratio = dt / self.t_melt.max(f64::EPSILON);
(1.0 - ratio.powf(self.softening_exponent)).max(0.0)
}
pub fn traction(&self, delta: f64, temp: f64) -> f64 {
self.base.traction(delta) * self.thermal_factor(temp)
}
pub fn fracture_toughness(&self, temp: f64) -> f64 {
self.base.fracture_toughness() * self.thermal_factor(temp)
}
pub fn heat_flux(&self, h_int: f64, delta_t: f64) -> f64 {
h_int * delta_t
}
}
#[derive(Debug, Clone, Copy)]
pub struct XuNeedlemanParams {
pub phi_n: f64,
pub phi_t: f64,
pub delta_n: f64,
pub delta_t: f64,
}
impl XuNeedlemanParams {
pub fn new(phi_n: f64, phi_t: f64, delta_n: f64, delta_t: f64) -> Self {
Self {
phi_n,
phi_t,
delta_n,
delta_t,
}
}
pub fn sigma_max(&self) -> f64 {
self.phi_n / (std::f64::consts::E * self.delta_n.max(f64::EPSILON))
}
pub fn tau_max(&self) -> f64 {
self.phi_t / ((std::f64::consts::E / 2.0).sqrt() * self.delta_t.max(f64::EPSILON))
}
pub fn normal_traction(&self, delta_n: f64, delta_t: f64) -> f64 {
let dn = self.delta_n.max(f64::EPSILON);
let dt = self.delta_t.max(f64::EPSILON);
let exp_n = (-delta_n / dn).exp();
let exp_t = (-(delta_t * delta_t) / (dt * dt)).exp();
(self.phi_n / dn) * exp_n * exp_t * (1.0 + delta_n / dn)
}
pub fn tangential_traction(&self, delta_n: f64, delta_t: f64) -> f64 {
let dn = self.delta_n.max(f64::EPSILON);
let dt = self.delta_t.max(f64::EPSILON);
let exp_n = (-delta_n / dn).exp();
let exp_t = (-(delta_t * delta_t) / (dt * dt)).exp();
2.0 * (self.phi_t / (dt * dt)) * delta_t * exp_n * exp_t
}
pub fn potential(&self, delta_n: f64, delta_t: f64) -> f64 {
let dn = self.delta_n.max(f64::EPSILON);
let dt = self.delta_t.max(f64::EPSILON);
self.phi_n
* (1.0
- (1.0 + delta_n / dn)
* (-delta_n / dn).exp()
* (-(delta_t * delta_t) / (dt * dt)).exp())
}
}
#[derive(Debug, Clone, Copy)]
pub struct FatigueCzmParams {
pub mono: BilinearCzmParams,
pub c_fatigue: f64,
pub m_fatigue: f64,
pub delta_threshold: f64,
}
impl FatigueCzmParams {
pub fn new(
mono: BilinearCzmParams,
c_fatigue: f64,
m_fatigue: f64,
delta_threshold: f64,
) -> Self {
Self {
mono,
c_fatigue,
m_fatigue,
delta_threshold,
}
}
pub fn cycles_to_failure(&self, delta_range: f64, d_initial: f64) -> Option<f64> {
if delta_range <= self.delta_threshold {
return None;
}
let dd_dn = self.c_fatigue * delta_range.powf(self.m_fatigue)
/ self.mono.delta_failure.max(f64::EPSILON);
if dd_dn < f64::EPSILON {
return None;
}
Some((1.0 - d_initial.clamp(0.0, 1.0)) / dd_dn)
}
}
#[derive(Debug, Clone, Copy)]
pub struct FrictionalCzmParams {
pub normal: BilinearCzmParams,
pub shear: BilinearCzmParams,
pub friction_coefficient: f64,
}
impl FrictionalCzmParams {
pub fn new(
normal: BilinearCzmParams,
shear: BilinearCzmParams,
friction_coefficient: f64,
) -> Self {
Self {
normal,
shear,
friction_coefficient,
}
}
pub fn normal_traction(&self, gap: f64, k_contact: f64) -> f64 {
if gap < 0.0 {
k_contact * gap.abs()
} else {
self.normal.traction(gap)
}
}
pub fn shear_traction(&self, delta_t: f64, gap: f64, k_contact: f64) -> f64 {
let t_coh = self.shear.traction(delta_t);
if gap < 0.0 {
let t_n = k_contact * gap.abs();
t_coh + self.friction_coefficient * t_n
} else {
t_coh
}
}
pub fn max_shear_capacity(&self, t_normal_contact: f64) -> f64 {
self.shear.traction_max + self.friction_coefficient * t_normal_contact
}
}
#[derive(Debug, Clone, Copy)]
pub struct MixedModeCriterion {
pub t_n_crit: f64,
pub t_t_crit: f64,
pub t_3_crit: f64,
}
impl MixedModeCriterion {
pub fn new(t_n_crit: f64, t_t_crit: f64, t_3_crit: f64) -> Self {
Self {
t_n_crit,
t_t_crit,
t_3_crit,
}
}
pub fn interaction_index(&self, t_n: f64, t_t: f64, t_3: f64) -> f64 {
let tn_contrib = if t_n > 0.0 {
(t_n / self.t_n_crit.max(f64::EPSILON)).powi(2)
} else {
0.0
};
let tt_contrib = (t_t / self.t_t_crit.max(f64::EPSILON)).powi(2);
let t3_contrib = (t_3 / self.t_3_crit.max(f64::EPSILON)).powi(2);
(tn_contrib + tt_contrib + t3_contrib).sqrt()
}
pub fn is_initiated(&self, t_n: f64, t_t: f64, t_3: f64) -> bool {
self.interaction_index(t_n, t_t, t_3) >= 1.0
}
pub fn mixed_mode_ratio(g1: f64, g2: f64) -> f64 {
let g_total = g1 + g2;
if g_total < f64::EPSILON {
return 0.0;
}
g2 / g_total
}
}
#[derive(Debug, Clone, Copy)]
pub struct ViscousCzmParams {
pub base: BilinearCzmParams,
pub viscosity: f64,
}
impl ViscousCzmParams {
pub fn new(base: BilinearCzmParams, viscosity: f64) -> Self {
Self { base, viscosity }
}
pub fn traction(&self, delta: f64, delta_dot: f64) -> f64 {
self.base.traction(delta) + self.viscosity * delta_dot
}
pub fn viscous_traction(&self, delta_dot: f64) -> f64 {
self.viscosity * delta_dot
}
pub fn viscous_dissipation(&self, delta_dot: f64, dt: f64) -> f64 {
self.viscosity * delta_dot * delta_dot * dt
}
pub fn consistent_tangent(&self, delta: f64, dt: f64) -> f64 {
let k_coh = self.base.secant_stiffness(delta);
k_coh + self.viscosity / dt.max(f64::EPSILON)
}
}
#[derive(Debug, Clone, Copy)]
pub struct RCurveModel {
pub k_init: f64,
pub k_steady: f64,
pub a_scale: f64,
}
impl RCurveModel {
pub fn new(k_init: f64, k_steady: f64, a_scale: f64) -> Self {
Self {
k_init,
k_steady,
a_scale,
}
}
pub fn k_resistance(&self, delta_a: f64) -> f64 {
let da = delta_a.max(0.0);
let a0 = self.a_scale.max(f64::EPSILON);
self.k_init + (self.k_steady - self.k_init) * (1.0 - (-da / a0).exp())
}
pub fn propagates(&self, k_applied: f64, delta_a: f64) -> bool {
k_applied >= self.k_resistance(delta_a)
}
pub fn stable_crack_extension(&self, k_applied: f64, da_max: f64, tol: f64) -> Option<f64> {
if k_applied < self.k_init {
return Some(0.0);
}
if k_applied >= self.k_steady {
return None;
}
let mut lo = 0.0_f64;
let mut hi = da_max;
for _ in 0..100 {
let mid = 0.5 * (lo + hi);
let kr = self.k_resistance(mid);
if (kr - k_applied).abs() < tol {
return Some(mid);
}
if kr < k_applied {
lo = mid;
} else {
hi = mid;
}
}
Some(0.5 * (lo + hi))
}
}
#[derive(Debug, Clone)]
pub struct FatigueCzmState {
pub d_mono: f64,
pub d_fatigue: f64,
pub n_cycles: u64,
pub delta_max_cycle: f64,
pub delta_min_cycle: f64,
pub delta_historic_max: f64,
}
impl FatigueCzmState {
pub fn new() -> Self {
Self {
d_mono: 0.0,
d_fatigue: 0.0,
n_cycles: 0,
delta_max_cycle: 0.0,
delta_min_cycle: f64::MAX,
delta_historic_max: 0.0,
}
}
pub fn total_damage(&self) -> f64 {
(self.d_mono + self.d_fatigue).min(1.0)
}
pub fn is_failed(&self) -> bool {
self.total_damage() >= 1.0
}
pub fn record_separation(&mut self, delta: f64) {
if delta > self.delta_max_cycle {
self.delta_max_cycle = delta;
}
if delta < self.delta_min_cycle {
self.delta_min_cycle = delta;
}
if delta > self.delta_historic_max {
self.delta_historic_max = delta;
}
}
pub fn advance_cycle(&mut self, c_fatigue: f64, m_fatigue: f64, delta_failure: f64) {
self.n_cycles += 1;
let delta_min = if self.delta_min_cycle == f64::MAX {
0.0
} else {
self.delta_min_cycle
};
let delta_range = (self.delta_max_cycle - delta_min).max(0.0);
let dd_dn = c_fatigue * delta_range.powf(m_fatigue) / delta_failure.max(f64::EPSILON);
self.d_fatigue = (self.d_fatigue + dd_dn).min(1.0);
self.delta_max_cycle = 0.0;
self.delta_min_cycle = f64::MAX;
}
pub fn update_monotonic(&mut self, delta: f64, params: &BilinearCzmParams) {
let d_new = params.damage(delta);
if d_new > self.d_mono {
self.d_mono = d_new;
}
}
}