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 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);
-1
View File
@@ -5,4 +5,3 @@ fn main() {
p.update(0.5);
println!("{}", p.patient_summary());
}
+5 -3
View File
@@ -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());
}
-1
View File
@@ -5,4 +5,3 @@ fn main() {
let class = medicallib_rust::classify_bmi(bmi);
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.
#[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(),
}
}
+1 -1
View File
@@ -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;
+25 -6
View File
@@ -12,18 +12,37 @@ pub struct Bladder {
impl Bladder {
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 {
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
}
}
+20 -7
View File
@@ -12,23 +12,36 @@ pub struct Brain {
impl Brain {
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 {
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
}
}
+19 -6
View File
@@ -10,17 +10,30 @@ pub struct Esophagus {
impl Esophagus {
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 {
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
}
}
+19 -7
View File
@@ -10,16 +10,28 @@ pub struct Gallbladder {
impl Gallbladder {
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 {
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
}
}
+19 -6
View File
@@ -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<String>, 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
}
}
+24 -6
View File
@@ -11,13 +11,21 @@ pub struct Intestines {
impl Intestines {
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 {
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
}
}
+20 -6
View File
@@ -12,18 +12,32 @@ pub struct Kidneys {
impl Kidneys {
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 {
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
}
}
+27 -6
View File
@@ -14,17 +14,38 @@ pub struct Liver {
impl Liver {
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 {
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
}
}
+29 -8
View File
@@ -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<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 {
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
}
}
+39 -27
View File
@@ -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<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.
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;
+19 -7
View File
@@ -10,16 +10,28 @@ pub struct Pancreas {
impl Pancreas {
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 {
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
}
}
+28 -8
View File
@@ -11,17 +11,37 @@ pub struct SpinalCord {
impl SpinalCord {
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 {
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 }
}
+19 -7
View File
@@ -10,16 +10,28 @@ pub struct Spleen {
impl Spleen {
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 {
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
}
}
+19 -7
View File
@@ -10,16 +10,28 @@ pub struct Stomach {
impl Stomach {
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 {
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
}
}
+76 -20
View File
@@ -9,7 +9,9 @@ use crate::types::{Blood, BloodPressure, OrganType};
pub struct Patient {
id: String,
organs: Vec<Box<dyn Organ>>,
/// 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<String>) -> crate::Result<Self> {
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::<Lungs>().map(|l| l.spo2_pct) {
if let Some(heart) = self.find_organ_typed_mut::<Heart>() {
@@ -111,7 +161,10 @@ impl Patient {
let produced_opt = self
.find_organ_typed::<crate::organs::Kidneys>()
.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;
}
}
@@ -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)]
+13 -1
View File
@@ -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
}
}
-1
View File
@@ -5,4 +5,3 @@ fn bmi_works() {
let cls = medicallib_rust::classify_bmi(bmi);
assert_eq!(cls, medicallib_rust::BMIClassification::Normal);
}
-4
View File
@@ -1,7 +1,6 @@
#[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);
@@ -16,13 +15,11 @@ fn ffi_bmi_and_patient() {
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);
@@ -31,5 +28,4 @@ fn ffi_errors() {
// Summary with null patient returns null
let s = medicallib_rust::ffi::ml_patient_summary(std::ptr::null());
assert!(s.is_null());
}
}
+3 -2
View File
@@ -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"));
}