Skip to main content

microscope_memory/
mental_stimulation.rs

1//! Mental Stimulation — continuous activity requirement to prevent cognitive decay
2//! Tracks activity, triggers novelty seeking when dormant, maintains engagement level
3
4use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8#[derive(Clone, Debug)]
9pub struct ActivityRecord {
10    pub timestamp_ms: u64,
11    pub activity_type: String, // "recall", "learn", "consolidate", etc.
12    pub intensity: f32,        // 0.0-1.0
13    pub engagement_gain: f32,
14}
15
16pub struct MentalStimulation {
17    pub activity_history: Vec<ActivityRecord>,
18    pub engagement_level: f32, // 0.0-1.0, current mental engagement
19    pub decay_rate: f32,       // how fast engagement decays without activity
20    pub last_activity_ms: u64,
21    pub stimulation_need: f32,  // 0.0-1.0, how much stimulation is needed
22    pub novelty_threshold: f32, // trigger new experiences when engagement drops below this
23}
24
25impl Default for MentalStimulation {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl MentalStimulation {
32    pub fn new() -> Self {
33        Self {
34            activity_history: Vec::new(),
35            engagement_level: 0.8,
36            decay_rate: 0.001, // 0.1% per second
37            last_activity_ms: Self::now_ms(),
38            stimulation_need: 0.0,
39            novelty_threshold: 0.3,
40        }
41    }
42
43    /// Record activity (recall, learning, etc.)
44    pub fn record_activity(&mut self, activity_type: &str, intensity: f32) {
45        let now = Self::now_ms();
46
47        // Apply decay since last activity
48        self.apply_decay(now);
49
50        // Boost engagement from activity
51        let engagement_gain = intensity * 0.2; // up to 0.2 per activity
52        self.engagement_level = (self.engagement_level + engagement_gain).min(1.0);
53
54        self.activity_history.push(ActivityRecord {
55            timestamp_ms: now,
56            activity_type: activity_type.to_string(),
57            intensity,
58            engagement_gain,
59        });
60
61        if self.activity_history.len() > 1000 {
62            self.activity_history.remove(0);
63        }
64
65        self.last_activity_ms = now;
66        self.stimulation_need = 0.0;
67    }
68
69    /// Apply engagement decay over time
70    pub fn apply_decay(&mut self, current_ms: u64) {
71        let time_since_activity_ms = current_ms.saturating_sub(self.last_activity_ms);
72        let decay_amount = self.decay_rate * (time_since_activity_ms as f32 / 1000.0);
73
74        self.engagement_level = (self.engagement_level - decay_amount).max(0.0);
75        self.stimulation_need = (1.0 - self.engagement_level).max(0.0);
76    }
77
78    /// Check if system needs stimulation (novelty seeking)
79    pub fn needs_stimulation(&mut self) -> bool {
80        let now = Self::now_ms();
81        self.apply_decay(now);
82        self.engagement_level < self.novelty_threshold
83    }
84
85    /// Get recommended stimulation activities
86    pub fn get_stimulation_activities(&self) -> Vec<&'static str> {
87        let mut activities = Vec::new();
88
89        if self.engagement_level < 0.2 {
90            // Critical: major novelty needed
91            activities.push("explore_new_patterns");
92            activities.push("cross_modal_learning");
93        } else if self.engagement_level < 0.4 {
94            // Low: moderate novelty
95            activities.push("challenge_existing_pathways");
96            activities.push("learn_new_domain");
97        } else if self.engagement_level < 0.6 {
98            // Moderate: light stimulation
99            activities.push("recall_with_variations");
100            activities.push("make_new_associations");
101        }
102
103        activities
104    }
105
106    /// Get activity statistics
107    pub fn stats(&self) -> (f32, u64, usize, f32) {
108        let now = Self::now_ms();
109        let time_since_last = now.saturating_sub(self.last_activity_ms);
110        let activity_count = self.activity_history.len();
111
112        let recent_avg_intensity = if !self.activity_history.is_empty() {
113            self.activity_history
114                .iter()
115                .rev()
116                .take(10)
117                .map(|a| a.intensity)
118                .sum::<f32>()
119                / self.activity_history.len().min(10) as f32
120        } else {
121            0.0
122        };
123
124        (
125            self.engagement_level,
126            time_since_last,
127            activity_count,
128            recent_avg_intensity,
129        )
130    }
131
132    /// Get activity diversity (prevents repetitive patterns)
133    pub fn activity_diversity(&self) -> f32 {
134        if self.activity_history.is_empty() {
135            return 0.0;
136        }
137
138        let mut type_counts = HashMap::new();
139        for activity in &self.activity_history {
140            *type_counts
141                .entry(activity.activity_type.clone())
142                .or_insert(0) += 1;
143        }
144
145        let unique_types = type_counts.len() as f32;
146        let total_activities = self.activity_history.len() as f32;
147
148        (unique_types / total_activities).min(1.0)
149    }
150
151    pub fn save(&self, dir: &Path) -> Result<(), String> {
152        let path = dir.join("mental_stimulation.bin");
153        let mut data = Vec::new();
154
155        data.extend_from_slice(b"STIM");
156        data.push(1);
157
158        data.extend_from_slice(&self.engagement_level.to_le_bytes());
159        data.extend_from_slice(&self.last_activity_ms.to_le_bytes());
160        data.extend_from_slice(&self.stimulation_need.to_le_bytes());
161
162        let activity_count = self.activity_history.len() as u32;
163        data.extend_from_slice(&activity_count.to_le_bytes());
164
165        for activity in &self.activity_history {
166            data.extend_from_slice(&activity.timestamp_ms.to_le_bytes());
167            data.extend_from_slice(&activity.intensity.to_le_bytes());
168            data.extend_from_slice(&activity.engagement_gain.to_le_bytes());
169
170            let type_bytes = activity.activity_type.as_bytes();
171            data.push(type_bytes.len() as u8);
172            data.extend_from_slice(type_bytes);
173        }
174
175        let tmp_path = dir.join("mental_stimulation.bin.tmp");
176        fs::write(&tmp_path, data).map_err(|e| e.to_string())?;
177        fs::rename(&tmp_path, &path).map_err(|e| e.to_string())
178    }
179
180    pub fn load(dir: &Path) -> Result<Self, String> {
181        let path = dir.join("mental_stimulation.bin");
182        if !path.exists() {
183            return Ok(Self::new());
184        }
185
186        let data = fs::read(&path).map_err(|e| e.to_string())?;
187        if data.len() < 5 || &data[0..4] != b"STIM" {
188            return Ok(Self::new());
189        }
190
191        let mut idx = 5;
192        let engagement_level =
193            f32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
194        idx += 4;
195
196        let last_activity_ms = u64::from_le_bytes([
197            data[idx],
198            data[idx + 1],
199            data[idx + 2],
200            data[idx + 3],
201            data[idx + 4],
202            data[idx + 5],
203            data[idx + 6],
204            data[idx + 7],
205        ]);
206        idx += 8;
207
208        let stimulation_need =
209            f32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
210        idx += 4;
211
212        let mut activity_history = Vec::new();
213
214        if idx + 4 <= data.len() {
215            let activity_count =
216                u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]])
217                    as usize;
218            idx += 4;
219
220            for _ in 0..activity_count {
221                if idx + 20 > data.len() {
222                    break;
223                }
224
225                let timestamp_ms = u64::from_le_bytes([
226                    data[idx],
227                    data[idx + 1],
228                    data[idx + 2],
229                    data[idx + 3],
230                    data[idx + 4],
231                    data[idx + 5],
232                    data[idx + 6],
233                    data[idx + 7],
234                ]);
235                idx += 8;
236
237                let intensity =
238                    f32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
239                idx += 4;
240
241                let engagement_gain =
242                    f32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
243                idx += 4;
244
245                if idx >= data.len() {
246                    break;
247                }
248                let type_len = data[idx] as usize;
249                idx += 1;
250
251                if idx + type_len > data.len() {
252                    break;
253                }
254                let activity_type = String::from_utf8_lossy(&data[idx..idx + type_len]).to_string();
255                idx += type_len;
256
257                activity_history.push(ActivityRecord {
258                    timestamp_ms,
259                    activity_type,
260                    intensity,
261                    engagement_gain,
262                });
263            }
264        }
265
266        Ok(Self {
267            activity_history,
268            engagement_level,
269            decay_rate: 0.001,
270            last_activity_ms,
271            stimulation_need,
272            novelty_threshold: 0.3,
273        })
274    }
275
276    pub fn load_or_init(dir: &Path) -> Self {
277        Self::load(dir).unwrap_or_else(|_| Self::new())
278    }
279
280    fn now_ms() -> u64 {
281        std::time::SystemTime::now()
282            .duration_since(std::time::UNIX_EPOCH)
283            .unwrap_or_default()
284            .as_millis() as u64
285    }
286}