use serde::Deserialize;
pub fn per_bond_uniform_samples(tag_a: u32, tag_b: u32, seed: u64) -> [f64; 4] {
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
let (lo, hi) = if tag_a <= tag_b {
(tag_a, tag_b)
} else {
(tag_b, tag_a)
};
let mut bond_seed = seed;
bond_seed = splitmix64(bond_seed ^ (lo as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
bond_seed = splitmix64(bond_seed ^ (hi as u64).wrapping_mul(0xBF58_476D_1CE4_E5B9));
let mut rng = SmallRng::seed_from_u64(bond_seed);
[
rng.random_range(1.0e-15..1.0 - 1.0e-15),
rng.random_range(1.0e-15..1.0 - 1.0e-15),
rng.random_range(1.0e-15..1.0 - 1.0e-15),
rng.random_range(1.0e-15..1.0 - 1.0e-15),
]
}
#[inline]
fn splitmix64(mut x: u64) -> u64 {
x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
x ^ (x >> 31)
}
#[derive(Clone, Copy, Debug)]
pub struct BondGeom {
pub r_b: f64,
pub area: f64,
pub iben: f64,
pub jpol: f64,
pub l0: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct BondLoads {
pub f_n: f64,
pub f_t_mag: f64,
pub m_bend_mag: f64,
pub m_tor_mag: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct BondKinematics {
pub eps_axial: f64,
pub gamma_shear: f64,
pub kappa_bend: f64,
pub kappa_tor: f64,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct BondThresholds {
pub t: [f64; 4],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BreakMode {
Tensile,
Shear,
Interaction,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ThresholdDistribution {
Constant {
value: f64,
},
Weibull {
mean: f64,
m: f64,
l_calib: f64,
#[serde(default)]
l_min: f64,
},
}
impl ThresholdDistribution {
pub fn sample(&self, l_bond: f64, u: f64) -> f64 {
match *self {
Self::Constant { value } => value,
Self::Weibull {
mean,
m,
l_calib,
l_min,
} => {
let l_eff = l_bond.max(l_min).max(f64::MIN_POSITIVE);
let size_factor = (l_calib / l_eff).powf(1.0 / m);
let u_clamped = u.clamp(1e-15, 1.0 - 1e-15);
let scale = mean / gamma_lanczos(1.0 + 1.0 / m);
scale * size_factor * (-((1.0 - u_clamped).ln())).powf(1.0 / m)
}
}
}
}
pub trait BreakageCriterion: Send + Sync + std::fmt::Debug {
fn num_thresholds(&self) -> usize;
fn sample(&self, l_bond: f64, u: [f64; 4]) -> BondThresholds;
fn check(
&self,
geom: &BondGeom,
loads: &BondLoads,
kin: &BondKinematics,
thr: &BondThresholds,
) -> Option<BreakMode>;
}
#[derive(Clone, Debug, Default)]
pub struct Unbreakable;
impl BreakageCriterion for Unbreakable {
fn num_thresholds(&self) -> usize {
0
}
fn sample(&self, _: f64, _: [f64; 4]) -> BondThresholds {
BondThresholds::default()
}
fn check(
&self,
_: &BondGeom,
_: &BondLoads,
_: &BondKinematics,
_: &BondThresholds,
) -> Option<BreakMode> {
None
}
}
macro_rules! impl_two_branch {
($name:ident, $tensile_expr:expr, $shear_expr:expr) => {
impl BreakageCriterion for $name {
fn num_thresholds(&self) -> usize {
2
}
fn sample(&self, l_bond: f64, u: [f64; 4]) -> BondThresholds {
let t0 = self.tensile.sample(l_bond, u[0]);
let t1 = match &self.shear {
Some(d) => d.sample(l_bond, u[1]),
None => f64::INFINITY,
};
BondThresholds {
t: [t0, t1, 0.0, 0.0],
}
}
fn check(
&self,
geom: &BondGeom,
loads: &BondLoads,
kin: &BondKinematics,
thr: &BondThresholds,
) -> Option<BreakMode> {
let tensile_val: f64 = $tensile_expr(geom, loads, kin);
if tensile_val > thr.t[0] {
return Some(BreakMode::Tensile);
}
let shear_val: f64 = $shear_expr(geom, loads, kin);
if shear_val > thr.t[1] {
return Some(BreakMode::Shear);
}
None
}
}
};
}
#[derive(Clone, Debug)]
pub struct AxialForce {
pub tensile: ThresholdDistribution,
pub shear: Option<ThresholdDistribution>,
}
impl_two_branch!(
AxialForce,
|_g: &BondGeom, l: &BondLoads, _k: &BondKinematics| l.f_n.max(0.0),
|_g: &BondGeom, l: &BondLoads, _k: &BondKinematics| l.f_t_mag
);
#[derive(Clone, Debug)]
pub struct AxialStress {
pub tensile: ThresholdDistribution,
pub shear: Option<ThresholdDistribution>,
}
impl_two_branch!(
AxialStress,
|g: &BondGeom, l: &BondLoads, _k: &BondKinematics| if g.area > 0.0 {
l.f_n.max(0.0) / g.area
} else {
0.0
},
|g: &BondGeom, l: &BondLoads, _k: &BondKinematics| if g.area > 0.0 {
l.f_t_mag / g.area
} else {
0.0
}
);
#[derive(Clone, Debug)]
pub struct AxialStrain {
pub tensile: ThresholdDistribution,
pub shear: Option<ThresholdDistribution>,
}
impl_two_branch!(
AxialStrain,
|_g: &BondGeom, _l: &BondLoads, k: &BondKinematics| k.eps_axial.max(0.0),
|_g: &BondGeom, _l: &BondLoads, k: &BondKinematics| k.gamma_shear
);
#[derive(Clone, Debug)]
pub struct CombinedStress {
pub tensile: ThresholdDistribution,
pub shear: Option<ThresholdDistribution>,
}
impl_two_branch!(
CombinedStress,
|g: &BondGeom, l: &BondLoads, _k: &BondKinematics| {
let axial = if g.area > 0.0 {
l.f_n.max(0.0) / g.area
} else {
0.0
};
let bend = if g.iben > 0.0 {
g.r_b * l.m_bend_mag / g.iben
} else {
0.0
};
axial + bend
},
|g: &BondGeom, l: &BondLoads, _k: &BondKinematics| {
let shear = if g.area > 0.0 {
l.f_t_mag / g.area
} else {
0.0
};
let tor = if g.jpol > 0.0 {
g.r_b * l.m_tor_mag / g.jpol
} else {
0.0
};
shear + tor
}
);
#[derive(Clone, Debug)]
pub struct CombinedStrain {
pub tensile: ThresholdDistribution,
pub shear: Option<ThresholdDistribution>,
}
impl_two_branch!(
CombinedStrain,
|g: &BondGeom, _l: &BondLoads, k: &BondKinematics| k.eps_axial.max(0.0) + g.r_b * k.kappa_bend,
|g: &BondGeom, _l: &BondLoads, k: &BondKinematics| k.gamma_shear + g.r_b * k.kappa_tor
);
#[derive(Clone, Debug)]
pub struct InteractionLinearForce {
pub axial: Option<ThresholdDistribution>,
pub shear: Option<ThresholdDistribution>,
pub bending: Option<ThresholdDistribution>,
pub twist: Option<ThresholdDistribution>,
}
#[derive(Clone, Debug)]
pub struct InteractionLinearStress {
pub axial: Option<ThresholdDistribution>,
pub shear: Option<ThresholdDistribution>,
pub bending: Option<ThresholdDistribution>,
pub twist: Option<ThresholdDistribution>,
}
#[derive(Clone, Debug)]
pub struct InteractionLinearStrain {
pub axial: Option<ThresholdDistribution>,
pub shear: Option<ThresholdDistribution>,
pub bending: Option<ThresholdDistribution>,
pub twist: Option<ThresholdDistribution>,
}
macro_rules! impl_interaction_linear {
($name:ident, $axial:expr, $shear:expr, $bending:expr, $twist:expr) => {
impl BreakageCriterion for $name {
fn num_thresholds(&self) -> usize {
4
}
fn sample(&self, l_bond: f64, u: [f64; 4]) -> BondThresholds {
let s = |d: &Option<ThresholdDistribution>, ui: f64| {
d.as_ref()
.map(|x| x.sample(l_bond, ui))
.unwrap_or(f64::INFINITY)
};
BondThresholds {
t: [
s(&self.axial, u[0]),
s(&self.shear, u[1]),
s(&self.bending, u[2]),
s(&self.twist, u[3]),
],
}
}
fn check(
&self,
geom: &BondGeom,
loads: &BondLoads,
kin: &BondKinematics,
thr: &BondThresholds,
) -> Option<BreakMode> {
let mut sum = 0.0;
let v_axial: f64 = $axial(geom, loads, kin);
let v_shear: f64 = $shear(geom, loads, kin);
let v_bending: f64 = $bending(geom, loads, kin);
let v_twist: f64 = $twist(geom, loads, kin);
sum += v_axial / thr.t[0];
sum += v_shear / thr.t[1];
sum += v_bending / thr.t[2];
sum += v_twist / thr.t[3];
if sum >= 1.0 {
Some(BreakMode::Interaction)
} else {
None
}
}
}
};
}
impl_interaction_linear!(
InteractionLinearForce,
|_g: &BondGeom, l: &BondLoads, _k: &BondKinematics| l.f_n.max(0.0).abs(),
|_g: &BondGeom, l: &BondLoads, _k: &BondKinematics| l.f_t_mag,
|_g: &BondGeom, l: &BondLoads, _k: &BondKinematics| l.m_bend_mag,
|_g: &BondGeom, l: &BondLoads, _k: &BondKinematics| l.m_tor_mag
);
impl_interaction_linear!(
InteractionLinearStress,
|g: &BondGeom, l: &BondLoads, _k: &BondKinematics| if g.area > 0.0 {
l.f_n.max(0.0) / g.area
} else {
0.0
},
|g: &BondGeom, l: &BondLoads, _k: &BondKinematics| if g.area > 0.0 {
l.f_t_mag / g.area
} else {
0.0
},
|g: &BondGeom, l: &BondLoads, _k: &BondKinematics| if g.iben > 0.0 {
g.r_b * l.m_bend_mag / g.iben
} else {
0.0
},
|g: &BondGeom, l: &BondLoads, _k: &BondKinematics| if g.jpol > 0.0 {
g.r_b * l.m_tor_mag / g.jpol
} else {
0.0
}
);
impl_interaction_linear!(
InteractionLinearStrain,
|_g: &BondGeom, _l: &BondLoads, k: &BondKinematics| k.eps_axial.max(0.0),
|_g: &BondGeom, _l: &BondLoads, k: &BondKinematics| k.gamma_shear,
|g: &BondGeom, _l: &BondLoads, k: &BondKinematics| g.r_b * k.kappa_bend,
|g: &BondGeom, _l: &BondLoads, k: &BondKinematics| g.r_b * k.kappa_tor
);
#[derive(Deserialize, Clone, Debug)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum BreakageConfig {
Unbreakable,
AxialForce {
tensile: ThresholdDistribution,
#[serde(default)]
shear: Option<ThresholdDistribution>,
},
AxialStress {
tensile: ThresholdDistribution,
#[serde(default)]
shear: Option<ThresholdDistribution>,
},
AxialStrain {
tensile: ThresholdDistribution,
#[serde(default)]
shear: Option<ThresholdDistribution>,
},
CombinedStress {
tensile: ThresholdDistribution,
#[serde(default)]
shear: Option<ThresholdDistribution>,
},
CombinedStrain {
tensile: ThresholdDistribution,
#[serde(default)]
shear: Option<ThresholdDistribution>,
},
InteractionLinearForce {
#[serde(default)]
axial: Option<ThresholdDistribution>,
#[serde(default)]
shear: Option<ThresholdDistribution>,
#[serde(default)]
bending: Option<ThresholdDistribution>,
#[serde(default)]
twist: Option<ThresholdDistribution>,
},
InteractionLinearStress {
#[serde(default)]
axial: Option<ThresholdDistribution>,
#[serde(default)]
shear: Option<ThresholdDistribution>,
#[serde(default)]
bending: Option<ThresholdDistribution>,
#[serde(default)]
twist: Option<ThresholdDistribution>,
},
InteractionLinearStrain {
#[serde(default)]
axial: Option<ThresholdDistribution>,
#[serde(default)]
shear: Option<ThresholdDistribution>,
#[serde(default)]
bending: Option<ThresholdDistribution>,
#[serde(default)]
twist: Option<ThresholdDistribution>,
},
}
impl BreakageConfig {
pub fn build(&self) -> Box<dyn BreakageCriterion> {
match self {
Self::Unbreakable => Box::new(Unbreakable),
Self::AxialForce { tensile, shear } => Box::new(AxialForce {
tensile: tensile.clone(),
shear: shear.clone(),
}),
Self::AxialStress { tensile, shear } => Box::new(AxialStress {
tensile: tensile.clone(),
shear: shear.clone(),
}),
Self::AxialStrain { tensile, shear } => Box::new(AxialStrain {
tensile: tensile.clone(),
shear: shear.clone(),
}),
Self::CombinedStress { tensile, shear } => Box::new(CombinedStress {
tensile: tensile.clone(),
shear: shear.clone(),
}),
Self::CombinedStrain { tensile, shear } => Box::new(CombinedStrain {
tensile: tensile.clone(),
shear: shear.clone(),
}),
Self::InteractionLinearForce {
axial,
shear,
bending,
twist,
} => Box::new(InteractionLinearForce {
axial: axial.clone(),
shear: shear.clone(),
bending: bending.clone(),
twist: twist.clone(),
}),
Self::InteractionLinearStress {
axial,
shear,
bending,
twist,
} => Box::new(InteractionLinearStress {
axial: axial.clone(),
shear: shear.clone(),
bending: bending.clone(),
twist: twist.clone(),
}),
Self::InteractionLinearStrain {
axial,
shear,
bending,
twist,
} => Box::new(InteractionLinearStrain {
axial: axial.clone(),
shear: shear.clone(),
bending: bending.clone(),
twist: twist.clone(),
}),
}
}
}
fn gamma_lanczos(z: f64) -> f64 {
const G: f64 = 7.0;
const P: [f64; 9] = [
0.999_999_999_999_809_93,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_13,
-176.615_029_162_140_59,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_571_6e-6,
1.505_632_735_149_311_6e-7,
];
if z < 0.5 {
std::f64::consts::PI / ((std::f64::consts::PI * z).sin() * gamma_lanczos(1.0 - z))
} else {
let z = z - 1.0;
let mut x = P[0];
for (i, &p) in P.iter().enumerate().skip(1) {
x += p / (z + i as f64);
}
let t = z + G + 0.5;
(2.0 * std::f64::consts::PI).sqrt() * t.powf(z + 0.5) * (-t).exp() * x
}
}
#[cfg(test)]
mod tests {
use super::*;
fn geom() -> BondGeom {
let r_b: f64 = 1.0e-3;
BondGeom {
r_b,
area: std::f64::consts::PI * r_b * r_b,
iben: 0.25 * std::f64::consts::PI * r_b.powi(4),
jpol: 0.5 * std::f64::consts::PI * r_b.powi(4),
l0: 2.0e-3,
}
}
fn zero_loads() -> BondLoads {
BondLoads {
f_n: 0.0,
f_t_mag: 0.0,
m_bend_mag: 0.0,
m_tor_mag: 0.0,
}
}
fn zero_kin() -> BondKinematics {
BondKinematics {
eps_axial: 0.0,
gamma_shear: 0.0,
kappa_bend: 0.0,
kappa_tor: 0.0,
}
}
#[test]
fn per_bond_samples_are_deterministic_in_pair_and_seed() {
let a = per_bond_uniform_samples(3, 17, 42);
let b = per_bond_uniform_samples(3, 17, 42);
assert_eq!(a, b);
for u in a {
assert!(u > 0.0 && u < 1.0, "sample {} outside (0,1)", u);
}
}
#[test]
fn per_bond_samples_are_canonical_in_tag_order() {
let a = per_bond_uniform_samples(7, 99, 12345);
let b = per_bond_uniform_samples(99, 7, 12345);
assert_eq!(a, b);
}
#[test]
fn per_bond_samples_differ_across_distinct_pairs() {
let p01 = per_bond_uniform_samples(0, 1, 1);
let p12 = per_bond_uniform_samples(1, 2, 1);
let p23 = per_bond_uniform_samples(2, 3, 1);
assert_ne!(p01, p12);
assert_ne!(p12, p23);
assert_ne!(p01, p23);
}
#[test]
fn per_bond_samples_change_with_seed() {
let s1 = per_bond_uniform_samples(5, 10, 1);
let s2 = per_bond_uniform_samples(5, 10, 2);
let s3 = per_bond_uniform_samples(5, 10, 999_999);
assert_ne!(s1, s2);
assert_ne!(s2, s3);
assert_ne!(s1, s3);
}
#[test]
fn per_bond_samples_roughly_uniform_over_many_pairs() {
let n_pairs = 4000;
let mut bins = [0usize; 10];
for pair_idx in 0..n_pairs {
let a = pair_idx as u32;
let b = (pair_idx + 1) as u32;
let u = per_bond_uniform_samples(a, b, 7);
let decile = (u[0] * 10.0).floor() as usize;
let decile = decile.min(9);
bins[decile] += 1;
}
let expected = n_pairs / 10;
let tolerance = (n_pairs as f64 * 0.10) as usize; for (i, &count) in bins.iter().enumerate() {
assert!(
count.abs_diff(expected) <= tolerance,
"decile {i}: count {count} too far from expected {expected}"
);
}
}
#[test]
fn per_bond_samples_replicate_under_simulated_mpi_partition() {
let bonds = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)];
let seed = 0xDEADBEEFu64;
let forward: Vec<[f64; 4]> = bonds
.iter()
.map(|(a, b)| per_bond_uniform_samples(*a, *b, seed))
.collect();
let reverse: Vec<[f64; 4]> = bonds
.iter()
.rev()
.map(|(a, b)| per_bond_uniform_samples(*a, *b, seed))
.collect();
for i in 0..bonds.len() {
assert_eq!(forward[i], reverse[bonds.len() - 1 - i]);
}
}
#[test]
fn gamma_reference_values() {
assert!((gamma_lanczos(1.0) - 1.0).abs() < 1e-12);
assert!((gamma_lanczos(2.0) - 1.0).abs() < 1e-12);
assert!((gamma_lanczos(1.5) - (std::f64::consts::PI.sqrt() / 2.0)).abs() < 1e-12);
assert!((gamma_lanczos(5.0) - 24.0).abs() < 1e-9);
}
#[test]
fn constant_distribution_passes_through() {
let d = ThresholdDistribution::Constant { value: 1.234 };
for u in [0.01, 0.5, 0.99] {
assert_eq!(d.sample(1.0e-3, u), 1.234);
}
}
#[test]
fn weibull_size_effect_reduces_threshold_for_longer_bond() {
let d = ThresholdDistribution::Weibull {
mean: 1.0e9,
m: 5.0,
l_calib: 1.0e-3,
l_min: 0.0,
};
let short = d.sample(1.0e-3, 0.5);
let long = d.sample(10.0e-3, 0.5);
assert!(
long < short,
"longer bond ({:.3e}) should be weaker than shorter ({:.3e})",
long,
short
);
let ratio = long / short;
assert!((ratio - 10f64.powf(-0.2)).abs() < 1e-12);
}
#[test]
fn weibull_mean_recovered_at_uniform_l() {
let d = ThresholdDistribution::Weibull {
mean: 5.0e7,
m: 5.0,
l_calib: 2.0e-3,
l_min: 0.0,
};
let u = 1.0 - (-1.0_f64).exp(); let v = d.sample(2.0e-3, u);
let expected = 5.0e7 / gamma_lanczos(1.0 + 1.0 / 5.0);
assert!((v - expected).abs() / expected < 1e-12);
}
#[test]
fn weibull_l_min_floor() {
let d = ThresholdDistribution::Weibull {
mean: 1.0e9,
m: 5.0,
l_calib: 1.0e-3,
l_min: 5.0e-3,
};
let very_short = d.sample(1.0e-9, 0.5);
let at_floor = d.sample(5.0e-3, 0.5);
assert_eq!(very_short, at_floor);
}
#[test]
fn unbreakable_never_breaks() {
let c = Unbreakable;
let thr = BondThresholds::default();
let g = geom();
let l = BondLoads {
f_n: 1.0e30,
f_t_mag: 1.0e30,
m_bend_mag: 1.0e30,
m_tor_mag: 1.0e30,
};
let k = BondKinematics {
eps_axial: 10.0,
gamma_shear: 10.0,
kappa_bend: 1.0e6,
kappa_tor: 1.0e6,
};
assert!(c.check(&g, &l, &k, &thr).is_none());
}
#[test]
fn axial_force_tensile_break() {
let c = AxialForce {
tensile: ThresholdDistribution::Constant { value: 100.0 },
shear: None,
};
let thr = c.sample(geom().l0, [0.5; 4]);
let g = geom();
let l = BondLoads {
f_n: 150.0,
..zero_loads()
};
assert_eq!(c.check(&g, &l, &zero_kin(), &thr), Some(BreakMode::Tensile));
let l = BondLoads {
f_n: -150.0,
..zero_loads()
};
assert_eq!(c.check(&g, &l, &zero_kin(), &thr), None);
}
#[test]
fn axial_stress_threshold() {
let c = AxialStress {
tensile: ThresholdDistribution::Constant { value: 1.0e6 },
shear: Some(ThresholdDistribution::Constant { value: 5.0e5 }),
};
let thr = c.sample(geom().l0, [0.5; 4]);
let g = geom();
let l = BondLoads {
f_n: 0.5e6 * g.area,
..zero_loads()
};
assert_eq!(c.check(&g, &l, &zero_kin(), &thr), None);
let l = BondLoads {
f_n: 2.0e6 * g.area,
..zero_loads()
};
assert_eq!(c.check(&g, &l, &zero_kin(), &thr), Some(BreakMode::Tensile));
let l = BondLoads {
f_t_mag: 1.0e6 * g.area,
..zero_loads()
};
assert_eq!(c.check(&g, &l, &zero_kin(), &thr), Some(BreakMode::Shear));
}
#[test]
fn axial_strain_threshold() {
let c = AxialStrain {
tensile: ThresholdDistribution::Constant { value: 0.02 },
shear: None,
};
let thr = c.sample(geom().l0, [0.5; 4]);
let g = geom();
let kin_under = BondKinematics {
eps_axial: 0.015,
..zero_kin()
};
let kin_over = BondKinematics {
eps_axial: 0.025,
..zero_kin()
};
assert_eq!(c.check(&g, &zero_loads(), &kin_under, &thr), None);
assert_eq!(
c.check(&g, &zero_loads(), &kin_over, &thr),
Some(BreakMode::Tensile)
);
}
#[test]
fn combined_stress_matches_guo_eq16() {
let c = CombinedStress {
tensile: ThresholdDistribution::Constant { value: 1.0e7 },
shear: None,
};
let thr = c.sample(geom().l0, [0.5; 4]);
let g = geom();
let l_axial_only = BondLoads {
f_n: 0.5e7 * g.area,
..zero_loads()
};
assert_eq!(c.check(&g, &l_axial_only, &zero_kin(), &thr), None);
let l_bend_only = BondLoads {
m_bend_mag: 0.5e7 * g.iben / g.r_b,
..zero_loads()
};
assert_eq!(c.check(&g, &l_bend_only, &zero_kin(), &thr), None);
let l_both = BondLoads {
f_n: 0.51e7 * g.area,
m_bend_mag: 0.5e7 * g.iben / g.r_b,
..zero_loads()
};
assert_eq!(
c.check(&g, &l_both, &zero_kin(), &thr),
Some(BreakMode::Tensile)
);
}
#[test]
fn combined_strain_matches_migration_doc_eq17() {
let c = CombinedStrain {
tensile: ThresholdDistribution::Constant { value: 0.02 },
shear: None,
};
let thr = c.sample(geom().l0, [0.5; 4]);
let g = geom();
let half_axial = BondKinematics {
eps_axial: 0.011,
..zero_kin()
};
let half_bend = BondKinematics {
kappa_bend: 0.011 / g.r_b,
..zero_kin()
};
let combined = BondKinematics {
eps_axial: 0.011,
kappa_bend: 0.011 / g.r_b,
..zero_kin()
};
assert_eq!(c.check(&g, &zero_loads(), &half_axial, &thr), None);
assert_eq!(c.check(&g, &zero_loads(), &half_bend, &thr), None);
assert_eq!(
c.check(&g, &zero_loads(), &combined, &thr),
Some(BreakMode::Tensile)
);
}
#[test]
fn interaction_linear_force_sums_to_one() {
let c = InteractionLinearForce {
axial: Some(ThresholdDistribution::Constant { value: 1.0 }),
shear: Some(ThresholdDistribution::Constant { value: 1.0 }),
bending: Some(ThresholdDistribution::Constant { value: 1.0 }),
twist: Some(ThresholdDistribution::Constant { value: 1.0 }),
};
let thr = c.sample(geom().l0, [0.5; 4]);
let g = geom();
let l_below = BondLoads {
f_n: 0.2,
f_t_mag: 0.2,
m_bend_mag: 0.2,
m_tor_mag: 0.2,
};
assert_eq!(c.check(&g, &l_below, &zero_kin(), &thr), None);
let l_above = BondLoads {
f_n: 0.3,
f_t_mag: 0.3,
m_bend_mag: 0.3,
m_tor_mag: 0.3,
};
assert_eq!(
c.check(&g, &l_above, &zero_kin(), &thr),
Some(BreakMode::Interaction)
);
}
#[test]
fn interaction_linear_disabled_channel_drops_out() {
let c = InteractionLinearForce {
axial: Some(ThresholdDistribution::Constant { value: 10.0 }),
shear: None,
bending: None,
twist: None,
};
let thr = c.sample(geom().l0, [0.5; 4]);
let g = geom();
let l = BondLoads {
f_n: 5.0,
f_t_mag: 1.0e6,
m_bend_mag: 1.0e6,
m_tor_mag: 1.0e6,
};
assert_eq!(c.check(&g, &l, &zero_kin(), &thr), None);
let l = BondLoads { f_n: 11.0, ..l };
assert_eq!(
c.check(&g, &l, &zero_kin(), &thr),
Some(BreakMode::Interaction)
);
}
#[test]
fn interaction_linear_stress_recovers_clemmer_bpm_rotational() {
let c = InteractionLinearStress {
axial: Some(ThresholdDistribution::Constant { value: 4.0e6 }),
shear: Some(ThresholdDistribution::Constant { value: 4.0e6 }),
bending: Some(ThresholdDistribution::Constant { value: 4.0e6 }),
twist: Some(ThresholdDistribution::Constant { value: 4.0e6 }),
};
let thr = c.sample(geom().l0, [0.5; 4]);
let g = geom();
let l = BondLoads {
f_n: 1.0e6 * g.area,
f_t_mag: 1.0e6 * g.area,
m_bend_mag: 1.0e6 * g.iben / g.r_b,
m_tor_mag: 1.0e6 * g.jpol / g.r_b,
};
assert_eq!(
c.check(&g, &l, &zero_kin(), &thr),
Some(BreakMode::Interaction)
);
}
#[test]
fn interaction_linear_strain_uses_kinematics() {
let c = InteractionLinearStrain {
axial: Some(ThresholdDistribution::Constant { value: 0.01 }),
shear: Some(ThresholdDistribution::Constant { value: 0.01 }),
bending: Some(ThresholdDistribution::Constant { value: 0.01 }),
twist: Some(ThresholdDistribution::Constant { value: 0.01 }),
};
let thr = c.sample(geom().l0, [0.5; 4]);
let g = geom();
let k = BondKinematics {
eps_axial: 0.003,
gamma_shear: 0.003,
kappa_bend: 0.003 / g.r_b,
kappa_tor: 0.003 / g.r_b,
};
assert_eq!(
c.check(&g, &zero_loads(), &k, &thr),
Some(BreakMode::Interaction)
);
}
}