use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CardiacPhase {
Diastolic,
Upstroke,
Refractory,
}
#[derive(Clone, Debug)]
pub struct CalciumClockConfig {
pub enabled: bool,
pub release_threshold: u8,
pub base_refill_rate_per_sec: u32,
pub ncx_depolarization: i16,
}
impl Default for CalciumClockConfig {
fn default() -> Self {
Self {
enabled: false,
release_threshold: 180,
base_refill_rate_per_sec: 400,
ncx_depolarization: 5,
}
}
}
pub struct CalciumClock {
pub sr_load: u8,
pub refill_rate_per_sec: u32,
pub released_this_cycle: bool,
pub release_threshold: u8,
pub base_refill_rate_per_sec: u32,
pub ncx_depolarization: i16,
}
impl CalciumClock {
fn new(config: &CalciumClockConfig) -> Self {
Self {
sr_load: 0,
refill_rate_per_sec: config.base_refill_rate_per_sec,
released_this_cycle: false,
release_threshold: config.release_threshold,
base_refill_rate_per_sec: config.base_refill_rate_per_sec,
ncx_depolarization: config.ncx_depolarization,
}
}
fn reset(&mut self) {
self.sr_load = 0;
self.released_this_cycle = false;
}
fn compute(&mut self, elapsed_us: u64) -> i16 {
if self.released_this_cycle {
return 0;
}
let load = (self.refill_rate_per_sec as u64 * elapsed_us) / 1_000_000;
self.sr_load = load.min(255) as u8;
if self.sr_load >= self.release_threshold {
self.released_this_cycle = true;
self.ncx_depolarization
} else {
0
}
}
}
#[rustfmt::skip]
const SINE_TABLE: [i8; 256] = [
0, 3, 6, 9, 12, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46,
49, 51, 54, 57, 60, 63, 65, 68, 71, 73, 76, 78, 81, 83, 85, 88,
90, 92, 94, 96, 98, 100, 102, 104, 106, 107, 109, 111, 112, 113, 115, 116,
117, 118, 120, 121, 122, 122, 123, 124, 125, 125, 126, 126, 126, 127, 127, 127,
127, 127, 127, 127, 126, 126, 126, 125, 125, 124, 123, 122, 122, 121, 120, 118,
117, 116, 115, 113, 112, 111, 109, 107, 106, 104, 102, 100, 98, 96, 94, 92,
90, 88, 85, 83, 81, 78, 76, 73, 71, 68, 65, 63, 60, 57, 54, 51,
49, 46, 43, 40, 37, 34, 31, 28, 25, 22, 19, 16, 12, 9, 6, 3,
0, -3, -6, -9, -12, -16, -19, -22, -25, -28, -31, -34, -37, -40, -43, -46,
-49, -51, -54, -57, -60, -63, -65, -68, -71, -73, -76, -78, -81, -83, -85, -88,
-90, -92, -94, -96, -98, -100, -102, -104, -106, -107, -109, -111, -112, -113, -115, -116,
-117, -118, -120, -121, -122, -122, -123, -124, -125, -125, -126, -126, -126, -127, -127, -127,
-127, -127, -127, -127, -126, -126, -126, -125, -125, -124, -123, -122, -122, -121, -120, -118,
-117, -116, -115, -113, -112, -111, -109, -107, -106, -104, -102, -100, -98, -96, -94, -92,
-90, -88, -85, -83, -81, -78, -76, -73, -71, -68, -65, -63, -60, -57, -54, -51,
-49, -46, -43, -40, -37, -34, -31, -28, -25, -22, -19, -16, -12, -9, -6, -3,
];
fn integer_sine(phase: u16) -> i8 {
SINE_TABLE[(phase >> 8) as usize]
}
#[derive(Clone, Debug)]
pub struct HrvConfig {
pub enabled: bool,
pub seed: u64,
pub rsa_amplitude: i16,
pub rsa_frequency: u16,
pub lf_amplitude: i16,
pub lf_frequency: u16,
pub intrinsic_jitter: i16,
}
impl Default for HrvConfig {
fn default() -> Self {
Self {
enabled: false,
seed: 0xDEAD_BEEF_CAFE_BABE,
rsa_amplitude: 3,
rsa_frequency: 14000,
lf_amplitude: 2,
lf_frequency: 5600,
intrinsic_jitter: 1,
}
}
}
pub struct HrvGenerator {
rng_state: u64,
rsa_phase: u16,
rsa_frequency: u16,
rsa_amplitude: i16,
lf_phase: u16,
lf_frequency: u16,
lf_amplitude: i16,
intrinsic_jitter: i16,
}
impl HrvGenerator {
fn new(config: &HrvConfig) -> Self {
Self {
rng_state: config.seed,
rsa_phase: 0,
rsa_frequency: config.rsa_frequency,
rsa_amplitude: config.rsa_amplitude,
lf_phase: 0,
lf_frequency: config.lf_frequency,
lf_amplitude: config.lf_amplitude,
intrinsic_jitter: config.intrinsic_jitter,
}
}
fn next_random(&mut self) -> u64 {
let mut x = self.rng_state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.rng_state = x;
x
}
fn cycle_jitter(&mut self) -> i16 {
let rsa = (integer_sine(self.rsa_phase) as i32 * self.rsa_amplitude as i32) / 127;
let lf = (integer_sine(self.lf_phase) as i32 * self.lf_amplitude as i32) / 127;
let noise = if self.intrinsic_jitter > 0 {
let r = self.next_random();
let range = (self.intrinsic_jitter as u64) * 2 + 1;
let val = (r % range) as i16 - self.intrinsic_jitter;
val
} else {
0
};
self.rsa_phase = self.rsa_phase.wrapping_add(self.rsa_frequency);
self.lf_phase = self.lf_phase.wrapping_add(self.lf_frequency);
(rsa + lf + noise as i32) as i16
}
}
#[derive(Clone, Debug)]
pub struct ZoneConfig {
pub resting_potential: i16,
pub threshold: i16,
pub peak_potential: i16,
pub base_leak_rate_per_sec: u32,
pub refractory_us: u64,
pub conduction_delay_us: u64,
pub ne_sensitivity: u8,
pub ach_sensitivity: u8,
pub calcium_clock: CalciumClockConfig,
pub hrv: HrvConfig,
}
pub struct CardiacZone {
pub membrane: i16,
pub phase: CardiacPhase,
pub phase_start: Instant,
pub leak_rate_per_sec: u32,
pub pending_trigger: bool,
pub trigger_received_at: Option<Instant>,
pub calcium_clock: Option<CalciumClock>,
pub hrv_generator: Option<HrvGenerator>,
pub effective_threshold: i16,
pub resting_potential: i16,
pub threshold: i16,
pub peak_potential: i16,
pub base_leak_rate_per_sec: u32,
pub refractory_duration: Duration,
pub conduction_delay: Duration,
pub ne_sensitivity: u8,
pub ach_sensitivity: u8,
}
impl CardiacZone {
pub fn new(config: &ZoneConfig, now: Instant) -> Self {
let calcium_clock = if config.calcium_clock.enabled {
Some(CalciumClock::new(&config.calcium_clock))
} else {
None
};
let hrv_generator = if config.hrv.enabled {
Some(HrvGenerator::new(&config.hrv))
} else {
None
};
Self {
membrane: config.resting_potential,
phase: CardiacPhase::Diastolic,
phase_start: now,
leak_rate_per_sec: config.base_leak_rate_per_sec,
pending_trigger: false,
trigger_received_at: None,
calcium_clock,
hrv_generator,
effective_threshold: config.threshold,
resting_potential: config.resting_potential,
threshold: config.threshold,
peak_potential: config.peak_potential,
base_leak_rate_per_sec: config.base_leak_rate_per_sec,
refractory_duration: Duration::from_micros(config.refractory_us),
conduction_delay: Duration::from_micros(config.conduction_delay_us),
ne_sensitivity: config.ne_sensitivity,
ach_sensitivity: config.ach_sensitivity,
}
}
pub fn apply_modulation(&mut self, ne: u8, ach: u8, cortisol: u8, metabolism: &MetabolismConfig) {
if self.base_leak_rate_per_sec == 0
|| (self.ne_sensitivity == 0 && self.ach_sensitivity == 0)
{
return;
}
let base = self.base_leak_rate_per_sec as u64;
let effective_ne_sens = (self.ne_sensitivity as u32)
.saturating_add(cortisol as u32 / 4)
.min(255);
let ne_suppression = 256u64
- (ach as u64 * metabolism.ach_ne_antagonism as u64) / (255 * 2);
let ne_boost = (base * 2 * ne as u64 * effective_ne_sens as u64 * ne_suppression)
/ (255 * 255 * 256);
let ach_suppression = 256u64
- (ne as u64 * metabolism.ne_ach_antagonism as u64) / (255 * 2);
let ach_brake = (base * ach as u64 * self.ach_sensitivity as u64 * ach_suppression)
/ (255 * 255 * 2 * 256);
let modulated = (base as i64) + (ne_boost as i64) - (ach_brake as i64);
let floor = (base / 4).max(1) as i64;
let ceiling = (base * 4) as i64;
self.leak_rate_per_sec = modulated.clamp(floor, ceiling) as u32;
if let Some(ref mut ca_clock) = self.calcium_clock {
let ratio_x256 = (self.leak_rate_per_sec as u64 * 256) / base;
ca_clock.refill_rate_per_sec =
((ca_clock.base_refill_rate_per_sec as u64 * ratio_x256) / 256) as u32;
}
}
pub fn update(&mut self, now: Instant) -> bool {
match self.phase {
CardiacPhase::Refractory => {
let elapsed = now.duration_since(self.phase_start);
if elapsed >= self.refractory_duration {
self.phase = CardiacPhase::Diastolic;
self.phase_start = now;
self.membrane = self.resting_potential;
if let Some(ref mut hrv) = self.hrv_generator {
let jitter = hrv.cycle_jitter();
let min_thresh = self.resting_potential + 5;
let max_thresh = self.peak_potential - 5;
self.effective_threshold =
(self.threshold as i32 + jitter as i32)
.clamp(min_thresh as i32, max_thresh as i32) as i16;
} else {
self.effective_threshold = self.threshold;
}
}
false
}
CardiacPhase::Diastolic => {
if self.pending_trigger {
if let Some(trigger_time) = self.trigger_received_at {
if !self.conduction_delay.is_zero() {
let since_trigger = now.duration_since(trigger_time);
if since_trigger >= self.conduction_delay {
self.pending_trigger = false;
self.trigger_received_at = None;
return self.fire(now);
}
} else {
self.pending_trigger = false;
self.trigger_received_at = None;
return self.fire(now);
}
}
}
if self.leak_rate_per_sec > 0 {
let elapsed = now.duration_since(self.phase_start);
let elapsed_us = elapsed.as_micros() as u64;
let leak_accumulated = (self.leak_rate_per_sec as u64 * elapsed_us) / 1_000_000;
let new_membrane = (self.resting_potential as i64) + (leak_accumulated as i64);
self.membrane = new_membrane.min(i16::MAX as i64) as i16;
if let Some(ref mut ca_clock) = self.calcium_clock {
let ncx_mv = ca_clock.compute(elapsed_us);
if ncx_mv > 0 {
self.membrane = self.membrane.saturating_add(ncx_mv);
}
}
if self.membrane >= self.effective_threshold {
self.pending_trigger = false;
self.trigger_received_at = None;
return self.fire(now);
}
}
false
}
CardiacPhase::Upstroke => {
self.phase = CardiacPhase::Refractory;
self.phase_start = now;
false
}
}
}
pub fn trigger(&mut self, now: Instant) {
if self.phase != CardiacPhase::Refractory {
self.pending_trigger = true;
self.trigger_received_at = Some(now);
}
}
pub fn electrotonic_depolarize(&mut self, amount: i16) {
if self.phase == CardiacPhase::Diastolic && amount > 0 {
self.membrane = self.membrane.saturating_add(amount)
.min(self.threshold - 1);
}
}
fn fire(&mut self, now: Instant) -> bool {
self.membrane = self.peak_potential;
self.phase = CardiacPhase::Upstroke;
self.phase_start = now;
if let Some(ref mut ca_clock) = self.calcium_clock {
ca_clock.reset();
}
true
}
}
#[derive(Clone, Debug)]
pub struct MetabolismConfig {
pub cortisol_ne_protection: u8,
pub ne_ach_antagonism: u8,
pub ach_ne_antagonism: u8,
}
impl Default for MetabolismConfig {
fn default() -> Self {
Self {
cortisol_ne_protection: 255, ne_ach_antagonism: 128, ach_ne_antagonism: 128,
}
}
}
#[derive(Clone, Debug)]
pub struct GapJunctionConfig {
pub strength: i16,
pub retrograde: bool,
}
impl Default for GapJunctionConfig {
fn default() -> Self {
Self {
strength: 3,
retrograde: false,
}
}
}
#[derive(Clone, Debug)]
pub struct CardiacConfig {
pub sa_node: ZoneConfig,
pub av_node: ZoneConfig,
pub conduction: ZoneConfig,
pub myocardium: ZoneConfig,
pub metabolism: MetabolismConfig,
pub gap_sa_av: GapJunctionConfig,
pub gap_av_cond: GapJunctionConfig,
pub gap_cond_myo: GapJunctionConfig,
}
impl Default for CardiacConfig {
fn default() -> Self {
Self {
sa_node: ZoneConfig {
resting_potential: -70,
threshold: -40,
peak_potential: 30,
base_leak_rate_per_sec: 60, refractory_us: 350_000, conduction_delay_us: 0,
ne_sensitivity: 128,
ach_sensitivity: 128,
calcium_clock: CalciumClockConfig {
enabled: true,
release_threshold: 180, base_refill_rate_per_sec: 400, ncx_depolarization: 5, },
hrv: HrvConfig {
enabled: true,
seed: 0xDEAD_BEEF_CAFE_BABE,
rsa_amplitude: 3, rsa_frequency: 14000, lf_amplitude: 2, lf_frequency: 5600, intrinsic_jitter: 1, },
},
av_node: ZoneConfig {
resting_potential: -70,
threshold: -40,
peak_potential: 30,
base_leak_rate_per_sec: 25, refractory_us: 250_000, conduction_delay_us: 120_000, ne_sensitivity: 0, ach_sensitivity: 0,
calcium_clock: CalciumClockConfig::default(),
hrv: HrvConfig::default(),
},
conduction: ZoneConfig {
resting_potential: -70,
threshold: -40,
peak_potential: 30,
base_leak_rate_per_sec: 12, refractory_us: 200_000, conduction_delay_us: 10_000, ne_sensitivity: 0, ach_sensitivity: 0,
calcium_clock: CalciumClockConfig::default(),
hrv: HrvConfig::default(),
},
myocardium: ZoneConfig {
resting_potential: -70,
threshold: -40,
peak_potential: 30,
base_leak_rate_per_sec: 0,
refractory_us: 300_000, conduction_delay_us: 0,
ne_sensitivity: 0,
ach_sensitivity: 0,
calcium_clock: CalciumClockConfig::default(),
hrv: HrvConfig::default(),
},
metabolism: MetabolismConfig::default(),
gap_sa_av: GapJunctionConfig { strength: 3, retrograde: false },
gap_av_cond: GapJunctionConfig { strength: 3, retrograde: true },
gap_cond_myo: GapJunctionConfig { strength: 5, retrograde: true },
}
}
}