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