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