microscope_memory/
emotional_state.rs1use std::fs;
19use std::path::Path;
20use std::time::{SystemTime, UNIX_EPOCH};
21
22const RING_MAGIC: &[u8; 4] = b"ESR1";
25const RING_SIZE: usize = 10;
26const STATE_BYTES: usize = 952; const BLEND_FACTOR: f32 = 0.3;
30
31const DECAY_PER_DAY: f32 = 0.15;
33
34const PRIMING_THRESHOLD: f32 = 0.15;
36
37pub struct EmotionalStateRing {
41 pub current: [f32; 21],
43 pub ring: Vec<[f32; 21]>,
45 head: usize,
47 count: usize,
49 pub last_update: u64,
51 pub total_updates: u64,
53}
54
55impl EmotionalStateRing {
56 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 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 pub fn update(&mut self, emotion: &[f32; 21], importance: u8) {
81 let imp_factor = 0.5 + (importance as f32 / 10.0) * 0.5; let blend = (BLEND_FACTOR * imp_factor).min(1.0);
83
84 for (cur, &inc) in self.current.iter_mut().zip(emotion.iter()) {
86 *cur = *cur * (1.0 - blend) + inc * blend;
87 }
88
89 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 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 pub fn intensity(&self) -> f32 {
114 let sum: f32 = self.current.iter().map(|x| x * x).sum();
115 sum.sqrt()
116 }
117
118 pub fn is_active(&self) -> bool {
120 self.intensity() > PRIMING_THRESHOLD
121 }
122
123 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 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 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; 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
226pub fn emotional_prime_weight(state: &EmotionalStateRing) -> f32 {
229 if !state.is_active() {
230 return 0.0;
231 }
232 let raw = (state.intensity() - PRIMING_THRESHOLD) / (1.0 - PRIMING_THRESHOLD);
234 (raw * 2.0).min(1.0) }
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 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; v[6] = 0.7; ring.update(&v, 10);
330 let dom = ring.dominant();
331 assert!(dom.is_some());
332 assert_eq!(dom.unwrap().0, "joy");
333 assert!((dom.unwrap().1 - 0.27).abs() < 0.01);
335 }
336}