feagi_evolutionary/genome/
saver.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Genome JSON saver.
6
7Serializes FEAGI connectome data back to JSON genome format.
8
9Copyright 2025 Neuraville Inc.
10Licensed under the Apache License, Version 2.0
11*/
12
13use serde_json::{json, Value};
14use std::collections::HashMap;
15
16use crate::types::{EvoError, EvoResult};
17#[cfg(test)]
18use feagi_structures::genomic::brain_regions::RegionID;
19use feagi_structures::genomic::cortical_area::{CorticalArea, CorticalID};
20#[cfg(test)]
21use feagi_structures::genomic::cortical_area::{
22    CorticalAreaDimensions, CorticalAreaType, IOCorticalAreaConfigurationFlag,
23};
24use feagi_structures::genomic::BrainRegion;
25
26/// Genome saver
27pub struct GenomeSaver;
28
29impl GenomeSaver {
30    /// Save connectome to genome JSON
31    ///
32    /// **DEPRECATED**: This method produces incomplete hierarchical format v2.1 without morphologies/physiology.
33    /// Use `feagi_evolutionary::save_genome_to_json(RuntimeGenome)` instead, which produces complete flat format v3.0.
34    ///
35    /// This method is kept only for legacy tests. Production code MUST use the RuntimeGenome saver.
36    ///
37    /// # Arguments
38    ///
39    /// * `cortical_areas` - Map of cortical areas
40    /// * `brain_regions` - Map of brain regions
41    /// * `genome_id` - Optional genome ID (generates default if None)
42    /// * `genome_title` - Optional genome title
43    ///
44    /// # Returns
45    ///
46    /// JSON string of the genome (hierarchical v2.1, incomplete)
47    ///
48    #[deprecated(
49        note = "Use feagi_evolutionary::save_genome_to_json(RuntimeGenome) instead. This produces incomplete v2.1 format."
50    )]
51    pub fn save_to_json(
52        cortical_areas: &HashMap<CorticalID, CorticalArea>,
53        brain_regions: &HashMap<String, (BrainRegion, Option<String>)>,
54        genome_id: Option<String>,
55        genome_title: Option<String>,
56    ) -> EvoResult<String> {
57        // Build blueprint section
58        let mut blueprint = serde_json::Map::new();
59
60        for (cortical_id, area) in cortical_areas {
61            let cortical_id_str = cortical_id.as_base_64();
62            let mut area_data = serde_json::Map::new();
63
64            // Required fields
65            area_data.insert("cortical_name".to_string(), json!(area.name));
66            area_data.insert(
67                "block_boundaries".to_string(),
68                json!([
69                    area.dimensions.width,
70                    area.dimensions.height,
71                    area.dimensions.depth
72                ]),
73            );
74            area_data.insert(
75                "relative_coordinate".to_string(),
76                json!([area.position.x, area.position.y, area.position.z]),
77            );
78
79            // Area type (from properties)
80            let cortical_type = area
81                .properties
82                .get("cortical_group")
83                .and_then(|v| v.as_str())
84                .unwrap_or("CUSTOM");
85            area_data.insert("cortical_type".to_string(), json!(cortical_type));
86
87            // Add all properties from the area's properties HashMap
88            for (key, value) in &area.properties {
89                area_data.insert(key.clone(), value.clone());
90            }
91
92            blueprint.insert(cortical_id_str, Value::Object(area_data));
93        }
94
95        // Build brain_regions section
96        let mut regions_map = serde_json::Map::new();
97
98        for (region_id, (region, parent_id)) in brain_regions {
99            let mut region_data = serde_json::Map::new();
100
101            region_data.insert("title".to_string(), json!(region.name));
102            region_data.insert(
103                "parent_region_id".to_string(),
104                if let Some(ref parent) = parent_id {
105                    json!(parent)
106                } else {
107                    Value::Null
108                },
109            );
110
111            // Cortical areas in this region (convert CorticalID to base64 strings)
112            let areas: Vec<String> = region
113                .cortical_areas
114                .iter()
115                .map(|id| id.as_base_64())
116                .collect();
117            region_data.insert("areas".to_string(), json!(areas));
118
119            // Add all properties from HashMap
120            for (key, value) in &region.properties {
121                region_data.insert(key.clone(), value.clone());
122            }
123
124            // Note: regions (child_regions) are not stored in properties - will be empty for now
125            region_data.insert("regions".to_string(), json!(Vec::<String>::new()));
126
127            regions_map.insert(region_id.to_string(), Value::Object(region_data));
128        }
129
130        // Build final genome structure
131        let genome = json!({
132            "genome_id": genome_id.unwrap_or_else(||
133                format!("genome_{}", chrono::Utc::now().timestamp())
134            ),
135            "genome_title": genome_title.unwrap_or_else(|| "Exported Genome".to_string()),
136            "version": "2.1",
137            "blueprint": blueprint,
138            "brain_regions": regions_map,
139            "neuron_morphologies": {},
140            "physiology": {}
141        });
142
143        // Serialize to pretty JSON
144        serde_json::to_string_pretty(&genome)
145            .map_err(|e| EvoError::Internal(format!("Failed to serialize genome: {}", e)))
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use feagi_structures::genomic::RegionType;
153
154    #[test]
155    fn test_save_minimal_genome() {
156        let mut cortical_areas = HashMap::new();
157        let mut brain_regions = HashMap::new();
158
159        // Create a test cortical area (use valid core ID)
160        use feagi_structures::genomic::cortical_area::CoreCorticalType;
161        let cortical_id = CoreCorticalType::Power.to_cortical_id();
162        let area = CorticalArea::new(
163            cortical_id,
164            0,
165            "Test Area".to_string(),
166            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
167            (0, 0, 0).into(),
168            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
169        )
170        .unwrap();
171
172        cortical_areas.insert(cortical_id, area);
173
174        // Create a test brain region
175        let region =
176            BrainRegion::new(RegionID::new(), "Root".to_string(), RegionType::Undefined).unwrap();
177
178        brain_regions.insert("root".to_string(), (region, None));
179
180        // Save to JSON
181        #[allow(deprecated)]
182        let json = GenomeSaver::save_to_json(
183            &cortical_areas,
184            &brain_regions,
185            Some("test-001".to_string()),
186            Some("Test Genome".to_string()),
187        )
188        .unwrap();
189
190        // Verify it's valid JSON
191        let parsed: Value = serde_json::from_str(&json).unwrap();
192
193        assert_eq!(parsed["genome_id"], "test-001");
194        assert_eq!(parsed["genome_title"], "Test Genome");
195        assert_eq!(parsed["version"], "2.1");
196        assert!(parsed["blueprint"].is_object());
197        assert!(parsed["brain_regions"].is_object());
198    }
199
200    #[test]
201    fn test_roundtrip() {
202        use crate::genome::GenomeParser;
203
204        // Create test data (use valid core ID)
205        use feagi_structures::genomic::cortical_area::CoreCorticalType;
206        let mut cortical_areas = HashMap::new();
207        let cortical_id = CoreCorticalType::Power.to_cortical_id();
208        let area = CorticalArea::new(
209            cortical_id,
210            0,
211            "Test Area".to_string(),
212            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
213            (5, 5, 5).into(),
214            CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
215        )
216        .unwrap();
217        cortical_areas.insert(cortical_id, area);
218
219        let mut brain_regions = HashMap::new();
220        let region = BrainRegion::new(
221            RegionID::new(),
222            "Root Region".to_string(),
223            RegionType::Undefined,
224        )
225        .unwrap();
226        brain_regions.insert("root".to_string(), (region, None));
227
228        // Save to JSON
229        #[allow(deprecated)]
230        let json = GenomeSaver::save_to_json(
231            &cortical_areas,
232            &brain_regions,
233            Some("test-roundtrip".to_string()),
234            Some("Roundtrip Test".to_string()),
235        )
236        .unwrap();
237
238        // Parse it back
239        let parsed = GenomeParser::parse(&json).unwrap();
240
241        // Verify data integrity
242        assert_eq!(parsed.genome_id, "test-roundtrip");
243        assert_eq!(parsed.genome_title, "Roundtrip Test");
244        assert_eq!(parsed.cortical_areas.len(), 1);
245        assert_eq!(parsed.brain_regions.len(), 1);
246
247        let area = &parsed.cortical_areas[0];
248        // cortical_id is now stored as CorticalID object after roundtrip
249        let expected_power_id =
250            feagi_structures::genomic::cortical_area::CoreCorticalType::Power.to_cortical_id();
251        assert_eq!(area.cortical_id, expected_power_id);
252        assert_eq!(area.name, "Test Area");
253        assert_eq!(area.dimensions.width, 10);
254        assert_eq!(area.position, (5, 5, 5).into());
255    }
256}