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