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