use serde::{Deserialize, Serialize};
use crate::error::{MastishkError, validate_dt};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutonomicState {
pub sympathetic: f32,
pub parasympathetic: f32,
pub hrv: f32,
}
impl Default for AutonomicState {
fn default() -> Self {
Self {
sympathetic: 0.3,
parasympathetic: 0.5,
hrv: 0.6,
}
}
}
impl AutonomicState {
#[inline]
pub fn tick(&mut self, dt: f32) -> Result<(), MastishkError> {
validate_dt(dt)?;
let alpha = 1.0 - (-0.08 * dt).exp();
let sym_target = (0.3 - self.parasympathetic * 0.2).max(0.05);
let para_target = (0.5 - self.sympathetic * 0.3).max(0.05);
self.sympathetic += (sym_target - self.sympathetic) * alpha;
self.parasympathetic += (para_target - self.parasympathetic) * alpha;
self.sympathetic = self.sympathetic.clamp(0.0, 1.0);
self.parasympathetic = self.parasympathetic.clamp(0.0, 1.0);
self.hrv = (self.parasympathetic * 0.7 + (1.0 - self.sympathetic) * 0.3).clamp(0.0, 1.0);
tracing::trace!(
sym = self.sympathetic,
para = self.parasympathetic,
hrv = self.hrv,
"autonomic tick"
);
Ok(())
}
#[inline]
#[must_use]
pub fn balance(&self) -> f32 {
let total = self.sympathetic + self.parasympathetic;
if total > f32::EPSILON {
self.parasympathetic / total
} else {
0.5
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_parasympathetic_dominant() {
let s = AutonomicState::default();
assert!(s.balance() > 0.5);
}
#[test]
fn test_hrv_reflects_balance() {
let mut s = AutonomicState {
sympathetic: 0.9,
parasympathetic: 0.1,
..Default::default()
};
s.tick(0.0).unwrap();
assert!(s.hrv < 0.4);
}
#[test]
fn test_serde_roundtrip() {
let s = AutonomicState::default();
let json = serde_json::to_string(&s).unwrap();
let s2: AutonomicState = serde_json::from_str(&json).unwrap();
assert!((s2.sympathetic - s.sympathetic).abs() < f32::EPSILON);
}
}