1use serde::{Deserialize, Serialize};
44use serde_json::Value;
45use std::collections::HashMap;
46use tracing::warn;
47
48use crate::types::{EvoError, EvoResult};
49use feagi_structures::genomic::brain_regions::RegionID;
50use feagi_structures::genomic::cortical_area::CorticalID;
51use feagi_structures::genomic::cortical_area::{
52 CorticalArea, CorticalAreaDimensions as Dimensions,
53};
54use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
55use feagi_structures::genomic::{BrainRegion, RegionType};
56
57#[derive(Debug, Clone)]
59pub struct ParsedGenome {
60 pub genome_id: String,
62 pub genome_title: String,
63 pub version: String,
64
65 pub cortical_areas: Vec<CorticalArea>,
67
68 pub brain_regions: Vec<(BrainRegion, Option<String>)>, pub neuron_morphologies: HashMap<String, Value>,
73
74 pub physiology: Option<Value>,
76}
77
78#[derive(Debug, Clone, Deserialize, Serialize)]
80pub struct RawGenome {
81 pub genome_id: Option<String>,
82 pub genome_title: Option<String>,
83 pub genome_description: Option<String>,
84 pub version: String,
85 pub blueprint: HashMap<String, RawCorticalArea>,
86 #[serde(default)]
87 pub brain_regions: HashMap<String, RawBrainRegion>,
88 #[serde(default)]
89 pub neuron_morphologies: HashMap<String, Value>,
90 #[serde(default)]
91 pub physiology: Option<Value>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub brain_regions_root: Option<String>,
95}
96
97#[derive(Debug, Clone, Deserialize, Serialize)]
99pub struct RawCorticalArea {
100 pub cortical_name: Option<String>,
101 pub block_boundaries: Option<Vec<u32>>,
102 pub relative_coordinate: Option<Vec<i32>>,
103 pub cortical_type: Option<String>,
104
105 pub group_id: Option<String>,
107 pub sub_group_id: Option<String>,
108 pub per_voxel_neuron_cnt: Option<u32>,
109 pub cortical_mapping_dst: Option<Value>,
110
111 pub synapse_attractivity: Option<f32>,
113 pub refractory_period: Option<u32>,
114 pub firing_threshold: Option<f32>,
115 pub leak_coefficient: Option<f32>,
116 pub neuron_excitability: Option<f32>,
117 pub postsynaptic_current: Option<f32>,
118 pub postsynaptic_current_max: Option<f32>,
119 pub degeneration: Option<f32>,
120 pub psp_uniform_distribution: Option<bool>,
121 pub mp_charge_accumulation: Option<bool>,
122 pub mp_driven_psp: Option<bool>,
123 pub visualization: Option<bool>,
124 #[serde(rename = "2d_coordinate")]
125 pub coordinate_2d: Option<Vec<i32>>,
126
127 pub is_mem_type: Option<bool>,
129 pub longterm_mem_threshold: Option<u32>,
130 pub lifespan_growth_rate: Option<f32>,
131 pub init_lifespan: Option<u32>,
132 pub temporal_depth: Option<u32>,
133 pub consecutive_fire_cnt_max: Option<u32>,
134 pub snooze_length: Option<u32>,
135
136 #[serde(flatten)]
138 pub other: HashMap<String, Value>,
139}
140
141#[derive(Debug, Clone, Deserialize, Serialize)]
143pub struct RawBrainRegion {
144 pub title: Option<String>,
145 pub description: Option<String>,
146 pub parent_region_id: Option<String>,
147 pub coordinate_2d: Option<Vec<i32>>,
148 pub coordinate_3d: Option<Vec<i32>>,
149 pub areas: Option<Vec<String>>,
150 pub regions: Option<Vec<String>>,
151 pub inputs: Option<Vec<String>>,
152 pub outputs: Option<Vec<String>>,
153 pub signature: Option<String>,
154}
155
156fn convert_dstmap_keys_to_base64(dstmap: &Value) -> Value {
160 if let Some(dstmap_obj) = dstmap.as_object() {
161 let mut converted = serde_json::Map::new();
162
163 for (dest_id_str, mapping_value) in dstmap_obj {
164 match string_to_cortical_id(dest_id_str) {
166 Ok(dest_cortical_id) => {
167 converted.insert(dest_cortical_id.as_base_64(), mapping_value.clone());
168 }
169 Err(e) => {
170 tracing::warn!(
172 "Failed to convert dstmap key '{}' to base64: {}, keeping original",
173 dest_id_str,
174 e
175 );
176 converted.insert(dest_id_str.clone(), mapping_value.clone());
177 }
178 }
179 }
180
181 Value::Object(converted)
182 } else {
183 dstmap.clone()
185 }
186}
187
188pub fn string_to_cortical_id(id_str: &str) -> EvoResult<CorticalID> {
192 use feagi_structures::genomic::cortical_area::CoreCorticalType;
193
194 if let Ok(cortical_id) = CorticalID::try_from_base_64(id_str) {
196 return Ok(cortical_id);
197 }
198
199 if id_str == "_power" {
201 return Ok(CoreCorticalType::Power.to_cortical_id());
202 }
203 if id_str == "_death" {
204 return Ok(CoreCorticalType::Death.to_cortical_id());
205 }
206
207 if id_str.len() == 6 {
209 let mut bytes = [b'_'; 8];
211 bytes[..6].copy_from_slice(id_str.as_bytes());
212
213 CorticalID::try_from_bytes(&bytes).map_err(|e| {
214 EvoError::InvalidArea(format!("Failed to convert cortical_id '{}': {}", id_str, e))
215 })
216 } else if id_str.len() == 8 {
217 let mut bytes = [0u8; 8];
219 bytes.copy_from_slice(id_str.as_bytes());
220
221 CorticalID::try_from_bytes(&bytes).map_err(|e| {
222 EvoError::InvalidArea(format!("Failed to convert cortical_id '{}': {}", id_str, e))
223 })
224 } else {
225 Err(EvoError::InvalidArea(format!(
226 "Invalid cortical_id length: '{}' (expected 6 or 8 ASCII chars, or base64)",
227 id_str
228 )))
229 }
230}
231
232pub struct GenomeParser;
234
235impl GenomeParser {
236 pub fn parse(json_str: &str) -> EvoResult<ParsedGenome> {
254 let raw: RawGenome = serde_json::from_str(json_str)
256 .map_err(|e| EvoError::InvalidGenome(format!("Failed to parse JSON: {}", e)))?;
257
258 if !raw.version.starts_with("2.") {
260 return Err(EvoError::InvalidGenome(format!(
261 "Unsupported genome version: {}. Expected 2.x",
262 raw.version
263 )));
264 }
265
266 let cortical_areas = Self::parse_cortical_areas(&raw.blueprint)?;
268
269 let brain_regions = Self::parse_brain_regions(&raw.brain_regions)?;
271
272 Ok(ParsedGenome {
273 genome_id: raw.genome_id.unwrap_or_else(|| "unknown".to_string()),
274 genome_title: raw.genome_title.unwrap_or_else(|| "Untitled".to_string()),
275 version: raw.version,
276 cortical_areas,
277 brain_regions,
278 neuron_morphologies: raw.neuron_morphologies,
279 physiology: raw.physiology,
280 })
281 }
282
283 fn parse_cortical_areas(
285 blueprint: &HashMap<String, RawCorticalArea>,
286 ) -> EvoResult<Vec<CorticalArea>> {
287 let mut areas = Vec::with_capacity(blueprint.len());
288
289 for (cortical_id_str, raw_area) in blueprint.iter() {
290 if cortical_id_str.is_empty() {
292 warn!(target: "feagi-evo","Skipping empty cortical_id");
293 continue;
294 }
295
296 let cortical_id = match string_to_cortical_id(cortical_id_str) {
298 Ok(id) => id,
299 Err(e) => {
300 warn!(target: "feagi-evo","Skipping invalid cortical_id '{}': {}", cortical_id_str, e);
301 continue;
302 }
303 };
304
305 let name = raw_area
307 .cortical_name
308 .clone()
309 .unwrap_or_else(|| cortical_id_str.clone());
310
311 let dimensions = if let Some(boundaries) = &raw_area.block_boundaries {
312 if boundaries.len() != 3 {
313 return Err(EvoError::InvalidArea(format!(
314 "Invalid block_boundaries for {}: expected 3 values, got {}",
315 cortical_id_str,
316 boundaries.len()
317 )));
318 }
319 Dimensions::new(boundaries[0], boundaries[1], boundaries[2])
320 .map_err(|e| EvoError::InvalidArea(format!("Invalid dimensions: {}", e)))?
321 } else {
322 warn!(target: "feagi-evo","Cortical area {} missing block_boundaries, defaulting to 1x1x1", cortical_id_str);
324 Dimensions::new(1, 1, 1).map_err(|e| {
325 EvoError::InvalidArea(format!("Invalid default dimensions: {}", e))
326 })?
327 };
328
329 let position = if let Some(coords) = &raw_area.relative_coordinate {
330 if coords.len() != 3 {
331 return Err(EvoError::InvalidArea(format!(
332 "Invalid relative_coordinate for {}: expected 3 values, got {}",
333 cortical_id_str,
334 coords.len()
335 )));
336 }
337 GenomeCoordinate3D::new(coords[0], coords[1], coords[2])
338 } else {
339 warn!(target: "feagi-evo","Cortical area {} missing relative_coordinate, defaulting to (0,0,0)", cortical_id_str);
341 GenomeCoordinate3D::new(0, 0, 0)
342 };
343
344 let cortical_type = cortical_id.as_cortical_type().map_err(|e| {
346 EvoError::InvalidArea(format!(
347 "Failed to determine cortical type from ID {}: {}",
348 cortical_id_str, e
349 ))
350 })?;
351
352 let mut area = CorticalArea::new(
354 cortical_id,
355 0, name,
357 dimensions,
358 position,
359 cortical_type,
360 )?;
361
362 if let Some(ref cortical_type_str) = raw_area.cortical_type {
364 area.properties.insert(
365 "cortical_group".to_string(),
366 serde_json::json!(cortical_type_str),
367 );
368 }
369
370 if let Some(v) = raw_area.synapse_attractivity {
373 area.properties
374 .insert("synapse_attractivity".to_string(), serde_json::json!(v));
375 }
376 if let Some(v) = raw_area.refractory_period {
377 area.properties
378 .insert("refractory_period".to_string(), serde_json::json!(v));
379 }
380 if let Some(v) = raw_area.firing_threshold {
381 area.properties
382 .insert("firing_threshold".to_string(), serde_json::json!(v));
383 }
384 if let Some(v) = raw_area.leak_coefficient {
385 area.properties
386 .insert("leak_coefficient".to_string(), serde_json::json!(v));
387 }
388 if let Some(v) = raw_area.neuron_excitability {
389 area.properties
390 .insert("neuron_excitability".to_string(), serde_json::json!(v));
391 }
392 if let Some(v) = raw_area.postsynaptic_current {
393 area.properties
394 .insert("postsynaptic_current".to_string(), serde_json::json!(v));
395 }
396 if let Some(v) = raw_area.postsynaptic_current_max {
397 area.properties
398 .insert("postsynaptic_current_max".to_string(), serde_json::json!(v));
399 }
400 if let Some(v) = raw_area.degeneration {
401 area.properties
402 .insert("degeneration".to_string(), serde_json::json!(v));
403 }
404
405 if let Some(v) = raw_area.psp_uniform_distribution {
407 area.properties
408 .insert("psp_uniform_distribution".to_string(), serde_json::json!(v));
409 }
410 if let Some(v) = raw_area.mp_charge_accumulation {
411 area.properties
412 .insert("mp_charge_accumulation".to_string(), serde_json::json!(v));
413 }
414 if let Some(v) = raw_area.mp_driven_psp {
415 area.properties
416 .insert("mp_driven_psp".to_string(), serde_json::json!(v));
417 }
418 if let Some(v) = raw_area.visualization {
419 area.properties
420 .insert("visualization".to_string(), serde_json::json!(v));
421 }
422 if let Some(v) = raw_area.is_mem_type {
423 area.properties
424 .insert("is_mem_type".to_string(), serde_json::json!(v));
425 }
426
427 if let Some(v) = raw_area.longterm_mem_threshold {
429 area.properties
430 .insert("longterm_mem_threshold".to_string(), serde_json::json!(v));
431 }
432 if let Some(v) = raw_area.lifespan_growth_rate {
433 area.properties
434 .insert("lifespan_growth_rate".to_string(), serde_json::json!(v));
435 }
436 if let Some(v) = raw_area.init_lifespan {
437 area.properties
438 .insert("init_lifespan".to_string(), serde_json::json!(v));
439 }
440 if let Some(v) = raw_area.temporal_depth {
441 area.properties
442 .insert("temporal_depth".to_string(), serde_json::json!(v));
443 }
444 if let Some(v) = raw_area.consecutive_fire_cnt_max {
445 area.properties
446 .insert("consecutive_fire_cnt_max".to_string(), serde_json::json!(v));
447 }
448 if let Some(v) = raw_area.snooze_length {
449 area.properties
450 .insert("snooze_length".to_string(), serde_json::json!(v));
451 }
452
453 if let Some(v) = &raw_area.group_id {
455 area.properties
456 .insert("group_id".to_string(), serde_json::json!(v));
457 }
458 if let Some(v) = &raw_area.sub_group_id {
459 area.properties
460 .insert("sub_group_id".to_string(), serde_json::json!(v));
461 }
462 if let Some(v) = raw_area.per_voxel_neuron_cnt {
464 area.properties
465 .insert("neurons_per_voxel".to_string(), serde_json::json!(v));
466 }
467 if let Some(v) = &raw_area.cortical_mapping_dst {
468 let converted_dstmap = convert_dstmap_keys_to_base64(v);
470 area.properties
471 .insert("cortical_mapping_dst".to_string(), converted_dstmap);
472 }
473 if let Some(v) = &raw_area.coordinate_2d {
474 area.properties
475 .insert("2d_coordinate".to_string(), serde_json::json!(v));
476 }
477
478 for (key, value) in &raw_area.other {
480 area.properties.insert(key.clone(), value.clone());
481 }
482
483 areas.push(area);
487 }
488
489 Ok(areas)
490 }
491
492 fn parse_brain_regions(
494 raw_regions: &HashMap<String, RawBrainRegion>,
495 ) -> EvoResult<Vec<(BrainRegion, Option<String>)>> {
496 let mut regions = Vec::with_capacity(raw_regions.len());
497
498 for (region_id_str, raw_region) in raw_regions.iter() {
499 let title = raw_region
500 .title
501 .clone()
502 .unwrap_or_else(|| region_id_str.clone());
503
504 let region_id = match RegionID::from_string(region_id_str) {
507 Ok(id) => id,
508 Err(_) => {
509 RegionID::new()
512 }
513 };
514
515 let region_type = RegionType::Undefined; let mut region = BrainRegion::new(region_id, title, region_type)?;
518
519 if let Some(areas) = &raw_region.areas {
521 for area_id in areas {
522 match string_to_cortical_id(area_id) {
524 Ok(cortical_id) => {
525 region.add_area(cortical_id);
526 }
527 Err(e) => {
528 warn!(target: "feagi-evo",
529 "Failed to convert brain region area ID '{}' to CorticalID: {}. Skipping.",
530 area_id, e);
531 }
532 }
533 }
534 }
535
536 if let Some(desc) = &raw_region.description {
538 region.add_property("description".to_string(), serde_json::json!(desc));
539 }
540 if let Some(coord_2d) = &raw_region.coordinate_2d {
541 region.add_property("coordinate_2d".to_string(), serde_json::json!(coord_2d));
542 }
543 if let Some(coord_3d) = &raw_region.coordinate_3d {
544 region.add_property("coordinate_3d".to_string(), serde_json::json!(coord_3d));
545 }
546 if let Some(inputs) = &raw_region.inputs {
548 let input_ids: Vec<String> = inputs
549 .iter()
550 .filter_map(|id| match string_to_cortical_id(id) {
551 Ok(cortical_id) => Some(cortical_id.as_base_64()),
552 Err(e) => {
553 warn!(target: "feagi-evo",
554 "Failed to convert brain region input ID '{}': {}. Skipping.",
555 id, e);
556 None
557 }
558 })
559 .collect();
560 if !input_ids.is_empty() {
561 region.add_property("inputs".to_string(), serde_json::json!(input_ids));
562 }
563 }
564 if let Some(outputs) = &raw_region.outputs {
565 let output_ids: Vec<String> = outputs
566 .iter()
567 .filter_map(|id| match string_to_cortical_id(id) {
568 Ok(cortical_id) => Some(cortical_id.as_base_64()),
569 Err(e) => {
570 warn!(target: "feagi-evo",
571 "Failed to convert brain region output ID '{}': {}. Skipping.",
572 id, e);
573 None
574 }
575 })
576 .collect();
577 if !output_ids.is_empty() {
578 region.add_property("outputs".to_string(), serde_json::json!(output_ids));
579 }
580 }
581 if let Some(signature) = &raw_region.signature {
582 region.add_property("signature".to_string(), serde_json::json!(signature));
583 }
584
585 let parent_id = raw_region.parent_region_id.clone();
587 if let Some(ref parent_id_str) = parent_id {
588 region.add_property(
590 "parent_region_id".to_string(),
591 serde_json::json!(parent_id_str),
592 );
593 }
594
595 regions.push((region, parent_id));
596 }
597
598 Ok(regions)
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 fn test_parse_minimal_genome() {
608 let json = r#"{
611 "version": "2.1",
612 "blueprint": {
613 "_power": {
614 "cortical_name": "Test Area",
615 "block_boundaries": [10, 10, 10],
616 "relative_coordinate": [0, 0, 0],
617 "cortical_type": "CORE"
618 }
619 },
620 "brain_regions": {
621 "root": {
622 "title": "Root",
623 "parent_region_id": null,
624 "areas": ["_power"]
625 }
626 }
627 }"#;
628
629 let parsed = GenomeParser::parse(json).unwrap();
630
631 assert_eq!(parsed.version, "2.1");
632 assert_eq!(parsed.cortical_areas.len(), 1);
633 assert_eq!(
635 parsed.cortical_areas[0].cortical_id.as_base_64(),
636 "X19fcG93ZXI="
637 );
638 assert_eq!(parsed.cortical_areas[0].name, "Test Area");
639 assert_eq!(parsed.brain_regions.len(), 1);
640
641 assert!(parsed.cortical_areas[0]
644 .cortical_id
645 .as_cortical_type()
646 .is_ok());
647 }
648
649 #[test]
650 fn test_parse_multiple_areas() {
651 let json = r#"{
653 "version": "2.1",
654 "blueprint": {
655 "_power": {
656 "cortical_name": "Area 1",
657 "cortical_type": "CORE",
658 "block_boundaries": [5, 5, 5],
659 "relative_coordinate": [0, 0, 0]
660 },
661 "_death": {
662 "cortical_name": "Area 2",
663 "cortical_type": "CORE",
664 "block_boundaries": [10, 10, 10],
665 "relative_coordinate": [5, 0, 0]
666 }
667 }
668 }"#;
669
670 let parsed = GenomeParser::parse(json).unwrap();
671
672 assert_eq!(parsed.cortical_areas.len(), 2);
673
674 for area in &parsed.cortical_areas {
676 assert!(
677 area.cortical_id.as_cortical_type().is_ok(),
678 "Area {} should have cortical_type_new populated",
679 area.cortical_id
680 );
681 }
682 }
683
684 #[test]
685 fn test_parse_with_properties() {
686 let json = r#"{
687 "version": "2.1",
688 "blueprint": {
689 "mem001": {
690 "cortical_name": "Memory Area",
691 "block_boundaries": [8, 8, 8],
692 "relative_coordinate": [0, 0, 0],
693 "cortical_type": "MEMORY",
694 "is_mem_type": true,
695 "firing_threshold": 50.0,
696 "leak_coefficient": 0.9
697 }
698 }
699 }"#;
700
701 let parsed = GenomeParser::parse(json).unwrap();
702
703 assert_eq!(parsed.cortical_areas.len(), 1);
704 let area = &parsed.cortical_areas[0];
705
706 use feagi_structures::genomic::cortical_area::CorticalAreaType;
708 assert!(matches!(area.cortical_type, CorticalAreaType::Memory(_)));
709
710 assert!(area.properties.contains_key("is_mem_type"));
712 assert!(area.properties.contains_key("firing_threshold"));
713 assert!(area.properties.contains_key("cortical_group"));
714
715 assert!(
717 area.cortical_id.as_cortical_type().is_ok(),
718 "cortical_id should be parseable to cortical_type"
719 );
720 if let Ok(cortical_type) = area.cortical_id.as_cortical_type() {
721 use feagi_structures::genomic::cortical_area::CorticalAreaType;
722 assert!(
723 matches!(cortical_type, CorticalAreaType::Memory(_)),
724 "Should be classified as MEMORY type"
725 );
726 }
727 }
728
729 #[test]
730 fn test_invalid_version() {
731 let json = r#"{
732 "version": "1.0",
733 "blueprint": {}
734 }"#;
735
736 let result = GenomeParser::parse(json);
737 assert!(result.is_err());
738 }
739
740 #[test]
741 fn test_malformed_json() {
742 let json = r#"{ "version": "2.1", "blueprint": { malformed"#;
743
744 let result = GenomeParser::parse(json);
745 assert!(result.is_err());
746 }
747
748 #[test]
749 fn test_cortical_type_new_population() {
750 use feagi_structures::genomic::cortical_area::CoreCorticalType;
753 let power_id = CoreCorticalType::Power.to_cortical_id().as_base_64();
754 let json = format!(
755 r#"{{
756 "version": "2.1",
757 "blueprint": {{
758 "cvision1": {{
759 "cortical_name": "Test Custom Vision",
760 "cortical_type": "CUSTOM",
761 "block_boundaries": [10, 10, 1],
762 "relative_coordinate": [0, 0, 0]
763 }},
764 "cmotor01": {{
765 "cortical_name": "Test Custom Motor",
766 "cortical_type": "CUSTOM",
767 "block_boundaries": [5, 5, 1],
768 "relative_coordinate": [0, 0, 0]
769 }},
770 "{}": {{
771 "cortical_name": "Test Core",
772 "cortical_type": "CORE",
773 "block_boundaries": [1, 1, 1],
774 "relative_coordinate": [0, 0, 0]
775 }}
776 }}
777 }}"#,
778 power_id
779 );
780
781 let parsed = GenomeParser::parse(&json).unwrap();
782 assert_eq!(parsed.cortical_areas.len(), 3);
783
784 for area in &parsed.cortical_areas {
786 assert!(
787 area.cortical_id.as_cortical_type().is_ok(),
788 "Area {} should have cortical_type_new populated",
789 area.cortical_id
790 );
791
792 assert!(
794 area.properties.contains_key("cortical_group"),
795 "Area {} should have cortical_group property",
796 area.cortical_id
797 );
798
799 if let Some(prop_group) = area
801 .properties
802 .get("cortical_group")
803 .and_then(|v| v.as_str())
804 {
805 assert!(
806 !prop_group.is_empty(),
807 "Area {} should have non-empty cortical_group property",
808 area.cortical_id.as_base_64()
809 );
810 }
811 }
812 }
813}