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    /// Minimum fired voxels inside a scan window for that window to be considered.
106    pub min_window_activity: u32,
107    /// Skip the entire field scan when current-burst density exceeds this fraction (0.0-1.0).
108    pub scan_skip_density: f32,
109}
110
111impl Default for MemoryAreaProperties {
112    fn default() -> Self {
113        Self {
114            // Enforce minimum temporal depth of 1; 0 is not a valid configuration because
115            // the pattern detector needs at least one timestep of history.
116            temporal_depth: 1,
117            longterm_threshold: 100,
118            lifespan_growth_rate: 1.0,
119            init_lifespan: 9,
120            mp_learning_enabled: false,
121            min_window_activity: 1,
122            scan_skip_density: 1.0,
123        }
124    }
125}
126
127/// Extract memory-specific properties from a cortical area's properties HashMap
128///
129/// Returns `Some(MemoryAreaProperties)` if the area is a memory area (`is_mem_type` = true),
130/// otherwise returns `None`.
131///
132/// # Arguments
133/// * `properties` - The cortical area properties HashMap
134///
135pub fn extract_memory_properties(
136    properties: &HashMap<String, Value>,
137) -> Option<MemoryAreaProperties> {
138    let is_memory = properties
139        .get("is_mem_type")
140        .and_then(|v| v.as_bool())
141        .unwrap_or(false);
142
143    if !is_memory {
144        return None;
145    }
146
147    Some(MemoryAreaProperties {
148        temporal_depth: properties
149            .get("temporal_depth")
150            .and_then(|v| v.as_u64())
151            .unwrap_or(1)
152            .max(1) as u32,
153        longterm_threshold: properties
154            .get("longterm_mem_threshold")
155            .and_then(|v| v.as_u64())
156            .unwrap_or(100) as u32,
157        lifespan_growth_rate: properties
158            .get("lifespan_growth_rate")
159            .and_then(|v| v.as_f64())
160            .unwrap_or(1.0) as f32,
161        init_lifespan: properties
162            .get("init_lifespan")
163            .and_then(|v| v.as_u64())
164            .unwrap_or(9) as u32,
165        mp_learning_enabled: properties
166            .get("mp_learning_enabled")
167            .and_then(|v| v.as_bool())
168            .unwrap_or(false),
169        min_window_activity: properties
170            .get("min_window_activity")
171            .and_then(|v| v.as_u64())
172            .unwrap_or(1)
173            .max(1) as u32,
174        scan_skip_density: properties
175            .get("scan_skip_density")
176            .and_then(|v| v.as_f64())
177            .unwrap_or(1.0)
178            .clamp(0.0, 1.0) as f32,
179    })
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use serde_json::json;
186
187    #[test]
188    fn test_no_plasticity() {
189        let genome = json!({
190            "blueprint": {
191                "_____10c-Y2FfX19fX50=-cx-_group-t": "CUSTOM",
192                "_____10c-Y2FfX19fX50=-cx-memory-b": false,
193                "_____10c-Y2FfX19fX50=-cx-dstmap-d": {
194                    "b2ltZwkAAAA=": [{
195                        "morphology_id": "projector",
196                        "plasticity_flag": false
197                    }]
198                }
199            }
200        });
201
202        assert!(!genome_has_plasticity(&genome));
203    }
204
205    #[test]
206    fn test_has_memory_areas() {
207        let genome = json!({
208            "blueprint": {
209                "_____10c-Y21fX19fXxg=-cx-_group-t": "CUSTOM",
210                "_____10c-Y21fX19fXxg=-cx-memory-b": true,
211                "_____10c-Y21fX19fXxg=-cx-mem__t-i": 100
212            }
213        });
214
215        assert!(genome_has_plasticity(&genome));
216        assert!(has_memory_areas(&genome));
217        assert!(!has_stdp_connections(&genome));
218    }
219
220    #[test]
221    fn test_has_stdp_connections() {
222        let genome = json!({
223            "blueprint": {
224                "_____10c-Y2FfX19fX50=-cx-_group-t": "CUSTOM",
225                "_____10c-Y2FfX19fX50=-cx-memory-b": false,
226                "_____10c-Y2FfX19fX50=-cx-dstmap-d": {
227                    "b2ltZwkAAAA=": [{
228                        "morphology_id": "projector",
229                        "plasticity_flag": true,
230                        "postSynapticCurrent_multiplier": 1
231                    }]
232                }
233            }
234        });
235
236        assert!(genome_has_plasticity(&genome));
237        assert!(!has_memory_areas(&genome));
238        assert!(has_stdp_connections(&genome));
239    }
240
241    #[test]
242    fn test_has_both_plasticity_types() {
243        let genome = json!({
244            "blueprint": {
245                "_____10c-Y21fX19fXxg=-cx-memory-b": true,
246                "_____10c-Y2FfX19fX50=-cx-dstmap-d": {
247                    "b2ltZwkAAAA=": [{
248                        "morphology_id": "projector",
249                        "plasticity_flag": true
250                    }]
251                }
252            }
253        });
254
255        assert!(genome_has_plasticity(&genome));
256        assert!(has_memory_areas(&genome));
257        assert!(has_stdp_connections(&genome));
258    }
259
260    #[test]
261    fn test_extract_memory_properties() {
262        let mut properties = HashMap::new();
263        properties.insert("is_mem_type".to_string(), json!(true));
264        properties.insert("temporal_depth".to_string(), json!(5));
265        properties.insert("longterm_mem_threshold".to_string(), json!(200));
266        properties.insert("lifespan_growth_rate".to_string(), json!(1.5));
267        properties.insert("init_lifespan".to_string(), json!(15));
268
269        let mem_props = extract_memory_properties(&properties).expect("Should extract properties");
270        assert_eq!(mem_props.temporal_depth, 5);
271        assert_eq!(mem_props.longterm_threshold, 200);
272        assert_eq!(mem_props.lifespan_growth_rate, 1.5);
273        assert_eq!(mem_props.init_lifespan, 15);
274    }
275
276    #[test]
277    fn test_extract_memory_properties_defaults() {
278        let mut properties = HashMap::new();
279        properties.insert("is_mem_type".to_string(), json!(true));
280
281        let mem_props = extract_memory_properties(&properties).expect("Should extract properties");
282        assert_eq!(mem_props.temporal_depth, 1);
283        assert_eq!(mem_props.longterm_threshold, 100);
284        assert_eq!(mem_props.lifespan_growth_rate, 1.0);
285        assert_eq!(mem_props.init_lifespan, 9);
286    }
287
288    #[test]
289    fn test_extract_memory_properties_non_memory() {
290        let mut properties = HashMap::new();
291        properties.insert("is_mem_type".to_string(), json!(false));
292
293        assert!(extract_memory_properties(&properties).is_none());
294    }
295}