use std::time::Instant;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RespiratoryPhase {
Inspiration,
EndInspiratory,
Expiration,
EndExpiratory,
}
pub struct LungState {
pub volume: u8,
pub flow: i16,
pub phase: RespiratoryPhase,
pub phase_start: Instant,
pub current_tidal_volume: u8,
}
impl LungState {
pub fn new(config: &RespiratoryConfig, now: Instant) -> Self {
Self {
volume: config.frc,
flow: 0,
phase: RespiratoryPhase::EndExpiratory,
phase_start: now,
current_tidal_volume: config.base_tidal_volume,
}
}
pub fn phase_progress_256(&self, now: Instant, config: &RespiratoryConfig) -> u8 {
let elapsed_us = now.duration_since(self.phase_start).as_micros() as u64;
let phase_duration_us = match self.phase {
RespiratoryPhase::Inspiration => config.inspiration_us,
RespiratoryPhase::EndInspiratory => config.end_inspiratory_pause_us,
RespiratoryPhase::Expiration => config.expiration_us,
RespiratoryPhase::EndExpiratory => config.end_expiratory_pause_us,
};
if phase_duration_us == 0 {
return 255;
}
let progress = (elapsed_us * 255) / phase_duration_us;
progress.min(255) as u8
}
pub fn update(
&mut self,
now: Instant,
generator: &mut RespiratoryGenerator,
config: &RespiratoryConfig,
) -> bool {
let elapsed_us = now.duration_since(self.phase_start).as_micros() as u64;
match self.phase {
RespiratoryPhase::Inspiration => {
let target_volume = config.frc.saturating_add(self.current_tidal_volume);
let volume_range = self.current_tidal_volume as u64;
if config.inspiration_us > 0 && volume_range > 0 {
let progress = (elapsed_us * volume_range) / config.inspiration_us;
self.volume = config.frc.saturating_add(progress.min(volume_range) as u8);
let flow = (volume_range * 1_000_000) / config.inspiration_us;
self.flow = flow.min(i16::MAX as u64) as i16;
}
if elapsed_us >= config.inspiration_us {
self.volume = target_volume;
self.flow = 0;
self.phase = RespiratoryPhase::EndInspiratory;
self.phase_start = now;
}
false
}
RespiratoryPhase::EndInspiratory => {
self.flow = 0;
if elapsed_us >= config.end_inspiratory_pause_us {
self.phase = RespiratoryPhase::Expiration;
self.phase_start = now;
}
false
}
RespiratoryPhase::Expiration => {
let peak_volume = config.frc.saturating_add(self.current_tidal_volume);
let volume_range = self.current_tidal_volume as u64;
if config.expiration_us > 0 && volume_range > 0 {
let progress = (elapsed_us * volume_range) / config.expiration_us;
let descent = progress.min(volume_range) as u8;
self.volume = peak_volume.saturating_sub(descent);
let flow = (volume_range * 1_000_000) / config.expiration_us;
self.flow = -(flow.min(i16::MAX as u64) as i16);
}
if elapsed_us >= config.expiration_us {
self.volume = config.frc;
self.flow = 0;
self.phase = RespiratoryPhase::EndExpiratory;
self.phase_start = now;
generator.reset_drive();
}
false
}
RespiratoryPhase::EndExpiratory => {
self.flow = 0;
self.volume = config.frc;
let drive = (generator.drive_rate_per_sec as u64 * elapsed_us) / 1_000_000;
generator.drive = drive.min(255) as u8;
if generator.drive >= generator.effective_threshold
&& elapsed_us >= config.end_expiratory_pause_us
{
self.phase = RespiratoryPhase::Inspiration;
self.phase_start = now;
generator.reset_drive();
return true; }
false
}
}
}
}
pub struct RespiratoryGenerator {
pub drive: u8,
pub drive_rate_per_sec: u32,
pub effective_threshold: u8,
base_drive_rate_per_sec: u32,
}
impl RespiratoryGenerator {
pub fn new(config: &RespiratoryConfig) -> Self {
Self {
drive: 0,
drive_rate_per_sec: config.base_drive_rate_per_sec,
effective_threshold: config.base_drive_threshold,
base_drive_rate_per_sec: config.base_drive_rate_per_sec,
}
}
pub fn reset_drive(&mut self) {
self.drive = 0;
}
pub fn apply_modulation(
&mut self,
ne: u8,
ach: u8,
co2: u8,
o2: u8,
config: &RespiratoryConfig,
) {
let base = self.base_drive_rate_per_sec as u64;
let ne_boost = (base * ne as u64 * config.ne_sensitivity as u64) / (255 * 255);
let ach_brake = (base * ach as u64 * config.ach_sensitivity as u64) / (255 * 255 * 2);
let modulated = (base as i64) + (ne_boost as i64) - (ach_brake as i64);
let floor = (base / 4).max(1) as i64; let ceiling = (base * 4) as i64;
self.drive_rate_per_sec = modulated.clamp(floor, ceiling) as u32;
let co2_excess = co2.saturating_sub(config.co2_baseline);
let co2_drive = (co2_excess as u32 * config.co2_sensitivity as u32) / 255;
let o2_drive = if o2 < config.o2_hypoxic_threshold {
let deficit = config.o2_hypoxic_threshold.saturating_sub(o2);
(deficit as u32 * config.o2_sensitivity as u32) / 255
} else {
0
};
let threshold = config.base_drive_threshold as i32
- co2_drive as i32
- o2_drive as i32;
self.effective_threshold = threshold
.clamp(config.min_drive_threshold as i32, config.base_drive_threshold as i32)
as u8;
}
}
pub struct GasPool {
pub co2: u8,
pub o2: u8,
last_metabolism: Instant,
}
impl GasPool {
pub fn new(config: &RespiratoryConfig, now: Instant) -> Self {
Self {
co2: config.co2_baseline,
o2: config.o2_baseline,
last_metabolism: now,
}
}
pub fn metabolize(
&mut self,
now: Instant,
ventilating: bool,
tidal_volume: u8,
config: &RespiratoryConfig,
) {
let elapsed_us = now.duration_since(self.last_metabolism).as_micros() as u64;
if elapsed_us < 250_000 {
return;
}
self.last_metabolism = now;
let co2_produced =
(config.co2_production_rate_per_sec as u64 * elapsed_us) / 1_000_000;
self.co2 = self.co2.saturating_add(co2_produced.min(255) as u8);
let o2_consumed =
(config.o2_consumption_rate_per_sec as u64 * elapsed_us) / 1_000_000;
self.o2 = self.o2.saturating_sub(o2_consumed.min(255) as u8);
if ventilating {
let tidal_ratio_x256 = if config.base_tidal_volume > 0 {
(tidal_volume as u64 * 256) / config.base_tidal_volume as u64
} else {
256
};
let base_clearance =
(config.co2_clearance_rate_per_sec as u64 * elapsed_us) / 1_000_000;
let effective_clearance = (base_clearance * tidal_ratio_x256) / 256;
if self.co2 > config.co2_baseline {
let distance = (self.co2 - config.co2_baseline) as u64;
let cleared = effective_clearance.min(distance);
self.co2 = self.co2.saturating_sub(cleared as u8);
}
let base_replenishment =
(config.o2_replenishment_rate_per_sec as u64 * elapsed_us) / 1_000_000;
let effective_replenishment = (base_replenishment * tidal_ratio_x256) / 256;
if self.o2 < config.o2_baseline {
let distance = (config.o2_baseline - self.o2) as u64;
let replenished = effective_replenishment.min(distance);
self.o2 = self.o2.saturating_add(replenished as u8);
}
}
}
}
pub fn compute_tidal_volume(ne: u8, ach: u8, co2: u8, config: &RespiratoryConfig) -> u8 {
let base = config.base_tidal_volume as i32;
let ne_depth = (base * ne as i32 * config.ne_depth_sensitivity as i32) / (255 * 255);
let ach_shallow = (base * ach as i32 * config.ach_depth_sensitivity as i32) / (255 * 255 * 2);
let co2_excess = co2.saturating_sub(config.co2_baseline) as i32;
let co2_depth = (base * co2_excess * config.co2_sensitivity as i32) / (255 * 255);
let modulated = base + ne_depth - ach_shallow + co2_depth;
modulated.clamp(config.min_tidal_volume as i32, config.max_tidal_volume as i32) as u8
}
#[derive(Clone, Debug)]
pub struct RespiratoryConfig {
pub inspiration_us: u64,
pub end_inspiratory_pause_us: u64,
pub expiration_us: u64,
pub end_expiratory_pause_us: u64,
pub frc: u8,
pub base_tidal_volume: u8,
pub max_tidal_volume: u8,
pub min_tidal_volume: u8,
pub base_drive_rate_per_sec: u32,
pub base_drive_threshold: u8,
pub min_drive_threshold: u8,
pub ne_sensitivity: u8,
pub ach_sensitivity: u8,
pub ne_depth_sensitivity: u8,
pub ach_depth_sensitivity: u8,
pub co2_baseline: u8,
pub o2_baseline: u8,
pub co2_sensitivity: u8,
pub o2_hypoxic_threshold: u8,
pub o2_sensitivity: u8,
pub co2_production_rate_per_sec: u32,
pub o2_consumption_rate_per_sec: u32,
pub co2_clearance_rate_per_sec: u32,
pub o2_replenishment_rate_per_sec: u32,
pub rsa_enabled: bool,
pub rsa_vagal_baseline: u8,
pub rsa_depth: u8,
pub rsa_expiratory_augmentation: u8,
}
impl Default for RespiratoryConfig {
fn default() -> Self {
Self {
inspiration_us: 1_600_000,
end_inspiratory_pause_us: 100_000,
expiration_us: 2_200_000,
end_expiratory_pause_us: 100_000,
frc: 75,
base_tidal_volume: 30,
max_tidal_volume: 120,
min_tidal_volume: 10,
base_drive_rate_per_sec: 64,
base_drive_threshold: 128,
min_drive_threshold: 40,
ne_sensitivity: 128,
ach_sensitivity: 96,
ne_depth_sensitivity: 100,
ach_depth_sensitivity: 80,
co2_baseline: 40,
o2_baseline: 200,
co2_sensitivity: 128,
o2_hypoxic_threshold: 120,
o2_sensitivity: 64,
co2_production_rate_per_sec: 20,
o2_consumption_rate_per_sec: 15,
co2_clearance_rate_per_sec: 35,
o2_replenishment_rate_per_sec: 28,
rsa_enabled: true,
rsa_vagal_baseline: 30,
rsa_depth: 180,
rsa_expiratory_augmentation: 15,
}
}
}
pub fn compute_vagal_modulation(
phase: RespiratoryPhase,
phase_progress_256: u8,
config: &RespiratoryConfig,
) -> u8 {
let baseline = config.rsa_vagal_baseline;
let depth = config.rsa_depth;
let max_reduction = (baseline as u16 * depth as u16) / 255;
let min_vagal = baseline.saturating_sub(max_reduction as u8);
let augmented = baseline.saturating_add(config.rsa_expiratory_augmentation);
match phase {
RespiratoryPhase::Inspiration => {
let reduction =
(max_reduction * phase_progress_256 as u16) / 255;
baseline.saturating_sub(reduction as u8)
}
RespiratoryPhase::EndInspiratory => {
min_vagal
}
RespiratoryPhase::Expiration => {
let range = augmented.saturating_sub(min_vagal) as u16;
let current =
min_vagal as u16 + (range * phase_progress_256 as u16) / 255;
current.min(255) as u8
}
RespiratoryPhase::EndExpiratory => {
augmented
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn phase_transitions_are_sequential() {
let config = RespiratoryConfig::default();
let base = Instant::now();
let mut lung = LungState::new(&config, base);
let mut gen = RespiratoryGenerator::new(&config);
gen.effective_threshold = 1;
gen.drive_rate_per_sec = 10000;
let mut phases = vec![lung.phase];
for ms in 1..10000u64 {
let now = base + Duration::from_millis(ms);
lung.update(now, &mut gen, &config);
if *phases.last().unwrap() != lung.phase {
phases.push(lung.phase);
}
if phases.len() >= 6 {
break;
}
}
assert!(phases.len() >= 5, "Got phases: {:?}", phases);
assert_eq!(phases[0], RespiratoryPhase::EndExpiratory);
assert_eq!(phases[1], RespiratoryPhase::Inspiration);
assert_eq!(phases[2], RespiratoryPhase::EndInspiratory);
assert_eq!(phases[3], RespiratoryPhase::Expiration);
assert_eq!(phases[4], RespiratoryPhase::EndExpiratory);
}
#[test]
fn volume_rises_during_inspiration() {
let config = RespiratoryConfig::default();
let base = Instant::now();
let mut lung = LungState::new(&config, base);
let mut gen = RespiratoryGenerator::new(&config);
lung.phase = RespiratoryPhase::Inspiration;
lung.phase_start = base;
lung.volume = config.frc;
let now = base + Duration::from_millis(800);
lung.update(now, &mut gen, &config);
assert!(lung.volume > config.frc, "Volume should rise during inspiration");
assert!(lung.flow > 0, "Flow should be positive (inspiratory)");
}
#[test]
fn volume_falls_during_expiration() {
let config = RespiratoryConfig::default();
let base = Instant::now();
let mut lung = LungState::new(&config, base);
let mut gen = RespiratoryGenerator::new(&config);
let peak = config.frc.saturating_add(config.base_tidal_volume);
lung.phase = RespiratoryPhase::Expiration;
lung.phase_start = base;
lung.volume = peak;
let now = base + Duration::from_millis(1100);
lung.update(now, &mut gen, &config);
assert!(lung.volume < peak, "Volume should fall during expiration");
assert!(lung.flow < 0, "Flow should be negative (expiratory)");
}
#[test]
fn cpg_drive_accumulates_during_pause() {
let config = RespiratoryConfig::default();
let base = Instant::now();
let mut lung = LungState::new(&config, base);
let mut gen = RespiratoryGenerator::new(&config);
assert_eq!(lung.phase, RespiratoryPhase::EndExpiratory);
assert_eq!(gen.drive, 0);
let now = base + Duration::from_secs(1);
lung.update(now, &mut gen, &config);
assert!(gen.drive > 0, "CPG drive should accumulate during EndExpiratory");
assert_eq!(gen.drive, 64);
}
#[test]
fn co2_lowers_cpg_threshold() {
let config = RespiratoryConfig::default();
let mut gen = RespiratoryGenerator::new(&config);
gen.apply_modulation(0, 0, config.co2_baseline, config.o2_baseline, &config);
assert_eq!(gen.effective_threshold, config.base_drive_threshold);
gen.apply_modulation(0, 0, config.co2_baseline + 50, config.o2_baseline, &config);
assert!(
gen.effective_threshold < config.base_drive_threshold,
"High CO2 should lower threshold: got {}",
gen.effective_threshold
);
}
#[test]
fn ne_increases_drive_rate() {
let config = RespiratoryConfig::default();
let mut gen = RespiratoryGenerator::new(&config);
gen.apply_modulation(0, 0, config.co2_baseline, config.o2_baseline, &config);
let resting_rate = gen.drive_rate_per_sec;
gen.apply_modulation(200, 0, config.co2_baseline, config.o2_baseline, &config);
assert!(
gen.drive_rate_per_sec > resting_rate,
"NE should increase drive rate"
);
}
#[test]
fn ach_decreases_drive_rate() {
let config = RespiratoryConfig::default();
let mut gen = RespiratoryGenerator::new(&config);
gen.apply_modulation(0, 0, config.co2_baseline, config.o2_baseline, &config);
let resting_rate = gen.drive_rate_per_sec;
gen.apply_modulation(0, 200, config.co2_baseline, config.o2_baseline, &config);
assert!(
gen.drive_rate_per_sec < resting_rate,
"ACh should decrease drive rate"
);
}
#[test]
fn gas_exchange_clears_co2_during_ventilation() {
let config = RespiratoryConfig::default();
let base = Instant::now();
let mut gas = GasPool::new(&config, base);
gas.co2 = 80;
let now = base + Duration::from_millis(500);
gas.metabolize(now, true, config.base_tidal_volume, &config);
assert!(
gas.co2 < 80,
"Ventilation should clear CO2: got {}",
gas.co2
);
}
#[test]
fn co2_accumulates_during_pause() {
let config = RespiratoryConfig::default();
let base = Instant::now();
let mut gas = GasPool::new(&config, base);
let now = base + Duration::from_millis(500);
gas.metabolize(now, false, 0, &config);
assert!(
gas.co2 > config.co2_baseline,
"CO2 should rise without ventilation: got {} vs baseline {}",
gas.co2,
config.co2_baseline
);
}
}