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 firing_threshold_limit: Option<f32>,
116 pub firing_threshold_increment_x: Option<f32>,
117 pub firing_threshold_increment_y: Option<f32>,
118 pub firing_threshold_increment_z: Option<f32>,
119 pub leak_coefficient: Option<f32>,
120 pub leak_variability: Option<f32>,
121 pub neuron_excitability: Option<f32>,
122 pub postsynaptic_current: Option<f32>,
123 pub postsynaptic_current_max: Option<f32>,
124 pub degeneration: Option<f32>,
125 pub psp_uniform_distribution: Option<bool>,
126 pub mp_charge_accumulation: Option<bool>,
127 pub mp_driven_psp: Option<bool>,
128 pub visualization: Option<bool>,
129 pub burst_engine_activation: Option<bool>,
130 #[serde(rename = "2d_coordinate")]
131 pub coordinate_2d: Option<Vec<i32>>,
132
133 pub is_mem_type: Option<bool>,
135 pub longterm_mem_threshold: Option<u32>,
136 pub lifespan_growth_rate: Option<f32>,
137 pub init_lifespan: Option<u32>,
138 pub temporal_depth: Option<u32>,
139 pub consecutive_fire_cnt_max: Option<u32>,
140 pub snooze_length: Option<u32>,
141
142 #[serde(flatten)]
144 pub other: HashMap<String, Value>,
145}
146
147#[derive(Debug, Clone, Deserialize, Serialize)]
149pub struct RawBrainRegion {
150 #[serde(alias = "name")]
151 pub title: Option<String>,
152 pub description: Option<String>,
153 pub parent_region_id: Option<String>,
154 pub coordinate_2d: Option<Vec<i32>>,
155 pub coordinate_3d: Option<Vec<i32>>,
156 #[serde(alias = "cortical_areas")]
157 pub areas: Option<Vec<String>>,
158 pub regions: Option<Vec<String>>,
159 pub inputs: Option<Vec<String>>,
160 pub outputs: Option<Vec<String>>,
161 pub designated_inputs: Option<Vec<String>>,
163 pub designated_outputs: Option<Vec<String>>,
164 pub signature: Option<String>,
165 pub properties: Option<HashMap<String, Value>>,
167}
168
169fn convert_dstmap_keys_to_base64(dstmap: &Value) -> Value {
173 if let Some(dstmap_obj) = dstmap.as_object() {
174 let mut converted = serde_json::Map::new();
175
176 for (dest_id_str, mapping_value) in dstmap_obj {
177 match string_to_cortical_id(dest_id_str) {
179 Ok(dest_cortical_id) => {
180 converted.insert(dest_cortical_id.as_base_64(), mapping_value.clone());
181 }
182 Err(e) => {
183 tracing::warn!(
185 "Failed to convert dstmap key '{}' to base64: {}, keeping original",
186 dest_id_str,
187 e
188 );
189 converted.insert(dest_id_str.clone(), mapping_value.clone());
190 }
191 }
192 }
193
194 Value::Object(converted)
195 } else {
196 dstmap.clone()
198 }
199}
200
201pub fn string_to_cortical_id(id_str: &str) -> EvoResult<CorticalID> {
205 use feagi_structures::genomic::cortical_area::CoreCorticalType;
206
207 if let Ok(cortical_id) = CorticalID::try_from_base_64(id_str) {
209 let mut bytes = [0u8; CorticalID::CORTICAL_ID_LENGTH];
210 cortical_id.write_id_to_bytes(&mut bytes);
211 if bytes == *b"___power" {
212 return Ok(CoreCorticalType::Power.to_cortical_id());
213 }
214 if bytes == *b"___death" {
215 return Ok(CoreCorticalType::Death.to_cortical_id());
216 }
217 if bytes == *b"___fatig" {
218 return Ok(CoreCorticalType::Fatigue.to_cortical_id());
219 }
220 return Ok(cortical_id);
221 }
222
223 if id_str == "_power" {
225 return Ok(CoreCorticalType::Power.to_cortical_id());
226 }
227 if id_str == "___pwr" {
229 return Ok(CoreCorticalType::Power.to_cortical_id());
230 }
231 if id_str == "___power" {
233 return Ok(CoreCorticalType::Power.to_cortical_id());
234 }
235 if id_str == "___pwr__" {
237 return Ok(CoreCorticalType::Power.to_cortical_id());
238 }
239 if id_str == "___death" {
240 return Ok(CoreCorticalType::Death.to_cortical_id());
241 }
242 if id_str == "___fatig" {
243 return Ok(CoreCorticalType::Fatigue.to_cortical_id());
244 }
245 if id_str == "_death" {
246 return Ok(CoreCorticalType::Death.to_cortical_id());
247 }
248 if id_str == "_fatigue" {
249 return Ok(CoreCorticalType::Fatigue.to_cortical_id());
250 }
251
252 if id_str.len() == 6 || id_str.len() == 8 {
254 CorticalID::try_from_legacy_ascii(id_str).map_err(|e| {
255 EvoError::InvalidArea(format!("Failed to convert cortical_id '{}': {}", id_str, e))
256 })
257 } else {
258 Err(EvoError::InvalidArea(format!(
259 "Invalid cortical_id length: '{}' (expected 6 or 8 ASCII chars, or base64)",
260 id_str
261 )))
262 }
263}
264
265pub struct GenomeParser;
267
268impl GenomeParser {
269 fn normalize_brain_region_cortical_id_list_properties(region: &mut BrainRegion, keys: &[&str]) {
271 for key in keys {
272 let Some(val) = region.get_property(key) else {
273 continue;
274 };
275 let Some(arr) = val.as_array() else {
276 continue;
277 };
278 let mut out: Vec<String> = Vec::new();
279 for item in arr {
280 let Some(s) = item.as_str() else {
281 continue;
282 };
283 match string_to_cortical_id(s) {
284 Ok(cortical_id) => out.push(cortical_id.as_base_64()),
285 Err(e) => {
286 warn!(target: "feagi-evo",
287 "Failed to convert brain region '{}' entry '{}': {}. Skipping.",
288 key, s, e);
289 }
290 }
291 }
292 if out.is_empty() {
293 region.properties.remove(*key);
294 } else {
295 region.add_property((*key).to_string(), serde_json::json!(out));
296 }
297 }
298 }
299
300 pub fn parse(json_str: &str) -> EvoResult<ParsedGenome> {
318 let raw: RawGenome = serde_json::from_str(json_str)
320 .map_err(|e| EvoError::InvalidGenome(format!("Failed to parse JSON: {}", e)))?;
321
322 if !raw.version.starts_with("2.") && !raw.version.starts_with("3.") && raw.version != "3" {
324 return Err(EvoError::InvalidGenome(format!(
325 "Unsupported genome version: {}. Expected 2.x or 3.x",
326 raw.version
327 )));
328 }
329
330 let cortical_areas = Self::parse_cortical_areas(&raw.blueprint)?;
332
333 let brain_regions = Self::parse_brain_regions(&raw.brain_regions)?;
335
336 Ok(ParsedGenome {
337 genome_id: raw.genome_id.unwrap_or_else(|| "unknown".to_string()),
338 genome_title: raw.genome_title.unwrap_or_else(|| "Untitled".to_string()),
339 version: raw.version,
340 cortical_areas,
341 brain_regions,
342 neuron_morphologies: raw.neuron_morphologies,
343 physiology: raw.physiology,
344 })
345 }
346
347 fn parse_cortical_areas(
349 blueprint: &HashMap<String, RawCorticalArea>,
350 ) -> EvoResult<Vec<CorticalArea>> {
351 let mut areas = Vec::with_capacity(blueprint.len());
352
353 for (cortical_id_str, raw_area) in blueprint.iter() {
354 if cortical_id_str.is_empty() {
356 warn!(target: "feagi-evo","Skipping empty cortical_id");
357 continue;
358 }
359
360 let cortical_id = match string_to_cortical_id(cortical_id_str) {
362 Ok(id) => id,
363 Err(e) => {
364 warn!(target: "feagi-evo","Skipping invalid cortical_id '{}': {}", cortical_id_str, e);
365 continue;
366 }
367 };
368
369 let name = raw_area
371 .cortical_name
372 .clone()
373 .unwrap_or_else(|| cortical_id_str.clone());
374
375 let dimensions = if let Some(boundaries) = &raw_area.block_boundaries {
376 if boundaries.len() != 3 {
377 return Err(EvoError::InvalidArea(format!(
378 "Invalid block_boundaries for {}: expected 3 values, got {}",
379 cortical_id_str,
380 boundaries.len()
381 )));
382 }
383 Dimensions::new(boundaries[0], boundaries[1], boundaries[2])
384 .map_err(|e| EvoError::InvalidArea(format!("Invalid dimensions: {}", e)))?
385 } else {
386 warn!(target: "feagi-evo","Cortical area {} missing block_boundaries, defaulting to 1x1x1", cortical_id_str);
388 Dimensions::new(1, 1, 1).map_err(|e| {
389 EvoError::InvalidArea(format!("Invalid default dimensions: {}", e))
390 })?
391 };
392
393 let position = if let Some(coords) = &raw_area.relative_coordinate {
394 if coords.len() != 3 {
395 return Err(EvoError::InvalidArea(format!(
396 "Invalid relative_coordinate for {}: expected 3 values, got {}",
397 cortical_id_str,
398 coords.len()
399 )));
400 }
401 GenomeCoordinate3D::new(coords[0], coords[1], coords[2])
402 } else {
403 warn!(target: "feagi-evo","Cortical area {} missing relative_coordinate, defaulting to (0,0,0)", cortical_id_str);
405 GenomeCoordinate3D::new(0, 0, 0)
406 };
407
408 let cortical_type = cortical_id.as_cortical_type().map_err(|e| {
410 EvoError::InvalidArea(format!(
411 "Failed to determine cortical type from ID {}: {}",
412 cortical_id_str, e
413 ))
414 })?;
415
416 let mut area = CorticalArea::new(
418 cortical_id,
419 0, name,
421 dimensions,
422 position,
423 cortical_type,
424 )?;
425
426 if let Some(ref cortical_type_str) = raw_area.cortical_type {
428 area.properties.insert(
429 "cortical_group".to_string(),
430 serde_json::json!(cortical_type_str),
431 );
432 }
433
434 if let Some(v) = raw_area.synapse_attractivity {
437 area.properties
438 .insert("synapse_attractivity".to_string(), serde_json::json!(v));
439 }
440 if let Some(v) = raw_area.refractory_period {
441 area.properties
442 .insert("refractory_period".to_string(), serde_json::json!(v));
443 }
444 if let Some(v) = raw_area.firing_threshold {
445 area.properties
446 .insert("firing_threshold".to_string(), serde_json::json!(v));
447 }
448 if let Some(v) = raw_area.firing_threshold_limit {
449 area.properties
450 .insert("firing_threshold_limit".to_string(), serde_json::json!(v));
451 }
452 if let Some(v) = raw_area.firing_threshold_increment_x {
453 area.properties.insert(
454 "firing_threshold_increment_x".to_string(),
455 serde_json::json!(v),
456 );
457 }
458 if let Some(v) = raw_area.firing_threshold_increment_y {
459 area.properties.insert(
460 "firing_threshold_increment_y".to_string(),
461 serde_json::json!(v),
462 );
463 }
464 if let Some(v) = raw_area.firing_threshold_increment_z {
465 area.properties.insert(
466 "firing_threshold_increment_z".to_string(),
467 serde_json::json!(v),
468 );
469 }
470 if let Some(v) = raw_area.leak_coefficient {
471 area.properties
472 .insert("leak_coefficient".to_string(), serde_json::json!(v));
473 }
474 if let Some(v) = raw_area.leak_variability {
475 area.properties
476 .insert("leak_variability".to_string(), serde_json::json!(v));
477 }
478 if let Some(v) = raw_area.neuron_excitability {
479 area.properties
480 .insert("neuron_excitability".to_string(), serde_json::json!(v));
481 }
482 if let Some(v) = raw_area.postsynaptic_current {
483 area.properties
484 .insert("postsynaptic_current".to_string(), serde_json::json!(v));
485 }
486 if let Some(v) = raw_area.postsynaptic_current_max {
487 area.properties
488 .insert("postsynaptic_current_max".to_string(), serde_json::json!(v));
489 }
490 if let Some(v) = raw_area.degeneration {
491 area.properties
492 .insert("degeneration".to_string(), serde_json::json!(v));
493 }
494
495 if let Some(v) = raw_area.psp_uniform_distribution {
497 area.properties
498 .insert("psp_uniform_distribution".to_string(), serde_json::json!(v));
499 }
500 if let Some(v) = raw_area.mp_charge_accumulation {
501 area.properties
502 .insert("mp_charge_accumulation".to_string(), serde_json::json!(v));
503 }
504 if let Some(v) = raw_area.mp_driven_psp {
505 area.properties
506 .insert("mp_driven_psp".to_string(), serde_json::json!(v));
507 tracing::info!(
508 target: "feagi-evo",
509 "[GENOME-LOAD] Loaded mp_driven_psp={} for area {}",
510 v,
511 cortical_id_str
512 );
513 } else {
514 tracing::debug!(
515 target: "feagi-evo",
516 "[GENOME-LOAD] mp_driven_psp not found in raw_area for {}, will use default=false",
517 cortical_id_str
518 );
519 }
520 if let Some(v) = raw_area.visualization {
521 area.properties
522 .insert("visualization".to_string(), serde_json::json!(v));
523 area.properties
525 .insert("visible".to_string(), serde_json::json!(v));
526 }
527 if let Some(v) = raw_area.burst_engine_activation {
528 area.properties
529 .insert("burst_engine_active".to_string(), serde_json::json!(v));
530 }
531 if let Some(v) = raw_area.is_mem_type {
532 area.properties
533 .insert("is_mem_type".to_string(), serde_json::json!(v));
534 }
535
536 if let Some(v) = raw_area.longterm_mem_threshold {
538 area.properties
539 .insert("longterm_mem_threshold".to_string(), serde_json::json!(v));
540 }
541 if let Some(v) = raw_area.lifespan_growth_rate {
542 area.properties
543 .insert("lifespan_growth_rate".to_string(), serde_json::json!(v));
544 }
545 if let Some(v) = raw_area.init_lifespan {
546 area.properties
547 .insert("init_lifespan".to_string(), serde_json::json!(v));
548 }
549 if let Some(v) = raw_area.temporal_depth {
550 area.properties
551 .insert("temporal_depth".to_string(), serde_json::json!(v));
552 }
553 if let Some(v) = raw_area.consecutive_fire_cnt_max {
554 area.properties
555 .insert("consecutive_fire_cnt_max".to_string(), serde_json::json!(v));
556 area.properties
558 .insert("consecutive_fire_limit".to_string(), serde_json::json!(v));
559 }
560 if let Some(v) = raw_area.snooze_length {
561 area.properties
562 .insert("snooze_period".to_string(), serde_json::json!(v));
563 }
564
565 if let Some(v) = &raw_area.group_id {
567 area.properties
568 .insert("group_id".to_string(), serde_json::json!(v));
569 }
570 if let Some(v) = &raw_area.sub_group_id {
571 area.properties
572 .insert("sub_group_id".to_string(), serde_json::json!(v));
573 }
574 if let Some(v) = raw_area.per_voxel_neuron_cnt {
576 area.properties
577 .insert("neurons_per_voxel".to_string(), serde_json::json!(v));
578 }
579 if let Some(v) = &raw_area.cortical_mapping_dst {
580 let converted_dstmap = convert_dstmap_keys_to_base64(v);
582 area.properties
583 .insert("cortical_mapping_dst".to_string(), converted_dstmap);
584 }
585 if let Some(v) = &raw_area.coordinate_2d {
586 area.properties
587 .insert("2d_coordinate".to_string(), serde_json::json!(v));
588 }
589
590 for (key, value) in &raw_area.other {
592 area.properties.insert(key.clone(), value.clone());
593 }
594
595 areas.push(area);
599 }
600
601 Ok(areas)
602 }
603
604 fn parse_brain_regions(
606 raw_regions: &HashMap<String, RawBrainRegion>,
607 ) -> EvoResult<Vec<(BrainRegion, Option<String>)>> {
608 let mut regions = Vec::with_capacity(raw_regions.len());
609
610 for (region_id_str, raw_region) in raw_regions.iter() {
611 let title = raw_region
612 .title
613 .clone()
614 .unwrap_or_else(|| region_id_str.clone());
615
616 let region_id = match RegionID::from_string(region_id_str) {
619 Ok(id) => id,
620 Err(_) => {
621 RegionID::new()
624 }
625 };
626
627 let region_type = RegionType::Undefined; let mut region = BrainRegion::new(region_id, title, region_type)?;
630
631 if let Some(props) = &raw_region.properties {
633 for (k, v) in props {
634 region.add_property(k.clone(), v.clone());
635 }
636 }
637
638 if let Some(areas) = &raw_region.areas {
640 for area_id in areas {
641 match string_to_cortical_id(area_id) {
643 Ok(cortical_id) => {
644 region.add_area(cortical_id);
645 }
646 Err(e) => {
647 warn!(target: "feagi-evo",
648 "Failed to convert brain region area ID '{}' to CorticalID: {}. Skipping.",
649 area_id, e);
650 }
651 }
652 }
653 }
654
655 if let Some(desc) = &raw_region.description {
657 region.add_property("description".to_string(), serde_json::json!(desc));
658 }
659 if let Some(coord_2d) = &raw_region.coordinate_2d {
660 region.add_property("coordinate_2d".to_string(), serde_json::json!(coord_2d));
661 }
662 if let Some(coord_3d) = &raw_region.coordinate_3d {
663 region.add_property("coordinate_3d".to_string(), serde_json::json!(coord_3d));
664 }
665 if let Some(inputs) = &raw_region.inputs {
667 let input_ids: Vec<String> = inputs
668 .iter()
669 .filter_map(|id| match string_to_cortical_id(id) {
670 Ok(cortical_id) => Some(cortical_id.as_base_64()),
671 Err(e) => {
672 warn!(target: "feagi-evo",
673 "Failed to convert brain region input ID '{}': {}. Skipping.",
674 id, e);
675 None
676 }
677 })
678 .collect();
679 if !input_ids.is_empty() {
680 region.add_property("inputs".to_string(), serde_json::json!(input_ids));
681 }
682 }
683 if let Some(outputs) = &raw_region.outputs {
684 let output_ids: Vec<String> = outputs
685 .iter()
686 .filter_map(|id| match string_to_cortical_id(id) {
687 Ok(cortical_id) => Some(cortical_id.as_base_64()),
688 Err(e) => {
689 warn!(target: "feagi-evo",
690 "Failed to convert brain region output ID '{}': {}. Skipping.",
691 id, e);
692 None
693 }
694 })
695 .collect();
696 if !output_ids.is_empty() {
697 region.add_property("outputs".to_string(), serde_json::json!(output_ids));
698 }
699 }
700 if let Some(signature) = &raw_region.signature {
701 region.add_property("signature".to_string(), serde_json::json!(signature));
702 }
703
704 if let Some(d) = &raw_region.designated_inputs {
705 let ids: Vec<String> = d
706 .iter()
707 .filter_map(|id| match string_to_cortical_id(id) {
708 Ok(cortical_id) => Some(cortical_id.as_base_64()),
709 Err(e) => {
710 warn!(target: "feagi-evo",
711 "Failed to convert designated_inputs entry '{}': {}. Skipping.",
712 id, e);
713 None
714 }
715 })
716 .collect();
717 if !ids.is_empty() {
718 region.add_property("designated_inputs".to_string(), serde_json::json!(ids));
719 }
720 }
721 if let Some(d) = &raw_region.designated_outputs {
722 let ids: Vec<String> = d
723 .iter()
724 .filter_map(|id| match string_to_cortical_id(id) {
725 Ok(cortical_id) => Some(cortical_id.as_base_64()),
726 Err(e) => {
727 warn!(target: "feagi-evo",
728 "Failed to convert designated_outputs entry '{}': {}. Skipping.",
729 id, e);
730 None
731 }
732 })
733 .collect();
734 if !ids.is_empty() {
735 region.add_property("designated_outputs".to_string(), serde_json::json!(ids));
736 }
737 }
738
739 Self::normalize_brain_region_cortical_id_list_properties(
740 &mut region,
741 &[
742 "inputs",
743 "outputs",
744 "designated_inputs",
745 "designated_outputs",
746 ],
747 );
748
749 let parent_id = raw_region.parent_region_id.clone();
751 if let Some(ref parent_id_str) = parent_id {
752 region.add_property(
754 "parent_region_id".to_string(),
755 serde_json::json!(parent_id_str),
756 );
757 }
758
759 regions.push((region, parent_id));
760 }
761
762 Ok(regions)
763 }
764}
765
766#[cfg(test)]
767mod tests {
768 use super::*;
769
770 #[test]
771 fn test_parse_minimal_genome() {
772 let json = r#"{
775 "version": "2.1",
776 "blueprint": {
777 "_power": {
778 "cortical_name": "Test Area",
779 "block_boundaries": [10, 10, 10],
780 "relative_coordinate": [0, 0, 0],
781 "cortical_type": "CORE"
782 }
783 },
784 "brain_regions": {
785 "root": {
786 "title": "Root",
787 "parent_region_id": null,
788 "areas": ["_power"]
789 }
790 }
791 }"#;
792
793 let parsed = GenomeParser::parse(json).unwrap();
794
795 assert_eq!(parsed.version, "2.1");
796 assert_eq!(parsed.cortical_areas.len(), 1);
797 assert_eq!(
799 parsed.cortical_areas[0].cortical_id.as_base_64(),
800 "X19fcG93ZXI="
801 );
802 assert_eq!(parsed.cortical_areas[0].name, "Test Area");
803 assert_eq!(parsed.brain_regions.len(), 1);
804
805 assert!(parsed.cortical_areas[0]
808 .cortical_id
809 .as_cortical_type()
810 .is_ok());
811 }
812
813 #[test]
814 fn test_parse_multiple_areas() {
815 let json = r#"{
817 "version": "2.1",
818 "blueprint": {
819 "_power": {
820 "cortical_name": "Area 1",
821 "cortical_type": "CORE",
822 "block_boundaries": [5, 5, 5],
823 "relative_coordinate": [0, 0, 0]
824 },
825 "_death": {
826 "cortical_name": "Area 2",
827 "cortical_type": "CORE",
828 "block_boundaries": [10, 10, 10],
829 "relative_coordinate": [5, 0, 0]
830 }
831 }
832 }"#;
833
834 let parsed = GenomeParser::parse(json).unwrap();
835
836 assert_eq!(parsed.cortical_areas.len(), 2);
837
838 for area in &parsed.cortical_areas {
840 assert!(
841 area.cortical_id.as_cortical_type().is_ok(),
842 "Area {} should have cortical_type_new populated",
843 area.cortical_id
844 );
845 }
846 }
847
848 #[test]
849 fn test_string_to_cortical_id_legacy_power_shorthand() {
850 use feagi_structures::genomic::cortical_area::CoreCorticalType;
853 let id = string_to_cortical_id("___pwr").unwrap();
854 assert_eq!(
855 id.as_base_64(),
856 CoreCorticalType::Power.to_cortical_id().as_base_64()
857 );
858 }
859
860 #[test]
861 fn test_string_to_cortical_id_legacy_power_padded() {
862 use feagi_structures::genomic::cortical_area::CoreCorticalType;
864 let id = string_to_cortical_id("___pwr__").unwrap();
865 assert_eq!(
866 id.as_base_64(),
867 CoreCorticalType::Power.to_cortical_id().as_base_64()
868 );
869 }
870
871 #[test]
872 fn test_parse_with_properties() {
873 let json = r#"{
874 "version": "2.1",
875 "blueprint": {
876 "mem001": {
877 "cortical_name": "Memory Area",
878 "block_boundaries": [8, 8, 8],
879 "relative_coordinate": [0, 0, 0],
880 "cortical_type": "MEMORY",
881 "is_mem_type": true,
882 "firing_threshold": 50.0,
883 "leak_coefficient": 0.9
884 }
885 }
886 }"#;
887
888 let parsed = GenomeParser::parse(json).unwrap();
889
890 assert_eq!(parsed.cortical_areas.len(), 1);
891 let area = &parsed.cortical_areas[0];
892
893 use feagi_structures::genomic::cortical_area::CorticalAreaType;
895 assert!(matches!(area.cortical_type, CorticalAreaType::Memory(_)));
896
897 assert!(area.properties.contains_key("is_mem_type"));
899 assert!(area.properties.contains_key("firing_threshold"));
900 assert!(area.properties.contains_key("cortical_group"));
901
902 assert!(
904 area.cortical_id.as_cortical_type().is_ok(),
905 "cortical_id should be parseable to cortical_type"
906 );
907 if let Ok(cortical_type) = area.cortical_id.as_cortical_type() {
908 use feagi_structures::genomic::cortical_area::CorticalAreaType;
909 assert!(
910 matches!(cortical_type, CorticalAreaType::Memory(_)),
911 "Should be classified as MEMORY type"
912 );
913 }
914 }
915
916 #[test]
918 fn test_parse_v3_brain_region_nested_properties_retains_designated_io() {
919 let json = r#"{
920 "version": "3.0",
921 "blueprint": {
922 "_power": {
923 "cortical_name": "Core",
924 "block_boundaries": [10, 10, 10],
925 "relative_coordinate": [0, 0, 0],
926 "cortical_type": "CORE"
927 }
928 },
929 "brain_regions": {
930 "550e8400-e29b-41d4-a716-446655440000": {
931 "name": "Sub",
932 "cortical_areas": ["_power"],
933 "properties": {
934 "designated_inputs": ["_power"],
935 "designated_outputs": []
936 }
937 }
938 }
939 }"#;
940
941 let parsed = GenomeParser::parse(json).unwrap();
942 assert_eq!(parsed.brain_regions.len(), 1);
943 let (region, _) = &parsed.brain_regions[0];
944 let di = region
945 .get_property("designated_inputs")
946 .and_then(|v| v.as_array())
947 .expect("designated_inputs");
948 assert_eq!(di.len(), 1);
949 assert_eq!(di[0].as_str().unwrap(), "X19fcG93ZXI=");
950 }
951
952 #[test]
953 fn test_invalid_version() {
954 let json = r#"{
955 "version": "1.0",
956 "blueprint": {}
957 }"#;
958
959 let result = GenomeParser::parse(json);
960 assert!(result.is_err());
961 }
962
963 #[test]
964 fn test_malformed_json() {
965 let json = r#"{ "version": "2.1", "blueprint": { malformed"#;
966
967 let result = GenomeParser::parse(json);
968 assert!(result.is_err());
969 }
970
971 #[test]
972 fn test_cortical_type_new_population() {
973 use feagi_structures::genomic::cortical_area::CoreCorticalType;
976 let power_id = CoreCorticalType::Power.to_cortical_id().as_base_64();
977 let json = format!(
978 r#"{{
979 "version": "2.1",
980 "blueprint": {{
981 "cvision1": {{
982 "cortical_name": "Test Custom Vision",
983 "cortical_type": "CUSTOM",
984 "block_boundaries": [10, 10, 1],
985 "relative_coordinate": [0, 0, 0]
986 }},
987 "cmotor01": {{
988 "cortical_name": "Test Custom Motor",
989 "cortical_type": "CUSTOM",
990 "block_boundaries": [5, 5, 1],
991 "relative_coordinate": [0, 0, 0]
992 }},
993 "{}": {{
994 "cortical_name": "Test Core",
995 "cortical_type": "CORE",
996 "block_boundaries": [1, 1, 1],
997 "relative_coordinate": [0, 0, 0]
998 }}
999 }}
1000 }}"#,
1001 power_id
1002 );
1003
1004 let parsed = GenomeParser::parse(&json).unwrap();
1005 assert_eq!(parsed.cortical_areas.len(), 3);
1006
1007 for area in &parsed.cortical_areas {
1009 assert!(
1010 area.cortical_id.as_cortical_type().is_ok(),
1011 "Area {} should have cortical_type_new populated",
1012 area.cortical_id
1013 );
1014
1015 assert!(
1017 area.properties.contains_key("cortical_group"),
1018 "Area {} should have cortical_group property",
1019 area.cortical_id
1020 );
1021
1022 if let Some(prop_group) = area
1024 .properties
1025 .get("cortical_group")
1026 .and_then(|v| v.as_str())
1027 {
1028 assert!(
1029 !prop_group.is_empty(),
1030 "Area {} should have non-empty cortical_group property",
1031 area.cortical_id.as_base_64()
1032 );
1033 }
1034 }
1035 }
1036}