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::classifiers::Classifier;
51use feagi_structures::genomic::cortical_area::CorticalID;
52use feagi_structures::genomic::cortical_area::{
53 CorticalArea, CorticalAreaDimensions as Dimensions,
54};
55use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
56use feagi_structures::genomic::{BrainRegion, RegionType};
57
58#[derive(Debug, Clone)]
60pub struct ParsedGenome {
61 pub genome_id: String,
63 pub genome_title: String,
64 pub version: String,
65
66 pub cortical_areas: Vec<CorticalArea>,
68
69 pub brain_regions: Vec<(BrainRegion, Option<String>)>, pub classifiers: Vec<Classifier>,
74
75 pub neuron_morphologies: HashMap<String, Value>,
77
78 pub physiology: Option<Value>,
80}
81
82#[derive(Debug, Clone, Deserialize, Serialize)]
84pub struct RawGenome {
85 pub genome_id: Option<String>,
86 pub genome_title: Option<String>,
87 pub genome_description: Option<String>,
88 pub version: String,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub genome_schema_version: Option<u32>,
95 pub blueprint: HashMap<String, RawCorticalArea>,
96 #[serde(default)]
97 pub brain_regions: HashMap<String, RawBrainRegion>,
98 #[serde(default)]
99 pub classifiers: HashMap<String, RawClassifier>,
100 #[serde(default)]
101 pub neuron_morphologies: HashMap<String, Value>,
102 #[serde(default)]
103 pub physiology: Option<Value>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub brain_regions_root: Option<String>,
107}
108
109#[derive(Debug, Clone, Deserialize, Serialize)]
111pub struct RawCorticalArea {
112 pub cortical_name: Option<String>,
113 pub block_boundaries: Option<Vec<u32>>,
114 pub relative_coordinate: Option<Vec<i32>>,
115 pub cortical_type: Option<String>,
116
117 pub group_id: Option<String>,
119 pub sub_group_id: Option<String>,
120 pub per_voxel_neuron_cnt: Option<u32>,
121 pub cortical_mapping_dst: Option<Value>,
122
123 pub synapse_attractivity: Option<f32>,
125 pub refractory_period: Option<u32>,
126 pub firing_threshold: Option<f32>,
127 pub firing_threshold_limit: Option<f32>,
128 pub firing_threshold_increment_x: Option<f32>,
129 pub firing_threshold_increment_y: Option<f32>,
130 pub firing_threshold_increment_z: Option<f32>,
131 pub leak_coefficient: Option<f32>,
132 pub leak_variability: Option<f32>,
133 pub neuron_excitability: Option<f32>,
134 pub postsynaptic_current: Option<f32>,
135 pub postsynaptic_current_max: Option<f32>,
136 pub degeneration: Option<f32>,
137 pub psp_uniform_distribution: Option<bool>,
138 pub mp_charge_accumulation: Option<bool>,
139 pub mp_driven_psp: Option<bool>,
140 pub visualization: Option<bool>,
141 pub burst_engine_activation: Option<bool>,
142 #[serde(rename = "2d_coordinate")]
143 pub coordinate_2d: Option<Vec<i32>>,
144
145 pub is_mem_type: Option<bool>,
147 pub longterm_mem_threshold: Option<u32>,
148 pub lifespan_growth_rate: Option<f32>,
149 pub init_lifespan: Option<u32>,
150 pub temporal_depth: Option<u32>,
151 pub mp_learning_enabled: Option<bool>,
152 pub min_window_activity: Option<u32>,
153 pub scan_skip_density: Option<f32>,
154 pub consecutive_fire_cnt_max: Option<u32>,
155 pub snooze_length: Option<u32>,
156
157 #[serde(flatten)]
159 pub other: HashMap<String, Value>,
160}
161
162#[derive(Debug, Clone, Deserialize, Serialize)]
164pub struct RawBrainRegion {
165 #[serde(alias = "name")]
166 pub title: Option<String>,
167 pub description: Option<String>,
168 pub parent_region_id: Option<String>,
169 pub coordinate_2d: Option<Vec<i32>>,
170 pub coordinate_3d: Option<Vec<i32>>,
171 #[serde(alias = "cortical_areas")]
172 pub areas: Option<Vec<String>>,
173 pub regions: Option<Vec<String>>,
174 pub inputs: Option<Vec<String>>,
175 pub outputs: Option<Vec<String>>,
176 pub designated_inputs: Option<Vec<String>>,
178 pub designated_outputs: Option<Vec<String>>,
179 pub signature: Option<String>,
180 pub properties: Option<HashMap<String, Value>>,
182}
183
184#[derive(Debug, Clone, Deserialize, Serialize)]
186pub struct RawClassifier {
187 #[serde(alias = "title")]
188 pub name: Option<String>,
189 pub parent_region_id: Option<String>,
190 #[serde(alias = "coordinate_3d")]
191 pub coordinates_3d: Option<Vec<i32>>,
192 pub kernel_area_id: Option<String>,
193 pub class_area_id: Option<String>,
194 #[serde(default)]
195 pub training_mode: Option<feagi_structures::genomic::classifiers::ClassifierTrainingMode>,
196 pub mask_area_id: Option<String>,
197 pub kernel_size: Option<[u32; 3]>,
198 pub fields: Option<Vec<feagi_structures::genomic::classifiers::ClassifierField>>,
200 pub field_area_id: Option<String>,
202 pub kernel_memory_id: Option<String>,
203 pub class_memory_id: Option<String>,
204 pub scan_twin_id: Option<String>,
206 pub properties: Option<HashMap<String, Value>>,
207}
208
209fn classifier_fields_from_raw(
210 raw: &RawClassifier,
211) -> Vec<feagi_structures::genomic::classifiers::ClassifierField> {
212 if let Some(fields) = &raw.fields {
213 return fields
214 .iter()
215 .filter(|field| !field.field_area_id.is_empty() && !field.scan_twin_id.is_empty())
216 .cloned()
217 .collect();
218 }
219 match (&raw.field_area_id, &raw.scan_twin_id) {
220 (Some(field_area_id), Some(scan_twin_id))
221 if !field_area_id.is_empty() && !scan_twin_id.is_empty() =>
222 {
223 vec![feagi_structures::genomic::classifiers::ClassifierField {
224 field_area_id: field_area_id.clone(),
225 scan_twin_id: scan_twin_id.clone(),
226 }]
227 }
228 _ => Vec::new(),
229 }
230}
231
232fn convert_dstmap_keys_to_base64(dstmap: &Value) -> Value {
236 if let Some(dstmap_obj) = dstmap.as_object() {
237 let mut converted = serde_json::Map::new();
238
239 for (dest_id_str, mapping_value) in dstmap_obj {
240 match string_to_cortical_id(dest_id_str) {
242 Ok(dest_cortical_id) => {
243 converted.insert(dest_cortical_id.as_base_64(), mapping_value.clone());
244 }
245 Err(e) => {
246 tracing::warn!(
248 "Failed to convert dstmap key '{}' to base64: {}, keeping original",
249 dest_id_str,
250 e
251 );
252 converted.insert(dest_id_str.clone(), mapping_value.clone());
253 }
254 }
255 }
256
257 Value::Object(converted)
258 } else {
259 dstmap.clone()
261 }
262}
263
264pub fn string_to_cortical_id(id_str: &str) -> EvoResult<CorticalID> {
268 use feagi_structures::genomic::cortical_area::CoreCorticalType;
269
270 if let Ok(cortical_id) = CorticalID::try_from_base_64(id_str) {
272 let mut bytes = [0u8; CorticalID::CORTICAL_ID_LENGTH];
273 cortical_id.write_id_to_bytes(&mut bytes);
274 if bytes == *b"___power" {
275 return Ok(CoreCorticalType::Power.to_cortical_id());
276 }
277 if bytes == *b"___death" {
278 return Ok(CoreCorticalType::Death.to_cortical_id());
279 }
280 if bytes == *b"___fatig" {
281 return Ok(CoreCorticalType::Fatigue.to_cortical_id());
282 }
283 if bytes == *b"___pain_" {
284 return Ok(CoreCorticalType::Pain.to_cortical_id());
285 }
286 if bytes == *b"___pleas" {
287 return Ok(CoreCorticalType::Pleasure.to_cortical_id());
288 }
289 if bytes == *b"___fear_" {
290 return Ok(CoreCorticalType::Fear.to_cortical_id());
291 }
292 if bytes == *b"___hope_" {
293 return Ok(CoreCorticalType::Hope.to_cortical_id());
294 }
295 return Ok(cortical_id);
296 }
297
298 if id_str == "_power" {
300 return Ok(CoreCorticalType::Power.to_cortical_id());
301 }
302 if id_str == "___pwr" {
304 return Ok(CoreCorticalType::Power.to_cortical_id());
305 }
306 if id_str == "___power" {
308 return Ok(CoreCorticalType::Power.to_cortical_id());
309 }
310 if id_str == "___pwr__" {
312 return Ok(CoreCorticalType::Power.to_cortical_id());
313 }
314 if id_str == "___death" {
315 return Ok(CoreCorticalType::Death.to_cortical_id());
316 }
317 if id_str == "___fatig" {
318 return Ok(CoreCorticalType::Fatigue.to_cortical_id());
319 }
320 if id_str == "___pain_" {
321 return Ok(CoreCorticalType::Pain.to_cortical_id());
322 }
323 if id_str == "___pleas" {
324 return Ok(CoreCorticalType::Pleasure.to_cortical_id());
325 }
326 if id_str == "___fear_" {
327 return Ok(CoreCorticalType::Fear.to_cortical_id());
328 }
329 if id_str == "___hope_" {
330 return Ok(CoreCorticalType::Hope.to_cortical_id());
331 }
332 if id_str == "_death" {
333 return Ok(CoreCorticalType::Death.to_cortical_id());
334 }
335 if id_str == "_fatigue" {
336 return Ok(CoreCorticalType::Fatigue.to_cortical_id());
337 }
338 if id_str == "_pain" {
339 return Ok(CoreCorticalType::Pain.to_cortical_id());
340 }
341 if id_str == "_pleasure" {
342 return Ok(CoreCorticalType::Pleasure.to_cortical_id());
343 }
344 if id_str == "_fear" {
345 return Ok(CoreCorticalType::Fear.to_cortical_id());
346 }
347 if id_str == "_hope" {
348 return Ok(CoreCorticalType::Hope.to_cortical_id());
349 }
350
351 if id_str.len() == 6 || id_str.len() == 8 {
353 CorticalID::try_from_legacy_ascii(id_str).map_err(|e| {
354 EvoError::InvalidArea(format!("Failed to convert cortical_id '{}': {}", id_str, e))
355 })
356 } else {
357 Err(EvoError::InvalidArea(format!(
358 "Invalid cortical_id length: '{}' (expected 6 or 8 ASCII chars, or base64)",
359 id_str
360 )))
361 }
362}
363
364pub struct GenomeParser;
366
367type NormalizedClassifierTraining = (
369 Option<String>,
370 Option<String>,
371 Option<String>,
372 Option<[u32; 3]>,
373);
374
375impl GenomeParser {
376 fn normalize_brain_region_cortical_id_list_properties(region: &mut BrainRegion, keys: &[&str]) {
378 for key in keys {
379 let Some(val) = region.get_property(key) else {
380 continue;
381 };
382 let Some(arr) = val.as_array() else {
383 continue;
384 };
385 let mut out: Vec<String> = Vec::new();
386 for item in arr {
387 let Some(s) = item.as_str() else {
388 continue;
389 };
390 match string_to_cortical_id(s) {
391 Ok(cortical_id) => out.push(cortical_id.as_base_64()),
392 Err(e) => {
393 warn!(target: "feagi-evo",
394 "Failed to convert brain region '{}' entry '{}': {}. Skipping.",
395 key, s, e);
396 }
397 }
398 }
399 if out.is_empty() {
400 region.properties.remove(*key);
401 } else {
402 region.add_property((*key).to_string(), serde_json::json!(out));
403 }
404 }
405 }
406
407 pub fn parse(json_str: &str) -> EvoResult<ParsedGenome> {
425 let raw: RawGenome = serde_json::from_str(json_str)
427 .map_err(|e| EvoError::InvalidGenome(format!("Failed to parse JSON: {}", e)))?;
428
429 if !raw.version.starts_with("2.") && !raw.version.starts_with("3.") && raw.version != "3" {
431 return Err(EvoError::InvalidGenome(format!(
432 "Unsupported genome version: {}. Expected 2.x or 3.x",
433 raw.version
434 )));
435 }
436
437 let cortical_areas = Self::parse_cortical_areas(&raw.blueprint)?;
439
440 let brain_regions = Self::parse_brain_regions(&raw.brain_regions)?;
442 let classifiers = Self::parse_classifiers(&raw.classifiers)?;
443
444 Ok(ParsedGenome {
445 genome_id: raw.genome_id.unwrap_or_else(|| "unknown".to_string()),
446 genome_title: raw.genome_title.unwrap_or_else(|| "Untitled".to_string()),
447 version: raw.version,
448 cortical_areas,
449 brain_regions,
450 classifiers,
451 neuron_morphologies: raw.neuron_morphologies,
452 physiology: raw.physiology,
453 })
454 }
455
456 fn parse_cortical_areas(
458 blueprint: &HashMap<String, RawCorticalArea>,
459 ) -> EvoResult<Vec<CorticalArea>> {
460 let mut areas = Vec::with_capacity(blueprint.len());
461
462 for (cortical_id_str, raw_area) in blueprint.iter() {
463 if cortical_id_str.is_empty() {
465 warn!(target: "feagi-evo","Skipping empty cortical_id");
466 continue;
467 }
468
469 let cortical_id = match string_to_cortical_id(cortical_id_str) {
471 Ok(id) => id,
472 Err(e) => {
473 warn!(target: "feagi-evo","Skipping invalid cortical_id '{}': {}", cortical_id_str, e);
474 continue;
475 }
476 };
477
478 let name = raw_area
480 .cortical_name
481 .clone()
482 .unwrap_or_else(|| cortical_id_str.clone());
483
484 let dimensions = if let Some(boundaries) = &raw_area.block_boundaries {
485 if boundaries.len() != 3 {
486 return Err(EvoError::InvalidArea(format!(
487 "Invalid block_boundaries for {}: expected 3 values, got {}",
488 cortical_id_str,
489 boundaries.len()
490 )));
491 }
492 Dimensions::new(boundaries[0], boundaries[1], boundaries[2])
493 .map_err(|e| EvoError::InvalidArea(format!("Invalid dimensions: {}", e)))?
494 } else {
495 warn!(target: "feagi-evo","Cortical area {} missing block_boundaries, defaulting to 1x1x1", cortical_id_str);
497 Dimensions::new(1, 1, 1).map_err(|e| {
498 EvoError::InvalidArea(format!("Invalid default dimensions: {}", e))
499 })?
500 };
501
502 let position = if let Some(coords) = &raw_area.relative_coordinate {
503 if coords.len() != 3 {
504 return Err(EvoError::InvalidArea(format!(
505 "Invalid relative_coordinate for {}: expected 3 values, got {}",
506 cortical_id_str,
507 coords.len()
508 )));
509 }
510 GenomeCoordinate3D::new(coords[0], coords[1], coords[2])
511 } else {
512 warn!(target: "feagi-evo","Cortical area {} missing relative_coordinate, defaulting to (0,0,0)", cortical_id_str);
514 GenomeCoordinate3D::new(0, 0, 0)
515 };
516
517 let cortical_type = cortical_id.as_cortical_type().map_err(|e| {
519 EvoError::InvalidArea(format!(
520 "Failed to determine cortical type from ID {}: {}",
521 cortical_id_str, e
522 ))
523 })?;
524
525 let mut area = CorticalArea::new(
527 cortical_id,
528 0, name,
530 dimensions,
531 position,
532 cortical_type,
533 )?;
534
535 if let Some(ref cortical_type_str) = raw_area.cortical_type {
537 area.properties.insert(
538 "cortical_group".to_string(),
539 serde_json::json!(cortical_type_str),
540 );
541 }
542
543 if let Some(v) = raw_area.synapse_attractivity {
546 area.properties
547 .insert("synapse_attractivity".to_string(), serde_json::json!(v));
548 }
549 if let Some(v) = raw_area.refractory_period {
550 area.properties
551 .insert("refractory_period".to_string(), serde_json::json!(v));
552 }
553 if let Some(v) = raw_area.firing_threshold {
554 area.properties
555 .insert("firing_threshold".to_string(), serde_json::json!(v));
556 }
557 if let Some(v) = raw_area.firing_threshold_limit {
558 area.properties
559 .insert("firing_threshold_limit".to_string(), serde_json::json!(v));
560 }
561 if let Some(v) = raw_area.firing_threshold_increment_x {
562 area.properties.insert(
563 "firing_threshold_increment_x".to_string(),
564 serde_json::json!(v),
565 );
566 }
567 if let Some(v) = raw_area.firing_threshold_increment_y {
568 area.properties.insert(
569 "firing_threshold_increment_y".to_string(),
570 serde_json::json!(v),
571 );
572 }
573 if let Some(v) = raw_area.firing_threshold_increment_z {
574 area.properties.insert(
575 "firing_threshold_increment_z".to_string(),
576 serde_json::json!(v),
577 );
578 }
579 if let Some(v) = raw_area.leak_coefficient {
580 area.properties
581 .insert("leak_coefficient".to_string(), serde_json::json!(v));
582 }
583 if let Some(v) = raw_area.leak_variability {
584 area.properties
585 .insert("leak_variability".to_string(), serde_json::json!(v));
586 }
587 if let Some(v) = raw_area.neuron_excitability {
588 area.properties
589 .insert("neuron_excitability".to_string(), serde_json::json!(v));
590 }
591 if let Some(v) = raw_area.postsynaptic_current {
592 area.properties
593 .insert("postsynaptic_current".to_string(), serde_json::json!(v));
594 }
595 if let Some(v) = raw_area.postsynaptic_current_max {
596 area.properties
597 .insert("postsynaptic_current_max".to_string(), serde_json::json!(v));
598 }
599 if let Some(v) = raw_area.degeneration {
600 area.properties
601 .insert("degeneration".to_string(), serde_json::json!(v));
602 }
603
604 if let Some(v) = raw_area.psp_uniform_distribution {
606 area.properties
607 .insert("psp_uniform_distribution".to_string(), serde_json::json!(v));
608 }
609 if let Some(v) = raw_area.mp_charge_accumulation {
610 area.properties
611 .insert("mp_charge_accumulation".to_string(), serde_json::json!(v));
612 }
613 if let Some(v) = raw_area.mp_driven_psp {
614 area.properties
615 .insert("mp_driven_psp".to_string(), serde_json::json!(v));
616 tracing::info!(
617 target: "feagi-evo",
618 "[GENOME-LOAD] Loaded mp_driven_psp={} for area {}",
619 v,
620 cortical_id_str
621 );
622 } else {
623 tracing::debug!(
624 target: "feagi-evo",
625 "[GENOME-LOAD] mp_driven_psp not found in raw_area for {}, will use default=false",
626 cortical_id_str
627 );
628 }
629 if let Some(v) = raw_area.visualization {
630 area.properties
631 .insert("visualization".to_string(), serde_json::json!(v));
632 area.properties
634 .insert("visible".to_string(), serde_json::json!(v));
635 }
636 if let Some(v) = raw_area.burst_engine_activation {
637 area.properties
638 .insert("burst_engine_active".to_string(), serde_json::json!(v));
639 }
640 if let Some(v) = raw_area.is_mem_type {
641 area.properties
642 .insert("is_mem_type".to_string(), serde_json::json!(v));
643 }
644
645 if let Some(v) = raw_area.longterm_mem_threshold {
647 area.properties
648 .insert("longterm_mem_threshold".to_string(), serde_json::json!(v));
649 }
650 if let Some(v) = raw_area.lifespan_growth_rate {
651 area.properties
652 .insert("lifespan_growth_rate".to_string(), serde_json::json!(v));
653 }
654 if let Some(v) = raw_area.init_lifespan {
655 area.properties
656 .insert("init_lifespan".to_string(), serde_json::json!(v));
657 }
658 if let Some(v) = raw_area.temporal_depth {
659 area.properties
660 .insert("temporal_depth".to_string(), serde_json::json!(v));
661 }
662 if let Some(v) = raw_area.mp_learning_enabled {
663 area.properties
664 .insert("mp_learning_enabled".to_string(), serde_json::json!(v));
665 }
666 if let Some(v) = raw_area.min_window_activity {
667 area.properties
668 .insert("min_window_activity".to_string(), serde_json::json!(v));
669 }
670 if let Some(v) = raw_area.scan_skip_density {
671 area.properties
672 .insert("scan_skip_density".to_string(), serde_json::json!(v));
673 }
674 if let Some(v) = raw_area.consecutive_fire_cnt_max {
675 area.properties
676 .insert("consecutive_fire_cnt_max".to_string(), serde_json::json!(v));
677 area.properties
679 .insert("consecutive_fire_limit".to_string(), serde_json::json!(v));
680 }
681 if let Some(v) = raw_area.snooze_length {
682 area.properties
683 .insert("snooze_period".to_string(), serde_json::json!(v));
684 }
685
686 if let Some(v) = &raw_area.group_id {
688 area.properties
689 .insert("group_id".to_string(), serde_json::json!(v));
690 }
691 if let Some(v) = &raw_area.sub_group_id {
692 area.properties
693 .insert("sub_group_id".to_string(), serde_json::json!(v));
694 }
695 if let Some(v) = raw_area.per_voxel_neuron_cnt {
697 area.properties
698 .insert("neurons_per_voxel".to_string(), serde_json::json!(v));
699 }
700 if let Some(v) = &raw_area.cortical_mapping_dst {
701 let converted_dstmap = convert_dstmap_keys_to_base64(v);
703 area.properties
704 .insert("cortical_mapping_dst".to_string(), converted_dstmap);
705 }
706 if let Some(v) = &raw_area.coordinate_2d {
707 area.properties
708 .insert("2d_coordinate".to_string(), serde_json::json!(v));
709 }
710
711 for (key, value) in &raw_area.other {
713 area.properties.insert(key.clone(), value.clone());
714 }
715
716 areas.push(area);
720 }
721
722 Ok(areas)
723 }
724
725 fn parse_brain_regions(
727 raw_regions: &HashMap<String, RawBrainRegion>,
728 ) -> EvoResult<Vec<(BrainRegion, Option<String>)>> {
729 let mut regions = Vec::with_capacity(raw_regions.len());
730
731 for (region_id_str, raw_region) in raw_regions.iter() {
732 let title = raw_region
733 .title
734 .clone()
735 .unwrap_or_else(|| region_id_str.clone());
736
737 let region_id = match RegionID::from_string(region_id_str) {
740 Ok(id) => id,
741 Err(_) => {
742 RegionID::new()
745 }
746 };
747
748 let region_type = RegionType::Undefined; let mut region = BrainRegion::new(region_id, title, region_type)?;
751
752 if let Some(props) = &raw_region.properties {
754 for (k, v) in props {
755 region.add_property(k.clone(), v.clone());
756 }
757 }
758
759 if let Some(areas) = &raw_region.areas {
761 for area_id in areas {
762 match string_to_cortical_id(area_id) {
764 Ok(cortical_id) => {
765 region.add_area(cortical_id);
766 }
767 Err(e) => {
768 warn!(target: "feagi-evo",
769 "Failed to convert brain region area ID '{}' to CorticalID: {}. Skipping.",
770 area_id, e);
771 }
772 }
773 }
774 }
775
776 if let Some(desc) = &raw_region.description {
778 region.add_property("description".to_string(), serde_json::json!(desc));
779 }
780 if let Some(coord_2d) = &raw_region.coordinate_2d {
781 region.add_property("coordinate_2d".to_string(), serde_json::json!(coord_2d));
782 }
783 if let Some(coord_3d) = &raw_region.coordinate_3d {
784 region.add_property("coordinate_3d".to_string(), serde_json::json!(coord_3d));
785 }
786 if let Some(inputs) = &raw_region.inputs {
788 let input_ids: Vec<String> = inputs
789 .iter()
790 .filter_map(|id| match string_to_cortical_id(id) {
791 Ok(cortical_id) => Some(cortical_id.as_base_64()),
792 Err(e) => {
793 warn!(target: "feagi-evo",
794 "Failed to convert brain region input ID '{}': {}. Skipping.",
795 id, e);
796 None
797 }
798 })
799 .collect();
800 if !input_ids.is_empty() {
801 region.add_property("inputs".to_string(), serde_json::json!(input_ids));
802 }
803 }
804 if let Some(outputs) = &raw_region.outputs {
805 let output_ids: Vec<String> = outputs
806 .iter()
807 .filter_map(|id| match string_to_cortical_id(id) {
808 Ok(cortical_id) => Some(cortical_id.as_base_64()),
809 Err(e) => {
810 warn!(target: "feagi-evo",
811 "Failed to convert brain region output ID '{}': {}. Skipping.",
812 id, e);
813 None
814 }
815 })
816 .collect();
817 if !output_ids.is_empty() {
818 region.add_property("outputs".to_string(), serde_json::json!(output_ids));
819 }
820 }
821 if let Some(signature) = &raw_region.signature {
822 region.add_property("signature".to_string(), serde_json::json!(signature));
823 }
824
825 if let Some(d) = &raw_region.designated_inputs {
826 let ids: Vec<String> = d
827 .iter()
828 .filter_map(|id| match string_to_cortical_id(id) {
829 Ok(cortical_id) => Some(cortical_id.as_base_64()),
830 Err(e) => {
831 warn!(target: "feagi-evo",
832 "Failed to convert designated_inputs entry '{}': {}. Skipping.",
833 id, e);
834 None
835 }
836 })
837 .collect();
838 if !ids.is_empty() {
839 region.add_property("designated_inputs".to_string(), serde_json::json!(ids));
840 }
841 }
842 if let Some(d) = &raw_region.designated_outputs {
843 let ids: Vec<String> = d
844 .iter()
845 .filter_map(|id| match string_to_cortical_id(id) {
846 Ok(cortical_id) => Some(cortical_id.as_base_64()),
847 Err(e) => {
848 warn!(target: "feagi-evo",
849 "Failed to convert designated_outputs entry '{}': {}. Skipping.",
850 id, e);
851 None
852 }
853 })
854 .collect();
855 if !ids.is_empty() {
856 region.add_property("designated_outputs".to_string(), serde_json::json!(ids));
857 }
858 }
859
860 Self::normalize_brain_region_cortical_id_list_properties(
861 &mut region,
862 &[
863 "inputs",
864 "outputs",
865 "designated_inputs",
866 "designated_outputs",
867 ],
868 );
869
870 let parent_id = raw_region.parent_region_id.clone();
872 if let Some(ref parent_id_str) = parent_id {
873 region.add_property(
875 "parent_region_id".to_string(),
876 serde_json::json!(parent_id_str),
877 );
878 }
879
880 regions.push((region, parent_id));
881 }
882
883 Ok(regions)
884 }
885
886 fn normalize_classifier_training(
887 classifier_id: &str,
888 training_mode: feagi_structures::genomic::classifiers::ClassifierTrainingMode,
889 kernel_area_id: Option<String>,
890 class_area_id: Option<String>,
891 mask_area_id: Option<String>,
892 kernel_size: Option<[u32; 3]>,
893 ) -> EvoResult<NormalizedClassifierTraining> {
894 use feagi_structures::genomic::classifiers::ClassifierTrainingMode;
895 match training_mode {
896 ClassifierTrainingMode::Kernel => {
897 if mask_area_id.is_some() || kernel_size.is_some() {
898 return Err(EvoError::InvalidArea(format!(
899 "Classifier '{classifier_id}' is in kernel mode and cannot store a mask or kernel size"
900 )));
901 }
902 Ok((kernel_area_id, class_area_id, None, None))
903 }
904 ClassifierTrainingMode::Scanner => {
905 if kernel_area_id.is_some() || class_area_id.is_some() {
906 return Err(EvoError::InvalidArea(format!(
907 "Classifier '{classifier_id}' is in scanner mode and cannot store kernel or class areas"
908 )));
909 }
910 let mask = mask_area_id
911 .filter(|id| !id.trim().is_empty())
912 .ok_or_else(|| {
913 EvoError::InvalidArea(format!(
914 "Classifier '{classifier_id}' is in scanner mode and is missing mask_area_id"
915 ))
916 })?;
917 let size = kernel_size.ok_or_else(|| {
918 EvoError::InvalidArea(format!(
919 "Classifier '{classifier_id}' is in scanner mode and is missing kernel_size"
920 ))
921 })?;
922 feagi_structures::genomic::classifiers::validate_kernel_size(size).map_err(
923 |e| EvoError::InvalidArea(format!("Classifier '{classifier_id}' {e}")),
924 )?;
925 Ok((None, None, Some(mask), Some(size)))
926 }
927 }
928 }
929
930 fn parse_classifiers(
931 raw_classifiers: &HashMap<String, RawClassifier>,
932 ) -> EvoResult<Vec<Classifier>> {
933 let mut classifiers = Vec::with_capacity(raw_classifiers.len());
934 for (classifier_id, raw) in raw_classifiers {
935 let name = raw
936 .name
937 .clone()
938 .filter(|n| !n.trim().is_empty())
939 .ok_or_else(|| {
940 EvoError::InvalidArea(format!("Classifier '{}' is missing name", classifier_id))
941 })?;
942 let parent_region_id = raw
943 .parent_region_id
944 .clone()
945 .filter(|n| !n.trim().is_empty())
946 .ok_or_else(|| {
947 EvoError::InvalidArea(format!(
948 "Classifier '{}' is missing parent_region_id",
949 classifier_id
950 ))
951 })?;
952 let coordinates_3d = match &raw.coordinates_3d {
953 Some(coords) if coords.len() == 3 => [coords[0], coords[1], coords[2]],
954 Some(coords) => {
955 return Err(EvoError::InvalidArea(format!(
956 "Classifier '{}' coordinates_3d must have 3 values, got {}",
957 classifier_id,
958 coords.len()
959 )))
960 }
961 None => [0, 0, 0],
962 };
963 let kernel_memory_id = raw.kernel_memory_id.clone().ok_or_else(|| {
964 EvoError::InvalidArea(format!(
965 "Classifier '{}' is missing kernel_memory_id",
966 classifier_id
967 ))
968 })?;
969 let class_memory_id = raw.class_memory_id.clone().ok_or_else(|| {
970 EvoError::InvalidArea(format!(
971 "Classifier '{}' is missing class_memory_id",
972 classifier_id
973 ))
974 })?;
975 let fields = classifier_fields_from_raw(raw);
976 let training_mode = raw.training_mode.unwrap_or_default();
977 let (kernel_area_id, class_area_id, mask_area_id, kernel_size) =
978 Self::normalize_classifier_training(
979 classifier_id,
980 training_mode,
981 raw.kernel_area_id.clone(),
982 raw.class_area_id.clone(),
983 raw.mask_area_id.clone(),
984 raw.kernel_size,
985 )?;
986 classifiers.push(Classifier {
987 classifier_id: classifier_id.clone(),
988 name,
989 parent_region_id,
990 coordinates_3d,
991 training_mode,
992 kernel_area_id,
993 class_area_id,
994 mask_area_id,
995 kernel_size,
996 fields,
997 kernel_memory_id,
998 class_memory_id,
999 properties: raw.properties.clone().unwrap_or_default(),
1000 });
1001 }
1002 Ok(classifiers)
1003 }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009
1010 #[test]
1011 fn test_parse_minimal_genome() {
1012 let json = r#"{
1015 "version": "2.1",
1016 "blueprint": {
1017 "_power": {
1018 "cortical_name": "Test Area",
1019 "block_boundaries": [10, 10, 10],
1020 "relative_coordinate": [0, 0, 0],
1021 "cortical_type": "CORE"
1022 }
1023 },
1024 "brain_regions": {
1025 "root": {
1026 "title": "Root",
1027 "parent_region_id": null,
1028 "areas": ["_power"]
1029 }
1030 }
1031 }"#;
1032
1033 let parsed = GenomeParser::parse(json).unwrap();
1034
1035 assert_eq!(parsed.version, "2.1");
1036 assert_eq!(parsed.cortical_areas.len(), 1);
1037 assert_eq!(
1039 parsed.cortical_areas[0].cortical_id.as_base_64(),
1040 "X19fcG93ZXI="
1041 );
1042 assert_eq!(parsed.cortical_areas[0].name, "Test Area");
1043 assert_eq!(parsed.brain_regions.len(), 1);
1044
1045 assert!(parsed.cortical_areas[0]
1048 .cortical_id
1049 .as_cortical_type()
1050 .is_ok());
1051 }
1052
1053 #[test]
1054 fn test_parse_multiple_areas() {
1055 let json = r#"{
1057 "version": "2.1",
1058 "blueprint": {
1059 "_power": {
1060 "cortical_name": "Area 1",
1061 "cortical_type": "CORE",
1062 "block_boundaries": [5, 5, 5],
1063 "relative_coordinate": [0, 0, 0]
1064 },
1065 "_death": {
1066 "cortical_name": "Area 2",
1067 "cortical_type": "CORE",
1068 "block_boundaries": [10, 10, 10],
1069 "relative_coordinate": [5, 0, 0]
1070 }
1071 }
1072 }"#;
1073
1074 let parsed = GenomeParser::parse(json).unwrap();
1075 assert!(parsed.classifiers.is_empty());
1076
1077 assert_eq!(parsed.cortical_areas.len(), 2);
1078
1079 for area in &parsed.cortical_areas {
1081 assert!(
1082 area.cortical_id.as_cortical_type().is_ok(),
1083 "Area {} should have cortical_type_new populated",
1084 area.cortical_id
1085 );
1086 }
1087 }
1088
1089 #[test]
1090 fn test_string_to_cortical_id_legacy_power_shorthand() {
1091 use feagi_structures::genomic::cortical_area::CoreCorticalType;
1094 let id = string_to_cortical_id("___pwr").unwrap();
1095 assert_eq!(
1096 id.as_base_64(),
1097 CoreCorticalType::Power.to_cortical_id().as_base_64()
1098 );
1099 }
1100
1101 #[test]
1102 fn test_parse_raw_imu_magnetometer_wire_id() {
1103 let json = r#"{
1106 "version": "3.0",
1107 "blueprint": {
1108 "aXJpbScAAgA=": {
1109 "cortical_name": "feagi_body_imu__Abdomen-2",
1110 "block_boundaries": [3, 1, 10],
1111 "relative_coordinate": [90, 0, -10],
1112 "cortical_type": "IPU"
1113 }
1114 },
1115 "brain_regions": {}
1116 }"#;
1117
1118 let parsed = GenomeParser::parse(json).expect("Raw IMU magnetometer genome");
1119 assert_eq!(parsed.cortical_areas.len(), 1);
1120 assert_eq!(
1121 parsed.cortical_areas[0].cortical_id.as_base_64(),
1122 "aXJpbScAAgA="
1123 );
1124 parsed.cortical_areas[0]
1125 .cortical_id
1126 .as_cortical_type()
1127 .expect("magnetometer IO flag must decode");
1128 }
1129
1130 #[test]
1131 fn test_parse_positional_servo_speed_wire_id() {
1132 let json = r#"{
1135 "version": "3.0",
1136 "blueprint": {
1137 "b3BzZSEAAAA=": {
1138 "cortical_name": "Positional Servo Speed",
1139 "block_boundaries": [6, 1, 20],
1140 "relative_coordinate": [-58, 0, -10],
1141 "cortical_type": "OPU"
1142 }
1143 },
1144 "brain_regions": {}
1145 }"#;
1146
1147 let parsed = GenomeParser::parse(json).expect("Positional Servo Speed genome");
1148 assert_eq!(parsed.cortical_areas.len(), 1);
1149 assert_eq!(
1150 parsed.cortical_areas[0].cortical_id.as_base_64(),
1151 "b3BzZSEAAAA="
1152 );
1153 parsed.cortical_areas[0]
1154 .cortical_id
1155 .as_cortical_type()
1156 .expect("positional servo speed IO flag must decode");
1157 }
1158
1159 #[test]
1160 fn test_string_to_cortical_id_legacy_power_padded() {
1161 use feagi_structures::genomic::cortical_area::CoreCorticalType;
1163 let id = string_to_cortical_id("___pwr__").unwrap();
1164 assert_eq!(
1165 id.as_base_64(),
1166 CoreCorticalType::Power.to_cortical_id().as_base_64()
1167 );
1168 }
1169
1170 #[test]
1171 fn test_parse_with_properties() {
1172 let json = r#"{
1173 "version": "2.1",
1174 "blueprint": {
1175 "mem001": {
1176 "cortical_name": "Memory Area",
1177 "block_boundaries": [8, 8, 8],
1178 "relative_coordinate": [0, 0, 0],
1179 "cortical_type": "MEMORY",
1180 "is_mem_type": true,
1181 "firing_threshold": 50.0,
1182 "leak_coefficient": 0.9
1183 }
1184 }
1185 }"#;
1186
1187 let parsed = GenomeParser::parse(json).unwrap();
1188
1189 assert_eq!(parsed.cortical_areas.len(), 1);
1190 let area = &parsed.cortical_areas[0];
1191
1192 use feagi_structures::genomic::cortical_area::CorticalAreaType;
1194 assert!(matches!(area.cortical_type, CorticalAreaType::Memory(_)));
1195
1196 assert!(area.properties.contains_key("is_mem_type"));
1198 assert!(area.properties.contains_key("firing_threshold"));
1199 assert!(area.properties.contains_key("cortical_group"));
1200
1201 assert!(
1203 area.cortical_id.as_cortical_type().is_ok(),
1204 "cortical_id should be parseable to cortical_type"
1205 );
1206 if let Ok(cortical_type) = area.cortical_id.as_cortical_type() {
1207 use feagi_structures::genomic::cortical_area::CorticalAreaType;
1208 assert!(
1209 matches!(cortical_type, CorticalAreaType::Memory(_)),
1210 "Should be classified as MEMORY type"
1211 );
1212 }
1213 }
1214
1215 #[test]
1217 fn test_parse_v3_brain_region_nested_properties_retains_designated_io() {
1218 let json = r#"{
1219 "version": "3.0",
1220 "blueprint": {
1221 "_power": {
1222 "cortical_name": "Core",
1223 "block_boundaries": [10, 10, 10],
1224 "relative_coordinate": [0, 0, 0],
1225 "cortical_type": "CORE"
1226 }
1227 },
1228 "brain_regions": {
1229 "550e8400-e29b-41d4-a716-446655440000": {
1230 "name": "Sub",
1231 "cortical_areas": ["_power"],
1232 "properties": {
1233 "designated_inputs": ["_power"],
1234 "designated_outputs": []
1235 }
1236 }
1237 }
1238 }"#;
1239
1240 let parsed = GenomeParser::parse(json).unwrap();
1241 assert_eq!(parsed.brain_regions.len(), 1);
1242 let (region, _) = &parsed.brain_regions[0];
1243 let di = region
1244 .get_property("designated_inputs")
1245 .and_then(|v| v.as_array())
1246 .expect("designated_inputs");
1247 assert_eq!(di.len(), 1);
1248 assert_eq!(di[0].as_str().unwrap(), "X19fcG93ZXI=");
1249 }
1250
1251 #[test]
1252 fn test_parse_brain_region_plain_text_description() {
1253 let json = r#"{
1254 "version": "2.1",
1255 "blueprint": {
1256 "_power": {
1257 "cortical_name": "Core",
1258 "block_boundaries": [10, 10, 10],
1259 "relative_coordinate": [0, 0, 0],
1260 "cortical_type": "CORE"
1261 }
1262 },
1263 "brain_regions": {
1264 "root": {
1265 "title": "Root",
1266 "description": "Holds core physiology and embodiment IO",
1267 "parent_region_id": null,
1268 "areas": ["_power"]
1269 }
1270 }
1271 }"#;
1272
1273 let parsed = GenomeParser::parse(json).unwrap();
1274 assert_eq!(parsed.brain_regions.len(), 1);
1275 let (region, _) = &parsed.brain_regions[0];
1276 assert_eq!(
1277 region.get_property("description"),
1278 Some(&serde_json::json!(
1279 "Holds core physiology and embodiment IO"
1280 ))
1281 );
1282 }
1283
1284 #[test]
1285 fn test_invalid_version() {
1286 let json = r#"{
1287 "version": "1.0",
1288 "blueprint": {}
1289 }"#;
1290
1291 let result = GenomeParser::parse(json);
1292 assert!(result.is_err());
1293 }
1294
1295 #[test]
1296 fn test_malformed_json() {
1297 let json = r#"{ "version": "2.1", "blueprint": { malformed"#;
1298
1299 let result = GenomeParser::parse(json);
1300 assert!(result.is_err());
1301 }
1302
1303 #[test]
1304 fn test_cortical_type_new_population() {
1305 use feagi_structures::genomic::cortical_area::CoreCorticalType;
1308 let power_id = CoreCorticalType::Power.to_cortical_id().as_base_64();
1309 let json = format!(
1310 r#"{{
1311 "version": "2.1",
1312 "blueprint": {{
1313 "cvision1": {{
1314 "cortical_name": "Test Custom Vision",
1315 "cortical_type": "CUSTOM",
1316 "block_boundaries": [10, 10, 1],
1317 "relative_coordinate": [0, 0, 0]
1318 }},
1319 "cmotor01": {{
1320 "cortical_name": "Test Custom Motor",
1321 "cortical_type": "CUSTOM",
1322 "block_boundaries": [5, 5, 1],
1323 "relative_coordinate": [0, 0, 0]
1324 }},
1325 "{}": {{
1326 "cortical_name": "Test Core",
1327 "cortical_type": "CORE",
1328 "block_boundaries": [1, 1, 1],
1329 "relative_coordinate": [0, 0, 0]
1330 }}
1331 }}
1332 }}"#,
1333 power_id
1334 );
1335
1336 let parsed = GenomeParser::parse(&json).unwrap();
1337 assert_eq!(parsed.cortical_areas.len(), 3);
1338
1339 for area in &parsed.cortical_areas {
1341 assert!(
1342 area.cortical_id.as_cortical_type().is_ok(),
1343 "Area {} should have cortical_type_new populated",
1344 area.cortical_id
1345 );
1346
1347 assert!(
1349 area.properties.contains_key("cortical_group"),
1350 "Area {} should have cortical_group property",
1351 area.cortical_id
1352 );
1353
1354 if let Some(prop_group) = area
1356 .properties
1357 .get("cortical_group")
1358 .and_then(|v| v.as_str())
1359 {
1360 assert!(
1361 !prop_group.is_empty(),
1362 "Area {} should have non-empty cortical_group property",
1363 area.cortical_id.as_base_64()
1364 );
1365 }
1366 }
1367 }
1368
1369 #[test]
1370 fn test_parse_classifiers_key_parallel_to_regions() {
1371 let json = r#"{
1372 "version": "3.0",
1373 "blueprint": {
1374 "cfield": {
1375 "cortical_name": "Field",
1376 "cortical_type": "CUSTOM",
1377 "block_boundaries": [4, 4, 1],
1378 "relative_coordinate": [0, 0, 0]
1379 },
1380 "mkmem1": {
1381 "cortical_name": "KernelMem",
1382 "cortical_type": "MEMORY",
1383 "block_boundaries": [2, 2, 2],
1384 "relative_coordinate": [10, 0, 0]
1385 },
1386 "mcmem1": {
1387 "cortical_name": "ClassMem",
1388 "cortical_type": "MEMORY",
1389 "block_boundaries": [2, 2, 2],
1390 "relative_coordinate": [20, 0, 0]
1391 },
1392 "cscan1": {
1393 "cortical_name": "ScanTwin",
1394 "cortical_type": "CUSTOM",
1395 "block_boundaries": [4, 4, 3],
1396 "relative_coordinate": [30, 0, 0]
1397 }
1398 },
1399 "brain_regions": {
1400 "root": {
1401 "title": "root",
1402 "parent_region_id": "",
1403 "coordinate_2d": [0, 0],
1404 "coordinate_3d": [0, 0, 0],
1405 "areas": ["cfield", "mkmem1", "mcmem1", "cscan1"],
1406 "regions": [],
1407 "inputs": [],
1408 "outputs": []
1409 }
1410 },
1411 "classifiers": {
1412 "clf-1": {
1413 "name": "object_class",
1414 "parent_region_id": "root",
1415 "coordinates_3d": [30, 0, 0],
1416 "field_area_id": "cfield",
1417 "kernel_memory_id": "mkmem1",
1418 "class_memory_id": "mcmem1",
1419 "scan_twin_id": "cscan1"
1420 }
1421 }
1422 }"#;
1423
1424 let parsed = GenomeParser::parse(json).expect("classifier genome");
1425 assert_eq!(parsed.classifiers.len(), 1);
1426 let classifier = &parsed.classifiers[0];
1427 assert_eq!(classifier.classifier_id, "clf-1");
1428 assert_eq!(classifier.name, "object_class");
1429 assert_eq!(classifier.parent_region_id, "root");
1430 assert_eq!(classifier.fields.len(), 1);
1431 assert_eq!(classifier.fields[0].field_area_id, "cfield");
1432 assert_eq!(classifier.kernel_memory_id, "mkmem1");
1433 assert_eq!(classifier.class_memory_id, "mcmem1");
1434 assert_eq!(classifier.fields[0].scan_twin_id, "cscan1");
1435 assert_eq!(classifier.owned_area_ids().len(), 3);
1436 assert_eq!(
1437 classifier.training_mode,
1438 feagi_structures::genomic::classifiers::ClassifierTrainingMode::Kernel
1439 );
1440 assert!(classifier.mask_area_id.is_none());
1441 assert!(classifier.kernel_size.is_none());
1442 }
1443
1444 #[test]
1445 fn test_parse_scanner_classifier_round_trip_fields() {
1446 let json = r#"{
1447 "version": "3.0",
1448 "blueprint": {},
1449 "brain_regions": {},
1450 "classifiers": {
1451 "clf-scan": {
1452 "name": "scan",
1453 "parent_region_id": "root",
1454 "coordinates_3d": [1, 2, 3],
1455 "training_mode": "scanner",
1456 "mask_area_id": "cmask",
1457 "kernel_size": [8, 8, 3],
1458 "kernel_memory_id": "mkmem1",
1459 "class_memory_id": "mcmem1"
1460 }
1461 }
1462 }"#;
1463 let parsed = GenomeParser::parse(json).expect("scanner classifier");
1464 let classifier = &parsed.classifiers[0];
1465 assert_eq!(
1466 classifier.training_mode,
1467 feagi_structures::genomic::classifiers::ClassifierTrainingMode::Scanner
1468 );
1469 assert_eq!(classifier.mask_area_id.as_deref(), Some("cmask"));
1470 assert_eq!(classifier.kernel_size, Some([8, 8, 3]));
1471 assert!(classifier.kernel_area_id.is_none());
1472 }
1473}