Skip to main content

microscope_memory/
emotional_state.rs

1//! Emotional State Ring — persistent running 21D emotional state.
2//!
3//! Automatically tracks the emotional context of stored memories
4//! and provides emotional priming for recall — the system's innate
5//! "mood" that biases which memories surface.
6//!
7//! Binary format: emotional_state.bin (ESR1)
8//! - magic: [0x45, 0x53, 0x52, 0x31] = "ESR1" (4 bytes)
9//! - current_state: [f32; 21] (84 bytes)
10//! - ring_head: u32 (4 bytes)
11//! - ring_count: u32 (4 bytes)
12//! - ring_buffer: [[f32; 21]; 10] (840 bytes)
13//! - last_update: u64 (8 bytes)
14//! - total_updates: u64 (8 bytes)
15//!
16//! Total: 952 bytes
17
18use std::fs;
19use std::path::Path;
20use std::time::{SystemTime, UNIX_EPOCH};
21
22// ─── Constants ──────────────────────────────────────
23
24const RING_MAGIC: &[u8; 4] = b"ESR1";
25const RING_SIZE: usize = 10;
26const STATE_BYTES: usize = 952; // 4 + 84 + 4 + 4 + 840 + 8 + 8
27
28/// How much of the new emotion bleeds into the running state (0.0–1.0).
29const BLEND_FACTOR: f32 = 0.3;
30
31/// How fast the emotional state decays toward neutral when no new data arrives (per day).
32const DECAY_PER_DAY: f32 = 0.15;
33
34/// If emotional intensity is above this, the state is "active" and primes recall.
35const PRIMING_THRESHOLD: f32 = 0.15;
36
37// ─── EmotionalStateRing ─────────────────────────────
38
39/// Persistent running emotional state with ring buffer history.
40pub struct EmotionalStateRing {
41    /// Current blended 21D emotional state vector.
42    pub current: [f32; 21],
43    /// Ring buffer of recent raw emotion vectors.
44    pub ring: Vec<[f32; 21]>,
45    /// Head index in ring buffer.
46    head: usize,
47    /// Number of valid entries in ring buffer.
48    count: usize,
49    /// Unix ms of last update.
50    pub last_update: u64,
51    /// Total lifetime updates.
52    pub total_updates: u64,
53}
54
55impl EmotionalStateRing {
56    /// Load from disk or return a fresh neutral state.
57    pub fn load_or_init(output_dir: &Path) -> Self {
58        let path = output_dir.join("emotional_state.bin");
59        if path.exists() {
60            if let Ok(data) = fs::read(&path) {
61                if data.len() >= STATE_BYTES && &data[0..4] == RING_MAGIC {
62                    return Self::from_bytes(&data);
63                }
64            }
65        }
66        Self::default()
67    }
68
69    /// Save to disk.
70    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
71        let path = output_dir.join("emotional_state.bin");
72        let tmp_path = output_dir.join("emotional_state.bin.tmp");
73        fs::write(&tmp_path, self.to_bytes())
74            .map_err(|e| format!("save emotional_state.bin: {}", e))?;
75        fs::rename(&tmp_path, &path).map_err(|e| format!("rename emotional_state.bin: {}", e))
76    }
77
78    /// Update the emotional state with a new incoming emotion vector.
79    /// Importance scales the blend: higher importance = stronger influence.
80    pub fn update(&mut self, emotion: &[f32; 21], importance: u8) {
81        let imp_factor = 0.5 + (importance as f32 / 10.0) * 0.5; // 0.55–1.0
82        let blend = (BLEND_FACTOR * imp_factor).min(1.0);
83
84        // Blend: current * (1 - blend) + incoming * blend
85        for (cur, &inc) in self.current.iter_mut().zip(emotion.iter()) {
86            *cur = *cur * (1.0 - blend) + inc * blend;
87        }
88
89        // Push into ring buffer
90        self.ring[self.head] = *emotion;
91        self.head = (self.head + 1) % RING_SIZE;
92        if self.count < RING_SIZE {
93            self.count += 1;
94        }
95
96        self.last_update = now_epoch_ms();
97        self.total_updates += 1;
98    }
99
100    /// Apply time-based decay toward neutral. Call once per session or periodically.
101    pub fn decay(&mut self) {
102        let age_ms = now_epoch_ms().saturating_sub(self.last_update);
103        let age_days = age_ms as f32 / 86_400_000.0;
104        let decay = (DECAY_PER_DAY * age_days).min(1.0);
105        if decay > 0.0 {
106            for i in 0..21 {
107                self.current[i] *= 1.0 - decay;
108            }
109        }
110    }
111
112    /// The magnitude (L2 norm) of the current emotional state.
113    pub fn intensity(&self) -> f32 {
114        let sum: f32 = self.current.iter().map(|x| x * x).sum();
115        sum.sqrt()
116    }
117
118    /// Returns true if the current state is strong enough to prime recall.
119    pub fn is_active(&self) -> bool {
120        self.intensity() > PRIMING_THRESHOLD
121    }
122
123    /// Get the dominant emotion dimension name and value.
124    pub fn dominant(&self) -> Option<(&'static str, f32)> {
125        let mut best: Option<(usize, f32)> = None;
126        for (i, &v) in self.current.iter().enumerate() {
127            if v > 0.05 && best.is_none_or(|(_, b)| v > b) {
128                best = Some((i, v));
129            }
130        }
131        best.map(|(i, v)| {
132            let name = crate::EMOTION_DIMS.get(i).copied().unwrap_or("?");
133            (name, v)
134        })
135    }
136
137    /// Compute the average emotion over the ring buffer (recent trend).
138    pub fn recent_trend(&self) -> [f32; 21] {
139        if self.count == 0 {
140            return [0.0f32; 21];
141        }
142        let mut avg = [0.0f32; 21];
143        for i in 0..self.count {
144            let idx = if i < self.count {
145                (self.head + RING_SIZE - 1 - i) % RING_SIZE
146            } else {
147                0
148            };
149            for (j, val) in avg.iter_mut().enumerate() {
150                *val += self.ring[idx][j];
151            }
152        }
153        let n = self.count as f32;
154        for val in avg.iter_mut() {
155            *val /= n;
156        }
157        avg
158    }
159
160    // ─── Binary serialization ──────────────────────
161
162    fn to_bytes(&self) -> Vec<u8> {
163        let mut buf = Vec::with_capacity(STATE_BYTES);
164        buf.extend_from_slice(RING_MAGIC);
165        for v in &self.current {
166            buf.extend_from_slice(&v.to_le_bytes());
167        }
168        buf.extend_from_slice(&(self.head as u32).to_le_bytes());
169        buf.extend_from_slice(&(self.count as u32).to_le_bytes());
170        for i in 0..RING_SIZE {
171            for j in 0..21 {
172                buf.extend_from_slice(&self.ring[i][j].to_le_bytes());
173            }
174        }
175        buf.extend_from_slice(&self.last_update.to_le_bytes());
176        buf.extend_from_slice(&self.total_updates.to_le_bytes());
177        buf
178    }
179
180    fn from_bytes(data: &[u8]) -> Self {
181        let mut pos = 4; // skip magic
182        let mut current = [0.0f32; 21];
183        for c in current.iter_mut() {
184            *c = f32::from_le_bytes(data[pos..pos + 4].try_into().unwrap_or([0u8; 4]));
185            pos += 4;
186        }
187        let head = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap_or([0u8; 4])) as usize;
188        pos += 4;
189        let count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap_or([0u8; 4])) as usize;
190        pos += 4;
191        let mut ring = vec![[0.0f32; 21]; RING_SIZE];
192        for slot in ring.iter_mut() {
193            for v in slot.iter_mut() {
194                *v = f32::from_le_bytes(data[pos..pos + 4].try_into().unwrap_or([0u8; 4]));
195                pos += 4;
196            }
197        }
198        let last_update = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap_or([0u8; 8]));
199        pos += 8;
200        let total_updates = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap_or([0u8; 8]));
201
202        EmotionalStateRing {
203            current,
204            ring,
205            head,
206            count,
207            last_update,
208            total_updates,
209        }
210    }
211}
212
213impl Default for EmotionalStateRing {
214    fn default() -> Self {
215        Self {
216            current: [0.0f32; 21],
217            ring: vec![[0.0f32; 21]; RING_SIZE],
218            head: 0,
219            count: 0,
220            last_update: 0,
221            total_updates: 0,
222        }
223    }
224}
225
226/// Get the emotional priming weight for use in recall scoring.
227/// Returns 0.0 if the state is inactive (below threshold).
228pub fn emotional_prime_weight(state: &EmotionalStateRing) -> f32 {
229    if !state.is_active() {
230        return 0.0;
231    }
232    // Scale: intensity above threshold → linear 0.0–1.0, then square for diminishing tail
233    let raw = (state.intensity() - PRIMING_THRESHOLD) / (1.0 - PRIMING_THRESHOLD);
234    (raw * 2.0).min(1.0) // amplify so even moderate intensity primes well
235}
236
237fn now_epoch_ms() -> u64 {
238    SystemTime::now()
239        .duration_since(UNIX_EPOCH)
240        .unwrap_or_default()
241        .as_millis() as u64
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_default_is_neutral() {
250        let ring = EmotionalStateRing::default();
251        assert_eq!(ring.intensity(), 0.0);
252        assert!(!ring.is_active());
253    }
254
255    #[test]
256    fn test_update_changes_state() {
257        let mut ring = EmotionalStateRing::default();
258        let happy = [
259            0.8f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3, 0.0,
260            0.0, 0.0, 0.0, 0.4,
261        ];
262        ring.update(&happy, 8);
263        assert!(ring.intensity() > 0.0);
264        assert!(ring.is_active());
265        assert_eq!(ring.count, 1);
266        assert_eq!(ring.total_updates, 1);
267    }
268
269    #[test]
270    fn test_multiple_updates_blend() {
271        let mut ring = EmotionalStateRing::default();
272        let v1 = [1.0f32; 21];
273        let v2 = [0.0f32; 21];
274        ring.update(&v1, 10);
275        let intense = ring.intensity();
276        ring.update(&v2, 1);
277        // Should have moved toward zero but not all the way
278        assert!(ring.intensity() < intense);
279        assert!(ring.intensity() > 0.0);
280    }
281
282    #[test]
283    fn test_ring_buffer_rotation() {
284        let mut ring = EmotionalStateRing::default();
285        for i in 0..15 {
286            let emo = [(i as f32) / 10.0; 21];
287            ring.update(&emo, 5);
288        }
289        assert_eq!(ring.count, RING_SIZE);
290        assert_eq!(ring.total_updates, 15);
291    }
292
293    #[test]
294    fn test_save_load_roundtrip() {
295        let dir = std::env::temp_dir().join("microscope_emotional_test");
296        let _ = std::fs::create_dir_all(&dir);
297        let mut ring = EmotionalStateRing::default();
298        let happy = [0.9f32; 21];
299        ring.update(&happy, 10);
300        ring.save(&dir).unwrap();
301
302        let loaded = EmotionalStateRing::load_or_init(&dir);
303        assert_eq!(loaded.total_updates, 1);
304        assert!((loaded.intensity() - ring.intensity()).abs() < 0.001);
305
306        let _ = std::fs::remove_dir_all(&dir);
307    }
308
309    #[test]
310    fn test_emotional_prime_weight_inactive() {
311        let ring = EmotionalStateRing::default();
312        assert_eq!(emotional_prime_weight(&ring), 0.0);
313    }
314
315    #[test]
316    fn test_emotional_prime_weight_active() {
317        let mut ring = EmotionalStateRing::default();
318        let strong = [0.8f32; 21];
319        ring.update(&strong, 10);
320        assert!(emotional_prime_weight(&ring) > 0.0);
321    }
322
323    #[test]
324    fn test_dominant_emotion() {
325        let mut ring = EmotionalStateRing::default();
326        let mut v = [0.0f32; 21];
327        v[0] = 0.9; // joy
328        v[6] = 0.7; // trust
329        ring.update(&v, 10);
330        let dom = ring.dominant();
331        assert!(dom.is_some());
332        assert_eq!(dom.unwrap().0, "joy");
333        // After blend (BLEND_FACTOR=0.3, importance=10 → imp_factor=1.0 → blend=0.3): 0.0*0.7 + 0.9*0.3 = 0.27
334        assert!((dom.unwrap().1 - 0.27).abs() < 0.01);
335    }
336}