Consistent formatting pass: line wrapping, import sorting, trailing whitespace removal, let-chain indentation, merged derive attributes, and unsafe block reformatting. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
308 lines
8.4 KiB
Rust
308 lines
8.4 KiB
Rust
//! Ionic models for cardiac action potential simulation.
|
|
//!
|
|
//! Implements various ionic models from simple to detailed:
|
|
//! - Mitchell-Schaeffer (2 variables)
|
|
//! - FitzHugh-Nagumo (2 variables)
|
|
//! - Aliev-Panfilov (2 variables)
|
|
//! - ten Tusscher-Panfilov (17 variables)
|
|
|
|
use cardiosim_shared::IonicModel;
|
|
|
|
/// Ionic state for a cell.
|
|
#[derive(Debug, Clone)]
|
|
pub struct IonicState {
|
|
model: IonicModel,
|
|
/// Recovery variable (w or h)
|
|
pub recovery: f32,
|
|
/// Additional state variables for complex models
|
|
pub aux_vars: Vec<f32>,
|
|
}
|
|
|
|
impl IonicState {
|
|
/// Create a new ionic state.
|
|
#[must_use]
|
|
pub fn new(model: IonicModel) -> Self {
|
|
let aux_vars = match model {
|
|
IonicModel::TenTusscherPanfilov | IonicModel::OHaraRudy => {
|
|
// Many gating variables
|
|
vec![0.0; 15]
|
|
}
|
|
_ => vec![],
|
|
};
|
|
|
|
Self {
|
|
model,
|
|
recovery: 0.0,
|
|
aux_vars,
|
|
}
|
|
}
|
|
|
|
/// Compute ionic currents.
|
|
#[must_use]
|
|
pub fn compute_currents(&self, voltage: f32) -> (f32, f32) {
|
|
match self.model {
|
|
IonicModel::MitchellSchaeffer => self.mitchell_schaeffer(voltage),
|
|
IonicModel::FitzHughNagumo => self.fitzhugh_nagumo(voltage),
|
|
IonicModel::AlievPanfilov => self.aliev_panfilov(voltage),
|
|
IonicModel::TenTusscherPanfilov => self.ten_tusscher(voltage),
|
|
IonicModel::OHaraRudy => self.ohara_rudy(voltage),
|
|
}
|
|
}
|
|
|
|
/// Mitchell-Schaeffer model.
|
|
/// Simple 2-variable model with good action potential shape.
|
|
fn mitchell_schaeffer(&self, v: f32) -> (f32, f32) {
|
|
// Parameters
|
|
let tau_in = 0.3;
|
|
let tau_out = 6.0;
|
|
let tau_open = 120.0;
|
|
let tau_close = 150.0;
|
|
let v_gate = 0.13;
|
|
|
|
// Normalize voltage to [0, 1]
|
|
let u = (v + 85.0) / 120.0;
|
|
let u = u.clamp(0.0, 1.0);
|
|
|
|
let h = self.recovery;
|
|
|
|
// Currents
|
|
let j_in = h * u * u * (1.0 - u) / tau_in;
|
|
let j_out = -u / tau_out;
|
|
let j_ion = j_in + j_out;
|
|
|
|
// Gate dynamics
|
|
let dh = if u < v_gate {
|
|
(1.0 - h) / tau_open
|
|
} else {
|
|
-h / tau_close
|
|
};
|
|
|
|
// Scale back to mV
|
|
(j_ion * 120.0, dh)
|
|
}
|
|
|
|
/// FitzHugh-Nagumo model.
|
|
/// Classic excitable media model.
|
|
fn fitzhugh_nagumo(&self, v: f32) -> (f32, f32) {
|
|
// Parameters
|
|
let a = 0.7;
|
|
let b = 0.8;
|
|
let tau = 12.5;
|
|
let epsilon = 0.08;
|
|
|
|
// Normalize
|
|
let u = (v + 85.0) / 120.0;
|
|
let u = u.clamp(-0.5, 1.5);
|
|
let w = self.recovery;
|
|
|
|
// Cubic nullcline
|
|
let du = u - u * u * u / 3.0 - w;
|
|
let dw = epsilon * (u + a - b * w);
|
|
|
|
(du * 120.0 / tau, dw)
|
|
}
|
|
|
|
/// Aliev-Panfilov model.
|
|
/// Modified FHN with more realistic restitution.
|
|
fn aliev_panfilov(&self, v: f32) -> (f32, f32) {
|
|
// Parameters
|
|
let k = 8.0;
|
|
let a = 0.15;
|
|
let epsilon0 = 0.002;
|
|
let mu1 = 0.2;
|
|
let mu2 = 0.3;
|
|
|
|
// Normalize
|
|
let u = (v + 85.0) / 120.0;
|
|
let u = u.clamp(0.0, 1.0);
|
|
let w = self.recovery;
|
|
|
|
// Dynamics
|
|
let du = -k * u * (u - a) * (u - 1.0) - u * w;
|
|
|
|
let epsilon = epsilon0 + mu1 * w / (u + mu2);
|
|
let dw = epsilon * (-w - k * u * (u - a - 1.0));
|
|
|
|
(du * 120.0, dw)
|
|
}
|
|
|
|
/// ten Tusscher-Panfilov model (simplified).
|
|
/// More detailed model with major ionic currents.
|
|
fn ten_tusscher(&self, v: f32) -> (f32, f32) {
|
|
// Simplified version with main currents
|
|
|
|
// Reversal potentials
|
|
let e_na = 70.0;
|
|
let e_k = -88.0;
|
|
let e_ca = 120.0;
|
|
|
|
// Maximum conductances (mS/cm²)
|
|
let g_na = 14.838;
|
|
let g_k1 = 5.405;
|
|
let g_to = 0.294;
|
|
let g_cal = 0.000175;
|
|
|
|
// Gating (simplified)
|
|
let m_inf = 1.0 / (1.0 + ((-v - 40.0) / 9.0).exp());
|
|
let h_inf = 1.0 / (1.0 + ((v + 70.0) / 7.0).exp());
|
|
let j_inf = h_inf;
|
|
|
|
let d_inf = 1.0 / (1.0 + ((-v - 5.0) / 6.0).exp());
|
|
let f_inf = 1.0 / (1.0 + ((v + 20.0) / 7.0).exp());
|
|
|
|
let xr1_inf = 1.0 / (1.0 + ((-v - 26.0) / 7.0).exp());
|
|
|
|
// Use recovery as simplified gating
|
|
let h = 0.5 + 0.5 * self.recovery;
|
|
|
|
// Currents
|
|
let i_na = g_na * m_inf.powi(3) * h * j_inf * (v - e_na);
|
|
let i_cal = g_cal * d_inf * f_inf * (v - e_ca);
|
|
let i_k1 = g_k1 * (v - e_k) / (1.0 + (0.1 * (v - e_k)).exp());
|
|
let i_to = g_to * xr1_inf * self.recovery * (v - e_k);
|
|
|
|
let i_ion = i_na + i_cal + i_k1 + i_to;
|
|
|
|
// Recovery dynamics (simplified)
|
|
let tau_h = 5.0 + 100.0 / (1.0 + ((v + 40.0) / 10.0).exp());
|
|
let dh = (h_inf - self.recovery) / tau_h;
|
|
|
|
(-i_ion, dh)
|
|
}
|
|
|
|
/// O'Hara-Rudy model (simplified).
|
|
fn ohara_rudy(&self, v: f32) -> (f32, f32) {
|
|
// Very simplified version
|
|
// Full model has 41 state variables
|
|
|
|
let e_na = 70.0;
|
|
let e_k = -88.0;
|
|
|
|
let g_na_fast = 75.0;
|
|
let g_na_late = 0.0075;
|
|
|
|
// Fast sodium
|
|
let m_inf = 1.0 / (1.0 + (-(v + 39.57) / 9.871).exp());
|
|
let h_inf = 1.0 / (1.0 + ((v + 82.9) / 6.086).exp());
|
|
|
|
let h = 0.5 + 0.5 * self.recovery;
|
|
|
|
let i_na_fast = g_na_fast * m_inf.powi(3) * h.powi(2) * (v - e_na);
|
|
let i_na_late = g_na_late * m_inf.powi(3) * (1.0 - h) * (v - e_na);
|
|
|
|
// Potassium
|
|
let xk1_inf = 1.0 / (1.0 + ((v + 2.55 + e_k) / 34.2).exp());
|
|
let i_k1 = 0.1 * xk1_inf * (v - e_k);
|
|
|
|
let i_ion = i_na_fast + i_na_late + i_k1;
|
|
|
|
// Recovery
|
|
let tau_h = 2.0 + 50.0 / (1.0 + ((v + 50.0) / 10.0).exp());
|
|
let dh = (h_inf - self.recovery) / tau_h;
|
|
|
|
(-i_ion, dh)
|
|
}
|
|
}
|
|
|
|
/// Action potential metrics.
|
|
#[derive(Debug, Clone)]
|
|
pub struct APMetrics {
|
|
/// Peak voltage (mV)
|
|
pub v_max: f32,
|
|
/// Resting voltage (mV)
|
|
pub v_rest: f32,
|
|
/// Maximum upstroke velocity (mV/ms)
|
|
pub dv_dt_max: f32,
|
|
/// APD at 50% repolarization (ms)
|
|
pub apd50: f32,
|
|
/// APD at 90% repolarization (ms)
|
|
pub apd90: f32,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_ionic_state_creation() {
|
|
let state = IonicState::new(IonicModel::MitchellSchaeffer);
|
|
assert_eq!(state.recovery, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mitchell_schaeffer() {
|
|
let state = IonicState::new(IonicModel::MitchellSchaeffer);
|
|
let (dv, dw) = state.compute_currents(-85.0);
|
|
// At rest, should have small currents
|
|
assert!(dv.abs() < 10.0);
|
|
assert!(dw.abs() < 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mitchell_schaeffer_upstroke() {
|
|
let mut state = IonicState::new(IonicModel::MitchellSchaeffer);
|
|
state.recovery = 0.8; // Gate open
|
|
let (dv, _) = state.compute_currents(-40.0);
|
|
// Should have positive dv (upstroke)
|
|
assert!(dv > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fitzhugh_nagumo() {
|
|
let state = IonicState::new(IonicModel::FitzHughNagumo);
|
|
let (dv, dw) = state.compute_currents(-85.0);
|
|
assert!(dv.is_finite());
|
|
assert!(dw.is_finite());
|
|
}
|
|
|
|
#[test]
|
|
fn test_aliev_panfilov() {
|
|
let state = IonicState::new(IonicModel::AlievPanfilov);
|
|
let (dv, dw) = state.compute_currents(-85.0);
|
|
assert!(dv.is_finite());
|
|
assert!(dw.is_finite());
|
|
}
|
|
|
|
#[test]
|
|
fn test_ten_tusscher() {
|
|
let state = IonicState::new(IonicModel::TenTusscherPanfilov);
|
|
let (dv, dw) = state.compute_currents(-85.0);
|
|
assert!(dv.is_finite());
|
|
assert!(dw.is_finite());
|
|
}
|
|
|
|
#[test]
|
|
fn test_ohara_rudy() {
|
|
let state = IonicState::new(IonicModel::OHaraRudy);
|
|
let (dv, dw) = state.compute_currents(-85.0);
|
|
assert!(dv.is_finite());
|
|
assert!(dw.is_finite());
|
|
}
|
|
|
|
#[test]
|
|
fn test_action_potential_cycle() {
|
|
// Simulate one action potential
|
|
let mut state = IonicState::new(IonicModel::MitchellSchaeffer);
|
|
let mut v = -85.0;
|
|
let dt = 0.1;
|
|
|
|
// Apply stimulus
|
|
v = -40.0;
|
|
state.recovery = 0.8;
|
|
|
|
let mut v_max = v;
|
|
for _ in 0..1000 {
|
|
let (dv, dw) = state.compute_currents(v);
|
|
v += dt * dv;
|
|
state.recovery += dt * dw;
|
|
|
|
if v > v_max {
|
|
v_max = v;
|
|
}
|
|
}
|
|
|
|
// Should have depolarized
|
|
assert!(v_max > 0.0);
|
|
}
|
|
}
|