1use std::path::Path;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use crate::reader::{emotional_similarity, load_emotion_lookup};
15use crate::{content_coords_blended, MicroscopeReader};
16
17const EMOTION_BLEND_RATE: f32 = 0.15;
21const SPATIAL_DRIFT_RATE: f32 = 0.02;
23const MAX_EMO_SIM_FOR_RECONSOLIDATION: f32 = 0.85;
25const MIN_QUALITY: u8 = 3;
27const STALE_THRESHOLD_MS: u64 = 86_400_000 * 3; pub fn reconsolidate_emotions(
36 output_dir: &Path,
37 reader: &MicroscopeReader,
38 _query: &str,
39 query_emotion: Option<&[f32; 21]>,
40 activated: &[(u32, f32)],
41) -> u32 {
42 let qe = match query_emotion {
43 Some(e) => e,
44 None => {
45 let ring = crate::emotional_state::EmotionalStateRing::load_or_init(output_dir);
47 if ring.is_active() {
48 let current: [f32; 21] = ring.current;
50 return reconsolidate_emotions_inner(output_dir, reader, ¤t, activated);
51 }
52 return 0;
53 }
54 };
55 reconsolidate_emotions_inner(output_dir, reader, qe, activated)
56}
57
58fn reconsolidate_emotions_inner(
59 output_dir: &Path,
60 reader: &MicroscopeReader,
61 query_emotion: &[f32; 21],
62 activated: &[(u32, f32)],
63) -> u32 {
64 let lookup = match load_emotion_lookup(output_dir) {
65 Some(l) => l,
66 None => return 0,
67 };
68 let emotions_path = output_dir.join("emotions.bin");
69 let mut blended = 0u32;
70 let mut updates: Vec<(usize, [f32; 21])> = Vec::new();
71
72 let block_count = reader.block_count;
74
75 for &(idx, _) in activated {
76 let idx_usize = idx as usize;
77 if idx_usize >= block_count {
78 continue;
79 }
80
81 let current_emo = lookup(idx_usize).unwrap_or([0.0f32; 21]);
83
84 let sim = emotional_similarity(query_emotion, ¤t_emo);
86 if sim > MAX_EMO_SIM_FOR_RECONSOLIDATION {
87 continue;
88 }
89
90 let rate = EMOTION_BLEND_RATE * (1.0 - sim); let mut new_emo = [0.0f32; 21];
93 for i in 0..21 {
94 new_emo[i] = current_emo[i] * (1.0 - rate) + query_emotion[i] * rate;
95 }
96
97 let max_val = new_emo.iter().cloned().fold(0.0f32, f32::max);
99 if max_val > 1.0 {
100 for v in &mut new_emo {
101 *v /= max_val;
102 }
103 }
104
105 updates.push((idx_usize, new_emo));
111 }
112
113 if !updates.is_empty() {
114 if let Err(e) = crate::reader::write_emotions_batch(&emotions_path, &updates) {
115 eprintln!(" WARN batch emotion write: {}", e);
116 return blended;
117 }
118 blended += updates.len() as u32;
119 }
120
121 blended
122}
123
124pub fn reconsolidate_spatial(
129 output_dir: &Path,
130 reader: &MicroscopeReader,
131 query: &str,
132 config: &crate::config::Config,
133 activated: &[(u32, f32)],
134) -> u32 {
135 let sw = config.search.semantic_weight;
136 let (qx, qy, qz) = content_coords_blended(query, "long_term", sw);
137 let block_count = reader.block_count;
138
139 let mut hebb = crate::hebbian::HebbianState::load_or_init(output_dir, block_count);
141 let now_ms = SystemTime::now()
142 .duration_since(UNIX_EPOCH)
143 .unwrap_or_default()
144 .as_millis() as u64;
145
146 let mut drifted = 0u32;
147
148 for &(idx, _) in activated {
149 let idx_usize = idx as usize;
150 if idx_usize >= block_count {
151 continue;
152 }
153
154 let hdr = reader.header(idx_usize);
155 let dx = qx - hdr.x;
156 let dy = qy - hdr.y;
157 let dz = qz - hdr.z;
158 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
159
160 if dist < 0.001 {
162 continue;
163 }
164
165 let recency_factor = if idx_usize < hebb.activations.len() {
167 let age = now_ms.saturating_sub(hebb.activations[idx_usize].last_activated_ms);
168 if age > STALE_THRESHOLD_MS {
169 2.0 } else {
171 1.0
172 }
173 } else {
174 1.0
175 };
176
177 let rate = SPATIAL_DRIFT_RATE * dist * recency_factor;
178 let rate = rate.min(0.1); if idx_usize < hebb.activations.len() {
181 hebb.activations[idx_usize].drift_x += dx * rate;
182 hebb.activations[idx_usize].drift_y += dy * rate;
183 hebb.activations[idx_usize].drift_z += dz * rate;
184 drifted += 1;
185 }
186 }
187
188 if drifted > 0 {
189 let _ = hebb.save(output_dir);
190 }
191
192 drifted
193}
194
195pub fn reconsolidate(
198 output_dir: &Path,
199 reader: &MicroscopeReader,
200 query: &str,
201 query_emotion: Option<&[f32; 21]>,
202 config: &crate::config::Config,
203 quality: u8,
204 activated: &[(u32, f32)],
205) -> (u32, u32) {
206 if quality < MIN_QUALITY || activated.is_empty() {
207 return (0, 0);
208 }
209
210 let emo_count = reconsolidate_emotions(output_dir, reader, query, query_emotion, activated);
211 let spatial_count = reconsolidate_spatial(output_dir, reader, query, config, activated);
212
213 (emo_count, spatial_count)
214}
215
216pub fn format_reconsolidation(emo: u32, spatial: u32) -> String {
218 if emo == 0 && spatial == 0 {
219 String::new()
220 } else {
221 format!(" RECONSOLIDATED emotion={} spatial={}", emo, spatial)
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_emotion_blend_basic() {
231 let qe = [
232 0.9f32, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
233 0.0, 0.0, 0.0, 0.0,
234 ];
235 let ce = [
236 0.1f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.8, 0.0, 0.0, 0.0,
237 0.0, 0.0, 0.0, 0.0,
238 ]; let sim = emotional_similarity(&qe, &ce);
240 assert!(sim < MAX_EMO_SIM_FOR_RECONSOLIDATION); let rate = EMOTION_BLEND_RATE * (1.0 - sim);
243 let mut blended = [0.0f32; 21];
244 for i in 0..21 {
245 blended[i] = ce[i] * (1.0 - rate) + qe[i] * rate;
246 }
247 assert!(blended[0] > 0.1);
249 assert!(blended[13] < ce[13]);
251 }
252
253 #[test]
254 fn test_high_similarity_skips_blend() {
255 let qe = [0.8f32; 21];
256 let ce = [0.78f32; 21];
257 let sim = emotional_similarity(&qe, &ce);
258 assert!(sim > MAX_EMO_SIM_FOR_RECONSOLIDATION); }
260}