use std::collections::{HashMap, VecDeque};
use chrono::Utc;
use crate::interoceptive::types::{
AdaptiveBaseline, DeviationLevel, DomainState, InteroceptiveSignal,
InteroceptiveState, SomaticMarker,
};
const DEFAULT_BUFFER_CAPACITY: usize = 1000;
const DEFAULT_MARKER_CACHE_SIZE: usize = 256;
const DEFAULT_ALPHA: f64 = 0.3;
type BaselineKey = (String, String);
const DEFAULT_BASELINE_MIN_SAMPLES: u64 = 20;
pub struct InteroceptiveHub {
domain_states: HashMap<String, DomainState>,
signal_buffer: VecDeque<InteroceptiveSignal>,
buffer_capacity: usize,
somatic_cache: HashMap<u64, SomaticMarker>,
marker_cache_size: usize,
alpha: f64,
baselines: HashMap<BaselineKey, AdaptiveBaseline>,
baseline_min_samples: u64,
}
impl Default for InteroceptiveHub {
fn default() -> Self {
Self::new()
}
}
impl InteroceptiveHub {
pub fn new() -> Self {
Self {
domain_states: HashMap::new(),
signal_buffer: VecDeque::with_capacity(DEFAULT_BUFFER_CAPACITY),
buffer_capacity: DEFAULT_BUFFER_CAPACITY,
somatic_cache: HashMap::new(),
marker_cache_size: DEFAULT_MARKER_CACHE_SIZE,
alpha: DEFAULT_ALPHA,
baselines: HashMap::new(),
baseline_min_samples: DEFAULT_BASELINE_MIN_SAMPLES,
}
}
pub fn with_capacity(
buffer_capacity: usize,
marker_cache_size: usize,
alpha: f64,
) -> Self {
Self {
domain_states: HashMap::new(),
signal_buffer: VecDeque::with_capacity(buffer_capacity.min(4096)),
buffer_capacity: buffer_capacity.max(1),
somatic_cache: HashMap::new(),
marker_cache_size: marker_cache_size.max(1),
alpha: alpha.clamp(0.01, 0.99),
baselines: HashMap::new(),
baseline_min_samples: DEFAULT_BASELINE_MIN_SAMPLES,
}
}
pub fn process_signal(&mut self, signal: InteroceptiveSignal) -> bool {
let notable = signal.is_negative() && signal.is_urgent();
let domain_key = signal
.domain
.clone()
.unwrap_or_else(|| "_global".to_string());
let ds = self
.domain_states
.entry(domain_key.clone())
.or_insert_with_key(|k| DomainState::new(k));
ds.update(&signal, self.alpha);
let baseline_key = (signal.source.to_string(), domain_key);
let baseline = self
.baselines
.entry(baseline_key)
.or_insert_with(|| AdaptiveBaseline::new(self.baseline_min_samples));
baseline.observe(signal.valence);
if self.signal_buffer.len() >= self.buffer_capacity {
self.signal_buffer.pop_front();
}
self.signal_buffer.push_back(signal);
notable
}
pub fn process_batch(&mut self, signals: Vec<InteroceptiveSignal>) -> usize {
let mut notable_count = 0;
for signal in signals {
if self.process_signal(signal) {
notable_count += 1;
}
}
notable_count
}
pub fn somatic_lookup(&mut self, situation_hash: u64, current_valence: f64) -> &SomaticMarker {
if self.somatic_cache.contains_key(&situation_hash) {
let marker = self.somatic_cache.get_mut(&situation_hash).unwrap();
marker.update(current_valence);
} else {
if self.somatic_cache.len() >= self.marker_cache_size {
self.evict_lru_marker();
}
self.somatic_cache
.insert(situation_hash, SomaticMarker::new(situation_hash, current_valence));
}
self.somatic_cache.get(&situation_hash).unwrap()
}
fn evict_lru_marker(&mut self) {
if let Some((&lru_hash, _)) = self
.somatic_cache
.iter()
.min_by_key(|(_, m)| m.last_accessed)
{
self.somatic_cache.remove(&lru_hash);
}
}
pub fn current_state(&self) -> InteroceptiveState {
let global_arousal = self.compute_global_arousal();
let active_markers: Vec<SomaticMarker> = self
.somatic_cache
.values()
.filter(|m| {
let age = Utc::now() - m.last_accessed;
age.num_minutes() < 30
})
.cloned()
.collect();
InteroceptiveState {
domain_states: self.domain_states.clone(),
global_arousal,
buffer_size: self.signal_buffer.len(),
active_markers,
timestamp: Utc::now(),
}
}
fn compute_global_arousal(&self) -> f64 {
if self.domain_states.is_empty() {
return 0.0;
}
let recent_window = 50.min(self.signal_buffer.len());
if recent_window == 0 {
return 0.0;
}
let sum: f64 = self
.signal_buffer
.iter()
.rev()
.take(recent_window)
.map(|s| s.arousal)
.sum();
(sum / recent_window as f64).clamp(0.0, 1.0)
}
pub fn domain_state(&self, domain: &str) -> Option<&DomainState> {
self.domain_states.get(domain)
}
pub fn all_domain_states(&self) -> &HashMap<String, DomainState> {
&self.domain_states
}
pub fn buffer_len(&self) -> usize {
self.signal_buffer.len()
}
pub fn domain_count(&self) -> usize {
self.domain_states.len()
}
pub fn marker_count(&self) -> usize {
self.somatic_cache.len()
}
pub fn baseline(&self, source: &str, domain: &str) -> Option<&AdaptiveBaseline> {
let key = (source.to_string(), domain.to_string());
self.baselines.get(&key)
}
pub fn deviation_level(&self, source: &str, domain: &str, value: f64) -> DeviationLevel {
match self.baseline(source, domain) {
Some(bl) => bl.deviation_level(value),
None => DeviationLevel::Uncalibrated,
}
}
pub fn all_baselines(&self) -> &HashMap<BaselineKey, AdaptiveBaseline> {
&self.baselines
}
pub fn is_domain_calibrated(&self, domain: &str) -> bool {
let domain_baselines: Vec<_> = self.baselines.iter()
.filter(|((_, d), _)| d == domain)
.collect();
if domain_baselines.is_empty() {
return false;
}
domain_baselines.iter().all(|(_, bl)| bl.is_calibrated())
}
pub fn clear(&mut self) {
self.domain_states.clear();
self.signal_buffer.clear();
self.somatic_cache.clear();
self.baselines.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interoceptive::types::SignalSource;
#[test]
fn hub_processes_signal_and_updates_domain() {
let mut hub = InteroceptiveHub::new();
let sig = InteroceptiveSignal::new(
SignalSource::Accumulator,
Some("coding".into()),
0.7,
0.3,
);
let notable = hub.process_signal(sig);
assert!(!notable);
assert_eq!(hub.domain_count(), 1);
assert_eq!(hub.buffer_len(), 1);
let ds = hub.domain_state("coding").unwrap();
assert!(ds.valence_trend > 0.0);
}
#[test]
fn hub_notable_signal() {
let mut hub = InteroceptiveHub::new();
let sig = InteroceptiveSignal::new(
SignalSource::Anomaly,
Some("trading".into()),
-0.8,
0.9,
);
assert!(hub.process_signal(sig)); }
#[test]
fn hub_buffer_fifo_eviction() {
let mut hub = InteroceptiveHub::with_capacity(5, 10, 0.3);
for i in 0..8 {
let sig = InteroceptiveSignal::new(
SignalSource::Accumulator,
Some("test".into()),
i as f64 * 0.1,
0.1,
);
hub.process_signal(sig);
}
assert_eq!(hub.buffer_len(), 5); }
#[test]
fn hub_global_arousal_computation() {
let mut hub = InteroceptiveHub::new();
for _ in 0..10 {
let sig = InteroceptiveSignal::new(
SignalSource::Anomaly,
Some("test".into()),
-0.5,
0.8,
);
hub.process_signal(sig);
}
let state = hub.current_state();
assert!(state.global_arousal > 0.5, "got {}", state.global_arousal);
}
#[test]
fn hub_somatic_marker_creation_and_update() {
let mut hub = InteroceptiveHub::new();
let marker = hub.somatic_lookup(42, 0.5);
assert_eq!(marker.encounter_count, 1);
assert_eq!(marker.valence, 0.5);
let marker = hub.somatic_lookup(42, -0.5);
assert_eq!(marker.encounter_count, 2);
assert!((marker.valence - 0.0).abs() < f64::EPSILON);
}
#[test]
fn hub_somatic_lru_eviction() {
let mut hub = InteroceptiveHub::with_capacity(100, 3, 0.3);
hub.somatic_lookup(1, 0.1);
hub.somatic_lookup(2, 0.2);
hub.somatic_lookup(3, 0.3);
assert_eq!(hub.marker_count(), 3);
hub.somatic_lookup(4, 0.4);
assert_eq!(hub.marker_count(), 3);
}
#[test]
fn hub_current_state_snapshot() {
let mut hub = InteroceptiveHub::new();
let sig = InteroceptiveSignal::new(
SignalSource::Feedback,
Some("coding".into()),
0.6,
0.2,
);
hub.process_signal(sig);
let state = hub.current_state();
assert_eq!(state.domain_states.len(), 1);
assert!(state.domain_states.contains_key("coding"));
assert_eq!(state.buffer_size, 1);
}
#[test]
fn hub_global_domain_signal() {
let mut hub = InteroceptiveHub::new();
let sig = InteroceptiveSignal::new(SignalSource::Confidence, None, 0.4, 0.1);
hub.process_signal(sig);
assert!(hub.domain_state("_global").is_some());
}
#[test]
fn hub_process_batch() {
let mut hub = InteroceptiveHub::new();
let signals = vec![
InteroceptiveSignal::new(SignalSource::Accumulator, Some("a".into()), 0.5, 0.2),
InteroceptiveSignal::new(SignalSource::Anomaly, Some("b".into()), -0.8, 0.9),
InteroceptiveSignal::new(SignalSource::Feedback, Some("a".into()), 0.3, 0.1),
];
let notable = hub.process_batch(signals);
assert_eq!(notable, 1); assert_eq!(hub.buffer_len(), 3);
assert_eq!(hub.domain_count(), 2); }
#[test]
fn hub_clear() {
let mut hub = InteroceptiveHub::new();
hub.process_signal(InteroceptiveSignal::new(
SignalSource::Accumulator,
Some("x".into()),
0.5,
0.2,
));
hub.somatic_lookup(99, 0.1);
hub.clear();
assert_eq!(hub.buffer_len(), 0);
assert_eq!(hub.domain_count(), 0);
assert_eq!(hub.marker_count(), 0);
}
}