use std::f64::consts::PI;
type Vec3 = [f64; 3];
#[inline]
fn dot3(a: Vec3, b: Vec3) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn norm3(v: Vec3) -> f64 {
dot3(v, v).sqrt()
}
#[inline]
fn normalize3(v: Vec3) -> Vec3 {
let n = norm3(v);
if n < 1e-15 {
[0.0; 3]
} else {
[v[0] / n, v[1] / n, v[2] / n]
}
}
#[inline]
fn add3(a: Vec3, b: Vec3) -> Vec3 {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn sub3(a: Vec3, b: Vec3) -> Vec3 {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn scale3(v: Vec3, s: f64) -> Vec3 {
[v[0] * s, v[1] * s, v[2] * s]
}
#[inline]
fn dist3(a: Vec3, b: Vec3) -> f64 {
norm3(sub3(a, b))
}
#[inline]
fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
if x < lo {
lo
} else if x > hi {
hi
} else {
x
}
}
#[derive(Debug, Clone)]
pub struct CorticalTension {
pub tension: f64,
pub internal_pressure: f64,
pub radius: f64,
pub bending_modulus: f64,
pub spontaneous_curvature: f64,
}
impl CorticalTension {
pub fn new(
tension: f64,
radius: f64,
bending_modulus: f64,
spontaneous_curvature: f64,
) -> Self {
let internal_pressure = if radius > 1e-15 {
2.0 * tension / radius
} else {
0.0
};
Self {
tension,
internal_pressure,
radius,
bending_modulus,
spontaneous_curvature,
}
}
pub fn young_laplace_pressure(&self) -> f64 {
if self.radius > 1e-15 {
2.0 * self.tension / self.radius
} else {
0.0
}
}
pub fn helfrich_energy(&self) -> f64 {
let h = if self.radius > 1e-15 {
1.0 / self.radius
} else {
0.0
};
let dh = h - self.spontaneous_curvature;
8.0 * PI * self.bending_modulus * dh * dh * self.radius * self.radius
}
pub fn equatorial_line_force(&self) -> f64 {
PI * self.radius * self.tension
}
pub fn equilibrium_radius(&self, dp: f64) -> f64 {
if dp > 1e-15 {
2.0 * self.tension / dp
} else {
f64::INFINITY
}
}
pub fn radial_spring_constant(&self) -> f64 {
if self.radius > 1e-15 {
2.0 * self.tension / (self.radius * self.radius)
} else {
0.0
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellCyclePhase {
G1,
S,
G2,
M,
}
#[derive(Debug, Clone)]
pub struct CellDivision {
pub position: Vec3,
pub radius: f64,
pub phase: CellCyclePhase,
pub phase_time: f64,
pub phase_durations: [f64; 4],
pub contractile_ring_tension: f64,
pub furrow_fraction: f64,
pub divided: bool,
}
impl CellDivision {
pub fn new(
position: Vec3,
radius: f64,
phase_durations: [f64; 4],
contractile_ring_tension: f64,
) -> Self {
Self {
position,
radius,
phase: CellCyclePhase::G1,
phase_time: 0.0,
phase_durations,
contractile_ring_tension,
furrow_fraction: 0.0,
divided: false,
}
}
pub fn step(&mut self, dt: f64) -> Option<(Vec3, Vec3)> {
if self.divided {
return None;
}
self.phase_time += dt;
let duration = match self.phase {
CellCyclePhase::G1 => self.phase_durations[0],
CellCyclePhase::S => self.phase_durations[1],
CellCyclePhase::G2 => self.phase_durations[2],
CellCyclePhase::M => self.phase_durations[3],
};
if self.phase_time >= duration {
self.phase_time = 0.0;
self.phase = match self.phase {
CellCyclePhase::G1 => CellCyclePhase::S,
CellCyclePhase::S => CellCyclePhase::G2,
CellCyclePhase::G2 => CellCyclePhase::M,
CellCyclePhase::M => {
self.divided = true;
let offset = self.radius * 0.6;
let da = [
self.position[0] + offset,
self.position[1],
self.position[2],
];
let db = [
self.position[0] - offset,
self.position[1],
self.position[2],
];
return Some((da, db));
}
};
}
if self.phase == CellCyclePhase::M {
let frac = self.phase_time / duration;
self.furrow_fraction = clamp(frac, 0.0, 1.0);
}
None
}
pub fn equatorial_radius(&self) -> f64 {
self.radius * (1.0 - self.furrow_fraction)
}
pub fn contractile_force(&self) -> f64 {
2.0 * PI * self.equatorial_radius() * self.contractile_ring_tension
}
}
#[derive(Debug, Clone)]
pub struct Filopodium {
pub tip: Vec3,
pub base: Vec3,
pub protrusion_speed: f64,
pub retraction_speed: f64,
pub tension: f64,
pub adhered: bool,
}
impl Filopodium {
pub fn new(
base: Vec3,
direction: Vec3,
initial_length: f64,
protrusion_speed: f64,
retraction_speed: f64,
tension: f64,
) -> Self {
let d = normalize3(direction);
let tip = add3(base, scale3(d, initial_length));
Self {
tip,
base,
protrusion_speed,
retraction_speed,
tension,
adhered: false,
}
}
pub fn length(&self) -> f64 {
dist3(self.tip, self.base)
}
pub fn direction(&self) -> Vec3 {
normalize3(sub3(self.tip, self.base))
}
pub fn protrude(&mut self, dt: f64) -> f64 {
let d = self.direction();
self.tip = add3(self.tip, scale3(d, self.protrusion_speed * dt));
self.length()
}
pub fn retract(&mut self, dt: f64) -> f64 {
let len = self.length();
let retraction = self.retraction_speed * dt;
if retraction >= len {
self.tip = self.base;
return 0.0;
}
let d = self.direction();
self.tip = sub3(self.tip, scale3(d, retraction));
self.length()
}
pub fn traction_force(&self) -> Vec3 {
if !self.adhered {
return [0.0; 3];
}
let d = normalize3(sub3(self.base, self.tip));
scale3(d, self.tension)
}
}
#[derive(Debug, Clone)]
pub struct Lamellipodium {
pub half_width: f64,
pub protrusion_velocity: f64,
pub retrograde_flow: f64,
pub traction_stress: f64,
pub leading_edge: f64,
}
impl Lamellipodium {
pub fn new(
initial_edge: f64,
half_width: f64,
protrusion_velocity: f64,
retrograde_flow: f64,
traction_stress: f64,
) -> Self {
Self {
half_width,
protrusion_velocity,
retrograde_flow,
traction_stress,
leading_edge: initial_edge,
}
}
pub fn net_velocity(&self) -> f64 {
self.protrusion_velocity - self.retrograde_flow
}
pub fn step(&mut self, dt: f64) {
self.leading_edge += self.net_velocity() * dt;
}
pub fn total_traction(&self, width_depth: f64) -> f64 {
self.traction_stress * 2.0 * self.half_width * width_depth
}
}
#[derive(Debug, Clone)]
pub struct CadherinBond {
pub rest_length: f64,
pub stiffness: f64,
pub k_off_zero: f64,
pub bell_force: f64,
pub engaged: bool,
}
impl CadherinBond {
pub fn new(rest_length: f64, stiffness: f64, k_off_zero: f64, bell_force: f64) -> Self {
Self {
rest_length,
stiffness,
k_off_zero,
bell_force,
engaged: true,
}
}
pub fn force(&self, d: f64) -> f64 {
if !self.engaged {
return 0.0;
}
self.stiffness * (d - self.rest_length).abs()
}
pub fn off_rate(&self, force: f64) -> f64 {
self.k_off_zero * (force / self.bell_force).exp()
}
pub fn survival_probability(&self, force: f64, dt: f64) -> f64 {
(-self.off_rate(force) * dt).exp()
}
pub fn update_stochastic(&mut self, force: f64, dt: f64, r: f64) {
if self.engaged && r > self.survival_probability(force, dt) {
self.engaged = false;
}
}
}
#[derive(Debug, Clone)]
pub struct CellAdhesion {
pub pos_a: Vec3,
pub pos_b: Vec3,
pub bonds: Vec<CadherinBond>,
}
impl CellAdhesion {
pub fn new(
pos_a: Vec3,
pos_b: Vec3,
n_bonds: usize,
rest_length: f64,
stiffness: f64,
k_off_zero: f64,
bell_force: f64,
) -> Self {
let bonds = (0..n_bonds)
.map(|_| CadherinBond::new(rest_length, stiffness, k_off_zero, bell_force))
.collect();
Self {
pos_a,
pos_b,
bonds,
}
}
pub fn active_bond_count(&self) -> usize {
self.bonds.iter().filter(|b| b.engaged).count()
}
pub fn total_force(&self) -> f64 {
let d = dist3(self.pos_a, self.pos_b);
self.bonds
.iter()
.filter(|b| b.engaged)
.map(|b| b.force(d))
.sum()
}
pub fn adhesion_direction(&self) -> Vec3 {
normalize3(sub3(self.pos_b, self.pos_a))
}
pub fn force_on_a(&self) -> Vec3 {
scale3(self.adhesion_direction(), self.total_force())
}
}
#[derive(Debug, Clone)]
pub struct ExtracellularMatrix {
pub young_modulus: f64,
pub poisson_ratio: f64,
pub relaxation_time: f64,
pub strain_stiffening: f64,
pub strain: f64,
pub collagen_fraction: f64,
}
impl ExtracellularMatrix {
pub fn new(
young_modulus: f64,
poisson_ratio: f64,
relaxation_time: f64,
strain_stiffening: f64,
collagen_fraction: f64,
) -> Self {
Self {
young_modulus,
poisson_ratio,
relaxation_time,
strain_stiffening,
strain: 0.0,
collagen_fraction,
}
}
pub fn effective_stiffness(&self) -> f64 {
self.young_modulus * (self.strain_stiffening * self.strain).exp()
}
pub fn stress(&self) -> f64 {
self.effective_stiffness() * self.strain
}
pub fn shear_modulus(&self) -> f64 {
self.young_modulus / (2.0 * (1.0 + self.poisson_ratio))
}
pub fn bulk_modulus(&self) -> f64 {
self.young_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
}
pub fn relax(&mut self, dt: f64) {
let decay = (-dt / self.relaxation_time).exp();
self.strain *= decay;
}
pub fn composite_stiffness(&self) -> f64 {
let e_fibre = 1.5e9_f64;
(1.0 - self.collagen_fraction) * self.young_modulus + self.collagen_fraction * e_fibre
}
}
#[derive(Debug, Clone)]
pub struct WoundHealing {
pub wound_width: f64,
pub edge_velocity: f64,
pub cable_tension: f64,
pub proliferation_rate: f64,
pub migration_speed: f64,
pub time: f64,
pub width_history: Vec<f64>,
}
impl WoundHealing {
pub fn new(
initial_width: f64,
cable_tension: f64,
proliferation_rate: f64,
migration_speed: f64,
) -> Self {
Self {
wound_width: initial_width,
edge_velocity: 0.0,
cable_tension,
proliferation_rate,
migration_speed,
time: 0.0,
width_history: vec![initial_width],
}
}
pub fn step(&mut self, dt: f64, viscosity: f64) {
let purse_string_velocity = if self.wound_width > 1e-10 {
self.cable_tension / (viscosity * self.wound_width)
} else {
0.0
};
let closure_rate = 2.0 * (self.migration_speed + purse_string_velocity);
let new_width = (self.wound_width - closure_rate * dt).max(0.0);
self.edge_velocity = closure_rate;
self.wound_width = new_width;
self.time += dt;
self.width_history.push(new_width);
}
pub fn is_closed(&self) -> bool {
self.wound_width < 1e-9
}
pub fn estimated_closure_time(&self) -> f64 {
if self.migration_speed > 1e-15 {
self.width_history[0] / (2.0 * self.migration_speed)
} else {
f64::INFINITY
}
}
pub fn closure_fraction(&self) -> f64 {
let w0 = self.width_history[0];
if w0 > 1e-15 {
(w0 - self.wound_width) / w0
} else {
1.0
}
}
}
#[derive(Debug, Clone)]
pub struct TumorGrowth {
pub radius: f64,
pub growth_rate: f64,
pub tissue_stiffness: f64,
pub osmotic_pressure: f64,
pub interstitial_pressure: f64,
pub necrotic_radius: f64,
}
impl TumorGrowth {
pub fn new(
initial_radius: f64,
growth_rate: f64,
tissue_stiffness: f64,
osmotic_pressure: f64,
) -> Self {
Self {
radius: initial_radius,
growth_rate,
tissue_stiffness,
osmotic_pressure,
interstitial_pressure: 0.0,
necrotic_radius: 0.0,
}
}
pub fn step(&mut self, dt: f64, max_stress: f64) {
let sigma = self.radial_stress_at_surface();
let inhibition = clamp(1.0 - sigma / max_stress, 0.0, 1.0);
self.radius += self.growth_rate * self.radius * inhibition * dt;
self.interstitial_pressure = self.osmotic_pressure * (self.radius / (self.radius + 1e-6));
let diffusion_limit = 2e-4;
if self.radius > diffusion_limit {
self.necrotic_radius = self.radius - diffusion_limit;
}
}
pub fn radial_stress_at_surface(&self) -> f64 {
self.tissue_stiffness * self.radius / 3.0
}
pub fn volume(&self) -> f64 {
4.0 / 3.0 * PI * self.radius.powi(3)
}
pub fn viable_volume(&self) -> f64 {
let v_total = self.volume();
let v_necrotic = 4.0 / 3.0 * PI * self.necrotic_radius.powi(3);
(v_total - v_necrotic).max(0.0)
}
pub fn doubling_time(&self) -> f64 {
if self.growth_rate > 1e-15 {
2_f64.ln() / self.growth_rate
} else {
f64::INFINITY
}
}
}
#[derive(Debug, Clone)]
pub struct SpectrinSpring {
pub nodes: [usize; 2],
pub rest_length: f64,
pub persistence_length: f64,
pub contour_length: f64,
}
impl SpectrinSpring {
pub fn new(
nodes: [usize; 2],
rest_length: f64,
persistence_length: f64,
contour_length: f64,
) -> Self {
Self {
nodes,
rest_length,
persistence_length,
contour_length,
}
}
pub fn wlc_force(&self, extension: f64) -> f64 {
let kt = 4.28e-21_f64; let x = clamp(extension, 0.0, self.contour_length * 0.9999);
let s = x / self.contour_length;
kt / (4.0 * self.persistence_length) * (1.0 / (1.0 - s).powi(2) - 1.0 + 4.0 * s)
}
pub fn force_at_length(&self, d: f64) -> f64 {
self.wlc_force(d)
}
}
#[derive(Debug, Clone)]
pub struct RedBloodCell {
pub nodes: Vec<Vec3>,
pub springs: Vec<SpectrinSpring>,
pub bending_modulus: f64,
pub area_modulus: f64,
pub target_area: f64,
pub target_volume: f64,
}
impl RedBloodCell {
pub fn new(
nodes: Vec<Vec3>,
springs: Vec<SpectrinSpring>,
bending_modulus: f64,
area_modulus: f64,
target_area: f64,
target_volume: f64,
) -> Self {
Self {
nodes,
springs,
bending_modulus,
area_modulus,
target_area,
target_volume,
}
}
pub fn elastic_energy(&self) -> f64 {
self.springs
.iter()
.map(|s| {
let d = dist3(self.nodes[s.nodes[0]], self.nodes[s.nodes[1]]);
let k = 1e-5_f64; 0.5 * k * (d - s.rest_length).powi(2)
})
.sum()
}
pub fn biconcave_ratio(&self) -> f64 {
if self.nodes.is_empty() {
return 0.0;
}
let max_r = self
.nodes
.iter()
.map(|p| (p[0] * p[0] + p[2] * p[2]).sqrt())
.fold(0.0_f64, f64::max);
let max_z = self
.nodes
.iter()
.map(|p| p[1].abs())
.fold(0.0_f64, f64::max);
if max_r > 1e-15 { max_z / max_r } else { 0.0 }
}
pub fn area_constraint_energy(&self, current_area: f64) -> f64 {
self.area_modulus * 0.5 * (current_area - self.target_area).powi(2) / self.target_area
}
pub fn reduced_volume(&self, current_volume: f64, current_area: f64) -> f64 {
if current_area < 1e-30 {
return 0.0;
}
let r_eff = (current_area / (4.0 * PI)).sqrt();
let v_sphere = 4.0 / 3.0 * PI * r_eff.powi(3);
if v_sphere > 1e-30 {
current_volume / v_sphere
} else {
0.0
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlateletState {
Resting,
Activated,
Aggregated,
}
#[derive(Debug, Clone)]
pub struct Platelet {
pub position: Vec3,
pub velocity: Vec3,
pub state: PlateletState,
pub activation_level: f64,
pub gpib_density: f64,
pub integrin_active_fraction: f64,
}
impl Platelet {
pub fn new(position: Vec3, gpib_density: f64) -> Self {
Self {
position,
velocity: [0.0; 3],
state: PlateletState::Resting,
activation_level: 0.0,
gpib_density,
integrin_active_fraction: 0.05,
}
}
pub fn activate(&mut self) {
self.state = PlateletState::Activated;
self.activation_level = 1.0;
self.integrin_active_fraction = 0.9;
}
pub fn step(&mut self, dt: f64, force: Vec3, r_disc: f64, viscosity: f64) {
let gamma = 16.0 * viscosity * r_disc;
for (i, &fi) in force.iter().enumerate() {
let a = (fi - gamma * self.velocity[i]) / 1e-14; self.velocity[i] += a * dt;
self.position[i] += self.velocity[i] * dt;
}
}
pub fn agonist_activation_increment(&self, c_adp: f64, dt: f64) -> f64 {
let k_max = 2.0_f64; let k_d = 5e-6_f64; let n = 1.5_f64;
k_max * c_adp.powf(n) / (k_d.powf(n) + c_adp.powf(n)) * dt
}
}
#[derive(Debug, Clone)]
pub struct PlateletAggregation {
pub platelets: Vec<Platelet>,
pub clot_radius: f64,
pub wall_shear_rate: f64,
pub adp_concentration: f64,
}
impl PlateletAggregation {
pub fn new(n_platelets: usize, wall_shear_rate: f64, adp_concentration: f64) -> Self {
let platelets = (0..n_platelets)
.map(|i| {
let x = (i as f64) * 2e-6;
Platelet::new([x, 0.0, 0.0], 5e13_f64)
})
.collect();
Self {
platelets,
clot_radius: 0.0,
wall_shear_rate,
adp_concentration,
}
}
pub fn activated_count(&self) -> usize {
self.platelets
.iter()
.filter(|p| p.state != PlateletState::Resting)
.count()
}
pub fn coverage_area(&self) -> f64 {
PI * self.clot_radius.powi(2)
}
pub fn step(&mut self, dt: f64) {
let c = self.adp_concentration;
for p in &mut self.platelets {
let inc = p.agonist_activation_increment(c, dt);
p.activation_level += inc;
if p.activation_level > 0.5 && p.state == PlateletState::Resting {
p.activate();
}
}
let active = self.activated_count() as f64;
self.clot_radius = (active * 2e-6 / PI).sqrt();
}
}
#[derive(Debug, Clone)]
pub struct BoneRemodeling {
pub density: f64,
pub reference_sed: f64,
pub remodeling_rate: f64,
pub dead_zone: f64,
pub density_min: f64,
pub density_max: f64,
pub von_mises_stress: f64,
pub young_modulus: f64,
}
impl BoneRemodeling {
pub fn new(
initial_density: f64,
reference_sed: f64,
remodeling_rate: f64,
dead_zone: f64,
) -> Self {
let young_modulus = Self::currey_modulus(initial_density);
Self {
density: initial_density,
reference_sed,
remodeling_rate,
dead_zone,
density_min: 0.01,
density_max: 2100.0,
von_mises_stress: 0.0,
young_modulus,
}
}
pub fn currey_modulus(density: f64) -> f64 {
7.88e-3 * density.powf(3.09)
}
pub fn strain_energy_density(&self, stress: f64) -> f64 {
if self.young_modulus > 1e-10 {
stress * stress / (2.0 * self.young_modulus)
} else {
0.0
}
}
pub fn remodeling_stimulus(&self, sed: f64) -> f64 {
let delta = sed - self.reference_sed;
if delta.abs() <= self.dead_zone {
0.0
} else if delta > 0.0 {
delta - self.dead_zone
} else {
delta + self.dead_zone
}
}
pub fn step(&mut self, dt: f64, stress: f64) {
self.von_mises_stress = stress;
let sed = self.strain_energy_density(stress);
let stimulus = self.remodeling_stimulus(sed);
self.density += self.remodeling_rate * stimulus * dt;
self.density = clamp(self.density, self.density_min, self.density_max);
self.young_modulus = Self::currey_modulus(self.density);
}
pub fn osteoblast_activity(&self, sed: f64) -> f64 {
let s = self.remodeling_stimulus(sed);
clamp(s / (self.reference_sed + 1.0), 0.0, 1.0)
}
pub fn osteoclast_activity(&self, sed: f64) -> f64 {
let s = -self.remodeling_stimulus(sed);
clamp(s / (self.reference_sed + 1.0), 0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-10;
#[test]
fn test_cortical_tension_young_laplace() {
let ct = CorticalTension::new(5e-4, 10e-6, 2e-19, 0.0);
let dp = ct.young_laplace_pressure();
assert!((dp - 100.0).abs() < 1e-6, "dp={dp}");
}
#[test]
fn test_cortical_tension_helfrich_energy_positive() {
let ct = CorticalTension::new(5e-4, 10e-6, 2e-19, 0.0);
let e = ct.helfrich_energy();
assert!(e >= 0.0, "Helfrich energy must be non-negative, got {e}");
}
#[test]
fn test_cortical_tension_equatorial_force() {
let ct = CorticalTension::new(1.0, 1.0, 1e-19, 0.0);
let f = ct.equatorial_line_force();
assert!((f - PI).abs() < EPS, "f={f}");
}
#[test]
fn test_cortical_tension_equilibrium_radius() {
let ct = CorticalTension::new(2.0, 1.0, 1e-19, 0.0);
let r = ct.equilibrium_radius(4.0); assert!((r - 1.0).abs() < EPS, "r={r}");
}
#[test]
fn test_cortical_tension_spring_constant() {
let ct = CorticalTension::new(1.0, 2.0, 1e-19, 0.0);
let k = ct.radial_spring_constant();
assert!((k - 0.5).abs() < EPS, "k={k}"); }
#[test]
fn test_cell_division_phase_progression() {
let durations = [100.0, 200.0, 50.0, 50.0];
let mut cell = CellDivision::new([0.0; 3], 10e-6, durations, 1e-3);
assert_eq!(cell.phase, CellCyclePhase::G1);
for _ in 0..110 {
cell.step(1.0);
}
assert_eq!(cell.phase, CellCyclePhase::S);
}
#[test]
fn test_cell_division_produces_daughters() {
let durations = [0.1, 0.1, 0.1, 0.1];
let mut cell = CellDivision::new([0.0, 0.0, 0.0], 10e-6, durations, 1e-3);
let mut daughters: Option<(Vec3, Vec3)> = None;
for _ in 0..100 {
if let Some(d) = cell.step(0.05) {
daughters = Some(d);
break;
}
}
assert!(daughters.is_some(), "Cell should have divided");
let (da, db) = daughters.unwrap();
assert!(dist3(da, db) > 1e-10, "Daughters should be separated");
}
#[test]
fn test_cell_division_contractile_force_positive() {
let mut cell = CellDivision::new([0.0; 3], 10e-6, [0.1, 0.1, 0.1, 1.0], 1e-3);
for _ in 0..30 {
cell.step(0.1);
}
let f = cell.contractile_force();
assert!(f >= 0.0, "Contractile force must be non-negative: {f}");
}
#[test]
fn test_filopodium_protrudes() {
let mut filo = Filopodium::new([0.0; 3], [1.0, 0.0, 0.0], 1e-6, 1e-7, 5e-8, 1e-11);
let len_before = filo.length();
filo.protrude(1.0);
assert!(filo.length() > len_before, "Filopodium should grow");
}
#[test]
fn test_filopodium_retracts() {
let mut filo = Filopodium::new([0.0; 3], [1.0, 0.0, 0.0], 1e-6, 1e-7, 5e-8, 1e-11);
let len_before = filo.length();
filo.retract(1.0);
assert!(filo.length() <= len_before, "Filopodium should retract");
}
#[test]
fn test_filopodium_traction_zero_unadhered() {
let filo = Filopodium::new([0.0; 3], [1.0, 0.0, 0.0], 1e-6, 1e-7, 5e-8, 1e-11);
let f = filo.traction_force();
assert!(
norm3(f) < EPS,
"Unadhered filopodium traction should be zero"
);
}
#[test]
fn test_lamellipodium_edge_advances() {
let mut lam = Lamellipodium::new(0.0, 5e-6, 1e-7, 3e-8, 100.0);
lam.step(10.0);
assert!(lam.leading_edge > 0.0, "Edge should advance");
}
#[test]
fn test_lamellipodium_net_velocity() {
let lam = Lamellipodium::new(0.0, 5e-6, 1e-7, 3e-8, 100.0);
let v = lam.net_velocity();
assert!((v - (1e-7 - 3e-8)).abs() < EPS, "v={v}");
}
#[test]
fn test_cadherin_bond_force_at_rest() {
let bond = CadherinBond::new(10e-9, 1e-3, 0.1, 5e-12);
let f = bond.force(10e-9);
assert!(f.abs() < EPS, "At rest length force should be zero: {f}");
}
#[test]
fn test_cadherin_bond_force_stretched() {
let bond = CadherinBond::new(10e-9, 1e-3, 0.1, 5e-12);
let f = bond.force(20e-9);
assert!(f > 0.0, "Stretched bond should have positive force");
}
#[test]
fn test_cadherin_bond_survival_probability_unity_at_zero_force() {
let bond = CadherinBond::new(10e-9, 1e-3, 1e-3, 5e-12);
let p = bond.survival_probability(0.0, 0.0); assert!((p - 1.0).abs() < EPS, "P at dt=0 should be 1: {p}");
}
#[test]
fn test_cell_adhesion_active_bonds_initial() {
let adh = CellAdhesion::new([0.0; 3], [1e-5, 0.0, 0.0], 10, 10e-9, 1e-3, 0.1, 5e-12);
assert_eq!(adh.active_bond_count(), 10);
}
#[test]
fn test_cell_adhesion_force_direction() {
let adh = CellAdhesion::new([0.0; 3], [1e-5, 0.0, 0.0], 5, 10e-9, 1e-3, 0.1, 5e-12);
let f = adh.force_on_a();
assert!(
f[0] > 0.0 || adh.total_force() == 0.0,
"Force on A should point towards B"
);
}
#[test]
fn test_ecm_effective_stiffness_unity_at_zero_strain() {
let ecm = ExtracellularMatrix::new(1000.0, 0.3, 10.0, 1.0, 0.1);
let e = ecm.effective_stiffness();
assert!((e - 1000.0).abs() < EPS, "At zero strain E_eff = E0: {e}");
}
#[test]
fn test_ecm_strain_stiffening() {
let mut ecm = ExtracellularMatrix::new(1000.0, 0.3, 10.0, 2.0, 0.0);
ecm.strain = 0.5;
let e = ecm.effective_stiffness();
let expected = 1000.0 * (2.0_f64 * 0.5).exp();
assert!(
(e - expected).abs() < 1e-6,
"Strain stiffening mismatch: {e} vs {expected}"
);
}
#[test]
fn test_ecm_relaxation_decreases_strain() {
let mut ecm = ExtracellularMatrix::new(1000.0, 0.3, 10.0, 1.0, 0.0);
ecm.strain = 0.1;
let before = ecm.strain;
ecm.relax(1.0);
assert!(ecm.strain < before, "Strain should relax");
}
#[test]
fn test_ecm_composite_stiffness_exceeds_matrix() {
let ecm = ExtracellularMatrix::new(1000.0, 0.3, 10.0, 1.0, 0.3);
let e_c = ecm.composite_stiffness();
assert!(
e_c > ecm.young_modulus,
"Composite stiffness should exceed matrix: {e_c}"
);
}
#[test]
fn test_wound_healing_closes() {
let mut wh = WoundHealing::new(100e-6, 1e-4, 1e-4, 2e-7);
for _ in 0..1000 {
wh.step(1.0, 1e3);
}
assert!(
wh.wound_width < 100e-6,
"Wound should have partially closed"
);
}
#[test]
fn test_wound_healing_closure_fraction_increases() {
let mut wh = WoundHealing::new(100e-6, 1e-4, 1e-4, 2e-7);
let f0 = wh.closure_fraction();
wh.step(100.0, 1e3);
let f1 = wh.closure_fraction();
assert!(f1 >= f0, "Closure fraction should not decrease");
}
#[test]
fn test_wound_healing_estimated_closure_time_positive() {
let wh = WoundHealing::new(100e-6, 1e-4, 1e-4, 2e-7);
let t = wh.estimated_closure_time();
assert!(
t > 0.0 && t.is_finite(),
"Closure time should be positive finite: {t}"
);
}
#[test]
fn test_tumor_growth_volume_positive() {
let tg = TumorGrowth::new(1e-3, 1e-5, 100.0, 1000.0);
assert!(tg.volume() > 0.0, "Volume must be positive");
}
#[test]
fn test_tumor_growth_step_increases_radius() {
let mut tg = TumorGrowth::new(1e-4, 1e-5, 10.0, 100.0);
let r0 = tg.radius;
tg.step(3600.0, 1e10); assert!(tg.radius >= r0, "Tumor should grow");
}
#[test]
fn test_tumor_growth_doubling_time_consistent() {
let tg = TumorGrowth::new(1e-3, 1.0, 100.0, 0.0);
let td = tg.doubling_time();
assert!((td - 2_f64.ln()).abs() < 1e-10, "Doubling time: {td}");
}
#[test]
fn test_rbc_elastic_energy_non_negative() {
let nodes = vec![[0.0_f64; 3], [1e-6, 0.0, 0.0]];
let springs = vec![SpectrinSpring::new([0, 1], 1e-6, 20e-9, 75e-9)];
let rbc = RedBloodCell::new(nodes, springs, 2e-19, 4.8e-6, 135e-12, 94e-18);
let e = rbc.elastic_energy();
assert!(e >= 0.0, "Elastic energy must be non-negative: {e}");
}
#[test]
fn test_rbc_area_constraint_zero_at_target() {
let rbc = RedBloodCell::new(vec![], vec![], 2e-19, 4.8e-6, 135e-12, 94e-18);
let e = rbc.area_constraint_energy(135e-12);
assert!(
e.abs() < EPS,
"Area constraint energy should be zero at target: {e}"
);
}
#[test]
fn test_rbc_wlc_force_increases_with_extension() {
let s = SpectrinSpring::new([0, 1], 40e-9, 20e-9, 75e-9);
let f1 = s.wlc_force(10e-9);
let f2 = s.wlc_force(60e-9);
assert!(
f2 > f1,
"WLC force should increase with extension: f1={f1}, f2={f2}"
);
}
#[test]
fn test_platelet_initially_resting() {
let pa = PlateletAggregation::new(5, 1000.0, 1e-6);
assert_eq!(pa.activated_count(), 0);
}
#[test]
fn test_platelet_activation_step() {
let mut pa = PlateletAggregation::new(5, 1000.0, 1e-3);
pa.step(10.0);
assert!(
pa.activated_count() > 0,
"Some platelets should activate at high ADP"
);
}
#[test]
fn test_platelet_coverage_area_grows() {
let mut pa = PlateletAggregation::new(20, 1000.0, 1e-3);
pa.step(1.0);
pa.step(5.0);
let a1 = pa.coverage_area();
pa.step(20.0);
let a2 = pa.coverage_area();
assert!(a2 >= a1, "Coverage area should grow: a1={a1}, a2={a2}");
}
#[test]
fn test_bone_remodeling_modulus_positive() {
let br = BoneRemodeling::new(1800.0, 5000.0, 1e-3, 500.0);
assert!(br.young_modulus > 0.0, "Modulus must be positive");
}
#[test]
fn test_bone_remodeling_density_increases_under_load() {
let mut br = BoneRemodeling::new(1000.0, 1000.0, 1.0, 100.0);
let rho0 = br.density;
for _ in 0..100 {
br.step(1.0, 1e8);
}
assert!(
br.density >= rho0,
"Density should increase under high load"
);
}
#[test]
fn test_bone_remodeling_density_bounded() {
let mut br = BoneRemodeling::new(1800.0, 1000.0, 100.0, 0.0);
for _ in 0..10000 {
br.step(1.0, 1e9);
}
assert!(
br.density <= br.density_max,
"Density must not exceed maximum"
);
assert!(
br.density >= br.density_min,
"Density must not go below minimum"
);
}
#[test]
fn test_bone_remodeling_dead_zone_no_change() {
let mut br = BoneRemodeling::new(1000.0, 5000.0, 1.0, 1e9);
let rho0 = br.density;
br.step(1.0, 0.0); assert!(
(br.density - rho0).abs() < EPS,
"In dead zone density should not change"
);
}
#[test]
fn test_bone_remodeling_stimulus_outside_dead_zone() {
let br = BoneRemodeling::new(1000.0, 1000.0, 1.0, 100.0);
let stim = br.remodeling_stimulus(2000.0); assert!(
stim > 0.0,
"Stimulus should be positive above dead zone: {stim}"
);
}
}