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:
+288
-12
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user