diff --git a/benches/heart.rs b/benches/heart.rs index c2c7f66..da8ffb4 100644 --- a/benches/heart.rs +++ b/benches/heart.rs @@ -1,4 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; +use medicallib_rust::Organ; fn bench_heart_update(c: &mut Criterion) { 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_main!(benches); - diff --git a/examples/patient.rs b/examples/patient.rs index a579ba3..88fef2c 100644 --- a/examples/patient.rs +++ b/examples/patient.rs @@ -5,4 +5,3 @@ fn main() { p.update(0.5); println!("{}", p.patient_summary()); } - diff --git a/examples/tracing_demo.rs b/examples/tracing_demo.rs index a92ed73..bf313ff 100644 --- a/examples/tracing_demo.rs +++ b/examples/tracing_demo.rs @@ -4,15 +4,17 @@ fn main() { { use tracing::{info, Level}; 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); 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() .with_lungs(); p.update(0.2); println!("{}", p.patient_summary()); } - diff --git a/examples/usage.rs b/examples/usage.rs index 78a8f93..a256308 100644 --- a/examples/usage.rs +++ b/examples/usage.rs @@ -5,4 +5,3 @@ fn main() { let class = medicallib_rust::classify_bmi(bmi); println!("BMI: {:.2} kg/m^2 — {:?}", bmi, class); } - diff --git a/src/ffi.rs b/src/ffi.rs index 99baf17..84b0e22 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -23,6 +23,7 @@ pub const ML_EINVAL: i32 = 2; /// Opaque patient handle type for C consumers. #[repr(C)] +#[derive(Debug)] pub struct MLPatient { inner: Patient, } @@ -31,7 +32,9 @@ pub struct MLPatient { /// On success writes result to `out_bmi`. #[no_mangle] 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) { Ok(v) => unsafe { *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. #[no_mangle] 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) }; match cstr.to_str() { 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(), @@ -58,15 +65,21 @@ pub extern "C" fn ml_patient_new(id: *const c_char) -> *mut MLPatient { /// Destroy a patient handle. Accepts null. #[no_mangle] pub extern "C" fn ml_patient_free(p: *mut MLPatient) { - if p.is_null() { return; } - unsafe { drop(Box::from_raw(p)); } + if p.is_null() { + return; + } + unsafe { + drop(Box::from_raw(p)); + } } /// Get a newly-allocated C string summary for the patient. /// Returns null on error. Free the returned string with `ml_string_free`. #[no_mangle] 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() }; match CString::new(summary) { 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. #[no_mangle] pub extern "C" fn ml_string_free(s: *mut c_char) { - if s.is_null() { return; } - unsafe { drop(CString::from_raw(s)); } + if s.is_null() { + return; + } + unsafe { + drop(CString::from_raw(s)); + } } /// Advance patient simulation by dt seconds. #[no_mangle] pub extern "C" fn ml_patient_update(p: *mut MLPatient, dt_seconds: f32) -> i32 { - if p.is_null() { return ML_EINVAL; } - unsafe { (*p).inner.update(dt_seconds); } + if p.is_null() { + return ML_EINVAL; + } + unsafe { + (*p).inner.update(dt_seconds); + } ML_OK } /// Return organ summary by type code. See header for codes. Caller frees string. #[no_mangle] 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 { 0 => OrganType::Heart, 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) }; 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(), } } diff --git a/src/lib.rs b/src/lib.rs index 346e21f..eb87cc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,9 +27,9 @@ #![warn(missing_docs, rust_2018_idioms, missing_debug_implementations)] mod error; -mod types; mod organs; mod patient; +mod types; #[cfg(feature = "ffi")] pub mod ffi; diff --git a/src/organs/bladder.rs b/src/organs/bladder.rs index 4286330..b62a65b 100644 --- a/src/organs/bladder.rs +++ b/src/organs/bladder.rs @@ -12,18 +12,37 @@ pub struct Bladder { impl Bladder { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, _dt_seconds: f32) { // simplistic pressure-volume relation 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 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!( + "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 + } } diff --git a/src/organs/brain.rs b/src/organs/brain.rs index 28e9326..78537d5 100644 --- a/src/organs/brain.rs +++ b/src/organs/brain.rs @@ -12,23 +12,36 @@ pub struct Brain { impl Brain { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, _dt_seconds: f32) { self.activity_index = self.activity_index.clamp(0.0, 2.0); } fn summary(&self) -> String { format!( "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_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 + } } - diff --git a/src/organs/esophagus.rs b/src/organs/esophagus.rs index a5fb5d7..9c557f6 100644 --- a/src/organs/esophagus.rs +++ b/src/organs/esophagus.rs @@ -10,17 +10,30 @@ pub struct Esophagus { impl Esophagus { pub fn new(id: impl Into) -> Self { - Self { info: OrganInfo::new(id, OrganType::Esophagus), reflux: 0 } + Self { + info: OrganInfo::new(id, OrganType::Esophagus), + reflux: 0, + } } } impl Organ for Esophagus { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, _dt_seconds: f32) { self.reflux = self.reflux.min(100); } - fn summary(&self) -> String { format!("Esophagus[id={}, reflux={}]", self.id(), self.reflux) } - 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!("Esophagus[id={}, reflux={}]", self.id(), self.reflux) + } + fn as_any(&self) -> &dyn core::any::Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn core::any::Any { + self + } } diff --git a/src/organs/gallbladder.rs b/src/organs/gallbladder.rs index 78b9c59..7e1bc7e 100644 --- a/src/organs/gallbladder.rs +++ b/src/organs/gallbladder.rs @@ -10,16 +10,28 @@ pub struct Gallbladder { impl Gallbladder { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, _dt_seconds: f32) {} - fn summary(&self) -> String { format!("Gallbladder[id={}, bile={:.0} ml]", self.id(), self.bile_ml) } - 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!("Gallbladder[id={}, bile={:.0} ml]", self.id(), self.bile_ml) + } + fn as_any(&self) -> &dyn core::any::Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn core::any::Any { + self + } } - diff --git a/src/organs/heart.rs b/src/organs/heart.rs index 1dbcb69..6e94c24 100644 --- a/src/organs/heart.rs +++ b/src/organs/heart.rs @@ -1,17 +1,22 @@ use super::{Organ, OrganInfo}; use crate::types::{BloodPressure, OrganType}; +/// Cardiac model with simple rate and arterial pressure coupling. #[derive(Debug, Clone)] pub struct Heart { info: OrganInfo, + /// Heart rate in beats per minute. pub heart_rate_bpm: f32, + /// Arterial blood pressure snapshot. pub arterial_bp: BloodPressure, + /// ECG lead count configured for this heart. pub leads: u8, /// Simplified arrhythmia flag; increases HR variability. pub arrhythmia: bool, } impl Heart { + /// Construct a new heart with the given id and number of ECG leads. pub fn new(id: impl Into, leads: u8) -> Self { Self { info: OrganInfo::new(id, OrganType::Heart), @@ -24,15 +29,19 @@ impl Heart { } impl Organ for Heart { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } 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 mut diff = target - self.heart_rate_bpm; if self.arrhythmia { // 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); // crude BP coupling to HR @@ -51,6 +60,10 @@ impl Organ for Heart { self.arterial_bp.diastolic ) } - fn as_any(&self) -> &dyn core::any::Any { self } - 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 + } } diff --git a/src/organs/intestines.rs b/src/organs/intestines.rs index 31fdf3e..76676f3 100644 --- a/src/organs/intestines.rs +++ b/src/organs/intestines.rs @@ -11,13 +11,21 @@ pub struct Intestines { impl Intestines { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, dt_seconds: f32) { if self.peristalsis { // minor oscillation around 80 @@ -26,7 +34,17 @@ impl Organ for Intestines { self.absorption = val as u8; } } - fn summary(&self) -> String { format!("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 } + fn summary(&self) -> String { + format!( + "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 + } } diff --git a/src/organs/kidneys.rs b/src/organs/kidneys.rs index 713bc41..b8045ee 100644 --- a/src/organs/kidneys.rs +++ b/src/organs/kidneys.rs @@ -12,18 +12,32 @@ pub struct Kidneys { impl Kidneys { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, _dt_seconds: f32) { self.gfr = self.gfr.clamp(0.0, 200.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 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!("Kidneys[id={}, GFR={:.0} ml/min]", self.id(), self.gfr) + } + fn as_any(&self) -> &dyn core::any::Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn core::any::Any { + self + } } diff --git a/src/organs/liver.rs b/src/organs/liver.rs index 4536981..55df4f1 100644 --- a/src/organs/liver.rs +++ b/src/organs/liver.rs @@ -14,17 +14,38 @@ pub struct Liver { impl Liver { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, _dt_seconds: f32) { 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 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!( + "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 + } } diff --git a/src/organs/lungs.rs b/src/organs/lungs.rs index 081de08..eb3ffb5 100644 --- a/src/organs/lungs.rs +++ b/src/organs/lungs.rs @@ -1,39 +1,60 @@ use super::{Organ, OrganInfo}; use crate::types::OrganType; +/// Pulmonary model tracking respiratory rate and oxygen saturation. #[derive(Debug, Clone)] pub struct Lungs { info: OrganInfo, + /// Respiratory rate in breaths per minute. pub respiratory_rate_bpm: f32, + /// Peripheral oxygen saturation percent. pub spo2_pct: f32, /// Respiratory distress flag reduces SpO2. pub distress: bool, } impl Lungs { + /// Construct lungs with a given id. pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } 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; self.respiratory_rate_bpm += 0.1 * (target_rr - self.respiratory_rate_bpm) * (dt / 1.0); // 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] self.spo2_pct = self.spo2_pct.clamp(70.0, 100.0); } fn summary(&self) -> String { format!( "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_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 + } } diff --git a/src/organs/mod.rs b/src/organs/mod.rs index 0808e29..d7fc159 100644 --- a/src/organs/mod.rs +++ b/src/organs/mod.rs @@ -1,6 +1,6 @@ //! Organ system trait and implementations. -use crate::types::{BloodPressure, OrganType}; +use crate::types::OrganType; use core::fmt::Debug; /// Common metadata for organs. @@ -12,47 +12,59 @@ pub struct OrganInfo { impl OrganInfo { pub fn new(id: impl Into, 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. pub trait Organ: Debug + Send { + /// Stable string identifier for this organ instance. fn id(&self) -> &str; + /// Kind of organ (e.g., heart, lungs). fn organ_type(&self) -> OrganType; + /// Advance the organ simulation by `dt_seconds`. fn update(&mut self, dt_seconds: f32); + /// One-line human-readable status summary. fn summary(&self) -> String; + /// Type-erased reference for downcasting. 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; } -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 brain; +mod esophagus; +mod gallbladder; +mod heart; +mod intestines; +mod kidneys; +mod liver; +mod lungs; +mod pancreas; +mod spinal_cord; 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 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 stomach::Stomach; diff --git a/src/organs/pancreas.rs b/src/organs/pancreas.rs index 5650b44..ceef908 100644 --- a/src/organs/pancreas.rs +++ b/src/organs/pancreas.rs @@ -10,16 +10,28 @@ pub struct Pancreas { impl Pancreas { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, _dt_seconds: f32) {} - fn summary(&self) -> String { format!("Pancreas[id={}, insulin={:.2}]", self.id(), self.insulin) } - 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!("Pancreas[id={}, insulin={:.2}]", self.id(), self.insulin) + } + fn as_any(&self) -> &dyn core::any::Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn core::any::Any { + self + } } - diff --git a/src/organs/spinal_cord.rs b/src/organs/spinal_cord.rs index 13d7aa7..e7c3f75 100644 --- a/src/organs/spinal_cord.rs +++ b/src/organs/spinal_cord.rs @@ -11,17 +11,37 @@ pub struct SpinalCord { impl SpinalCord { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - 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 id(&self) -> &str { + self.info.id() + } + 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 } } diff --git a/src/organs/spleen.rs b/src/organs/spleen.rs index 67923bc..536a5e4 100644 --- a/src/organs/spleen.rs +++ b/src/organs/spleen.rs @@ -10,16 +10,28 @@ pub struct Spleen { impl Spleen { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, _dt_seconds: f32) {} - fn summary(&self) -> String { format!("Spleen[id={}, immune={}]", self.id(), self.immune_activity) } - 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!("Spleen[id={}, immune={}]", self.id(), self.immune_activity) + } + fn as_any(&self) -> &dyn core::any::Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn core::any::Any { + self + } } - diff --git a/src/organs/stomach.rs b/src/organs/stomach.rs index 1ef6ec9..2434213 100644 --- a/src/organs/stomach.rs +++ b/src/organs/stomach.rs @@ -10,16 +10,28 @@ pub struct Stomach { impl Stomach { pub fn new(id: impl Into) -> 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 { - fn id(&self) -> &str { self.info.id() } - fn organ_type(&self) -> OrganType { self.info.kind() } + fn id(&self) -> &str { + self.info.id() + } + fn organ_type(&self) -> OrganType { + self.info.kind() + } fn update(&mut self, _dt_seconds: f32) {} - fn summary(&self) -> String { format!("Stomach[id={}, acid={}]", self.id(), self.acid_level) } - 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!("Stomach[id={}, acid={}]", self.id(), self.acid_level) + } + fn as_any(&self) -> &dyn core::any::Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn core::any::Any { + self + } } - diff --git a/src/patient.rs b/src/patient.rs index 8d4c58e..3d12e99 100644 --- a/src/patient.rs +++ b/src/patient.rs @@ -9,7 +9,9 @@ use crate::types::{Blood, BloodPressure, OrganType}; pub struct Patient { id: String, organs: Vec>, + /// Laboratory blood panel snapshot. pub blood: Blood, + /// Non-invasive brachial blood pressure. pub blood_pressure: BloodPressure, } @@ -18,7 +20,9 @@ impl Patient { pub fn new(id: impl Into) -> crate::Result { let id = id.into(); 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 { id, @@ -29,7 +33,9 @@ impl Patient { } /// Patient identifier. - pub fn id(&self) -> &str { &self.id } + pub fn id(&self) -> &str { + &self.id + } /// Add an organ to the patient. pub fn add_organ(&mut self, organ: impl Organ + 'static) { @@ -38,7 +44,10 @@ impl Patient { /// Find the first organ matching the given type. 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. @@ -79,26 +88,67 @@ impl Patient { /// Attach a default organ by type. pub fn with_organ(mut self, kind: OrganType) -> Self { match kind { - OrganType::Heart => { let id = format!("{}-heart", self.id); self.add_organ(Heart::new(id, 12)); } - OrganType::Lungs => { let id = format!("{}-lungs", self.id); self.add_organ(Lungs::new(id)); } - OrganType::Brain => { let id = format!("{}-brain", self.id); self.add_organ(crate::organs::Brain::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)); } + OrganType::Heart => { + let id = format!("{}-heart", self.id); + self.add_organ(Heart::new(id, 12)); + } + OrganType::Lungs => { + let id = format!("{}-lungs", self.id); + self.add_organ(Lungs::new(id)); + } + OrganType::Brain => { + let id = format!("{}-brain", self.id); + self.add_organ(crate::organs::Brain::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 } /// Advance simulation by `dt_seconds`. 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. if let Some(spo2) = self.find_organ_typed::().map(|l| l.spo2_pct) { if let Some(heart) = self.find_organ_typed_mut::() { @@ -111,7 +161,10 @@ impl Patient { let produced_opt = self .find_organ_typed::() .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::()) { + if let (Some(produced), Some(bladder)) = ( + produced_opt, + self.find_organ_typed_mut::(), + ) { bladder.volume_ml += produced; } } @@ -147,8 +200,11 @@ impl Default for Patient { fn is_valid_id(id: &str) -> bool { let len = id.len(); - if !(1..=64).contains(&len) { return false; } - id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + if !(1..=64).contains(&len) { + return false; + } + id.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') } #[cfg(test)] diff --git a/src/types.rs b/src/types.rs index 8467430..09bd96f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,18 +6,31 @@ use core::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// Known organ categories. pub enum OrganType { + /// Heart Heart, + /// Lungs Lungs, + /// Brain Brain, + /// Spinal cord SpinalCord, + /// Stomach Stomach, + /// Liver Liver, + /// Gallbladder Gallbladder, + /// Pancreas Pancreas, + /// Intestines Intestines, + /// Esophagus Esophagus, + /// Kidneys Kidneys, + /// Urinary bladder Bladder, + /// Spleen Spleen, } @@ -89,4 +102,3 @@ impl Blood { hgb_ok && hct_ok && spo2_ok && glucose_ok } } - diff --git a/tests/basic.rs b/tests/basic.rs index cd2ef5b..8473754 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -5,4 +5,3 @@ fn bmi_works() { let cls = medicallib_rust::classify_bmi(bmi); assert_eq!(cls, medicallib_rust::BMIClassification::Normal); } - diff --git a/tests/ffi.rs b/tests/ffi.rs index be45eb8..70fec2a 100644 --- a/tests/ffi.rs +++ b/tests/ffi.rs @@ -1,35 +1,31 @@ #[cfg(feature = "ffi")] #[test] fn ffi_bmi_and_patient() { - unsafe { - let mut out: f32 = 0.0; - 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!(out > 10.0); + let mut out: f32 = 0.0; + 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!(out > 10.0); - use std::ffi::CString; - let id = CString::new("ffi-test").unwrap(); - let p = medicallib_rust::ffi::ml_patient_new(id.as_ptr()); - assert!(!p.is_null()); - let _ = medicallib_rust::ffi::ml_patient_update(p, 0.1); - let s = medicallib_rust::ffi::ml_patient_summary(p); - assert!(!s.is_null()); - medicallib_rust::ffi::ml_string_free(s); - medicallib_rust::ffi::ml_patient_free(p); - } + use std::ffi::CString; + let id = CString::new("ffi-test").unwrap(); + let p = medicallib_rust::ffi::ml_patient_new(id.as_ptr()); + assert!(!p.is_null()); + let _ = medicallib_rust::ffi::ml_patient_update(p, 0.1); + let s = medicallib_rust::ffi::ml_patient_summary(p); + assert!(!s.is_null()); + medicallib_rust::ffi::ml_string_free(s); + medicallib_rust::ffi::ml_patient_free(p); } #[cfg(feature = "ffi")] #[test] fn ffi_errors() { - unsafe { - // Null out pointer arguments should error gracefully - let rc = medicallib_rust::ffi::medicallib_bmi(70.0, 1.75, std::ptr::null_mut()); - assert_eq!(rc, medicallib_rust::ffi::ML_EINVAL); - let p = medicallib_rust::ffi::ml_patient_new(std::ptr::null()); - assert!(p.is_null()); - // Summary with null patient returns null - let s = medicallib_rust::ffi::ml_patient_summary(std::ptr::null()); - assert!(s.is_null()); - } + // Null out pointer arguments should error gracefully + let rc = medicallib_rust::ffi::medicallib_bmi(70.0, 1.75, std::ptr::null_mut()); + assert_eq!(rc, medicallib_rust::ffi::ML_EINVAL); + let p = medicallib_rust::ffi::ml_patient_new(std::ptr::null()); + assert!(p.is_null()); + // Summary with null patient returns null + let s = medicallib_rust::ffi::ml_patient_summary(std::ptr::null()); + assert!(s.is_null()); } diff --git a/tests/patient.rs b/tests/patient.rs index aed3bed..63dbfe7 100644 --- a/tests/patient.rs +++ b/tests/patient.rs @@ -1,8 +1,9 @@ #[test] 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); let s = p.patient_summary(); assert!(s.contains("Heart")); } -