Skip to main content

microscope_memory/
synaptic_plasticity.rs

1//! Synaptic Plasticity — Long-term potentiation, long-term depression, spike-timing-dependent plasticity
2//! Core mechanisms of learning at synaptic level: activity-dependent weight changes
3
4use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8#[derive(Clone, Debug)]
9pub struct Synapse {
10    pub pre_block: u32,
11    pub post_block: u32,
12    pub weight: f32, // 0.0-1.0
13    pub last_pre_spike_ms: u64,
14    pub last_post_spike_ms: u64,
15    pub spike_history: Vec<i64>, // relative timing of spikes (ms)
16    pub ltp_count: u32,          // LTP events
17    pub ltd_count: u32,          // LTD events
18}
19
20pub struct SynapticPlasticity {
21    pub synapses: HashMap<(u32, u32), Synapse>,
22    pub ltp_threshold_ms: i64, // spike timing window for LTP (+ms)
23    pub ltd_threshold_ms: i64, // spike timing window for LTD (-ms)
24    pub learning_rate: f32,    // 0.0-1.0
25    pub total_updates: u32,
26}
27
28impl Default for SynapticPlasticity {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl SynapticPlasticity {
35    pub fn new() -> Self {
36        Self {
37            synapses: HashMap::new(),
38            ltp_threshold_ms: 20,  // post before pre: LTP
39            ltd_threshold_ms: -20, // pre before post: LTD
40            learning_rate: 0.1,
41            total_updates: 0,
42        }
43    }
44
45    /// Long-Term Potentiation: strengthen synapse
46    pub fn ltp(&mut self, pre_block: u32, post_block: u32) -> f32 {
47        let key = (pre_block, post_block);
48        let now = Self::now_ms();
49
50        let synapse = self.synapses.entry(key).or_insert_with(|| Synapse {
51            pre_block,
52            post_block,
53            weight: 0.5,
54            last_pre_spike_ms: now,
55            last_post_spike_ms: now,
56            spike_history: Vec::new(),
57            ltp_count: 0,
58            ltd_count: 0,
59        });
60
61        // Consolidate: weight increase
62        let increment = self.learning_rate * (1.0 - synapse.weight);
63        synapse.weight = (synapse.weight + increment).min(1.0);
64        synapse.ltp_count += 1;
65        synapse.last_pre_spike_ms = now;
66        self.total_updates += 1;
67
68        synapse.weight
69    }
70
71    /// Long-Term Depression: weaken synapse
72    pub fn ltd(&mut self, pre_block: u32, post_block: u32) -> f32 {
73        let key = (pre_block, post_block);
74        let now = Self::now_ms();
75
76        let synapse = self.synapses.entry(key).or_insert_with(|| Synapse {
77            pre_block,
78            post_block,
79            weight: 0.5,
80            last_pre_spike_ms: now,
81            last_post_spike_ms: now,
82            spike_history: Vec::new(),
83            ltp_count: 0,
84            ltd_count: 0,
85        });
86
87        // Weaken: weight decrease
88        let decrement = self.learning_rate * synapse.weight;
89        synapse.weight = (synapse.weight - decrement).max(0.0);
90        synapse.ltd_count += 1;
91        synapse.last_post_spike_ms = now;
92        self.total_updates += 1;
93
94        synapse.weight
95    }
96
97    /// Spike-Timing-Dependent Plasticity (STDP)
98    /// Pre spike then post spike (causal): LTP
99    /// Post spike then pre spike (acausal): LTD
100    pub fn stdp(
101        &mut self,
102        pre_block: u32,
103        post_block: u32,
104        pre_time_ms: i64,
105        post_time_ms: i64,
106    ) -> f32 {
107        let key = (pre_block, post_block);
108        let timing_diff = post_time_ms - pre_time_ms; // positive = post after pre
109
110        let synapse = self.synapses.entry(key).or_insert_with(|| Synapse {
111            pre_block,
112            post_block,
113            weight: 0.5,
114            last_pre_spike_ms: Self::now_ms(),
115            last_post_spike_ms: Self::now_ms(),
116            spike_history: Vec::new(),
117            ltp_count: 0,
118            ltd_count: 0,
119        });
120
121        synapse.spike_history.push(timing_diff);
122        if synapse.spike_history.len() > 50 {
123            synapse.spike_history.remove(0);
124        }
125
126        // Causal: pre before post → LTP
127        if timing_diff > 0 && timing_diff <= self.ltp_threshold_ms {
128            let strength = 1.0 - (timing_diff as f32 / self.ltp_threshold_ms as f32);
129            let increment = self.learning_rate * strength * (1.0 - synapse.weight);
130            synapse.weight = (synapse.weight + increment).min(1.0);
131            synapse.ltp_count += 1;
132        }
133        // Anti-causal: post before pre → LTD
134        else if timing_diff < 0 && timing_diff >= self.ltd_threshold_ms {
135            let strength = 1.0 - (-timing_diff as f32 / -self.ltd_threshold_ms as f32);
136            let decrement = self.learning_rate * strength * synapse.weight;
137            synapse.weight = (synapse.weight - decrement).max(0.0);
138            synapse.ltd_count += 1;
139        }
140
141        self.total_updates += 1;
142        synapse.weight
143    }
144
145    /// Get synapse weight
146    pub fn get_weight(&self, pre_block: u32, post_block: u32) -> f32 {
147        self.synapses
148            .get(&(pre_block, post_block))
149            .map(|s| s.weight)
150            .unwrap_or(0.0)
151    }
152
153    /// Get strongest synapses
154    pub fn strongest_synapses(&self, k: usize) -> Vec<(&(u32, u32), &Synapse)> {
155        let mut synapses: Vec<_> = self.synapses.iter().collect();
156        synapses.sort_by(|a, b| b.1.weight.total_cmp(&a.1.weight));
157        synapses.into_iter().take(k).collect()
158    }
159
160    /// Get synapses by plasticity type
161    pub fn ltp_dominant(&self) -> Vec<&Synapse> {
162        self.synapses
163            .values()
164            .filter(|s| s.ltp_count > s.ltd_count)
165            .collect()
166    }
167
168    pub fn ltd_dominant(&self) -> Vec<&Synapse> {
169        self.synapses
170            .values()
171            .filter(|s| s.ltd_count > s.ltp_count)
172            .collect()
173    }
174
175    /// Calculate STDP curve value
176    pub fn stdp_curve(&self, timing_diff_ms: i64) -> f32 {
177        if timing_diff_ms > 0 && timing_diff_ms <= self.ltp_threshold_ms {
178            // LTP: exponential decay
179            (1.0 - (timing_diff_ms as f32 / self.ltp_threshold_ms as f32).abs()).max(0.0)
180        } else if timing_diff_ms < 0 && timing_diff_ms >= self.ltd_threshold_ms {
181            // LTD: negative decay
182            -((timing_diff_ms as f32 / self.ltd_threshold_ms as f32).abs()).min(1.0)
183        } else {
184            0.0
185        }
186    }
187
188    /// Get statistics
189    pub fn stats(&self) -> (u32, u32, u32, f32, f32) {
190        let total_synapses = self.synapses.len() as u32;
191        let total_ltp: u32 = self.synapses.values().map(|s| s.ltp_count).sum();
192        let total_ltd: u32 = self.synapses.values().map(|s| s.ltd_count).sum();
193
194        let avg_weight = if !self.synapses.is_empty() {
195            self.synapses.values().map(|s| s.weight).sum::<f32>() / self.synapses.len() as f32
196        } else {
197            0.0
198        };
199
200        let ltp_ratio = if total_ltp + total_ltd > 0 {
201            total_ltp as f32 / (total_ltp + total_ltd) as f32
202        } else {
203            0.0
204        };
205
206        (total_synapses, total_ltp, total_ltd, avg_weight, ltp_ratio)
207    }
208
209    pub fn save(&self, dir: &Path) -> Result<(), String> {
210        let path = dir.join("synaptic_plasticity.bin");
211        let mut data = Vec::new();
212
213        data.extend_from_slice(b"SYPLS");
214        data.push(1);
215
216        let synapse_count = self.synapses.len() as u32;
217        data.extend_from_slice(&synapse_count.to_le_bytes());
218
219        for ((pre, post), synapse) in &self.synapses {
220            data.extend_from_slice(&pre.to_le_bytes());
221            data.extend_from_slice(&post.to_le_bytes());
222            data.extend_from_slice(&synapse.weight.to_le_bytes());
223            data.extend_from_slice(&synapse.last_pre_spike_ms.to_le_bytes());
224            data.extend_from_slice(&synapse.last_post_spike_ms.to_le_bytes());
225            data.extend_from_slice(&synapse.ltp_count.to_le_bytes());
226            data.extend_from_slice(&synapse.ltd_count.to_le_bytes());
227
228            let hist_count = synapse.spike_history.len() as u16;
229            data.extend_from_slice(&hist_count.to_le_bytes());
230            for &timing in &synapse.spike_history {
231                data.extend_from_slice(&timing.to_le_bytes());
232            }
233        }
234
235        let tmp_path = dir.join("synaptic_plasticity.bin.tmp");
236        fs::write(&tmp_path, data).map_err(|e| e.to_string())?;
237        fs::rename(&tmp_path, &path).map_err(|e| e.to_string())
238    }
239
240    pub fn load(dir: &Path) -> Result<Self, String> {
241        let path = dir.join("synaptic_plasticity.bin");
242        if !path.exists() {
243            return Ok(Self::new());
244        }
245
246        let data = fs::read(&path).map_err(|e| e.to_string())?;
247        if data.len() < 5 || &data[0..5] != b"SYPLS" {
248            return Ok(Self::new());
249        }
250
251        let mut idx = 6;
252        let mut synapses = HashMap::new();
253
254        if idx + 4 <= data.len() {
255            let synapse_count =
256                u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]])
257                    as usize;
258            idx += 4;
259
260            for _ in 0..synapse_count {
261                if idx + 40 > data.len() {
262                    break;
263                }
264
265                let pre =
266                    u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
267                idx += 4;
268                let post =
269                    u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
270                idx += 4;
271                let weight =
272                    f32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
273                idx += 4;
274                let last_pre = u64::from_le_bytes([
275                    data[idx],
276                    data[idx + 1],
277                    data[idx + 2],
278                    data[idx + 3],
279                    data[idx + 4],
280                    data[idx + 5],
281                    data[idx + 6],
282                    data[idx + 7],
283                ]);
284                idx += 8;
285                let last_post = u64::from_le_bytes([
286                    data[idx],
287                    data[idx + 1],
288                    data[idx + 2],
289                    data[idx + 3],
290                    data[idx + 4],
291                    data[idx + 5],
292                    data[idx + 6],
293                    data[idx + 7],
294                ]);
295                idx += 8;
296                let ltp_count =
297                    u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
298                idx += 4;
299                let ltd_count =
300                    u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
301                idx += 4;
302
303                let hist_count = u16::from_le_bytes([data[idx], data[idx + 1]]) as usize;
304                idx += 2;
305
306                let mut spike_history = Vec::new();
307                for _ in 0..hist_count {
308                    if idx + 8 > data.len() {
309                        break;
310                    }
311                    let timing = i64::from_le_bytes([
312                        data[idx],
313                        data[idx + 1],
314                        data[idx + 2],
315                        data[idx + 3],
316                        data[idx + 4],
317                        data[idx + 5],
318                        data[idx + 6],
319                        data[idx + 7],
320                    ]);
321                    spike_history.push(timing);
322                    idx += 8;
323                }
324
325                synapses.insert(
326                    (pre, post),
327                    Synapse {
328                        pre_block: pre,
329                        post_block: post,
330                        weight,
331                        last_pre_spike_ms: last_pre,
332                        last_post_spike_ms: last_post,
333                        spike_history,
334                        ltp_count,
335                        ltd_count,
336                    },
337                );
338            }
339        }
340
341        Ok(Self {
342            synapses,
343            ltp_threshold_ms: 20,
344            ltd_threshold_ms: -20,
345            learning_rate: 0.1,
346            total_updates: 0,
347        })
348    }
349
350    pub fn load_or_init(dir: &Path) -> Self {
351        Self::load(dir).unwrap_or_else(|_| Self::new())
352    }
353
354    /// Heterosynaptic Depression — weaken other synapses when one is active
355    pub fn heterosynaptic_depression(
356        &mut self,
357        active_synapse: (u32, u32),
358        neighborhood_radius: u32,
359    ) {
360        if let Some(active) = self.synapses.get(&active_synapse) {
361            let pre = active.pre_block;
362            let depression_strength = self.learning_rate * 0.5;
363
364            // Weaken other synapses on same post-neuron
365            let post = active_synapse.1;
366            let keys_to_weaken: Vec<_> = self
367                .synapses
368                .keys()
369                .filter(|(p_pre, p_post)| {
370                    *p_post == post && self.distance(pre, *p_pre) <= neighborhood_radius
371                })
372                .cloned()
373                .collect();
374
375            for key in keys_to_weaken {
376                if key != active_synapse {
377                    if let Some(synapse) = self.synapses.get_mut(&key) {
378                        synapse.weight = (synapse.weight - depression_strength).max(0.0);
379                        synapse.ltd_count += 1;
380                    }
381                }
382            }
383        }
384    }
385
386    /// Calculate simple distance between blocks
387    fn distance(&self, a: u32, b: u32) -> u32 {
388        a.abs_diff(b)
389    }
390
391    /// Time-dependent plasticity: high at start, decreases with practice, increases with new strategy
392    pub fn time_dependent_plasticity(
393        &mut self,
394        synapse_key: (u32, u32),
395        practice_count: u32,
396        strategy_age_ms: u64,
397    ) -> f32 {
398        let now = Self::now_ms();
399
400        if let Some(synapse) = self.synapses.get_mut(&synapse_key) {
401            // Phase 1: Early learning (high plasticity, 0-10 practices)
402            let early_phase_rate = if practice_count < 10 {
403                0.2 + (10 - practice_count) as f32 * 0.01 // 0.2-0.3
404            } else {
405                0.05
406            };
407
408            // Phase 2: Consolidation (low plasticity, 10-50 practices)
409            let consolidation_decay = if (10..50).contains(&practice_count) {
410                0.05 * (50 - practice_count) as f32 / 40.0 // 0.0625 → 0.05
411            } else if practice_count >= 50 {
412                0.02 // Very low
413            } else {
414                early_phase_rate
415            };
416
417            // Phase 3: New strategy detection (high plasticity boost if strategy is new)
418            let strategy_boost = if strategy_age_ms < 300_000 {
419                // 5 minutes
420                0.1 * (1.0 - (strategy_age_ms as f32 / 300_000.0)) // 0.1 → 0.0
421            } else {
422                0.0
423            };
424
425            self.learning_rate = (consolidation_decay + strategy_boost).min(0.3);
426            synapse.last_pre_spike_ms = now;
427
428            self.learning_rate
429        } else {
430            0.2 // Default high plasticity for new synapses
431        }
432    }
433
434    fn now_ms() -> u64 {
435        std::time::SystemTime::now()
436            .duration_since(std::time::UNIX_EPOCH)
437            .unwrap_or_default()
438            .as_millis() as u64
439    }
440}