Files
medicallib_rust/src/organs/stomach.rs
T
zack3d f439894864 feat(organs): add detailed physiology + coupling
- 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.
2025-09-24 01:34:34 -07:00

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