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.
285 lines
9.2 KiB
Rust
285 lines
9.2 KiB
Rust
use super::{Organ, OrganInfo};
|
|
use crate::types::OrganType;
|
|
|
|
/// Gastric functional phase.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum GastricPhase {
|
|
Fasting,
|
|
Cephalic,
|
|
Gastric,
|
|
Intestinal,
|
|
DelayedEmptying,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Stomach {
|
|
info: OrganInfo,
|
|
/// Acid level 0..=100 (higher = more secretion).
|
|
pub acid_level: u8,
|
|
/// Gastric lumen pH.
|
|
pub ph: f32,
|
|
/// Current gastric volume (ml).
|
|
pub volume_ml: f32,
|
|
/// Gastric motility index (0..=1).
|
|
pub motility_index: f32,
|
|
/// Antral pump strength (0..=1).
|
|
pub antral_pump_strength: f32,
|
|
/// Gastric emptying rate (ml/min).
|
|
pub emptying_rate_ml_min: f32,
|
|
/// Ghrelin level (pg/mL proxy).
|
|
pub ghrelin: f32,
|
|
/// Gastrin level (pg/mL proxy).
|
|
pub gastrin: f32,
|
|
/// Histamine release (relative units).
|
|
pub histamine: f32,
|
|
/// Somatostatin brake (relative units).
|
|
pub somatostatin: f32,
|
|
/// Protective mucus production (g/hour).
|
|
pub mucus_production_g_per_h: f32,
|
|
/// Intrinsic factor secretion (relative units).
|
|
pub intrinsic_factor: f32,
|
|
/// Vagal tone (0..=1).
|
|
pub vagal_tone: f32,
|
|
/// Gastric phase.
|
|
pub phase: GastricPhase,
|
|
/// Pending meal caloric load (kcal).
|
|
pub nutrient_load_kcal: f32,
|
|
time_in_phase_s: f32,
|
|
fasting_clock_s: f32,
|
|
target_meal_interval_s: f32,
|
|
}
|
|
|
|
impl Stomach {
|
|
pub fn new(id: impl Into<String>) -> Self {
|
|
Self {
|
|
info: OrganInfo::new(id, OrganType::Stomach),
|
|
acid_level: 50,
|
|
ph: 2.2,
|
|
volume_ml: 120.0,
|
|
motility_index: 0.35,
|
|
antral_pump_strength: 0.3,
|
|
emptying_rate_ml_min: 1.5,
|
|
ghrelin: 950.0,
|
|
gastrin: 80.0,
|
|
histamine: 0.4,
|
|
somatostatin: 0.3,
|
|
mucus_production_g_per_h: 15.0,
|
|
intrinsic_factor: 0.6,
|
|
vagal_tone: 0.4,
|
|
phase: GastricPhase::Fasting,
|
|
nutrient_load_kcal: 60.0,
|
|
time_in_phase_s: 0.0,
|
|
fasting_clock_s: 0.0,
|
|
target_meal_interval_s: 4.5 * 3600.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 simulate_meals(&mut self, dt_seconds: f32) {
|
|
self.fasting_clock_s += dt_seconds;
|
|
if self.fasting_clock_s >= self.target_meal_interval_s {
|
|
self.phase = GastricPhase::Cephalic;
|
|
self.time_in_phase_s = 0.0;
|
|
self.vagal_tone = 0.85;
|
|
self.ghrelin = 600.0;
|
|
self.gastrin = 160.0;
|
|
self.nutrient_load_kcal = 650.0;
|
|
self.volume_ml = (self.volume_ml + 450.0).clamp(80.0, 1600.0);
|
|
self.target_meal_interval_s =
|
|
(4.0 + 1.0 * (self.mucus_production_g_per_h / 15.0)) * 3600.0;
|
|
self.fasting_clock_s = 0.0;
|
|
} else {
|
|
self.vagal_tone = Self::approach(self.vagal_tone, 0.35, 0.04, dt_seconds);
|
|
self.ghrelin = Self::approach(self.ghrelin, 1200.0, 1.0, dt_seconds);
|
|
}
|
|
}
|
|
|
|
fn update_phase(&mut self) {
|
|
self.phase = match self.phase {
|
|
GastricPhase::Cephalic => {
|
|
if self.time_in_phase_s > 300.0 {
|
|
GastricPhase::Gastric
|
|
} else {
|
|
GastricPhase::Cephalic
|
|
}
|
|
}
|
|
GastricPhase::Gastric => {
|
|
if self.volume_ml < 200.0 {
|
|
GastricPhase::Intestinal
|
|
} else {
|
|
GastricPhase::Gastric
|
|
}
|
|
}
|
|
GastricPhase::Intestinal => {
|
|
if self.nutrient_load_kcal < 80.0 {
|
|
GastricPhase::Fasting
|
|
} else if self.emptying_rate_ml_min < 1.0 {
|
|
GastricPhase::DelayedEmptying
|
|
} else {
|
|
GastricPhase::Intestinal
|
|
}
|
|
}
|
|
GastricPhase::DelayedEmptying => {
|
|
if self.emptying_rate_ml_min > 1.5 {
|
|
GastricPhase::Fasting
|
|
} else {
|
|
GastricPhase::DelayedEmptying
|
|
}
|
|
}
|
|
GastricPhase::Fasting => {
|
|
if self.nutrient_load_kcal > 120.0 {
|
|
GastricPhase::Cephalic
|
|
} else {
|
|
GastricPhase::Fasting
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
fn update_secretions(&mut self, dt_seconds: f32) {
|
|
let gastrin_target = match self.phase {
|
|
GastricPhase::Cephalic => 180.0,
|
|
GastricPhase::Gastric => 220.0,
|
|
GastricPhase::Intestinal => 120.0,
|
|
GastricPhase::DelayedEmptying => 160.0,
|
|
GastricPhase::Fasting => 60.0,
|
|
};
|
|
self.gastrin = Self::approach(
|
|
self.gastrin,
|
|
(gastrin_target + 0.5 * (self.volume_ml - 250.0).max(0.0)).clamp(40.0, 320.0),
|
|
0.5,
|
|
dt_seconds,
|
|
);
|
|
self.histamine = Self::approach(
|
|
self.histamine,
|
|
(0.3 + 0.004 * self.gastrin + 0.2 * (self.vagal_tone - 0.4).max(0.0)).clamp(0.1, 2.0),
|
|
0.3,
|
|
dt_seconds,
|
|
);
|
|
self.somatostatin = Self::approach(
|
|
self.somatostatin,
|
|
(0.25
|
|
+ 0.2 * (self.ph - 2.0).max(0.0)
|
|
+ 0.3 * (self.phase == GastricPhase::Intestinal) as i32 as f32)
|
|
.clamp(0.1, 2.0),
|
|
0.4,
|
|
dt_seconds,
|
|
);
|
|
let acid_drive =
|
|
(self.gastrin / 200.0 + self.histamine - self.somatostatin).clamp(0.0, 2.0);
|
|
let acid_numeric = (50.0 + 35.0 * acid_drive).clamp(10.0, 100.0);
|
|
self.acid_level = acid_numeric.round() as u8;
|
|
self.ph = Self::approach(
|
|
self.ph,
|
|
(7.0 - 0.045 * self.acid_level as f32 + 0.4 * (self.volume_ml / 500.0)).clamp(1.2, 6.5),
|
|
0.6,
|
|
dt_seconds,
|
|
);
|
|
self.mucus_production_g_per_h = Self::approach(
|
|
self.mucus_production_g_per_h,
|
|
(15.0
|
|
+ 6.0 * (self.acid_level as f32 / 60.0)
|
|
+ 4.0 * (self.somatostatin - 0.3).max(0.0))
|
|
.clamp(8.0, 40.0),
|
|
0.2,
|
|
dt_seconds,
|
|
);
|
|
self.intrinsic_factor = Self::approach(
|
|
self.intrinsic_factor,
|
|
(0.6 + 0.4 * (self.acid_level as f32 / 80.0)).clamp(0.2, 1.2),
|
|
0.2,
|
|
dt_seconds,
|
|
);
|
|
}
|
|
|
|
fn update_motility(&mut self, dt_seconds: f32) {
|
|
let motility_target = match self.phase {
|
|
GastricPhase::Cephalic => 0.4,
|
|
GastricPhase::Gastric => 0.75,
|
|
GastricPhase::Intestinal => 0.6,
|
|
GastricPhase::DelayedEmptying => 0.35,
|
|
GastricPhase::Fasting => 0.3,
|
|
};
|
|
self.motility_index = Self::approach(self.motility_index, motility_target, 0.5, dt_seconds);
|
|
self.antral_pump_strength = Self::approach(
|
|
self.antral_pump_strength,
|
|
(0.3 + 0.5 * self.motility_index + 0.3 * self.vagal_tone).clamp(0.2, 0.95),
|
|
0.5,
|
|
dt_seconds,
|
|
);
|
|
self.emptying_rate_ml_min = Self::approach(
|
|
self.emptying_rate_ml_min,
|
|
(1.5 + 3.5 * self.antral_pump_strength
|
|
- 1.0 * (self.ph - 3.0).max(0.0)
|
|
- 0.5 * (self.nutrient_load_kcal / 300.0))
|
|
.clamp(0.2, 9.0),
|
|
0.4,
|
|
dt_seconds,
|
|
);
|
|
}
|
|
|
|
fn update_volume(&mut self, dt_seconds: f32) {
|
|
let emptied = self.emptying_rate_ml_min * dt_seconds / 60.0;
|
|
let metabolic_use = (self.nutrient_load_kcal * 0.3) * dt_seconds / 3600.0;
|
|
self.volume_ml = (self.volume_ml - emptied).clamp(30.0, 1800.0);
|
|
self.nutrient_load_kcal = (self.nutrient_load_kcal - metabolic_use).max(0.0);
|
|
self.ghrelin = Self::approach(
|
|
self.ghrelin,
|
|
(1200.0 - 0.8 * self.volume_ml).clamp(200.0, 1400.0),
|
|
0.4,
|
|
dt_seconds,
|
|
);
|
|
}
|
|
}
|
|
|
|
impl Organ for Stomach {
|
|
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_phase_s += dt_seconds;
|
|
self.simulate_meals(dt_seconds);
|
|
self.update_phase();
|
|
self.update_secretions(dt_seconds);
|
|
self.update_motility(dt_seconds);
|
|
self.update_volume(dt_seconds);
|
|
}
|
|
fn summary(&self) -> String {
|
|
format!(
|
|
"Stomach[id={}, phase={:?}, vol={:.0} ml, pH={:.1}, acid={}]",
|
|
self.id(),
|
|
self.phase,
|
|
self.volume_ml,
|
|
self.ph,
|
|
self.acid_level
|
|
)
|
|
}
|
|
fn as_any(&self) -> &dyn core::any::Any {
|
|
self
|
|
}
|
|
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
|
|
self
|
|
}
|
|
}
|