From d849f71127bb83866f47a3ccde47133fcd92d1ae Mon Sep 17 00:00:00 2001 From: Zack3D Date: Wed, 24 Sep 2025 02:01:53 -0700 Subject: [PATCH] feat(patient): integrate brain/bladder/esophagus/stomach coupling Introduce richer neuro-visceral coupling and signal integration across organs to improve physiological realism: - Add Brain, Esophagus, Bladder, Stomach, Spinal signal structs - Couple bladder with spinal autonomics and brain state to drive parasympathetic/sympathetic/somatic outputs and thresholds - Deliver esophageal bolus into stomach; update hiatal pressure and LES tone from stomach distension/acid/motility - Move stomach emptying into intestines within patient update loop - Replace spleen state penalty with immune activity-based adjustment - Simplify gallbladder control using intestinal nutrient energy - Refine heart autonomic tone using brain/spinal outputs - Re-export BladderPhase, SleepStage, EsophagealStage No breaking API changes. --- src/organs/mod.rs | 6 +- src/patient.rs | 338 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 269 insertions(+), 75 deletions(-) diff --git a/src/organs/mod.rs b/src/organs/mod.rs index d7fc159..03d85ce 100644 --- a/src/organs/mod.rs +++ b/src/organs/mod.rs @@ -55,9 +55,9 @@ mod spinal_cord; mod spleen; mod stomach; -pub use bladder::Bladder; -pub use brain::Brain; -pub use esophagus::Esophagus; +pub use bladder::{Bladder, BladderPhase}; +pub use brain::{Brain, SleepStage}; +pub use esophagus::{EsophagealStage, Esophagus}; pub use gallbladder::Gallbladder; pub use heart::Heart; pub use intestines::Intestines; diff --git a/src/patient.rs b/src/patient.rs index 316a0fb..eee73fc 100644 --- a/src/patient.rs +++ b/src/patient.rs @@ -2,10 +2,10 @@ use crate::error::MedicalError; use crate::organs::{ - Bladder, Brain, Gallbladder, Heart, IntestinalPhase, Intestines, Kidneys, Liver, Lungs, Organ, - Pancreas, SpinalCord, Spleen, SplenicState, Stomach, + Bladder, BladderPhase, Brain, EsophagealStage, Esophagus, Gallbladder, Heart, Intestines, + Kidneys, Liver, Lungs, Organ, Pancreas, SleepStage, SpinalCord, Spleen, Stomach, }; -use crate::organs::intestines::IntestinalPhase;\r\nuse crate::organs::spleen::SplenicState;\r\n\r\nuse crate::types::{Blood, BloodPressure, OrganType}; +use crate::types::{Blood, BloodPressure, OrganType}; /// Patient container and simulation entry. #[derive(Debug)] @@ -20,21 +20,48 @@ pub struct Patient { #[derive(Clone, Copy)] struct HeartSignals { - systolic: f32, - diastolic: f32, map: f32, cardiac_output: f32, - heart_rate: f32, } #[derive(Clone, Copy)] struct LungSignals { spo2_pct: f32, - alveolar_po2_mm_hg: f32, alveolar_pco2_mm_hg: f32, - minute_ventilation_l_min: f32, - oxygen_delivery_ml_min: f32, - shunt_fraction: f32, +} + +#[derive(Clone, Copy)] +struct BrainSignals { + brainstem_drive: f32, + autonomic_variability: f32, + consciousness: f32, + sleep_depth: f32, + rem_tone: f32, +} + +#[derive(Clone, Copy)] +struct EsophagusSignals { + stage: EsophagealStage, + bolus_volume_ml: f32, + peristaltic_progress_cm: f32, + lower_sphincter_tone: f32, + hiatal_pressure_cm_h2o: f32, +} + +#[derive(Clone, Copy)] +struct BladderSignals { + afferent_signal: f32, + urgency: f32, + phase: BladderPhase, +} + +#[derive(Clone, Copy)] +struct StomachSignals { + emptying_rate_ml_min: f32, + nutrient_load_kcal: f32, + volume_ml: f32, + acid_level: u8, + motility_index: f32, } #[derive(Clone, Copy)] @@ -52,7 +79,6 @@ struct PancreasSignals { #[derive(Clone, Copy)] struct IntestineSignals { - phase: IntestinalPhase, nutrient_energy_kcal: f32, } @@ -65,6 +91,7 @@ struct GallbladderSignals { struct SpinalSignals { sympathetic_outflow: f32, parasympathetic_outflow: f32, + reflex_gain: f32, } #[derive(Clone, Copy)] @@ -77,7 +104,6 @@ struct SpleenSignals { immune_activity: u8, red_pulp_volume_ml: f32, platelet_reservoir: f32, - state: SplenicState, } impl Patient { @@ -191,7 +217,7 @@ impl Patient { } OrganType::Esophagus => { let id = format!("{}-eso", self.id); - self.add_organ(crate::organs::Esophagus::new(id)); + self.add_organ(Esophagus::new(id)); } OrganType::Kidneys => { let id = format!("{}-kidneys", self.id); @@ -219,21 +245,22 @@ impl Patient { let systolic = h.arterial_bp.systolic as f32; let diastolic = h.arterial_bp.diastolic as f32; HeartSignals { - systolic, - diastolic, map: diastolic + (systolic - diastolic) / 3.0, cardiac_output: h.cardiac_output_l_min, - heart_rate: h.heart_rate_bpm, } }); let lungs_signals = self.find_organ_typed::().map(|l| LungSignals { spo2_pct: l.spo2_pct, - alveolar_po2_mm_hg: l.alveolar_po2_mm_hg, alveolar_pco2_mm_hg: l.alveolar_pco2_mm_hg, - minute_ventilation_l_min: l.minute_ventilation_l_min, - oxygen_delivery_ml_min: l.oxygen_delivery_ml_min, - shunt_fraction: l.shunt_fraction, + }); + + let stomach_signals = self.find_organ_typed::().map(|s| StomachSignals { + emptying_rate_ml_min: s.emptying_rate_ml_min, + nutrient_load_kcal: s.nutrient_load_kcal, + volume_ml: s.volume_ml, + acid_level: s.acid_level, + motility_index: s.motility_index, }); let liver_signals = self.find_organ_typed::().map(|l| LiverSignals { @@ -243,7 +270,6 @@ impl Patient { let intestine_signals = self .find_organ_typed::() .map(|i| IntestineSignals { - phase: i.phase, nutrient_energy_kcal: i.nutrient_energy_kcal, }); @@ -253,6 +279,16 @@ impl Patient { bile_acid_concentration_mmol_l: g.bile_acid_concentration_mmol_l, }); + let esophagus_before = self + .find_organ_typed::() + .map(|e| EsophagusSignals { + stage: e.stage, + bolus_volume_ml: e.bolus_volume_ml, + peristaltic_progress_cm: e.peristaltic_progress_cm, + lower_sphincter_tone: e.lower_sphincter_tone, + hiatal_pressure_cm_h2o: e.hiatal_pressure_gradient_cm_h2o, + }); + let kidney_signals = self.find_organ_typed::().map(|k| KidneySignals { urine_flow_ml_min: k.urine_flow_ml_min, }); @@ -262,10 +298,12 @@ impl Patient { .map(|s| SpinalSignals { sympathetic_outflow: s.sympathetic_outflow, parasympathetic_outflow: s.parasympathetic_outflow, + reflex_gain: s.reflex_gain, }); + let blood_glucose = self.blood.glucose_mg_dl; if let Some(pancreas) = self.find_organ_typed_mut::() { - pancreas.blood_glucose_mg_dl = self.blood.glucose_mg_dl; + pancreas.blood_glucose_mg_dl = blood_glucose; if let Some(intestines) = intestine_signals { let incretin_target = (intestines.nutrient_energy_kcal / 400.0).clamp(0.05, 1.0); pancreas.incretin_signal = @@ -302,8 +340,9 @@ impl Patient { } } + let blood_glucose_for_kidneys = self.blood.glucose_mg_dl; if let Some(kidneys) = self.find_organ_typed_mut::() { - let osm_target = 285.0 + (self.blood.glucose_mg_dl - 95.0) * 0.06; + let osm_target = 285.0 + (blood_glucose_for_kidneys - 95.0) * 0.06; kidneys.serum_osmolality_mosm = Self::relax_value(kidneys.serum_osmolality_mosm, osm_target, dt_seconds, 120.0); if let Some(heart) = heart_signals { @@ -324,10 +363,7 @@ impl Patient { ); } if let Some(intestines) = intestine_signals { - if matches!( - intestines.phase, - IntestinalPhase::IlealBrake | IntestinalPhase::Dysmotility - ) { + if intestines.nutrient_energy_kcal < 80.0 { gallbladder.sphincter_of_oddi_tone = (gallbladder.sphincter_of_oddi_tone + 0.05 * dt_seconds / 60.0) .clamp(0.2, 0.95); @@ -372,6 +408,16 @@ impl Patient { } } + if let Some(stomach) = stomach_signals { + let delivered_ml = stomach.emptying_rate_ml_min * dt_seconds / 60.0; + let delivered_kcal = (delivered_ml * 0.8).min(stomach.nutrient_load_kcal); + if delivered_kcal > 0.0 { + if let Some(intestines) = self.find_organ_typed_mut::() { + intestines.nutrient_energy_kcal += delivered_kcal; + } + } + } + for organ in &mut self.organs { organ.update(dt_seconds); } @@ -417,21 +463,14 @@ impl Patient { immune_activity: s.immune_activity, red_pulp_volume_ml: s.red_pulp_volume_ml, platelet_reservoir: s.platelet_reservoir, - state: s.state, }); if let Some(spleen) = spleen_after { - let state_penalty = match spleen.state { - SplenicState::SympatheticContraction => -2.0, - SplenicState::HyperimmuneActivation => 2.5, - SplenicState::Sequestration => 3.0, - SplenicState::Hypofunction => -1.5, - SplenicState::Homeostatic => 0.0, - }; + let immune_penalty = (spleen.immune_activity as f32 - 80.0) / 120.0; let hematocrit_target = 42.0 - (spleen.red_pulp_volume_ml - 180.0) / 8.0 - (spleen.platelet_reservoir - 70.0) / 30.0 - + state_penalty; + + immune_penalty * 2.0; self.blood.hematocrit_pct = Self::relax_value( self.blood.hematocrit_pct, hematocrit_target.clamp(30.0, 55.0), @@ -440,8 +479,9 @@ impl Patient { ); } + let blood_glucose_for_pancreas = self.blood.glucose_mg_dl; if let Some(pancreas) = self.find_organ_typed_mut::() { - pancreas.blood_glucose_mg_dl = self.blood.glucose_mg_dl; + pancreas.blood_glucose_mg_dl = blood_glucose_for_pancreas; } let pancreas_after = self @@ -453,6 +493,188 @@ impl Patient { somatostatin: p.somatostatin, }); + let brain_after = self.find_organ_typed::().map(|b| BrainSignals { + brainstem_drive: b.brainstem_autonomic_drive, + autonomic_variability: b.autonomic_variability, + consciousness: b.consciousness as f32 / 100.0, + sleep_depth: match b.sleep_stage { + SleepStage::Wake => 0.0, + SleepStage::N1 => 0.25, + SleepStage::N2 => 0.55, + SleepStage::N3 => 0.9, + SleepStage::Rem => 0.4, + }, + rem_tone: matches!(b.sleep_stage, SleepStage::Rem) as i32 as f32, + }); + let spinal_after = self + .find_organ_typed::() + .map(|s| SpinalSignals { + sympathetic_outflow: s.sympathetic_outflow, + parasympathetic_outflow: s.parasympathetic_outflow, + reflex_gain: s.reflex_gain, + }); + let bladder_after = self.find_organ_typed::().map(|b| BladderSignals { + afferent_signal: b.afferent_signal, + urgency: b.urgency, + phase: b.phase, + }); + let esophagus_after = self + .find_organ_typed::() + .map(|e| EsophagusSignals { + stage: e.stage, + bolus_volume_ml: e.bolus_volume_ml, + peristaltic_progress_cm: e.peristaltic_progress_cm, + lower_sphincter_tone: e.lower_sphincter_tone, + hiatal_pressure_cm_h2o: e.hiatal_pressure_gradient_cm_h2o, + }); + + if let (Some(before), Some(after)) = (esophagus_before, esophagus_after) { + let mut delivered_ml = 0.0; + if matches!( + after.stage, + EsophagealStage::Clearing | EsophagealStage::Idle + ) { + delivered_ml = (before.bolus_volume_ml - after.bolus_volume_ml).max(0.0); + } else if after.peristaltic_progress_cm > 22.0 && before.peristaltic_progress_cm <= 22.0 + { + delivered_ml = before.bolus_volume_ml * 0.6; + } + let delivered_ml = delivered_ml.clamp(0.0, 40.0); + if delivered_ml > 0.0 { + if let Some(stomach) = self.find_organ_typed_mut::() { + stomach.volume_ml = (stomach.volume_ml + delivered_ml).clamp(80.0, 1600.0); + stomach.nutrient_load_kcal += delivered_ml * 0.8; + } + } + } + + if let (Some(stomach), Some(esophagus_state)) = (stomach_signals, esophagus_after) { + if let Some(esophagus) = self.find_organ_typed_mut::() { + let distension = ((stomach.volume_ml - 250.0) / 160.0).clamp(-0.3, 2.2); + let acid_factor = (stomach.acid_level as f32 / 100.0 - 0.65).max(0.0); + let motility_relief = (stomach.motility_index - 0.5).min(0.0).abs(); + let gradient_target = + (esophagus_state.hiatal_pressure_cm_h2o + distension * 6.0 + acid_factor * 3.0 + - motility_relief * 2.0) + .clamp(4.0, 22.0); + esophagus.hiatal_pressure_gradient_cm_h2o = Self::relax_value( + esophagus.hiatal_pressure_gradient_cm_h2o, + gradient_target, + dt_seconds, + 90.0, + ); + let les_target = (esophagus_state.lower_sphincter_tone + + 0.15 * (0.6 - distension).clamp(-0.4, 0.4) + - 0.1 * acid_factor) + .clamp(0.2, 0.95); + esophagus.lower_sphincter_tone = Self::relax_value( + esophagus.lower_sphincter_tone, + les_target, + dt_seconds, + 120.0, + ); + } + } + + if let Some(bladder_state) = bladder_after { + if let Some(spinal) = self.find_organ_typed_mut::() { + let stretch = bladder_state.afferent_signal; + let urgency = bladder_state.urgency; + let voiding_signal = + matches!(bladder_state.phase, BladderPhase::Voiding) as i32 as f32; + let sleep_bias = brain_after.map(|b| b.sleep_depth).unwrap_or(0.0); + let para_target = (0.42 + 0.5 * stretch + 0.25 * voiding_signal + - 0.15 * sleep_bias) + .clamp(0.2, 0.95); + let sym_target = (0.65 - 0.4 * stretch - 0.25 * voiding_signal + 0.2 * sleep_bias) + .clamp(0.15, 0.9); + spinal.parasympathetic_outflow = Self::relax_value( + spinal.parasympathetic_outflow, + para_target, + dt_seconds, + 90.0, + ); + spinal.sympathetic_outflow = + Self::relax_value(spinal.sympathetic_outflow, sym_target, dt_seconds, 90.0); + let sedation = brain_after.map(|b| 1.0 - b.consciousness).unwrap_or(0.0); + let reflex_target = + (1.0 + 0.55 * stretch + 0.3 * voiding_signal - 0.3 * sedation).clamp(0.6, 1.6); + spinal.reflex_gain = + Self::relax_value(spinal.reflex_gain, reflex_target, dt_seconds, 120.0); + spinal.nociceptive_facilitation = Self::relax_value( + spinal.nociceptive_facilitation, + (0.2 + 0.5 * urgency + 0.1 * voiding_signal).clamp(0.15, 1.0), + dt_seconds, + 120.0, + ); + } + + if let Some(bladder) = self.find_organ_typed_mut::() { + if let Some(brain) = brain_after { + let sedation = (1.0 - brain.consciousness).clamp(0.0, 1.0); + let sleep_bonus = brain.sleep_depth * 0.25 + brain.rem_tone * 0.1; + let capacity = bladder.capacity_ml.max(1.0); + let micturition_target = (capacity + * (0.65 + sleep_bonus + sedation * 0.2 - bladder_state.urgency * 0.05)) + .clamp(capacity * 0.5, capacity * 0.9); + bladder.micturition_threshold_ml = Self::relax_value( + bladder.micturition_threshold_ml, + micturition_target, + dt_seconds, + 900.0, + ); + let urge_target = (capacity * (0.4 + sleep_bonus * 0.5 + sedation * 0.15)) + .clamp(capacity * 0.3, capacity * 0.7); + bladder.urge_threshold_ml = Self::relax_value( + bladder.urge_threshold_ml, + urge_target, + dt_seconds, + 600.0, + ); + } + + if let Some(brain) = brain_after { + let voiding_signal = + matches!(bladder_state.phase, BladderPhase::Voiding) as i32 as f32; + let spinal_sym = spinal_after.map(|s| s.sympathetic_outflow).unwrap_or(0.6); + let spinal_para = spinal_after + .map(|s| s.parasympathetic_outflow) + .unwrap_or(0.5); + let reflex_gain = spinal_after.map(|s| s.reflex_gain).unwrap_or(1.0); + let para_target = (0.3 + + brain.brainstem_drive * 0.4 + + spinal_para * 0.3 + + bladder_state.urgency * 0.5 + + voiding_signal * 0.3) + .clamp(0.05, 1.1); + bladder.parasympathetic_drive = Self::relax_value( + bladder.parasympathetic_drive, + para_target, + dt_seconds, + 120.0, + ) + .clamp(0.0, 1.0); + let sym_target = + (0.55 + (1.0 - brain.brainstem_drive) * 0.35 + spinal_sym * 0.4 + - bladder_state.urgency * 0.4 + - voiding_signal * 0.4) + .clamp(0.1, 1.0); + bladder.sympathetic_drive = + Self::relax_value(bladder.sympathetic_drive, sym_target, dt_seconds, 160.0) + .clamp(0.0, 1.0); + let voluntary = brain.consciousness; + let somatic_target = ((0.55 + voluntary * 0.35 - brain.sleep_depth * 0.3) + * (1.0 - bladder_state.urgency * 0.55) + + reflex_gain * 0.1 + - voiding_signal * 0.5) + .clamp(0.1, 0.95); + bladder.somatic_drive = + Self::relax_value(bladder.somatic_drive, somatic_target, dt_seconds, 110.0) + .clamp(0.0, 1.0); + } + } + } + if let Some(pancreas) = pancreas_after { if let Some(liver) = self.find_organ_typed_mut::() { let insulin_target = (pancreas.insulin / 60.0).clamp(0.1, 1.0); @@ -464,28 +686,6 @@ impl Patient { } } - if let Some(liver) = self.find_organ_typed::() { - if let Some(gallbladder) = self.find_organ_typed_mut::() { - let inflow_target = (liver.bile_secretion_ml_min * 0.8).clamp(0.05, 2.4); - gallbladder.hepatic_bile_flow_ml_per_min = Self::relax_value( - gallbladder.hepatic_bile_flow_ml_per_min, - inflow_target, - dt_seconds, - 80.0, - ); - } - } - - if let Some(stomach) = self.find_organ_typed::() { - let delivered_ml = stomach.emptying_rate_ml_min * dt_seconds / 60.0; - let delivered_kcal = (delivered_ml * 0.8).min(stomach.nutrient_load_kcal); - if delivered_kcal > 0.0 { - if let Some(intestines) = self.find_organ_typed_mut::() { - intestines.nutrient_energy_kcal += delivered_kcal; - } - } - } - if let Some((_, urine_flow)) = kidneys_after { let produced = (urine_flow * dt_seconds / 60.0).max(0.0); if produced > 0.0 { @@ -495,17 +695,13 @@ impl Patient { } } - let brain_after = self - .find_organ_typed::() - .map(|b| (b.brainstem_autonomic_drive, b.autonomic_variability)); - let spinal_after = self - .find_organ_typed::() - .map(|s| (s.sympathetic_outflow, s.parasympathetic_outflow)); if let Some(heart) = self.find_organ_typed_mut::() { - if let Some((brain_drive, brain_sympathetic)) = brain_after { - let mut tone_target = (brain_drive - 0.5) * 1.2 + (brain_sympathetic - 0.5) * 0.8; - if let Some((sym, para)) = spinal_after { - tone_target += (sym - para) * 0.6; + if let Some(brain) = brain_after { + let mut tone_target = + (brain.brainstem_drive - 0.5) * 1.2 + (brain.autonomic_variability - 0.5) * 0.8; + if let Some(spinal) = spinal_after { + tone_target += + (spinal.sympathetic_outflow - spinal.parasympathetic_outflow) * 0.6; } heart.autonomic_tone = Self::relax_value( heart.autonomic_tone, @@ -584,7 +780,7 @@ fn is_valid_id(id: &str) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::organs::intestines::IntestinalPhase;\r\nuse crate::organs::spleen::SplenicState;\r\n\r\nuse crate::types::OrganType; + use crate::types::OrganType; #[test] fn patient_lifecycle() { @@ -603,5 +799,3 @@ mod tests { assert!(Patient::new("bad id").is_err()); } } - -