use serde::{Deserialize, Serialize};
use crate::error::{MastishkError, validate_dt};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TransporterType {
Sert,
Dat,
Net,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ReceptorSubtype {
Ht1a,
Ht2a,
D1,
D2,
Alpha1,
Alpha2,
Beta,
GabaA,
GabaB,
Cb1,
MuOpioid,
Nmda,
Ampa,
Ox1,
Ox2,
Ht2c,
NicotinicA4b2,
NicotinicA7,
H1,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceptorState {
pub availability: f32,
pub baseline: f32,
pub occupancy_ema: f32,
pub tau_turnover: f32,
pub k_des: f32,
pub k_up: f32,
}
impl ReceptorState {
#[must_use]
pub fn new(baseline: f32, tau_turnover: f32, k_des: f32, k_up: f32) -> Self {
Self {
availability: baseline,
baseline,
occupancy_ema: 0.0,
tau_turnover,
k_des,
k_up,
}
}
#[inline]
pub fn tick(&mut self, occupancy: f32, dt: f32) -> Result<(), MastishkError> {
validate_dt(dt)?;
self.tick_unchecked(occupancy, dt);
Ok(())
}
#[inline]
pub(crate) fn tick_unchecked(&mut self, occupancy: f32, dt: f32) {
let ema_alpha = 1.0 - (-dt / 300.0).exp();
self.occupancy_ema += (occupancy - self.occupancy_ema) * ema_alpha;
let baseline_return = (self.baseline - self.availability) / self.tau_turnover;
let desensitization = self.k_des * self.occupancy_ema;
let upregulation =
self.k_up * (1.0 - self.occupancy_ema) * (self.baseline - self.availability).max(0.0);
self.availability += (baseline_return - desensitization + upregulation) * dt;
self.availability = self.availability.clamp(0.0, 1.5);
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReceptorOccupancies {
pub ht1a: f32,
pub ht2a: f32,
pub d1: f32,
pub d2: f32,
pub alpha1: f32,
pub alpha2: f32,
pub beta: f32,
pub gaba_a: f32,
pub gaba_b: f32,
pub cb1: f32,
pub mu_opioid: f32,
pub nmda: f32,
pub ampa: f32,
pub ox1: f32,
pub ox2: f32,
pub ht2c: f32,
pub nicotinic_a4b2: f32,
pub nicotinic_a7: f32,
pub h1: f32,
}
impl ReceptorOccupancies {
#[inline]
#[must_use]
pub fn get(&self, subtype: ReceptorSubtype) -> f32 {
match subtype {
ReceptorSubtype::Ht1a => self.ht1a,
ReceptorSubtype::Ht2a => self.ht2a,
ReceptorSubtype::D1 => self.d1,
ReceptorSubtype::D2 => self.d2,
ReceptorSubtype::Alpha1 => self.alpha1,
ReceptorSubtype::Alpha2 => self.alpha2,
ReceptorSubtype::Beta => self.beta,
ReceptorSubtype::GabaA => self.gaba_a,
ReceptorSubtype::GabaB => self.gaba_b,
ReceptorSubtype::Cb1 => self.cb1,
ReceptorSubtype::MuOpioid => self.mu_opioid,
ReceptorSubtype::Nmda => self.nmda,
ReceptorSubtype::Ampa => self.ampa,
ReceptorSubtype::Ox1 => self.ox1,
ReceptorSubtype::Ox2 => self.ox2,
ReceptorSubtype::Ht2c => self.ht2c,
ReceptorSubtype::NicotinicA4b2 => self.nicotinic_a4b2,
ReceptorSubtype::NicotinicA7 => self.nicotinic_a7,
ReceptorSubtype::H1 => self.h1,
}
}
#[inline]
pub fn add(&mut self, subtype: ReceptorSubtype, value: f32) {
let field = match subtype {
ReceptorSubtype::Ht1a => &mut self.ht1a,
ReceptorSubtype::Ht2a => &mut self.ht2a,
ReceptorSubtype::D1 => &mut self.d1,
ReceptorSubtype::D2 => &mut self.d2,
ReceptorSubtype::Alpha1 => &mut self.alpha1,
ReceptorSubtype::Alpha2 => &mut self.alpha2,
ReceptorSubtype::Beta => &mut self.beta,
ReceptorSubtype::GabaA => &mut self.gaba_a,
ReceptorSubtype::GabaB => &mut self.gaba_b,
ReceptorSubtype::Cb1 => &mut self.cb1,
ReceptorSubtype::MuOpioid => &mut self.mu_opioid,
ReceptorSubtype::Nmda => &mut self.nmda,
ReceptorSubtype::Ampa => &mut self.ampa,
ReceptorSubtype::Ox1 => &mut self.ox1,
ReceptorSubtype::Ox2 => &mut self.ox2,
ReceptorSubtype::Ht2c => &mut self.ht2c,
ReceptorSubtype::NicotinicA4b2 => &mut self.nicotinic_a4b2,
ReceptorSubtype::NicotinicA7 => &mut self.nicotinic_a7,
ReceptorSubtype::H1 => &mut self.h1,
};
*field = (*field + value).min(1.0);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceptorMap {
pub ht1a: ReceptorState,
pub ht2a: ReceptorState,
pub d1: ReceptorState,
pub d2: ReceptorState,
pub alpha1: ReceptorState,
pub alpha2: ReceptorState,
pub beta: ReceptorState,
pub gaba_a: ReceptorState,
pub gaba_b: ReceptorState,
#[serde(default = "default_cb1")]
pub cb1: ReceptorState,
#[serde(default = "default_mu_opioid")]
pub mu_opioid: ReceptorState,
#[serde(default = "default_nmda")]
pub nmda: ReceptorState,
#[serde(default = "default_ampa")]
pub ampa: ReceptorState,
#[serde(default = "default_ox1")]
pub ox1: ReceptorState,
#[serde(default = "default_ox2")]
pub ox2: ReceptorState,
#[serde(default = "default_ht2c")]
pub ht2c: ReceptorState,
#[serde(default = "default_nic_a4b2")]
pub nicotinic_a4b2: ReceptorState,
#[serde(default = "default_nic_a7")]
pub nicotinic_a7: ReceptorState,
#[serde(default = "default_h1")]
pub h1: ReceptorState,
}
fn default_ht2c() -> ReceptorState {
ReceptorState::new(1.0, 864_000.0, 0.000_001_5, 0.000_000_8) }
fn default_nic_a4b2() -> ReceptorState {
ReceptorState::new(1.0, 172_800.0, 0.000_004, 0.000_002) }
fn default_nic_a7() -> ReceptorState {
ReceptorState::new(1.0, 172_800.0, 0.000_003, 0.000_002)
}
fn default_h1() -> ReceptorState {
ReceptorState::new(1.0, 345_600.0, 0.000_002, 0.000_001)
}
fn default_ox1() -> ReceptorState {
ReceptorState::new(1.0, 345_600.0, 0.000_002, 0.000_001)
}
fn default_ox2() -> ReceptorState {
ReceptorState::new(1.0, 345_600.0, 0.000_002, 0.000_001)
}
fn default_ampa() -> ReceptorState {
ReceptorState::new(1.0, 172_800.0, 0.000_003, 0.000_002)
}
fn default_mu_opioid() -> ReceptorState {
ReceptorState::new(1.0, 432_000.0, 0.000_003, 0.000_001)
}
fn default_nmda() -> ReceptorState {
ReceptorState::new(1.0, 432_000.0, 0.000_002, 0.000_001)
}
fn default_cb1() -> ReceptorState {
ReceptorState::new(1.0, 432_000.0, 0.000_002, 0.000_001)
}
impl Default for ReceptorMap {
fn default() -> Self {
Self {
ht1a: ReceptorState::new(1.0, 604_800.0, 0.000_002, 0.000_001),
ht2a: ReceptorState::new(1.0, 345_600.0, 0.000_003, 0.000_001_5),
d1: ReceptorState::new(1.0, 432_000.0, 0.000_002_5, 0.000_001),
d2: ReceptorState::new(1.0, 1_209_600.0, 0.000_001_5, 0.000_000_8),
alpha1: ReceptorState::new(1.0, 345_600.0, 0.000_002, 0.000_001),
alpha2: ReceptorState::new(1.0, 172_800.0, 0.000_003, 0.000_002),
beta: ReceptorState::new(1.0, 345_600.0, 0.000_002, 0.000_001),
gaba_a: ReceptorState::new(1.0, 518_400.0, 0.000_003, 0.000_001_5),
gaba_b: ReceptorState::new(1.0, 432_000.0, 0.000_002, 0.000_001),
cb1: default_cb1(),
mu_opioid: default_mu_opioid(),
nmda: default_nmda(),
ampa: default_ampa(),
ox1: default_ox1(),
ox2: default_ox2(),
ht2c: default_ht2c(),
nicotinic_a4b2: default_nic_a4b2(),
nicotinic_a7: default_nic_a7(),
h1: default_h1(),
}
}
}
impl ReceptorMap {
#[inline]
#[must_use]
pub fn get(&self, subtype: ReceptorSubtype) -> &ReceptorState {
match subtype {
ReceptorSubtype::Ht1a => &self.ht1a,
ReceptorSubtype::Ht2a => &self.ht2a,
ReceptorSubtype::D1 => &self.d1,
ReceptorSubtype::D2 => &self.d2,
ReceptorSubtype::Alpha1 => &self.alpha1,
ReceptorSubtype::Alpha2 => &self.alpha2,
ReceptorSubtype::Beta => &self.beta,
ReceptorSubtype::GabaA => &self.gaba_a,
ReceptorSubtype::GabaB => &self.gaba_b,
ReceptorSubtype::Cb1 => &self.cb1,
ReceptorSubtype::MuOpioid => &self.mu_opioid,
ReceptorSubtype::Nmda => &self.nmda,
ReceptorSubtype::Ampa => &self.ampa,
ReceptorSubtype::Ox1 => &self.ox1,
ReceptorSubtype::Ox2 => &self.ox2,
ReceptorSubtype::Ht2c => &self.ht2c,
ReceptorSubtype::NicotinicA4b2 => &self.nicotinic_a4b2,
ReceptorSubtype::NicotinicA7 => &self.nicotinic_a7,
ReceptorSubtype::H1 => &self.h1,
}
}
#[inline]
pub fn get_mut(&mut self, subtype: ReceptorSubtype) -> &mut ReceptorState {
match subtype {
ReceptorSubtype::Ht1a => &mut self.ht1a,
ReceptorSubtype::Ht2a => &mut self.ht2a,
ReceptorSubtype::D1 => &mut self.d1,
ReceptorSubtype::D2 => &mut self.d2,
ReceptorSubtype::Alpha1 => &mut self.alpha1,
ReceptorSubtype::Alpha2 => &mut self.alpha2,
ReceptorSubtype::Beta => &mut self.beta,
ReceptorSubtype::GabaA => &mut self.gaba_a,
ReceptorSubtype::GabaB => &mut self.gaba_b,
ReceptorSubtype::Cb1 => &mut self.cb1,
ReceptorSubtype::MuOpioid => &mut self.mu_opioid,
ReceptorSubtype::Nmda => &mut self.nmda,
ReceptorSubtype::Ampa => &mut self.ampa,
ReceptorSubtype::Ox1 => &mut self.ox1,
ReceptorSubtype::Ox2 => &mut self.ox2,
ReceptorSubtype::Ht2c => &mut self.ht2c,
ReceptorSubtype::NicotinicA4b2 => &mut self.nicotinic_a4b2,
ReceptorSubtype::NicotinicA7 => &mut self.nicotinic_a7,
ReceptorSubtype::H1 => &mut self.h1,
}
}
#[inline]
pub fn tick_all(
&mut self,
occupancies: &ReceptorOccupancies,
dt: f32,
) -> Result<(), MastishkError> {
validate_dt(dt)?;
self.ht1a.tick_unchecked(occupancies.ht1a, dt);
self.ht2a.tick_unchecked(occupancies.ht2a, dt);
self.d1.tick_unchecked(occupancies.d1, dt);
self.d2.tick_unchecked(occupancies.d2, dt);
self.alpha1.tick_unchecked(occupancies.alpha1, dt);
self.alpha2.tick_unchecked(occupancies.alpha2, dt);
self.beta.tick_unchecked(occupancies.beta, dt);
self.gaba_a.tick_unchecked(occupancies.gaba_a, dt);
self.gaba_b.tick_unchecked(occupancies.gaba_b, dt);
self.cb1.tick_unchecked(occupancies.cb1, dt);
self.mu_opioid.tick_unchecked(occupancies.mu_opioid, dt);
self.nmda.tick_unchecked(occupancies.nmda, dt);
self.ampa.tick_unchecked(occupancies.ampa, dt);
self.ox1.tick_unchecked(occupancies.ox1, dt);
self.ox2.tick_unchecked(occupancies.ox2, dt);
self.ht2c.tick_unchecked(occupancies.ht2c, dt);
self.nicotinic_a4b2
.tick_unchecked(occupancies.nicotinic_a4b2, dt);
self.nicotinic_a7
.tick_unchecked(occupancies.nicotinic_a7, dt);
self.h1.tick_unchecked(occupancies.h1, dt);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_receptor_state_at_baseline() {
let r = ReceptorState::new(1.0, 604_800.0, 0.000_002, 0.000_001);
assert!((r.availability - 1.0).abs() < f32::EPSILON);
assert!((r.occupancy_ema - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_desensitization_under_chronic_agonist() {
let mut r = ReceptorState::new(1.0, 259_200.0, 0.000_01, 0.000_001);
for _ in 0..86400 {
r.tick_unchecked(0.8, 1.0);
}
assert!(r.availability < 1.0, "avail={}", r.availability);
}
#[test]
fn test_upregulation_on_withdrawal() {
let mut r = ReceptorState::new(1.0, 259_200.0, 0.000_01, 0.000_005);
for _ in 0..86400 {
r.tick_unchecked(0.9, 1.0);
}
let desensitized = r.availability;
assert!(desensitized < 1.0);
for _ in 0..172800 {
r.tick_unchecked(0.0, 1.0);
}
assert!(r.availability > desensitized);
}
#[test]
fn test_availability_clamped() {
let mut r = ReceptorState::new(1.0, 100.0, 0.0, 0.1);
for _ in 0..1000 {
r.tick_unchecked(0.0, 1.0);
}
assert!(r.availability <= 1.5);
}
#[test]
fn test_negative_dt_rejected() {
let mut r = ReceptorState::new(1.0, 604_800.0, 0.000_002, 0.000_001);
assert!(r.tick(0.5, -1.0).is_err());
}
#[test]
fn test_receptor_map_default_all_available() {
let map = ReceptorMap::default();
assert!((map.ht1a.availability - 1.0).abs() < f32::EPSILON);
assert!((map.d2.availability - 1.0).abs() < f32::EPSILON);
assert!((map.gaba_a.availability - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_receptor_map_get() {
let map = ReceptorMap::default();
assert!((map.get(ReceptorSubtype::Ht1a).availability - 1.0).abs() < f32::EPSILON);
assert!((map.get(ReceptorSubtype::GabaA).tau_turnover - 518_400.0).abs() < f32::EPSILON);
}
#[test]
fn test_receptor_map_tick_all() {
let mut map = ReceptorMap::default();
let occ = ReceptorOccupancies {
gaba_a: 0.8,
..Default::default()
};
for _ in 0..86400 {
map.tick_all(&occ, 1.0).unwrap();
}
assert!(map.gaba_a.availability < 1.0);
assert!((map.ht1a.availability - 1.0).abs() < 0.01);
}
#[test]
fn test_occupancies_add_clamps() {
let mut occ = ReceptorOccupancies::default();
occ.add(ReceptorSubtype::GabaA, 0.6);
occ.add(ReceptorSubtype::GabaA, 0.6); assert!((occ.gaba_a - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_upregulation_does_not_apply_when_above_baseline() {
let mut r = ReceptorState::new(1.0, 259_200.0, 0.0, 0.01);
r.availability = 1.3; r.tick_unchecked(0.0, 86400.0); assert!(r.availability < 1.3);
assert!(r.availability >= 1.0); }
#[test]
fn test_zero_occupancy_maintains_baseline() {
let mut r = ReceptorState::new(1.0, 604_800.0, 0.000_002, 0.000_001);
for _ in 0..1000 {
r.tick_unchecked(0.0, 1.0);
}
assert!((r.availability - 1.0).abs() < 0.01);
}
#[test]
fn test_serde_roundtrip() {
let map = ReceptorMap::default();
let json = serde_json::to_string(&map).unwrap();
let map2: ReceptorMap = serde_json::from_str(&json).unwrap();
assert!((map2.ht1a.availability - map.ht1a.availability).abs() < f32::EPSILON);
}
}