#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use std::f64::consts::PI;
fn mat3_identity() -> [[f64; 3]; 3] {
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
}
fn mat3_trace(m: &[[f64; 3]; 3]) -> f64 {
m[0][0] + m[1][1] + m[2][2]
}
fn mat3_det(m: &[[f64; 3]; 3]) -> f64 {
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])
}
fn mat3_transpose(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut t = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
t[i][j] = m[j][i];
}
}
t
}
fn mat3_mul(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut c = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
fn invariants(f: &[[f64; 3]; 3]) -> (f64, f64, f64) {
let ft = mat3_transpose(f);
let c = mat3_mul(&ft, f);
let i1 = mat3_trace(&c);
let c2 = mat3_mul(&c, &c);
let i2 = 0.5 * (i1 * i1 - mat3_trace(&c2));
let j = mat3_det(f);
(i1, i2, j)
}
#[derive(Debug, Clone)]
pub struct SoftTissue {
pub c: f64,
pub b: f64,
pub k1: f64,
pub k2: f64,
pub elastin_modulus: f64,
pub collagen_fraction: f64,
pub elastin_fraction: f64,
pub fiber_direction: [f64; 3],
pub density: f64,
}
impl SoftTissue {
pub fn skin_dermis() -> Self {
Self {
c: 1200.0,
b: 12.0,
k1: 1500.0,
k2: 10.0,
elastin_modulus: 300e3,
collagen_fraction: 0.35,
elastin_fraction: 0.04,
fiber_direction: [1.0, 0.0, 0.0],
density: 1100.0,
}
}
pub fn myocardium() -> Self {
Self {
c: 500.0,
b: 8.0,
k1: 2000.0,
k2: 15.0,
elastin_modulus: 200e3,
collagen_fraction: 0.25,
elastin_fraction: 0.03,
fiber_direction: [1.0, 0.0, 0.0],
density: 1050.0,
}
}
pub fn strain_energy_density(&self, f: &[[f64; 3]; 3]) -> f64 {
let (i1, _i2, _j) = invariants(f);
let ft = mat3_transpose(f);
let c_mat = mat3_mul(&ft, f);
let a = &self.fiber_direction;
let ca = [
c_mat[0][0] * a[0] + c_mat[0][1] * a[1] + c_mat[0][2] * a[2],
c_mat[1][0] * a[0] + c_mat[1][1] * a[1] + c_mat[1][2] * a[2],
c_mat[2][0] * a[0] + c_mat[2][1] * a[1] + c_mat[2][2] * a[2],
];
let i4 = a[0] * ca[0] + a[1] * ca[1] + a[2] * ca[2];
let w_matrix = self.c / 2.0 * ((self.b * (i1 - 3.0)).exp() - 1.0);
let w_fiber = if i4 > 1.0 {
let xi = i4 - 1.0;
self.k1 / (2.0 * self.k2) * ((self.k2 * xi * xi).exp() - 1.0)
} else {
0.0
};
let w_elastin = self.elastin_modulus / 2.0 * (i1 - 3.0);
w_matrix + w_fiber + w_elastin
}
pub fn small_strain_modulus(&self) -> f64 {
let dw_di1 = self.c * self.b + self.elastin_modulus;
4.0 * dw_di1
}
pub fn toe_region_stretch(&self) -> f64 {
1.0 + (1.0 / (2.0 * self.k2)).sqrt()
}
pub fn uniaxial_cauchy_stress(&self, lambda: f64) -> f64 {
if lambda <= 0.0 {
return 0.0;
}
let lambda_perp = 1.0 / lambda.sqrt();
let i1 = lambda * lambda + 2.0 * lambda_perp * lambda_perp;
let i4 = lambda * lambda;
let dw_di1 = self.c * self.b * (self.b * (i1 - 3.0)).exp() + self.elastin_modulus;
let dw_di4 = if i4 > 1.0 {
let xi = i4 - 1.0;
self.k1 * xi * (self.k2 * xi * xi).exp()
} else {
0.0
};
2.0 * dw_di1 * (lambda * lambda - lambda_perp * lambda_perp)
+ 2.0 * lambda * lambda * dw_di4
}
}
#[derive(Debug, Clone)]
pub struct BoneMaterial {
pub e_longitudinal: f64,
pub e_transverse: f64,
pub g_lt: f64,
pub nu_lt: f64,
pub nu_tt: f64,
pub density: f64,
pub uts: f64,
pub ucs: f64,
pub volume_fraction: f64,
pub is_cortical: bool,
}
impl BoneMaterial {
pub fn cortical_femur() -> Self {
Self {
e_longitudinal: 20.0e9,
e_transverse: 12.0e9,
g_lt: 4.5e9,
nu_lt: 0.29,
nu_tt: 0.63,
density: 1900.0,
uts: 120.0e6,
ucs: 180.0e6,
volume_fraction: 1.0,
is_cortical: true,
}
}
pub fn cancellous_vertebra() -> Self {
Self {
e_longitudinal: 0.3e9,
e_transverse: 0.15e9,
g_lt: 0.05e9,
nu_lt: 0.25,
nu_tt: 0.40,
density: 300.0,
uts: 5.0e6,
ucs: 8.0e6,
volume_fraction: 0.15,
is_cortical: false,
}
}
pub fn voigt_upper_bound(&self, e_phase2: f64) -> f64 {
self.volume_fraction * self.e_longitudinal + (1.0 - self.volume_fraction) * e_phase2
}
pub fn reuss_lower_bound(&self, e_phase2: f64) -> f64 {
let vf = self.volume_fraction;
1.0 / (vf / self.e_longitudinal + (1.0 - vf) / e_phase2)
}
pub fn hashin_shtrikman_estimate(&self, e_phase2: f64) -> f64 {
0.5 * (self.voigt_upper_bound(e_phase2) + self.reuss_lower_bound(e_phase2))
}
pub fn modulus_from_ash_density(ash_density_g_cm3: f64) -> f64 {
6.95e9 * ash_density_g_cm3.powf(1.49)
}
pub fn anisotropy_ratio(&self) -> f64 {
self.e_longitudinal / self.e_transverse
}
pub fn longitudinal_wave_speed(&self) -> f64 {
(self.e_longitudinal / self.density).sqrt()
}
pub fn shear_wave_speed(&self) -> f64 {
(self.g_lt / self.density).sqrt()
}
pub fn compressive_yield_strain(&self) -> f64 {
self.ucs / self.e_longitudinal
}
}
#[derive(Debug, Clone)]
pub struct CartilageModel {
pub aggregate_modulus: f64,
pub e_solid: f64,
pub nu_solid: f64,
pub permeability: f64,
pub thickness: f64,
pub porosity: f64,
pub fixed_charge_density: f64,
pub density: f64,
}
impl CartilageModel {
pub fn articular_cartilage() -> Self {
Self::human_knee_cartilage()
}
pub fn human_knee_cartilage() -> Self {
let e_solid = 0.8e6;
let nu = 0.15;
let ha = e_solid * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu));
Self {
aggregate_modulus: ha,
e_solid,
nu_solid: nu,
permeability: 1.0e-15,
thickness: 3.0e-3,
porosity: 0.75,
fixed_charge_density: 0.1,
density: 1100.0,
}
}
pub fn compute_aggregate_modulus(e: f64, nu: f64) -> f64 {
e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu))
}
pub fn creep_time_constant(&self) -> f64 {
let h = self.thickness;
h * h / (self.aggregate_modulus * self.permeability)
}
pub fn creep_compliance(&self, t: f64) -> f64 {
let tau = self.creep_time_constant();
let c_inf = 1.0 / self.aggregate_modulus;
let c0 = 1.0 / (self.aggregate_modulus + 4.0 / 3.0 * self.e_solid);
c_inf - (c_inf - c0) * (-t / tau).exp()
}
pub fn donnan_swelling_pressure(&self, c_ext: f64) -> f64 {
let rt = 2436.0;
let cf = self.fixed_charge_density;
rt * ((cf * cf / 4.0 + c_ext * c_ext).sqrt() - c_ext)
}
pub fn darcy_fluid_flux(&self, dp_dz: f64) -> f64 {
self.permeability * dp_dz
}
pub fn strain_dependent_permeability(&self, e: f64) -> f64 {
let m = 4.0; self.permeability * (m * e).exp()
}
}
#[derive(Debug, Clone)]
pub struct VascularWall {
pub inner_radius: f64,
pub outer_radius: f64,
pub opening_angle: f64,
pub intima_thickness: f64,
pub media_thickness: f64,
pub adventitia_thickness: f64,
pub c10: f64,
pub k1: f64,
pub k2: f64,
pub fiber_angle: f64,
pub collagen_fraction: f64,
pub smooth_muscle_fraction: f64,
pub density: f64,
}
impl VascularWall {
pub fn human_aorta() -> Self {
Self {
inner_radius: 12.5e-3,
outer_radius: 15.0e-3,
opening_angle: 100.0_f64.to_radians(),
intima_thickness: 0.3e-3,
media_thickness: 1.5e-3,
adventitia_thickness: 0.7e-3,
c10: 3000.0,
k1: 2000.0,
k2: 11.0,
fiber_angle: 49.0_f64.to_radians(),
collagen_fraction: 0.22,
smooth_muscle_fraction: 0.15,
density: 1050.0,
}
}
pub fn coronary_artery() -> Self {
Self {
inner_radius: 1.5e-3,
outer_radius: 2.2e-3,
opening_angle: 115.0_f64.to_radians(),
intima_thickness: 0.1e-3,
media_thickness: 0.5e-3,
adventitia_thickness: 0.1e-3,
c10: 5000.0,
k1: 3500.0,
k2: 14.0,
fiber_angle: 44.0_f64.to_radians(),
collagen_fraction: 0.28,
smooth_muscle_fraction: 0.20,
density: 1050.0,
}
}
pub fn wall_thickness(&self) -> f64 {
self.outer_radius - self.inner_radius
}
pub fn residual_stretch_circumferential(&self, r: f64) -> f64 {
let alpha = self.opening_angle;
let ri = self.inner_radius;
let ro = self.outer_radius;
let r_mid = 0.5 * (ri + ro);
(r / r_mid) * (PI / (PI - alpha))
}
pub fn hgo_strain_energy(&self, f: &[[f64; 3]; 3]) -> f64 {
let (i1, _i2, j) = invariants(f);
let ft = mat3_transpose(f);
let c_mat = mat3_mul(&ft, f);
let cos_a = self.fiber_angle.cos();
let sin_a = self.fiber_angle.sin();
let a1 = [cos_a, sin_a, 0.0];
let i4 = a1[0] * (c_mat[0][0] * a1[0] + c_mat[0][1] * a1[1])
+ a1[1] * (c_mat[1][0] * a1[0] + c_mat[1][1] * a1[1]);
let d = 1.0 / (2.0 * self.c10); let w_vol = (j - 1.0).powi(2) / (2.0 * d);
let j_23 = j.powf(-2.0 / 3.0);
let i1_bar = j_23 * i1;
let w_iso = self.c10 * (i1_bar - 3.0);
let w_fiber = if i4 > 1.0 {
let e = i4 - 1.0;
self.k1 / (2.0 * self.k2) * ((self.k2 * e * e).exp() - 1.0)
} else {
0.0
};
w_vol + w_iso + w_fiber
}
pub fn laplace_tension(&self, internal_pressure: f64) -> f64 {
internal_pressure * self.inner_radius
}
pub fn circumferential_stress(&self, internal_pressure: f64) -> f64 {
let h = self.wall_thickness();
self.laplace_tension(internal_pressure) / h
}
pub fn vascular_compliance(&self, e_wall: f64) -> f64 {
let r = 0.5 * (self.inner_radius + self.outer_radius);
let h = self.wall_thickness();
2.0 * PI * r * r * r / (e_wall * h)
}
}
#[derive(Debug, Clone)]
pub struct CellMechanics {
pub cortical_tension: f64,
pub radius: f64,
pub cytoskeletal_modulus: f64,
pub active_stress: f64,
pub adhesion_energy: f64,
pub density: f64,
pub num_stress_fibers: usize,
pub prestress: f64,
}
impl CellMechanics {
pub fn fibroblast() -> Self {
Self {
cortical_tension: 0.05e-3,
radius: 10.0e-6,
cytoskeletal_modulus: 400.0,
active_stress: 1000.0,
adhesion_energy: 1.0e-4,
density: 1060.0,
num_stress_fibers: 20,
prestress: 500.0,
}
}
pub fn red_blood_cell() -> Self {
Self {
cortical_tension: 0.002e-3,
radius: 4.0e-6,
cytoskeletal_modulus: 6.0,
active_stress: 0.0,
adhesion_energy: 0.0,
density: 1080.0,
num_stress_fibers: 0,
prestress: 0.0,
}
}
pub fn laplace_pressure(&self) -> f64 {
2.0 * self.cortical_tension / self.radius
}
pub fn effective_modulus(&self) -> f64 {
self.cytoskeletal_modulus + self.active_stress
}
pub fn spreading_area(&self) -> f64 {
let w = self.adhesion_energy;
let tc = self.cortical_tension;
if tc < 1e-20 {
return 0.0;
}
PI * self.radius * self.radius * w / tc
}
pub fn membrane_bending_stiffness(&self) -> f64 {
let kbt = 4.28e-21_f64;
25.0 * kbt
}
pub fn volume(&self) -> f64 {
4.0 / 3.0 * PI * self.radius.powi(3)
}
pub fn osmotic_pressure(&self, volumetric_strain: f64) -> f64 {
-self.cytoskeletal_modulus * volumetric_strain
}
}
#[derive(Debug, Clone)]
pub struct HydrogelModel {
pub phi0: f64,
pub phi_eq: f64,
pub chi: f64,
pub crosslink_density: f64,
pub dry_modulus: f64,
pub solvent_molar_volume: f64,
pub temperature: f64,
}
impl HydrogelModel {
pub fn peg_hydrogel() -> Self {
Self {
phi0: 0.10,
phi_eq: 0.08,
chi: 0.45,
crosslink_density: 500.0,
dry_modulus: 50.0e3,
solvent_molar_volume: 1.8e-5,
temperature: 310.0,
}
}
pub fn hyaluronic_acid() -> Self {
Self {
phi0: 0.02,
phi_eq: 0.015,
chi: 0.50,
crosslink_density: 100.0,
dry_modulus: 1.0e3,
solvent_molar_volume: 1.8e-5,
temperature: 310.0,
}
}
const R: f64 = 8.314;
pub fn network_modulus(&self) -> f64 {
let n_c = self.crosslink_density;
let phi = self.phi_eq;
n_c * Self::R * self.temperature * phi.powf(1.0 / 3.0)
}
pub fn mixing_chemical_potential(&self) -> f64 {
let phi = self.phi_eq;
Self::R * self.temperature * ((1.0 - phi).ln() + phi + self.chi * phi * phi)
}
pub fn osmotic_pressure(&self) -> f64 {
let phi = self.phi_eq;
let rtvs = Self::R * self.temperature / self.solvent_molar_volume;
let pi_mix = -rtvs * ((1.0 - phi).ln() + phi + self.chi * phi * phi);
let pi_el = self.network_modulus() * (phi.powf(1.0 / 3.0) - phi / 2.0);
pi_mix + pi_el
}
pub fn swelling_ratio(&self) -> f64 {
1.0 / self.phi_eq
}
pub fn linear_swelling_strain(&self) -> f64 {
self.swelling_ratio().powf(1.0 / 3.0) - 1.0
}
pub fn swollen_modulus(&self) -> f64 {
self.network_modulus()
}
pub fn effective_diffusivity(&self, d0: f64) -> f64 {
let phi = self.phi_eq;
d0 * (-0.84 * phi / (1.0 - phi)).exp()
}
}
#[derive(Debug, Clone)]
pub struct BiomechanicsAnalysis {
pub implant_modulus: f64,
pub bone_modulus: f64,
pub implant_area: f64,
pub bone_area: f64,
pub applied_load: f64,
pub fatigue_exponent: f64,
pub reference_fatigue_stress: f64,
pub reference_cycles: f64,
}
impl BiomechanicsAnalysis {
pub fn hip_replacement() -> Self {
Self {
implant_modulus: 110.0e9, bone_modulus: 18.0e9,
implant_area: 200.0e-6,
bone_area: 400.0e-6,
applied_load: 2500.0,
fatigue_exponent: 6.0,
reference_fatigue_stress: 60.0e6,
reference_cycles: 1.0e6,
}
}
pub fn stress_shielding_factor(&self) -> f64 {
let ea_implant = self.implant_modulus * self.implant_area;
let ea_bone = self.bone_modulus * self.bone_area;
ea_implant / (ea_implant + ea_bone)
}
pub fn bone_stress_with_implant(&self) -> f64 {
let f = self.stress_shielding_factor();
(1.0 - f) * self.applied_load / self.bone_area
}
pub fn bone_stress_without_implant(&self) -> f64 {
self.applied_load / self.bone_area
}
#[allow(clippy::too_many_arguments)]
pub fn tsai_wu_failure_index(
sigma11: f64,
sigma22: f64,
tau12: f64,
f1t: f64,
f1c: f64,
f2t: f64,
f2c: f64,
f12: f64,
) -> f64 {
let h1 = 1.0 / f1t - 1.0 / f1c;
let h2 = 1.0 / f2t - 1.0 / f2c;
let h11 = 1.0 / (f1t * f1c);
let h22 = 1.0 / (f2t * f2c);
let h66 = 1.0 / (f12 * f12);
let h12 = -0.5 * (h11 * h22).sqrt(); h1 * sigma11
+ h2 * sigma22
+ h11 * sigma11 * sigma11
+ h22 * sigma22 * sigma22
+ h66 * tau12 * tau12
+ 2.0 * h12 * sigma11 * sigma22
}
pub fn von_mises_stress(s11: f64, s22: f64, s33: f64, s12: f64, s23: f64, s13: f64) -> f64 {
(0.5 * ((s11 - s22).powi(2)
+ (s22 - s33).powi(2)
+ (s33 - s11).powi(2)
+ 6.0 * (s12 * s12 + s23 * s23 + s13 * s13)))
.sqrt()
}
pub fn fatigue_life(&self, sigma_a: f64) -> f64 {
if sigma_a <= 0.0 {
return f64::INFINITY;
}
self.reference_cycles
* (self.reference_fatigue_stress / sigma_a).powf(self.fatigue_exponent)
}
pub fn miners_damage(&self, cycles_at_stress: &[(f64, f64)]) -> f64 {
cycles_at_stress
.iter()
.map(|(n, sigma)| n / self.fatigue_life(*sigma))
.sum()
}
pub fn max_principal_stress_failure(
&self,
sigma1: f64,
sigma2: f64,
sigma3: f64,
tensile_strength: f64,
compressive_strength: f64,
) -> bool {
sigma1 > tensile_strength
|| sigma2 > tensile_strength
|| sigma3 > tensile_strength
|| sigma1 < -compressive_strength
|| sigma2 < -compressive_strength
|| sigma3 < -compressive_strength
}
pub fn remodelling_stimulus(sed: f64, sed_ref: f64, dead_zone: f64) -> f64 {
let ratio = (sed - sed_ref) / sed_ref;
if ratio.abs() < dead_zone {
0.0
} else {
ratio - dead_zone.copysign(ratio)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-6;
#[test]
fn test_soft_tissue_strain_energy_at_rest() {
let st = SoftTissue::skin_dermis();
let f_id = mat3_identity();
let w = st.strain_energy_density(&f_id);
assert!(
w.abs() < 1e-3,
"strain energy at rest should be ~0, got {w}"
);
}
#[test]
fn test_soft_tissue_strain_energy_positive_under_stretch() {
let st = SoftTissue::skin_dermis();
let lambda = 1.2_f64;
let lp = 1.0 / lambda.sqrt();
let f = [[lambda, 0.0, 0.0], [0.0, lp, 0.0], [0.0, 0.0, lp]];
let w = st.strain_energy_density(&f);
assert!(
w > 0.0,
"strain energy should be positive under stretch, got {w}"
);
}
#[test]
fn test_soft_tissue_cauchy_stress_positive_stretch() {
let st = SoftTissue::skin_dermis();
let sigma = st.uniaxial_cauchy_stress(1.3);
assert!(sigma > 0.0, "Cauchy stress should be positive, got {sigma}");
}
#[test]
fn test_soft_tissue_cauchy_stress_at_one() {
let st = SoftTissue::skin_dermis();
let sigma = st.uniaxial_cauchy_stress(1.0);
assert!(
sigma.abs() < 1e-3,
"Cauchy stress at λ=1 should be ~0, got {sigma}"
);
}
#[test]
fn test_soft_tissue_small_strain_modulus_positive() {
let st = SoftTissue::myocardium();
let e = st.small_strain_modulus();
assert!(e > 0.0, "small-strain modulus should be positive, got {e}");
}
#[test]
fn test_soft_tissue_toe_region_stretch_greater_than_one() {
let st = SoftTissue::skin_dermis();
let toe = st.toe_region_stretch();
assert!(toe > 1.0, "toe-region stretch should be >1, got {toe}");
}
#[test]
fn test_soft_tissue_stress_increases_with_stretch() {
let st = SoftTissue::skin_dermis();
let s1 = st.uniaxial_cauchy_stress(1.1);
let s2 = st.uniaxial_cauchy_stress(1.5);
assert!(
s2 > s1,
"stress should increase with stretch: s1={s1}, s2={s2}"
);
}
#[test]
fn test_bone_anisotropy_ratio() {
let bone = BoneMaterial::cortical_femur();
let r = bone.anisotropy_ratio();
assert!(
(r - 20.0e9 / 12.0e9).abs() < TOL,
"anisotropy ratio should be E_L/E_T"
);
}
#[test]
fn test_bone_voigt_reuss_bounds() {
let bone = BoneMaterial::cancellous_vertebra();
let e2 = 1.0e6; let voigt = bone.voigt_upper_bound(e2);
let reuss = bone.reuss_lower_bound(e2);
assert!(
voigt >= reuss,
"Voigt bound should be ≥ Reuss bound: {voigt} vs {reuss}"
);
}
#[test]
fn test_bone_hs_estimate_between_bounds() {
let bone = BoneMaterial::cortical_femur();
let e2 = 0.1e9;
let v = bone.voigt_upper_bound(e2);
let r = bone.reuss_lower_bound(e2);
let hs = bone.hashin_shtrikman_estimate(e2);
assert!(
hs >= r && hs <= v,
"HS estimate {hs} should be between Reuss {r} and Voigt {v}"
);
}
#[test]
fn test_bone_wave_speeds_positive() {
let bone = BoneMaterial::cortical_femur();
assert!(bone.longitudinal_wave_speed() > 0.0);
assert!(bone.shear_wave_speed() > 0.0);
}
#[test]
fn test_bone_compressive_yield_strain() {
let bone = BoneMaterial::cortical_femur();
let eps = bone.compressive_yield_strain();
assert!(
eps > 0.0 && eps < 0.02,
"yield strain should be ~0.9 %, got {eps}"
);
}
#[test]
fn test_bone_ash_density_modulus_power_law() {
let e = BoneMaterial::modulus_from_ash_density(0.8);
assert!(e > 0.0, "modulus should be positive, got {e}");
}
#[test]
fn test_cartilage_aggregate_modulus_formula() {
let e = 0.8e6;
let nu = 0.15;
let ha = CartilageModel::compute_aggregate_modulus(e, nu);
let expected = e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu));
assert!((ha - expected).abs() < 1.0, "aggregate modulus mismatch");
}
#[test]
fn test_cartilage_creep_time_constant_positive() {
let cart = CartilageModel::human_knee_cartilage();
let tau = cart.creep_time_constant();
assert!(
tau > 0.0,
"creep time constant should be positive, got {tau}"
);
}
#[test]
fn test_cartilage_creep_compliance_monotone() {
let cart = CartilageModel::human_knee_cartilage();
let c0 = cart.creep_compliance(0.0);
let c100 = cart.creep_compliance(100.0);
let cinf = cart.creep_compliance(1.0e12);
assert!(c0 <= c100, "compliance should increase over time");
assert!(c100 <= cinf, "compliance should approach equilibrium");
}
#[test]
fn test_cartilage_darcy_flux() {
let cart = CartilageModel::human_knee_cartilage();
let flux = cart.darcy_fluid_flux(1.0e6);
assert!(
flux > 0.0,
"Darcy flux should be positive for positive pressure gradient"
);
}
#[test]
fn test_cartilage_donnan_pressure_positive() {
let cart = CartilageModel::human_knee_cartilage();
let pi = cart.donnan_swelling_pressure(0.15);
assert!(pi > 0.0, "Donnan pressure should be positive, got {pi}");
}
#[test]
fn test_cartilage_strain_dependent_permeability() {
let cart = CartilageModel::human_knee_cartilage();
let k_comp = cart.strain_dependent_permeability(-0.2);
let k_tens = cart.strain_dependent_permeability(0.2);
assert!(
k_comp < cart.permeability,
"permeability should decrease in compression"
);
assert!(
k_tens > cart.permeability,
"permeability should increase in tension"
);
}
#[test]
fn test_vascular_wall_thickness() {
let aw = VascularWall::human_aorta();
let h = aw.wall_thickness();
assert!(
(h - 2.5e-3).abs() < 1e-6,
"aorta wall thickness should be ~2.5 mm, got {h}"
);
}
#[test]
fn test_vascular_laplace_tension() {
let aw = VascularWall::human_aorta();
let p = 13_332.0; let t = aw.laplace_tension(p);
assert!(t > 0.0, "Laplace tension should be positive, got {t}");
}
#[test]
fn test_vascular_circumferential_stress() {
let aw = VascularWall::human_aorta();
let s = aw.circumferential_stress(13_332.0);
assert!(
s > 0.0,
"circumferential stress should be positive, got {s}"
);
}
#[test]
fn test_vascular_hgo_energy_at_identity() {
let aw = VascularWall::human_aorta();
let f_id = mat3_identity();
let w = aw.hgo_strain_energy(&f_id);
assert!(
w.abs() < 1.0,
"HGO energy at identity should be near 0, got {w}"
);
}
#[test]
fn test_vascular_hgo_energy_positive_under_stretch() {
let aw = VascularWall::human_aorta();
let lambda = 1.2_f64;
let lp = 1.0 / lambda.sqrt();
let f = [[lambda, 0.0, 0.0], [0.0, lp, 0.0], [0.0, 0.0, lp]];
let w = aw.hgo_strain_energy(&f);
assert!(
w > 0.0,
"HGO energy should be positive under stretch, got {w}"
);
}
#[test]
fn test_vascular_residual_stretch() {
let aw = VascularWall::human_aorta();
let r_mid = 0.5 * (aw.inner_radius + aw.outer_radius);
let lam = aw.residual_stretch_circumferential(r_mid);
assert!(lam > 0.0, "residual stretch should be positive, got {lam}");
}
#[test]
fn test_cell_laplace_pressure() {
let cell = CellMechanics::fibroblast();
let p = cell.laplace_pressure();
assert!(p > 0.0, "Laplace pressure should be positive, got {p}");
}
#[test]
fn test_cell_effective_modulus() {
let cell = CellMechanics::fibroblast();
let e = cell.effective_modulus();
assert!(
e > cell.cytoskeletal_modulus,
"effective modulus should exceed cytoskeletal modulus"
);
}
#[test]
fn test_cell_volume_positive() {
let cell = CellMechanics::fibroblast();
assert!(cell.volume() > 0.0, "cell volume should be positive");
}
#[test]
fn test_cell_osmotic_pressure_sign() {
let cell = CellMechanics::fibroblast();
let p = cell.osmotic_pressure(-0.1);
assert!(
p > 0.0,
"osmotic pressure under compression should be positive"
);
}
#[test]
fn test_cell_membrane_bending_stiffness() {
let cell = CellMechanics::red_blood_cell();
let kappa = cell.membrane_bending_stiffness();
assert!(
kappa > 0.0,
"bending stiffness should be positive, got {kappa}"
);
}
#[test]
fn test_hydrogel_swelling_ratio() {
let gel = HydrogelModel::peg_hydrogel();
let q = gel.swelling_ratio();
assert!(
q > 1.0,
"swelling ratio should be >1 for swollen gel, got {q}"
);
}
#[test]
fn test_hydrogel_linear_swelling_strain() {
let gel = HydrogelModel::peg_hydrogel();
let eps = gel.linear_swelling_strain();
assert!(
eps > 0.0,
"linear swelling strain should be positive, got {eps}"
);
}
#[test]
fn test_hydrogel_network_modulus_positive() {
let gel = HydrogelModel::peg_hydrogel();
let g = gel.network_modulus();
assert!(g > 0.0, "network modulus should be positive, got {g}");
}
#[test]
fn test_hydrogel_effective_diffusivity() {
let gel = HydrogelModel::hyaluronic_acid();
let d0 = 1.0e-9; let deff = gel.effective_diffusivity(d0);
assert!(
deff > 0.0,
"effective diffusivity should be positive, got {deff}"
);
assert!(
deff < d0,
"effective diffusivity should be less than free-solution value"
);
}
#[test]
fn test_stress_shielding_factor_range() {
let ba = BiomechanicsAnalysis::hip_replacement();
let f = ba.stress_shielding_factor();
assert!(
f > 0.0 && f < 1.0,
"stress shielding factor should be in (0,1), got {f}"
);
}
#[test]
fn test_bone_stress_with_implant_less_than_without() {
let ba = BiomechanicsAnalysis::hip_replacement();
let with_imp = ba.bone_stress_with_implant();
let without = ba.bone_stress_without_implant();
assert!(
with_imp < without,
"bone stress with implant should be less (stress shielding): {with_imp} vs {without}"
);
}
#[test]
fn test_fatigue_life_decreases_with_stress() {
let ba = BiomechanicsAnalysis::hip_replacement();
let n1 = ba.fatigue_life(50.0e6);
let n2 = ba.fatigue_life(100.0e6);
assert!(
n1 > n2,
"fatigue life should decrease with higher stress: N1={n1}, N2={n2}"
);
}
#[test]
fn test_fatigue_life_infinite_at_zero_stress() {
let ba = BiomechanicsAnalysis::hip_replacement();
let n = ba.fatigue_life(0.0);
assert!(
n.is_infinite(),
"fatigue life should be infinite at zero stress"
);
}
#[test]
fn test_miners_damage_accumulates() {
let ba = BiomechanicsAnalysis::hip_replacement();
let loading = vec![(1.0e5, 60.0e6), (2.0e5, 80.0e6)];
let d = ba.miners_damage(&loading);
assert!(d > 0.0, "Miner's damage should be positive, got {d}");
}
#[test]
fn test_von_mises_stress_positive() {
let vm =
BiomechanicsAnalysis::von_mises_stress(100.0e6, 50.0e6, 20.0e6, 10.0e6, 5.0e6, 3.0e6);
assert!(vm > 0.0, "Von Mises stress should be positive, got {vm}");
}
#[test]
fn test_von_mises_stress_hydrostatic_zero() {
let vm = BiomechanicsAnalysis::von_mises_stress(100.0e6, 100.0e6, 100.0e6, 0.0, 0.0, 0.0);
assert!(
vm.abs() < 1.0,
"VM stress under hydrostatic loading should be 0, got {vm}"
);
}
#[test]
fn test_tsai_wu_failure_below_one_safe() {
let fi = BiomechanicsAnalysis::tsai_wu_failure_index(
10.0e6, 5.0e6, 2.0e6, 120.0e6, 180.0e6, 60.0e6, 80.0e6, 50.0e6,
);
assert!(
fi < 1.0,
"Tsai-Wu index should be <1 for safe loading, got {fi}"
);
}
#[test]
fn test_tsai_wu_failure_above_one_failed() {
let fi = BiomechanicsAnalysis::tsai_wu_failure_index(
200.0e6, 0.0, 0.0, 120.0e6, 180.0e6, 60.0e6, 80.0e6, 50.0e6,
);
assert!(
fi > 1.0,
"Tsai-Wu index should be >1 for overloaded case, got {fi}"
);
}
#[test]
fn test_remodelling_stimulus_zero_in_dead_zone() {
let stimulus = BiomechanicsAnalysis::remodelling_stimulus(1.0, 1.0, 0.1);
assert!(
stimulus.abs() < TOL,
"remodelling stimulus should be 0 in dead zone"
);
}
#[test]
fn test_remodelling_stimulus_positive_above_reference() {
let stimulus = BiomechanicsAnalysis::remodelling_stimulus(2.0, 1.0, 0.05);
assert!(
stimulus > 0.0,
"remodelling stimulus should be positive above reference"
);
}
#[test]
fn test_max_principal_stress_failure_trigger() {
let failed = BiomechanicsAnalysis {
implant_modulus: 0.0,
bone_modulus: 0.0,
implant_area: 0.0,
bone_area: 1.0,
applied_load: 0.0,
fatigue_exponent: 6.0,
reference_fatigue_stress: 60.0e6,
reference_cycles: 1.0e6,
}
.max_principal_stress_failure(200.0e6, 0.0, 0.0, 120.0e6, 180.0e6);
assert!(
failed,
"should detect failure when stress exceeds tensile strength"
);
}
#[test]
fn test_max_principal_stress_no_failure() {
let ba = BiomechanicsAnalysis::hip_replacement();
let safe = ba.max_principal_stress_failure(50.0e6, 30.0e6, 10.0e6, 120.0e6, 180.0e6);
assert!(!safe, "should be safe when stresses are below strength");
}
}