use serde::{Deserialize, Serialize};
use crate::chronobiology::CircadianState;
use crate::circuit::Circuit;
use crate::coupling::{
CouplingParams, apply_amygdala_hpa_coupling, apply_arousal_circuit_coupling,
apply_circadian_hpa_coupling, apply_dmn_hpa_coupling, apply_nt_amygdala_coupling,
apply_nt_basal_ganglia_coupling, apply_nt_cerebellum_coupling, apply_nt_hippocampus_coupling,
apply_nt_pfc_coupling, apply_sleep_neurotransmitter_coupling, composite_arousal,
composite_stress,
};
use crate::dmn::DmnState;
use crate::error::{MastishkError, validate_dt};
use crate::hpa::HpaState;
use crate::neurotransmitter::NeurotransmitterProfile;
use crate::pharmacology::{DrugProfile, PharmacologyState};
use crate::regions::{
AccState, AmygdalaState, BasalGangliaState, CerebellumState, HippocampusState, InsulaState,
LocusCoeruleusState, PfcState, RapheState, RewardCircuitState,
};
use crate::sleep::SleepState;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BrainState {
pub neurotransmitter: NeurotransmitterProfile,
pub circuit: Circuit,
pub sleep: SleepState,
pub hpa: HpaState,
pub dmn: DmnState,
pub circadian: CircadianState,
pub coupling: CouplingParams,
#[serde(default)]
pub pharmacology: PharmacologyState,
#[serde(default)]
pub pfc: PfcState,
#[serde(default)]
pub amygdala: AmygdalaState,
#[serde(default)]
pub hippocampus: HippocampusState,
#[serde(default)]
pub basal_ganglia: BasalGangliaState,
#[serde(default)]
pub cerebellum: CerebellumState,
#[serde(default)]
pub reward_circuit: RewardCircuitState,
#[serde(default)]
pub td_learner: TdLearner,
#[serde(default)]
pub opponent_process: OpponentProcess,
#[serde(default)]
pub acc: AccState,
#[serde(default)]
pub insula: InsulaState,
#[serde(default)]
pub locus_coeruleus: LocusCoeruleusState,
#[serde(default)]
pub raphe: RapheState,
#[serde(default)]
pub hormones: SexHormoneState,
#[serde(default)]
pub inflammation: crate::inflammation::InflammationState,
#[serde(default)]
pub gut_brain: crate::gut_brain::GutBrainState,
#[serde(default)]
pub autonomic: crate::autonomic::AutonomicState,
#[serde(default)]
pub interoception: InteroceptiveState,
#[serde(default)]
pub eeg: crate::eeg::EegState,
#[serde(default)]
pub age: AgeProfile,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SexHormoneState {
pub estradiol: f32,
pub testosterone: f32,
}
impl Default for SexHormoneState {
fn default() -> Self {
Self {
estradiol: 0.5,
testosterone: 0.5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InteroceptiveState {
pub body_prediction: f32,
pub prediction_error: f32,
pub interoceptive_accuracy: f32,
}
impl Default for InteroceptiveState {
fn default() -> Self {
Self {
body_prediction: 0.5,
prediction_error: 0.0,
interoceptive_accuracy: 0.5,
}
}
}
impl InteroceptiveState {
#[inline]
pub fn update_from_autonomic(&mut self, autonomic_balance: f32, dt: f32) {
let actual = autonomic_balance;
let raw_error = (self.body_prediction - actual).abs() * self.interoceptive_accuracy;
let alpha = 1.0 - (-0.2 * dt).exp();
self.prediction_error += (raw_error - self.prediction_error) * alpha;
self.prediction_error = self.prediction_error.clamp(0.0, 1.0);
let learn_alpha = 1.0 - (-0.05 * dt).exp();
self.body_prediction += (actual - self.body_prediction) * learn_alpha;
}
#[inline]
#[must_use]
pub fn anxiety_contribution(&self) -> f32 {
(self.prediction_error * 1.5).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgeProfile {
pub age_years: f32,
}
impl Default for AgeProfile {
fn default() -> Self {
Self { age_years: 30.0 }
}
}
impl AgeProfile {
#[inline]
#[must_use]
pub fn pfc_maturation(&self) -> f32 {
let rising = 1.0 / (1.0 + (-0.3 * (self.age_years - 18.0)).exp());
let decline = if self.age_years > 60.0 {
1.0 - (self.age_years - 60.0) * 0.005
} else {
1.0
};
(rising * decline).clamp(0.0, 1.0)
}
#[inline]
#[must_use]
pub fn dopamine_capacity(&self) -> f32 {
if self.age_years <= 40.0 {
1.0
} else {
(1.0 - 0.01 * (self.age_years - 40.0)).clamp(0.5, 1.0)
}
}
#[inline]
#[must_use]
pub fn deep_sleep_capacity(&self) -> f32 {
(1.0 - 0.008 * (self.age_years - 20.0).max(0.0)).clamp(0.3, 1.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TdLearner {
pub value_estimate: f32,
pub learning_rate: f32,
pub discount: f32,
}
impl Default for TdLearner {
fn default() -> Self {
Self {
value_estimate: 0.3,
learning_rate: 0.1,
discount: 0.95,
}
}
}
impl TdLearner {
#[inline]
pub fn update(&mut self, dopamine_phasic: f32) {
self.value_estimate += self.learning_rate * dopamine_phasic;
self.value_estimate = self.value_estimate.clamp(0.0, 1.0);
tracing::trace!(
value = self.value_estimate,
rpe = dopamine_phasic,
"TD update"
);
}
#[inline]
#[must_use]
pub fn td_error(&self, reward: f32, next_value: f32) -> f32 {
(reward + self.discount * next_value - self.value_estimate).clamp(-1.0, 1.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpponentProcess {
pub a_process: f32,
pub b_process: f32,
pub b_strength: f32,
}
impl Default for OpponentProcess {
fn default() -> Self {
Self {
a_process: 0.0,
b_process: 0.0,
b_strength: 0.1,
}
}
}
impl OpponentProcess {
#[inline]
pub fn tick(&mut self, dt: f32) {
self.a_process *= (-dt / 0.5).exp(); self.b_process *= (-dt / 5.0).exp(); }
#[inline]
pub fn trigger(&mut self, magnitude: f32) {
self.a_process = (self.a_process + magnitude).min(1.0);
self.b_process = (self.b_process + magnitude * self.b_strength).min(1.0);
self.b_strength = (self.b_strength + magnitude * 0.01).min(1.0);
}
#[inline]
#[must_use]
pub fn net_hedonic(&self) -> f32 {
(self.a_process - self.b_process).clamp(-1.0, 1.0)
}
}
impl BrainState {
#[inline]
pub fn tick(&mut self, dt: f32) -> Result<(), MastishkError> {
validate_dt(dt)?;
let dt_hours = dt / 3600.0;
tracing::trace!(dt, dt_hours, "brain state tick");
self.circadian.tick(dt_hours)?;
apply_circadian_hpa_coupling(
&self.circadian,
&mut self.hpa,
self.coupling.circadian_hpa_smoothing,
dt,
)?;
apply_sleep_neurotransmitter_coupling(
self.sleep.stage,
&mut self.neurotransmitter,
self.coupling.sleep_nt_smoothing,
dt,
)?;
self.pharmacology.tick(dt, &mut self.neurotransmitter)?;
self.locus_coeruleus.tick(dt)?;
self.neurotransmitter.norepinephrine.baseline =
self.locus_coeruleus.ne_output().clamp(0.1, 0.9);
self.raphe.tick(self.neurotransmitter.serotonin.level, dt)?;
{
let raphe_mod = self.raphe.serotonin_synthesis_modifier();
let alpha = 1.0 - (-0.02 * dt).exp();
self.neurotransmitter.serotonin.synthesis_rate *= 1.0 + (raphe_mod - 1.0) * alpha;
self.neurotransmitter.serotonin.synthesis_rate = self
.neurotransmitter
.serotonin
.synthesis_rate
.clamp(0.005, 0.1);
}
self.neurotransmitter.tick_all(dt)?;
{
let alpha = 1.0 - (-0.01 * dt).exp();
let estradiol_boost = (self.hormones.estradiol - 0.5) * 0.02;
self.neurotransmitter.serotonin.synthesis_rate += estradiol_boost * alpha;
self.neurotransmitter.serotonin.synthesis_rate = self
.neurotransmitter
.serotonin
.synthesis_rate
.clamp(0.005, 0.1);
let testosterone_boost = (self.hormones.testosterone - 0.5) * 0.05;
self.amygdala.activation =
(self.amygdala.activation + testosterone_boost * alpha).clamp(0.0, 1.0);
}
apply_nt_amygdala_coupling(
&self.neurotransmitter,
&mut self.amygdala,
&self.pfc,
&self.coupling.region,
dt,
)?;
apply_nt_hippocampus_coupling(
&self.neurotransmitter,
&mut self.hippocampus,
&self.amygdala,
&self.sleep,
&self.coupling.region,
dt,
)?;
apply_nt_pfc_coupling(
&self.neurotransmitter,
&mut self.pfc,
&self.hpa,
&self.sleep,
&self.amygdala,
dt,
)?;
apply_amygdala_hpa_coupling(
&self.amygdala,
&mut self.hpa,
self.coupling.region.amygdala_hpa_gain,
dt,
)?;
apply_dmn_hpa_coupling(&self.dmn, &mut self.hpa, &self.coupling, dt)?;
if self.inflammation.cytokine_level > 0.1 {
self.hpa.stress(self.inflammation.cytokine_level * 0.2 * dt);
}
self.hpa.tick(dt)?;
{
let dampen = self.gut_brain.microbiome_diversity * 0.1;
let alpha = 1.0 - (-0.02 * dt).exp();
self.inflammation.microglial_activation =
(self.inflammation.microglial_activation - dampen * alpha).max(0.0);
}
{
let depletion = self.inflammation.tryptophan_depletion();
let alpha = 1.0 - (-0.05 * dt).exp();
self.neurotransmitter.serotonin.synthesis_rate *= 1.0 - depletion * 0.3 * alpha;
self.neurotransmitter.serotonin.synthesis_rate = self
.neurotransmitter
.serotonin
.synthesis_rate
.clamp(0.005, 0.1);
let fatigue = self.inflammation.sickness_behavior * 0.15;
self.neurotransmitter.dopamine.baseline =
(self.neurotransmitter.dopamine.baseline - fatigue * alpha).max(0.1);
}
self.inflammation.tick(dt)?;
{
let modifier = self.gut_brain.central_serotonin_modifier();
let alpha = 1.0 - (-0.01 * dt).exp();
self.neurotransmitter.serotonin.synthesis_rate *= 1.0 + (modifier - 1.0) * alpha;
self.neurotransmitter.serotonin.synthesis_rate = self
.neurotransmitter
.serotonin
.synthesis_rate
.clamp(0.005, 0.1);
}
self.gut_brain.tick(dt)?;
apply_nt_basal_ganglia_coupling(
&self.neurotransmitter,
&mut self.basal_ganglia,
&self.pfc,
&self.hippocampus,
&self.coupling.region,
dt,
)?;
apply_nt_cerebellum_coupling(
&self.neurotransmitter,
&mut self.cerebellum,
&self.sleep,
dt,
)?;
self.amygdala.tick(dt)?;
self.hippocampus.tick(dt)?;
self.pfc.tick(dt)?;
self.basal_ganglia.tick(dt)?;
self.cerebellum.tick(dt)?;
self.reward_circuit.tick(dt)?;
self.td_learner
.update(self.neurotransmitter.dopamine_phasic);
self.opponent_process.tick(dt);
self.acc.tick(dt)?;
self.insula.update_from_body(
self.autonomic.sympathetic,
self.inflammation.sickness_behavior,
dt,
);
self.insula.tick(dt)?;
{
let alpha = 1.0 - (-0.1 * dt).exp();
let sym_drive = self.neurotransmitter.norepinephrine.level * 0.3
+ self.hpa.cortisol * 0.2
+ self.amygdala.threat_response() * 0.25;
self.autonomic.sympathetic = (self.autonomic.sympathetic + sym_drive * alpha).min(1.0);
let para_drive = self.gut_brain.vagal_tone * 0.3;
self.autonomic.parasympathetic =
(self.autonomic.parasympathetic + para_drive * alpha).min(1.0);
}
self.autonomic.tick(dt)?;
self.interoception
.update_from_autonomic(self.autonomic.balance(), dt);
apply_arousal_circuit_coupling(
&self.neurotransmitter,
&mut self.circuit,
&self.coupling.circuit_gain,
self.pharmacology.gaba_pam_multiplier(),
dt,
)?;
self.sleep.tick_adenosine(dt_hours)?;
self.sleep.tick_stage_transitions(dt_hours);
{
use crate::eeg::EegState;
use crate::sleep::SleepStage;
let target = match self.sleep.stage {
SleepStage::Nrem3 => EegState {
delta: 0.8,
theta: 0.1,
alpha: 0.02,
beta: 0.02,
gamma: 0.01,
},
SleepStage::Nrem2 => EegState {
delta: 0.4,
theta: 0.3,
alpha: 0.1,
beta: 0.05,
gamma: 0.02,
},
SleepStage::Nrem1 => EegState {
delta: 0.2,
theta: 0.4,
alpha: 0.2,
beta: 0.1,
gamma: 0.05,
},
SleepStage::Rem => EegState {
delta: 0.1,
theta: 0.3,
alpha: 0.1,
beta: 0.3,
gamma: 0.2,
},
SleepStage::Wake => {
let focus = self.pfc.executive_control;
let meditation = self.dmn.meditation_depth;
EegState {
delta: 0.05,
theta: 0.1 + meditation * 0.3,
alpha: 0.4 * (1.0 - focus) + meditation * 0.2,
beta: 0.3 * focus + 0.1,
gamma: 0.1 + self.amygdala.activation * 0.2,
}
}
};
self.eeg.tick_toward(&target, dt)?;
}
{
let modifier = self.circadian.serotonin_photoperiod_modifier();
let alpha = 1.0 - (-0.001 * dt).exp(); self.neurotransmitter.serotonin.synthesis_rate *= 1.0 + (modifier - 1.0) * alpha;
self.neurotransmitter.serotonin.synthesis_rate = self
.neurotransmitter
.serotonin
.synthesis_rate
.clamp(0.005, 0.1);
}
{
let pfc_factor = self.age.pfc_maturation();
self.pfc.working_memory_capacity *= pfc_factor;
self.pfc.working_memory_capacity = self.pfc.working_memory_capacity.clamp(0.2, 1.0);
}
Ok(())
}
pub fn administer_drug(&mut self, profile: DrugProfile, dose: f32) {
self.pharmacology.administer(profile, dose);
}
#[inline]
#[must_use]
pub fn arousal(&self) -> f32 {
composite_arousal(&self.neurotransmitter, &self.circadian, &self.sleep)
}
#[inline]
#[must_use]
pub fn stress(&self) -> f32 {
composite_stress(&self.hpa, &self.dmn, &self.sleep)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::circuit::NeuralPopulation;
use crate::sleep::SleepStage;
fn brain_with_circuit() -> BrainState {
let mut brain = BrainState::default();
let a = brain
.circuit
.add_population(NeuralPopulation::new("excitatory", 0.5, 0.1, true));
let b = brain
.circuit
.add_population(NeuralPopulation::new("inhibitory", 0.2, 0.2, false));
brain.circuit.add_synapse(a, b, 0.5).unwrap();
brain.circuit.add_synapse(b, a, -0.3).unwrap();
brain
}
#[test]
fn test_default_creates_valid_state() {
let brain = BrainState::default();
assert!((0.0..=1.0).contains(&brain.arousal()));
assert!((0.0..=1.0).contains(&brain.stress()));
}
#[test]
fn test_tick_no_panic() {
let mut brain = brain_with_circuit();
brain.tick(1.0).unwrap();
brain.tick(0.016).unwrap(); brain.tick(60.0).unwrap(); }
#[test]
fn test_tick_negative_dt_rejected() {
let mut brain = BrainState::default();
assert!(brain.tick(-1.0).is_err());
}
#[test]
fn test_tick_zero_dt() {
let mut brain = brain_with_circuit();
brain.tick(0.0).unwrap(); }
#[test]
fn test_serde_roundtrip() {
let brain = brain_with_circuit();
let json = serde_json::to_string(&brain).unwrap();
let brain2: BrainState = serde_json::from_str(&json).unwrap();
assert!(
(brain2.neurotransmitter.serotonin.level - brain.neurotransmitter.serotonin.level)
.abs()
< f32::EPSILON
);
assert_eq!(brain2.circuit.populations.len(), 2);
}
#[test]
fn test_circadian_drives_hpa_baseline() {
let mut brain = BrainState::default();
brain.circadian.phase_hours = 8.0;
let initial_baseline = brain.hpa.cortisol_baseline;
for _ in 0..100 {
brain.tick(1.0).unwrap();
}
let distance_now = (brain.hpa.cortisol_baseline - brain.circadian.cortisol_circadian).abs();
let distance_initial = (initial_baseline - brain.circadian.cortisol_circadian).abs();
assert!(distance_now < distance_initial);
}
#[test]
fn test_sleep_rem_drives_ach_baseline() {
let mut brain = BrainState::default();
brain.sleep.stage = SleepStage::Rem;
for _ in 0..100 {
brain.tick(1.0).unwrap();
}
assert!(brain.neurotransmitter.acetylcholine.baseline > 0.6);
assert!(brain.neurotransmitter.serotonin.baseline < 0.3);
}
#[test]
fn test_rumination_drives_cortisol() {
let mut brain = BrainState::default();
brain.dmn.rumination = 0.8; let initial_cortisol = brain.hpa.cortisol;
for _ in 0..50 {
brain.tick(1.0).unwrap();
}
assert!(brain.hpa.cortisol > initial_cortisol);
}
#[test]
fn test_full_day_cycle() {
let mut brain = brain_with_circuit();
brain.circadian.phase_hours = 0.0;
for _ in 0..1440 {
brain.tick(60.0).unwrap();
}
assert!(brain.circadian.phase_hours < 1.0 || brain.circadian.phase_hours > 23.0);
assert!((0.0..=1.0).contains(&brain.arousal()));
assert!((0.0..=1.0).contains(&brain.stress()));
}
#[test]
fn test_arousal_drops_during_sleep() {
let mut brain = BrainState::default();
let awake_arousal = brain.arousal();
brain.sleep.stage = SleepStage::Nrem3;
let asleep_arousal = brain.arousal();
assert!(asleep_arousal < awake_arousal);
}
#[test]
fn test_all_values_finite_after_extended_simulation() {
let mut brain = brain_with_circuit();
brain.administer_drug(crate::pharmacology::DrugProfile::ssri_fluoxetine(), 0.5);
brain.dmn.rumination = 0.6;
brain.sleep.stage = SleepStage::Rem;
for _ in 0..3600 {
brain.tick(1.0).unwrap();
}
assert!(brain.neurotransmitter.serotonin.level.is_finite());
assert!(brain.neurotransmitter.dopamine.level.is_finite());
assert!(brain.hpa.cortisol.is_finite());
assert!(brain.hpa.allostatic_load.is_finite());
assert!(brain.circadian.melatonin.is_finite());
assert!(brain.arousal().is_finite());
assert!(brain.stress().is_finite());
assert!(brain.pharmacology.gaba_pam_multiplier().is_finite());
}
}