Skip to main content

feagi_evolutionary/
plasticity_detector.rs

1//! Plasticity detection for genome analysis
2//!
3//! This module provides utilities to detect whether a genome contains
4//! plasticity features (neuroplasticity via memory areas or synaptic plasticity via STDP).
5
6use serde_json::Value;
7use std::collections::HashMap;
8use tracing::debug;
9
10/// Check if a genome JSON contains any form of plasticity
11///
12/// This function checks for:
13/// 1. Memory cortical areas (identified by `memory-b` flag = true)
14/// 2. STDP connections (identified by `plasticity_flag` = true in morphologies)
15///
16/// # Arguments
17/// * `genome_json` - The genome JSON Value
18///
19/// # Returns
20/// * `true` if plasticity is detected, `false` otherwise
21///
22pub fn genome_has_plasticity(genome_json: &Value) -> bool {
23    let has_memory = has_memory_areas(genome_json);
24    let has_stdp = has_stdp_connections(genome_json);
25
26    debug!(
27        target: "feagi-evolutionary",
28        "Plasticity detection: memory_areas={}, stdp_connections={}",
29        has_memory, has_stdp
30    );
31
32    has_memory || has_stdp
33}
34
35/// Check if genome has memory cortical areas
36///
37/// Memory areas are identified by the `memory-b` property set to `true` in the blueprint.
38/// The `_group` field may still be "CUSTOM", so we rely on the `memory-b` flag.
39///
40fn has_memory_areas(genome_json: &Value) -> bool {
41    if let Some(blueprint) = genome_json.get("blueprint").and_then(|b| b.as_object()) {
42        for (key, value) in blueprint {
43            // Check for memory-b flag
44            if key.ends_with("-cx-memory-b") {
45                if let Some(is_memory) = value.as_bool() {
46                    if is_memory {
47                        debug!(
48                            target: "feagi-evolutionary",
49                            "Found memory area via key: {}", key
50                        );
51                        return true;
52                    }
53                }
54            }
55        }
56    }
57    false
58}
59
60/// Check if genome has STDP connections (plastic synapses)
61///
62/// STDP connections are identified by `plasticity_flag: true` in the destination map morphologies.
63///
64fn has_stdp_connections(genome_json: &Value) -> bool {
65    if let Some(blueprint) = genome_json.get("blueprint").and_then(|b| b.as_object()) {
66        for (key, value) in blueprint {
67            // Check for destination mapping (dstmap)
68            if key.ends_with("-cx-dstmap-d") {
69                if let Some(dstmap) = value.as_object() {
70                    for (dst_area_id, morphology_list) in dstmap {
71                        if let Some(morphologies) = morphology_list.as_array() {
72                            for morph in morphologies {
73                                if let Some(plasticity_flag) = morph.get("plasticity_flag") {
74                                    if plasticity_flag.as_bool() == Some(true) {
75                                        debug!(
76                                            target: "feagi-evolutionary",
77                                            "Found STDP connection: key={}, dst={}", key, dst_area_id
78                                        );
79                                        return true;
80                                    }
81                                }
82                            }
83                        }
84                    }
85                }
86            }
87        }
88    }
89    false
90}
91
92/// Memory-specific cortical area properties
93#[derive(Debug, Clone)]
94pub struct MemoryAreaProperties {
95    /// Number of timesteps to consider for temporal pattern detection
96    pub temporal_depth: u32,
97    /// Threshold for long-term memory formation (number of activations)
98    pub longterm_threshold: u32,
99    /// Rate at which neuron lifespan grows with reactivations
100    pub lifespan_growth_rate: f32,
101    /// Initial lifespan for newly created memory neurons
102    pub init_lifespan: u32,
103    /// When true, membrane potentials are captured and stored with replay frames
104    pub mp_learning_enabled: bool,
105}
106
107impl Default for MemoryAreaProperties {
108    fn default() -> Self {
109        Self {
110            // Enforce minimum temporal depth of 1; 0 is not a valid configuration because
111            // the pattern detector needs at least one timestep of history.
112            temporal_depth: 1,
113            longterm_threshold: 100,
114            lifespan_growth_rate: 1.0,
115            init_lifespan: 9,
116            mp_learning_enabled: false,
117        }
118    }
119}
120
121/// Extract memory-specific properties from a cortical area's properties HashMap
122///
123/// Returns `Some(MemoryAreaProperties)` if the area is a memory area (`is_mem_type` = true),
124/// otherwise returns `None`.
125///
126/// # Arguments
127/// * `properties` - The cortical area properties HashMap
128///
129pub fn extract_memory_properties(
130    properties: &HashMap<String, Value>,
131) -> Option<MemoryAreaProperties> {
132    let is_memory = properties
133        .get("is_mem_type")
134        .and_then(|v| v.as_bool())
135        .unwrap_or(false);
136
137    if !is_memory {
138        return None;
139    }
140
141    Some(MemoryAreaProperties {
142        temporal_depth: properties
143            .get("temporal_depth")
144            .and_then(|v| v.as_u64())
145            .unwrap_or(1)
146            .max(1) as u32,
147        longterm_threshold: properties
148            .get("longterm_mem_threshold")
149            .and_then(|v| v.as_u64())
150            .unwrap_or(100) as u32,
151        lifespan_growth_rate: properties
152            .get("lifespan_growth_rate")
153            .and_then(|v| v.as_f64())
154            .unwrap_or(1.0) as f32,
155        init_lifespan: properties
156            .get("init_lifespan")
157            .and_then(|v| v.as_u64())
158            .unwrap_or(9) as u32,
159        mp_learning_enabled: properties
160            .get("mp_learning_enabled")
161            .and_then(|v| v.as_bool())
162            .unwrap_or(false),
163    })
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use serde_json::json;
170
171    #[test]
172    fn test_no_plasticity() {
173        let genome = json!({
174            "blueprint": {
175                "_____10c-Y2FfX19fX50=-cx-_group-t": "CUSTOM",
176                "_____10c-Y2FfX19fX50=-cx-memory-b": false,
177                "_____10c-Y2FfX19fX50=-cx-dstmap-d": {
178                    "b2ltZwkAAAA=": [{
179                        "morphology_id": "projector",
180                        "plasticity_flag": false
181                    }]
182                }
183            }
184        });
185
186        assert!(!genome_has_plasticity(&genome));
187    }
188
189    #[test]
190    fn test_has_memory_areas() {
191        let genome = json!({
192            "blueprint": {
193                "_____10c-Y21fX19fXxg=-cx-_group-t": "CUSTOM",
194                "_____10c-Y21fX19fXxg=-cx-memory-b": true,
195                "_____10c-Y21fX19fXxg=-cx-mem__t-i": 100
196            }
197        });
198
199        assert!(genome_has_plasticity(&genome));
200        assert!(has_memory_areas(&genome));
201        assert!(!has_stdp_connections(&genome));
202    }
203
204    #[test]
205    fn test_has_stdp_connections() {
206        let genome = json!({
207            "blueprint": {
208                "_____10c-Y2FfX19fX50=-cx-_group-t": "CUSTOM",
209                "_____10c-Y2FfX19fX50=-cx-memory-b": false,
210                "_____10c-Y2FfX19fX50=-cx-dstmap-d": {
211                    "b2ltZwkAAAA=": [{
212                        "morphology_id": "projector",
213                        "plasticity_flag": true,
214                        "postSynapticCurrent_multiplier": 1
215                    }]
216                }
217            }
218        });
219
220        assert!(genome_has_plasticity(&genome));
221        assert!(!has_memory_areas(&genome));
222        assert!(has_stdp_connections(&genome));
223    }
224
225    #[test]
226    fn test_has_both_plasticity_types() {
227        let genome = json!({
228            "blueprint": {
229                "_____10c-Y21fX19fXxg=-cx-memory-b": true,
230                "_____10c-Y2FfX19fX50=-cx-dstmap-d": {
231                    "b2ltZwkAAAA=": [{
232                        "morphology_id": "projector",
233                        "plasticity_flag": true
234                    }]
235                }
236            }
237        });
238
239        assert!(genome_has_plasticity(&genome));
240        assert!(has_memory_areas(&genome));
241        assert!(has_stdp_connections(&genome));
242    }
243
244    #[test]
245    fn test_extract_memory_properties() {
246        let mut properties = HashMap::new();
247        properties.insert("is_mem_type".to_string(), json!(true));
248        properties.insert("temporal_depth".to_string(), json!(5));
249        properties.insert("longterm_mem_threshold".to_string(), json!(200));
250        properties.insert("lifespan_growth_rate".to_string(), json!(1.5));
251        properties.insert("init_lifespan".to_string(), json!(15));
252
253        let mem_props = extract_memory_properties(&properties).expect("Should extract properties");
254        assert_eq!(mem_props.temporal_depth, 5);
255        assert_eq!(mem_props.longterm_threshold, 200);
256        assert_eq!(mem_props.lifespan_growth_rate, 1.5);
257        assert_eq!(mem_props.init_lifespan, 15);
258    }
259
260    #[test]
261    fn test_extract_memory_properties_defaults() {
262        let mut properties = HashMap::new();
263        properties.insert("is_mem_type".to_string(), json!(true));
264
265        let mem_props = extract_memory_properties(&properties).expect("Should extract properties");
266        assert_eq!(mem_props.temporal_depth, 1);
267        assert_eq!(mem_props.longterm_threshold, 100);
268        assert_eq!(mem_props.lifespan_growth_rate, 1.0);
269        assert_eq!(mem_props.init_lifespan, 9);
270    }
271
272    #[test]
273    fn test_extract_memory_properties_non_memory() {
274        let mut properties = HashMap::new();
275        properties.insert("is_mem_type".to_string(), json!(false));
276
277        assert!(extract_memory_properties(&properties).is_none());
278    }
279}