pub type Vec3 = [f64; 3];
pub type StrainTensor = [f64; 6];
#[derive(Debug, Clone)]
pub struct SoftTissue {
pub fung_c: f64,
pub fung_b: [f64; 6],
pub hgo_mu: f64,
pub hgo_k1: f64,
pub hgo_k2: f64,
pub hgo_kappa: f64,
pub fiber_angle: f64,
pub active_stress: f64,
}
impl SoftTissue {
pub fn new_fung(fung_c: f64, fung_b: [f64; 6]) -> Self {
Self {
fung_c,
fung_b,
hgo_mu: 0.0,
hgo_k1: 0.0,
hgo_k2: 0.0,
hgo_kappa: 0.0,
fiber_angle: 0.0,
active_stress: 0.0,
}
}
pub fn new_hgo(mu: f64, k1: f64, k2: f64, kappa: f64, fiber_angle: f64) -> Self {
Self {
fung_c: 0.0,
fung_b: [0.0; 6],
hgo_mu: mu,
hgo_k1: k1,
hgo_k2: k2,
hgo_kappa: kappa,
fiber_angle,
active_stress: 0.0,
}
}
pub fn fung_strain_energy(&self, strain: &StrainTensor) -> f64 {
let [e11, e22, e33, _e12, _e13, _e23] = *strain;
let [b11, b22, b33, b12, b13, b23] = self.fung_b;
let q = b11 * e11 * e11
+ b22 * e22 * e22
+ b33 * e33 * e33
+ 2.0 * b12 * e11 * e22
+ 2.0 * b13 * e11 * e33
+ 2.0 * b23 * e22 * e33;
self.fung_c * (q.exp() - 1.0)
}
pub fn fung_stress_s11(&self, strain: &StrainTensor) -> f64 {
let [e11, e22, e33, _e12, _e13, _e23] = *strain;
let [b11, b22, b33, b12, b13, b23] = self.fung_b;
let q = b11 * e11 * e11
+ b22 * e22 * e22
+ b33 * e33 * e33
+ 2.0 * b12 * e11 * e22
+ 2.0 * b13 * e11 * e33
+ 2.0 * b23 * e22 * e33;
2.0 * self.fung_c * (b11 * e11 + b12 * e22 + b13 * e33) * q.exp()
}
pub fn hgo_i4(&self, stretch_circ: f64, stretch_axial: f64) -> f64 {
let cos_a = self.fiber_angle.cos();
let sin_a = self.fiber_angle.sin();
let c11 = stretch_circ * stretch_circ;
let c22 = stretch_axial * stretch_axial;
cos_a * cos_a * c11 + sin_a * sin_a * c22
}
#[allow(clippy::too_many_arguments)]
pub fn hgo_fiber_energy(
&self,
stretch_circ: f64,
stretch_axial: f64,
stretch_radial: f64,
) -> f64 {
let i1 = stretch_circ * stretch_circ
+ stretch_axial * stretch_axial
+ stretch_radial * stretch_radial;
let i4 = self.hgo_i4(stretch_circ, stretch_axial);
if i4 <= 1.0 {
return 0.0;
}
let e_val = self.hgo_kappa * i1 + (1.0 - 3.0 * self.hgo_kappa) * i4 - 1.0;
2.0 * self.hgo_k1 / (2.0 * self.hgo_k2) * ((self.hgo_k2 * e_val * e_val).exp() - 1.0)
}
pub fn hgo_total_energy(
&self,
stretch_circ: f64,
stretch_axial: f64,
stretch_radial: f64,
) -> f64 {
let i1 = stretch_circ * stretch_circ
+ stretch_axial * stretch_axial
+ stretch_radial * stretch_radial;
let w_iso = self.hgo_mu / 2.0 * (i1 - 3.0);
let w_fiber = self.hgo_fiber_energy(stretch_circ, stretch_axial, stretch_radial);
w_iso + w_fiber
}
pub fn total_stress_with_active(&self, strain: &StrainTensor) -> f64 {
self.fung_stress_s11(strain) + self.active_stress
}
pub fn set_active_stress(&mut self, sigma_a: f64) {
self.active_stress = sigma_a;
}
}
#[derive(Debug, Clone)]
pub struct BoneModel {
pub density: f64,
pub mineral_fraction: f64,
pub e_axial: f64,
pub e_transverse: f64,
pub shear_modulus: f64,
pub trabecular_thickness: f64,
pub trabecular_length: f64,
}
impl BoneModel {
pub fn new(
density: f64,
mineral_fraction: f64,
e_axial: f64,
e_transverse: f64,
shear_modulus: f64,
) -> Self {
Self {
density,
mineral_fraction,
e_axial,
e_transverse,
shear_modulus,
trabecular_thickness: 0.15,
trabecular_length: 1.2,
}
}
pub fn density_to_modulus_kk(density: f64) -> f64 {
6850.0 * density.powf(1.49)
}
pub fn gibson_ashby_modulus(&self, solid_modulus_gpa: f64) -> f64 {
let rho_s = 1.9_f64; let relative_density = (self.density / rho_s).min(1.0);
solid_modulus_gpa * relative_density * relative_density
}
pub fn trabecular_modulus(&self) -> f64 {
self.gibson_ashby_modulus(18.0)
}
pub fn wolff_adapt(&mut self, sigma_actual: f64, sigma_ref: f64, k: f64, s: f64, dt: f64) {
let error = sigma_actual - sigma_ref;
let d_rho = if error > s {
k * (error - s)
} else if error < -s {
k * (error + s)
} else {
0.0
};
self.density = (self.density + d_rho * dt).clamp(0.05, 1.9);
}
pub fn yield_strength(&self) -> f64 {
94.0 * self.mineral_fraction.powf(3.84)
}
}
#[derive(Debug, Clone)]
pub struct BloodRheology {
pub eta_0: f64,
pub eta_inf: f64,
pub lambda: f64,
pub carreau_a: f64,
pub power_n: f64,
pub yield_stress: f64,
pub casson_eta: f64,
pub hematocrit: f64,
}
impl BloodRheology {
pub fn new_physiological() -> Self {
Self {
eta_0: 0.056, eta_inf: 0.0035, lambda: 3.313,
carreau_a: 2.0,
power_n: 0.3568,
yield_stress: 0.004, casson_eta: 0.0035,
hematocrit: 0.45,
}
}
#[allow(clippy::too_many_arguments)]
pub fn new(
eta_0: f64,
eta_inf: f64,
lambda: f64,
carreau_a: f64,
power_n: f64,
yield_stress: f64,
casson_eta: f64,
hematocrit: f64,
) -> Self {
Self {
eta_0,
eta_inf,
lambda,
carreau_a,
power_n,
yield_stress,
casson_eta,
hematocrit,
}
}
pub fn carreau_viscosity(&self, shear_rate: f64) -> f64 {
let base = 1.0 + (self.lambda * shear_rate.abs()).powf(self.carreau_a);
self.eta_inf
+ (self.eta_0 - self.eta_inf) * base.powf((self.power_n - 1.0) / self.carreau_a)
}
pub fn casson_stress(&self, shear_rate: f64) -> f64 {
let gamma_abs = shear_rate.abs();
if gamma_abs < 1e-12 {
return 0.0;
}
let sqrt_tau = self.yield_stress.sqrt() + (self.casson_eta * gamma_abs).sqrt();
sqrt_tau * sqrt_tau * shear_rate.signum()
}
pub fn casson_viscosity(&self, shear_rate: f64) -> f64 {
let gamma_abs = shear_rate.abs();
if gamma_abs < 1e-12 {
return self.eta_0; }
self.casson_stress(shear_rate) / shear_rate
}
pub fn hematocrit_viscosity(&self) -> f64 {
let h = self.hematocrit.clamp(0.0, 0.99);
self.eta_inf / (1.0 - h / 2.0).powi(2)
}
pub fn is_newtonian_limit(&self, shear_rate: f64) -> bool {
let eta = self.carreau_viscosity(shear_rate);
(eta - self.eta_inf).abs() / (self.eta_0 - self.eta_inf + 1e-20) < 0.01
}
}
#[derive(Debug, Clone)]
pub struct HydrogelModel {
pub chi: f64,
pub n_chains: f64,
pub nu_0: f64,
pub molar_volume_solvent: f64,
pub shear_modulus: f64,
}
impl HydrogelModel {
pub fn new(chi: f64, n_chains: f64, shear_modulus: f64) -> Self {
Self {
chi,
n_chains,
nu_0: 1.0,
molar_volume_solvent: 18e-6, shear_modulus,
}
}
pub fn mixing_chemical_potential(&self, phi: f64) -> f64 {
(1.0 - phi).ln() + phi + self.chi * phi * phi
}
pub fn elastic_chemical_potential(&self, phi: f64) -> f64 {
(1.0 / self.n_chains) * (phi / 2.0 - phi.powf(1.0 / 3.0))
}
pub fn total_chemical_potential(&self, phi: f64) -> f64 {
self.mixing_chemical_potential(phi) + self.elastic_chemical_potential(phi)
}
pub fn equilibrium_swelling_ratio(&self) -> f64 {
let mut lo = 1e-4_f64;
let mut hi = 1.0 - 1e-4;
let mu_lo = self.total_chemical_potential(lo);
let mu_hi = self.total_chemical_potential(hi);
if mu_lo * mu_hi > 0.0 {
if mu_lo.abs() < mu_hi.abs() {
return 1.0 / lo;
} else {
return 1.0 / hi;
}
}
for _ in 0..60 {
let mid = 0.5 * (lo + hi);
if self.total_chemical_potential(lo) * self.total_chemical_potential(mid) <= 0.0 {
hi = mid;
} else {
lo = mid;
}
}
let phi_eq = 0.5 * (lo + hi);
1.0 / phi_eq
}
}
#[derive(Debug, Clone)]
pub struct CartilageModel {
pub aggregate_modulus: f64,
pub permeability: f64,
pub poisson_solid: f64,
pub thickness_mm: f64,
}
impl CartilageModel {
pub fn new(
aggregate_modulus: f64,
permeability: f64,
poisson_solid: f64,
thickness_mm: f64,
) -> Self {
Self {
aggregate_modulus,
permeability,
poisson_solid,
thickness_mm,
}
}
pub fn characteristic_time(&self) -> f64 {
let h_m = self.thickness_mm * 1e-3;
h_m * h_m / (self.aggregate_modulus * 1e6 * self.permeability)
}
pub fn creep_response(&self, sigma0: f64, time: f64) -> f64 {
let t_star = self.characteristic_time();
let u_inf = sigma0 / self.aggregate_modulus;
u_inf * (1.0 - (-time / t_star).exp())
}
pub fn stress_relaxation(&self, epsilon0: f64, time: f64) -> f64 {
let t_star = self.characteristic_time();
let sigma0 = self.aggregate_modulus * epsilon0;
sigma0 * (-time / t_star).exp()
}
pub fn lame_lambda(&self) -> f64 {
let nu = self.poisson_solid;
let e = 2.0 * self.aggregate_modulus * (1.0 - nu) / (1.0 - 2.0 * nu);
e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu))
}
}
#[derive(Debug, Clone)]
pub struct ArterialWall {
pub intima: SoftTissue,
pub media: SoftTissue,
pub adventitia: SoftTissue,
pub layer_fractions: [f64; 3],
pub opening_angle: f64,
pub inner_radius: f64,
pub outer_radius: f64,
}
impl ArterialWall {
pub fn new_aorta() -> Self {
let intima = SoftTissue::new_hgo(7.64e3, 996.6, 524.6, 0.226, 0.523);
let media = SoftTissue::new_hgo(3.0e3, 15.0e3, 20.0, 0.226, 0.464);
let adventitia = SoftTissue::new_hgo(0.3e3, 57.0e3, 11.2, 0.226, 0.785);
Self {
intima,
media,
adventitia,
layer_fractions: [0.1, 0.5, 0.4],
opening_angle: 0.541, inner_radius: 9.0,
outer_radius: 13.5,
}
}
pub fn residual_strain_inner(&self) -> f64 {
let alpha = std::f64::consts::PI - self.opening_angle;
let lambda = std::f64::consts::PI * self.inner_radius / (alpha * self.inner_radius);
lambda - 1.0
}
pub fn wall_stress_at_stretch(&self, lambda_circ: f64) -> f64 {
let lambda_axial = 1.1; let lambda_radial = 1.0 / (lambda_circ * lambda_axial); let s_i = self
.intima
.hgo_total_energy(lambda_circ, lambda_axial, lambda_radial);
let s_m = self
.media
.hgo_total_energy(lambda_circ, lambda_axial, lambda_radial);
let s_a = self
.adventitia
.hgo_total_energy(lambda_circ, lambda_axial, lambda_radial);
self.layer_fractions[0] * s_i
+ self.layer_fractions[1] * s_m
+ self.layer_fractions[2] * s_a
}
}
#[derive(Debug, Clone)]
pub struct DegradablePolymer {
pub mw_initial: f64,
pub mw_current: f64,
pub hydrolysis_rate: f64,
pub autocatalysis: f64,
pub mw_critical: f64,
}
impl DegradablePolymer {
pub fn new_plga_50_50() -> Self {
Self {
mw_initial: 100_000.0, mw_current: 100_000.0,
hydrolysis_rate: 0.003, autocatalysis: 0.5,
mw_critical: 5_000.0, }
}
pub fn new(
mw_initial: f64,
hydrolysis_rate: f64,
autocatalysis: f64,
mw_critical: f64,
) -> Self {
Self {
mw_initial,
mw_current: mw_initial,
hydrolysis_rate,
autocatalysis,
mw_critical,
}
}
pub fn step(&mut self, dt: f64) {
let degradation_factor =
1.0 + self.autocatalysis * (self.mw_initial - self.mw_current) / self.mw_initial;
let rate = self.hydrolysis_rate * degradation_factor * self.mw_current;
self.mw_current = (self.mw_current - rate * dt).max(self.mw_critical * 0.5);
}
pub fn run(&mut self, days: f64, dt: f64) {
let steps = (days / dt).ceil() as usize;
for _ in 0..steps {
self.step(dt);
}
}
pub fn mw_retention(&self) -> f64 {
self.mw_current / self.mw_initial
}
pub fn is_failed(&self) -> bool {
self.mw_current <= self.mw_critical
}
pub fn modulus_retention(&self) -> f64 {
self.mw_retention().sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-10;
#[test]
fn test_fung_zero_strain_zero_energy() {
let tissue = SoftTissue::new_fung(0.5e3, [1.0, 1.0, 1.0, 0.5, 0.5, 0.5]);
let w = tissue.fung_strain_energy(&[0.0; 6]);
assert!(w.abs() < EPS, "W at zero strain should be 0, got {w}");
}
#[test]
fn test_fung_zero_strain_zero_stress() {
let tissue = SoftTissue::new_fung(0.5e3, [1.0, 1.0, 1.0, 0.5, 0.5, 0.5]);
let s = tissue.fung_stress_s11(&[0.0; 6]);
assert!(s.abs() < EPS, "S11 at zero strain should be 0, got {s}");
}
#[test]
fn test_fung_stress_increases_with_strain() {
let tissue = SoftTissue::new_fung(0.5e3, [2.0, 1.0, 1.0, 0.5, 0.5, 0.5]);
let s1 = tissue.fung_stress_s11(&[0.1, 0.0, 0.0, 0.0, 0.0, 0.0]);
let s2 = tissue.fung_stress_s11(&[0.2, 0.0, 0.0, 0.0, 0.0, 0.0]);
assert!(
s2 > s1,
"Fung stress should increase with strain: s1={s1:.6}, s2={s2:.6}"
);
}
#[test]
fn test_fung_energy_positive() {
let tissue = SoftTissue::new_fung(1.0e3, [1.0, 1.0, 1.0, 0.0, 0.0, 0.0]);
let w = tissue.fung_strain_energy(&[0.1, 0.05, 0.02, 0.0, 0.0, 0.0]);
assert!(w > 0.0, "Fung energy should be positive, got {w}");
}
#[test]
fn test_fung_stress_exponential_growth() {
let tissue = SoftTissue::new_fung(1.0, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let strains = [0.1, 0.2, 0.4, 0.8];
let mut prev = 0.0_f64;
for &e in &strains {
let s = tissue.fung_stress_s11(&[e, 0.0, 0.0, 0.0, 0.0, 0.0]);
assert!(
s > prev,
"stress at {e} ({s:.6}) should exceed prev ({prev:.6})"
);
prev = s;
}
}
#[test]
fn test_active_stress_adds() {
let mut tissue = SoftTissue::new_fung(1.0e3, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let passive = tissue.fung_stress_s11(&[0.1, 0.0, 0.0, 0.0, 0.0, 0.0]);
tissue.set_active_stress(500.0);
let total = tissue.total_stress_with_active(&[0.1, 0.0, 0.0, 0.0, 0.0, 0.0]);
assert!(
(total - passive - 500.0).abs() < 1e-6,
"Total stress should be passive + active, got {total:.6}"
);
}
#[test]
fn test_hgo_no_fiber_compression() {
let tissue = SoftTissue::new_hgo(1.0e3, 1.0e4, 10.0, 0.1, 0.0);
let w = tissue.hgo_fiber_energy(0.9, 1.0, 1.0 / 0.9);
assert_eq!(
w, 0.0,
"Fiber energy should be 0 under compression, got {w}"
);
}
#[test]
fn test_hgo_fiber_tension_positive() {
let tissue = SoftTissue::new_hgo(1.0e3, 1.0e4, 10.0, 0.1, 0.0);
let w = tissue.hgo_fiber_energy(1.2, 1.0, 1.0 / 1.2);
assert!(
w > 0.0,
"Fiber energy should be positive under tension, got {w}"
);
}
#[test]
fn test_hgo_total_energy_geq_isotropic() {
let tissue = SoftTissue::new_hgo(3.0e3, 1.5e4, 5.0, 0.1, 0.5);
let w_total = tissue.hgo_total_energy(1.3, 1.1, 1.0 / (1.3 * 1.1));
let radial = 1.0_f64 / (1.3 * 1.1);
let i1 = 1.3f64 * 1.3 + 1.1_f64 * 1.1 + radial * radial;
let w_iso = tissue.hgo_mu / 2.0 * (i1 - 3.0);
assert!(
w_total >= w_iso,
"Total energy should be ≥ isotropic: w_total={w_total:.6}, w_iso={w_iso:.6}"
);
}
#[test]
fn test_hgo_i4_identity() {
let tissue = SoftTissue::new_hgo(1.0e3, 1.0e4, 5.0, 0.1, 0.3);
let i4 = tissue.hgo_i4(1.0, 1.0);
assert!(
(i4 - 1.0).abs() < 1e-10,
"I4 at identity stretch should be 1, got {i4}"
);
}
#[test]
fn test_bone_density_modulus_increases() {
let e1 = BoneModel::density_to_modulus_kk(0.3);
let e2 = BoneModel::density_to_modulus_kk(0.6);
let e3 = BoneModel::density_to_modulus_kk(1.0);
assert!(e1 < e2 && e2 < e3, "Modulus should increase with density");
}
#[test]
fn test_bone_gibson_ashby_scaling() {
let bone1 = BoneModel::new(0.5, 0.6, 18.0, 12.0, 3.5);
let bone2 = BoneModel::new(1.0, 0.6, 18.0, 12.0, 3.5);
let e1 = bone1.gibson_ashby_modulus(18.0);
let e2 = bone2.gibson_ashby_modulus(18.0);
let expected_ratio = 4.0; assert!(
(e2 / e1 - expected_ratio).abs() < 0.01,
"Gibson-Ashby should give 4x modulus for 2x density: ratio={:.6}",
e2 / e1
);
}
#[test]
fn test_wolff_law_density_increases() {
let mut bone = BoneModel::new(0.5, 0.6, 12.0, 8.0, 3.0);
let initial_rho = bone.density;
bone.wolff_adapt(10.0, 5.0, 0.1, 0.5, 1.0);
assert!(
bone.density > initial_rho,
"Density should increase under high stress, got {:.4}",
bone.density
);
}
#[test]
fn test_wolff_law_density_decreases() {
let mut bone = BoneModel::new(1.0, 0.7, 18.0, 12.0, 3.5);
let initial_rho = bone.density;
bone.wolff_adapt(1.0, 8.0, 0.1, 0.5, 1.0);
assert!(
bone.density < initial_rho,
"Density should decrease under disuse, got {:.4}",
bone.density
);
}
#[test]
fn test_bone_yield_strength() {
let bone1 = BoneModel::new(1.5, 0.5, 18.0, 12.0, 3.5);
let bone2 = BoneModel::new(1.5, 0.7, 18.0, 12.0, 3.5);
assert!(
bone2.yield_strength() > bone1.yield_strength(),
"Higher mineral fraction → higher yield strength"
);
}
#[test]
fn test_blood_shear_thinning() {
let blood = BloodRheology::new_physiological();
let eta1 = blood.carreau_viscosity(1.0);
let eta2 = blood.carreau_viscosity(100.0);
assert!(
eta2 < eta1,
"Blood should shear-thin: η(1)={eta1:.6}, η(100)={eta2:.6}"
);
}
#[test]
fn test_blood_high_shear_newtonian() {
let blood = BloodRheology::new_physiological();
assert!(
blood.is_newtonian_limit(10000.0),
"Blood should be Newtonian at very high shear rate"
);
}
#[test]
fn test_casson_zero_shear() {
let blood = BloodRheology::new_physiological();
let tau = blood.casson_stress(0.0);
assert!(
tau.abs() < EPS,
"Casson stress at zero shear should be 0, got {tau}"
);
}
#[test]
fn test_casson_stress_increases() {
let blood = BloodRheology::new_physiological();
let tau1 = blood.casson_stress(10.0);
let tau2 = blood.casson_stress(100.0);
assert!(tau2 > tau1, "Casson stress should increase with shear rate");
}
#[test]
fn test_hematocrit_viscosity_greater_than_plasma() {
let blood = BloodRheology::new_physiological();
assert!(
blood.hematocrit_viscosity() > blood.eta_inf,
"Blood viscosity should exceed plasma viscosity"
);
}
#[test]
fn test_casson_positive_shear_positive_stress() {
let blood = BloodRheology::new_physiological();
assert!(
blood.casson_stress(5.0) > 0.0,
"Positive shear should give positive Casson stress"
);
}
#[test]
fn test_carreau_bounds() {
let blood = BloodRheology::new_physiological();
for &gamma in &[0.001, 0.1, 1.0, 10.0, 1000.0] {
let eta = blood.carreau_viscosity(gamma);
assert!(
eta >= blood.eta_inf && eta <= blood.eta_0,
"Carreau viscosity out of [η_∞, η_0] at γ̇={gamma}: η={eta:.6}"
);
}
}
#[test]
fn test_hydrogel_mixing_potential_negative_low_phi() {
let gel = HydrogelModel::new(0.4, 50.0, 1.0e4);
let mu = gel.mixing_chemical_potential(0.01);
assert!(
mu < 0.0,
"Mixing potential should be negative at low phi, got {mu:.6}"
);
}
#[test]
fn test_hydrogel_swelling_ratio_gt1() {
let gel = HydrogelModel::new(0.3, 100.0, 1.0e4);
let q = gel.equilibrium_swelling_ratio();
assert!(
q > 1.0,
"Equilibrium swelling ratio should be > 1, got {q:.6}"
);
}
#[test]
fn test_hydrogel_crosslink_reduces_swelling() {
let gel_loose = HydrogelModel::new(0.3, 500.0, 1.0e3);
let gel_tight = HydrogelModel::new(0.3, 10.0, 1.0e3);
let phi = 0.1;
let mu_loose = gel_loose.elastic_chemical_potential(phi);
let mu_tight = gel_tight.elastic_chemical_potential(phi);
assert!(
mu_tight.abs() > mu_loose.abs(),
"Tighter cross-linking should have larger |elastic potential|: |mu_tight|={:.6}, |mu_loose|={:.6}",
mu_tight.abs(),
mu_loose.abs()
);
}
#[test]
fn test_cartilage_char_time_positive() {
let cart = CartilageModel::new(0.79, 1.16e-15, 0.1, 2.0);
let t_star = cart.characteristic_time();
assert!(
t_star > 0.0,
"Characteristic time should be positive, got {t_star}"
);
}
#[test]
fn test_cartilage_creep_increases() {
let cart = CartilageModel::new(0.79, 1.16e-15, 0.1, 2.0);
let u1 = cart.creep_response(0.5, 100.0);
let u2 = cart.creep_response(0.5, 1000.0);
assert!(u2 > u1, "Creep deformation should increase with time");
}
#[test]
fn test_cartilage_stress_relaxation_decreases() {
let cart = CartilageModel::new(0.79, 1.16e-15, 0.1, 2.0);
let s1 = cart.stress_relaxation(0.1, 100.0);
let s2 = cart.stress_relaxation(0.1, 1000.0);
assert!(s2 < s1, "Stress relaxation should decrease with time");
}
#[test]
fn test_arterial_wall_stress_increases() {
let wall = ArterialWall::new_aorta();
let s1 = wall.wall_stress_at_stretch(1.1);
let s2 = wall.wall_stress_at_stretch(1.5);
assert!(
s2 > s1,
"Wall stress should increase with stretch: s1={s1:.6}, s2={s2:.6}"
);
}
#[test]
fn test_arterial_layer_fractions_sum_to_one() {
let wall = ArterialWall::new_aorta();
let sum: f64 = wall.layer_fractions.iter().sum();
assert!(
(sum - 1.0).abs() < 1e-10,
"Layer fractions should sum to 1, got {sum:.6}"
);
}
#[test]
fn test_polymer_mw_decreases() {
let mut poly = DegradablePolymer::new_plga_50_50();
poly.run(100.0, 1.0);
assert!(
poly.mw_current < poly.mw_initial,
"MW should decrease after degradation, got {:.0}",
poly.mw_current
);
}
#[test]
fn test_polymer_mw_retention_bounded() {
let mut poly = DegradablePolymer::new_plga_50_50();
poly.run(30.0, 1.0);
let ret = poly.mw_retention();
assert!(
(0.0..=1.0).contains(&ret),
"MW retention should be in [0, 1], got {ret:.6}"
);
}
#[test]
fn test_polymer_modulus_retention_leq1() {
let mut poly = DegradablePolymer::new_plga_50_50();
poly.run(50.0, 1.0);
let e_ret = poly.modulus_retention();
assert!(
e_ret <= 1.0,
"Modulus retention should be ≤ 1, got {e_ret:.6}"
);
}
#[test]
fn test_polymer_higher_rate_more_degradation() {
let mut slow = DegradablePolymer::new(100_000.0, 0.001, 0.5, 5_000.0);
let mut fast = DegradablePolymer::new(100_000.0, 0.01, 0.5, 5_000.0);
slow.run(100.0, 1.0);
fast.run(100.0, 1.0);
assert!(
fast.mw_current < slow.mw_current,
"Higher rate should degrade more: fast={:.0}, slow={:.0}",
fast.mw_current,
slow.mw_current
);
}
#[test]
fn test_polymer_autocatalysis_accelerates() {
let mut no_auto = DegradablePolymer::new(100_000.0, 0.003, 0.0, 1_000.0);
let mut with_auto = DegradablePolymer::new(100_000.0, 0.003, 2.0, 1_000.0);
no_auto.run(100.0, 1.0);
with_auto.run(100.0, 1.0);
assert!(
with_auto.mw_current < no_auto.mw_current,
"Autocatalysis should accelerate degradation"
);
}
}