init
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
|||||||
|
target/
|
||||||
|
dist/
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
|
.DS_Store
|
||||||
|
*.pdb
|
||||||
|
*.dSYM/
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.log
|
||||||
|
node_modules/
|
||||||
|
coverage/
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
- Crate facade in `src/lib.rs` re-exports curated types.
|
||||||
|
- Error handling with `thiserror` via `MedicalError` and `type Result<T>`.
|
||||||
|
- Core domain types in `src/types.rs` (e.g., `BloodPressure`, `Blood`).
|
||||||
|
- Organ system:
|
||||||
|
- Trait and common `OrganInfo` in `src/organs/mod.rs`
|
||||||
|
- Per-organ modules (Heart, Lungs, Brain, etc.) implement `Organ`
|
||||||
|
- `Patient` in `src/patient.rs` owns a vector of `Box<dyn Organ>` and coordinates updates.
|
||||||
|
- FFI (feature `ffi`) in `src/ffi.rs`, header `ffi/medicallib.h`.
|
||||||
|
- Examples in `examples/`, benches in `benches/` (criterion).
|
||||||
|
|
||||||
|
Inter-organ communication examples:
|
||||||
|
- Lungs SpO2 influences Heart rate.
|
||||||
|
- Kidneys GFR produces urine into Bladder volume.
|
||||||
|
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
[package]
|
||||||
|
name = "medicallib_rust"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "MedicalSim core library rewrite in Rust: basic clinical calculations and types."
|
||||||
|
authors = ["MedicalSim Team"]
|
||||||
|
license = "UNLICENSED"
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["rlib", "cdylib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
serde = ["dep:serde"]
|
||||||
|
ffi = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
thiserror = "1"
|
||||||
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
tracing = { version = "0.1", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = { version = "0.5", default-features = false, features = ["cargo_bench_support"] }
|
||||||
|
tracing-subscriber = { version = "0.3" }
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "heart"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# Installation and Usage (C/C++)
|
||||||
|
|
||||||
|
1. Build the cdylib:
|
||||||
|
- `cargo build --features ffi --release`
|
||||||
|
- Outputs under `target/release/` (`.so`/`.dll`/`.dylib`)
|
||||||
|
2. Include header `ffi/medicallib.h` in your C/C++ project.
|
||||||
|
3. Link your app against the produced library.
|
||||||
|
4. See `examples/c/ffi_example.c` for usage.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Free all strings returned by the library with `ml_string_free`.
|
||||||
|
- Use the organ type codes (ML_ORGAN_*) to query specific organ summaries.
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Migration Guide: C++ MedicalSim -> medicallib_rust
|
||||||
|
|
||||||
|
This guide summarizes the mapping and differences between the legacy C++ library and the new Rust core.
|
||||||
|
|
||||||
|
- Organ base class -> Rust `Organ` trait (`src/organs/mod.rs`)
|
||||||
|
- Organ implementations -> dedicated structs per organ (e.g., Heart, Lungs)
|
||||||
|
- `initializePatient()` -> `Patient::initialize_default()` or `Patient::with_heart(leads)`
|
||||||
|
- `updatePatient(dt)` -> `Patient::update(dt_seconds)`
|
||||||
|
- Template organ getters -> `Patient::find_organ_typed::<T>()` and `find_organ_typed_mut::<T>()`
|
||||||
|
- C API -> feature `ffi`, header `ffi/medicallib.h`, with `MLPatient*` opaque handle
|
||||||
|
|
||||||
|
Notable differences:
|
||||||
|
- Memory safety and ownership are enforced by Rust; no raw pointer ownership in the library
|
||||||
|
- Errors use `thiserror` with `Result<T, MedicalError>`
|
||||||
|
- Logging is not emitted by the library by default; use `tracing` in binaries/examples
|
||||||
|
- Additional organ systems are modeled with simplified states as a baseline
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
PACKAGE_TARGET?=
|
||||||
|
|
||||||
|
.PHONY: package
|
||||||
|
package:
|
||||||
|
@bash ../scripts/package.sh $(PACKAGE_TARGET)
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# medicallib_rust
|
||||||
|
|
||||||
|
MedicalSim core library rewrite in Rust.
|
||||||
|
|
||||||
|
- Safe, minimal API for medical simulation primitives
|
||||||
|
- Optional `serde` feature for serialization
|
||||||
|
- Optional `ffi` feature to expose a stable C ABI (`cdylib`)
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
- Build: `cargo build` (tests: `cargo test`)
|
||||||
|
- FFI build: `cargo build --features ffi --release`
|
||||||
|
- Format/Lint: `cargo fmt --all` and `cargo clippy -D warnings`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
- Rust: `cargo run --example usage` and `cargo run --example patient`
|
||||||
|
- C FFI: see `examples/c/ffi_example.c`, header in `ffi/medicallib.h`
|
||||||
|
|
||||||
|
## FFI ABI
|
||||||
|
|
||||||
|
- Opaque `MLPatient` handle
|
||||||
|
- Functions: `medicallib_bmi`, `ml_patient_new/free/update`, `ml_patient_summary`, `ml_patient_organ_summary`
|
||||||
|
- All allocated strings must be freed with `ml_string_free`
|
||||||
|
|
||||||
|
## Version Matrix
|
||||||
|
|
||||||
|
- Rust 1.70+ (Edition 2021)
|
||||||
|
- OS: Linux, macOS, Windows (see GitHub Actions CI)
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- Architecture: `ARCHITECTURE.md`
|
||||||
|
- Migration (C++ -> Rust): `MIGRATION.md`
|
||||||
|
- C/C++ install: `INSTALL.md`
|
||||||
|
|
||||||
|
## Packaging (binary distribution)
|
||||||
|
|
||||||
|
- Linux/macOS: run `../scripts/package.sh [target-triple]` from `medicallib_rust/` (or `make package`).
|
||||||
|
- Windows: run `..\scripts\package.ps1 [target-triple]` from `medicallib_rust\`.
|
||||||
|
- Artifacts are placed in `medicallib_rust/dist/` as both `.tar.gz` (Unix) and `.zip`.
|
||||||
|
|
||||||
|
## CI: GitHub + Gitea
|
||||||
|
|
||||||
|
- GitHub Actions workflow: `.github/workflows/ci.yml`
|
||||||
|
- Gitea Actions workflow: `.gitea/workflows/ci.yml`
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use criterion::{criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
|
fn bench_heart_update(c: &mut Criterion) {
|
||||||
|
c.bench_function("heart_update_1s", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let mut h = medicallib_rust::Heart::new("h", 12);
|
||||||
|
h.update(1.0);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_heart_update);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "../../ffi/medicallib.h"
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
float bmi = 0.0f;
|
||||||
|
if (medicallib_bmi(70.0f, 1.75f, &bmi) != ML_OK) {
|
||||||
|
fprintf(stderr, "BMI error\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("BMI: %.2f\n", bmi);
|
||||||
|
|
||||||
|
MLPatient* p = ml_patient_new("ffi-01");
|
||||||
|
if (!p) { fprintf(stderr, "patient new failed\n"); return 1; }
|
||||||
|
ml_patient_update(p, 0.5f);
|
||||||
|
|
||||||
|
char* sum = ml_patient_summary(p);
|
||||||
|
if (sum) { printf("%s\n", sum); ml_string_free(sum); }
|
||||||
|
|
||||||
|
char* h = ml_patient_organ_summary(p, ML_ORGAN_HEART);
|
||||||
|
if (h) { printf("%s\n", h); ml_string_free(h); }
|
||||||
|
|
||||||
|
ml_patient_free(p);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fn main() {
|
||||||
|
let mut p = medicallib_rust::Patient::new("demo-01")
|
||||||
|
.expect("valid id")
|
||||||
|
.initialize_default();
|
||||||
|
p.update(0.5);
|
||||||
|
println!("{}", p.patient_summary());
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
fn main() {
|
||||||
|
// Consumer decides logging; library remains silent by default.
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
{
|
||||||
|
use tracing::{info, Level};
|
||||||
|
use tracing_subscriber::FmtSubscriber;
|
||||||
|
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()
|
||||||
|
.initialize_default()
|
||||||
|
.with_lungs();
|
||||||
|
p.update(0.2);
|
||||||
|
println!("{}", p.patient_summary());
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fn main() {
|
||||||
|
let weight = 70.0_f32;
|
||||||
|
let height = 1.75_f32;
|
||||||
|
let bmi = medicallib_rust::calculate_bmi(weight, height).expect("valid inputs");
|
||||||
|
let class = medicallib_rust::classify_bmi(bmi);
|
||||||
|
println!("BMI: {:.2} kg/m^2 — {:?}", bmi, class);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Minimal C header for medicallib_rust FFI
|
||||||
|
// Build with: cargo build --features ffi --release
|
||||||
|
// Link your C app against the produced cdylib and ensure the header codes
|
||||||
|
// match the Rust definitions.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Return codes
|
||||||
|
#define ML_OK 0
|
||||||
|
#define ML_ERR 1
|
||||||
|
#define ML_EINVAL 2
|
||||||
|
|
||||||
|
typedef struct MLPatient MLPatient; // opaque
|
||||||
|
|
||||||
|
// Organ codes (keep in sync with Rust)
|
||||||
|
#define ML_ORGAN_HEART 0
|
||||||
|
#define ML_ORGAN_LUNGS 1
|
||||||
|
#define ML_ORGAN_BRAIN 2
|
||||||
|
#define ML_ORGAN_SPINAL_CORD 3
|
||||||
|
#define ML_ORGAN_STOMACH 4
|
||||||
|
#define ML_ORGAN_LIVER 5
|
||||||
|
#define ML_ORGAN_GALLBLADDER 6
|
||||||
|
#define ML_ORGAN_PANCREAS 7
|
||||||
|
#define ML_ORGAN_INTESTINES 8
|
||||||
|
#define ML_ORGAN_ESOPHAGUS 9
|
||||||
|
#define ML_ORGAN_KIDNEYS 10
|
||||||
|
#define ML_ORGAN_BLADDER 11
|
||||||
|
#define ML_ORGAN_SPLEEN 12
|
||||||
|
|
||||||
|
// BMI computation. Returns 0 on success, writes to out_bmi.
|
||||||
|
int32_t medicallib_bmi(float weight_kg, float height_m, float* out_bmi);
|
||||||
|
|
||||||
|
// Patient lifecycle
|
||||||
|
MLPatient* ml_patient_new(const char* id);
|
||||||
|
void ml_patient_free(MLPatient* p);
|
||||||
|
|
||||||
|
// Returns a newly-allocated C string. Free with ml_string_free.
|
||||||
|
char* ml_patient_summary(const MLPatient* p);
|
||||||
|
void ml_string_free(char* s);
|
||||||
|
|
||||||
|
// Advance simulation by dt seconds.
|
||||||
|
int32_t ml_patient_update(MLPatient* p, float dt_seconds);
|
||||||
|
|
||||||
|
// Get summary for a specific organ type code.
|
||||||
|
char* ml_patient_organ_summary(const MLPatient* p, uint32_t organ_code);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//! Error types used by the `medicallib_rust` library.
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Library-wide error type.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum MedicalError {
|
||||||
|
/// Input validation failed (e.g., negative or zero values).
|
||||||
|
#[error("invalid input: {0}")]
|
||||||
|
InvalidInput(&'static str),
|
||||||
|
/// Domain validation failed with a message.
|
||||||
|
#[error("validation error: {0}")]
|
||||||
|
Validation(&'static str),
|
||||||
|
/// Requested item not found (e.g., organ by type or id).
|
||||||
|
#[error("not found: {0}")]
|
||||||
|
NotFound(&'static str),
|
||||||
|
/// Parsing or conversion error for textual inputs.
|
||||||
|
#[error("parse error: {0}")]
|
||||||
|
Parse(&'static str),
|
||||||
|
/// FFI boundary error with a message safe to expose cross-language.
|
||||||
|
#[error("ffi error: {0}")]
|
||||||
|
Ffi(&'static str),
|
||||||
|
}
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
//! C-compatible FFI interface. Enable with `--features ffi`.
|
||||||
|
//!
|
||||||
|
//! Safety: All functions validate pointers and return error codes where
|
||||||
|
//! applicable. Callers must free returned strings via `ml_string_free` and
|
||||||
|
//! destroy opaque handles via their corresponding `free` functions.
|
||||||
|
|
||||||
|
#![allow(unsafe_code)]
|
||||||
|
#![allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||||
|
|
||||||
|
use core::ffi::{c_char, CStr};
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
use crate::patient::Patient;
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
/// Successful return code.
|
||||||
|
pub const ML_OK: i32 = 0;
|
||||||
|
/// Generic error.
|
||||||
|
pub const ML_ERR: i32 = 1;
|
||||||
|
/// Invalid argument.
|
||||||
|
pub const ML_EINVAL: i32 = 2;
|
||||||
|
|
||||||
|
/// Opaque patient handle type for C consumers.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct MLPatient {
|
||||||
|
inner: Patient,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute BMI; returns 0 on success, non-zero on error.
|
||||||
|
/// 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; }
|
||||||
|
match crate::calculate_bmi(weight_kg, height_m) {
|
||||||
|
Ok(v) => unsafe {
|
||||||
|
*out_bmi = v;
|
||||||
|
ML_OK
|
||||||
|
},
|
||||||
|
Err(_) => ML_ERR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(); }
|
||||||
|
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() })),
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
},
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(); }
|
||||||
|
let summary = unsafe { (*p).inner.patient_summary() };
|
||||||
|
match CString::new(summary) {
|
||||||
|
Ok(s) => s.into_raw(),
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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); }
|
||||||
|
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(); }
|
||||||
|
let kind = match organ_code {
|
||||||
|
0 => OrganType::Heart,
|
||||||
|
1 => OrganType::Lungs,
|
||||||
|
2 => OrganType::Brain,
|
||||||
|
3 => OrganType::SpinalCord,
|
||||||
|
4 => OrganType::Stomach,
|
||||||
|
5 => OrganType::Liver,
|
||||||
|
6 => OrganType::Gallbladder,
|
||||||
|
7 => OrganType::Pancreas,
|
||||||
|
8 => OrganType::Intestines,
|
||||||
|
9 => OrganType::Esophagus,
|
||||||
|
10 => OrganType::Kidneys,
|
||||||
|
11 => OrganType::Bladder,
|
||||||
|
12 => OrganType::Spleen,
|
||||||
|
_ => return ptr::null_mut(),
|
||||||
|
};
|
||||||
|
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() },
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
//! MedicalSim core library rewrite in Rust.
|
||||||
|
//!
|
||||||
|
//! This crate provides foundational types and utilities for medical
|
||||||
|
//! simulations and calculations. The initial surface focuses on
|
||||||
|
//! basic, well-defined helpers (e.g., BMI) and composable domain types,
|
||||||
|
//! with room to expand to vitals processing, scoring systems, and
|
||||||
|
//! scenarios.
|
||||||
|
//!
|
||||||
|
//! Examples
|
||||||
|
//! ```
|
||||||
|
//! // Calculate BMI and classify it.
|
||||||
|
//! let bmi = medicallib_rust::calculate_bmi(70.0, 1.75).unwrap();
|
||||||
|
//! assert!((bmi - 22.857145).abs() < 1e-5);
|
||||||
|
//! let class = medicallib_rust::classify_bmi(bmi);
|
||||||
|
//! assert_eq!(class, medicallib_rust::BMIClassification::Normal);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! // Initialize a patient with a heart and update.
|
||||||
|
//! let mut p = medicallib_rust::Patient::new("case-01").unwrap().initialize_default();
|
||||||
|
//! p.update(0.1);
|
||||||
|
//! let summary = p.patient_summary();
|
||||||
|
//! assert!(summary.contains("Patient[id=case-01"));
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![deny(unsafe_code)]
|
||||||
|
#![warn(missing_docs, rust_2018_idioms, missing_debug_implementations)]
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod types;
|
||||||
|
mod organs;
|
||||||
|
mod patient;
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi")]
|
||||||
|
pub mod ffi;
|
||||||
|
|
||||||
|
pub use crate::error::MedicalError;
|
||||||
|
pub use crate::organs::{Heart, Organ};
|
||||||
|
pub use crate::patient::Patient;
|
||||||
|
pub use crate::types::{Blood, BloodPressure, OrganType};
|
||||||
|
|
||||||
|
/// Convenient result alias for this library.
|
||||||
|
pub type Result<T> = core::result::Result<T, MedicalError>;
|
||||||
|
|
||||||
|
/// Common vital signs.
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum VitalSign {
|
||||||
|
/// Heart rate (beats per minute).
|
||||||
|
HeartRate,
|
||||||
|
/// Respiratory rate (breaths per minute).
|
||||||
|
RespiratoryRate,
|
||||||
|
/// Systolic blood pressure (mmHg).
|
||||||
|
SystolicBP,
|
||||||
|
/// Diastolic blood pressure (mmHg).
|
||||||
|
DiastolicBP,
|
||||||
|
/// Body temperature (°C).
|
||||||
|
TemperatureC,
|
||||||
|
/// Peripheral capillary oxygen saturation (%).
|
||||||
|
SpO2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple measurement wrapper carrying a unit label.
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Measurement<T> {
|
||||||
|
/// Numeric value of the measurement.
|
||||||
|
pub value: T,
|
||||||
|
/// Unit string (e.g., "bpm", "mmHg", "kg/m^2").
|
||||||
|
pub unit: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Measurement<T> {
|
||||||
|
/// Creates a new measurement value with a unit.
|
||||||
|
pub const fn new(value: T, unit: &'static str) -> Self {
|
||||||
|
Self { value, unit }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WHO BMI classification bands.
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum BMIClassification {
|
||||||
|
/// BMI < 18.5
|
||||||
|
Underweight,
|
||||||
|
/// 18.5 ≤ BMI < 25.0
|
||||||
|
Normal,
|
||||||
|
/// 25.0 ≤ BMI < 30.0
|
||||||
|
Overweight,
|
||||||
|
/// 30.0 ≤ BMI < 35.0
|
||||||
|
ObesityClass1,
|
||||||
|
/// 35.0 ≤ BMI < 40.0
|
||||||
|
ObesityClass2,
|
||||||
|
/// BMI ≥ 40.0
|
||||||
|
ObesityClass3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate Body Mass Index (BMI) given weight (kg) and height (m).
|
||||||
|
///
|
||||||
|
/// Returns an error when `weight_kg <= 0` or `height_m <= 0`.
|
||||||
|
///
|
||||||
|
/// The result is dimensionally `kg/m^2`.
|
||||||
|
pub fn calculate_bmi(weight_kg: f32, height_m: f32) -> Result<f32> {
|
||||||
|
if !(weight_kg.is_finite() && height_m.is_finite()) {
|
||||||
|
return Err(MedicalError::InvalidInput("inputs must be finite numbers"));
|
||||||
|
}
|
||||||
|
if weight_kg <= 0.0 {
|
||||||
|
return Err(MedicalError::InvalidInput("weight_kg must be > 0"));
|
||||||
|
}
|
||||||
|
if height_m <= 0.0 {
|
||||||
|
return Err(MedicalError::InvalidInput("height_m must be > 0"));
|
||||||
|
}
|
||||||
|
Ok(weight_kg / (height_m * height_m))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify BMI according to WHO bands.
|
||||||
|
pub fn classify_bmi(bmi: f32) -> BMIClassification {
|
||||||
|
match bmi {
|
||||||
|
x if x < 18.5 => BMIClassification::Underweight,
|
||||||
|
x if x < 25.0 => BMIClassification::Normal,
|
||||||
|
x if x < 30.0 => BMIClassification::Overweight,
|
||||||
|
x if x < 35.0 => BMIClassification::ObesityClass1,
|
||||||
|
x if x < 40.0 => BMIClassification::ObesityClass2,
|
||||||
|
_ => BMIClassification::ObesityClass3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to expose BMI as a typed measurement with unit `kg/m^2`.
|
||||||
|
pub fn bmi_measurement(weight_kg: f32, height_m: f32) -> Result<Measurement<f32>> {
|
||||||
|
calculate_bmi(weight_kg, height_m).map(|v| Measurement::new(v, "kg/m^2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bmi_calculation_and_classification() {
|
||||||
|
let bmi = calculate_bmi(70.0, 1.75).unwrap();
|
||||||
|
assert!((bmi - 22.857145).abs() < 1e-5);
|
||||||
|
assert_eq!(classify_bmi(bmi), BMIClassification::Normal);
|
||||||
|
let m = bmi_measurement(70.0, 1.75).unwrap();
|
||||||
|
assert_eq!(m.unit, "kg/m^2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bmi_invalid_inputs() {
|
||||||
|
assert!(calculate_bmi(0.0, 1.7).is_err());
|
||||||
|
assert!(calculate_bmi(70.0, 0.0).is_err());
|
||||||
|
assert!(calculate_bmi(f32::NAN, 1.7).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patient_and_organs() {
|
||||||
|
let mut p = Patient::new("case_01").unwrap().initialize_default();
|
||||||
|
p.update(0.2);
|
||||||
|
let s = p.patient_summary();
|
||||||
|
assert!(s.contains("Patient[id=case_01"));
|
||||||
|
let heart = p.find_organ_typed::<Heart>().unwrap();
|
||||||
|
assert_eq!(heart.organ_type(), OrganType::Heart);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Bladder {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// Urine volume ml
|
||||||
|
pub volume_ml: f32,
|
||||||
|
/// Pressure proxy (cmH2O)
|
||||||
|
pub pressure: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bladder {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Brain {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// 0..=100 scale of consciousness.
|
||||||
|
pub consciousness: u8,
|
||||||
|
/// Simplified neural activity index.
|
||||||
|
pub activity_index: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Brain {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn as_any(&self) -> &dyn core::any::Any { self }
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Esophagus {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// Reflux severity 0..=100
|
||||||
|
pub reflux: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Esophagus {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Gallbladder {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// Bile volume ml
|
||||||
|
pub bile_ml: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gallbladder {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 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 }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::{BloodPressure, OrganType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Heart {
|
||||||
|
info: OrganInfo,
|
||||||
|
pub heart_rate_bpm: f32,
|
||||||
|
pub arterial_bp: BloodPressure,
|
||||||
|
pub leads: u8,
|
||||||
|
/// Simplified arrhythmia flag; increases HR variability.
|
||||||
|
pub arrhythmia: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Heart {
|
||||||
|
pub fn new(id: impl Into<String>, leads: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
info: OrganInfo::new(id, OrganType::Heart),
|
||||||
|
heart_rate_bpm: 70.0,
|
||||||
|
arterial_bp: BloodPressure::default(),
|
||||||
|
leads,
|
||||||
|
arrhythmia: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Organ for Heart {
|
||||||
|
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 target = 70.0f32;
|
||||||
|
let mut diff = target - self.heart_rate_bpm;
|
||||||
|
if self.arrhythmia {
|
||||||
|
// add variability
|
||||||
|
diff += (self.heart_rate_bpm.sin() * 5.0);
|
||||||
|
}
|
||||||
|
self.heart_rate_bpm += 0.1 * diff * (dt / 1.0);
|
||||||
|
// crude BP coupling to HR
|
||||||
|
let sys = (90.0 + 0.5 * self.heart_rate_bpm).clamp(80.0, 220.0) as u16;
|
||||||
|
let dia = (60.0 + 0.3 * self.heart_rate_bpm).clamp(40.0, 140.0) as u16;
|
||||||
|
self.arterial_bp.systolic = sys;
|
||||||
|
self.arterial_bp.diastolic = dia.min(sys.saturating_sub(10));
|
||||||
|
}
|
||||||
|
fn summary(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"Heart[id={}, leads={}, HR={:.1} bpm, BP={}/{} mmHg]",
|
||||||
|
self.id(),
|
||||||
|
self.leads,
|
||||||
|
self.heart_rate_bpm,
|
||||||
|
self.arterial_bp.systolic,
|
||||||
|
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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Intestines {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// Nutrient absorption rate 0..=100
|
||||||
|
pub absorption: u8,
|
||||||
|
pub peristalsis: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Intestines {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 update(&mut self, dt_seconds: f32) {
|
||||||
|
if self.peristalsis {
|
||||||
|
// minor oscillation around 80
|
||||||
|
let delta = (dt_seconds * 0.1).sin();
|
||||||
|
let val = (self.absorption as f32 + delta).clamp(0.0, 100.0);
|
||||||
|
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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Kidneys {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// Filtration rate ml/min
|
||||||
|
pub gfr: f32,
|
||||||
|
/// Electrolyte balance index -1..=1
|
||||||
|
pub electrolyte_balance: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Kidneys {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Liver {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// Detox capacity 0..=100
|
||||||
|
pub detox: u8,
|
||||||
|
/// Metabolism index
|
||||||
|
pub metabolism: f32,
|
||||||
|
/// Enzyme production index
|
||||||
|
pub enzymes: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Organ for Liver {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Lungs {
|
||||||
|
info: OrganInfo,
|
||||||
|
pub respiratory_rate_bpm: f32,
|
||||||
|
pub spo2_pct: f32,
|
||||||
|
/// Respiratory distress flag reduces SpO2.
|
||||||
|
pub distress: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lungs {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Organ for Lungs {
|
||||||
|
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 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); }
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn as_any(&self) -> &dyn core::any::Any { self }
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn core::any::Any { self }
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
//! Organ system trait and implementations.
|
||||||
|
|
||||||
|
use crate::types::{BloodPressure, OrganType};
|
||||||
|
use core::fmt::Debug;
|
||||||
|
|
||||||
|
/// Common metadata for organs.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OrganInfo {
|
||||||
|
id: String,
|
||||||
|
kind: OrganType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OrganInfo {
|
||||||
|
pub fn new(id: impl Into<String>, kind: OrganType) -> Self {
|
||||||
|
Self { id: id.into(), 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 {
|
||||||
|
fn id(&self) -> &str;
|
||||||
|
fn organ_type(&self) -> OrganType;
|
||||||
|
fn update(&mut self, dt_seconds: f32);
|
||||||
|
fn summary(&self) -> String;
|
||||||
|
fn as_any(&self) -> &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 spleen;
|
||||||
|
|
||||||
|
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 spleen::Spleen;
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Pancreas {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// Insulin secretion index
|
||||||
|
pub insulin: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pancreas {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 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 }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SpinalCord {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// 0..=100 nerve signal integrity.
|
||||||
|
pub signal_integrity: u8,
|
||||||
|
pub injury: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpinalCord {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Spleen {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// Immune activity 0..=100
|
||||||
|
pub immune_activity: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Spleen {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 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 }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use super::{Organ, OrganInfo};
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Stomach {
|
||||||
|
info: OrganInfo,
|
||||||
|
/// Acid level 0..=100
|
||||||
|
pub acid_level: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stomach {
|
||||||
|
pub fn new(id: impl Into<String>) -> Self {
|
||||||
|
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 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 }
|
||||||
|
}
|
||||||
|
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
//! Patient type holding organs and core physiology snapshots.
|
||||||
|
|
||||||
|
use crate::error::MedicalError;
|
||||||
|
use crate::organs::{Heart, Lungs, Organ};
|
||||||
|
use crate::types::{Blood, BloodPressure, OrganType};
|
||||||
|
|
||||||
|
/// Patient container and simulation entry.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Patient {
|
||||||
|
id: String,
|
||||||
|
organs: Vec<Box<dyn Organ>>,
|
||||||
|
pub blood: Blood,
|
||||||
|
pub blood_pressure: BloodPressure,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Patient {
|
||||||
|
/// Construct a new patient with validated id.
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
id,
|
||||||
|
organs: Vec::with_capacity(4),
|
||||||
|
blood: Blood::default(),
|
||||||
|
blood_pressure: BloodPressure::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Patient identifier.
|
||||||
|
pub fn id(&self) -> &str { &self.id }
|
||||||
|
|
||||||
|
/// Add an organ to the patient.
|
||||||
|
pub fn add_organ(&mut self, organ: impl Organ + 'static) {
|
||||||
|
self.organs.push(Box::new(organ));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a typed organ by downcasting.
|
||||||
|
pub fn find_organ_typed<T: Organ + 'static>(&self) -> Option<&T> {
|
||||||
|
self.organs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|o| o.as_any().downcast_ref::<T>())
|
||||||
|
.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mutable typed organ lookup.
|
||||||
|
pub fn find_organ_typed_mut<T: Organ + 'static>(&mut self) -> Option<&mut T> {
|
||||||
|
self.organs
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|o| o.as_any_mut().downcast_mut::<T>())
|
||||||
|
.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a patient with a 12-lead heart.
|
||||||
|
pub fn initialize_default(self) -> Self {
|
||||||
|
self.with_heart(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a patient with a heart with `leads`.
|
||||||
|
pub fn with_heart(mut self, leads: u8) -> Self {
|
||||||
|
let id = format!("{}-heart", self.id);
|
||||||
|
self.add_organ(Heart::new(id, leads));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach default lungs.
|
||||||
|
pub fn with_lungs(mut self) -> Self {
|
||||||
|
let id = format!("{}-lungs", self.id);
|
||||||
|
self.add_organ(Lungs::new(id));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)); }
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance simulation by `dt_seconds`.
|
||||||
|
pub fn update(&mut self, dt_seconds: f32) {
|
||||||
|
for organ in &mut self.organs { organ.update(dt_seconds); }
|
||||||
|
// Simple inter-organ signaling: low SpO2 nudges heart rate higher.
|
||||||
|
if let Some(lungs) = self.find_organ_typed::<Lungs>() {
|
||||||
|
if let Some(heart) = self.find_organ_typed_mut::<Heart>() {
|
||||||
|
let target = if lungs.spo2_pct < 92.0 { 90.0 } else { 70.0 };
|
||||||
|
let diff = target - heart.heart_rate_bpm;
|
||||||
|
heart.heart_rate_bpm += 0.05 * diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Kidneys produce urine into bladder
|
||||||
|
if let (Some(kidneys), Some(bladder)) = (
|
||||||
|
self.find_organ_typed::<crate::organs::Kidneys>(),
|
||||||
|
self.find_organ_typed_mut::<crate::organs::Bladder>(),
|
||||||
|
) {
|
||||||
|
let produced = (kidneys.gfr * (dt_seconds / 60.0)).max(0.0) * 0.5; // ml
|
||||||
|
bladder.volume_ml += produced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-line summary of a specific organ by type.
|
||||||
|
pub fn organ_summary(&self, kind: OrganType) -> crate::Result<String> {
|
||||||
|
match self.find_organ(kind) {
|
||||||
|
Some(o) => Ok(o.summary()),
|
||||||
|
None => Err(MedicalError::NotFound("organ not present")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aggregate summary of patient and all organs.
|
||||||
|
pub fn patient_summary(&self) -> String {
|
||||||
|
let mut parts = Vec::with_capacity(self.organs.len() + 2);
|
||||||
|
parts.push(format!(
|
||||||
|
"Patient[id={}, BP={}, Hgb={:.1} g/dL, SpO2={:.0}%]",
|
||||||
|
self.id, self.blood_pressure, self.blood.hemoglobin_g_dl, self.blood.spo2_pct
|
||||||
|
));
|
||||||
|
for o in &self.organs {
|
||||||
|
parts.push(o.summary());
|
||||||
|
}
|
||||||
|
parts.join(" | ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Patient {
|
||||||
|
fn default() -> Self {
|
||||||
|
// A benign default id that passes validation.
|
||||||
|
Self::new("patient-0").unwrap().initialize_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::types::OrganType;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patient_lifecycle() {
|
||||||
|
let mut p = Patient::new("alice-01").unwrap().initialize_default();
|
||||||
|
assert!(p.organ_summary(OrganType::Heart).unwrap().contains("Heart"));
|
||||||
|
p.update(0.5);
|
||||||
|
assert!(p.patient_summary().contains("Patient[id=alice-01"));
|
||||||
|
// Downcast to Heart and tweak
|
||||||
|
let h = p.find_organ_typed::<crate::organs::Heart>().unwrap();
|
||||||
|
assert_eq!(h.organ_type(), OrganType::Heart);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_id() {
|
||||||
|
assert!(Patient::new("").is_err());
|
||||||
|
assert!(Patient::new("bad id").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//! Core domain types (vitals, blood, organ categories).
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
/// Known organ categories.
|
||||||
|
pub enum OrganType {
|
||||||
|
Heart,
|
||||||
|
Lungs,
|
||||||
|
Brain,
|
||||||
|
SpinalCord,
|
||||||
|
Stomach,
|
||||||
|
Liver,
|
||||||
|
Gallbladder,
|
||||||
|
Pancreas,
|
||||||
|
Intestines,
|
||||||
|
Esophagus,
|
||||||
|
Kidneys,
|
||||||
|
Bladder,
|
||||||
|
Spleen,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
/// Blood pressure in mmHg.
|
||||||
|
pub struct BloodPressure {
|
||||||
|
/// Systolic pressure (mmHg).
|
||||||
|
pub systolic: u16,
|
||||||
|
/// Diastolic pressure (mmHg).
|
||||||
|
pub diastolic: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BloodPressure {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
systolic: 120,
|
||||||
|
diastolic: 80,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BloodPressure {
|
||||||
|
/// Validate physiological plausibility: systolic > diastolic, reasonable ranges.
|
||||||
|
pub fn validate(&self) -> bool {
|
||||||
|
let (sys, dia) = (self.systolic, self.diastolic);
|
||||||
|
sys > dia && (40..=300).contains(&sys) && (20..=180).contains(&dia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BloodPressure {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}/{} mmHg", self.systolic, self.diastolic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
/// Simplified blood chemistry panel.
|
||||||
|
pub struct Blood {
|
||||||
|
/// Hemoglobin (g/dL).
|
||||||
|
pub hemoglobin_g_dl: f32,
|
||||||
|
/// Hematocrit (%).
|
||||||
|
pub hematocrit_pct: f32,
|
||||||
|
/// Oxygen saturation (%).
|
||||||
|
pub spo2_pct: f32,
|
||||||
|
/// Glucose (mg/dL).
|
||||||
|
pub glucose_mg_dl: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Blood {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
hemoglobin_g_dl: 14.0,
|
||||||
|
hematocrit_pct: 42.0,
|
||||||
|
spo2_pct: 98.0,
|
||||||
|
glucose_mg_dl: 90.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Blood {
|
||||||
|
/// Returns true when fields are within typical healthy ranges.
|
||||||
|
pub fn validate(&self) -> bool {
|
||||||
|
let hgb_ok = (3.0..=25.0).contains(&self.hemoglobin_g_dl);
|
||||||
|
let hct_ok = (10.0..=70.0).contains(&self.hematocrit_pct);
|
||||||
|
let spo2_ok = (50.0..=100.0).contains(&self.spo2_pct);
|
||||||
|
let glucose_ok = (40.0..=500.0).contains(&self.glucose_mg_dl);
|
||||||
|
hgb_ok && hct_ok && spo2_ok && glucose_ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#[test]
|
||||||
|
fn bmi_works() {
|
||||||
|
let bmi = medicallib_rust::calculate_bmi(70.0, 1.75).unwrap();
|
||||||
|
assert!((bmi - 22.857145).abs() < 1e-5);
|
||||||
|
let cls = medicallib_rust::classify_bmi(bmi);
|
||||||
|
assert_eq!(cls, medicallib_rust::BMIClassification::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#[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);
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#[test]
|
||||||
|
fn default_patient_heart() {
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user