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