Skip to main content

feagi_evolutionary/
modulators.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Genome modulator instances.
5//!
6//! An instance is a named copy of a built-in [`ModulatorKind`] plus the cortical
7//! id of the 1x1x1 driver area that turns it on. Areas and mapping rules
8//! subscribe by instance id.
9
10use feagi_structures::genomic::cortical_area::CorticalID;
11use feagi_structures::genomic::{
12    validate_instance_fields, ModulatorKind, ModulatorValidationError,
13};
14use serde::{Deserialize, Serialize};
15use serde_json::{json, Value};
16use std::collections::HashMap;
17
18use crate::types::{EvoError, EvoResult};
19
20/// One user-defined modulator and the driver area that owns it.
21#[derive(Debug, Clone, PartialEq)]
22pub struct ModulatorInstance {
23    pub kind: ModulatorKind,
24    pub magnitude_percent: f32,
25    pub effect_duration_bursts: u16,
26    pub rest_bursts: u16,
27    pub graded: bool,
28    pub full_scale_potential: Option<f32>,
29    pub driver_cortical_id: CorticalID,
30}
31
32impl ModulatorInstance {
33    /// Reject fields that cannot produce a defined signal.
34    pub fn validate(&self) -> Result<(), ModulatorValidationError> {
35        validate_instance_fields(
36            self.magnitude_percent,
37            self.effect_duration_bursts,
38            self.graded,
39            self.full_scale_potential,
40        )
41    }
42
43    /// Hierarchical genome object for this instance.
44    pub fn to_json(&self) -> Value {
45        let mut object = serde_json::Map::new();
46        object.insert("type".to_string(), json!(self.kind.as_str()));
47        object.insert(
48            "magnitude_percent".to_string(),
49            json!(self.magnitude_percent),
50        );
51        object.insert(
52            "effect_duration_bursts".to_string(),
53            json!(self.effect_duration_bursts),
54        );
55        object.insert("rest_bursts".to_string(), json!(self.rest_bursts));
56        object.insert("graded".to_string(), json!(self.graded));
57        if let Some(scale) = self.full_scale_potential {
58            object.insert("full_scale_potential".to_string(), json!(scale));
59        }
60        object.insert(
61            "driver_cortical_id".to_string(),
62            json!(self.driver_cortical_id.as_base_64()),
63        );
64        Value::Object(object)
65    }
66}
67
68/// All modulator instances in a genome, keyed by instance id.
69#[derive(Debug, Clone, Default)]
70pub struct ModulatorRegistry {
71    modulators: HashMap<String, ModulatorInstance>,
72}
73
74impl ModulatorRegistry {
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    pub fn insert(&mut self, id: String, instance: ModulatorInstance) {
80        self.modulators.insert(id, instance);
81    }
82
83    pub fn get(&self, id: &str) -> Option<&ModulatorInstance> {
84        self.modulators.get(id)
85    }
86
87    pub fn get_mut(&mut self, id: &str) -> Option<&mut ModulatorInstance> {
88        self.modulators.get_mut(id)
89    }
90
91    pub fn contains(&self, id: &str) -> bool {
92        self.modulators.contains_key(id)
93    }
94
95    pub fn remove(&mut self, id: &str) -> Option<ModulatorInstance> {
96        self.modulators.remove(id)
97    }
98
99    pub fn rename(&mut self, old_id: &str, new_id: String) -> bool {
100        if let Some(instance) = self.modulators.remove(old_id) {
101            self.modulators.insert(new_id, instance);
102            true
103        } else {
104            false
105        }
106    }
107
108    pub fn len(&self) -> usize {
109        self.modulators.len()
110    }
111
112    pub fn is_empty(&self) -> bool {
113        self.modulators.is_empty()
114    }
115
116    pub fn ids(&self) -> Vec<String> {
117        let mut ids: Vec<String> = self.modulators.keys().cloned().collect();
118        ids.sort();
119        ids
120    }
121
122    pub fn iter(&self) -> impl Iterator<Item = (&String, &ModulatorInstance)> {
123        self.modulators.iter()
124    }
125
126    /// Hierarchical `modulators` object.
127    pub fn to_json(&self) -> Value {
128        let mut object = serde_json::Map::new();
129        let mut ids: Vec<&String> = self.modulators.keys().collect();
130        ids.sort();
131        for id in ids {
132            if let Some(instance) = self.modulators.get(id) {
133                object.insert(id.clone(), instance.to_json());
134            }
135        }
136        Value::Object(object)
137    }
138}
139
140/// Parse the top-level `modulators` object. An absent object is an empty registry.
141pub fn parse_modulator_registry(value: Option<&Value>) -> EvoResult<ModulatorRegistry> {
142    let Some(value) = value else {
143        return Ok(ModulatorRegistry::new());
144    };
145    let Some(object) = value.as_object() else {
146        return Err(EvoError::InvalidGenome(
147            "modulators must be an object".to_string(),
148        ));
149    };
150    let mut registry = ModulatorRegistry::new();
151    for (id, body) in object {
152        registry.insert(id.clone(), parse_modulator_instance(id, body)?);
153    }
154    Ok(registry)
155}
156
157fn parse_modulator_instance(id: &str, body: &Value) -> EvoResult<ModulatorInstance> {
158    let object = body
159        .as_object()
160        .ok_or_else(|| EvoError::InvalidGenome(format!("modulator '{id}' must be an object")))?;
161    let type_name = object
162        .get("type")
163        .and_then(|v| v.as_str())
164        .ok_or_else(|| EvoError::InvalidGenome(format!("modulator '{id}' is missing type")))?;
165    let kind = ModulatorKind::parse(type_name)
166        .map_err(|err| EvoError::InvalidGenome(format!("modulator '{id}': {err}")))?;
167    let magnitude_percent = object
168        .get("magnitude_percent")
169        .and_then(|v| v.as_f64())
170        .ok_or_else(|| {
171            EvoError::InvalidGenome(format!("modulator '{id}' is missing magnitude_percent"))
172        })? as f32;
173    let effect_duration_bursts = object
174        .get("effect_duration_bursts")
175        .and_then(|v| v.as_u64())
176        .ok_or_else(|| {
177            EvoError::InvalidGenome(format!(
178                "modulator '{id}' is missing effect_duration_bursts"
179            ))
180        })?;
181    if effect_duration_bursts > u16::MAX as u64 {
182        return Err(EvoError::InvalidGenome(format!(
183            "modulator '{id}' effect_duration_bursts exceeds u16"
184        )));
185    }
186    let rest_bursts = object
187        .get("rest_bursts")
188        .and_then(|v| v.as_u64())
189        .unwrap_or(0);
190    if rest_bursts > u16::MAX as u64 {
191        return Err(EvoError::InvalidGenome(format!(
192            "modulator '{id}' rest_bursts exceeds u16"
193        )));
194    }
195    let graded = object
196        .get("graded")
197        .and_then(|v| v.as_bool())
198        .unwrap_or(false);
199    let full_scale_potential = object
200        .get("full_scale_potential")
201        .and_then(|v| v.as_f64())
202        .map(|v| v as f32);
203    let driver = object
204        .get("driver_cortical_id")
205        .and_then(|v| v.as_str())
206        .ok_or_else(|| {
207            EvoError::InvalidGenome(format!("modulator '{id}' is missing driver_cortical_id"))
208        })?;
209    let driver_cortical_id = CorticalID::try_from_base_64(driver)
210        .map_err(|err| EvoError::InvalidGenome(format!("modulator '{id}' driver id: {err}")))?;
211    let instance = ModulatorInstance {
212        kind,
213        magnitude_percent,
214        effect_duration_bursts: effect_duration_bursts as u16,
215        rest_bursts: rest_bursts as u16,
216        graded,
217        full_scale_potential,
218        driver_cortical_id,
219    };
220    instance
221        .validate()
222        .map_err(|err| EvoError::InvalidGenome(format!("modulator '{id}': {err}")))?;
223    Ok(instance)
224}
225
226/// Locked neuron properties the instance writes onto its driver area.
227pub fn driver_locked_properties(instance: &ModulatorInstance) -> HashMap<String, Value> {
228    let mut properties = HashMap::new();
229    properties.insert("refractory_period".to_string(), json!(0));
230    properties.insert("spike_train".to_string(), json!(true));
231    properties.insert(
232        "consecutive_fire_cnt_max".to_string(),
233        json!(instance.effect_duration_bursts),
234    );
235    properties.insert(
236        "consecutive_fire_limit".to_string(),
237        json!(instance.effect_duration_bursts),
238    );
239    properties.insert("snooze_length".to_string(), json!(instance.rest_bursts));
240    properties.insert("snooze_period".to_string(), json!(instance.rest_bursts));
241    properties.insert("mp_driven_psp".to_string(), json!(instance.graded));
242    properties.insert("cortical_group".to_string(), json!("MODULATOR"));
243    if let Some(scale) = instance.full_scale_potential {
244        properties.insert("full_scale_potential".to_string(), json!(scale));
245    }
246    properties
247}
248
249/// Property names the API and Brain Visualizer must leave read-only on a driver.
250pub fn driver_locked_property_names() -> &'static [&'static str] {
251    &[
252        "refractory_period",
253        "spike_train",
254        "consecutive_fire_cnt_max",
255        "consecutive_fire_limit",
256        "neuron_consecutive_fire_count",
257        "consecutive_fire_count",
258        "snooze_length",
259        "snooze_period",
260        "mp_driven_psp",
261    ]
262}
263
264/// Request body shape shared by create and update. Serde is used by the API layer.
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct ModulatorWrite {
267    #[serde(rename = "type")]
268    pub kind: String,
269    pub magnitude_percent: f32,
270    pub effect_duration_bursts: u16,
271    pub rest_bursts: u16,
272    #[serde(default)]
273    pub graded: bool,
274    #[serde(default)]
275    pub full_scale_potential: Option<f32>,
276}
277
278impl ModulatorWrite {
279    pub fn into_instance(self, driver_cortical_id: CorticalID) -> EvoResult<ModulatorInstance> {
280        let kind = ModulatorKind::parse(&self.kind).map_err(EvoError::InvalidGenome)?;
281        let instance = ModulatorInstance {
282            kind,
283            magnitude_percent: self.magnitude_percent,
284            effect_duration_bursts: self.effect_duration_bursts,
285            rest_bursts: self.rest_bursts,
286            graded: self.graded,
287            full_scale_potential: self.full_scale_potential,
288            driver_cortical_id,
289        };
290        instance
291            .validate()
292            .map_err(|err| EvoError::InvalidGenome(err.0))?;
293        Ok(instance)
294    }
295}