Files
medicallib_rust/src/organs/pancreas.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

241 lines
8.4 KiB
Rust

use super::{Organ, OrganInfo};
use crate::types::OrganType;
/// Dominant endocrine/exocrine activity mode of the pancreas.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PancreaticState {
Basal,
PostprandialAnabolic,
HypoglycemicCounterregulation,
BetaCellExhaustion,
}
#[derive(Debug, Clone)]
pub struct Pancreas {
info: OrganInfo,
/// Insulin secretion index (µU/mL proxy).
pub insulin: f32,
/// Glucagon secretion index (pg/mL proxy).
pub glucagon: f32,
/// Somatostatin output (pg/mL proxy).
pub somatostatin: f32,
/// Pancreatic polypeptide level.
pub pancreatic_polypeptide: f32,
/// Enzyme output (kIU/min).
pub digestive_enzyme_output: f32,
/// Bicarbonate secretion (mmol/min).
pub bicarbonate_output_mmol_min: f32,
/// Estimated beta-cell functional mass fraction (0..=1).
pub beta_cell_mass_fraction: f32,
/// Islet stress index (0..=1).
pub islet_stress_index: f32,
/// Acinar secretion flow (ml/min).
pub acinar_flow_ml_min: f32,
/// Ductal pressure (cmH2O).
pub duct_pressure_cm_h2o: f32,
/// Blood glucose sensed by islets (mg/dL).
pub blood_glucose_mg_dl: f32,
/// Incretin stimulus (0..=1).
pub incretin_signal: f32,
/// Autonomic tone (-1 vagal, +1 sympathetic).
pub autonomic_tone: f32,
/// Current pancreas state.
pub state: PancreaticState,
time_in_state_s: f32,
feeding_clock_s: f32,
target_meal_interval_s: f32,
}
impl Pancreas {
pub fn new(id: impl Into<String>) -> Self {
Self {
info: OrganInfo::new(id, OrganType::Pancreas),
insulin: 12.0,
glucagon: 60.0,
somatostatin: 20.0,
pancreatic_polypeptide: 120.0,
digestive_enzyme_output: 18.0,
bicarbonate_output_mmol_min: 1.8,
beta_cell_mass_fraction: 0.92,
islet_stress_index: 0.25,
acinar_flow_ml_min: 0.7,
duct_pressure_cm_h2o: 6.0,
blood_glucose_mg_dl: 95.0,
incretin_signal: 0.2,
autonomic_tone: 0.0,
state: PancreaticState::Basal,
time_in_state_s: 0.0,
feeding_clock_s: 0.0,
target_meal_interval_s: 4.2 * 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.feeding_clock_s += dt_seconds;
if self.feeding_clock_s >= self.target_meal_interval_s {
self.blood_glucose_mg_dl = 155.0;
self.incretin_signal = 0.85;
self.autonomic_tone = -0.4; // vagal dominance
self.feeding_clock_s = 0.0;
self.state = PancreaticState::PostprandialAnabolic;
self.time_in_state_s = 0.0;
self.target_meal_interval_s = (3.5 + 1.2 * self.islet_stress_index) * 3600.0;
} else {
self.incretin_signal = Self::approach(self.incretin_signal, 0.15, 0.06, dt_seconds);
self.autonomic_tone = Self::approach(self.autonomic_tone, 0.1, 0.08, dt_seconds);
}
self.blood_glucose_mg_dl = Self::approach(
self.blood_glucose_mg_dl,
90.0 + 12.0 * (-self.autonomic_tone).max(0.0),
0.1,
dt_seconds,
);
}
fn update_state(&mut self) {
self.state = if self.beta_cell_mass_fraction < 0.6 || self.islet_stress_index > 0.75 {
PancreaticState::BetaCellExhaustion
} else if self.blood_glucose_mg_dl < 70.0 {
PancreaticState::HypoglycemicCounterregulation
} else if self.blood_glucose_mg_dl > 130.0 || self.incretin_signal > 0.5 {
PancreaticState::PostprandialAnabolic
} else {
PancreaticState::Basal
};
}
fn update_endocrine(&mut self, dt_seconds: f32) {
let insulin_target = match self.state {
PancreaticState::PostprandialAnabolic => {
8.0 + 0.6 * (self.blood_glucose_mg_dl - 90.0).max(0.0) + 25.0 * self.incretin_signal
}
PancreaticState::Basal => 10.0 + 0.2 * (self.blood_glucose_mg_dl - 90.0),
PancreaticState::HypoglycemicCounterregulation => 4.0,
PancreaticState::BetaCellExhaustion => 6.0,
};
self.insulin = Self::approach(
self.insulin,
(insulin_target * self.beta_cell_mass_fraction).clamp(2.0, 80.0),
0.5,
dt_seconds,
);
let glucagon_target = match self.state {
PancreaticState::HypoglycemicCounterregulation => 150.0,
PancreaticState::Basal => 70.0,
PancreaticState::PostprandialAnabolic => 40.0,
PancreaticState::BetaCellExhaustion => 110.0,
};
self.glucagon = Self::approach(
self.glucagon,
(glucagon_target + 20.0 * self.autonomic_tone.max(0.0)).clamp(20.0, 200.0),
0.4,
dt_seconds,
);
let somatostatin_target = (20.0
+ 15.0 * (self.incretin_signal - 0.3).max(0.0)
+ 0.3 * (self.blood_glucose_mg_dl - 90.0))
.clamp(10.0, 80.0);
self.somatostatin = Self::approach(self.somatostatin, somatostatin_target, 0.5, dt_seconds);
self.pancreatic_polypeptide = Self::approach(
self.pancreatic_polypeptide,
(100.0 + 80.0 * (-self.autonomic_tone).max(0.0) + 40.0 * self.incretin_signal)
.clamp(60.0, 260.0),
0.3,
dt_seconds,
);
self.islet_stress_index = Self::approach(
self.islet_stress_index,
(0.2 + 0.4 * (self.blood_glucose_mg_dl - 100.0).max(0.0) / 80.0
+ 0.3 * (self.autonomic_tone).max(0.0))
.clamp(0.05, 0.95),
0.04,
dt_seconds,
);
self.beta_cell_mass_fraction = (self.beta_cell_mass_fraction
- 0.00002 * dt_seconds * (self.islet_stress_index - 0.3).max(0.0)
+ 0.000015 * dt_seconds * (0.5 - self.islet_stress_index).max(0.0))
.clamp(0.4, 1.05);
}
fn update_exocrine(&mut self, dt_seconds: f32) {
let enzyme_target =
(15.0 + 25.0 * self.incretin_signal + 10.0 * (-self.autonomic_tone).max(0.0))
.clamp(5.0, 60.0);
self.digestive_enzyme_output =
Self::approach(self.digestive_enzyme_output, enzyme_target, 0.5, dt_seconds);
let bicarb_target =
(1.5 + 2.5 * self.incretin_signal - 0.5 * self.islet_stress_index).clamp(0.5, 5.0);
self.bicarbonate_output_mmol_min = Self::approach(
self.bicarbonate_output_mmol_min,
bicarb_target,
0.4,
dt_seconds,
);
self.acinar_flow_ml_min = Self::approach(
self.acinar_flow_ml_min,
(0.6 + 0.5 * self.incretin_signal + 0.3 * (-self.autonomic_tone).max(0.0))
.clamp(0.3, 2.0),
0.4,
dt_seconds,
);
self.duct_pressure_cm_h2o = Self::approach(
self.duct_pressure_cm_h2o,
(6.0 + 4.0 * (self.acinar_flow_ml_min - 0.7)).clamp(4.0, 18.0),
0.3,
dt_seconds,
);
}
}
impl Organ for Pancreas {
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_state_s += dt_seconds;
self.simulate_meals(dt_seconds);
self.update_state();
self.update_endocrine(dt_seconds);
self.update_exocrine(dt_seconds);
}
fn summary(&self) -> String {
format!(
"Pancreas[id={}, state={:?}, insulin={:.1}, glucagon={:.0}, enzymes={:.1} kIU/min]",
self.id(),
self.state,
self.insulin,
self.glucagon,
self.digestive_enzyme_output
)
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
}