Skip to main content

feagi_services/genome/
change_classifier.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Classification system for cortical area changes to enable intelligent update routing.
6
7This module determines whether cortical area changes require:
8- Neuron array updates only (parameter changes)
9- Metadata updates only (name changes)
10- Synapse rebuild (structural changes like dimensions/neuron density)
11
12Based on Python implementation at: feagi-py/feagi/api/core/services/genome/change_classifier.py
13
14Copyright 2025 Neuraville Inc.
15Licensed under the Apache License, Version 2.0
16*/
17
18use serde_json::Value;
19use std::collections::{HashMap, HashSet};
20
21/// Types of cortical area changes requiring different update strategies
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum ChangeType {
24    /// Direct neuron array updates (NO synapse rebuild)
25    /// Examples: firing_threshold, leak_coefficient, refractory_period
26    /// Performance: ~2-5ms
27    Parameter,
28
29    /// Simple property updates (NO neuron/synapse changes)
30    /// Examples: cortical_name
31    /// Performance: ~1ms
32    Metadata,
33
34    /// Requires synapse rebuild (localized to affected area)
35    /// Examples: cortical_dimensions, neurons_per_voxel, coordinates_3d
36    /// Performance: ~100-200ms
37    Structural,
38
39    /// Multiple types mixed - requires intelligent routing
40    Hybrid,
41}
42
43/// Classifies cortical area changes to route them to optimal update mechanisms
44pub struct CorticalChangeClassifier;
45
46impl CorticalChangeClassifier {
47    /// Properties requiring synapse rebuild (affect neuron topology/count/connections)
48    ///
49    /// CRITICAL: These changes require deleting and rebuilding synapses TO and FROM
50    /// the affected cortical area via localized neuroembryogenesis
51    pub fn structural_changes() -> HashSet<&'static str> {
52        [
53            // Dimension changes → neuron count changes → synapse rebuild required
54            "cortical_dimensions",
55            "cortical_dimensions_per_device",
56            "dimensions",
57            // Neuron density changes → neuron count changes → synapse rebuild required
58            "per_voxel_neuron_cnt",
59            "cortical_neuron_per_vox_count",
60            "neuron_density",
61            "neurons_per_voxel",
62            // Type/role changes
63            "cortical_type",
64            "area_type",
65            // Topology changes
66            "cortical_mapping_dst",
67            // Classification changes
68            "group_id",
69            "sub_group_id",
70            "region_id",
71            "brain_region_id",
72            "parent_region_id",
73        ]
74        .iter()
75        .copied()
76        .collect()
77    }
78
79    /// Simple metadata that can be updated without affecting neurons/synapses
80    ///
81    /// NOTE: Position/coordinates are visualization metadata only - they don't affect
82    /// neural structure or connections in FEAGI (connections are topology-based, not spatial)
83    pub fn metadata_changes() -> HashSet<&'static str> {
84        [
85            "cortical_name",
86            "name",
87            "visible",
88            // Position changes are purely for visualization
89            "coordinate_2d",
90            "coordinates_2d",
91            "coordinates_3d",
92            "coordinate_3d",
93            "coordinates",
94            "position",
95            // Visualization-only aggregation control (BV/UI-driven)
96            "visualization_voxel_granularity",
97            // IO coding updates (cortical ID remap)
98            "coding_signage",
99            "coding_behavior",
100            "coding_type",
101            "new_cortical_id",
102        ]
103        .iter()
104        .copied()
105        .collect()
106    }
107
108    /// Parameters mappable to direct neuron array updates (NO synapse rebuild)
109    ///
110    /// CRITICAL: These changes ONLY update neuron array values in batch.
111    /// They do NOT affect neuron count, topology, or connections.
112    pub fn parameter_changes() -> HashSet<&'static str> {
113        [
114            // Firing threshold parameters
115            "firing_threshold",
116            "neuron_fire_threshold",
117            "firing_threshold_limit",
118            "neuron_firing_threshold_limit",
119            // Spatial gradient increments - can be updated in-place without rebuild
120            "firing_threshold_increment",
121            "neuron_fire_threshold_increment",
122            "firing_threshold_increment_x",
123            "firing_threshold_increment_y",
124            "firing_threshold_increment_z",
125            // Refractory period
126            "refractory_period",
127            "neuron_refractory_period",
128            "refrac",
129            // Leak parameters
130            "leak_coefficient",
131            "neuron_leak_coefficient",
132            "leak",
133            // NOTE: leak_variability is in special_parameters() - requires rebuild
134            // Consecutive fire parameters
135            "consecutive_fire_cnt_max",
136            "neuron_consecutive_fire_count",
137            "consecutive_fire_count",
138            // Snooze period
139            "snooze_length",
140            "neuron_snooze_period",
141            "snooze_period",
142            // Excitability
143            "neuron_excitability",
144            // Degeneration
145            "degeneration",
146            "neuron_degeneracy_coefficient",
147            // Postsynaptic current
148            "postsynaptic_current",
149            "neuron_post_synaptic_potential",
150            "postsynaptic_current_max",
151            "neuron_post_synaptic_potential_max",
152            // Memory parameters
153            "longterm_mem_threshold",
154            "neuron_longterm_mem_threshold",
155            "lifespan_growth_rate",
156            "neuron_lifespan_growth_rate",
157            "init_lifespan",
158            "neuron_init_lifespan",
159            "temporal_depth",
160            // Membrane potential
161            "mp_charge_accumulation",
162            "neuron_mp_charge_accumulation",
163            "mp_driven_psp",
164            "neuron_mp_driven_psp",
165            // Postsynaptic current distribution mode
166            // NOTE: This is a runtime propagation flag, not a structural/topology change.
167            "psp_uniform_distribution",
168            "neuron_psp_uniform_distribution",
169            // Plasticity
170            "plasticity_constant",
171            // Burst engine
172            "burst_engine_active",
173        ]
174        .iter()
175        .copied()
176        .collect()
177    }
178
179    /// Parameters that need special handling (may require rebuild)
180    ///
181    /// NOTE: Spatial gradient increments were moved to parameter_changes()
182    /// because they can now be updated in-place using position reconstruction.
183    pub fn special_parameters() -> HashSet<&'static str> {
184        [
185            "leak_variability",
186            "neuron_leak_variability",
187            "is_mem_type",
188            "dev_count",
189            "synapse_attractivity",
190            "visualization",
191            "location_generation_type",
192        ]
193        .iter()
194        .copied()
195        .collect()
196    }
197
198    /// Classify if changes are structural, parameter, metadata, or hybrid
199    pub fn classify_changes(changes: &HashMap<String, Value>) -> ChangeType {
200        let structural = Self::structural_changes();
201        let parameters = Self::parameter_changes();
202        let metadata = Self::metadata_changes();
203        let special = Self::special_parameters();
204
205        let has_structural = changes.keys().any(|k| structural.contains(k.as_str()));
206        let has_parameters = changes.keys().any(|k| parameters.contains(k.as_str()));
207        let has_metadata = changes.keys().any(|k| metadata.contains(k.as_str()));
208        let has_special = changes.keys().any(|k| special.contains(k.as_str()));
209
210        // Count change types
211        let change_count = [has_structural, has_parameters, has_metadata, has_special]
212            .iter()
213            .filter(|&&x| x)
214            .count();
215
216        if change_count > 1 {
217            ChangeType::Hybrid
218        } else if has_structural || has_special {
219            // Special params need rebuild for now
220            ChangeType::Structural
221        } else if has_parameters {
222            ChangeType::Parameter
223        } else if has_metadata {
224            ChangeType::Metadata
225        } else {
226            // Unknown changes - be safe and rebuild
227            tracing::warn!("Unknown change types detected: {:?}", changes.keys());
228            ChangeType::Structural
229        }
230    }
231
232    /// Separate changes into buckets by type for hybrid processing
233    pub fn separate_changes_by_type(
234        changes: &HashMap<String, Value>,
235    ) -> HashMap<ChangeType, HashMap<String, Value>> {
236        let structural = Self::structural_changes();
237        let parameters = Self::parameter_changes();
238        let metadata = Self::metadata_changes();
239        let special = Self::special_parameters();
240
241        let mut separated = HashMap::new();
242        separated.insert(ChangeType::Structural, HashMap::new());
243        separated.insert(ChangeType::Parameter, HashMap::new());
244        separated.insert(ChangeType::Metadata, HashMap::new());
245
246        for (key, value) in changes {
247            if structural.contains(key.as_str()) || special.contains(key.as_str()) {
248                separated
249                    .get_mut(&ChangeType::Structural)
250                    .unwrap()
251                    .insert(key.clone(), value.clone());
252            } else if parameters.contains(key.as_str()) {
253                separated
254                    .get_mut(&ChangeType::Parameter)
255                    .unwrap()
256                    .insert(key.clone(), value.clone());
257            } else if metadata.contains(key.as_str()) {
258                separated
259                    .get_mut(&ChangeType::Metadata)
260                    .unwrap()
261                    .insert(key.clone(), value.clone());
262            } else {
263                // Unknown - treat as structural to be safe
264                separated
265                    .get_mut(&ChangeType::Structural)
266                    .unwrap()
267                    .insert(key.clone(), value.clone());
268            }
269        }
270
271        separated
272    }
273
274    /// Log the classification result for debugging and monitoring
275    pub fn log_classification_result(changes: &HashMap<String, Value>, change_type: ChangeType) {
276        let change_summary: Vec<String> = changes
277            .iter()
278            .map(|(k, v)| format!("{}={}", k, v))
279            .collect();
280
281        tracing::info!(
282            "[CHANGE-CLASSIFIER] Type: {:?} | Changes: {}",
283            change_type,
284            change_summary.join(", ")
285        );
286
287        match change_type {
288            ChangeType::Parameter => {
289                tracing::info!(
290                    "[OPTIMIZATION] Fast parameter update path selected - avoiding synapse rebuild"
291                );
292            }
293            ChangeType::Metadata => {
294                tracing::info!("[OPTIMIZATION] Metadata-only update - minimal processing required");
295            }
296            ChangeType::Structural => {
297                tracing::info!("[STRUCTURAL] Synapse rebuild required for this change");
298            }
299            ChangeType::Hybrid => {
300                tracing::info!(
301                    "[HYBRID] Mixed changes - using optimized combination of update paths"
302                );
303            }
304        }
305    }
306}