feat(lungs): add breathing cycle and diaphragm kinematics
Introduce a time-based breathing cycle with phases and diaphragm movement to improve realism and observability. - Add BreathingPhase enum (Inhalation, Exhalation, Pause) - Compute phase fractions/durations from RR and chemoreceptor drive - Advance phases over time and update diaphragm position/velocity - Call update_breath_cycle from Lungs::update - Extend summary with phase and diaphragm position - Re-export BreathingPhase and Lungs in lib and organs mod - Add unit test for phase transitions and kinematics No breaking changes; public API gains a new enum and fields.
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@ pub mod ffi;
|
||||
|
||||
pub use crate::ekg::{EkgLead, EkgMonitor, EkgSnapshot, HeartElectricalState};
|
||||
pub use crate::error::MedicalError;
|
||||
pub use crate::organs::{Heart, Organ};
|
||||
pub use crate::organs::{BreathingPhase, Heart, Lungs, Organ};
|
||||
pub use crate::patient::Patient;
|
||||
pub use crate::types::{Blood, BloodPressure, OrganType};
|
||||
|
||||
|
||||
+201
-2
@@ -1,6 +1,17 @@
|
||||
use super::{Organ, OrganInfo};
|
||||
use crate::types::OrganType;
|
||||
|
||||
/// High-level breathing cycle state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BreathingPhase {
|
||||
/// Active inspiration with diaphragmatic contraction.
|
||||
Inhalation,
|
||||
/// Passive expiration and recoil.
|
||||
Exhalation,
|
||||
/// Brief end-expiratory pause before the next breath.
|
||||
Pause,
|
||||
}
|
||||
|
||||
/// Ventilatory operating mode reflecting dominant chemoreceptor drive.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VentilatoryState {
|
||||
@@ -58,14 +69,27 @@ pub struct Lungs {
|
||||
time_in_state_s: f32,
|
||||
metabolic_o2_consumption_ml_min: f32,
|
||||
metabolic_co2_production_ml_min: f32,
|
||||
/// Current respiratory cycle phase.
|
||||
pub breathing_phase: BreathingPhase,
|
||||
phase_elapsed_s: f32,
|
||||
current_phase_duration_s: f32,
|
||||
breath_period_s: f32,
|
||||
/// Diaphragm displacement fraction (0 relaxed, 1 fully contracted).
|
||||
pub diaphragm_position: f32,
|
||||
/// Rate of diaphragm displacement change per second.
|
||||
pub diaphragm_velocity: f32,
|
||||
}
|
||||
|
||||
impl Lungs {
|
||||
/// Construct lungs with a given id.
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
let respiratory_rate_bpm = 14.0_f32;
|
||||
let breath_period_s = (60.0_f32 / respiratory_rate_bpm).clamp(0.5, 12.0);
|
||||
let inhale_fraction = 0.42;
|
||||
let inhale_duration = breath_period_s * inhale_fraction;
|
||||
Self {
|
||||
info: OrganInfo::new(id, OrganType::Lungs),
|
||||
respiratory_rate_bpm: 14.0,
|
||||
respiratory_rate_bpm,
|
||||
spo2_pct: 98.0,
|
||||
distress: false,
|
||||
tidal_volume_ml: 500.0,
|
||||
@@ -88,6 +112,12 @@ impl Lungs {
|
||||
time_in_state_s: 0.0,
|
||||
metabolic_o2_consumption_ml_min: 250.0,
|
||||
metabolic_co2_production_ml_min: 200.0,
|
||||
breathing_phase: BreathingPhase::Inhalation,
|
||||
phase_elapsed_s: 0.0,
|
||||
current_phase_duration_s: inhale_duration.max(0.1),
|
||||
breath_period_s,
|
||||
diaphragm_position: 0.0,
|
||||
diaphragm_velocity: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +258,130 @@ impl Lungs {
|
||||
);
|
||||
}
|
||||
|
||||
fn breath_phase_fractions(&self) -> (f32, f32, f32) {
|
||||
let base_inhale = match self.state {
|
||||
VentilatoryState::Resting => 0.42,
|
||||
VentilatoryState::HypercapnicResponse => 0.36,
|
||||
VentilatoryState::HypoxicResponse => 0.38,
|
||||
VentilatoryState::ExerciseAugmented => 0.46,
|
||||
VentilatoryState::MechanicalDistress => 0.32,
|
||||
};
|
||||
let base_pause = match self.state {
|
||||
VentilatoryState::Resting => 0.06,
|
||||
VentilatoryState::HypercapnicResponse => 0.03,
|
||||
VentilatoryState::HypoxicResponse => 0.04,
|
||||
VentilatoryState::ExerciseAugmented => 0.0,
|
||||
VentilatoryState::MechanicalDistress => 0.02,
|
||||
};
|
||||
let drive_adjust = (self.muscle_drive - 0.5).clamp(-0.5, 0.5);
|
||||
let inhale = (base_inhale + 0.1 * drive_adjust).clamp(0.25, 0.55);
|
||||
let pause = (base_pause - 0.05 * drive_adjust.max(0.0)).clamp(0.0, 0.1);
|
||||
let exhale = (1.0 - inhale - pause).clamp(0.25, 0.7);
|
||||
let total = inhale + exhale + pause;
|
||||
if (total - 1.0).abs() < 1e-4 {
|
||||
(inhale, exhale, pause)
|
||||
} else {
|
||||
let norm = if total <= 0.0 { 1.0 } else { total };
|
||||
(inhale / norm, exhale / norm, pause / norm)
|
||||
}
|
||||
}
|
||||
|
||||
fn phase_durations(&self) -> (f32, f32, f32) {
|
||||
let rate = self.respiratory_rate_bpm.clamp(4.0, 45.0);
|
||||
let period = (60.0 / rate).clamp(0.6, 15.0);
|
||||
let (inhale_frac, _exhale_frac, pause_frac) = self.breath_phase_fractions();
|
||||
let mut inhale = (period * inhale_frac).max(0.12);
|
||||
let mut pause = (period * pause_frac).max(0.0);
|
||||
let mut exhale = (period - inhale - pause).max(0.2);
|
||||
let total = inhale + exhale + pause;
|
||||
let scale = if total > 0.0 { period / total } else { 1.0 };
|
||||
inhale *= scale;
|
||||
exhale *= scale;
|
||||
pause *= scale;
|
||||
(inhale, exhale, pause)
|
||||
}
|
||||
|
||||
fn advance_phase(&mut self, pause_duration: f32) {
|
||||
self.breathing_phase = match self.breathing_phase {
|
||||
BreathingPhase::Inhalation => BreathingPhase::Exhalation,
|
||||
BreathingPhase::Exhalation => {
|
||||
if pause_duration <= 1e-3 {
|
||||
BreathingPhase::Inhalation
|
||||
} else {
|
||||
BreathingPhase::Pause
|
||||
}
|
||||
}
|
||||
BreathingPhase::Pause => BreathingPhase::Inhalation,
|
||||
};
|
||||
self.phase_elapsed_s = 0.0;
|
||||
}
|
||||
|
||||
fn easing(progress: f32) -> f32 {
|
||||
let p = progress.clamp(0.0, 1.0);
|
||||
p * p * (3.0 - 2.0 * p)
|
||||
}
|
||||
|
||||
fn update_breath_cycle(&mut self, dt_seconds: f32) {
|
||||
if dt_seconds <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let total_dt = dt_seconds;
|
||||
let (inhale_duration, exhale_duration, pause_duration) = self.phase_durations();
|
||||
self.breath_period_s = inhale_duration + exhale_duration + pause_duration;
|
||||
|
||||
let previous_duration = self.current_phase_duration_s.max(1e-6);
|
||||
let mut phase_progress = (self.phase_elapsed_s / previous_duration).clamp(0.0, 1.0);
|
||||
|
||||
let mut current_duration = match self.breathing_phase {
|
||||
BreathingPhase::Inhalation => inhale_duration,
|
||||
BreathingPhase::Exhalation => exhale_duration,
|
||||
BreathingPhase::Pause => pause_duration.max(1e-3),
|
||||
};
|
||||
if current_duration <= 0.0 {
|
||||
current_duration = 1e-3;
|
||||
}
|
||||
self.current_phase_duration_s = current_duration;
|
||||
self.phase_elapsed_s = phase_progress * current_duration;
|
||||
|
||||
let mut remaining = dt_seconds;
|
||||
while remaining > 0.0 {
|
||||
let duration = self.current_phase_duration_s.max(1e-3);
|
||||
let time_left = (duration - self.phase_elapsed_s).max(0.0);
|
||||
if remaining < time_left {
|
||||
self.phase_elapsed_s += remaining;
|
||||
remaining = 0.0;
|
||||
} else {
|
||||
remaining -= time_left;
|
||||
self.advance_phase(pause_duration);
|
||||
let new_duration = match self.breathing_phase {
|
||||
BreathingPhase::Inhalation => inhale_duration,
|
||||
BreathingPhase::Exhalation => exhale_duration,
|
||||
BreathingPhase::Pause => pause_duration.max(1e-3),
|
||||
}
|
||||
.max(1e-3);
|
||||
self.current_phase_duration_s = new_duration;
|
||||
}
|
||||
}
|
||||
|
||||
let current_duration = self.current_phase_duration_s.max(1e-3);
|
||||
phase_progress = (self.phase_elapsed_s / current_duration).clamp(0.0, 1.0);
|
||||
let start_position = self.diaphragm_position;
|
||||
let target_position = match self.breathing_phase {
|
||||
BreathingPhase::Inhalation => Self::easing(phase_progress),
|
||||
BreathingPhase::Exhalation => 1.0 - Self::easing(phase_progress),
|
||||
BreathingPhase::Pause => {
|
||||
if pause_duration <= 1e-3 {
|
||||
0.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
};
|
||||
self.diaphragm_position = target_position.clamp(0.0, 1.0);
|
||||
self.diaphragm_velocity = (self.diaphragm_position - start_position) / total_dt;
|
||||
}
|
||||
|
||||
fn update_gas_exchange(&mut self, dt_seconds: f32) {
|
||||
let effective_ventilation =
|
||||
self.minute_ventilation_l_min * (1.0 - self.dead_space_fraction);
|
||||
@@ -313,15 +467,18 @@ impl Organ for Lungs {
|
||||
self.update_state();
|
||||
self.update_drives(dt_seconds);
|
||||
self.update_mechanics(dt_seconds);
|
||||
self.update_breath_cycle(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}]",
|
||||
"Lungs[id={}, state={:?}, phase={:?}, RR={:.0}, diaphragm={:.2}, VT={:.0} ml, SpO2={:.0}%, PaO2~{:.0}]",
|
||||
self.id(),
|
||||
self.state,
|
||||
self.breathing_phase,
|
||||
self.respiratory_rate_bpm,
|
||||
self.diaphragm_position,
|
||||
self.tidal_volume_ml,
|
||||
self.spo2_pct,
|
||||
self.alveolar_po2_mm_hg
|
||||
@@ -334,3 +491,45 @@ impl Organ for Lungs {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn breathing_cycle_advances_and_moves_diaphragm() {
|
||||
let mut lungs = Lungs::new("test-lungs");
|
||||
assert_eq!(lungs.breathing_phase, BreathingPhase::Inhalation);
|
||||
|
||||
let mut seen_exhalation = false;
|
||||
let mut seen_pause = false;
|
||||
let mut min_position = f32::MAX;
|
||||
let mut max_position = f32::MIN;
|
||||
let mut max_velocity: f32 = 0.0;
|
||||
|
||||
for _ in 0..20 {
|
||||
lungs.update(0.5);
|
||||
min_position = min_position.min(lungs.diaphragm_position);
|
||||
max_position = max_position.max(lungs.diaphragm_position);
|
||||
max_velocity = max_velocity.max(lungs.diaphragm_velocity.abs());
|
||||
match lungs.breathing_phase {
|
||||
BreathingPhase::Exhalation => seen_exhalation = true,
|
||||
BreathingPhase::Pause => seen_pause = true,
|
||||
BreathingPhase::Inhalation => {}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(seen_exhalation, "expected cycle to reach exhalation phase");
|
||||
assert!(seen_pause, "expected cycle to reach post-breath pause");
|
||||
assert!(max_position <= 1.0 + 1e-3, "diaphragm position upper bound");
|
||||
assert!(min_position >= -1e-3, "diaphragm position lower bound");
|
||||
assert!(
|
||||
max_position - min_position > 0.3,
|
||||
"diaphragm should sweep a meaningful range"
|
||||
);
|
||||
assert!(
|
||||
max_velocity > 0.05,
|
||||
"diaphragm velocity should become non-zero"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ pub use heart::{CardiacRhythmState, Heart};
|
||||
pub use intestines::Intestines;
|
||||
pub use kidneys::Kidneys;
|
||||
pub use liver::Liver;
|
||||
pub use lungs::Lungs;
|
||||
pub use lungs::{BreathingPhase, Lungs};
|
||||
pub use pancreas::Pancreas;
|
||||
pub use spinal_cord::SpinalCord;
|
||||
pub use spleen::Spleen;
|
||||
|
||||
Reference in New Issue
Block a user