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