use std::time::Instant;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CardiacRhythm {
NormalSinus,
SinusTachycardia,
SinusBradycardia,
Arrhythmia,
Fibrillation,
Asystole,
Indeterminate,
}
#[derive(Clone, Debug)]
pub struct BeatEvent {
pub instant: Instant,
pub beat_number: u64,
pub ibi_us: u64,
pub stroke_force: u8,
}
pub struct CardiacVitals {
ibi_history: [u64; 8],
ibi_head: usize,
ibi_count: u8,
last_beat_instant: Option<Instant>,
pub beat_count: u64,
pub tachycardia_bpm: u16,
pub bradycardia_bpm: u16,
pub asystole_timeout_us: u64,
}
impl CardiacVitals {
pub fn new() -> Self {
Self {
ibi_history: [0; 8],
ibi_head: 0,
ibi_count: 0,
last_beat_instant: None,
beat_count: 0,
tachycardia_bpm: 150,
bradycardia_bpm: 40,
asystole_timeout_us: 5_000_000, }
}
pub fn record_beat(&mut self, now: Instant) -> BeatEvent {
let ibi_us = if let Some(last) = self.last_beat_instant {
let elapsed = now.duration_since(last);
elapsed.as_micros() as u64
} else {
0
};
if self.beat_count > 0 && ibi_us > 0 {
self.ibi_history[self.ibi_head] = ibi_us;
self.ibi_head = (self.ibi_head + 1) % 8;
if self.ibi_count < 8 {
self.ibi_count += 1;
}
}
let beat_number = self.beat_count;
self.beat_count += 1;
self.last_beat_instant = Some(now);
BeatEvent {
instant: now,
beat_number,
ibi_us,
stroke_force: 200,
}
}
pub fn ibi_mean_us(&self) -> u64 {
if self.ibi_count == 0 {
return 0;
}
let sum: u64 = self.active_ibis().iter().sum();
sum / self.ibi_count as u64
}
pub fn bpm(&self) -> u16 {
let mean_us = self.ibi_mean_us();
if mean_us == 0 {
return 0;
}
let bpm = 60_000_000u64 / mean_us;
bpm.min(u16::MAX as u64) as u16
}
pub fn ibi_cv(&self) -> u8 {
if self.ibi_count < 2 {
return 0;
}
let mean = self.ibi_mean_us();
if mean == 0 {
return 0;
}
let ibis = self.active_ibis();
let mut variance_sum: u128 = 0;
for &ibi in ibis.iter() {
let diff = if ibi >= mean { ibi - mean } else { mean - ibi };
variance_sum += (diff as u128) * (diff as u128);
}
let variance = (variance_sum / self.ibi_count as u128) as u64;
let stddev = isqrt(variance);
let cv_scaled = (stddev * 255) / mean;
cv_scaled.min(255) as u8
}
pub fn rmssd_us(&self) -> u64 {
if self.ibi_count < 2 {
return 0;
}
let ibis = self.active_ibis();
let n = ibis.len();
let mut sum_sq_diff: u128 = 0;
let mut pairs: u64 = 0;
for i in 1..n {
let a = ibis[i - 1];
let b = ibis[i];
let diff = if a >= b { a - b } else { b - a };
sum_sq_diff += (diff as u128) * (diff as u128);
pairs += 1;
}
if pairs == 0 {
return 0;
}
isqrt((sum_sq_diff / pairs as u128) as u64)
}
pub fn classify(&self, now: Instant) -> CardiacRhythm {
if let Some(last) = self.last_beat_instant {
let silence_us = now.duration_since(last).as_micros() as u64;
if silence_us > self.asystole_timeout_us {
return CardiacRhythm::Asystole;
}
} else if self.beat_count == 0 {
return CardiacRhythm::Indeterminate;
}
if self.ibi_count < 2 {
return CardiacRhythm::Indeterminate;
}
let cv = self.ibi_cv();
if cv > 204 {
return CardiacRhythm::Fibrillation;
}
if cv > 102 {
return CardiacRhythm::Arrhythmia;
}
let bpm = self.bpm();
if bpm > self.tachycardia_bpm {
return CardiacRhythm::SinusTachycardia;
}
if bpm > 0 && bpm < self.bradycardia_bpm {
return CardiacRhythm::SinusBradycardia;
}
CardiacRhythm::NormalSinus
}
pub fn silence_us(&self, now: Instant) -> Option<u64> {
self.last_beat_instant
.map(|last| now.duration_since(last).as_micros() as u64)
}
fn active_ibis(&self) -> &[u64] {
let count = self.ibi_count as usize;
if count < 8 {
&self.ibi_history[..count]
} else {
&self.ibi_history
}
}
}
impl Default for CardiacVitals {
fn default() -> Self {
Self::new()
}
}
fn isqrt(n: u64) -> u64 {
if n == 0 {
return 0;
}
let mut x = n;
let mut y = (x + 1) / 2;
while y < x {
x = y;
y = (x + n / x) / 2;
}
x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn isqrt_correctness() {
assert_eq!(isqrt(0), 0);
assert_eq!(isqrt(1), 1);
assert_eq!(isqrt(4), 2);
assert_eq!(isqrt(9), 3);
assert_eq!(isqrt(100), 10);
assert_eq!(isqrt(99), 9); assert_eq!(isqrt(10000), 100);
}
#[test]
fn vitals_no_beats() {
let v = CardiacVitals::new();
assert_eq!(v.ibi_mean_us(), 0);
assert_eq!(v.bpm(), 0);
assert_eq!(v.ibi_cv(), 0);
assert_eq!(v.classify(Instant::now()), CardiacRhythm::Indeterminate);
}
#[test]
fn vitals_regular_rhythm() {
let mut v = CardiacVitals::new();
let base = Instant::now();
for i in 0..10 {
let beat_time = base + std::time::Duration::from_millis(i * 800);
v.record_beat(beat_time);
}
assert_eq!(v.ibi_mean_us(), 800_000);
assert_eq!(v.bpm(), 75);
assert_eq!(v.ibi_cv(), 0);
}
#[test]
fn vitals_variable_rhythm() {
let mut v = CardiacVitals::new();
let base = Instant::now();
let mut t = 0u64;
v.record_beat(base);
for i in 0..8 {
t += if i % 2 == 0 { 600 } else { 1000 };
v.record_beat(base + std::time::Duration::from_millis(t));
}
assert_eq!(v.ibi_mean_us(), 800_000);
assert!(v.ibi_cv() > 0);
}
}