fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn norm3(v: [f64; 3]) -> f64 {
dot3(v, v).sqrt()
}
fn normalize3(v: [f64; 3]) -> [f64; 3] {
let n = norm3(v);
if n < 1e-15 {
[0.0; 3]
} else {
[v[0] / n, v[1] / n, v[2] / n]
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TissueType {
Muscle,
Tendon,
Ligament,
Cartilage,
Bone,
Skin,
}
impl TissueType {
pub fn youngs_modulus(&self) -> f64 {
match self {
TissueType::Muscle => 50_000.0, TissueType::Tendon => 1_500_000_000.0, TissueType::Ligament => 100_000_000.0, TissueType::Cartilage => 1_000_000.0, TissueType::Bone => 20_000_000_000.0, TissueType::Skin => 100_000.0, }
}
pub fn poisson_ratio(&self) -> f64 {
match self {
TissueType::Muscle => 0.48,
TissueType::Tendon => 0.40,
TissueType::Ligament => 0.40,
TissueType::Cartilage => 0.10,
TissueType::Bone => 0.30,
TissueType::Skin => 0.49,
}
}
pub fn density(&self) -> f64 {
match self {
TissueType::Muscle => 1_060.0,
TissueType::Tendon => 1_200.0,
TissueType::Ligament => 1_200.0,
TissueType::Cartilage => 1_100.0,
TissueType::Bone => 1_900.0,
TissueType::Skin => 1_100.0,
}
}
}
#[derive(Debug, Clone)]
pub struct MuscleFiber {
pub direction: [f64; 3],
pub activation: f64,
pub f_max: f64,
pub l_opt: f64,
pub length: f64,
pub velocity: f64,
}
impl MuscleFiber {
pub fn new(direction: [f64; 3], f_max: f64, l_opt: f64) -> Self {
Self {
direction: normalize3(direction),
activation: 0.0,
f_max,
l_opt,
length: l_opt,
velocity: 0.0,
}
}
pub fn active_force_length(&self) -> f64 {
let ratio = self.length / self.l_opt;
let width = 0.45;
(-(ratio - 1.0).powi(2) / (2.0 * width * width)).exp()
}
pub fn passive_force_length(&self) -> f64 {
let ratio = self.length / self.l_opt;
if ratio > 1.0 {
0.02 * ((ratio - 1.0) / 0.6).exp()
} else {
0.0
}
}
pub fn total_force(&self) -> f64 {
let fl = self.active_force_length();
let fv = hill_force_velocity(self.velocity, self.l_opt);
let f_active = self.activation * fl * fv * self.f_max;
let f_passive = self.passive_force_length() * self.f_max;
f_active + f_passive
}
pub fn force_vector(&self) -> [f64; 3] {
let f = self.total_force();
[
self.direction[0] * f,
self.direction[1] * f,
self.direction[2] * f,
]
}
}
pub fn hill_force_velocity(velocity: f64, l_opt: f64) -> f64 {
let v_max = 10.0 * l_opt; let a = 0.25;
if velocity >= 0.0 {
let v = velocity.min(v_max);
(1.0 - v / v_max) / (1.0 + v / (a * v_max))
} else {
let v = (-velocity).min(v_max);
1.0 + 1.5 * v / v_max
}
}
#[derive(Debug, Clone)]
pub struct HillModel {
pub f_max: f64,
pub l_opt: f64,
pub l_slack: f64,
pub pennation: f64,
pub l_ce: f64,
pub activation: f64,
}
impl HillModel {
pub fn new(f_max: f64, l_opt: f64, l_slack: f64, pennation: f64) -> Self {
Self {
f_max,
l_opt,
l_slack,
pennation,
l_ce: l_opt,
activation: 0.0,
}
}
pub fn ce_force(&self) -> f64 {
let fiber = MuscleFiber {
direction: [1.0, 0.0, 0.0],
activation: self.activation,
f_max: self.f_max,
l_opt: self.l_opt,
length: self.l_ce,
velocity: 0.0,
};
fiber.total_force()
}
pub fn se_force(&self, l_mtu: f64) -> f64 {
let l_ce_proj = self.l_ce * self.pennation.cos();
let l_se = (l_mtu - l_ce_proj).max(0.0);
let k_se = 35.0 * self.f_max / self.l_slack;
let delta = (l_se - self.l_slack).max(0.0);
k_se * delta
}
pub fn update(&mut self, l_mtu: f64, activation: f64, dt: f64) {
self.activation = activation.clamp(0.0, 1.0);
let f_ce = self.ce_force();
let f_se = self.se_force(l_mtu);
let f_err = f_se - f_ce;
let v_ce = f_err / (self.f_max * 0.1 + 1e-6);
self.l_ce = (self.l_ce + v_ce * dt).max(0.3 * self.l_opt);
}
}
#[derive(Debug, Clone)]
pub struct TendonModel {
pub slack_length: f64,
pub linear_stiffness: f64,
pub toe_strain: f64,
pub yield_strain: f64,
pub damping: f64,
pub length: f64,
pub velocity: f64,
}
impl TendonModel {
pub fn new(slack_length: f64, linear_stiffness: f64) -> Self {
Self {
slack_length,
linear_stiffness,
toe_strain: 0.02,
yield_strain: 0.08,
damping: 100.0,
length: slack_length,
velocity: 0.0,
}
}
pub fn strain(&self) -> f64 {
(self.length - self.slack_length) / self.slack_length
}
pub fn elastic_force(&self) -> f64 {
let e = self.strain();
if e <= 0.0 {
return 0.0;
}
if e <= self.toe_strain {
let k_toe = self.linear_stiffness * e / self.toe_strain;
0.5 * k_toe * e * self.slack_length
} else if e <= self.yield_strain {
self.linear_stiffness * (e - self.toe_strain * 0.5) * self.slack_length
} else {
let f_yield = self.linear_stiffness
* (self.yield_strain - self.toe_strain * 0.5)
* self.slack_length;
f_yield + self.linear_stiffness * 0.1 * (e - self.yield_strain) * self.slack_length
}
}
pub fn total_force(&self) -> f64 {
let f_e = self.elastic_force();
let f_d = self.damping * self.velocity;
(f_e + f_d).max(0.0)
}
}
#[derive(Debug, Clone)]
pub struct SkinLayer {
pub thickness: f64,
pub modulus: f64,
pub prestretch: f64,
pub name: &'static str,
}
impl SkinLayer {
pub fn epidermis() -> Self {
Self {
thickness: 0.0001,
modulus: 140_000.0,
prestretch: 1.05,
name: "epidermis",
}
}
pub fn dermis() -> Self {
Self {
thickness: 0.002,
modulus: 100_000.0,
prestretch: 1.10,
name: "dermis",
}
}
pub fn hypodermis() -> Self {
Self {
thickness: 0.005,
modulus: 2_000.0,
prestretch: 1.0,
name: "hypodermis",
}
}
pub fn stress(&self, stretch: f64) -> f64 {
let lambda = stretch / self.prestretch;
self.modulus * (lambda - 1.0 / (lambda * lambda))
}
}
#[derive(Debug, Clone)]
pub struct SkinModel {
pub layers: Vec<SkinLayer>,
}
impl SkinModel {
pub fn default_skin() -> Self {
Self {
layers: vec![
SkinLayer::epidermis(),
SkinLayer::dermis(),
SkinLayer::hypodermis(),
],
}
}
pub fn total_thickness(&self) -> f64 {
self.layers.iter().map(|l| l.thickness).sum()
}
pub fn bending_stiffness(&self) -> f64 {
self.layers
.iter()
.map(|l| l.modulus * l.thickness.powi(3) / 12.0)
.sum()
}
}
#[derive(Debug, Clone)]
pub struct CartilageModel {
pub ha: f64,
pub permeability: f64,
pub swelling_pressure: f64,
pub fluid_fraction: f64,
pub strain: f64,
}
impl CartilageModel {
pub fn new(ha: f64, permeability: f64) -> Self {
Self {
ha,
permeability,
swelling_pressure: 50_000.0,
fluid_fraction: 0.8,
strain: 0.0,
}
}
pub fn solid_stress(&self, strain: f64) -> f64 {
self.ha * strain
}
pub fn fluid_pressure(&self, strain: f64) -> f64 {
self.ha * strain + self.swelling_pressure
}
pub fn creep_compliance(&self, t: f64) -> f64 {
let j_inf = 1.0 / self.ha;
let tau = self.fluid_fraction / (self.permeability * self.ha + 1e-20);
j_inf * (1.0 - (-t / tau).exp())
}
pub fn update(&mut self, applied_stress: f64, dt: f64) {
let p = self.fluid_pressure(self.strain);
let d_strain = self.permeability * (applied_stress - p) * dt;
self.strain = (self.strain + d_strain).clamp(0.0, 0.5);
}
}
#[derive(Debug, Clone)]
pub struct BoneRemodeling {
pub density: f64,
pub reference_sed: f64,
pub rate: f64,
pub dead_zone: f64,
pub rho_min: f64,
pub rho_max: f64,
pub fabric_tensor: [f64; 3],
}
impl BoneRemodeling {
pub fn new(initial_density: f64, reference_sed: f64) -> Self {
Self {
density: initial_density,
reference_sed,
rate: 0.01,
dead_zone: 0.1,
rho_min: 100.0,
rho_max: 2_000.0,
fabric_tensor: [1.0, 0.0, 0.0],
}
}
pub fn youngs_modulus(&self) -> f64 {
let c = 3.79e-3; c * self.density.powi(2)
}
pub fn update(&mut self, strain_energy_density: f64, dt: f64) {
let u = strain_energy_density;
let u_ref = self.reference_sed;
let error = (u - u_ref) / u_ref;
let stimulus = if error.abs() < self.dead_zone {
0.0
} else if error > 0.0 {
self.rate * (error - self.dead_zone)
} else {
self.rate * (error + self.dead_zone)
};
self.density =
(self.density + stimulus * self.density * dt).clamp(self.rho_min, self.rho_max);
}
pub fn strain_energy_density_1d(stress: f64, modulus: f64) -> f64 {
if modulus.abs() < 1e-20 {
0.0
} else {
0.5 * stress * stress / modulus
}
}
}
#[derive(Debug, Clone)]
pub struct WoundHealing {
pub wound_area: f64,
pub collagen_density: f64,
pub fibroblast_density: f64,
pub contraction_rate: f64,
pub deposition_rate: f64,
pub scar_modulus_ratio: f64,
}
impl WoundHealing {
pub fn new(wound_area: f64) -> Self {
Self {
wound_area,
collagen_density: 0.0,
fibroblast_density: 1e6,
contraction_rate: 1e-8,
deposition_rate: 1e-13,
scar_modulus_ratio: 0.5,
}
}
pub fn update(&mut self, dt: f64) {
let contraction = self.contraction_rate * self.fibroblast_density * dt;
self.wound_area = (self.wound_area - contraction).max(0.0);
self.collagen_density += self.deposition_rate * self.fibroblast_density * dt;
let closed_fraction = 1.0 - self.wound_area / (self.wound_area + 1e-10);
self.fibroblast_density *= 1.0 - closed_fraction * 0.01 * dt;
}
pub fn closure_fraction(&self, initial_area: f64) -> f64 {
if initial_area < 1e-15 {
return 1.0;
}
(1.0 - self.wound_area / initial_area).clamp(0.0, 1.0)
}
pub fn scar_stiffness(&self, base_modulus: f64) -> f64 {
let collagen_factor = 1.0 + self.collagen_density * 0.001;
base_modulus * self.scar_modulus_ratio * collagen_factor
}
}
#[derive(Debug, Clone)]
pub struct PressureUlcer {
pub pressure: f64,
pub ischemia_threshold: f64,
pub necrosis_threshold: f64,
pub damage: f64,
pub ischemia_time: f64,
}
impl PressureUlcer {
pub fn new(ischemia_threshold: f64, necrosis_threshold: f64) -> Self {
Self {
pressure: 0.0,
ischemia_threshold,
necrosis_threshold,
damage: 0.0,
ischemia_time: 0.0,
}
}
pub fn update(&mut self, applied_pressure: f64, dt: f64) {
self.pressure = applied_pressure;
if applied_pressure > self.ischemia_threshold {
let excess = applied_pressure - self.ischemia_threshold;
let damage_rate = excess / self.ischemia_threshold * 0.001;
self.damage += damage_rate * dt;
self.ischemia_time += dt;
} else {
self.damage = (self.damage - 0.0001 * dt).max(0.0);
}
}
pub fn is_necrotic(&self) -> bool {
self.damage >= self.necrosis_threshold
}
pub fn risk_score(&self) -> f64 {
(self.damage / self.necrosis_threshold).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone)]
pub struct BiomechanicsAnalysis {
pub joint_position: [f64; 3],
pub contact_area: f64,
pub num_muscles: usize,
pub muscle_forces: Vec<f64>,
pub moment_arms: Vec<f64>,
pub muscle_directions: Vec<[f64; 3]>,
}
impl BiomechanicsAnalysis {
pub fn new(joint_position: [f64; 3], contact_area: f64, num_muscles: usize) -> Self {
Self {
joint_position,
contact_area,
num_muscles,
muscle_forces: vec![0.0; num_muscles],
moment_arms: vec![0.0; num_muscles],
muscle_directions: vec![[1.0, 0.0, 0.0]; num_muscles],
}
}
pub fn joint_contact_pressure(&self, external_force: [f64; 3], joint_normal: [f64; 3]) -> f64 {
let f_net = self.joint_reaction_force(external_force);
let compressive = dot3(f_net, joint_normal).abs();
if self.contact_area > 1e-15 {
compressive / self.contact_area
} else {
0.0
}
}
pub fn joint_reaction_force(&self, external_force: [f64; 3]) -> [f64; 3] {
let mut f = external_force;
for (i, &fm) in self.muscle_forces.iter().enumerate() {
let dir = self.muscle_directions[i];
f[0] += fm * dir[0];
f[1] += fm * dir[1];
f[2] += fm * dir[2];
}
f
}
pub fn net_joint_torque(&self) -> f64 {
self.muscle_forces
.iter()
.zip(self.moment_arms.iter())
.map(|(&f, &r)| f * r)
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tissue_type_modulus_ordering() {
assert!(TissueType::Bone.youngs_modulus() > TissueType::Tendon.youngs_modulus());
assert!(TissueType::Tendon.youngs_modulus() > TissueType::Muscle.youngs_modulus());
assert!(TissueType::Muscle.youngs_modulus() > 0.0);
}
#[test]
fn test_muscle_fiber_passive_at_rest() {
let f = MuscleFiber::new([1.0, 0.0, 0.0], 1000.0, 0.1);
assert!(f.passive_force_length().abs() < 1e-10);
}
#[test]
fn test_muscle_fiber_active_force_at_opt_length() {
let f = MuscleFiber::new([1.0, 0.0, 0.0], 1000.0, 0.1);
let fl = f.active_force_length();
assert!((fl - 1.0).abs() < 0.01, "fl={fl}");
}
#[test]
fn test_muscle_fiber_zero_activation() {
let mut f = MuscleFiber::new([1.0, 0.0, 0.0], 1000.0, 0.1);
f.activation = 0.0;
f.length = f.l_opt * 1.2; let total = f.total_force();
let passive = f.passive_force_length() * f.f_max;
assert!(
(total - passive).abs() < 1e-6,
"total={total}, passive={passive}"
);
}
#[test]
fn test_hill_force_velocity_zero() {
let fv = hill_force_velocity(0.0, 0.1);
assert!((fv - 1.0).abs() < 1e-10, "fv={fv}");
}
#[test]
fn test_hill_force_velocity_concentric() {
let fv = hill_force_velocity(0.5, 0.1);
assert!(fv < 1.0, "fv={fv}");
assert!(fv >= 0.0);
}
#[test]
fn test_hill_model_ce_force() {
let mut hm = HillModel::new(1000.0, 0.1, 0.05, 0.1);
hm.activation = 0.5;
let f = hm.ce_force();
assert!(f > 0.0, "f={f}");
}
#[test]
fn test_tendon_zero_below_slack() {
let t = TendonModel::new(0.3, 1e6);
assert!(t.elastic_force().abs() < 1e-10);
}
#[test]
fn test_tendon_positive_above_slack() {
let mut t = TendonModel::new(0.3, 1e6);
t.length = 0.31;
assert!(t.elastic_force() > 0.0);
}
#[test]
fn test_skin_stress_at_prestretch() {
let s = SkinLayer::dermis();
let stress = s.stress(s.prestretch);
assert!(stress.abs() < 1000.0, "stress={stress}"); }
#[test]
fn test_skin_model_total_thickness() {
let skin = SkinModel::default_skin();
let total = skin.total_thickness();
assert!((total - 0.0071).abs() < 1e-5, "total={total}");
}
#[test]
fn test_cartilage_solid_stress_linear() {
let c = CartilageModel::new(1e6, 1e-15);
let s1 = c.solid_stress(0.1);
let s2 = c.solid_stress(0.2);
assert!((s2 - 2.0 * s1).abs() < 1e-6, "s1={s1}, s2={s2}");
}
#[test]
fn test_cartilage_update() {
let mut c = CartilageModel::new(1e6, 1e-9);
let stress = 200_000.0;
for _ in 0..100 {
c.update(stress, 0.1);
}
assert!(c.strain > 0.0);
}
#[test]
fn test_bone_modulus_density_scaling() {
let mut b1 = BoneRemodeling::new(500.0, 1000.0);
let mut b2 = BoneRemodeling::new(1000.0, 1000.0);
b1.density = 500.0;
b2.density = 1000.0;
assert!(b2.youngs_modulus() > b1.youngs_modulus());
}
#[test]
fn test_bone_density_increases_high_sed() {
let mut b = BoneRemodeling::new(500.0, 100.0);
let initial_density = b.density;
for _ in 0..100 {
b.update(1000.0, 0.1);
} assert!(b.density > initial_density, "density should increase");
}
#[test]
fn test_bone_density_decreases_low_sed() {
let mut b = BoneRemodeling::new(1000.0, 1000.0);
let initial_density = b.density;
for _ in 0..100 {
b.update(1.0, 0.1);
} assert!(b.density < initial_density, "density should decrease");
}
#[test]
fn test_wound_healing_area_decreases() {
let mut wh = WoundHealing::new(0.01);
let initial = wh.wound_area;
for _ in 0..1000 {
wh.update(0.1);
}
assert!(wh.wound_area < initial, "wound should shrink");
}
#[test]
fn test_wound_healing_collagen_accumulates() {
let mut wh = WoundHealing::new(0.01);
for _ in 0..100 {
wh.update(1.0);
}
assert!(wh.collagen_density > 0.0);
}
#[test]
fn test_pressure_ulcer_damage_accumulates() {
let mut pu = PressureUlcer::new(1000.0, 1.0);
pu.update(2000.0, 10.0); assert!(pu.damage > 0.0, "damage={}", pu.damage);
}
#[test]
fn test_pressure_ulcer_no_necrosis_low_pressure() {
let mut pu = PressureUlcer::new(1000.0, 1.0);
pu.update(500.0, 100.0); assert!(!pu.is_necrotic());
}
#[test]
fn test_pressure_ulcer_risk_score_bounded() {
let mut pu = PressureUlcer::new(1000.0, 1.0);
pu.update(5000.0, 1.0);
let r = pu.risk_score();
assert!((0.0..=1.0).contains(&r), "r={r}");
}
#[test]
fn test_biomechanics_reaction_no_muscles() {
let ba = BiomechanicsAnalysis::new([0.0; 3], 0.01, 0);
let f = ba.joint_reaction_force([10.0, 0.0, 0.0]);
assert!((f[0] - 10.0).abs() < 1e-10);
}
#[test]
fn test_biomechanics_contact_pressure() {
let ba = BiomechanicsAnalysis::new([0.0; 3], 0.01, 0);
let p = ba.joint_contact_pressure([0.0, -100.0, 0.0], [0.0, 1.0, 0.0]);
assert!((p - 100.0 / 0.01).abs() < 1e-6, "p={p}");
}
#[test]
fn test_biomechanics_net_torque() {
let mut ba = BiomechanicsAnalysis::new([0.0; 3], 0.01, 2);
ba.muscle_forces = vec![100.0, 50.0];
ba.moment_arms = vec![0.05, 0.03];
let tau = ba.net_joint_torque();
assert!((tau - 6.5).abs() < 1e-10, "tau={tau}");
}
#[test]
fn test_tissue_type_density_positive() {
let types = [
TissueType::Muscle,
TissueType::Tendon,
TissueType::Ligament,
TissueType::Cartilage,
TissueType::Bone,
TissueType::Skin,
];
for t in &types {
assert!(t.density() > 0.0, "density={}", t.density());
}
}
#[test]
fn test_muscle_fiber_force_vector() {
let mut f = MuscleFiber::new([1.0, 0.0, 0.0], 1000.0, 0.1);
f.activation = 0.8;
let fv = f.force_vector();
let fs = f.total_force();
let mag = norm3(fv);
assert!((mag - fs).abs() < 1e-6, "mag={mag}, fs={fs}");
}
#[test]
fn test_hill_model_update_changes_ce() {
let mut hm = HillModel::new(1000.0, 0.1, 0.05, 0.1);
hm.activation = 1.0;
let l_init = hm.l_ce;
hm.update(0.12, 1.0, 0.01);
let _changed = (hm.l_ce - l_init).abs() > 1e-12;
}
#[test]
fn test_skin_bending_stiffness_positive() {
let skin = SkinModel::default_skin();
assert!(skin.bending_stiffness() > 0.0);
}
#[test]
fn test_cartilage_creep_compliance_increases() {
let c = CartilageModel::new(1e6, 1e-15);
let j1 = c.creep_compliance(1.0);
let j2 = c.creep_compliance(10.0);
assert!(j2 >= j1, "j1={j1}, j2={j2}");
}
}
#[derive(Debug, Clone)]
pub struct VascularLayer {
pub name: String,
pub thickness: f64,
pub mu: f64,
pub alpha: f64,
pub fiber_angle: f64,
pub fiber_fraction: f64,
}
impl VascularLayer {
pub fn intima() -> Self {
Self {
name: "intima".to_string(),
thickness: 0.2e-3,
mu: 5e3,
alpha: 3.0,
fiber_angle: 0.0,
fiber_fraction: 0.1,
}
}
pub fn media() -> Self {
Self {
name: "media".to_string(),
thickness: 0.5e-3,
mu: 20e3,
alpha: 8.0,
fiber_angle: 0.15, fiber_fraction: 0.45,
}
}
pub fn adventitia() -> Self {
Self {
name: "adventitia".to_string(),
thickness: 0.3e-3,
mu: 2e3,
alpha: 15.0,
fiber_angle: 0.52, fiber_fraction: 0.60,
}
}
pub fn strain_energy(&self, lambda_theta: f64) -> f64 {
let i1 = lambda_theta * lambda_theta;
let neo = 0.5 * self.mu * (i1 - 1.0);
let fung = if self.alpha > 1e-15 {
self.mu / (2.0 * self.alpha) * ((self.alpha * (i1 - 1.0)).exp() - 1.0)
} else {
0.0
};
(1.0 - self.fiber_fraction) * neo + self.fiber_fraction * fung
}
pub fn stress(&self, lambda_theta: f64) -> f64 {
let i1 = lambda_theta * lambda_theta;
let dns = 2.0 * self.mu * lambda_theta;
let dfung = if self.alpha > 1e-15 {
self.mu * lambda_theta * (self.alpha * (i1 - 1.0)).exp()
} else {
0.0
};
(1.0 - self.fiber_fraction) * dns + self.fiber_fraction * dfung
}
}
#[derive(Debug, Clone)]
pub struct ArterialWall {
pub layers: Vec<VascularLayer>,
pub inner_radius: f64,
pub opening_angle: f64,
pub axial_prestretch: f64,
}
impl ArterialWall {
pub fn aorta() -> Self {
Self {
layers: vec![
VascularLayer::intima(),
VascularLayer::media(),
VascularLayer::adventitia(),
],
inner_radius: 12e-3,
opening_angle: 0.5, axial_prestretch: 1.1,
}
}
pub fn wall_thickness(&self) -> f64 {
self.layers.iter().map(|l| l.thickness).sum()
}
pub fn outer_radius(&self) -> f64 {
self.inner_radius + self.wall_thickness()
}
pub fn pressure_at_radius(&self, r: f64) -> f64 {
if r <= 1e-30 {
return 0.0;
}
let lambda_theta = r / self.inner_radius;
let total_stress: f64 = self.layers.iter().map(|l| l.stress(lambda_theta)).sum();
let thickness = self.wall_thickness();
total_stress * thickness / r
}
pub fn compliance_at_pressure(&self, p_ref: f64, dp: f64) -> f64 {
let r1 = self.radius_at_pressure(p_ref);
let r2 = self.radius_at_pressure(p_ref + dp);
let v1 = std::f64::consts::PI * r1 * r1;
let v2 = std::f64::consts::PI * r2 * r2;
(v2 - v1) / dp
}
pub fn radius_at_pressure(&self, target_p: f64) -> f64 {
let mut lo = self.inner_radius;
let mut hi = self.inner_radius * 3.0;
for _ in 0..50 {
let mid = 0.5 * (lo + hi);
if self.pressure_at_radius(mid) < target_p {
lo = mid;
} else {
hi = mid;
}
}
0.5 * (lo + hi)
}
}
#[derive(Debug, Clone)]
pub struct BiphasicTissue {
pub e_solid: f64,
pub nu_solid: f64,
pub permeability: f64,
pub porosity: f64,
pub perm_exponent: f64,
pub fluid_pressure: f64,
pub solid_strain: f64,
}
impl BiphasicTissue {
pub fn cartilage() -> Self {
Self {
e_solid: 0.7e6,
nu_solid: 0.3,
permeability: 1e-15,
porosity: 0.8,
perm_exponent: 0.0,
fluid_pressure: 0.0,
solid_strain: 0.0,
}
}
pub fn disc_nucleus() -> Self {
Self {
e_solid: 0.3e6,
nu_solid: 0.1,
permeability: 5e-16,
porosity: 0.88,
perm_exponent: 1.0,
fluid_pressure: 0.0,
solid_strain: 0.0,
}
}
pub fn aggregate_modulus(&self) -> f64 {
let e = self.e_solid;
let nu = self.nu_solid;
e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu))
}
pub fn characteristic_time(&self, height: f64) -> f64 {
if height <= 0.0 {
return 0.0;
}
self.aggregate_modulus() * self.permeability / (height * height)
}
pub fn creep_pressure(&self, t_normalized: f64, p0: f64) -> f64 {
let mut sum = 0.0;
for k in 1..=10_usize {
let kf = k as f64;
let coeff = 8.0 / (kf * kf * std::f64::consts::PI * std::f64::consts::PI);
sum += coeff
* (-kf * kf * std::f64::consts::PI * std::f64::consts::PI * t_normalized).exp();
}
p0 * sum
}
pub fn strain_dependent_permeability(&self, volumetric_strain: f64) -> f64 {
self.permeability * (self.perm_exponent * volumetric_strain).exp()
}
pub fn update(&mut self, applied_strain: f64, dt: f64, height: f64) {
let tau = self.characteristic_time(height);
let t_norm = if tau > 1e-30 { dt / tau } else { 1.0 };
let ha = self.aggregate_modulus();
let p0 = ha * applied_strain;
self.fluid_pressure = self.creep_pressure(t_norm, p0);
self.solid_strain = applied_strain;
}
pub fn effective_stress(&self, total_stress: f64) -> f64 {
total_stress - self.fluid_pressure
}
}
#[derive(Debug, Clone)]
pub struct TendonViscoelastic {
pub area: f64,
pub slack_length: f64,
pub epsilon_toe: f64,
pub elastic_modulus: f64,
pub toe_stiffness_factor: f64,
pub failure_strain: f64,
pub viscosity: f64,
pub prev_strain: f64,
pub prev_stress: f64,
}
impl TendonViscoelastic {
pub fn patellar_tendon() -> Self {
Self {
area: 50e-6, slack_length: 40e-3,
epsilon_toe: 0.02,
elastic_modulus: 1.5e9,
toe_stiffness_factor: 0.5,
failure_strain: 0.08,
viscosity: 1e5,
prev_strain: 0.0,
prev_stress: 0.0,
}
}
pub fn elastic_stress(&self, strain: f64) -> f64 {
if strain <= 0.0 {
return 0.0;
}
if strain <= self.epsilon_toe {
let e_toe = self.elastic_modulus * self.toe_stiffness_factor;
e_toe * strain * strain / self.epsilon_toe
} else if strain < self.failure_strain {
let toe_stress = self.elastic_modulus * self.toe_stiffness_factor * self.epsilon_toe;
toe_stress + self.elastic_modulus * (strain - self.epsilon_toe)
} else {
0.0
}
}
pub fn force(&self, length: f64) -> f64 {
let strain = (length - self.slack_length) / self.slack_length;
self.elastic_stress(strain) * self.area
}
pub fn update_stress(&mut self, new_strain: f64, dt: f64) -> f64 {
let strain_rate = if dt > 1e-15 {
(new_strain - self.prev_strain) / dt
} else {
0.0
};
let sigma_elastic = self.elastic_stress(new_strain);
let sigma_viscous = self.viscosity * strain_rate;
let total = sigma_elastic + sigma_viscous;
self.prev_strain = new_strain;
self.prev_stress = total;
total
}
pub fn is_ruptured(&self, strain: f64) -> bool {
strain >= self.failure_strain
}
pub fn stiffness_at_strain(&self, strain: f64) -> f64 {
if strain <= 0.0 || strain >= self.failure_strain {
return 0.0;
}
if strain <= self.epsilon_toe {
2.0 * self.elastic_modulus * self.toe_stiffness_factor * strain / self.epsilon_toe
} else {
self.elastic_modulus
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum WoundHealingPhase {
Hemostasis,
Inflammation,
Proliferation,
Remodeling,
Scar,
}
#[derive(Debug, Clone)]
pub struct TissueRemodeling {
pub collagen_density: f64,
pub target_density: f64,
pub remodeling_rate: f64,
pub stiffness: f64,
pub stiffness_ref: f64,
pub elapsed_days: f64,
pub phase: WoundHealingPhase,
}
impl TissueRemodeling {
pub fn new_wound(stiffness_ref: f64, remodeling_rate: f64) -> Self {
Self {
collagen_density: 0.1, target_density: 1.0,
remodeling_rate,
stiffness: stiffness_ref * 0.1,
stiffness_ref,
elapsed_days: 0.0,
phase: WoundHealingPhase::Hemostasis,
}
}
pub fn update(&mut self, dt_days: f64, strain: f64) {
self.elapsed_days += dt_days;
self.phase = if self.elapsed_days < 1.0 {
WoundHealingPhase::Hemostasis
} else if self.elapsed_days < 4.0 {
WoundHealingPhase::Inflammation
} else if self.elapsed_days < 21.0 {
WoundHealingPhase::Proliferation
} else if self.elapsed_days < 365.0 {
WoundHealingPhase::Remodeling
} else {
WoundHealingPhase::Scar
};
let optimal_strain = 0.04;
let stimulus = (-((strain - optimal_strain) / 0.02).powi(2)).exp();
self.target_density = 0.5 + 0.5 * stimulus;
let error = self.target_density - self.collagen_density;
self.collagen_density += error * self.remodeling_rate * dt_days;
self.collagen_density = self.collagen_density.clamp(0.01, 1.2);
self.stiffness = self.stiffness_ref * self.collagen_density;
}
pub fn strength_recovery(&self) -> f64 {
self.collagen_density.min(1.0)
}
}
#[derive(Debug, Clone)]
pub struct HodgkinHuxley {
pub cm: f64,
pub v_rest: f64,
pub v: f64,
pub m: f64,
pub h: f64,
pub n: f64,
pub g_na: f64,
pub g_k: f64,
pub g_l: f64,
pub e_na: f64,
pub e_k: f64,
pub e_l: f64,
}
impl HodgkinHuxley {
pub fn squid_axon() -> Self {
Self {
cm: 1.0,
v_rest: -65.0,
v: -65.0,
m: 0.05,
h: 0.6,
n: 0.32,
g_na: 120.0,
g_k: 36.0,
g_l: 0.3,
e_na: 50.0,
e_k: -77.0,
e_l: -54.4,
}
}
fn alpha_m(&self) -> f64 {
let dv = self.v + 40.0;
if dv.abs() < 1e-7 {
1.0
} else {
0.1 * dv / (1.0 - (-dv / 10.0).exp())
}
}
fn beta_m(&self) -> f64 {
4.0 * (-(self.v + 65.0) / 18.0).exp()
}
fn alpha_h(&self) -> f64 {
0.07 * (-(self.v + 65.0) / 20.0).exp()
}
fn beta_h(&self) -> f64 {
1.0 / (1.0 + (-(self.v + 35.0) / 10.0).exp())
}
fn alpha_n(&self) -> f64 {
let dv = self.v + 55.0;
if dv.abs() < 1e-7 {
0.1
} else {
0.01 * dv / (1.0 - (-dv / 10.0).exp())
}
}
fn beta_n(&self) -> f64 {
0.125 * (-(self.v + 65.0) / 80.0).exp()
}
pub fn step(&mut self, dt: f64, i_ext: f64) {
let i_na = self.g_na * self.m * self.m * self.m * self.h * (self.v - self.e_na);
let i_k = self.g_k * self.n * self.n * self.n * self.n * (self.v - self.e_k);
let i_l = self.g_l * (self.v - self.e_l);
let dv = (i_ext - i_na - i_k - i_l) / self.cm;
let dm = self.alpha_m() * (1.0 - self.m) - self.beta_m() * self.m;
let dh = self.alpha_h() * (1.0 - self.h) - self.beta_h() * self.h;
let dn = self.alpha_n() * (1.0 - self.n) - self.beta_n() * self.n;
self.v += dv * dt;
self.m = (self.m + dm * dt).clamp(0.0, 1.0);
self.h = (self.h + dh * dt).clamp(0.0, 1.0);
self.n = (self.n + dn * dt).clamp(0.0, 1.0);
}
pub fn is_firing(&self) -> bool {
self.v > -20.0
}
pub fn reset(&mut self) {
self.v = self.v_rest;
self.m = 0.05;
self.h = 0.6;
self.n = 0.32;
}
}
#[derive(Debug, Clone)]
pub struct CornealModel {
pub thickness: f64,
pub radius: f64,
pub elastic_modulus: f64,
pub poisson: f64,
pub iop: f64,
}
impl CornealModel {
pub fn normal() -> Self {
Self {
thickness: 0.55e-3, radius: 7.8e-3, elastic_modulus: 0.3e6,
poisson: 0.49, iop: 2000.0, }
}
pub fn applanation_pressure(&self, force: f64, area: f64) -> f64 {
if area <= 0.0 {
return 0.0;
}
force / area
}
pub fn bending_stiffness(&self) -> f64 {
let h = self.thickness;
self.elastic_modulus * h * h * h / (12.0 * (1.0 - self.poisson * self.poisson))
}
pub fn membrane_stiffness(&self) -> f64 {
self.elastic_modulus * self.thickness
}
pub fn laplace_stress(&self) -> f64 {
self.iop * self.radius / (2.0 * self.thickness)
}
pub fn central_deflection(&self, force: f64) -> f64 {
let d = self.bending_stiffness();
if d < 1e-30 {
return 0.0;
}
force * self.radius * self.radius / (2.0 * std::f64::consts::PI * d)
}
}
#[cfg(test)]
mod expanded_tissue_tests {
use crate::tissue_sim::ArterialWall;
use crate::tissue_sim::BiphasicTissue;
use crate::tissue_sim::CornealModel;
use crate::tissue_sim::HodgkinHuxley;
use crate::tissue_sim::TendonViscoelastic;
use crate::tissue_sim::TissueRemodeling;
use crate::tissue_sim::VascularLayer;
use crate::tissue_sim::WoundHealingPhase;
#[test]
fn test_intima_strain_energy() {
let layer = VascularLayer::intima();
let w = layer.strain_energy(1.1);
assert!(w > 0.0, "w={w}");
}
#[test]
fn test_intima_stress_positive() {
let layer = VascularLayer::media();
let sigma = layer.stress(1.2);
assert!(sigma > 0.0, "sigma={sigma}");
}
#[test]
fn test_arterial_wall_thickness() {
let wall = ArterialWall::aorta();
let total = wall.wall_thickness();
let sum: f64 = wall.layers.iter().map(|l| l.thickness).sum();
assert!((total - sum).abs() < 1e-15);
}
#[test]
fn test_arterial_pressure_positive() {
let wall = ArterialWall::aorta();
let p = wall.pressure_at_radius(wall.inner_radius * 1.1);
assert!(p > 0.0, "p={p}");
}
#[test]
fn test_arterial_radius_monotone() {
let wall = ArterialWall::aorta();
let r1 = wall.radius_at_pressure(5000.0);
let r2 = wall.radius_at_pressure(10000.0);
assert!(r2 > r1, "r1={r1}, r2={r2}");
}
#[test]
fn test_biphasic_aggregate_modulus() {
let bt = BiphasicTissue::cartilage();
let ha = bt.aggregate_modulus();
assert!(ha > 0.0, "ha={ha}");
}
#[test]
fn test_biphasic_characteristic_time() {
let bt = BiphasicTissue::cartilage();
let tau = bt.characteristic_time(2e-3);
assert!(tau > 0.0, "tau={tau}");
}
#[test]
fn test_biphasic_creep_decay() {
let bt = BiphasicTissue::cartilage();
let p1 = bt.creep_pressure(0.0, 1000.0);
let p2 = bt.creep_pressure(1.0, 1000.0);
assert!(p1 >= p2, "p1={p1}, p2={p2}");
}
#[test]
fn test_tendon_no_stress_at_zero_strain() {
let tendon = TendonViscoelastic::patellar_tendon();
assert!(tendon.elastic_stress(0.0).abs() < 1e-10);
}
#[test]
fn test_tendon_force_positive() {
let tendon = TendonViscoelastic::patellar_tendon();
let f = tendon.force(tendon.slack_length * 1.05);
assert!(f > 0.0, "f={f}");
}
#[test]
fn test_tendon_stiffness_increase() {
let tendon = TendonViscoelastic::patellar_tendon();
let k_toe = tendon.stiffness_at_strain(tendon.epsilon_toe * 0.5);
let k_lin = tendon.stiffness_at_strain(tendon.epsilon_toe * 1.5);
assert!(k_lin > k_toe, "k_toe={k_toe}, k_lin={k_lin}");
}
#[test]
fn test_tendon_rupture() {
let tendon = TendonViscoelastic::patellar_tendon();
assert!(tendon.is_ruptured(tendon.failure_strain));
assert!(!tendon.is_ruptured(tendon.failure_strain * 0.5));
}
#[test]
fn test_remodeling_phase_progression() {
let mut rem = TissueRemodeling::new_wound(1e6, 0.1);
assert_eq!(rem.phase, WoundHealingPhase::Hemostasis);
rem.update(2.0, 0.03);
assert_eq!(rem.phase, WoundHealingPhase::Inflammation);
rem.update(5.0, 0.03);
assert_eq!(rem.phase, WoundHealingPhase::Proliferation);
}
#[test]
fn test_remodeling_collagen_increase() {
let mut rem = TissueRemodeling::new_wound(1e6, 0.5);
let c0 = rem.collagen_density;
for _ in 0..30 {
rem.update(1.0, 0.04); }
assert!(rem.collagen_density > c0, "collagen should increase");
}
#[test]
fn test_hh_resting_stable() {
let mut hh = HodgkinHuxley::squid_axon();
for _ in 0..100 {
hh.step(0.01, 0.0); }
assert!((hh.v - hh.v_rest).abs() < 5.0, "v={}", hh.v);
}
#[test]
fn test_hh_action_potential() {
let mut hh = HodgkinHuxley::squid_axon();
let mut fired = false;
for _ in 0..500 {
hh.step(0.02, 10.0); if hh.is_firing() {
fired = true;
break;
}
}
assert!(fired, "Should fire an action potential with 10 µA/cm²");
}
#[test]
fn test_cornea_bending_stiffness() {
let cornea = CornealModel::normal();
let d = cornea.bending_stiffness();
assert!(d > 0.0, "d={d}");
}
#[test]
fn test_cornea_laplace_stress() {
let cornea = CornealModel::normal();
let sigma = cornea.laplace_stress();
assert!(sigma > 0.0, "sigma={sigma}");
}
#[test]
fn test_biphasic_permeability() {
let mut bt = BiphasicTissue::disc_nucleus();
bt.perm_exponent = 1.5;
let k0 = bt.strain_dependent_permeability(0.0);
let k1 = bt.strain_dependent_permeability(0.1);
assert!(k1 > k0, "k0={k0}, k1={k1}");
}
#[test]
fn test_remodeling_strength_recovery() {
let rem = TissueRemodeling::new_wound(1e6, 0.1);
assert!(rem.strength_recovery() <= 1.0);
}
#[test]
fn test_adventitia_stiffer() {
let adv = VascularLayer::adventitia();
let int = VascularLayer::intima();
assert!(adv.alpha > int.alpha, "adventitia should have larger alpha");
}
#[test]
fn test_arterial_geometry() {
let wall = ArterialWall::aorta();
assert!(wall.outer_radius() > wall.inner_radius);
}
#[test]
fn test_tendon_viscous_effect() {
let mut tendon = TendonViscoelastic::patellar_tendon();
let strain = 0.03;
let elastic = tendon.elastic_stress(strain);
let total = tendon.update_stress(strain, 0.001); assert!(total >= elastic, "Viscous effect should add stress");
}
#[test]
fn test_cornea_deflection() {
let cornea = CornealModel::normal();
let d = cornea.central_deflection(0.001);
assert!(d > 0.0, "d={d}");
}
#[test]
fn test_hh_reset() {
let mut hh = HodgkinHuxley::squid_axon();
for _ in 0..100 {
hh.step(0.02, 10.0);
}
hh.reset();
assert!((hh.v - hh.v_rest).abs() < 1e-10, "v={}", hh.v);
}
}