f439894864
- 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.
337 lines
12 KiB
Rust
337 lines
12 KiB
Rust
use super::{Organ, OrganInfo};
|
|
use crate::types::OrganType;
|
|
|
|
/// 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,
|
|
/// Respiratory rate in breaths per minute.
|
|
pub respiratory_rate_bpm: f32,
|
|
/// Peripheral oxygen saturation percent.
|
|
pub spo2_pct: f32,
|
|
/// 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 {
|
|
/// Construct lungs with a given id.
|
|
pub fn new(id: impl Into<String>) -> Self {
|
|
Self {
|
|
info: OrganInfo::new(id, OrganType::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 {
|
|
fn id(&self) -> &str {
|
|
self.info.id()
|
|
}
|
|
fn organ_type(&self) -> OrganType {
|
|
self.info.kind()
|
|
}
|
|
fn update(&mut self, dt_seconds: f32) {
|
|
if dt_seconds <= 0.0 {
|
|
return;
|
|
}
|
|
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={}, state={:?}, RR={:.0}, VT={:.0} ml, SpO2={:.0}%, PaO2~{:.0}]",
|
|
self.id(),
|
|
self.state,
|
|
self.respiratory_rate_bpm,
|
|
self.tidal_volume_ml,
|
|
self.spo2_pct,
|
|
self.alveolar_po2_mm_hg
|
|
)
|
|
}
|
|
fn as_any(&self) -> &dyn core::any::Any {
|
|
self
|
|
}
|
|
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
|
|
self
|
|
}
|
|
}
|