use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crate::cardiac::{CardiacConfig, CardiacZone};
use crate::vitals::{BeatEvent, CardiacRhythm, CardiacVitals};
#[derive(Clone, Copy, Debug)]
pub enum Chemical {
NE,
ACh,
Cortisol,
}
#[derive(Clone, Copy, Debug)]
pub struct ChemicalInjection {
pub chemical: Chemical,
pub amount: u8,
}
pub struct ChemicalPool {
pub ne: u8,
pub ach: u8,
pub cortisol: u8,
pub ne_baseline: u8,
pub ach_baseline: u8,
pub cortisol_baseline: u8,
pub ne_halflife_us: u64,
pub ach_halflife_us: u64,
pub cortisol_halflife_us: u64,
last_metabolism: Instant,
}
impl ChemicalPool {
fn new(now: Instant) -> Self {
Self {
ne: 0,
ach: 30, cortisol: 10,
ne_baseline: 0,
ach_baseline: 30,
cortisol_baseline: 10,
ne_halflife_us: 2_500_000, ach_halflife_us: 500_000, cortisol_halflife_us: 10_000_000,
last_metabolism: now,
}
}
fn inject(&mut self, chemical: Chemical, amount: u8) {
match chemical {
Chemical::NE => self.ne = self.ne.saturating_add(amount),
Chemical::ACh => self.ach = self.ach.saturating_add(amount),
Chemical::Cortisol => self.cortisol = self.cortisol.saturating_add(amount),
}
}
fn metabolize(&mut self, now: Instant, cortisol_ne_protection: u8) {
let elapsed_us = now.duration_since(self.last_metabolism).as_micros() as u64;
if elapsed_us < 250_000 {
return;
}
self.last_metabolism = now;
let ne_halflife = {
let extension = (self.cortisol as u64 * cortisol_ne_protection as u64) / 255;
(self.ne_halflife_us * (256 + extension)) / 256
};
self.ne = decay_toward(self.ne, self.ne_baseline, elapsed_us, ne_halflife);
self.ach = decay_toward(self.ach, self.ach_baseline, elapsed_us, self.ach_halflife_us);
self.cortisol = decay_toward(
self.cortisol,
self.cortisol_baseline,
elapsed_us,
self.cortisol_halflife_us,
);
}
}
fn decay_toward(current: u8, baseline: u8, elapsed_us: u64, halflife_us: u64) -> u8 {
if current == baseline || halflife_us == 0 {
return current;
}
let distance = if current > baseline {
(current - baseline) as u64
} else {
(baseline - current) as u64
};
let full_halflives = elapsed_us / halflife_us;
let remainder_us = elapsed_us % halflife_us;
let mut remaining_distance = if full_halflives >= 8 {
0 } else {
distance >> full_halflives
};
if remaining_distance > 0 && remainder_us > 0 {
let decay_amount = (remaining_distance * remainder_us * 693) / (halflife_us * 1000);
if decay_amount > 0 {
remaining_distance = remaining_distance.saturating_sub(decay_amount);
}
}
if current > baseline {
baseline.saturating_add(remaining_distance.min(255) as u8)
} else {
baseline.saturating_sub(remaining_distance.min(255) as u8)
}
}
pub struct HeartHandle {
injector: Sender<ChemicalInjection>,
pub beats: Receiver<BeatEvent>,
alive: Arc<AtomicBool>,
thread: Option<JoinHandle<HeartSnapshot>>,
}
impl HeartHandle {
pub fn inject_ne(&self, amount: u8) {
let _ = self.injector.send(ChemicalInjection {
chemical: Chemical::NE,
amount,
});
}
pub fn inject_ach(&self, amount: u8) {
let _ = self.injector.send(ChemicalInjection {
chemical: Chemical::ACh,
amount,
});
}
pub fn inject_cortisol(&self, amount: u8) {
let _ = self.injector.send(ChemicalInjection {
chemical: Chemical::Cortisol,
amount,
});
}
pub fn is_alive(&self) -> bool {
self.alive.load(Ordering::Relaxed)
}
pub fn stop(mut self) -> HeartSnapshot {
self.alive.store(false, Ordering::Relaxed);
if let Some(handle) = self.thread.take() {
handle.join().expect("heart thread panicked")
} else {
HeartSnapshot {
beat_count: 0,
last_bpm: 0,
last_rhythm: CardiacRhythm::Asystole,
last_ibi_us: 0,
final_ne: 0,
final_ach: 0,
final_cortisol: 0,
}
}
}
}
impl Drop for HeartHandle {
fn drop(&mut self) {
self.alive.store(false, Ordering::Relaxed);
}
}
#[derive(Clone, Debug)]
pub struct HeartSnapshot {
pub beat_count: u64,
pub last_bpm: u16,
pub last_rhythm: CardiacRhythm,
pub last_ibi_us: u64,
pub final_ne: u8,
pub final_ach: u8,
pub final_cortisol: u8,
}
pub struct CardiacPipeline;
impl CardiacPipeline {
pub fn start() -> HeartHandle {
Self::start_with_config(CardiacConfig::default())
}
pub fn start_with_config(config: CardiacConfig) -> HeartHandle {
let alive = Arc::new(AtomicBool::new(true));
let (inject_tx, inject_rx) = std::sync::mpsc::channel();
let (beat_tx, beat_rx) = std::sync::mpsc::channel();
let alive_clone = Arc::clone(&alive);
let thread = thread::Builder::new()
.name("cardiac".into())
.spawn(move || heart_loop(config, inject_rx, alive_clone, beat_tx))
.expect("failed to spawn cardiac thread");
HeartHandle {
injector: inject_tx,
beats: beat_rx,
alive,
thread: Some(thread),
}
}
}
fn heart_loop(
config: CardiacConfig,
inject_rx: Receiver<ChemicalInjection>,
alive: Arc<AtomicBool>,
beat_tx: Sender<BeatEvent>,
) -> HeartSnapshot {
let now = Instant::now();
let mut sa_node = CardiacZone::new(&config.sa_node, now);
let mut av_node = CardiacZone::new(&config.av_node, now);
let mut conduction = CardiacZone::new(&config.conduction, now);
let mut myocardium = CardiacZone::new(&config.myocardium, now);
let mut vitals = CardiacVitals::new();
let mut pool = ChemicalPool::new(now);
let sleep_duration = Duration::from_micros(100);
while alive.load(Ordering::Relaxed) {
let now = Instant::now();
while let Ok(injection) = inject_rx.try_recv() {
pool.inject(injection.chemical, injection.amount);
}
pool.metabolize(now, config.metabolism.cortisol_ne_protection);
sa_node.apply_modulation(pool.ne, pool.ach, pool.cortisol, &config.metabolism);
let sa_fired = sa_node.update(now);
if sa_fired {
av_node.trigger(now);
av_node.electrotonic_depolarize(config.gap_sa_av.strength);
}
let av_fired = av_node.update(now);
if av_fired {
conduction.trigger(now);
conduction.electrotonic_depolarize(config.gap_av_cond.strength);
if config.gap_av_cond.retrograde {
sa_node.electrotonic_depolarize(config.gap_av_cond.strength / 2);
}
}
let cond_fired = conduction.update(now);
if cond_fired {
myocardium.trigger(now);
myocardium.electrotonic_depolarize(config.gap_cond_myo.strength);
if config.gap_cond_myo.retrograde {
av_node.electrotonic_depolarize(config.gap_cond_myo.strength / 2);
}
}
let myo_fired = myocardium.update(now);
if myo_fired {
let beat = vitals.record_beat(now);
let _ = beat_tx.send(beat);
if config.gap_cond_myo.retrograde {
conduction.electrotonic_depolarize(config.gap_cond_myo.strength / 2);
}
}
thread::sleep(sleep_duration);
}
let now = Instant::now();
HeartSnapshot {
beat_count: vitals.beat_count,
last_bpm: vitals.bpm(),
last_rhythm: vitals.classify(now),
last_ibi_us: vitals.ibi_mean_us(),
final_ne: pool.ne,
final_ach: pool.ach,
final_cortisol: pool.cortisol,
}
}