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