Skip to main content

feagi_evolutionary/genome/
parser.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Genome JSON parser.
6
7Parses FEAGI 2.1 genome JSON format into runtime data structures.
8
9## Genome Structure (v2.1)
10
11```json
12{
13  "genome_id": "...",
14  "genome_title": "...",
15  "version": "2.1",
16  "blueprint": {
17    "cortical_id": {
18      "cortical_name": "...",
19      "block_boundaries": [x, y, z],
20      "relative_coordinate": [x, y, z],
21      "cortical_type": "IPU/OPU/CUSTOM/CORE/MEMORY",
22      ...
23    }
24  },
25  "brain_regions": {
26    "root": {
27      "title": "...",
28      "parent_region_id": null,
29      "coordinate_3d": [x, y, z],
30      "areas": ["cortical_id1", ...],
31      "regions": ["child_region_id1", ...]
32    }
33  },
34  "neuron_morphologies": { ... },
35  "physiology": { ... }
36}
37```
38
39Copyright 2025 Neuraville Inc.
40Licensed under the Apache License, Version 2.0
41*/
42
43use 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/// Parsed genome data ready for ConnectomeManager
59#[derive(Debug, Clone)]
60pub struct ParsedGenome {
61    /// Genome metadata
62    pub genome_id: String,
63    pub genome_title: String,
64    pub version: String,
65
66    /// Cortical areas extracted from blueprint
67    pub cortical_areas: Vec<CorticalArea>,
68
69    /// Brain regions and hierarchy
70    pub brain_regions: Vec<(BrainRegion, Option<String>)>, // (region, parent_id)
71
72    /// First-class classifier assemblies (parallel to brain_regions)
73    pub classifiers: Vec<Classifier>,
74
75    /// Raw neuron morphologies (for later processing)
76    pub neuron_morphologies: HashMap<String, Value>,
77
78    /// Raw physiology data (for later processing)
79    pub physiology: Option<Value>,
80}
81
82/// Raw genome JSON structure for deserialization
83#[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    /// Integer schema version. Optional on the wire so older genomes that
90    /// pre-date this field still deserialize. The authoritative resolver
91    /// is `crate::genome::schema::detect_schema_version` and consumers
92    /// MUST go through it instead of branching on this field directly.
93    #[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    /// Root brain region ID (UUID string) - for O(1) root lookup
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub brain_regions_root: Option<String>,
107}
108
109/// Raw cortical area from blueprint
110#[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    // Optional properties
118    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    // Neural properties
124    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    // Memory properties
146    pub is_mem_type: Option<bool>,
147    pub longterm_mem_threshold: Option<u32>,
148    pub lifespan_growth_rate: Option<f32>,
149    pub init_lifespan: Option<u32>,
150    pub temporal_depth: Option<u32>,
151    pub mp_learning_enabled: Option<bool>,
152    pub min_window_activity: Option<u32>,
153    pub scan_skip_density: Option<f32>,
154    pub consecutive_fire_cnt_max: Option<u32>,
155    pub snooze_length: Option<u32>,
156
157    // Allow any other properties (future-proofing)
158    #[serde(flatten)]
159    pub other: HashMap<String, Value>,
160}
161
162/// Raw brain region from genome
163#[derive(Debug, Clone, Deserialize, Serialize)]
164pub struct RawBrainRegion {
165    #[serde(alias = "name")]
166    pub title: Option<String>,
167    pub description: Option<String>,
168    pub parent_region_id: Option<String>,
169    pub coordinate_2d: Option<Vec<i32>>,
170    pub coordinate_3d: Option<Vec<i32>>,
171    #[serde(alias = "cortical_areas")]
172    pub areas: Option<Vec<String>>,
173    pub regions: Option<Vec<String>>,
174    pub inputs: Option<Vec<String>>,
175    pub outputs: Option<Vec<String>>,
176    /// Declared interface lists (persisted from RuntimeGenome / PUT region).
177    pub designated_inputs: Option<Vec<String>>,
178    pub designated_outputs: Option<Vec<String>>,
179    pub signature: Option<String>,
180    /// v3 `serde_json::to_value(BrainRegion)` nests `inputs` / `designated_*` under `properties`.
181    pub properties: Option<HashMap<String, Value>>,
182}
183
184/// Raw classifier assembly from the top-level `classifiers` genome key.
185#[derive(Debug, Clone, Deserialize, Serialize)]
186pub struct RawClassifier {
187    #[serde(alias = "title")]
188    pub name: Option<String>,
189    pub parent_region_id: Option<String>,
190    #[serde(alias = "coordinate_3d")]
191    pub coordinates_3d: Option<Vec<i32>>,
192    pub kernel_area_id: Option<String>,
193    pub class_area_id: Option<String>,
194    /// Current field bindings. Each entry is one Classifier mapping and its twin.
195    pub fields: Option<Vec<feagi_structures::genomic::classifiers::ClassifierField>>,
196    /// Previous singular field record. Loaded as one binding when `fields` is absent.
197    pub field_area_id: Option<String>,
198    pub kernel_memory_id: Option<String>,
199    pub class_memory_id: Option<String>,
200    /// Previous singular twin record. Paired with `field_area_id` on load.
201    pub scan_twin_id: Option<String>,
202    pub properties: Option<HashMap<String, Value>>,
203}
204
205fn classifier_fields_from_raw(
206    raw: &RawClassifier,
207) -> Vec<feagi_structures::genomic::classifiers::ClassifierField> {
208    if let Some(fields) = &raw.fields {
209        return fields
210            .iter()
211            .filter(|field| !field.field_area_id.is_empty() && !field.scan_twin_id.is_empty())
212            .cloned()
213            .collect();
214    }
215    match (&raw.field_area_id, &raw.scan_twin_id) {
216        (Some(field_area_id), Some(scan_twin_id))
217            if !field_area_id.is_empty() && !scan_twin_id.is_empty() =>
218        {
219            vec![feagi_structures::genomic::classifiers::ClassifierField {
220                field_area_id: field_area_id.clone(),
221                scan_twin_id: scan_twin_id.clone(),
222            }]
223        }
224        _ => Vec::new(),
225    }
226}
227
228/// Convert cortical_mapping_dst keys from old format to base64
229///
230/// This ensures all destination cortical IDs in dstmap are stored in the new base64 format.
231fn convert_dstmap_keys_to_base64(dstmap: &Value) -> Value {
232    if let Some(dstmap_obj) = dstmap.as_object() {
233        let mut converted = serde_json::Map::new();
234
235        for (dest_id_str, mapping_value) in dstmap_obj {
236            // Convert destination cortical_id to base64 format
237            match string_to_cortical_id(dest_id_str) {
238                Ok(dest_cortical_id) => {
239                    converted.insert(dest_cortical_id.as_base_64(), mapping_value.clone());
240                }
241                Err(e) => {
242                    // If conversion fails, keep original and log warning
243                    tracing::warn!(
244                        "Failed to convert dstmap key '{}' to base64: {}, keeping original",
245                        dest_id_str,
246                        e
247                    );
248                    converted.insert(dest_id_str.clone(), mapping_value.clone());
249                }
250            }
251        }
252
253        Value::Object(converted)
254    } else {
255        // Not an object, return as-is
256        dstmap.clone()
257    }
258}
259
260/// Convert a string cortical_id to CorticalID
261/// Handles both old 6-char format and new base64 format
262/// CRITICAL: Uses feagi-data-processing types as single source of truth for core areas
263pub fn string_to_cortical_id(id_str: &str) -> EvoResult<CorticalID> {
264    use feagi_structures::genomic::cortical_area::CoreCorticalType;
265
266    // Try base64 first (new format)
267    if let Ok(cortical_id) = CorticalID::try_from_base_64(id_str) {
268        let mut bytes = [0u8; CorticalID::CORTICAL_ID_LENGTH];
269        cortical_id.write_id_to_bytes(&mut bytes);
270        if bytes == *b"___power" {
271            return Ok(CoreCorticalType::Power.to_cortical_id());
272        }
273        if bytes == *b"___death" {
274            return Ok(CoreCorticalType::Death.to_cortical_id());
275        }
276        if bytes == *b"___fatig" {
277            return Ok(CoreCorticalType::Fatigue.to_cortical_id());
278        }
279        if bytes == *b"___pain_" {
280            return Ok(CoreCorticalType::Pain.to_cortical_id());
281        }
282        if bytes == *b"___pleas" {
283            return Ok(CoreCorticalType::Pleasure.to_cortical_id());
284        }
285        if bytes == *b"___fear_" {
286            return Ok(CoreCorticalType::Fear.to_cortical_id());
287        }
288        if bytes == *b"___hope_" {
289            return Ok(CoreCorticalType::Hope.to_cortical_id());
290        }
291        return Ok(cortical_id);
292    }
293
294    // Handle legacy CORE area names (6-char format) - use proper types from feagi-data-processing
295    if id_str == "_power" {
296        return Ok(CoreCorticalType::Power.to_cortical_id());
297    }
298    // Legacy shorthand used by older FEAGI genomes: "___pwr" (6-char) refers to core Power.
299    if id_str == "___pwr" {
300        return Ok(CoreCorticalType::Power.to_cortical_id());
301    }
302    // Legacy 8-char core names used in some BV caches
303    if id_str == "___power" {
304        return Ok(CoreCorticalType::Power.to_cortical_id());
305    }
306    // 8-char padded form of ___pwr (from 6-char padding in legacy flat genomes)
307    if id_str == "___pwr__" {
308        return Ok(CoreCorticalType::Power.to_cortical_id());
309    }
310    if id_str == "___death" {
311        return Ok(CoreCorticalType::Death.to_cortical_id());
312    }
313    if id_str == "___fatig" {
314        return Ok(CoreCorticalType::Fatigue.to_cortical_id());
315    }
316    if id_str == "___pain_" {
317        return Ok(CoreCorticalType::Pain.to_cortical_id());
318    }
319    if id_str == "___pleas" {
320        return Ok(CoreCorticalType::Pleasure.to_cortical_id());
321    }
322    if id_str == "___fear_" {
323        return Ok(CoreCorticalType::Fear.to_cortical_id());
324    }
325    if id_str == "___hope_" {
326        return Ok(CoreCorticalType::Hope.to_cortical_id());
327    }
328    if id_str == "_death" {
329        return Ok(CoreCorticalType::Death.to_cortical_id());
330    }
331    if id_str == "_fatigue" {
332        return Ok(CoreCorticalType::Fatigue.to_cortical_id());
333    }
334    if id_str == "_pain" {
335        return Ok(CoreCorticalType::Pain.to_cortical_id());
336    }
337    if id_str == "_pleasure" {
338        return Ok(CoreCorticalType::Pleasure.to_cortical_id());
339    }
340    if id_str == "_fear" {
341        return Ok(CoreCorticalType::Fear.to_cortical_id());
342    }
343    if id_str == "_hope" {
344        return Ok(CoreCorticalType::Hope.to_cortical_id());
345    }
346
347    // For non-core areas, use CorticalID's legacy ASCII parser (6-char and 8-char)
348    if id_str.len() == 6 || id_str.len() == 8 {
349        CorticalID::try_from_legacy_ascii(id_str).map_err(|e| {
350            EvoError::InvalidArea(format!("Failed to convert cortical_id '{}': {}", id_str, e))
351        })
352    } else {
353        Err(EvoError::InvalidArea(format!(
354            "Invalid cortical_id length: '{}' (expected 6 or 8 ASCII chars, or base64)",
355            id_str
356        )))
357    }
358}
359
360/// Genome parser
361pub struct GenomeParser;
362
363impl GenomeParser {
364    /// Normalize cortical ID list properties (inputs, outputs, designated_*) to base64 strings.
365    fn normalize_brain_region_cortical_id_list_properties(region: &mut BrainRegion, keys: &[&str]) {
366        for key in keys {
367            let Some(val) = region.get_property(key) else {
368                continue;
369            };
370            let Some(arr) = val.as_array() else {
371                continue;
372            };
373            let mut out: Vec<String> = Vec::new();
374            for item in arr {
375                let Some(s) = item.as_str() else {
376                    continue;
377                };
378                match string_to_cortical_id(s) {
379                    Ok(cortical_id) => out.push(cortical_id.as_base_64()),
380                    Err(e) => {
381                        warn!(target: "feagi-evo",
382                            "Failed to convert brain region '{}' entry '{}': {}. Skipping.",
383                            key, s, e);
384                    }
385                }
386            }
387            if out.is_empty() {
388                region.properties.remove(*key);
389            } else {
390                region.add_property((*key).to_string(), serde_json::json!(out));
391            }
392        }
393    }
394
395    /// Parse a genome JSON string into a ParsedGenome
396    ///
397    /// # Arguments
398    ///
399    /// * `json_str` - JSON string of the genome
400    ///
401    /// # Returns
402    ///
403    /// Parsed genome ready for loading into ConnectomeManager
404    ///
405    /// # Errors
406    ///
407    /// Returns error if:
408    /// - JSON is malformed
409    /// - Required fields are missing
410    /// - Data types are invalid
411    ///
412    pub fn parse(json_str: &str) -> EvoResult<ParsedGenome> {
413        // Deserialize raw genome
414        let raw: RawGenome = serde_json::from_str(json_str)
415            .map_err(|e| EvoError::InvalidGenome(format!("Failed to parse JSON: {}", e)))?;
416
417        // Validate version - support 2.x and 3.x (3.0 is flat format with base64 IDs)
418        if !raw.version.starts_with("2.") && !raw.version.starts_with("3.") && raw.version != "3" {
419            return Err(EvoError::InvalidGenome(format!(
420                "Unsupported genome version: {}. Expected 2.x or 3.x",
421                raw.version
422            )));
423        }
424
425        // Parse cortical areas from blueprint
426        let cortical_areas = Self::parse_cortical_areas(&raw.blueprint)?;
427
428        // Parse brain regions
429        let brain_regions = Self::parse_brain_regions(&raw.brain_regions)?;
430        let classifiers = Self::parse_classifiers(&raw.classifiers)?;
431
432        Ok(ParsedGenome {
433            genome_id: raw.genome_id.unwrap_or_else(|| "unknown".to_string()),
434            genome_title: raw.genome_title.unwrap_or_else(|| "Untitled".to_string()),
435            version: raw.version,
436            cortical_areas,
437            brain_regions,
438            classifiers,
439            neuron_morphologies: raw.neuron_morphologies,
440            physiology: raw.physiology,
441        })
442    }
443
444    /// Parse cortical areas from blueprint
445    fn parse_cortical_areas(
446        blueprint: &HashMap<String, RawCorticalArea>,
447    ) -> EvoResult<Vec<CorticalArea>> {
448        let mut areas = Vec::with_capacity(blueprint.len());
449
450        for (cortical_id_str, raw_area) in blueprint.iter() {
451            // Skip empty IDs
452            if cortical_id_str.is_empty() {
453                warn!(target: "feagi-evo","Skipping empty cortical_id");
454                continue;
455            }
456
457            // Convert string cortical_id to CorticalID (handles 6-char legacy and base64)
458            let cortical_id = match string_to_cortical_id(cortical_id_str) {
459                Ok(id) => id,
460                Err(e) => {
461                    warn!(target: "feagi-evo","Skipping invalid cortical_id '{}': {}", cortical_id_str, e);
462                    continue;
463                }
464            };
465
466            // Extract required fields
467            let name = raw_area
468                .cortical_name
469                .clone()
470                .unwrap_or_else(|| cortical_id_str.clone());
471
472            let dimensions = if let Some(boundaries) = &raw_area.block_boundaries {
473                if boundaries.len() != 3 {
474                    return Err(EvoError::InvalidArea(format!(
475                        "Invalid block_boundaries for {}: expected 3 values, got {}",
476                        cortical_id_str,
477                        boundaries.len()
478                    )));
479                }
480                Dimensions::new(boundaries[0], boundaries[1], boundaries[2])
481                    .map_err(|e| EvoError::InvalidArea(format!("Invalid dimensions: {}", e)))?
482            } else {
483                // Default to 1x1x1 if not specified (should not happen in valid genomes)
484                warn!(target: "feagi-evo","Cortical area {} missing block_boundaries, defaulting to 1x1x1", cortical_id_str);
485                Dimensions::new(1, 1, 1).map_err(|e| {
486                    EvoError::InvalidArea(format!("Invalid default dimensions: {}", e))
487                })?
488            };
489
490            let position = if let Some(coords) = &raw_area.relative_coordinate {
491                if coords.len() != 3 {
492                    return Err(EvoError::InvalidArea(format!(
493                        "Invalid relative_coordinate for {}: expected 3 values, got {}",
494                        cortical_id_str,
495                        coords.len()
496                    )));
497                }
498                GenomeCoordinate3D::new(coords[0], coords[1], coords[2])
499            } else {
500                // Default to origin if not specified
501                warn!(target: "feagi-evo","Cortical area {} missing relative_coordinate, defaulting to (0,0,0)", cortical_id_str);
502                GenomeCoordinate3D::new(0, 0, 0)
503            };
504
505            // Determine cortical type from cortical_id
506            let cortical_type = cortical_id.as_cortical_type().map_err(|e| {
507                EvoError::InvalidArea(format!(
508                    "Failed to determine cortical type from ID {}: {}",
509                    cortical_id_str, e
510                ))
511            })?;
512
513            // Create cortical area with CorticalID object (zero-copy, type-safe)
514            let mut area = CorticalArea::new(
515                cortical_id,
516                0, // cortical_idx will be assigned by ConnectomeManager
517                name,
518                dimensions,
519                position,
520                cortical_type,
521            )?;
522
523            // Store cortical_type as cortical_group for new type system
524            if let Some(ref cortical_type_str) = raw_area.cortical_type {
525                area.properties.insert(
526                    "cortical_group".to_string(),
527                    serde_json::json!(cortical_type_str),
528                );
529            }
530
531            // Store all properties in the properties HashMap
532            // Neural properties
533            if let Some(v) = raw_area.synapse_attractivity {
534                area.properties
535                    .insert("synapse_attractivity".to_string(), serde_json::json!(v));
536            }
537            if let Some(v) = raw_area.refractory_period {
538                area.properties
539                    .insert("refractory_period".to_string(), serde_json::json!(v));
540            }
541            if let Some(v) = raw_area.firing_threshold {
542                area.properties
543                    .insert("firing_threshold".to_string(), serde_json::json!(v));
544            }
545            if let Some(v) = raw_area.firing_threshold_limit {
546                area.properties
547                    .insert("firing_threshold_limit".to_string(), serde_json::json!(v));
548            }
549            if let Some(v) = raw_area.firing_threshold_increment_x {
550                area.properties.insert(
551                    "firing_threshold_increment_x".to_string(),
552                    serde_json::json!(v),
553                );
554            }
555            if let Some(v) = raw_area.firing_threshold_increment_y {
556                area.properties.insert(
557                    "firing_threshold_increment_y".to_string(),
558                    serde_json::json!(v),
559                );
560            }
561            if let Some(v) = raw_area.firing_threshold_increment_z {
562                area.properties.insert(
563                    "firing_threshold_increment_z".to_string(),
564                    serde_json::json!(v),
565                );
566            }
567            if let Some(v) = raw_area.leak_coefficient {
568                area.properties
569                    .insert("leak_coefficient".to_string(), serde_json::json!(v));
570            }
571            if let Some(v) = raw_area.leak_variability {
572                area.properties
573                    .insert("leak_variability".to_string(), serde_json::json!(v));
574            }
575            if let Some(v) = raw_area.neuron_excitability {
576                area.properties
577                    .insert("neuron_excitability".to_string(), serde_json::json!(v));
578            }
579            if let Some(v) = raw_area.postsynaptic_current {
580                area.properties
581                    .insert("postsynaptic_current".to_string(), serde_json::json!(v));
582            }
583            if let Some(v) = raw_area.postsynaptic_current_max {
584                area.properties
585                    .insert("postsynaptic_current_max".to_string(), serde_json::json!(v));
586            }
587            if let Some(v) = raw_area.degeneration {
588                area.properties
589                    .insert("degeneration".to_string(), serde_json::json!(v));
590            }
591
592            // Boolean properties
593            if let Some(v) = raw_area.psp_uniform_distribution {
594                area.properties
595                    .insert("psp_uniform_distribution".to_string(), serde_json::json!(v));
596            }
597            if let Some(v) = raw_area.mp_charge_accumulation {
598                area.properties
599                    .insert("mp_charge_accumulation".to_string(), serde_json::json!(v));
600            }
601            if let Some(v) = raw_area.mp_driven_psp {
602                area.properties
603                    .insert("mp_driven_psp".to_string(), serde_json::json!(v));
604                tracing::info!(
605                    target: "feagi-evo",
606                    "[GENOME-LOAD] Loaded mp_driven_psp={} for area {}",
607                    v,
608                    cortical_id_str
609                );
610            } else {
611                tracing::debug!(
612                    target: "feagi-evo",
613                    "[GENOME-LOAD] mp_driven_psp not found in raw_area for {}, will use default=false",
614                    cortical_id_str
615                );
616            }
617            if let Some(v) = raw_area.visualization {
618                area.properties
619                    .insert("visualization".to_string(), serde_json::json!(v));
620                // Also store as "visible" for compatibility with getters
621                area.properties
622                    .insert("visible".to_string(), serde_json::json!(v));
623            }
624            if let Some(v) = raw_area.burst_engine_activation {
625                area.properties
626                    .insert("burst_engine_active".to_string(), serde_json::json!(v));
627            }
628            if let Some(v) = raw_area.is_mem_type {
629                area.properties
630                    .insert("is_mem_type".to_string(), serde_json::json!(v));
631            }
632
633            // Memory properties
634            if let Some(v) = raw_area.longterm_mem_threshold {
635                area.properties
636                    .insert("longterm_mem_threshold".to_string(), serde_json::json!(v));
637            }
638            if let Some(v) = raw_area.lifespan_growth_rate {
639                area.properties
640                    .insert("lifespan_growth_rate".to_string(), serde_json::json!(v));
641            }
642            if let Some(v) = raw_area.init_lifespan {
643                area.properties
644                    .insert("init_lifespan".to_string(), serde_json::json!(v));
645            }
646            if let Some(v) = raw_area.temporal_depth {
647                area.properties
648                    .insert("temporal_depth".to_string(), serde_json::json!(v));
649            }
650            if let Some(v) = raw_area.mp_learning_enabled {
651                area.properties
652                    .insert("mp_learning_enabled".to_string(), serde_json::json!(v));
653            }
654            if let Some(v) = raw_area.min_window_activity {
655                area.properties
656                    .insert("min_window_activity".to_string(), serde_json::json!(v));
657            }
658            if let Some(v) = raw_area.scan_skip_density {
659                area.properties
660                    .insert("scan_skip_density".to_string(), serde_json::json!(v));
661            }
662            if let Some(v) = raw_area.consecutive_fire_cnt_max {
663                area.properties
664                    .insert("consecutive_fire_cnt_max".to_string(), serde_json::json!(v));
665                // Also store as "consecutive_fire_limit" for getter compatibility
666                area.properties
667                    .insert("consecutive_fire_limit".to_string(), serde_json::json!(v));
668            }
669            if let Some(v) = raw_area.snooze_length {
670                area.properties
671                    .insert("snooze_period".to_string(), serde_json::json!(v));
672            }
673
674            // Other properties
675            if let Some(v) = &raw_area.group_id {
676                area.properties
677                    .insert("group_id".to_string(), serde_json::json!(v));
678            }
679            if let Some(v) = &raw_area.sub_group_id {
680                area.properties
681                    .insert("sub_group_id".to_string(), serde_json::json!(v));
682            }
683            // Store neurons_per_voxel in properties HashMap
684            if let Some(v) = raw_area.per_voxel_neuron_cnt {
685                area.properties
686                    .insert("neurons_per_voxel".to_string(), serde_json::json!(v));
687            }
688            if let Some(v) = &raw_area.cortical_mapping_dst {
689                // Convert dstmap keys from old format to base64
690                let converted_dstmap = convert_dstmap_keys_to_base64(v);
691                area.properties
692                    .insert("cortical_mapping_dst".to_string(), converted_dstmap);
693            }
694            if let Some(v) = &raw_area.coordinate_2d {
695                area.properties
696                    .insert("2d_coordinate".to_string(), serde_json::json!(v));
697            }
698
699            // Store any other custom properties
700            for (key, value) in &raw_area.other {
701                area.properties.insert(key.clone(), value.clone());
702            }
703
704            // Note: cortical_type parsing disabled - CorticalArea is now a minimal data structure
705            // CorticalAreaType information is stored in properties["cortical_group"] if needed
706
707            areas.push(area);
708        }
709
710        Ok(areas)
711    }
712
713    /// Parse brain regions
714    fn parse_brain_regions(
715        raw_regions: &HashMap<String, RawBrainRegion>,
716    ) -> EvoResult<Vec<(BrainRegion, Option<String>)>> {
717        let mut regions = Vec::with_capacity(raw_regions.len());
718
719        for (region_id_str, raw_region) in raw_regions.iter() {
720            let title = raw_region
721                .title
722                .clone()
723                .unwrap_or_else(|| region_id_str.clone());
724
725            // Convert string region_id to RegionID (UUID)
726            // For now, try to parse as UUID if it's already a UUID, otherwise generate new one
727            let region_id = match RegionID::from_string(region_id_str) {
728                Ok(id) => id,
729                Err(_) => {
730                    // If not a valid UUID, generate a new one
731                    // This handles legacy string-based region IDs
732                    RegionID::new()
733                }
734            };
735
736            let region_type = RegionType::Undefined; // Default to Undefined
737
738            let mut region = BrainRegion::new(region_id, title, region_type)?;
739
740            // v3 RuntimeGenome sections nest IO under `properties`; merge before list fields.
741            if let Some(props) = &raw_region.properties {
742                for (k, v) in props {
743                    region.add_property(k.clone(), v.clone());
744                }
745            }
746
747            // Add cortical areas to region (using CorticalID directly)
748            if let Some(areas) = &raw_region.areas {
749                for area_id in areas {
750                    // Convert area_id to CorticalID
751                    match string_to_cortical_id(area_id) {
752                        Ok(cortical_id) => {
753                            region.add_area(cortical_id);
754                        }
755                        Err(e) => {
756                            warn!(target: "feagi-evo",
757                                "Failed to convert brain region area ID '{}' to CorticalID: {}. Skipping.",
758                                area_id, e);
759                        }
760                    }
761                }
762            }
763
764            // Store properties in HashMap
765            if let Some(desc) = &raw_region.description {
766                region.add_property("description".to_string(), serde_json::json!(desc));
767            }
768            if let Some(coord_2d) = &raw_region.coordinate_2d {
769                region.add_property("coordinate_2d".to_string(), serde_json::json!(coord_2d));
770            }
771            if let Some(coord_3d) = &raw_region.coordinate_3d {
772                region.add_property("coordinate_3d".to_string(), serde_json::json!(coord_3d));
773            }
774            // Store inputs/outputs as base64 strings
775            if let Some(inputs) = &raw_region.inputs {
776                let input_ids: Vec<String> = inputs
777                    .iter()
778                    .filter_map(|id| match string_to_cortical_id(id) {
779                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
780                        Err(e) => {
781                            warn!(target: "feagi-evo",
782                                    "Failed to convert brain region input ID '{}': {}. Skipping.",
783                                    id, e);
784                            None
785                        }
786                    })
787                    .collect();
788                if !input_ids.is_empty() {
789                    region.add_property("inputs".to_string(), serde_json::json!(input_ids));
790                }
791            }
792            if let Some(outputs) = &raw_region.outputs {
793                let output_ids: Vec<String> = outputs
794                    .iter()
795                    .filter_map(|id| match string_to_cortical_id(id) {
796                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
797                        Err(e) => {
798                            warn!(target: "feagi-evo",
799                                    "Failed to convert brain region output ID '{}': {}. Skipping.",
800                                    id, e);
801                            None
802                        }
803                    })
804                    .collect();
805                if !output_ids.is_empty() {
806                    region.add_property("outputs".to_string(), serde_json::json!(output_ids));
807                }
808            }
809            if let Some(signature) = &raw_region.signature {
810                region.add_property("signature".to_string(), serde_json::json!(signature));
811            }
812
813            if let Some(d) = &raw_region.designated_inputs {
814                let ids: Vec<String> = d
815                    .iter()
816                    .filter_map(|id| match string_to_cortical_id(id) {
817                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
818                        Err(e) => {
819                            warn!(target: "feagi-evo",
820                                "Failed to convert designated_inputs entry '{}': {}. Skipping.",
821                                id, e);
822                            None
823                        }
824                    })
825                    .collect();
826                if !ids.is_empty() {
827                    region.add_property("designated_inputs".to_string(), serde_json::json!(ids));
828                }
829            }
830            if let Some(d) = &raw_region.designated_outputs {
831                let ids: Vec<String> = d
832                    .iter()
833                    .filter_map(|id| match string_to_cortical_id(id) {
834                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
835                        Err(e) => {
836                            warn!(target: "feagi-evo",
837                                "Failed to convert designated_outputs entry '{}': {}. Skipping.",
838                                id, e);
839                            None
840                        }
841                    })
842                    .collect();
843                if !ids.is_empty() {
844                    region.add_property("designated_outputs".to_string(), serde_json::json!(ids));
845                }
846            }
847
848            Self::normalize_brain_region_cortical_id_list_properties(
849                &mut region,
850                &[
851                    "inputs",
852                    "outputs",
853                    "designated_inputs",
854                    "designated_outputs",
855                ],
856            );
857
858            // Store parent_id for hierarchy construction
859            let parent_id = raw_region.parent_region_id.clone();
860            if let Some(ref parent_id_str) = parent_id {
861                // Store as property for serialization
862                region.add_property(
863                    "parent_region_id".to_string(),
864                    serde_json::json!(parent_id_str),
865                );
866            }
867
868            regions.push((region, parent_id));
869        }
870
871        Ok(regions)
872    }
873
874    fn parse_classifiers(
875        raw_classifiers: &HashMap<String, RawClassifier>,
876    ) -> EvoResult<Vec<Classifier>> {
877        let mut classifiers = Vec::with_capacity(raw_classifiers.len());
878        for (classifier_id, raw) in raw_classifiers {
879            let name = raw
880                .name
881                .clone()
882                .filter(|n| !n.trim().is_empty())
883                .ok_or_else(|| {
884                    EvoError::InvalidArea(format!("Classifier '{}' is missing name", classifier_id))
885                })?;
886            let parent_region_id = raw
887                .parent_region_id
888                .clone()
889                .filter(|n| !n.trim().is_empty())
890                .ok_or_else(|| {
891                    EvoError::InvalidArea(format!(
892                        "Classifier '{}' is missing parent_region_id",
893                        classifier_id
894                    ))
895                })?;
896            let coordinates_3d = match &raw.coordinates_3d {
897                Some(coords) if coords.len() == 3 => [coords[0], coords[1], coords[2]],
898                Some(coords) => {
899                    return Err(EvoError::InvalidArea(format!(
900                        "Classifier '{}' coordinates_3d must have 3 values, got {}",
901                        classifier_id,
902                        coords.len()
903                    )))
904                }
905                None => [0, 0, 0],
906            };
907            let kernel_memory_id = raw.kernel_memory_id.clone().ok_or_else(|| {
908                EvoError::InvalidArea(format!(
909                    "Classifier '{}' is missing kernel_memory_id",
910                    classifier_id
911                ))
912            })?;
913            let class_memory_id = raw.class_memory_id.clone().ok_or_else(|| {
914                EvoError::InvalidArea(format!(
915                    "Classifier '{}' is missing class_memory_id",
916                    classifier_id
917                ))
918            })?;
919            let fields = classifier_fields_from_raw(raw);
920            classifiers.push(Classifier {
921                classifier_id: classifier_id.clone(),
922                name,
923                parent_region_id,
924                coordinates_3d,
925                kernel_area_id: raw.kernel_area_id.clone(),
926                class_area_id: raw.class_area_id.clone(),
927                fields,
928                kernel_memory_id,
929                class_memory_id,
930                properties: raw.properties.clone().unwrap_or_default(),
931            });
932        }
933        Ok(classifiers)
934    }
935}
936
937#[cfg(test)]
938mod tests {
939    use super::*;
940
941    #[test]
942    fn test_parse_minimal_genome() {
943        // Test backward compatibility: parsing v2.1 genome with old 6-byte cortical ID
944        // Parser should convert old format to base64 for storage
945        let json = r#"{
946            "version": "2.1",
947            "blueprint": {
948                "_power": {
949                    "cortical_name": "Test Area",
950                    "block_boundaries": [10, 10, 10],
951                    "relative_coordinate": [0, 0, 0],
952                    "cortical_type": "CORE"
953                }
954            },
955            "brain_regions": {
956                "root": {
957                    "title": "Root",
958                    "parent_region_id": null,
959                    "areas": ["_power"]
960                }
961            }
962        }"#;
963
964        let parsed = GenomeParser::parse(json).unwrap();
965
966        assert_eq!(parsed.version, "2.1");
967        assert_eq!(parsed.cortical_areas.len(), 1);
968        // Input was "_power" (6 bytes), converted to "___power" (8 bytes, padded at start with underscores) then base64 encoded
969        assert_eq!(
970            parsed.cortical_areas[0].cortical_id.as_base_64(),
971            "X19fcG93ZXI="
972        );
973        assert_eq!(parsed.cortical_areas[0].name, "Test Area");
974        assert_eq!(parsed.brain_regions.len(), 1);
975
976        // Phase 2: Verify cortical_type_new is populated
977        // Note: cortical_type_new field removed - type is encoded in cortical_id
978        assert!(parsed.cortical_areas[0]
979            .cortical_id
980            .as_cortical_type()
981            .is_ok());
982    }
983
984    #[test]
985    fn test_parse_multiple_areas() {
986        // Test parsing multiple cortical areas with old format IDs
987        let json = r#"{
988            "version": "2.1",
989            "blueprint": {
990                "_power": {
991                    "cortical_name": "Area 1",
992                    "cortical_type": "CORE",
993                    "block_boundaries": [5, 5, 5],
994                    "relative_coordinate": [0, 0, 0]
995                },
996                "_death": {
997                    "cortical_name": "Area 2",
998                    "cortical_type": "CORE",
999                    "block_boundaries": [10, 10, 10],
1000                    "relative_coordinate": [5, 0, 0]
1001                }
1002            }
1003        }"#;
1004
1005        let parsed = GenomeParser::parse(json).unwrap();
1006        assert!(parsed.classifiers.is_empty());
1007
1008        assert_eq!(parsed.cortical_areas.len(), 2);
1009
1010        // Phase 2: Verify both areas have cortical_type_new populated
1011        for area in &parsed.cortical_areas {
1012            assert!(
1013                area.cortical_id.as_cortical_type().is_ok(),
1014                "Area {} should have cortical_type_new populated",
1015                area.cortical_id
1016            );
1017        }
1018    }
1019
1020    #[test]
1021    fn test_string_to_cortical_id_legacy_power_shorthand() {
1022        // Older FEAGI genomes may encode the power core area as "___pwr" (6-char shorthand).
1023        // Migration must map this deterministically to the core Power cortical ID.
1024        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1025        let id = string_to_cortical_id("___pwr").unwrap();
1026        assert_eq!(
1027            id.as_base_64(),
1028            CoreCorticalType::Power.to_cortical_id().as_base_64()
1029        );
1030    }
1031
1032    #[test]
1033    fn test_parse_raw_imu_magnetometer_wire_id() {
1034        // Embodiment-registered Raw IMU magnetometer (subunit 2) from live FEAGI.
1035        // Connectome auto-save must be able to rehydrate this into a runtime genome.
1036        let json = r#"{
1037            "version": "3.0",
1038            "blueprint": {
1039                "aXJpbScAAgA=": {
1040                    "cortical_name": "feagi_body_imu__Abdomen-2",
1041                    "block_boundaries": [3, 1, 10],
1042                    "relative_coordinate": [90, 0, -10],
1043                    "cortical_type": "IPU"
1044                }
1045            },
1046            "brain_regions": {}
1047        }"#;
1048
1049        let parsed = GenomeParser::parse(json).expect("Raw IMU magnetometer genome");
1050        assert_eq!(parsed.cortical_areas.len(), 1);
1051        assert_eq!(
1052            parsed.cortical_areas[0].cortical_id.as_base_64(),
1053            "aXJpbScAAgA="
1054        );
1055        parsed.cortical_areas[0]
1056            .cortical_id
1057            .as_cortical_type()
1058            .expect("magnetometer IO flag must decode");
1059    }
1060
1061    #[test]
1062    fn test_parse_positional_servo_speed_wire_id() {
1063        // Embodiment-registered Positional Servo Speed (subunit 2) from live FEAGI.
1064        // Connectome auto-save must be able to rehydrate this into a runtime genome.
1065        let json = r#"{
1066            "version": "3.0",
1067            "blueprint": {
1068                "b3BzZSEAAAA=": {
1069                    "cortical_name": "Positional Servo Speed",
1070                    "block_boundaries": [6, 1, 20],
1071                    "relative_coordinate": [-58, 0, -10],
1072                    "cortical_type": "OPU"
1073                }
1074            },
1075            "brain_regions": {}
1076        }"#;
1077
1078        let parsed = GenomeParser::parse(json).expect("Positional Servo Speed genome");
1079        assert_eq!(parsed.cortical_areas.len(), 1);
1080        assert_eq!(
1081            parsed.cortical_areas[0].cortical_id.as_base_64(),
1082            "b3BzZSEAAAA="
1083        );
1084        parsed.cortical_areas[0]
1085            .cortical_id
1086            .as_cortical_type()
1087            .expect("positional servo speed IO flag must decode");
1088    }
1089
1090    #[test]
1091    fn test_string_to_cortical_id_legacy_power_padded() {
1092        // 8-char padded form ___pwr__ (from 6-char padding in legacy flat genomes).
1093        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1094        let id = string_to_cortical_id("___pwr__").unwrap();
1095        assert_eq!(
1096            id.as_base_64(),
1097            CoreCorticalType::Power.to_cortical_id().as_base_64()
1098        );
1099    }
1100
1101    #[test]
1102    fn test_parse_with_properties() {
1103        let json = r#"{
1104            "version": "2.1",
1105            "blueprint": {
1106                "mem001": {
1107                    "cortical_name": "Memory Area",
1108                    "block_boundaries": [8, 8, 8],
1109                    "relative_coordinate": [0, 0, 0],
1110                    "cortical_type": "MEMORY",
1111                    "is_mem_type": true,
1112                    "firing_threshold": 50.0,
1113                    "leak_coefficient": 0.9
1114                }
1115            }
1116        }"#;
1117
1118        let parsed = GenomeParser::parse(json).unwrap();
1119
1120        assert_eq!(parsed.cortical_areas.len(), 1);
1121        let area = &parsed.cortical_areas[0];
1122
1123        // Old type system (deprecated)
1124        use feagi_structures::genomic::cortical_area::CorticalAreaType;
1125        assert!(matches!(area.cortical_type, CorticalAreaType::Memory(_)));
1126
1127        // Properties stored correctly
1128        assert!(area.properties.contains_key("is_mem_type"));
1129        assert!(area.properties.contains_key("firing_threshold"));
1130        assert!(area.properties.contains_key("cortical_group"));
1131
1132        // NEW: cortical_type should be derivable from cortical_id (Phase 2)
1133        assert!(
1134            area.cortical_id.as_cortical_type().is_ok(),
1135            "cortical_id should be parseable to cortical_type"
1136        );
1137        if let Ok(cortical_type) = area.cortical_id.as_cortical_type() {
1138            use feagi_structures::genomic::cortical_area::CorticalAreaType;
1139            assert!(
1140                matches!(cortical_type, CorticalAreaType::Memory(_)),
1141                "Should be classified as MEMORY type"
1142            );
1143        }
1144    }
1145
1146    /// v3 save embeds IO lists under `properties`; loading must preserve designated_inputs for BV presets.
1147    #[test]
1148    fn test_parse_v3_brain_region_nested_properties_retains_designated_io() {
1149        let json = r#"{
1150            "version": "3.0",
1151            "blueprint": {
1152                "_power": {
1153                    "cortical_name": "Core",
1154                    "block_boundaries": [10, 10, 10],
1155                    "relative_coordinate": [0, 0, 0],
1156                    "cortical_type": "CORE"
1157                }
1158            },
1159            "brain_regions": {
1160                "550e8400-e29b-41d4-a716-446655440000": {
1161                    "name": "Sub",
1162                    "cortical_areas": ["_power"],
1163                    "properties": {
1164                        "designated_inputs": ["_power"],
1165                        "designated_outputs": []
1166                    }
1167                }
1168            }
1169        }"#;
1170
1171        let parsed = GenomeParser::parse(json).unwrap();
1172        assert_eq!(parsed.brain_regions.len(), 1);
1173        let (region, _) = &parsed.brain_regions[0];
1174        let di = region
1175            .get_property("designated_inputs")
1176            .and_then(|v| v.as_array())
1177            .expect("designated_inputs");
1178        assert_eq!(di.len(), 1);
1179        assert_eq!(di[0].as_str().unwrap(), "X19fcG93ZXI=");
1180    }
1181
1182    #[test]
1183    fn test_parse_brain_region_plain_text_description() {
1184        let json = r#"{
1185            "version": "2.1",
1186            "blueprint": {
1187                "_power": {
1188                    "cortical_name": "Core",
1189                    "block_boundaries": [10, 10, 10],
1190                    "relative_coordinate": [0, 0, 0],
1191                    "cortical_type": "CORE"
1192                }
1193            },
1194            "brain_regions": {
1195                "root": {
1196                    "title": "Root",
1197                    "description": "Holds core physiology and embodiment IO",
1198                    "parent_region_id": null,
1199                    "areas": ["_power"]
1200                }
1201            }
1202        }"#;
1203
1204        let parsed = GenomeParser::parse(json).unwrap();
1205        assert_eq!(parsed.brain_regions.len(), 1);
1206        let (region, _) = &parsed.brain_regions[0];
1207        assert_eq!(
1208            region.get_property("description"),
1209            Some(&serde_json::json!(
1210                "Holds core physiology and embodiment IO"
1211            ))
1212        );
1213    }
1214
1215    #[test]
1216    fn test_invalid_version() {
1217        let json = r#"{
1218            "version": "1.0",
1219            "blueprint": {}
1220        }"#;
1221
1222        let result = GenomeParser::parse(json);
1223        assert!(result.is_err());
1224    }
1225
1226    #[test]
1227    fn test_malformed_json() {
1228        let json = r#"{ "version": "2.1", "blueprint": { malformed"#;
1229
1230        let result = GenomeParser::parse(json);
1231        assert!(result.is_err());
1232    }
1233
1234    #[test]
1235    fn test_cortical_type_new_population() {
1236        // Test that cortical_type_new field is populated during parsing (Phase 2)
1237        // This tests that parsing works with valid cortical IDs and populates types correctly
1238        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1239        let power_id = CoreCorticalType::Power.to_cortical_id().as_base_64();
1240        let json = format!(
1241            r#"{{
1242            "version": "2.1",
1243            "blueprint": {{
1244                "cvision1": {{
1245                    "cortical_name": "Test Custom Vision",
1246                    "cortical_type": "CUSTOM",
1247                    "block_boundaries": [10, 10, 1],
1248                    "relative_coordinate": [0, 0, 0]
1249                }},
1250                "cmotor01": {{
1251                    "cortical_name": "Test Custom Motor",
1252                    "cortical_type": "CUSTOM",
1253                    "block_boundaries": [5, 5, 1],
1254                    "relative_coordinate": [0, 0, 0]
1255                }},
1256                "{}": {{
1257                    "cortical_name": "Test Core",
1258                    "cortical_type": "CORE",
1259                    "block_boundaries": [1, 1, 1],
1260                    "relative_coordinate": [0, 0, 0]
1261                }}
1262            }}
1263        }}"#,
1264            power_id
1265        );
1266
1267        let parsed = GenomeParser::parse(&json).unwrap();
1268        assert_eq!(parsed.cortical_areas.len(), 3);
1269
1270        // Verify all areas have cortical_type_new populated
1271        for area in &parsed.cortical_areas {
1272            assert!(
1273                area.cortical_id.as_cortical_type().is_ok(),
1274                "Area {} should have cortical_type_new populated",
1275                area.cortical_id
1276            );
1277
1278            // Verify cortical_group property is also set
1279            assert!(
1280                area.properties.contains_key("cortical_group"),
1281                "Area {} should have cortical_group property",
1282                area.cortical_id
1283            );
1284
1285            // Verify cortical group is consistent (avoid depending on feagi-brain-development)
1286            if let Some(prop_group) = area
1287                .properties
1288                .get("cortical_group")
1289                .and_then(|v| v.as_str())
1290            {
1291                assert!(
1292                    !prop_group.is_empty(),
1293                    "Area {} should have non-empty cortical_group property",
1294                    area.cortical_id.as_base_64()
1295                );
1296            }
1297        }
1298    }
1299
1300    #[test]
1301    fn test_parse_classifiers_key_parallel_to_regions() {
1302        let json = r#"{
1303            "version": "3.0",
1304            "blueprint": {
1305                "cfield": {
1306                    "cortical_name": "Field",
1307                    "cortical_type": "CUSTOM",
1308                    "block_boundaries": [4, 4, 1],
1309                    "relative_coordinate": [0, 0, 0]
1310                },
1311                "mkmem1": {
1312                    "cortical_name": "KernelMem",
1313                    "cortical_type": "MEMORY",
1314                    "block_boundaries": [2, 2, 2],
1315                    "relative_coordinate": [10, 0, 0]
1316                },
1317                "mcmem1": {
1318                    "cortical_name": "ClassMem",
1319                    "cortical_type": "MEMORY",
1320                    "block_boundaries": [2, 2, 2],
1321                    "relative_coordinate": [20, 0, 0]
1322                },
1323                "cscan1": {
1324                    "cortical_name": "ScanTwin",
1325                    "cortical_type": "CUSTOM",
1326                    "block_boundaries": [4, 4, 3],
1327                    "relative_coordinate": [30, 0, 0]
1328                }
1329            },
1330            "brain_regions": {
1331                "root": {
1332                    "title": "root",
1333                    "parent_region_id": "",
1334                    "coordinate_2d": [0, 0],
1335                    "coordinate_3d": [0, 0, 0],
1336                    "areas": ["cfield", "mkmem1", "mcmem1", "cscan1"],
1337                    "regions": [],
1338                    "inputs": [],
1339                    "outputs": []
1340                }
1341            },
1342            "classifiers": {
1343                "clf-1": {
1344                    "name": "object_class",
1345                    "parent_region_id": "root",
1346                    "coordinates_3d": [30, 0, 0],
1347                    "field_area_id": "cfield",
1348                    "kernel_memory_id": "mkmem1",
1349                    "class_memory_id": "mcmem1",
1350                    "scan_twin_id": "cscan1"
1351                }
1352            }
1353        }"#;
1354
1355        let parsed = GenomeParser::parse(json).expect("classifier genome");
1356        assert_eq!(parsed.classifiers.len(), 1);
1357        let classifier = &parsed.classifiers[0];
1358        assert_eq!(classifier.classifier_id, "clf-1");
1359        assert_eq!(classifier.name, "object_class");
1360        assert_eq!(classifier.parent_region_id, "root");
1361        assert_eq!(classifier.fields.len(), 1);
1362        assert_eq!(classifier.fields[0].field_area_id, "cfield");
1363        assert_eq!(classifier.kernel_memory_id, "mkmem1");
1364        assert_eq!(classifier.class_memory_id, "mcmem1");
1365        assert_eq!(classifier.fields[0].scan_twin_id, "cscan1");
1366        assert_eq!(classifier.owned_area_ids().len(), 3);
1367    }
1368}