feat(organs): retune heart and bladder dynamics

- Heart:
  - Raise baseline SVR to 18.5 and derive initial CO/afterload from baseline
  - Correct baroreflex direction: SVR increases with sympathetic tone
  - Recompute BP via raw diastolic + pulse pressure relationship
  - Tighten clamps for physiologic ranges and stabilize resting state
  - Add unit test: resting_state_stays_stable
- Bladder:
  - Replace steep nonlinear compliance with scaled linear compliance
  - Add volume-based abdominal term and nonlinear passive gain
  - Reduce active pressure gain and clamp max pressure to 80 cm H2O
- Patient/tests:
  - Add multi-organ homeostasis stability integration test (5h sim)
  - Assert physiologic bounds across lungs, brain, kidneys, liver,
    GI, pancreas, spleen, bladder, esophagus, and spinal cord
- Build/examples:
  - Add demo-monitor feature flag and demo_app example

These changes improve physiologic realism and long-run stability while
adding coverage to prevent regressions.
This commit is contained in:
2025-09-24 02:55:29 -07:00
parent 21b9ca894f
commit 886484919d
5 changed files with 911 additions and 14 deletions
+6
View File
@@ -14,6 +14,7 @@ crate-type = ["rlib", "cdylib"]
default = []
serde = ["dep:serde"]
ffi = []
demo-monitor = []
[dependencies]
thiserror = "1"
@@ -24,6 +25,11 @@ tracing = { version = "0.1", optional = true }
criterion = { version = "0.5", default-features = false, features = ["cargo_bench_support"] }
tracing-subscriber = { version = "0.3" }
[[example]]
name = "demo_app"
path = "examples/demo_app.rs"
required-features = ["demo-monitor"]
[[bench]]
name = "heart"
harness = false
+536
View File
@@ -0,0 +1,536 @@
use medicallib_rust::{
bmi_measurement, calculate_bmi, classify_bmi, BloodPressure, Heart, Measurement, OrganType,
Patient, Result as MedicalResult, VitalSign,
};
use std::fmt::Write as _;
use std::io::{self, Write};
const EXTRA_ORGANS: [OrganType; 11] = [
OrganType::Brain,
OrganType::SpinalCord,
OrganType::Stomach,
OrganType::Liver,
OrganType::Gallbladder,
OrganType::Pancreas,
OrganType::Intestines,
OrganType::Esophagus,
OrganType::Kidneys,
OrganType::Bladder,
OrganType::Spleen,
];
const MONITORED_ORGANS: [OrganType; 13] = [
OrganType::Heart,
OrganType::Lungs,
OrganType::Brain,
OrganType::SpinalCord,
OrganType::Stomach,
OrganType::Liver,
OrganType::Gallbladder,
OrganType::Pancreas,
OrganType::Intestines,
OrganType::Esophagus,
OrganType::Kidneys,
OrganType::Bladder,
OrganType::Spleen,
];
const VITAL_SIGNS: [VitalSign; 6] = [
VitalSign::HeartRate,
VitalSign::RespiratoryRate,
VitalSign::SystolicBP,
VitalSign::DiastolicBP,
VitalSign::TemperatureC,
VitalSign::SpO2,
];
const HELP_TEXT: &str = r#"
Available commands:
help Show this help text
tick [dt] Advance the simulation by dt seconds (default: configured step)
run <steps> [dt] Run multiple ticks back-to-back
set dt <seconds> Update the default tick size
set arrhythmia <on|off> Force arrhythmic behaviour on the heart
set tone <value> Set heart autonomic tone (-1.0..=1.0)
set svr <value> Set heart systemic vascular resistance (mmHg*min/L)
set glucose <mg/dL> Override blood glucose
set spo2 <percent> Override blood SpO2 (0-100)
set bp <systolic> <diastolic> Override brachial blood pressure
set bmi <weight_kg> <height_m> Update the tracked BMI inputs
bmi <weight_kg> <height_m> Compute BMI on the fly without storing it
summary Print the aggregate patient summary string
organs Print one-line summaries for every organ
reset Reset the patient and vitals to defaults
quit | exit | q Leave the monitor
(empty input) Advance once using the configured step size
"#;
struct MonitorState {
patient: Patient,
sim_time: f32,
tick_seconds: f32,
bmi_inputs: (f32, f32),
}
impl MonitorState {
fn new() -> MedicalResult<Self> {
let mut patient = Patient::new("monitor")?.initialize_default().with_lungs();
for organ in EXTRA_ORGANS {
patient = patient.with_organ(organ);
}
Ok(Self {
patient,
sim_time: 0.0,
tick_seconds: 0.5,
bmi_inputs: (82.0, 1.84),
})
}
fn reset(&mut self) -> MedicalResult<()> {
*self = Self::new()?;
Ok(())
}
fn advance(&mut self, dt: f32) {
if dt <= 0.0 {
return;
}
self.patient.update(dt);
self.sim_time += dt;
}
fn heart_mut(&mut self) -> Option<&mut Heart> {
self.patient.find_organ_typed_mut::<Heart>()
}
}
enum CommandOutcome {
Continue(String),
Exit,
}
fn main() -> MedicalResult<()> {
let mut state = MonitorState::new()?;
let mut status = String::from("Type 'help' to list available commands.");
let stdin = io::stdin();
let mut input = String::new();
loop {
render_dashboard(&state, &status);
print!("monitor> ");
io::stdout().flush().expect("flush stdout");
input.clear();
let read = match stdin.read_line(&mut input) {
Ok(n) => n,
Err(err) => {
status = format!("Failed to read input: {err}");
continue;
}
};
if read == 0 {
status = String::from("End of input detected, shutting down.");
render_dashboard(&state, &status);
println!();
break;
}
let trimmed = input.trim();
if trimmed.is_empty() {
let dt = state.tick_seconds;
state.advance(dt);
status = format!("Advanced simulation by {:.2} s using the default step.", dt);
continue;
}
match handle_command(&mut state, trimmed) {
Ok(CommandOutcome::Continue(message)) => {
status = message;
}
Ok(CommandOutcome::Exit) => {
status = String::from("Exiting monitor.");
render_dashboard(&state, &status);
println!();
break;
}
Err(err) => {
status = format!("Error: {err}");
}
}
}
println!("Goodbye!");
Ok(())
}
fn handle_command(state: &mut MonitorState, input: &str) -> Result<CommandOutcome, String> {
let mut parts = input.split_whitespace();
let cmd = parts
.next()
.ok_or_else(|| String::from("expected a command"))?
.to_ascii_lowercase();
match cmd.as_str() {
"help" => Ok(CommandOutcome::Continue(String::from(HELP_TEXT))),
"tick" => {
let dt = match parts.next() {
Some(raw) => parse_f32(raw)?,
None => state.tick_seconds,
};
if dt <= 0.0 {
return Err(String::from("dt must be greater than 0"));
}
state.advance(dt);
Ok(CommandOutcome::Continue(format!(
"Advanced simulation by {:.2} s.",
dt
)))
}
"run" => {
let steps_raw = parts
.next()
.ok_or_else(|| String::from("run expects a number of steps"))?;
let steps: usize = steps_raw
.parse()
.map_err(|_| format!("could not parse steps '{steps_raw}'"))?;
if steps == 0 {
return Err(String::from("steps must be greater than 0"));
}
let dt = match parts.next() {
Some(raw) => parse_f32(raw)?,
None => state.tick_seconds,
};
if dt <= 0.0 {
return Err(String::from("dt must be greater than 0"));
}
for _ in 0..steps {
state.advance(dt);
}
Ok(CommandOutcome::Continue(format!(
"Ran {steps} step(s) at {:.2} s per step.",
dt
)))
}
"set" => handle_set_command(state, parts),
"summary" => Ok(CommandOutcome::Continue(state.patient.patient_summary())),
"organs" => {
let mut details = String::new();
for organ in MONITORED_ORGANS {
let _ = writeln!(
details,
"{} -> {}",
organ_label(organ),
organ_snapshot(&state.patient, organ)
);
}
Ok(CommandOutcome::Continue(details))
}
"reset" => {
state.reset().map_err(|err| err.to_string())?;
Ok(CommandOutcome::Continue(String::from(
"Patient and monitor reset to defaults.",
)))
}
"bmi" => {
let weight_raw = parts
.next()
.ok_or_else(|| String::from("bmi expects weight in kg"))?;
let height_raw = parts
.next()
.ok_or_else(|| String::from("bmi expects height in m"))?;
let weight = parse_f32(weight_raw)?;
let height = parse_f32(height_raw)?;
let value = calculate_bmi(weight, height).map_err(|err| err.to_string())?;
let measurement = Measurement::new(value, "kg/m^2");
let class = classify_bmi(value);
Ok(CommandOutcome::Continue(format!(
"BMI {:.2} {} => {:?}",
measurement.value, measurement.unit, class
)))
}
"quit" | "exit" | "q" => Ok(CommandOutcome::Exit),
other => Err(format!("unknown command '{other}'")),
}
}
fn handle_set_command<'a>(
state: &mut MonitorState,
mut parts: impl Iterator<Item = &'a str>,
) -> Result<CommandOutcome, String> {
let field = parts
.next()
.ok_or_else(|| String::from("set expects a field to modify"))?
.to_ascii_lowercase();
match field.as_str() {
"dt" | "step" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set dt expects a numeric value"))?;
let dt = parse_f32(raw)?;
if dt <= 0.0 {
return Err(String::from("step size must be > 0"));
}
state.tick_seconds = dt;
Ok(CommandOutcome::Continue(format!(
"Default tick size set to {:.2} s.",
dt
)))
}
"arrhythmia" => {
let flag = parse_toggle(parts.next())?;
let heart = state
.heart_mut()
.ok_or_else(|| String::from("heart organ is not present"))?;
heart.arrhythmia = flag;
Ok(CommandOutcome::Continue(format!(
"Heart arrhythmia forcing set to {}.",
yes_no(flag)
)))
}
"tone" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set tone expects a value"))?;
let value = parse_f32(raw)?;
let clamped = value.clamp(-1.0, 1.0);
let heart = state
.heart_mut()
.ok_or_else(|| String::from("heart organ is not present"))?;
heart.autonomic_tone = clamped;
Ok(CommandOutcome::Continue(format!(
"Heart autonomic tone set to {:+.2} (input {:+.2}).",
clamped, value
)))
}
"svr" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set svr expects a value"))?;
let value = parse_f32(raw)?;
if value <= 0.0 {
return Err(String::from("systemic vascular resistance must be > 0"));
}
let heart = state
.heart_mut()
.ok_or_else(|| String::from("heart organ is not present"))?;
heart.systemic_vascular_resistance = value;
Ok(CommandOutcome::Continue(format!(
"Heart systemic vascular resistance set to {:.2} mmHg*min/L.",
value
)))
}
"glucose" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set glucose expects mg/dL"))?;
let value = parse_f32(raw)?;
state.patient.blood.glucose_mg_dl = value;
Ok(CommandOutcome::Continue(format!(
"Blood glucose set to {:.1} mg/dL.",
value
)))
}
"spo2" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set spo2 expects a percentage"))?;
let value = parse_f32(raw)?.clamp(0.0, 100.0);
state.patient.blood.spo2_pct = value;
Ok(CommandOutcome::Continue(format!(
"Blood SpO2 set to {:.0}%.",
value
)))
}
"bp" | "bloodpressure" => {
let systolic_raw = parts
.next()
.ok_or_else(|| String::from("set bp expects systolic and diastolic values"))?;
let diastolic_raw = parts
.next()
.ok_or_else(|| String::from("set bp expects systolic and diastolic values"))?;
let systolic = parse_u16(systolic_raw)?;
let diastolic = parse_u16(diastolic_raw)?;
if systolic <= diastolic {
return Err(String::from("systolic must be greater than diastolic"));
}
state.patient.blood_pressure = BloodPressure {
systolic,
diastolic,
};
Ok(CommandOutcome::Continue(format!(
"Blood pressure set to {}/{} mmHg.",
systolic, diastolic
)))
}
"bmi" => {
let weight_raw = parts
.next()
.ok_or_else(|| String::from("set bmi expects weight in kg"))?;
let height_raw = parts
.next()
.ok_or_else(|| String::from("set bmi expects height in m"))?;
let weight = parse_f32(weight_raw)?;
let height = parse_f32(height_raw)?;
let measurement = bmi_measurement(weight, height).map_err(|err| err.to_string())?;
let class = classify_bmi(measurement.value);
state.bmi_inputs = (weight, height);
Ok(CommandOutcome::Continue(format!(
"Tracked BMI updated -> {:.2} {} ({:?}).",
measurement.value, measurement.unit, class
)))
}
other => Err(format!("unknown field '{other}'")),
}
}
fn render_dashboard(state: &MonitorState, status: &str) {
clear_screen();
println!("=== MedicalLib Console Monitor ===");
println!(
"Simulation time: {:>7.2} s | Step: {:>4.2} s | Organs: {}",
state.sim_time,
state.tick_seconds,
MONITORED_ORGANS.len()
);
let vitals = VITAL_SIGNS
.iter()
.map(|v| format!("{:?}", v))
.collect::<Vec<_>>()
.join(", ");
println!("Tracked vital signs: {vitals}");
println!();
let bp = state.patient.blood_pressure;
println!(
"Blood pressure : {} (valid: {})",
bp,
yes_no(bp.validate())
);
let blood = &state.patient.blood;
println!(
"Blood chemistry : Hgb={:.1} g/dL | Hct={:.1}% | SpO2={:.0}% | Glucose={:.1} mg/dL (valid: {})",
blood.hemoglobin_g_dl,
blood.hematocrit_pct,
blood.spo2_pct,
blood.glucose_mg_dl,
yes_no(blood.validate())
);
let (weight, height) = state.bmi_inputs;
let bmi_line = match bmi_measurement(weight, height) {
Ok(measurement) => {
let class = classify_bmi(measurement.value);
format!(
"Tracked BMI : {:.1} kg / {:.2} m -> {:.2} {} ({:?})",
weight, height, measurement.value, measurement.unit, class
)
}
Err(err) => format!(
"Tracked BMI : inputs {:.1} kg / {:.2} m -> error ({err})",
weight, height
),
};
println!("{bmi_line}");
println!();
if let Some(heart) = state.patient.find_organ_typed::<Heart>() {
let rate = Measurement::new(heart.heart_rate_bpm, "bpm");
println!(
"Heart : HR={:.0} {} | Rhythm={:?} | CO={:.1} L/min | Tone={:+.2} | SVR={:.1}",
rate.value,
rate.unit,
heart.rhythm_state,
heart.cardiac_output_l_min,
heart.autonomic_tone,
heart.systemic_vascular_resistance
);
println!(
" Arrhythmia forced={} | EF={:.0}% | MAP~{:.0} mmHg",
yes_no(heart.arrhythmia),
heart.ejection_fraction * 100.0,
mean_arterial_pressure(bp)
);
} else {
println!("Heart : <not attached>");
}
println!();
println!("Organ snapshots:");
for organ in MONITORED_ORGANS {
println!(
" {:<12} {}",
organ_label(organ),
organ_snapshot(&state.patient, organ)
);
}
println!();
println!("Status:");
for line in status.lines() {
println!(" {line}");
}
println!();
println!("(press Enter without typing to step once; type 'help' for commands)");
}
fn organ_snapshot(patient: &Patient, kind: OrganType) -> String {
patient
.organ_summary(kind)
.unwrap_or_else(|err| format!("n/a ({err})"))
}
fn organ_label(kind: OrganType) -> &'static str {
match kind {
OrganType::Heart => "Heart",
OrganType::Lungs => "Lungs",
OrganType::Brain => "Brain",
OrganType::SpinalCord => "Spinal cord",
OrganType::Stomach => "Stomach",
OrganType::Liver => "Liver",
OrganType::Gallbladder => "Gallbladder",
OrganType::Pancreas => "Pancreas",
OrganType::Intestines => "Intestines",
OrganType::Esophagus => "Esophagus",
OrganType::Kidneys => "Kidneys",
OrganType::Bladder => "Bladder",
OrganType::Spleen => "Spleen",
}
}
fn parse_f32(raw: &str) -> Result<f32, String> {
raw.parse::<f32>()
.map_err(|_| format!("unable to parse '{raw}' as a decimal number"))
}
fn parse_u16(raw: &str) -> Result<u16, String> {
raw.parse::<u16>()
.map_err(|_| format!("unable to parse '{raw}' as an integer"))
}
fn parse_toggle(value: Option<&str>) -> Result<bool, String> {
let raw = value.ok_or_else(|| String::from("expected on/off"))?;
match raw.to_ascii_lowercase().as_str() {
"on" | "true" | "1" | "yes" => Ok(true),
"off" | "false" | "0" | "no" => Ok(false),
other => Err(format!("expected on/off but received '{other}'")),
}
}
fn yes_no(value: bool) -> &'static str {
if value {
"yes"
} else {
"no"
}
}
fn mean_arterial_pressure(bp: BloodPressure) -> f32 {
let systolic = bp.systolic as f32;
let diastolic = bp.diastolic as f32;
diastolic + (systolic - diastolic) / 3.0
}
fn clear_screen() {
print!("\x1b[2J\x1b[H");
let _ = io::stdout().flush();
}
+9 -7
View File
@@ -139,16 +139,18 @@ impl Bladder {
fn update_pressure(&mut self) {
let passive_volume_ml = (self.volume_ml - 30.0).max(0.0);
let normalized_volume = (self.volume_ml / self.capacity_ml).clamp(0.0, 1.5);
let compliance_factor = 1.0 + 4.0 * normalized_volume.powf(4.0);
let passive_pressure = if self.compliance_ml_per_cm_h2o > 0.0 {
passive_volume_ml / self.compliance_ml_per_cm_h2o * compliance_factor
let normalized_volume = (self.volume_ml / self.capacity_ml).clamp(0.0, 1.6);
let compliance_scale = 1.0 + 2.4 * normalized_volume;
let passive_base = if self.compliance_ml_per_cm_h2o > 0.0 {
passive_volume_ml / (self.compliance_ml_per_cm_h2o * compliance_scale)
} else {
0.0
};
let active_pressure = 40.0 * self.parasympathetic_drive;
let abdominal = self.baseline_pressure_cm_h2o;
self.pressure = (abdominal + passive_pressure + active_pressure).clamp(0.0, 90.0);
let nonlinear_gain = 1.0 + normalized_volume.powf(2.0);
let passive_pressure = passive_base * nonlinear_gain;
let active_pressure = 34.0 * self.parasympathetic_drive;
let abdominal = self.baseline_pressure_cm_h2o + 2.0 * normalized_volume;
self.pressure = (abdominal + passive_pressure + active_pressure).clamp(0.0, 80.0);
}
fn handle_filling_phase(&mut self, _dt_seconds: f32) {
+51 -7
View File
@@ -2,7 +2,7 @@ use super::{Organ, OrganInfo};
use crate::types::{BloodPressure, OrganType};
const BASE_SV_ML: f32 = 70.0;
const BASE_SVR_MMHG_MIN_PER_L: f32 = 17.0;
const BASE_SVR_MMHG_MIN_PER_L: f32 = 18.5;
const BAROREFLEX_SET_POINT_MMHG: f32 = 93.0;
/// Rhythm archetypes representing dominant autonomic/conduction control of the heart.
@@ -78,15 +78,15 @@ impl Heart {
leads,
arrhythmia: false,
stroke_volume_ml: BASE_SV_ML,
cardiac_output_l_min: 5.0,
cardiac_output_l_min: BASE_SV_ML * 72.0 / 1000.0,
end_diastolic_volume_ml: 120.0,
end_systolic_volume_ml: 50.0,
ejection_fraction: 0.58,
contractility_index: 1.0,
preload_mm_hg: 8.0,
afterload_mm_hg: 85.0,
afterload_mm_hg: BASE_SVR_MMHG_MIN_PER_L * (BASE_SV_ML * 72.0 / 1000.0),
systemic_vascular_resistance: BASE_SVR_MMHG_MIN_PER_L,
venous_return_l_min: 5.0,
venous_return_l_min: BASE_SV_ML * 72.0 / 1000.0,
sa_node_rate_bpm: 72.0,
av_delay_ms: 160.0,
autonomic_tone: 0.0,
@@ -139,7 +139,7 @@ impl Heart {
5.0,
dt_seconds,
);
let svr_target = (BASE_SVR_MMHG_MIN_PER_L - 5.5 * self.autonomic_tone).clamp(10.0, 26.0);
let svr_target = (BASE_SVR_MMHG_MIN_PER_L + 5.5 * self.autonomic_tone).clamp(10.0, 26.0);
self.systemic_vascular_resistance = Self::approach(
self.systemic_vascular_resistance,
svr_target,
@@ -230,8 +230,10 @@ impl Heart {
);
let map_target = self.cardiac_output_l_min * self.systemic_vascular_resistance;
let pulse_pressure = (self.stroke_volume_ml / BASE_SV_ML).clamp(0.6, 2.0) * 40.0;
let systolic = (map_target + pulse_pressure / 2.0).clamp(80.0, 220.0);
let diastolic = (map_target - pulse_pressure / 2.5).clamp(40.0, systolic - 5.0);
let raw_diastolic = map_target - pulse_pressure / 3.0;
let raw_systolic = raw_diastolic + pulse_pressure;
let systolic = raw_systolic.clamp(80.0, 220.0);
let diastolic = raw_diastolic.clamp(40.0, (systolic - 5.0).max(40.0));
self.arterial_bp.systolic = systolic.round() as u16;
self.arterial_bp.diastolic = diastolic.round() as u16;
self.coronary_perfusion_mm_hg =
@@ -258,6 +260,48 @@ impl Heart {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resting_state_stays_stable() {
let mut heart = Heart::new("test-heart", 12);
for _ in 0..600 {
heart.update(1.0);
}
let baseline_hr = heart.heart_rate_bpm;
let baseline_map = heart.mean_arterial_pressure();
for _ in 0..600 {
heart.update(1.0);
}
let later_hr = heart.heart_rate_bpm;
let later_map = heart.mean_arterial_pressure();
assert!(
(60.0..=90.0).contains(&baseline_hr),
"baseline heart rate out of expected range: {baseline_hr}"
);
assert!(
(68.0..=78.0).contains(&later_hr),
"resting heart rate failed to settle: initial {baseline_hr}, later {later_hr}"
);
assert!(
(90.0..=96.0).contains(&baseline_map) && (90.0..=96.0).contains(&later_map),
"mean arterial pressure unstable: baseline {baseline_map}, later {later_map}"
);
assert!(
heart.autonomic_tone.abs() <= 0.2,
"autonomic tone should remain near neutral, found {} (hr={later_hr}, map={later_map})",
heart.autonomic_tone
);
assert!(
(18.0..=20.5).contains(&heart.systemic_vascular_resistance),
"systemic vascular resistance drifted to {}",
heart.systemic_vascular_resistance
);
}
}
impl Organ for Heart {
fn id(&self) -> &str {
self.info.id()
+309
View File
@@ -798,4 +798,313 @@ mod tests {
assert!(Patient::new("").is_err());
assert!(Patient::new("bad id").is_err());
}
#[test]
fn multi_organ_homeostasis_stability() {
use crate::organs::{
Bladder, Brain, Esophagus, Gallbladder, Intestines, Kidneys, Liver, Lungs, Pancreas,
SpinalCord, Spleen, Stomach,
};
struct Range {
min: f32,
max: f32,
}
impl Range {
fn new() -> Self {
Self {
min: f32::INFINITY,
max: f32::NEG_INFINITY,
}
}
fn observe(&mut self, value: f32) {
if !value.is_finite() {
return;
}
if value < self.min {
self.min = value;
}
if value > self.max {
self.max = value;
}
}
fn assert_within(&self, lo: f32, hi: f32, label: &str) {
assert!(
self.min.is_finite() && self.max.is_finite(),
"{label} never observed"
);
assert!(self.min >= lo, "{label} dipped below {lo}: {:.3}", self.min);
assert!(self.max <= hi, "{label} exceeded {hi}: {:.3}", self.max);
}
}
struct HomeostasisStats {
lung_rr: Range,
lung_spo2: Range,
lung_pco2: Range,
lung_ventilation: Range,
brain_icp: Range,
brain_cpp: Range,
brain_consciousness: Range,
brain_o2: Range,
kidney_gfr: Range,
kidney_urine_flow: Range,
kidney_osm: Range,
kidney_plasma: Range,
liver_glycogen: Range,
liver_albumin: Range,
liver_fat: Range,
liver_portal_pressure: Range,
stomach_volume: Range,
stomach_ph: Range,
intestines_motility: Range,
intestines_volume: Range,
intestines_microbiome: Range,
pancreas_insulin: Range,
pancreas_glucagon: Range,
pancreas_beta_mass: Range,
pancreas_glucose: Range,
gallbladder_volume: Range,
gallbladder_csi: Range,
spleen_platelets: Range,
spleen_red_pulp: Range,
bladder_volume: Range,
bladder_pressure: Range,
esophagus_acid: Range,
esophagus_ph: Range,
spinal_sympathetic: Range,
spinal_cpp: Range,
}
impl HomeostasisStats {
fn new() -> Self {
Self {
lung_rr: Range::new(),
lung_spo2: Range::new(),
lung_pco2: Range::new(),
lung_ventilation: Range::new(),
brain_icp: Range::new(),
brain_cpp: Range::new(),
brain_consciousness: Range::new(),
brain_o2: Range::new(),
kidney_gfr: Range::new(),
kidney_urine_flow: Range::new(),
kidney_osm: Range::new(),
kidney_plasma: Range::new(),
liver_glycogen: Range::new(),
liver_albumin: Range::new(),
liver_fat: Range::new(),
liver_portal_pressure: Range::new(),
stomach_volume: Range::new(),
stomach_ph: Range::new(),
intestines_motility: Range::new(),
intestines_volume: Range::new(),
intestines_microbiome: Range::new(),
pancreas_insulin: Range::new(),
pancreas_glucagon: Range::new(),
pancreas_beta_mass: Range::new(),
pancreas_glucose: Range::new(),
gallbladder_volume: Range::new(),
gallbladder_csi: Range::new(),
spleen_platelets: Range::new(),
spleen_red_pulp: Range::new(),
bladder_volume: Range::new(),
bladder_pressure: Range::new(),
esophagus_acid: Range::new(),
esophagus_ph: Range::new(),
spinal_sympathetic: Range::new(),
spinal_cpp: Range::new(),
}
}
fn observe(&mut self, patient: &Patient) {
let lungs = patient.find_organ_typed::<Lungs>().expect("lungs present");
self.lung_rr.observe(lungs.respiratory_rate_bpm);
self.lung_spo2.observe(lungs.spo2_pct);
self.lung_pco2.observe(lungs.alveolar_pco2_mm_hg);
self.lung_ventilation
.observe(lungs.minute_ventilation_l_min);
let brain = patient.find_organ_typed::<Brain>().expect("brain present");
self.brain_icp.observe(brain.intracranial_pressure_mm_hg);
self.brain_cpp
.observe(brain.cerebral_perfusion_pressure_mm_hg);
self.brain_consciousness.observe(brain.consciousness as f32);
self.brain_o2.observe(brain.oxygenation_saturation);
let kidneys = patient
.find_organ_typed::<Kidneys>()
.expect("kidneys present");
self.kidney_gfr.observe(kidneys.gfr);
self.kidney_urine_flow.observe(kidneys.urine_flow_ml_min);
self.kidney_osm.observe(kidneys.serum_osmolality_mosm);
self.kidney_plasma.observe(kidneys.plasma_volume_l);
let liver = patient.find_organ_typed::<Liver>().expect("liver present");
self.liver_glycogen.observe(liver.glycogen_store_g);
self.liver_albumin.observe(liver.albumin_g_dl);
self.liver_fat.observe(liver.hepatic_fat_fraction_pct);
self.liver_portal_pressure
.observe(liver.portal_pressure_mm_hg);
let stomach = patient
.find_organ_typed::<Stomach>()
.expect("stomach present");
self.stomach_volume.observe(stomach.volume_ml);
self.stomach_ph.observe(stomach.ph);
let intestines = patient
.find_organ_typed::<Intestines>()
.expect("intestines present");
self.intestines_motility.observe(intestines.motility_index);
self.intestines_volume.observe(intestines.lumen_volume_ml);
self.intestines_microbiome
.observe(intestines.microbiome_balance);
let pancreas = patient
.find_organ_typed::<Pancreas>()
.expect("pancreas present");
self.pancreas_insulin.observe(pancreas.insulin);
self.pancreas_glucagon.observe(pancreas.glucagon);
self.pancreas_beta_mass
.observe(pancreas.beta_cell_mass_fraction);
self.pancreas_glucose.observe(pancreas.blood_glucose_mg_dl);
let gallbladder = patient
.find_organ_typed::<Gallbladder>()
.expect("gallbladder present");
self.gallbladder_volume.observe(gallbladder.bile_volume_ml);
self.gallbladder_csi
.observe(gallbladder.cholesterol_saturation_index);
let spleen = patient
.find_organ_typed::<Spleen>()
.expect("spleen present");
self.spleen_platelets.observe(spleen.platelet_reservoir);
self.spleen_red_pulp.observe(spleen.red_pulp_volume_ml);
let bladder = patient
.find_organ_typed::<Bladder>()
.expect("bladder present");
self.bladder_volume.observe(bladder.volume_ml);
self.bladder_pressure.observe(bladder.pressure);
let esophagus = patient
.find_organ_typed::<Esophagus>()
.expect("esophagus present");
self.esophagus_acid
.observe(esophagus.acid_exposure_fraction);
self.esophagus_ph.observe(esophagus.luminal_ph);
let spinal = patient
.find_organ_typed::<SpinalCord>()
.expect("spinal cord present");
self.spinal_sympathetic.observe(spinal.sympathetic_outflow);
self.spinal_cpp
.observe(spinal.cord_perfusion_pressure_mm_hg);
}
fn assert_within_ranges(&self) {
self.lung_rr.assert_within(10.0, 28.0, "respiratory rate");
self.lung_spo2.assert_within(93.0, 100.0, "SpO2");
self.lung_pco2.assert_within(30.0, 48.0, "alveolar PCO2");
self.lung_ventilation
.assert_within(4.0, 18.0, "minute ventilation");
self.brain_icp.assert_within(6.0, 20.0, "ICP");
self.brain_cpp.assert_within(55.0, 100.0, "CPP");
self.brain_consciousness
.assert_within(40.0, 100.0, "consciousness");
self.brain_o2.assert_within(0.9, 1.0, "brain oxygenation");
self.kidney_gfr.assert_within(60.0, 135.0, "GFR");
self.kidney_urine_flow.assert_within(0.2, 6.0, "urine flow");
self.kidney_osm
.assert_within(275.0, 305.0, "serum osmolality");
self.kidney_plasma.assert_within(2.4, 3.6, "plasma volume");
self.liver_glycogen
.assert_within(20.0, 130.0, "glycogen store");
self.liver_albumin.assert_within(3.0, 4.8, "albumin");
self.liver_fat
.assert_within(2.0, 18.0, "hepatic fat fraction");
self.liver_portal_pressure
.assert_within(4.0, 12.0, "portal pressure");
self.stomach_volume
.assert_within(30.0, 1600.0, "stomach volume");
self.stomach_ph.assert_within(1.2, 5.5, "stomach pH");
self.intestines_motility
.assert_within(0.3, 0.9, "intestinal motility");
self.intestines_volume
.assert_within(120.0, 800.0, "intestinal volume");
self.intestines_microbiome
.assert_within(0.4, 0.95, "microbiome balance");
self.pancreas_insulin.assert_within(4.0, 60.0, "insulin");
self.pancreas_glucagon
.assert_within(30.0, 150.0, "glucagon");
self.pancreas_beta_mass
.assert_within(0.6, 1.0, "beta-cell mass fraction");
self.pancreas_glucose
.assert_within(80.0, 155.0, "blood glucose");
self.gallbladder_volume
.assert_within(8.0, 50.0, "gallbladder volume");
self.gallbladder_csi
.assert_within(0.6, 1.4, "gallbladder CSI");
self.spleen_platelets
.assert_within(30.0, 150.0, "splenic platelets");
self.spleen_red_pulp
.assert_within(100.0, 300.0, "red pulp volume");
self.bladder_volume
.assert_within(20.0, 620.0, "bladder volume");
self.bladder_pressure
.assert_within(0.0, 80.0, "bladder pressure");
self.esophagus_acid
.assert_within(0.0, 0.6, "esophageal acid exposure");
self.esophagus_ph.assert_within(1.0, 7.0, "esophageal pH");
self.spinal_sympathetic
.assert_within(0.3, 0.7, "spinal sympathetic outflow");
self.spinal_cpp
.assert_within(55.0, 90.0, "spinal cord perfusion");
}
}
let mut patient = Patient::new("homeostasis")
.unwrap()
.initialize_default()
.with_lungs();
for organ in [
OrganType::Brain,
OrganType::SpinalCord,
OrganType::Stomach,
OrganType::Liver,
OrganType::Gallbladder,
OrganType::Pancreas,
OrganType::Intestines,
OrganType::Esophagus,
OrganType::Kidneys,
OrganType::Bladder,
OrganType::Spleen,
] {
patient = patient.with_organ(organ);
}
let mut stats = HomeostasisStats::new();
let dt = 1.0;
let steps = 5 * 3600; // 5 hours of simulated time
for _ in 0..steps {
patient.update(dt);
stats.observe(&patient);
}
stats.assert_within_ranges();
}
}