use crate::types::world::{GainMode, LearnedState};
const GAIN_EMA_RATE: f64 = 0.35;
const PHASIC_THRESHOLD: f64 = 0.7;
const TONIC_THRESHOLD: f64 = 0.3;
const AROUSAL_PHASIC_THRESHOLD: f64 = 0.35;
#[derive(Debug, Clone)]
pub struct LocusCoeruleus {
pub tick: u32,
gain_mode: GainMode,
gain_smoothed_confidence: f64,
last_arousal: f64,
}
impl LocusCoeruleus {
pub fn new() -> Self {
Self {
tick: 0,
gain_mode: GainMode::Neutral,
gain_smoothed_confidence: 0.5,
last_arousal: 0.0,
}
}
pub fn gain_mode(&self) -> GainMode {
self.gain_mode
}
pub fn set_arousal(&mut self, arousal: f64) {
self.last_arousal = arousal;
if arousal >= AROUSAL_PHASIC_THRESHOLD {
self.gain_mode = GainMode::Phasic;
}
}
pub fn nudge_gain_from_confidence(&mut self, gate_confidence: f64) {
self.tick += 1;
self.gain_smoothed_confidence =
(1.0 - GAIN_EMA_RATE) * self.gain_smoothed_confidence + GAIN_EMA_RATE * gate_confidence;
if gate_confidence < 0.4
&& self.gain_mode != GainMode::Tonic
&& self.gain_smoothed_confidence <= TONIC_THRESHOLD
{
self.gain_mode = GainMode::Tonic;
} else if gate_confidence > 0.85
&& self.gain_mode != GainMode::Phasic
&& self.gain_smoothed_confidence >= PHASIC_THRESHOLD
{
self.gain_mode = GainMode::Phasic;
}
}
pub fn sync_from_learned(&mut self, learned: &LearnedState) {
self.gain_mode = learned.gain_mode;
self.tick = learned.tick;
}
pub fn sync_to_learned(&self, learned: &mut LearnedState) {
learned.gain_mode = self.gain_mode;
learned.tick = self.tick;
}
}
impl Default for LocusCoeruleus {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_state_is_neutral() {
let lc = LocusCoeruleus::new();
assert_eq!(lc.gain_mode(), GainMode::Neutral);
assert_eq!(lc.tick, 0);
}
#[test]
fn high_arousal_triggers_phasic() {
let mut lc = LocusCoeruleus::new();
lc.set_arousal(0.5);
assert_eq!(lc.gain_mode(), GainMode::Phasic);
}
#[test]
fn low_arousal_leaves_neutral() {
let mut lc = LocusCoeruleus::new();
lc.set_arousal(0.1);
assert_eq!(lc.gain_mode(), GainMode::Neutral);
}
#[test]
fn sustained_low_confidence_drifts_tonic() {
let mut lc = LocusCoeruleus::new();
for _ in 0..20 {
lc.nudge_gain_from_confidence(0.1);
}
assert_eq!(lc.gain_mode(), GainMode::Tonic);
}
#[test]
fn sustained_high_confidence_drifts_phasic() {
let mut lc = LocusCoeruleus::new();
for _ in 0..20 {
lc.nudge_gain_from_confidence(0.9);
}
assert_eq!(lc.gain_mode(), GainMode::Phasic);
}
#[test]
fn sync_roundtrip() {
let mut lc = LocusCoeruleus::new();
lc.set_arousal(0.5); lc.tick = 7;
let mut learned = LearnedState::default();
lc.sync_to_learned(&mut learned);
let mut lc2 = LocusCoeruleus::new();
lc2.sync_from_learned(&learned);
assert_eq!(lc2.gain_mode(), GainMode::Phasic);
assert_eq!(lc2.tick, 7);
}
}