feat(organs): add detailed physiology + coupling

- Implement rich state machines and hemodynamic/metabolic models across
  organs (brain, heart, lungs, kidneys, liver, stomach, intestines,
  pancreas, gallbladder, spleen, spinal cord, bladder, esophagus)
- Add new enums for organ phases/states (e.g., SleepStage,
  VentilatoryState, CardiacRhythmState, RenalAutoregulationState,
  GastricPhase, etc.)
- Extend organ structs with explicit physiology fields; rewrite update()
  loops and summaries to reflect realistic dynamics
- Wire inter-organ signaling in Patient (oxygenation, CPP, autonomic,
  hormones, bile, bile acids, urine→bladder, gastric emptying→intestines)
  using a relax_value smoothing helper
- Minor formatting in build.rs

BREAKING CHANGE: public organ structs gained/renamed fields and updated
summaries; code using struct literals or prior field names will break.
Use constructors (e.g., new()) and updated fields; summary outputs have
changed.
This commit is contained in:
2025-09-24 01:34:34 -07:00
parent dea5049be5
commit f439894864
15 changed files with 3649 additions and 132 deletions
+288 -12
View File
@@ -1,7 +1,17 @@
use super::{Organ, OrganInfo};
use crate::types::OrganType;
/// Pulmonary model tracking respiratory rate and oxygen saturation.
/// Ventilatory operating mode reflecting dominant chemoreceptor drive.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VentilatoryState {
Resting,
HypercapnicResponse,
HypoxicResponse,
ExerciseAugmented,
MechanicalDistress,
}
/// Pulmonary model tracking ventilation mechanics and gas exchange.
#[derive(Debug, Clone)]
pub struct Lungs {
info: OrganInfo,
@@ -9,8 +19,45 @@ pub struct Lungs {
pub respiratory_rate_bpm: f32,
/// Peripheral oxygen saturation percent.
pub spo2_pct: f32,
/// Respiratory distress flag reduces SpO2.
/// Flag indicating external distress/vq mismatch triggers.
pub distress: bool,
/// Tidal volume (ml).
pub tidal_volume_ml: f32,
/// Minute ventilation (L/min).
pub minute_ventilation_l_min: f32,
/// Dead-space fraction of each breath (0..=0.5).
pub dead_space_fraction: f32,
/// Alveolar oxygen partial pressure (mmHg).
pub alveolar_po2_mm_hg: f32,
/// Alveolar carbon dioxide partial pressure (mmHg).
pub alveolar_pco2_mm_hg: f32,
/// End tidal CO2 (mmHg).
pub end_tidal_co2_mm_hg: f32,
/// Lung compliance (ml/cmH2O).
pub compliance_ml_cm_h2o: f32,
/// Airway resistance (cmH2O·s/L).
pub airway_resistance_cm_h2o_l_s: f32,
/// Respiratory muscle drive (0..=1).
pub muscle_drive: f32,
/// Chemoreceptor drive (0..=1).
pub chemoreceptor_drive: f32,
/// Ventilation/perfusion ratio.
pub ventilation_perfusion_ratio: f32,
/// Shunt fraction (0..=0.4).
pub shunt_fraction: f32,
/// Pulmonary artery pressure (mmHg).
pub pulmonary_artery_pressure_mm_hg: f32,
/// Pulmonary capillary wedge pressure (mmHg).
pub pcwp_mm_hg: f32,
/// Oxygen delivery (ml O2/min).
pub oxygen_delivery_ml_min: f32,
/// CO2 elimination (ml/min).
pub co2_elimination_ml_min: f32,
/// Functional state.
pub state: VentilatoryState,
time_in_state_s: f32,
metabolic_o2_consumption_ml_min: f32,
metabolic_co2_production_ml_min: f32,
}
impl Lungs {
@@ -21,8 +68,233 @@ impl Lungs {
respiratory_rate_bpm: 14.0,
spo2_pct: 98.0,
distress: false,
tidal_volume_ml: 500.0,
minute_ventilation_l_min: 6.5,
dead_space_fraction: 0.28,
alveolar_po2_mm_hg: 100.0,
alveolar_pco2_mm_hg: 38.0,
end_tidal_co2_mm_hg: 36.0,
compliance_ml_cm_h2o: 110.0,
airway_resistance_cm_h2o_l_s: 2.0,
muscle_drive: 0.45,
chemoreceptor_drive: 0.4,
ventilation_perfusion_ratio: 0.96,
shunt_fraction: 0.03,
pulmonary_artery_pressure_mm_hg: 18.0,
pcwp_mm_hg: 9.0,
oxygen_delivery_ml_min: 960.0,
co2_elimination_ml_min: 180.0,
state: VentilatoryState::Resting,
time_in_state_s: 0.0,
metabolic_o2_consumption_ml_min: 250.0,
metabolic_co2_production_ml_min: 200.0,
}
}
fn approach(current: f32, target: f32, rate_per_second: f32, dt_seconds: f32) -> f32 {
let rate = rate_per_second.max(0.0);
if rate == 0.0 || dt_seconds <= 0.0 {
return current;
}
let delta = target - current;
let max_step = rate * dt_seconds;
if delta > max_step {
current + max_step
} else if delta < -max_step {
current - max_step
} else {
target
}
}
fn update_metabolic_demand(&mut self, dt_seconds: f32) {
let exercise_factor =
matches!(self.state, VentilatoryState::ExerciseAugmented) as u8 as f32;
let distress_factor = if self.distress { 0.2 } else { 0.0 };
let o2_target = 250.0 * (1.0 + 0.8 * exercise_factor + distress_factor);
let co2_target = 200.0 * (1.0 + 0.9 * exercise_factor + distress_factor);
self.metabolic_o2_consumption_ml_min = Self::approach(
self.metabolic_o2_consumption_ml_min,
o2_target,
0.4,
dt_seconds,
);
self.metabolic_co2_production_ml_min = Self::approach(
self.metabolic_co2_production_ml_min,
co2_target,
0.4,
dt_seconds,
);
}
fn update_state(&mut self) {
self.state = if self.distress {
VentilatoryState::MechanicalDistress
} else if self.alveolar_pco2_mm_hg > 45.0 {
VentilatoryState::HypercapnicResponse
} else if self.alveolar_po2_mm_hg < 70.0 {
VentilatoryState::HypoxicResponse
} else if self.metabolic_o2_consumption_ml_min > 300.0 {
VentilatoryState::ExerciseAugmented
} else {
VentilatoryState::Resting
};
}
fn chemoreceptor_targets(&self) -> (f32, f32) {
match self.state {
VentilatoryState::Resting => (0.45, 0.48),
VentilatoryState::HypercapnicResponse => (0.8, 0.75),
VentilatoryState::HypoxicResponse => (0.85, 0.82),
VentilatoryState::ExerciseAugmented => (0.9, 0.7),
VentilatoryState::MechanicalDistress => (0.95, 0.65),
}
}
fn update_drives(&mut self, dt_seconds: f32) {
let (chemo_target, muscle_target) = self.chemoreceptor_targets();
let hypoxia_error = (90.0 - self.alveolar_po2_mm_hg).max(0.0) / 40.0;
let hypercapnia_error = (self.alveolar_pco2_mm_hg - 40.0).max(0.0) / 20.0;
let drive_boost = (hypoxia_error + hypercapnia_error).clamp(0.0, 1.0);
self.chemoreceptor_drive = Self::approach(
self.chemoreceptor_drive,
(chemo_target + 0.6 * drive_boost).clamp(0.2, 1.0),
0.8,
dt_seconds,
);
self.muscle_drive = Self::approach(
self.muscle_drive,
(muscle_target + 0.5 * drive_boost).clamp(0.2, 1.0),
0.6,
dt_seconds,
);
}
fn update_mechanics(&mut self, dt_seconds: f32) {
let rate_target = (12.0
+ 18.0 * self.muscle_drive
+ 6.0 * (self.chemoreceptor_drive - 0.5).max(0.0)
+ 8.0 * matches!(self.state, VentilatoryState::MechanicalDistress) as i32 as f32)
.clamp(8.0, 40.0);
self.respiratory_rate_bpm =
Self::approach(self.respiratory_rate_bpm, rate_target, 1.5, dt_seconds);
let compliance_target = if self.distress {
65.0
} else {
110.0 - 20.0 * (self.muscle_drive - 0.5).max(0.0)
}
.clamp(40.0, 140.0);
self.compliance_ml_cm_h2o = Self::approach(
self.compliance_ml_cm_h2o,
compliance_target,
0.2,
dt_seconds,
);
let resistance_target = if self.distress {
4.5
} else {
2.0 + 1.5 * (0.4 - self.compliance_ml_cm_h2o / 150.0).max(0.0)
}
.clamp(1.2, 6.0);
self.airway_resistance_cm_h2o_l_s = Self::approach(
self.airway_resistance_cm_h2o_l_s,
resistance_target,
0.3,
dt_seconds,
);
let tidal_target = (450.0 + 160.0 * (self.muscle_drive - 0.4)
- 50.0 * self.airway_resistance_cm_h2o_l_s)
.clamp(250.0, 900.0);
self.tidal_volume_ml = Self::approach(self.tidal_volume_ml, tidal_target, 30.0, dt_seconds);
self.dead_space_fraction = Self::approach(
self.dead_space_fraction,
(0.28
+ 0.15 * (self.airway_resistance_cm_h2o_l_s - 2.0).max(0.0)
+ 0.1 * self.shunt_fraction)
.clamp(0.15, 0.5),
0.2,
dt_seconds,
);
let alveolar_ventilation = (self.tidal_volume_ml * (1.0 - self.dead_space_fraction)
/ 1000.0)
* self.respiratory_rate_bpm;
self.minute_ventilation_l_min =
(self.tidal_volume_ml / 1000.0 * self.respiratory_rate_bpm).clamp(3.0, 25.0);
self.ventilation_perfusion_ratio = Self::approach(
self.ventilation_perfusion_ratio,
(alveolar_ventilation / 5.0).clamp(0.4, 1.4),
0.3,
dt_seconds,
);
}
fn update_gas_exchange(&mut self, dt_seconds: f32) {
let effective_ventilation =
self.minute_ventilation_l_min * (1.0 - self.dead_space_fraction);
let po2_target = (100.0 + 12.0 * (effective_ventilation - 5.5)
- 30.0 * self.shunt_fraction
- 15.0 * (1.0 - self.ventilation_perfusion_ratio))
.clamp(40.0, 120.0);
let pco2_target = (40.0 - 5.0 * (effective_ventilation - 6.0) + 10.0 * self.shunt_fraction)
.clamp(25.0, 60.0);
self.alveolar_po2_mm_hg =
Self::approach(self.alveolar_po2_mm_hg, po2_target, 0.5, dt_seconds);
self.alveolar_pco2_mm_hg =
Self::approach(self.alveolar_pco2_mm_hg, pco2_target, 0.5, dt_seconds);
self.end_tidal_co2_mm_hg = Self::approach(
self.end_tidal_co2_mm_hg,
self.alveolar_pco2_mm_hg,
1.2,
dt_seconds,
);
let spo2_target = (97.0 + 8.0 * (self.alveolar_po2_mm_hg - 90.0) / 40.0
- 12.0 * self.shunt_fraction
- 5.0 * (self.metabolic_o2_consumption_ml_min - 250.0) / 200.0)
.clamp(70.0, 100.0);
self.spo2_pct = Self::approach(self.spo2_pct, spo2_target, 0.6, dt_seconds);
self.shunt_fraction = Self::approach(
self.shunt_fraction,
(0.03
+ 0.2 * (1.0 - self.ventilation_perfusion_ratio).max(0.0)
+ if self.distress { 0.12 } else { 0.0 })
.clamp(0.0, 0.35),
0.4,
dt_seconds,
);
self.oxygen_delivery_ml_min = Self::approach(
self.oxygen_delivery_ml_min,
self.spo2_pct * 10.0,
2.0,
dt_seconds,
);
self.co2_elimination_ml_min = Self::approach(
self.co2_elimination_ml_min,
(self.metabolic_co2_production_ml_min
* (self.minute_ventilation_l_min / 6.0).clamp(0.5, 2.0))
.clamp(80.0, 600.0),
1.5,
dt_seconds,
);
}
fn update_pressures(&mut self, dt_seconds: f32) {
let pap_target = (18.0
+ 8.0 * (self.shunt_fraction - 0.05).max(0.0)
+ 4.0 * (self.minute_ventilation_l_min - 6.0) / 6.0)
.clamp(12.0, 35.0);
self.pulmonary_artery_pressure_mm_hg = Self::approach(
self.pulmonary_artery_pressure_mm_hg,
pap_target,
0.2,
dt_seconds,
);
self.pcwp_mm_hg = Self::approach(
self.pcwp_mm_hg,
(8.0 + 0.5 * (self.pulmonary_artery_pressure_mm_hg - 18.0)).clamp(5.0, 18.0),
0.2,
dt_seconds,
);
}
}
impl Organ for Lungs {
@@ -33,22 +305,26 @@ impl Organ for Lungs {
self.info.kind()
}
fn update(&mut self, dt_seconds: f32) {
let dt = dt_seconds.clamp(0.0, 10.0);
let target_rr = 14.0;
self.respiratory_rate_bpm += 0.1 * (target_rr - self.respiratory_rate_bpm) * (dt / 1.0);
// distress drifts SpO2 downward
if self.distress {
self.spo2_pct -= 0.5 * (dt / 1.0);
if dt_seconds <= 0.0 {
return;
}
// keep spo2 in [70, 100]
self.spo2_pct = self.spo2_pct.clamp(70.0, 100.0);
self.time_in_state_s += dt_seconds;
self.update_metabolic_demand(dt_seconds);
self.update_state();
self.update_drives(dt_seconds);
self.update_mechanics(dt_seconds);
self.update_gas_exchange(dt_seconds);
self.update_pressures(dt_seconds);
}
fn summary(&self) -> String {
format!(
"Lungs[id={}, RR={:.1} bpm, SpO2={:.0}%]",
"Lungs[id={}, state={:?}, RR={:.0}, VT={:.0} ml, SpO2={:.0}%, PaO2~{:.0}]",
self.id(),
self.state,
self.respiratory_rate_bpm,
self.spo2_pct
self.tidal_volume_ml,
self.spo2_pct,
self.alveolar_po2_mm_hg
)
}
fn as_any(&self) -> &dyn core::any::Any {