use crate::error::{MastishkError, validate_dt};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransmitterState {
pub level: f32,
pub baseline: f32,
pub synthesis_rate: f32,
pub clearance_rate: f32,
}
impl TransmitterState {
#[must_use]
pub fn at_baseline(baseline: f32, synthesis_rate: f32, clearance_rate: f32) -> Self {
Self {
level: baseline,
baseline,
synthesis_rate,
clearance_rate,
}
}
#[inline]
pub fn stimulate(&mut self, delta: f32) {
self.level = (self.level + delta).clamp(0.0, 1.0);
tracing::debug!(delta, level = self.level, "transmitter stimulated");
}
#[inline]
pub fn tick(&mut self, dt: f32) -> Result<(), MastishkError> {
validate_dt(dt)?;
self.tick_unchecked(dt);
Ok(())
}
#[inline]
pub(crate) fn tick_unchecked(&mut self, dt: f32) {
let diff = self.baseline - self.level;
let rate = if diff > 0.0 {
self.synthesis_rate
} else {
self.clearance_rate
};
self.level += diff * (1.0 - (-rate * dt).exp());
self.level = self.level.clamp(0.0, 1.0);
}
#[inline]
#[must_use]
pub fn deviation(&self) -> f32 {
self.level - self.baseline
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NeurotransmitterProfile {
pub serotonin: TransmitterState,
pub dopamine: TransmitterState,
pub dopamine_phasic: f32,
pub norepinephrine: TransmitterState,
pub gaba: TransmitterState,
pub glutamate: TransmitterState,
pub oxytocin: TransmitterState,
pub endorphins: TransmitterState,
pub acetylcholine: TransmitterState,
pub bdnf: TransmitterState,
#[serde(default = "default_histamine")]
pub histamine: TransmitterState,
#[serde(default = "default_endocannabinoid")]
pub endocannabinoid: TransmitterState,
#[serde(default = "default_orexin")]
pub orexin: TransmitterState,
}
fn default_histamine() -> TransmitterState {
TransmitterState::at_baseline(0.6, 0.05, 0.06)
}
fn default_endocannabinoid() -> TransmitterState {
TransmitterState::at_baseline(0.4, 0.01, 0.02)
}
fn default_orexin() -> TransmitterState {
TransmitterState::at_baseline(0.6, 0.04, 0.05)
}
impl Default for NeurotransmitterProfile {
fn default() -> Self {
Self {
serotonin: TransmitterState::at_baseline(0.5, 0.02, 0.03),
dopamine: TransmitterState::at_baseline(0.4, 0.03, 0.05),
dopamine_phasic: 0.0,
norepinephrine: TransmitterState::at_baseline(0.3, 0.04, 0.06),
gaba: TransmitterState::at_baseline(0.5, 0.03, 0.03),
glutamate: TransmitterState::at_baseline(0.5, 0.04, 0.04),
oxytocin: TransmitterState::at_baseline(0.3, 0.01, 0.02),
endorphins: TransmitterState::at_baseline(0.2, 0.01, 0.03),
acetylcholine: TransmitterState::at_baseline(0.4, 0.03, 0.04),
bdnf: TransmitterState::at_baseline(0.5, 0.005, 0.005),
histamine: default_histamine(),
endocannabinoid: default_endocannabinoid(),
orexin: default_orexin(),
}
}
}
impl NeurotransmitterProfile {
#[inline]
pub fn tick_all(&mut self, dt: f32) -> Result<(), MastishkError> {
validate_dt(dt)?;
tracing::trace!(dt, "ticking all neurotransmitters");
self.serotonin.tick_unchecked(dt);
self.dopamine.tick_unchecked(dt);
self.norepinephrine.tick_unchecked(dt);
self.gaba.tick_unchecked(dt);
self.glutamate.tick_unchecked(dt);
self.oxytocin.tick_unchecked(dt);
self.endorphins.tick_unchecked(dt);
self.acetylcholine.tick_unchecked(dt);
self.bdnf.tick_unchecked(dt);
self.histamine.tick_unchecked(dt);
self.endocannabinoid.tick_unchecked(dt);
self.orexin.tick_unchecked(dt);
self.dopamine_phasic *= (-dt / 0.5).exp();
Ok(())
}
#[inline]
pub fn fire_dopamine_burst(&mut self, magnitude: f32) {
self.dopamine_phasic = (self.dopamine_phasic + magnitude).clamp(-1.0, 1.0);
tracing::debug!(
magnitude,
phasic = self.dopamine_phasic,
"dopamine burst fired"
);
}
#[inline]
#[must_use]
pub fn inhibition_ratio(&self) -> f32 {
if self.glutamate.level > f32::EPSILON {
(self.gaba.level / self.glutamate.level).min(100.0)
} else {
100.0
}
}
#[inline]
#[must_use]
pub fn arousal(&self) -> f32 {
((self.norepinephrine.level + self.glutamate.level - self.gaba.level) / 2.0).clamp(0.0, 1.0)
}
#[inline]
#[must_use]
pub fn reward_sensitivity(&self) -> f32 {
self.dopamine.level
}
#[inline]
#[must_use]
pub fn plasticity_rate(&self) -> f32 {
self.bdnf.level
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transmitter_at_baseline() {
let t = TransmitterState::at_baseline(0.5, 0.02, 0.03);
assert!((t.level - 0.5).abs() < f32::EPSILON);
assert!((t.deviation()).abs() < f32::EPSILON);
}
#[test]
fn test_stimulate_clamps() {
let mut t = TransmitterState::at_baseline(0.5, 0.02, 0.03);
t.stimulate(0.8);
assert!((t.level - 1.0).abs() < f32::EPSILON);
t.stimulate(-2.0);
assert!(t.level >= 0.0);
}
#[test]
fn test_tick_toward_baseline() {
let mut t = TransmitterState::at_baseline(0.5, 0.1, 0.1);
t.level = 0.9;
t.tick(30.0).unwrap();
assert!((t.level - t.baseline).abs() < 0.1);
}
#[test]
fn test_profile_default() {
let p = NeurotransmitterProfile::default();
assert!(p.arousal() >= 0.0 && p.arousal() <= 1.0);
assert!(p.inhibition_ratio() > 0.0);
}
#[test]
fn test_tick_all() {
let mut p = NeurotransmitterProfile::default();
p.serotonin.stimulate(0.3);
p.tick_all(1.0).unwrap();
assert!(p.serotonin.level < 0.8);
}
#[test]
fn test_serde_roundtrip() {
let p = NeurotransmitterProfile::default();
let json = serde_json::to_string(&p).unwrap();
let p2: NeurotransmitterProfile = serde_json::from_str(&json).unwrap();
assert!((p2.serotonin.level - p.serotonin.level).abs() < f32::EPSILON);
}
#[test]
fn test_negative_dt_rejected() {
let mut t = TransmitterState::at_baseline(0.5, 0.1, 0.1);
assert!(t.tick(-1.0).is_err());
let mut p = NeurotransmitterProfile::default();
assert!(p.tick_all(-0.5).is_err());
}
#[test]
fn test_zero_dt_is_noop() {
let mut t = TransmitterState::at_baseline(0.5, 0.1, 0.1);
t.level = 0.8;
t.tick(0.0).unwrap();
assert!((t.level - 0.8).abs() < f32::EPSILON);
}
#[test]
fn test_reward_sensitivity() {
let mut p = NeurotransmitterProfile::default();
let base = p.reward_sensitivity();
p.dopamine.stimulate(0.3);
assert!(p.reward_sensitivity() > base);
}
#[test]
fn test_plasticity_rate() {
let mut p = NeurotransmitterProfile::default();
let base = p.plasticity_rate();
p.bdnf.stimulate(0.2);
assert!(p.plasticity_rate() > base);
}
#[test]
fn test_inhibition_ratio_zero_glutamate() {
let mut p = NeurotransmitterProfile::default();
p.glutamate.level = 0.0;
assert!((p.inhibition_ratio() - 100.0).abs() < f32::EPSILON);
}
#[test]
fn test_arousal_boundary_clamp() {
let mut p = NeurotransmitterProfile::default();
p.norepinephrine.level = 1.0;
p.glutamate.level = 1.0;
p.gaba.level = 0.0;
assert!(p.arousal() <= 1.0);
p.norepinephrine.level = 0.0;
p.glutamate.level = 0.0;
p.gaba.level = 1.0;
assert!(p.arousal() >= 0.0);
}
}