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:
@@ -14,6 +14,7 @@ crate-type = ["rlib", "cdylib"]
|
|||||||
default = []
|
default = []
|
||||||
serde = ["dep:serde"]
|
serde = ["dep:serde"]
|
||||||
ffi = []
|
ffi = []
|
||||||
|
demo-monitor = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
@@ -24,6 +25,11 @@ tracing = { version = "0.1", optional = true }
|
|||||||
criterion = { version = "0.5", default-features = false, features = ["cargo_bench_support"] }
|
criterion = { version = "0.5", default-features = false, features = ["cargo_bench_support"] }
|
||||||
tracing-subscriber = { version = "0.3" }
|
tracing-subscriber = { version = "0.3" }
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "demo_app"
|
||||||
|
path = "examples/demo_app.rs"
|
||||||
|
required-features = ["demo-monitor"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "heart"
|
name = "heart"
|
||||||
harness = false
|
harness = false
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -139,16 +139,18 @@ impl Bladder {
|
|||||||
|
|
||||||
fn update_pressure(&mut self) {
|
fn update_pressure(&mut self) {
|
||||||
let passive_volume_ml = (self.volume_ml - 30.0).max(0.0);
|
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 normalized_volume = (self.volume_ml / self.capacity_ml).clamp(0.0, 1.6);
|
||||||
let compliance_factor = 1.0 + 4.0 * normalized_volume.powf(4.0);
|
let compliance_scale = 1.0 + 2.4 * normalized_volume;
|
||||||
let passive_pressure = if self.compliance_ml_per_cm_h2o > 0.0 {
|
let passive_base = if self.compliance_ml_per_cm_h2o > 0.0 {
|
||||||
passive_volume_ml / self.compliance_ml_per_cm_h2o * compliance_factor
|
passive_volume_ml / (self.compliance_ml_per_cm_h2o * compliance_scale)
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
let active_pressure = 40.0 * self.parasympathetic_drive;
|
let nonlinear_gain = 1.0 + normalized_volume.powf(2.0);
|
||||||
let abdominal = self.baseline_pressure_cm_h2o;
|
let passive_pressure = passive_base * nonlinear_gain;
|
||||||
self.pressure = (abdominal + passive_pressure + active_pressure).clamp(0.0, 90.0);
|
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) {
|
fn handle_filling_phase(&mut self, _dt_seconds: f32) {
|
||||||
|
|||||||
+51
-7
@@ -2,7 +2,7 @@ use super::{Organ, OrganInfo};
|
|||||||
use crate::types::{BloodPressure, OrganType};
|
use crate::types::{BloodPressure, OrganType};
|
||||||
|
|
||||||
const BASE_SV_ML: f32 = 70.0;
|
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;
|
const BAROREFLEX_SET_POINT_MMHG: f32 = 93.0;
|
||||||
|
|
||||||
/// Rhythm archetypes representing dominant autonomic/conduction control of the heart.
|
/// Rhythm archetypes representing dominant autonomic/conduction control of the heart.
|
||||||
@@ -78,15 +78,15 @@ impl Heart {
|
|||||||
leads,
|
leads,
|
||||||
arrhythmia: false,
|
arrhythmia: false,
|
||||||
stroke_volume_ml: BASE_SV_ML,
|
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_diastolic_volume_ml: 120.0,
|
||||||
end_systolic_volume_ml: 50.0,
|
end_systolic_volume_ml: 50.0,
|
||||||
ejection_fraction: 0.58,
|
ejection_fraction: 0.58,
|
||||||
contractility_index: 1.0,
|
contractility_index: 1.0,
|
||||||
preload_mm_hg: 8.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,
|
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,
|
sa_node_rate_bpm: 72.0,
|
||||||
av_delay_ms: 160.0,
|
av_delay_ms: 160.0,
|
||||||
autonomic_tone: 0.0,
|
autonomic_tone: 0.0,
|
||||||
@@ -139,7 +139,7 @@ impl Heart {
|
|||||||
5.0,
|
5.0,
|
||||||
dt_seconds,
|
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 = Self::approach(
|
||||||
self.systemic_vascular_resistance,
|
self.systemic_vascular_resistance,
|
||||||
svr_target,
|
svr_target,
|
||||||
@@ -230,8 +230,10 @@ impl Heart {
|
|||||||
);
|
);
|
||||||
let map_target = self.cardiac_output_l_min * self.systemic_vascular_resistance;
|
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 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 raw_diastolic = map_target - pulse_pressure / 3.0;
|
||||||
let diastolic = (map_target - pulse_pressure / 2.5).clamp(40.0, systolic - 5.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.systolic = systolic.round() as u16;
|
||||||
self.arterial_bp.diastolic = diastolic.round() as u16;
|
self.arterial_bp.diastolic = diastolic.round() as u16;
|
||||||
self.coronary_perfusion_mm_hg =
|
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 {
|
impl Organ for Heart {
|
||||||
fn id(&self) -> &str {
|
fn id(&self) -> &str {
|
||||||
self.info.id()
|
self.info.id()
|
||||||
|
|||||||
+309
@@ -798,4 +798,313 @@ mod tests {
|
|||||||
assert!(Patient::new("").is_err());
|
assert!(Patient::new("").is_err());
|
||||||
assert!(Patient::new("bad id").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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user