1use serde_json::Value;
7use std::collections::HashMap;
8use tracing::{debug, error};
9
10pub 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
35fn 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 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
60fn 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 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
92pub const MP_CHANGE_MODE_KEY: &str = "mp_change_mode";
94pub const MP_DELTA_QUANTIZATION_KEY: &str = "mp_delta_quantization";
96pub const MP_RATIO_QUANTIZATION_KEY: &str = "mp_ratio_quantization";
98pub const MP_LEARNING_ENABLED_KEY: &str = "mp_learning_enabled";
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub enum MpChangeMode {
108 #[default]
110 None,
111 Differential,
113 Ratio,
115}
116
117impl MpChangeMode {
118 pub const fn as_str(self) -> &'static str {
120 match self {
121 MpChangeMode::None => "none",
122 MpChangeMode::Differential => "mp_differential",
123 MpChangeMode::Ratio => "mp_ratio",
124 }
125 }
126
127 pub fn parse(value: &str) -> Option<Self> {
129 match value {
130 "none" => Some(MpChangeMode::None),
131 "mp_differential" => Some(MpChangeMode::Differential),
132 "mp_ratio" => Some(MpChangeMode::Ratio),
133 _ => None,
134 }
135 }
136}
137
138pub fn validate_memory_mp_properties(properties: &HashMap<String, Value>) -> Result<(), String> {
144 let mode = match properties.get(MP_CHANGE_MODE_KEY) {
145 None | Some(Value::Null) => MpChangeMode::None,
146 Some(Value::String(s)) => MpChangeMode::parse(s).ok_or_else(|| {
147 format!(
148 "{MP_CHANGE_MODE_KEY} '{s}' is invalid; expected one of 'none', 'mp_differential', 'mp_ratio'"
149 )
150 })?,
151 Some(other) => {
152 return Err(format!(
153 "{MP_CHANGE_MODE_KEY} must be a string, got {other}"
154 ))
155 }
156 };
157
158 for key in [MP_DELTA_QUANTIZATION_KEY, MP_RATIO_QUANTIZATION_KEY] {
159 match properties.get(key) {
160 None | Some(Value::Null) => {}
161 Some(value) => match value.as_f64() {
162 Some(q) if q.is_finite() && q > 0.0 => {}
163 _ => {
164 return Err(format!(
165 "{key} must be a number greater than 0, got {value}"
166 ))
167 }
168 },
169 }
170 }
171
172 let mp_learning = match properties.get(MP_LEARNING_ENABLED_KEY) {
173 None | Some(Value::Null) => false,
174 Some(Value::Bool(b)) => *b,
175 Some(other) => {
176 return Err(format!(
177 "{MP_LEARNING_ENABLED_KEY} must be a boolean, got {other}"
178 ))
179 }
180 };
181 if mp_learning && mode != MpChangeMode::None {
182 return Err(format!(
183 "{MP_LEARNING_ENABLED_KEY} and {MP_CHANGE_MODE_KEY}='{}' are mutually exclusive",
184 mode.as_str()
185 ));
186 }
187 Ok(())
188}
189
190#[derive(Debug, Clone)]
192pub struct MemoryAreaProperties {
193 pub temporal_depth: u32,
195 pub longterm_threshold: u32,
197 pub lifespan_growth_rate: f32,
199 pub init_lifespan: u32,
201 pub mp_learning_enabled: bool,
203 pub mp_change_mode: MpChangeMode,
205 pub mp_delta_quantization: f32,
207 pub mp_ratio_quantization: f32,
209 pub min_window_activity: u32,
211 pub scan_skip_density: f32,
213}
214
215impl Default for MemoryAreaProperties {
216 fn default() -> Self {
217 Self {
218 temporal_depth: 1,
221 longterm_threshold: 100,
222 lifespan_growth_rate: 1.0,
223 init_lifespan: 9,
224 mp_learning_enabled: false,
225 mp_change_mode: MpChangeMode::None,
226 mp_delta_quantization: 1.0,
227 mp_ratio_quantization: 20.0,
228 min_window_activity: 1,
229 scan_skip_density: 1.0,
230 }
231 }
232}
233
234pub fn extract_memory_properties(
243 properties: &HashMap<String, Value>,
244) -> Option<MemoryAreaProperties> {
245 let is_memory = properties
246 .get("is_mem_type")
247 .and_then(|v| v.as_bool())
248 .unwrap_or(false);
249
250 if !is_memory {
251 return None;
252 }
253
254 let defaults = MemoryAreaProperties::default();
255 let mp_change_mode = match properties.get(MP_CHANGE_MODE_KEY).and_then(|v| v.as_str()) {
256 None => defaults.mp_change_mode,
257 Some(s) => MpChangeMode::parse(s).unwrap_or_else(|| {
258 error!(
261 target: "feagi-evolutionary",
262 "Unknown {} '{}'; change encoding disabled for this memory area",
263 MP_CHANGE_MODE_KEY, s
264 );
265 MpChangeMode::None
266 }),
267 };
268
269 Some(MemoryAreaProperties {
270 temporal_depth: properties
271 .get("temporal_depth")
272 .and_then(|v| v.as_u64())
273 .unwrap_or(1)
274 .max(1) as u32,
275 longterm_threshold: properties
276 .get("longterm_mem_threshold")
277 .and_then(|v| v.as_u64())
278 .unwrap_or(100) as u32,
279 lifespan_growth_rate: properties
280 .get("lifespan_growth_rate")
281 .and_then(|v| v.as_f64())
282 .unwrap_or(1.0) as f32,
283 init_lifespan: properties
284 .get("init_lifespan")
285 .and_then(|v| v.as_u64())
286 .unwrap_or(9) as u32,
287 mp_learning_enabled: properties
288 .get(MP_LEARNING_ENABLED_KEY)
289 .and_then(|v| v.as_bool())
290 .unwrap_or(defaults.mp_learning_enabled),
291 mp_change_mode,
292 mp_delta_quantization: properties
293 .get(MP_DELTA_QUANTIZATION_KEY)
294 .and_then(|v| v.as_f64())
295 .map(|v| v as f32)
296 .unwrap_or(defaults.mp_delta_quantization),
297 mp_ratio_quantization: properties
298 .get(MP_RATIO_QUANTIZATION_KEY)
299 .and_then(|v| v.as_f64())
300 .map(|v| v as f32)
301 .unwrap_or(defaults.mp_ratio_quantization),
302 min_window_activity: properties
303 .get("min_window_activity")
304 .and_then(|v| v.as_u64())
305 .unwrap_or(1)
306 .max(1) as u32,
307 scan_skip_density: properties
308 .get("scan_skip_density")
309 .and_then(|v| v.as_f64())
310 .unwrap_or(1.0)
311 .clamp(0.0, 1.0) as f32,
312 })
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use serde_json::json;
319
320 #[test]
321 fn test_no_plasticity() {
322 let genome = json!({
323 "blueprint": {
324 "_____10c-Y2FfX19fX50=-cx-_group-t": "CUSTOM",
325 "_____10c-Y2FfX19fX50=-cx-memory-b": false,
326 "_____10c-Y2FfX19fX50=-cx-dstmap-d": {
327 "b2ltZwkAAAA=": [{
328 "morphology_id": "projector",
329 "plasticity_flag": false
330 }]
331 }
332 }
333 });
334
335 assert!(!genome_has_plasticity(&genome));
336 }
337
338 #[test]
339 fn test_has_memory_areas() {
340 let genome = json!({
341 "blueprint": {
342 "_____10c-Y21fX19fXxg=-cx-_group-t": "CUSTOM",
343 "_____10c-Y21fX19fXxg=-cx-memory-b": true,
344 "_____10c-Y21fX19fXxg=-cx-mem__t-i": 100
345 }
346 });
347
348 assert!(genome_has_plasticity(&genome));
349 assert!(has_memory_areas(&genome));
350 assert!(!has_stdp_connections(&genome));
351 }
352
353 #[test]
354 fn test_has_stdp_connections() {
355 let genome = json!({
356 "blueprint": {
357 "_____10c-Y2FfX19fX50=-cx-_group-t": "CUSTOM",
358 "_____10c-Y2FfX19fX50=-cx-memory-b": false,
359 "_____10c-Y2FfX19fX50=-cx-dstmap-d": {
360 "b2ltZwkAAAA=": [{
361 "morphology_id": "projector",
362 "plasticity_flag": true,
363 "postSynapticCurrent_multiplier": 1
364 }]
365 }
366 }
367 });
368
369 assert!(genome_has_plasticity(&genome));
370 assert!(!has_memory_areas(&genome));
371 assert!(has_stdp_connections(&genome));
372 }
373
374 #[test]
375 fn test_has_both_plasticity_types() {
376 let genome = json!({
377 "blueprint": {
378 "_____10c-Y21fX19fXxg=-cx-memory-b": true,
379 "_____10c-Y2FfX19fX50=-cx-dstmap-d": {
380 "b2ltZwkAAAA=": [{
381 "morphology_id": "projector",
382 "plasticity_flag": true
383 }]
384 }
385 }
386 });
387
388 assert!(genome_has_plasticity(&genome));
389 assert!(has_memory_areas(&genome));
390 assert!(has_stdp_connections(&genome));
391 }
392
393 #[test]
394 fn test_extract_memory_properties() {
395 let mut properties = HashMap::new();
396 properties.insert("is_mem_type".to_string(), json!(true));
397 properties.insert("temporal_depth".to_string(), json!(5));
398 properties.insert("longterm_mem_threshold".to_string(), json!(200));
399 properties.insert("lifespan_growth_rate".to_string(), json!(1.5));
400 properties.insert("init_lifespan".to_string(), json!(15));
401
402 let mem_props = extract_memory_properties(&properties).expect("Should extract properties");
403 assert_eq!(mem_props.temporal_depth, 5);
404 assert_eq!(mem_props.longterm_threshold, 200);
405 assert_eq!(mem_props.lifespan_growth_rate, 1.5);
406 assert_eq!(mem_props.init_lifespan, 15);
407 }
408
409 #[test]
410 fn test_extract_memory_properties_defaults() {
411 let mut properties = HashMap::new();
412 properties.insert("is_mem_type".to_string(), json!(true));
413
414 let mem_props = extract_memory_properties(&properties).expect("Should extract properties");
415 assert_eq!(mem_props.temporal_depth, 1);
416 assert_eq!(mem_props.longterm_threshold, 100);
417 assert_eq!(mem_props.lifespan_growth_rate, 1.0);
418 assert_eq!(mem_props.init_lifespan, 9);
419 }
420
421 fn memory_props(entries: &[(&str, Value)]) -> HashMap<String, Value> {
422 let mut properties = HashMap::new();
423 properties.insert("is_mem_type".to_string(), json!(true));
424 for (k, v) in entries {
425 properties.insert(k.to_string(), v.clone());
426 }
427 properties
428 }
429
430 #[test]
431 fn test_mp_change_defaults() {
432 let mem_props = extract_memory_properties(&memory_props(&[])).unwrap();
433 assert_eq!(mem_props.mp_change_mode, MpChangeMode::None);
434 assert_eq!(mem_props.mp_delta_quantization, 1.0);
435 assert_eq!(mem_props.mp_ratio_quantization, 20.0);
436 assert!(validate_memory_mp_properties(&memory_props(&[])).is_ok());
437 }
438
439 #[test]
440 fn test_mp_change_mode_round_trip() {
441 for mode in [
442 MpChangeMode::None,
443 MpChangeMode::Differential,
444 MpChangeMode::Ratio,
445 ] {
446 assert_eq!(MpChangeMode::parse(mode.as_str()), Some(mode));
447 }
448 assert_eq!(MpChangeMode::parse("MP_RATIO"), None);
449 assert_eq!(MpChangeMode::parse(""), None);
450 }
451
452 #[test]
453 fn test_extract_mp_change_properties() {
454 let props = memory_props(&[
455 (MP_CHANGE_MODE_KEY, json!("mp_ratio")),
456 (MP_DELTA_QUANTIZATION_KEY, json!(0.5)),
457 (MP_RATIO_QUANTIZATION_KEY, json!(10)),
458 ]);
459 let mem_props = extract_memory_properties(&props).unwrap();
460 assert_eq!(mem_props.mp_change_mode, MpChangeMode::Ratio);
461 assert_eq!(mem_props.mp_delta_quantization, 0.5);
462 assert_eq!(mem_props.mp_ratio_quantization, 10.0);
463 }
464
465 #[test]
466 fn test_validate_rejects_unknown_mode() {
467 let props = memory_props(&[(MP_CHANGE_MODE_KEY, json!("mp_percent"))]);
468 assert!(validate_memory_mp_properties(&props)
469 .unwrap_err()
470 .contains("mp_percent"));
471 let props = memory_props(&[(MP_CHANGE_MODE_KEY, json!(1))]);
472 assert!(validate_memory_mp_properties(&props).is_err());
473 }
474
475 #[test]
476 fn test_validate_rejects_non_positive_quantization() {
477 for key in [MP_DELTA_QUANTIZATION_KEY, MP_RATIO_QUANTIZATION_KEY] {
478 for bad in [json!(0), json!(-1.0), json!("1")] {
479 let props = memory_props(&[(key, bad.clone())]);
480 assert!(
481 validate_memory_mp_properties(&props).is_err(),
482 "{key}={bad} should be rejected"
483 );
484 }
485 }
486 }
487
488 #[test]
489 fn test_validate_rejects_mp_learning_with_change_mode() {
490 let props = memory_props(&[
491 (MP_LEARNING_ENABLED_KEY, json!(true)),
492 (MP_CHANGE_MODE_KEY, json!("mp_differential")),
493 ]);
494 assert!(validate_memory_mp_properties(&props)
495 .unwrap_err()
496 .contains("mutually exclusive"));
497
498 let props = memory_props(&[
499 (MP_LEARNING_ENABLED_KEY, json!(true)),
500 (MP_CHANGE_MODE_KEY, json!("none")),
501 ]);
502 assert!(validate_memory_mp_properties(&props).is_ok());
503 }
504
505 #[test]
506 fn test_extract_memory_properties_non_memory() {
507 let mut properties = HashMap::new();
508 properties.insert("is_mem_type".to_string(), json!(false));
509
510 assert!(extract_memory_properties(&properties).is_none());
511 }
512}