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
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:
+1
-1
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ fn main() {
|
|||||||
p.update(0.5);
|
p.update(0.5);
|
||||||
println!("{}", p.patient_summary());
|
println!("{}", p.patient_summary());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user