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.
241 lines
8.4 KiB
Rust
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
|
|
}
|
|
}
|