diff --git a/Cargo.toml b/Cargo.toml index 7e0d861..b135259 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/examples/demo_app.rs b/examples/demo_app.rs new file mode 100644 index 0000000..27e0d02 --- /dev/null +++ b/examples/demo_app.rs @@ -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 [dt] Run multiple ticks back-to-back + set dt Update the default tick size + set arrhythmia Force arrhythmic behaviour on the heart + set tone Set heart autonomic tone (-1.0..=1.0) + set svr Set heart systemic vascular resistance (mmHg*min/L) + set glucose Override blood glucose + set spo2 Override blood SpO2 (0-100) + set bp Override brachial blood pressure + set bmi Update the tracked BMI inputs + bmi 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 { + 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::() + } +} + +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 { + 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, +) -> Result { + 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::>() + .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::() { + 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 : "); + } + + 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 { + raw.parse::() + .map_err(|_| format!("unable to parse '{raw}' as a decimal number")) +} + +fn parse_u16(raw: &str) -> Result { + raw.parse::() + .map_err(|_| format!("unable to parse '{raw}' as an integer")) +} + +fn parse_toggle(value: Option<&str>) -> Result { + 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(); +} diff --git a/src/organs/bladder.rs b/src/organs/bladder.rs index e774d5f..1a8aab2 100644 --- a/src/organs/bladder.rs +++ b/src/organs/bladder.rs @@ -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) { diff --git a/src/organs/heart.rs b/src/organs/heart.rs index 1489ae9..f603f6e 100644 --- a/src/organs/heart.rs +++ b/src/organs/heart.rs @@ -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() diff --git a/src/patient.rs b/src/patient.rs index eee73fc..1f39271 100644 --- a/src/patient.rs +++ b/src/patient.rs @@ -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::().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::().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::() + .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::().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::() + .expect("stomach present"); + self.stomach_volume.observe(stomach.volume_ml); + self.stomach_ph.observe(stomach.ph); + + let intestines = patient + .find_organ_typed::() + .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::() + .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::() + .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::() + .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::() + .expect("bladder present"); + self.bladder_volume.observe(bladder.volume_ml); + self.bladder_pressure.observe(bladder.pressure); + + let esophagus = patient + .find_organ_typed::() + .expect("esophagus present"); + self.esophagus_acid + .observe(esophagus.acid_exposure_fraction); + self.esophagus_ph.observe(esophagus.luminal_ph); + + let spinal = patient + .find_organ_typed::() + .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(); + } }