dez
Quick CI / quick-test (push) Failing after 1m12s
Multi-Platform CI / test-platforms (ubuntu-22.04) (push) Successful in 7m13s
Multi-Platform CI / test-platforms (windows-latest) (push) Successful in 7m8s
Multi-Platform CI / package (ubuntu-22.04) (push) Failing after 7m12s
Multi-Platform CI / package (windows-latest) (push) Failing after 7m13s

This commit is contained in:
2025-09-22 00:24:27 -07:00
parent d71b0ec5ff
commit 3fc010301d
25 changed files with 484 additions and 182 deletions
+1 -1
View File
@@ -1,4 +1,5 @@
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use medicallib_rust::Organ;
fn bench_heart_update(c: &mut Criterion) { fn bench_heart_update(c: &mut Criterion) {
c.bench_function("heart_update_1s", |b| { c.bench_function("heart_update_1s", |b| {
@@ -11,4 +12,3 @@ fn bench_heart_update(c: &mut Criterion) {
criterion_group!(benches, bench_heart_update); criterion_group!(benches, bench_heart_update);
criterion_main!(benches); criterion_main!(benches);
-1
View File
@@ -5,4 +5,3 @@ fn main() {
p.update(0.5); p.update(0.5);
println!("{}", p.patient_summary()); println!("{}", p.patient_summary());
} }
+5 -3
View File
@@ -4,15 +4,17 @@ fn main() {
{ {
use tracing::{info, Level}; use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber; use tracing_subscriber::FmtSubscriber;
let subscriber = FmtSubscriber::builder().with_max_level(Level::INFO).finish(); let subscriber = FmtSubscriber::builder()
.with_max_level(Level::INFO)
.finish();
let _ = tracing::subscriber::set_global_default(subscriber); let _ = tracing::subscriber::set_global_default(subscriber);
info!("starting simulation"); info!("starting simulation");
} }
let mut p = medicallib_rust::Patient::new("trace-demo").unwrap() let mut p = medicallib_rust::Patient::new("trace-demo")
.unwrap()
.initialize_default() .initialize_default()
.with_lungs(); .with_lungs();
p.update(0.2); p.update(0.2);
println!("{}", p.patient_summary()); println!("{}", p.patient_summary());
} }
-1
View File
@@ -5,4 +5,3 @@ fn main() {
let class = medicallib_rust::classify_bmi(bmi); let class = medicallib_rust::classify_bmi(bmi);
println!("BMI: {:.2} kg/m^2 — {:?}", bmi, class); println!("BMI: {:.2} kg/m^2 — {:?}", bmi, class);
} }
+38 -12
View File
@@ -23,6 +23,7 @@ pub const ML_EINVAL: i32 = 2;
/// Opaque patient handle type for C consumers. /// Opaque patient handle type for C consumers.
#[repr(C)] #[repr(C)]
#[derive(Debug)]
pub struct MLPatient { pub struct MLPatient {
inner: Patient, inner: Patient,
} }
@@ -31,7 +32,9 @@ pub struct MLPatient {
/// On success writes result to `out_bmi`. /// On success writes result to `out_bmi`.
#[no_mangle] #[no_mangle]
pub extern "C" fn medicallib_bmi(weight_kg: f32, height_m: f32, out_bmi: *mut f32) -> i32 { pub extern "C" fn medicallib_bmi(weight_kg: f32, height_m: f32, out_bmi: *mut f32) -> i32 {
if out_bmi.is_null() { return ML_EINVAL; } if out_bmi.is_null() {
return ML_EINVAL;
}
match crate::calculate_bmi(weight_kg, height_m) { match crate::calculate_bmi(weight_kg, height_m) {
Ok(v) => unsafe { Ok(v) => unsafe {
*out_bmi = v; *out_bmi = v;
@@ -44,11 +47,15 @@ pub extern "C" fn medicallib_bmi(weight_kg: f32, height_m: f32, out_bmi: *mut f3
/// Create a new patient with id string. Returns null on error. /// Create a new patient with id string. Returns null on error.
#[no_mangle] #[no_mangle]
pub extern "C" fn ml_patient_new(id: *const c_char) -> *mut MLPatient { pub extern "C" fn ml_patient_new(id: *const c_char) -> *mut MLPatient {
if id.is_null() { return ptr::null_mut(); } if id.is_null() {
return ptr::null_mut();
}
let cstr = unsafe { CStr::from_ptr(id) }; let cstr = unsafe { CStr::from_ptr(id) };
match cstr.to_str() { match cstr.to_str() {
Ok(s) => match Patient::new(s) { Ok(s) => match Patient::new(s) {
Ok(p) => Box::into_raw(Box::new(MLPatient { inner: p.initialize_default() })), Ok(p) => Box::into_raw(Box::new(MLPatient {
inner: p.initialize_default(),
})),
Err(_) => ptr::null_mut(), Err(_) => ptr::null_mut(),
}, },
Err(_) => ptr::null_mut(), Err(_) => ptr::null_mut(),
@@ -58,15 +65,21 @@ pub extern "C" fn ml_patient_new(id: *const c_char) -> *mut MLPatient {
/// Destroy a patient handle. Accepts null. /// Destroy a patient handle. Accepts null.
#[no_mangle] #[no_mangle]
pub extern "C" fn ml_patient_free(p: *mut MLPatient) { pub extern "C" fn ml_patient_free(p: *mut MLPatient) {
if p.is_null() { return; } if p.is_null() {
unsafe { drop(Box::from_raw(p)); } return;
}
unsafe {
drop(Box::from_raw(p));
}
} }
/// Get a newly-allocated C string summary for the patient. /// Get a newly-allocated C string summary for the patient.
/// Returns null on error. Free the returned string with `ml_string_free`. /// Returns null on error. Free the returned string with `ml_string_free`.
#[no_mangle] #[no_mangle]
pub extern "C" fn ml_patient_summary(p: *const MLPatient) -> *mut c_char { pub extern "C" fn ml_patient_summary(p: *const MLPatient) -> *mut c_char {
if p.is_null() { return ptr::null_mut(); } if p.is_null() {
return ptr::null_mut();
}
let summary = unsafe { (*p).inner.patient_summary() }; let summary = unsafe { (*p).inner.patient_summary() };
match CString::new(summary) { match CString::new(summary) {
Ok(s) => s.into_raw(), Ok(s) => s.into_raw(),
@@ -77,22 +90,32 @@ pub extern "C" fn ml_patient_summary(p: *const MLPatient) -> *mut c_char {
/// Frees a C string previously returned by this library. Accepts null. /// Frees a C string previously returned by this library. Accepts null.
#[no_mangle] #[no_mangle]
pub extern "C" fn ml_string_free(s: *mut c_char) { pub extern "C" fn ml_string_free(s: *mut c_char) {
if s.is_null() { return; } if s.is_null() {
unsafe { drop(CString::from_raw(s)); } return;
}
unsafe {
drop(CString::from_raw(s));
}
} }
/// Advance patient simulation by dt seconds. /// Advance patient simulation by dt seconds.
#[no_mangle] #[no_mangle]
pub extern "C" fn ml_patient_update(p: *mut MLPatient, dt_seconds: f32) -> i32 { pub extern "C" fn ml_patient_update(p: *mut MLPatient, dt_seconds: f32) -> i32 {
if p.is_null() { return ML_EINVAL; } if p.is_null() {
unsafe { (*p).inner.update(dt_seconds); } return ML_EINVAL;
}
unsafe {
(*p).inner.update(dt_seconds);
}
ML_OK ML_OK
} }
/// Return organ summary by type code. See header for codes. Caller frees string. /// Return organ summary by type code. See header for codes. Caller frees string.
#[no_mangle] #[no_mangle]
pub extern "C" fn ml_patient_organ_summary(p: *const MLPatient, organ_code: u32) -> *mut c_char { pub extern "C" fn ml_patient_organ_summary(p: *const MLPatient, organ_code: u32) -> *mut c_char {
if p.is_null() { return ptr::null_mut(); } if p.is_null() {
return ptr::null_mut();
}
let kind = match organ_code { let kind = match organ_code {
0 => OrganType::Heart, 0 => OrganType::Heart,
1 => OrganType::Lungs, 1 => OrganType::Lungs,
@@ -111,7 +134,10 @@ pub extern "C" fn ml_patient_organ_summary(p: *const MLPatient, organ_code: u32)
}; };
let summary = unsafe { (*p).inner.organ_summary(kind) }; let summary = unsafe { (*p).inner.organ_summary(kind) };
match summary { match summary {
Ok(s) => match CString::new(s) { Ok(cs) => cs.into_raw(), Err(_) => ptr::null_mut() }, Ok(s) => match CString::new(s) {
Ok(cs) => cs.into_raw(),
Err(_) => ptr::null_mut(),
},
Err(_) => ptr::null_mut(), Err(_) => ptr::null_mut(),
} }
} }
+1 -1
View File
@@ -27,9 +27,9 @@
#![warn(missing_docs, rust_2018_idioms, missing_debug_implementations)] #![warn(missing_docs, rust_2018_idioms, missing_debug_implementations)]
mod error; mod error;
mod types;
mod organs; mod organs;
mod patient; mod patient;
mod types;
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
pub mod ffi; pub mod ffi;
+25 -6
View File
@@ -12,18 +12,37 @@ pub struct Bladder {
impl Bladder { impl Bladder {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Bladder), volume_ml: 100.0, pressure: 5.0 } Self {
info: OrganInfo::new(id, OrganType::Bladder),
volume_ml: 100.0,
pressure: 5.0,
}
} }
} }
impl Organ for Bladder { impl Organ for Bladder {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) { fn update(&mut self, _dt_seconds: f32) {
// simplistic pressure-volume relation // simplistic pressure-volume relation
self.pressure = (self.volume_ml / 50.0).clamp(0.0, 30.0); self.pressure = (self.volume_ml / 50.0).clamp(0.0, 30.0);
} }
fn summary(&self) -> String { format!("Bladder[id={}, vol={:.0} ml, P={:.1}]", self.id(), self.volume_ml, self.pressure) } fn summary(&self) -> String {
fn as_any(&self) -> &dyn core::any::Any { self } format!(
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } "Bladder[id={}, vol={:.0} ml, P={:.1}]",
self.id(),
self.volume_ml,
self.pressure
)
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+20 -7
View File
@@ -12,23 +12,36 @@ pub struct Brain {
impl Brain { impl Brain {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Brain), consciousness: 100, activity_index: 1.0 } Self {
info: OrganInfo::new(id, OrganType::Brain),
consciousness: 100,
activity_index: 1.0,
}
} }
} }
impl Organ for Brain { impl Organ for Brain {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) { fn update(&mut self, _dt_seconds: f32) {
self.activity_index = self.activity_index.clamp(0.0, 2.0); self.activity_index = self.activity_index.clamp(0.0, 2.0);
} }
fn summary(&self) -> String { fn summary(&self) -> String {
format!( format!(
"Brain[id={}, GCS~{}, activity={:.2}]", "Brain[id={}, GCS~{}, activity={:.2}]",
self.id(), self.consciousness, self.activity_index self.id(),
self.consciousness,
self.activity_index
) )
} }
fn as_any(&self) -> &dyn core::any::Any { self } fn as_any(&self) -> &dyn core::any::Any {
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+19 -6
View File
@@ -10,17 +10,30 @@ pub struct Esophagus {
impl Esophagus { impl Esophagus {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Esophagus), reflux: 0 } Self {
info: OrganInfo::new(id, OrganType::Esophagus),
reflux: 0,
}
} }
} }
impl Organ for Esophagus { impl Organ for Esophagus {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) { fn update(&mut self, _dt_seconds: f32) {
self.reflux = self.reflux.min(100); self.reflux = self.reflux.min(100);
} }
fn summary(&self) -> String { format!("Esophagus[id={}, reflux={}]", self.id(), self.reflux) } fn summary(&self) -> String {
fn as_any(&self) -> &dyn core::any::Any { self } format!("Esophagus[id={}, reflux={}]", self.id(), self.reflux)
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } }
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+19 -7
View File
@@ -10,16 +10,28 @@ pub struct Gallbladder {
impl Gallbladder { impl Gallbladder {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Gallbladder), bile_ml: 30.0 } Self {
info: OrganInfo::new(id, OrganType::Gallbladder),
bile_ml: 30.0,
}
} }
} }
impl Organ for Gallbladder { impl Organ for Gallbladder {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) {} fn update(&mut self, _dt_seconds: f32) {}
fn summary(&self) -> String { format!("Gallbladder[id={}, bile={:.0} ml]", self.id(), self.bile_ml) } fn summary(&self) -> String {
fn as_any(&self) -> &dyn core::any::Any { self } format!("Gallbladder[id={}, bile={:.0} ml]", self.id(), self.bile_ml)
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } }
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+19 -6
View File
@@ -1,17 +1,22 @@
use super::{Organ, OrganInfo}; use super::{Organ, OrganInfo};
use crate::types::{BloodPressure, OrganType}; use crate::types::{BloodPressure, OrganType};
/// Cardiac model with simple rate and arterial pressure coupling.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Heart { pub struct Heart {
info: OrganInfo, info: OrganInfo,
/// Heart rate in beats per minute.
pub heart_rate_bpm: f32, pub heart_rate_bpm: f32,
/// Arterial blood pressure snapshot.
pub arterial_bp: BloodPressure, pub arterial_bp: BloodPressure,
/// ECG lead count configured for this heart.
pub leads: u8, pub leads: u8,
/// Simplified arrhythmia flag; increases HR variability. /// Simplified arrhythmia flag; increases HR variability.
pub arrhythmia: bool, pub arrhythmia: bool,
} }
impl Heart { impl Heart {
/// Construct a new heart with the given id and number of ECG leads.
pub fn new(id: impl Into<String>, leads: u8) -> Self { pub fn new(id: impl Into<String>, leads: u8) -> Self {
Self { Self {
info: OrganInfo::new(id, OrganType::Heart), info: OrganInfo::new(id, OrganType::Heart),
@@ -24,15 +29,19 @@ impl Heart {
} }
impl Organ for Heart { impl Organ for Heart {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, dt_seconds: f32) { fn update(&mut self, dt_seconds: f32) {
let dt = dt_seconds.max(0.0).min(10.0); let dt = dt_seconds.clamp(0.0, 10.0);
let target = 70.0f32; let target = 70.0f32;
let mut diff = target - self.heart_rate_bpm; let mut diff = target - self.heart_rate_bpm;
if self.arrhythmia { if self.arrhythmia {
// add variability // add variability
diff += (self.heart_rate_bpm.sin() * 5.0); diff += self.heart_rate_bpm.sin() * 5.0;
} }
self.heart_rate_bpm += 0.1 * diff * (dt / 1.0); self.heart_rate_bpm += 0.1 * diff * (dt / 1.0);
// crude BP coupling to HR // crude BP coupling to HR
@@ -51,6 +60,10 @@ impl Organ for Heart {
self.arterial_bp.diastolic self.arterial_bp.diastolic
) )
} }
fn as_any(&self) -> &dyn core::any::Any { self } fn as_any(&self) -> &dyn core::any::Any {
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+24 -6
View File
@@ -11,13 +11,21 @@ pub struct Intestines {
impl Intestines { impl Intestines {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Intestines), absorption: 80, peristalsis: true } Self {
info: OrganInfo::new(id, OrganType::Intestines),
absorption: 80,
peristalsis: true,
}
} }
} }
impl Organ for Intestines { impl Organ for Intestines {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, dt_seconds: f32) { fn update(&mut self, dt_seconds: f32) {
if self.peristalsis { if self.peristalsis {
// minor oscillation around 80 // minor oscillation around 80
@@ -26,7 +34,17 @@ impl Organ for Intestines {
self.absorption = val as u8; self.absorption = val as u8;
} }
} }
fn summary(&self) -> String { format!("Intestines[id={}, absorption={}]", self.id(), self.absorption) } fn summary(&self) -> String {
fn as_any(&self) -> &dyn core::any::Any { self } format!(
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } "Intestines[id={}, absorption={}]",
self.id(),
self.absorption
)
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+20 -6
View File
@@ -12,18 +12,32 @@ pub struct Kidneys {
impl Kidneys { impl Kidneys {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Kidneys), gfr: 100.0, electrolyte_balance: 0.0 } Self {
info: OrganInfo::new(id, OrganType::Kidneys),
gfr: 100.0,
electrolyte_balance: 0.0,
}
} }
} }
impl Organ for Kidneys { impl Organ for Kidneys {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) { fn update(&mut self, _dt_seconds: f32) {
self.gfr = self.gfr.clamp(0.0, 200.0); self.gfr = self.gfr.clamp(0.0, 200.0);
self.electrolyte_balance = self.electrolyte_balance.clamp(-1.0, 1.0); self.electrolyte_balance = self.electrolyte_balance.clamp(-1.0, 1.0);
} }
fn summary(&self) -> String { format!("Kidneys[id={}, GFR={:.0} ml/min]", self.id(), self.gfr) } fn summary(&self) -> String {
fn as_any(&self) -> &dyn core::any::Any { self } format!("Kidneys[id={}, GFR={:.0} ml/min]", self.id(), self.gfr)
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } }
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+27 -6
View File
@@ -14,17 +14,38 @@ pub struct Liver {
impl Liver { impl Liver {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Liver), detox: 100, metabolism: 1.0, enzymes: 1.0 } Self {
info: OrganInfo::new(id, OrganType::Liver),
detox: 100,
metabolism: 1.0,
enzymes: 1.0,
}
} }
} }
impl Organ for Liver { impl Organ for Liver {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) { fn update(&mut self, _dt_seconds: f32) {
self.enzymes = self.enzymes.clamp(0.0, 2.0); self.enzymes = self.enzymes.clamp(0.0, 2.0);
} }
fn summary(&self) -> String { format!("Liver[id={}, detox={}, k={:.2}, enz={:.2}]", self.id(), self.detox, self.metabolism, self.enzymes) } fn summary(&self) -> String {
fn as_any(&self) -> &dyn core::any::Any { self } format!(
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } "Liver[id={}, detox={}, k={:.2}, enz={:.2}]",
self.id(),
self.detox,
self.metabolism,
self.enzymes
)
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+29 -8
View File
@@ -1,39 +1,60 @@
use super::{Organ, OrganInfo}; use super::{Organ, OrganInfo};
use crate::types::OrganType; use crate::types::OrganType;
/// Pulmonary model tracking respiratory rate and oxygen saturation.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Lungs { pub struct Lungs {
info: OrganInfo, info: OrganInfo,
/// Respiratory rate in breaths per minute.
pub respiratory_rate_bpm: f32, pub respiratory_rate_bpm: f32,
/// Peripheral oxygen saturation percent.
pub spo2_pct: f32, pub spo2_pct: f32,
/// Respiratory distress flag reduces SpO2. /// Respiratory distress flag reduces SpO2.
pub distress: bool, pub distress: bool,
} }
impl Lungs { impl Lungs {
/// Construct lungs with a given id.
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Lungs), respiratory_rate_bpm: 14.0, spo2_pct: 98.0, distress: false } Self {
info: OrganInfo::new(id, OrganType::Lungs),
respiratory_rate_bpm: 14.0,
spo2_pct: 98.0,
distress: false,
}
} }
} }
impl Organ for Lungs { impl Organ for Lungs {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, dt_seconds: f32) { fn update(&mut self, dt_seconds: f32) {
let dt = dt_seconds.max(0.0).min(10.0); let dt = dt_seconds.clamp(0.0, 10.0);
let target_rr = 14.0; let target_rr = 14.0;
self.respiratory_rate_bpm += 0.1 * (target_rr - self.respiratory_rate_bpm) * (dt / 1.0); self.respiratory_rate_bpm += 0.1 * (target_rr - self.respiratory_rate_bpm) * (dt / 1.0);
// distress drifts SpO2 downward // distress drifts SpO2 downward
if self.distress { self.spo2_pct -= 0.5 * (dt / 1.0); } if self.distress {
self.spo2_pct -= 0.5 * (dt / 1.0);
}
// keep spo2 in [70, 100] // keep spo2 in [70, 100]
self.spo2_pct = self.spo2_pct.clamp(70.0, 100.0); self.spo2_pct = self.spo2_pct.clamp(70.0, 100.0);
} }
fn summary(&self) -> String { fn summary(&self) -> String {
format!( format!(
"Lungs[id={}, RR={:.1} bpm, SpO2={:.0}%]", "Lungs[id={}, RR={:.1} bpm, SpO2={:.0}%]",
self.id(), self.respiratory_rate_bpm, self.spo2_pct self.id(),
self.respiratory_rate_bpm,
self.spo2_pct
) )
} }
fn as_any(&self) -> &dyn core::any::Any { self } fn as_any(&self) -> &dyn core::any::Any {
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+39 -27
View File
@@ -1,6 +1,6 @@
//! Organ system trait and implementations. //! Organ system trait and implementations.
use crate::types::{BloodPressure, OrganType}; use crate::types::OrganType;
use core::fmt::Debug; use core::fmt::Debug;
/// Common metadata for organs. /// Common metadata for organs.
@@ -12,47 +12,59 @@ pub struct OrganInfo {
impl OrganInfo { impl OrganInfo {
pub fn new(id: impl Into<String>, kind: OrganType) -> Self { pub fn new(id: impl Into<String>, kind: OrganType) -> Self {
Self { id: id.into(), kind } Self {
id: id.into(),
kind,
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn kind(&self) -> OrganType {
self.kind
} }
pub fn id(&self) -> &str { &self.id }
pub fn kind(&self) -> OrganType { self.kind }
} }
/// Trait implemented by all organs. /// Trait implemented by all organs.
pub trait Organ: Debug + Send { pub trait Organ: Debug + Send {
/// Stable string identifier for this organ instance.
fn id(&self) -> &str; fn id(&self) -> &str;
/// Kind of organ (e.g., heart, lungs).
fn organ_type(&self) -> OrganType; fn organ_type(&self) -> OrganType;
/// Advance the organ simulation by `dt_seconds`.
fn update(&mut self, dt_seconds: f32); fn update(&mut self, dt_seconds: f32);
/// One-line human-readable status summary.
fn summary(&self) -> String; fn summary(&self) -> String;
/// Type-erased reference for downcasting.
fn as_any(&self) -> &dyn core::any::Any; fn as_any(&self) -> &dyn core::any::Any;
/// Type-erased mutable reference for downcasting.
fn as_any_mut(&mut self) -> &mut dyn core::any::Any; fn as_any_mut(&mut self) -> &mut dyn core::any::Any;
} }
mod heart;
mod lungs;
mod brain;
mod spinal_cord;
mod stomach;
mod liver;
mod gallbladder;
mod pancreas;
mod intestines;
mod esophagus;
mod kidneys;
mod bladder; mod bladder;
mod brain;
mod esophagus;
mod gallbladder;
mod heart;
mod intestines;
mod kidneys;
mod liver;
mod lungs;
mod pancreas;
mod spinal_cord;
mod spleen; mod spleen;
mod stomach;
pub use heart::Heart;
pub use lungs::Lungs;
pub use brain::Brain;
pub use spinal_cord::SpinalCord;
pub use stomach::Stomach;
pub use liver::Liver;
pub use gallbladder::Gallbladder;
pub use pancreas::Pancreas;
pub use intestines::Intestines;
pub use esophagus::Esophagus;
pub use kidneys::Kidneys;
pub use bladder::Bladder; pub use bladder::Bladder;
pub use brain::Brain;
pub use esophagus::Esophagus;
pub use gallbladder::Gallbladder;
pub use heart::Heart;
pub use intestines::Intestines;
pub use kidneys::Kidneys;
pub use liver::Liver;
pub use lungs::Lungs;
pub use pancreas::Pancreas;
pub use spinal_cord::SpinalCord;
pub use spleen::Spleen; pub use spleen::Spleen;
pub use stomach::Stomach;
+19 -7
View File
@@ -10,16 +10,28 @@ pub struct Pancreas {
impl Pancreas { impl Pancreas {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Pancreas), insulin: 1.0 } Self {
info: OrganInfo::new(id, OrganType::Pancreas),
insulin: 1.0,
}
} }
} }
impl Organ for Pancreas { impl Organ for Pancreas {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) {} fn update(&mut self, _dt_seconds: f32) {}
fn summary(&self) -> String { format!("Pancreas[id={}, insulin={:.2}]", self.id(), self.insulin) } fn summary(&self) -> String {
fn as_any(&self) -> &dyn core::any::Any { self } format!("Pancreas[id={}, insulin={:.2}]", self.id(), self.insulin)
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } }
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+28 -8
View File
@@ -11,17 +11,37 @@ pub struct SpinalCord {
impl SpinalCord { impl SpinalCord {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::SpinalCord), signal_integrity: 100, injury: false } Self {
info: OrganInfo::new(id, OrganType::SpinalCord),
signal_integrity: 100,
injury: false,
}
} }
} }
impl Organ for SpinalCord { impl Organ for SpinalCord {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
fn update(&mut self, _dt_seconds: f32) { }
if self.injury { self.signal_integrity = self.signal_integrity.saturating_sub(1); } fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) {
if self.injury {
self.signal_integrity = self.signal_integrity.saturating_sub(1);
}
}
fn summary(&self) -> String {
format!(
"SpinalCord[id={}, integrity={}]",
self.id(),
self.signal_integrity
)
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
} }
fn summary(&self) -> String { format!("SpinalCord[id={}, integrity={}]", self.id(), self.signal_integrity) }
fn as_any(&self) -> &dyn core::any::Any { self }
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self }
} }
+19 -7
View File
@@ -10,16 +10,28 @@ pub struct Spleen {
impl Spleen { impl Spleen {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Spleen), immune_activity: 80 } Self {
info: OrganInfo::new(id, OrganType::Spleen),
immune_activity: 80,
}
} }
} }
impl Organ for Spleen { impl Organ for Spleen {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) {} fn update(&mut self, _dt_seconds: f32) {}
fn summary(&self) -> String { format!("Spleen[id={}, immune={}]", self.id(), self.immune_activity) } fn summary(&self) -> String {
fn as_any(&self) -> &dyn core::any::Any { self } format!("Spleen[id={}, immune={}]", self.id(), self.immune_activity)
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } }
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+19 -7
View File
@@ -10,16 +10,28 @@ pub struct Stomach {
impl Stomach { impl Stomach {
pub fn new(id: impl Into<String>) -> Self { pub fn new(id: impl Into<String>) -> Self {
Self { info: OrganInfo::new(id, OrganType::Stomach), acid_level: 50 } Self {
info: OrganInfo::new(id, OrganType::Stomach),
acid_level: 50,
}
} }
} }
impl Organ for Stomach { impl Organ for Stomach {
fn id(&self) -> &str { self.info.id() } fn id(&self) -> &str {
fn organ_type(&self) -> OrganType { self.info.kind() } self.info.id()
}
fn organ_type(&self) -> OrganType {
self.info.kind()
}
fn update(&mut self, _dt_seconds: f32) {} fn update(&mut self, _dt_seconds: f32) {}
fn summary(&self) -> String { format!("Stomach[id={}, acid={}]", self.id(), self.acid_level) } fn summary(&self) -> String {
fn as_any(&self) -> &dyn core::any::Any { self } format!("Stomach[id={}, acid={}]", self.id(), self.acid_level)
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self } }
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
} }
+76 -20
View File
@@ -9,7 +9,9 @@ use crate::types::{Blood, BloodPressure, OrganType};
pub struct Patient { pub struct Patient {
id: String, id: String,
organs: Vec<Box<dyn Organ>>, organs: Vec<Box<dyn Organ>>,
/// Laboratory blood panel snapshot.
pub blood: Blood, pub blood: Blood,
/// Non-invasive brachial blood pressure.
pub blood_pressure: BloodPressure, pub blood_pressure: BloodPressure,
} }
@@ -18,7 +20,9 @@ impl Patient {
pub fn new(id: impl Into<String>) -> crate::Result<Self> { pub fn new(id: impl Into<String>) -> crate::Result<Self> {
let id = id.into(); let id = id.into();
if !is_valid_id(&id) { if !is_valid_id(&id) {
return Err(MedicalError::Validation("patient id must be [A-Za-z0-9_-]+ and 1..64 chars")); return Err(MedicalError::Validation(
"patient id must be [A-Za-z0-9_-]+ and 1..64 chars",
));
} }
Ok(Self { Ok(Self {
id, id,
@@ -29,7 +33,9 @@ impl Patient {
} }
/// Patient identifier. /// Patient identifier.
pub fn id(&self) -> &str { &self.id } pub fn id(&self) -> &str {
&self.id
}
/// Add an organ to the patient. /// Add an organ to the patient.
pub fn add_organ(&mut self, organ: impl Organ + 'static) { pub fn add_organ(&mut self, organ: impl Organ + 'static) {
@@ -38,7 +44,10 @@ impl Patient {
/// Find the first organ matching the given type. /// Find the first organ matching the given type.
pub fn find_organ(&self, kind: OrganType) -> Option<&dyn Organ> { pub fn find_organ(&self, kind: OrganType) -> Option<&dyn Organ> {
self.organs.iter().map(|b| b.as_ref()).find(|o| o.organ_type() == kind) self.organs
.iter()
.map(|b| b.as_ref())
.find(|o| o.organ_type() == kind)
} }
/// Find a typed organ by downcasting. /// Find a typed organ by downcasting.
@@ -79,26 +88,67 @@ impl Patient {
/// Attach a default organ by type. /// Attach a default organ by type.
pub fn with_organ(mut self, kind: OrganType) -> Self { pub fn with_organ(mut self, kind: OrganType) -> Self {
match kind { match kind {
OrganType::Heart => { let id = format!("{}-heart", self.id); self.add_organ(Heart::new(id, 12)); } OrganType::Heart => {
OrganType::Lungs => { let id = format!("{}-lungs", self.id); self.add_organ(Lungs::new(id)); } let id = format!("{}-heart", self.id);
OrganType::Brain => { let id = format!("{}-brain", self.id); self.add_organ(crate::organs::Brain::new(id)); } self.add_organ(Heart::new(id, 12));
OrganType::SpinalCord => { let id = format!("{}-sc", self.id); self.add_organ(crate::organs::SpinalCord::new(id)); } }
OrganType::Stomach => { let id = format!("{}-stomach", self.id); self.add_organ(crate::organs::Stomach::new(id)); } OrganType::Lungs => {
OrganType::Liver => { let id = format!("{}-liver", self.id); self.add_organ(crate::organs::Liver::new(id)); } let id = format!("{}-lungs", self.id);
OrganType::Gallbladder => { let id = format!("{}-gb", self.id); self.add_organ(crate::organs::Gallbladder::new(id)); } self.add_organ(Lungs::new(id));
OrganType::Pancreas => { let id = format!("{}-pancreas", self.id); self.add_organ(crate::organs::Pancreas::new(id)); } }
OrganType::Intestines => { let id = format!("{}-intestines", self.id); self.add_organ(crate::organs::Intestines::new(id)); } OrganType::Brain => {
OrganType::Esophagus => { let id = format!("{}-eso", self.id); self.add_organ(crate::organs::Esophagus::new(id)); } let id = format!("{}-brain", self.id);
OrganType::Kidneys => { let id = format!("{}-kidneys", self.id); self.add_organ(crate::organs::Kidneys::new(id)); } self.add_organ(crate::organs::Brain::new(id));
OrganType::Bladder => { let id = format!("{}-bladder", self.id); self.add_organ(crate::organs::Bladder::new(id)); } }
OrganType::Spleen => { let id = format!("{}-spleen", self.id); self.add_organ(crate::organs::Spleen::new(id)); } OrganType::SpinalCord => {
let id = format!("{}-sc", self.id);
self.add_organ(crate::organs::SpinalCord::new(id));
}
OrganType::Stomach => {
let id = format!("{}-stomach", self.id);
self.add_organ(crate::organs::Stomach::new(id));
}
OrganType::Liver => {
let id = format!("{}-liver", self.id);
self.add_organ(crate::organs::Liver::new(id));
}
OrganType::Gallbladder => {
let id = format!("{}-gb", self.id);
self.add_organ(crate::organs::Gallbladder::new(id));
}
OrganType::Pancreas => {
let id = format!("{}-pancreas", self.id);
self.add_organ(crate::organs::Pancreas::new(id));
}
OrganType::Intestines => {
let id = format!("{}-intestines", self.id);
self.add_organ(crate::organs::Intestines::new(id));
}
OrganType::Esophagus => {
let id = format!("{}-eso", self.id);
self.add_organ(crate::organs::Esophagus::new(id));
}
OrganType::Kidneys => {
let id = format!("{}-kidneys", self.id);
self.add_organ(crate::organs::Kidneys::new(id));
}
OrganType::Bladder => {
let id = format!("{}-bladder", self.id);
self.add_organ(crate::organs::Bladder::new(id));
}
OrganType::Spleen => {
let id = format!("{}-spleen", self.id);
self.add_organ(crate::organs::Spleen::new(id));
}
} }
self self
} }
/// Advance simulation by `dt_seconds`. /// Advance simulation by `dt_seconds`.
pub fn update(&mut self, dt_seconds: f32) { pub fn update(&mut self, dt_seconds: f32) {
for organ in &mut self.organs { organ.update(dt_seconds); } for organ in &mut self.organs {
organ.update(dt_seconds);
}
// Simple inter-organ signaling: low SpO2 nudges heart rate higher. // Simple inter-organ signaling: low SpO2 nudges heart rate higher.
if let Some(spo2) = self.find_organ_typed::<Lungs>().map(|l| l.spo2_pct) { if let Some(spo2) = self.find_organ_typed::<Lungs>().map(|l| l.spo2_pct) {
if let Some(heart) = self.find_organ_typed_mut::<Heart>() { if let Some(heart) = self.find_organ_typed_mut::<Heart>() {
@@ -111,7 +161,10 @@ impl Patient {
let produced_opt = self let produced_opt = self
.find_organ_typed::<crate::organs::Kidneys>() .find_organ_typed::<crate::organs::Kidneys>()
.map(|kidneys| (kidneys.gfr * (dt_seconds / 60.0)).max(0.0) * 0.5); // ml .map(|kidneys| (kidneys.gfr * (dt_seconds / 60.0)).max(0.0) * 0.5); // ml
if let (Some(produced), Some(bladder)) = (produced_opt, self.find_organ_typed_mut::<crate::organs::Bladder>()) { if let (Some(produced), Some(bladder)) = (
produced_opt,
self.find_organ_typed_mut::<crate::organs::Bladder>(),
) {
bladder.volume_ml += produced; bladder.volume_ml += produced;
} }
} }
@@ -147,8 +200,11 @@ impl Default for Patient {
fn is_valid_id(id: &str) -> bool { fn is_valid_id(id: &str) -> bool {
let len = id.len(); let len = id.len();
if !(1..=64).contains(&len) { return false; } if !(1..=64).contains(&len) {
id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') return false;
}
id.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
} }
#[cfg(test)] #[cfg(test)]
+13 -1
View File
@@ -6,18 +6,31 @@ use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Known organ categories. /// Known organ categories.
pub enum OrganType { pub enum OrganType {
/// Heart
Heart, Heart,
/// Lungs
Lungs, Lungs,
/// Brain
Brain, Brain,
/// Spinal cord
SpinalCord, SpinalCord,
/// Stomach
Stomach, Stomach,
/// Liver
Liver, Liver,
/// Gallbladder
Gallbladder, Gallbladder,
/// Pancreas
Pancreas, Pancreas,
/// Intestines
Intestines, Intestines,
/// Esophagus
Esophagus, Esophagus,
/// Kidneys
Kidneys, Kidneys,
/// Urinary bladder
Bladder, Bladder,
/// Spleen
Spleen, Spleen,
} }
@@ -89,4 +102,3 @@ impl Blood {
hgb_ok && hct_ok && spo2_ok && glucose_ok hgb_ok && hct_ok && spo2_ok && glucose_ok
} }
} }
-1
View File
@@ -5,4 +5,3 @@ fn bmi_works() {
let cls = medicallib_rust::classify_bmi(bmi); let cls = medicallib_rust::classify_bmi(bmi);
assert_eq!(cls, medicallib_rust::BMIClassification::Normal); assert_eq!(cls, medicallib_rust::BMIClassification::Normal);
} }
+21 -25
View File
@@ -1,35 +1,31 @@
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
#[test] #[test]
fn ffi_bmi_and_patient() { fn ffi_bmi_and_patient() {
unsafe { let mut out: f32 = 0.0;
let mut out: f32 = 0.0; let rc = medicallib_rust::ffi::medicallib_bmi(70.0, 1.75, &mut out as *mut f32);
let rc = medicallib_rust::ffi::medicallib_bmi(70.0, 1.75, &mut out as *mut f32); assert_eq!(rc, medicallib_rust::ffi::ML_OK);
assert_eq!(rc, medicallib_rust::ffi::ML_OK); assert!(out > 10.0);
assert!(out > 10.0);
use std::ffi::CString; use std::ffi::CString;
let id = CString::new("ffi-test").unwrap(); let id = CString::new("ffi-test").unwrap();
let p = medicallib_rust::ffi::ml_patient_new(id.as_ptr()); let p = medicallib_rust::ffi::ml_patient_new(id.as_ptr());
assert!(!p.is_null()); assert!(!p.is_null());
let _ = medicallib_rust::ffi::ml_patient_update(p, 0.1); let _ = medicallib_rust::ffi::ml_patient_update(p, 0.1);
let s = medicallib_rust::ffi::ml_patient_summary(p); let s = medicallib_rust::ffi::ml_patient_summary(p);
assert!(!s.is_null()); assert!(!s.is_null());
medicallib_rust::ffi::ml_string_free(s); medicallib_rust::ffi::ml_string_free(s);
medicallib_rust::ffi::ml_patient_free(p); medicallib_rust::ffi::ml_patient_free(p);
}
} }
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
#[test] #[test]
fn ffi_errors() { fn ffi_errors() {
unsafe { // Null out pointer arguments should error gracefully
// Null out pointer arguments should error gracefully let rc = medicallib_rust::ffi::medicallib_bmi(70.0, 1.75, std::ptr::null_mut());
let rc = medicallib_rust::ffi::medicallib_bmi(70.0, 1.75, std::ptr::null_mut()); assert_eq!(rc, medicallib_rust::ffi::ML_EINVAL);
assert_eq!(rc, medicallib_rust::ffi::ML_EINVAL); let p = medicallib_rust::ffi::ml_patient_new(std::ptr::null());
let p = medicallib_rust::ffi::ml_patient_new(std::ptr::null()); assert!(p.is_null());
assert!(p.is_null()); // Summary with null patient returns null
// Summary with null patient returns null let s = medicallib_rust::ffi::ml_patient_summary(std::ptr::null());
let s = medicallib_rust::ffi::ml_patient_summary(std::ptr::null()); assert!(s.is_null());
assert!(s.is_null());
}
} }
+3 -2
View File
@@ -1,8 +1,9 @@
#[test] #[test]
fn default_patient_heart() { fn default_patient_heart() {
let mut p = medicallib_rust::Patient::new("int-01").unwrap().initialize_default(); let mut p = medicallib_rust::Patient::new("int-01")
.unwrap()
.initialize_default();
p.update(0.1); p.update(0.1);
let s = p.patient_summary(); let s = p.patient_summary();
assert!(s.contains("Heart")); assert!(s.contains("Heart"));
} }