Skip to main content

feagi_evolutionary/
runtime.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Runtime genome representation for FEAGI.
6
7This module defines the in-memory Rust objects that represent a loaded genome.
8These objects are created by the genome parser and consumed by neuroembryogenesis.
9
10Copyright 2025 Neuraville Inc.
11Licensed under the Apache License, Version 2.0
12*/
13
14use feagi_structures::genomic::classifiers::Classifier;
15use feagi_structures::genomic::cortical_area::CorticalArea;
16use feagi_structures::genomic::cortical_area::CorticalID;
17use feagi_structures::genomic::BrainRegion;
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// Complete runtime genome representation
22#[derive(Debug, Clone)]
23pub struct RuntimeGenome {
24    /// Genome metadata
25    pub metadata: GenomeMetadata,
26
27    /// Cortical areas (by cortical_id as CorticalID)
28    pub cortical_areas: HashMap<CorticalID, CorticalArea>,
29
30    /// Brain regions (by region_id)
31    pub brain_regions: HashMap<String, BrainRegion>,
32
33    /// Classifier assemblies (by classifier_id). Parallel to `brain_regions`;
34    /// not a region and not exportable as a circuit.
35    pub classifiers: HashMap<String, Classifier>,
36
37    /// Morphology registry
38    pub morphologies: MorphologyRegistry,
39
40    /// Physiology configuration
41    pub physiology: PhysiologyConfig,
42
43    /// Genome signatures
44    pub signatures: GenomeSignatures,
45
46    /// Statistics
47    pub stats: GenomeStats,
48
49    /// Recorded structural changes (optional top-level `change_history` key).
50    ///
51    /// Opaque entries owned by the change ledger in `feagi-services`; the genome only
52    /// carries them. Never part of `signatures`.
53    pub change_history: Vec<serde_json::Value>,
54}
55
56impl RuntimeGenome {
57    /// Add every rule a classifier requires that its mapping lists lack.
58    ///
59    /// The classifier record is authoritative for its own edges. Kernel encode
60    /// (`episodic_memory`) and field scan (`episodic_scan`) can share one edge, and
61    /// each is required on its own. Existing rules are left as they are.
62    /// Returns the number of rules added.
63    pub fn apply_classifier_required_mappings(&mut self) -> usize {
64        let mut added = 0usize;
65        for classifier in self.classifiers.values() {
66            let Some(associative_window) =
67                CorticalID::try_from_base_64(&classifier.kernel_memory_id)
68                    .ok()
69                    .and_then(|id| self.cortical_areas.get(&id))
70                    .and_then(|area| crate::extract_memory_properties(&area.properties))
71                    .map(|props| props.temporal_depth)
72            else {
73                continue;
74            };
75            for mapping in classifier.required_mappings() {
76                let Ok(dst_id) = CorticalID::try_from_base_64(&mapping.dst_area_id) else {
77                    continue;
78                };
79                if !self.cortical_areas.contains_key(&dst_id) {
80                    continue;
81                }
82                let Ok(src_id) = CorticalID::try_from_base_64(&mapping.src_area_id) else {
83                    continue;
84                };
85                let Some(src_area) = self.cortical_areas.get_mut(&src_id) else {
86                    continue;
87                };
88                let Some(mapping_dst) = src_area
89                    .properties
90                    .entry("cortical_mapping_dst".to_string())
91                    .or_insert_with(|| serde_json::json!({}))
92                    .as_object_mut()
93                else {
94                    continue;
95                };
96                let Some(rules) = mapping_dst
97                    .entry(mapping.dst_area_id.clone())
98                    .or_insert_with(|| serde_json::json!([]))
99                    .as_array_mut()
100                else {
101                    continue;
102                };
103                let present = rules.iter().any(|rule| {
104                    rule.get("morphology_id").and_then(|v| v.as_str())
105                        == Some(mapping.morphology_id.as_str())
106                });
107                if present {
108                    continue;
109                }
110                rules.push(
111                    feagi_structures::genomic::classifiers::classifier_mapping_rule(
112                        &mapping.morphology_id,
113                        associative_window,
114                    ),
115                );
116                added += 1;
117            }
118        }
119        added
120    }
121}
122
123/// Genome metadata
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct GenomeMetadata {
126    pub genome_id: String,
127    pub genome_title: String,
128    pub genome_description: String,
129    pub version: String,
130    pub timestamp: f64, // Unix timestamp
131
132    /// Root brain region ID (UUID string) - explicit identification for O(1) lookup
133    /// This eliminates the need to search through all regions to find which has no parent
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub brain_regions_root: Option<String>,
136}
137
138/// Neuron morphology registry
139#[derive(Debug, Clone, Default)]
140pub struct MorphologyRegistry {
141    /// All morphologies by morphology_id
142    morphologies: HashMap<String, Morphology>,
143}
144
145impl MorphologyRegistry {
146    /// Create empty registry
147    pub fn new() -> Self {
148        Self::default()
149    }
150
151    /// Add a morphology
152    pub fn add_morphology(&mut self, id: String, morphology: Morphology) {
153        self.morphologies.insert(id, morphology);
154    }
155
156    /// Get a morphology by ID
157    pub fn get(&self, id: &str) -> Option<&Morphology> {
158        self.morphologies.get(id)
159    }
160
161    /// Check if morphology exists
162    pub fn contains(&self, id: &str) -> bool {
163        self.morphologies.contains_key(id)
164    }
165
166    /// Get all morphology IDs
167    pub fn morphology_ids(&self) -> Vec<String> {
168        self.morphologies.keys().cloned().collect()
169    }
170
171    /// Remove a morphology by ID.
172    ///
173    /// Returns true if the morphology existed and was removed.
174    pub fn remove_morphology(&mut self, id: &str) -> bool {
175        self.morphologies.remove(id).is_some()
176    }
177
178    /// Get count of morphologies
179    pub fn count(&self) -> usize {
180        self.morphologies.len()
181    }
182
183    /// Iterate over all morphologies
184    pub fn iter(&self) -> impl Iterator<Item = (&String, &Morphology)> {
185        self.morphologies.iter()
186    }
187}
188
189/// Neuron morphology definition
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct Morphology {
192    /// Morphology type: "vectors", "patterns", "functions", or "composite"
193    pub morphology_type: MorphologyType,
194
195    /// Morphology parameters
196    pub parameters: MorphologyParameters,
197
198    /// Morphology class: "core", "custom", etc.
199    pub class: String,
200}
201
202/// Morphology type enum
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204#[serde(rename_all = "lowercase")]
205pub enum MorphologyType {
206    /// Vector-based morphology (3D offset vectors)
207    Vectors,
208
209    /// Pattern-based morphology (source → destination patterns)
210    Patterns,
211
212    /// Function-based morphology (built-in algorithms)
213    Functions,
214
215    /// Composite morphology (combines multiple morphologies)
216    Composite,
217}
218
219/// Morphology parameters (type-specific)
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(untagged)]
222pub enum MorphologyParameters {
223    /// Vector parameters: list of [x, y, z] offsets
224    Vectors { vectors: Vec<[i32; 3]> },
225
226    /// Pattern parameters: list of [source_pattern, dest_pattern] pairs
227    Patterns {
228        patterns: Vec<[Vec<PatternElement>; 2]>,
229    },
230
231    /// Function parameters: empty for built-in functions
232    Functions {},
233
234    /// Composite parameters: combines seed + pattern + mapper
235    Composite {
236        src_seed: [u32; 3],
237        src_pattern: Vec<[i32; 2]>,
238        mapper_morphology: String,
239    },
240}
241
242/// Pattern element: exact value, wildcard (*), skip (?), exclude (!), relative, or `N..M`
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub enum PatternElement {
245    /// Exact coordinate value
246    Value(i32),
247    /// Wildcard - matches any value
248    Wildcard, // "*"
249    /// Skip - don't check this coordinate
250    Skip, // "?"
251    /// Exclude - exclude this coordinate
252    Exclude, // "!"
253    /// All coordinates strictly above source on this axis
254    DirectionPositive, // "?+"
255    /// All coordinates strictly below source on this axis
256    DirectionNegative, // "?-"
257    /// All coordinates at or above source on this axis
258    DirectionPositiveInclusive, // "?+="
259    /// All coordinates at or below source on this axis
260    DirectionNegativeInclusive, // "?-="
261    /// Single coordinate at offset from source
262    Offset(i32), // "?+N" or "?-N"
263    /// Inclusive range relative to source [src+lo, src+hi]
264    Range(i32, i32), // "?-A:?+B"
265    /// Inclusive absolute range [N, M]
266    AbsoluteRange(i32, i32), // "N..M"
267}
268
269// Custom serialization to convert PatternElement back to JSON properly
270impl Serialize for PatternElement {
271    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
272    where
273        S: serde::Serializer,
274    {
275        match self {
276            PatternElement::Value(v) => serializer.serialize_i32(*v),
277            PatternElement::Wildcard => serializer.serialize_str("*"),
278            PatternElement::Skip => serializer.serialize_str("?"),
279            PatternElement::Exclude => serializer.serialize_str("!"),
280            PatternElement::DirectionPositive => serializer.serialize_str("?+"),
281            PatternElement::DirectionNegative => serializer.serialize_str("?-"),
282            PatternElement::DirectionPositiveInclusive => serializer.serialize_str("?+="),
283            PatternElement::DirectionNegativeInclusive => serializer.serialize_str("?-="),
284            PatternElement::Offset(off) => {
285                if *off >= 0 {
286                    serializer.serialize_str(&format!("?+{}", off))
287                } else {
288                    serializer.serialize_str(&format!("?{}", off))
289                }
290            }
291            PatternElement::Range(lo, hi) => {
292                let lo_str = if *lo >= 0 {
293                    format!("?+{}", lo)
294                } else {
295                    format!("?{}", lo)
296                };
297                let hi_str = if *hi >= 0 {
298                    format!("?+{}", hi)
299                } else {
300                    format!("?{}", hi)
301                };
302                serializer.serialize_str(&format!("{}:{}", lo_str, hi_str))
303            }
304            PatternElement::AbsoluteRange(lo, hi) => {
305                serializer.serialize_str(&format!("{}..{}", lo, hi))
306            }
307        }
308    }
309}
310
311// Custom deserialization to parse JSON into PatternElement
312impl<'de> Deserialize<'de> for PatternElement {
313    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
314    where
315        D: serde::Deserializer<'de>,
316    {
317        let value = serde_json::Value::deserialize(deserializer)?;
318        match value {
319            serde_json::Value::Number(n) => {
320                if let Some(i) = n.as_i64() {
321                    Ok(PatternElement::Value(i as i32))
322                } else {
323                    Err(serde::de::Error::custom(
324                        "Pattern element must be an integer",
325                    ))
326                }
327            }
328            serde_json::Value::String(s) => Self::parse_string(&s)
329                .ok_or_else(|| serde::de::Error::custom(format!("Unknown pattern element: {}", s))),
330            _ => Err(serde::de::Error::custom(
331                "Pattern element must be number or string",
332            )),
333        }
334    }
335}
336
337impl PatternElement {
338    /// Parse a pattern element from its string representation.
339    pub fn parse_string(s: &str) -> Option<Self> {
340        match s {
341            "*" => Some(PatternElement::Wildcard),
342            "?" => Some(PatternElement::Skip),
343            "!" => Some(PatternElement::Exclude),
344            "?+" => Some(PatternElement::DirectionPositive),
345            "?-" => Some(PatternElement::DirectionNegative),
346            "?+=" => Some(PatternElement::DirectionPositiveInclusive),
347            "?-=" => Some(PatternElement::DirectionNegativeInclusive),
348            _ => {
349                if let Some(range) = Self::try_parse_range(s) {
350                    return Some(range);
351                }
352                if let Some(abs_range) = Self::try_parse_absolute_range(s) {
353                    return Some(abs_range);
354                }
355                if let Some(offset) = Self::try_parse_offset(s) {
356                    return Some(offset);
357                }
358                None
359            }
360        }
361    }
362
363    fn try_parse_range(s: &str) -> Option<Self> {
364        let parts: Vec<&str> = s.split(':').collect();
365        if parts.len() != 2 {
366            return None;
367        }
368        let lo = Self::extract_relative_offset(parts[0])?;
369        let hi = Self::extract_relative_offset(parts[1])?;
370        Some(PatternElement::Range(lo, hi))
371    }
372
373    fn try_parse_absolute_range(s: &str) -> Option<Self> {
374        let idx = s.find("..")?;
375        if s[idx + 2..].contains("..") {
376            return None;
377        }
378        let lo = s[..idx].parse::<i32>().ok()?;
379        let hi = s[idx + 2..].parse::<i32>().ok()?;
380        Some(PatternElement::AbsoluteRange(lo, hi))
381    }
382
383    fn try_parse_offset(s: &str) -> Option<Self> {
384        let offset = Self::extract_relative_offset(s)?;
385        Some(PatternElement::Offset(offset))
386    }
387
388    fn extract_relative_offset(s: &str) -> Option<i32> {
389        if !s.starts_with('?') {
390            return None;
391        }
392        let rest = &s[1..];
393        if rest.is_empty() || rest == "+" || rest == "-" || rest == "+=" || rest == "-=" {
394            return None;
395        }
396        rest.parse::<i32>().ok()
397    }
398}
399
400/// Physiology configuration (runtime parameters)
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct PhysiologyConfig {
403    /// Simulation timestep in seconds (formerly burst_delay)
404    pub simulation_timestep: f64,
405
406    /// Maximum neuron age
407    pub max_age: u64,
408
409    /// Evolution burst count
410    pub evolution_burst_count: u64,
411
412    /// IPU idle threshold
413    pub ipu_idle_threshold: u64,
414
415    /// Plasticity queue depth
416    pub plasticity_queue_depth: usize,
417
418    /// Lifespan management interval
419    pub lifespan_mgmt_interval: u64,
420
421    /// Quantization precision for numeric values
422    /// Options: "fp32" (default), "fp16", "int8"
423    #[serde(default = "default_quantization_precision")]
424    pub quantization_precision: String,
425}
426
427pub fn default_quantization_precision() -> String {
428    "int8".to_string() // Default to INT8 for memory efficiency
429}
430
431impl Default for PhysiologyConfig {
432    fn default() -> Self {
433        Self {
434            simulation_timestep: 0.025,
435            max_age: 10_000_000,
436            evolution_burst_count: 50,
437            ipu_idle_threshold: 1000,
438            plasticity_queue_depth: 3,
439            lifespan_mgmt_interval: 10,
440            quantization_precision: default_quantization_precision(),
441        }
442    }
443}
444
445/// Genome signatures for comparison
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct GenomeSignatures {
448    /// Full genome signature
449    pub genome: String,
450
451    /// Blueprint signature
452    pub blueprint: String,
453
454    /// Physiology signature
455    pub physiology: String,
456
457    /// Morphologies signature (optional, for future extension)
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub morphologies: Option<String>,
460}
461
462/// Genome statistics
463#[derive(Debug, Clone, Serialize, Deserialize, Default)]
464pub struct GenomeStats {
465    /// Innate cortical area count
466    pub innate_cortical_area_count: usize,
467
468    /// Innate neuron count
469    pub innate_neuron_count: usize,
470
471    /// Innate synapse count
472    pub innate_synapse_count: usize,
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    #[test]
480    fn test_morphology_registry_creation() {
481        let registry = MorphologyRegistry::new();
482        assert_eq!(registry.count(), 0);
483    }
484
485    #[test]
486    fn test_morphology_registry_add_and_get() {
487        let mut registry = MorphologyRegistry::new();
488
489        let morphology = Morphology {
490            morphology_type: MorphologyType::Vectors,
491            parameters: MorphologyParameters::Vectors {
492                vectors: vec![[1, 0, 0], [0, 1, 0]],
493            },
494            class: "test".to_string(),
495        };
496
497        registry.add_morphology("test_morph".to_string(), morphology);
498
499        assert_eq!(registry.count(), 1);
500        assert!(registry.contains("test_morph"));
501        assert!(registry.get("test_morph").is_some());
502    }
503
504    fn classifier_genome(kernel_to_kmem_rules: Vec<serde_json::Value>) -> RuntimeGenome {
505        use feagi_structures::genomic::classifiers::{
506            Classifier, ClassifierField, ClassifierTrainingMode,
507        };
508        use feagi_structures::genomic::cortical_area::{
509            CorticalAreaDimensions, CorticalAreaType, CustomCorticalType, MemoryCorticalType,
510        };
511
512        let area = |id: &str, is_memory: bool| {
513            let kind = if is_memory {
514                CorticalAreaType::Memory(MemoryCorticalType::Memory)
515            } else {
516                CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire)
517            };
518            let mut area = CorticalArea::new(
519                CorticalID::try_from_base_64(id).expect("id"),
520                0,
521                id.to_string(),
522                CorticalAreaDimensions::new(1, 1, 1).expect("dims"),
523                (0, 0, 0).into(),
524                kind,
525            )
526            .expect("area");
527            if is_memory {
528                area.properties
529                    .insert("is_mem_type".to_string(), serde_json::json!(true));
530                area.properties
531                    .insert("temporal_depth".to_string(), serde_json::json!(2));
532            }
533            area
534        };
535        let mut kernel = area("Y01OSVNUX9w=", false);
536        kernel.properties.insert(
537            "cortical_mapping_dst".to_string(),
538            serde_json::json!({ "bU1OSVNUXx8=": kernel_to_kmem_rules }),
539        );
540        let mut cortical_areas = HashMap::new();
541        for a in [
542            kernel,
543            area("Y01OSVNUX+E=", false),
544            area("Y01OSVNUX8Y=", false),
545            area("bU1OSVNUXx8=", true),
546            area("bU1OSVNUXyA=", true),
547        ] {
548            cortical_areas.insert(a.cortical_id, a);
549        }
550        let classifier = Classifier {
551            classifier_id: "clf".to_string(),
552            name: "clf".to_string(),
553            parent_region_id: "region".to_string(),
554            coordinates_3d: [0, 0, 0],
555            training_mode: ClassifierTrainingMode::Kernel,
556            kernel_area_id: Some("Y01OSVNUX9w=".to_string()),
557            class_area_id: Some("Y01OSVNUX+E=".to_string()),
558            mask_area_id: None,
559            class_count: None,
560            kernel_size: None,
561            fields: vec![ClassifierField {
562                field_area_id: "Y01OSVNUX9w=".to_string(),
563                scan_twin_id: "Y01OSVNUX8Y=".to_string(),
564            }],
565            kernel_memory_id: "bU1OSVNUXx8=".to_string(),
566            class_memory_id: "bU1OSVNUXyA=".to_string(),
567            reward_training: false,
568            answer_feedback_area_id: None,
569            pain_area_id: None,
570            pleasure_area_id: None,
571            answer_latency_bursts: 0,
572            learn_area_id: None,
573            confidence_area_id: None,
574            properties: HashMap::new(),
575        };
576        RuntimeGenome {
577            metadata: GenomeMetadata {
578                genome_id: "t".to_string(),
579                genome_title: "t".to_string(),
580                genome_description: String::new(),
581                version: "3.0".to_string(),
582                timestamp: 0.0,
583                brain_regions_root: None,
584            },
585            cortical_areas,
586            brain_regions: HashMap::new(),
587            classifiers: HashMap::from([("clf".to_string(), classifier)]),
588            morphologies: MorphologyRegistry::new(),
589            physiology: PhysiologyConfig::default(),
590            signatures: GenomeSignatures {
591                genome: "0".to_string(),
592                blueprint: "0".to_string(),
593                physiology: "0".to_string(),
594                morphologies: None,
595            },
596            stats: GenomeStats::default(),
597            change_history: Vec::new(),
598        }
599    }
600
601    fn morphologies(genome: &RuntimeGenome, src: &str, dst: &str) -> Vec<String> {
602        genome.cortical_areas[&CorticalID::try_from_base_64(src).unwrap()]
603            .properties
604            .get("cortical_mapping_dst")
605            .and_then(|m| m.get(dst))
606            .and_then(|r| r.as_array())
607            .map(|rules| {
608                rules
609                    .iter()
610                    .filter_map(|r| r["morphology_id"].as_str().map(str::to_string))
611                    .collect()
612            })
613            .unwrap_or_default()
614    }
615
616    #[test]
617    fn scan_only_kernel_edge_regains_episodic_memory() {
618        use feagi_structures::genomic::classifiers::classifier_mapping_rule;
619        let mut genome = classifier_genome(vec![classifier_mapping_rule("episodic_scan", 2)]);
620
621        let added = genome.apply_classifier_required_mappings();
622
623        let kernel_edge = morphologies(&genome, "Y01OSVNUX9w=", "bU1OSVNUXx8=");
624        assert!(kernel_edge.contains(&"episodic_scan".to_string()));
625        assert!(kernel_edge.contains(&"episodic_memory".to_string()));
626        assert_eq!(
627            morphologies(&genome, "Y01OSVNUX+E=", "bU1OSVNUXyA="),
628            vec!["episodic_memory".to_string()]
629        );
630        let assoc = morphologies(&genome, "bU1OSVNUXx8=", "bU1OSVNUXyA=");
631        assert_eq!(assoc, vec!["associative_memory".to_string()]);
632        let assoc_rule = &genome.cortical_areas
633            [&CorticalID::try_from_base_64("bU1OSVNUXx8=").unwrap()]
634            .properties["cortical_mapping_dst"]["bU1OSVNUXyA="][0];
635        assert_eq!(assoc_rule["plasticity_window"], serde_json::json!(2));
636        assert_eq!(added, 3);
637    }
638
639    #[test]
640    fn complete_classifier_edges_are_left_unchanged() {
641        use feagi_structures::genomic::classifiers::classifier_mapping_rule;
642        let mut genome = classifier_genome(vec![
643            classifier_mapping_rule("episodic_memory", 2),
644            classifier_mapping_rule("episodic_scan", 2),
645        ]);
646        genome.apply_classifier_required_mappings();
647        let before = genome.cortical_areas.clone();
648
649        assert_eq!(genome.apply_classifier_required_mappings(), 0);
650        for (id, area) in &before {
651            assert_eq!(
652                area.properties.get("cortical_mapping_dst"),
653                genome.cortical_areas[id]
654                    .properties
655                    .get("cortical_mapping_dst")
656            );
657        }
658    }
659
660    #[test]
661    fn test_physiology_config_default() {
662        let config = PhysiologyConfig::default();
663        assert_eq!(config.simulation_timestep, 0.025);
664        assert_eq!(config.max_age, 10_000_000);
665    }
666}