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) -> 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 } }