Skip to main content

feagi_brain_development/
connectome_manager.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5ConnectomeManager - Core brain connectivity manager.
6
7This is the central orchestrator for the FEAGI connectome, managing:
8- Cortical areas and their metadata
9- Brain regions and hierarchy
10- Neuron/synapse queries (delegates to NPU for actual data)
11- Genome loading and persistence
12
13## Architecture
14
15The ConnectomeManager is a **metadata manager** that:
161. Stores cortical area/region definitions
172. Provides a high-level API for brain structure queries
183. Delegates neuron/synapse CRUD to the NPU (Structure of Arrays)
19
20## Design Principles
21
22- **Singleton**: One global instance per FEAGI process
23- **Thread-safe**: Uses RwLock for concurrent reads
24- **Performance**: Optimized for hot-path queries (area lookups)
25- **NPU Delegation**: Neuron/synapse data lives in NPU, not here
26
27Copyright 2025 Neuraville Inc.
28Licensed under the Apache License, Version 2.0
29*/
30
31use once_cell::sync::Lazy;
32use parking_lot::RwLock;
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::hash::Hasher;
36use std::sync::atomic::{AtomicUsize, Ordering};
37use std::sync::{Arc, Mutex};
38use tracing::{debug, error, info, trace, warn};
39use xxhash_rust::xxh64::Xxh64;
40
41type BrainRegionIoRegistry = HashMap<String, (Vec<String>, Vec<String>)>;
42
43use crate::models::{BrainRegion, BrainRegionHierarchy, CorticalArea, CorticalAreaDimensions};
44use crate::types::{BduError, BduResult};
45use feagi_npu_neural::types::NeuronId;
46use feagi_structures::genomic::cortical_area::{
47    CoreCorticalType, CorticalAreaType, CorticalID, CustomCorticalType,
48};
49use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
50
51// State manager access for fatigue calculation
52// Note: feagi-state-manager is always available when std is enabled (it's a default feature)
53use feagi_state_manager::StateManager;
54
55const DATA_HASH_SEED: u64 = 0;
56// JSON number precision (Godot) is limited to 53 bits; mask to keep hashes stable across transports.
57const HASH_SAFE_MASK: u64 = (1u64 << 53) - 1;
58
59// NPU integration (optional dependency)
60// use feagi_npu_burst_engine::RustNPU; // Now using DynamicNPU
61
62/// Global singleton instance of ConnectomeManager
63static INSTANCE: Lazy<Arc<RwLock<ConnectomeManager>>> =
64    Lazy::new(|| Arc::new(RwLock::new(ConnectomeManager::new())));
65
66/// Configuration for ConnectomeManager
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ConnectomeConfig {
69    /// Maximum number of neurons (for NPU sizing)
70    pub max_neurons: usize,
71
72    /// Maximum number of synapses (for NPU sizing)
73    pub max_synapses: usize,
74
75    /// Backend type ("cpu", "cuda", "wgpu")
76    pub backend: String,
77}
78
79impl Default for ConnectomeConfig {
80    fn default() -> Self {
81        Self {
82            max_neurons: 10_000_000,
83            max_synapses: 100_000_000,
84            backend: "cpu".to_string(),
85        }
86    }
87}
88
89/// Central manager for the FEAGI connectome
90///
91/// ## Responsibilities
92///
93/// 1. **Cortical Area Management**: Add, remove, query cortical areas
94/// 2. **Brain Region Management**: Hierarchical organization
95/// 3. **Neuron/Synapse Queries**: High-level API (delegates to NPU)
96/// 4. **Genome I/O**: Load/save brain structure
97///
98/// ## Data Storage
99///
100/// - **Cortical areas**: Stored in HashMap for O(1) lookup
101/// - **Brain regions**: Stored in BrainRegionHierarchy
102/// - **Neuron data**: Lives in NPU (not stored here)
103/// - **Synapse data**: Lives in NPU (not stored here)
104///
105/// ## Thread Safety
106///
107/// Uses `RwLock` for concurrent reads with exclusive writes.
108/// Multiple threads can read simultaneously, but writes block.
109///
110pub struct ConnectomeManager {
111    /// Map of cortical_id -> CorticalArea metadata
112    cortical_areas: HashMap<CorticalID, CorticalArea>,
113
114    /// Map of cortical_id -> cortical_idx (fast reverse lookup)
115    cortical_id_to_idx: HashMap<CorticalID, u32>,
116
117    /// Map of cortical_idx -> cortical_id (fast reverse lookup)
118    cortical_idx_to_id: HashMap<u32, CorticalID>,
119
120    /// Next available cortical index
121    next_cortical_idx: u32,
122
123    /// Brain region hierarchy
124    brain_regions: BrainRegionHierarchy,
125
126    /// Morphology registry from loaded genome
127    morphology_registry: feagi_evolutionary::MorphologyRegistry,
128
129    /// Configuration
130    config: ConnectomeConfig,
131
132    /// Optional reference to the Rust NPU for neuron/synapse queries
133    ///
134    /// This is set by the Python process manager after NPU initialization.
135    /// All neuron/synapse data queries delegate to the NPU.
136    /// Wrapped in TracingMutex to automatically log all lock acquisitions
137    npu: Option<Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>>,
138
139    /// Plasticity executor reference (optional, only when plasticity feature is enabled)
140    #[cfg(feature = "plasticity")]
141    plasticity_executor:
142        Option<Arc<std::sync::Mutex<feagi_npu_plasticity::AsyncPlasticityExecutor>>>,
143
144    /// Cached neuron count (lock-free read) - updated by burst engine
145    /// This prevents health checks from blocking on NPU lock
146    cached_neuron_count: Arc<AtomicUsize>,
147
148    /// Cached synapse count (lock-free read) - updated by burst engine
149    /// This prevents health checks from blocking on NPU lock
150    cached_synapse_count: Arc<AtomicUsize>,
151
152    /// Per-area neuron count cache (lock-free reads) - updated when neurons are created/deleted
153    /// This prevents health checks from blocking on NPU lock
154    cached_neuron_counts_per_area: Arc<RwLock<HashMap<CorticalID, AtomicUsize>>>,
155
156    /// Per-area synapse count cache (lock-free reads) - updated when synapses are created/deleted
157    /// This prevents health checks from blocking on NPU lock
158    cached_synapse_counts_per_area: Arc<RwLock<HashMap<CorticalID, AtomicUsize>>>,
159
160    /// Is the connectome initialized (has cortical areas)?
161    initialized: bool,
162
163    /// Last fatigue index calculation time (for rate limiting)
164    last_fatigue_calculation: Arc<Mutex<std::time::Instant>>,
165}
166
167/// Type alias for neuron batch data: (x, y, z, threshold, threshold_limit, leak, resting, neuron_type, refractory_period, excitability, consecutive_fire_limit, snooze_period, mp_charge_accumulation)
168type NeuronData = (
169    u32,
170    u32,
171    u32,
172    f32,
173    f32,
174    f32,
175    f32,
176    i32,
177    u16,
178    f32,
179    u16,
180    u16,
181    bool,
182);
183
184impl ConnectomeManager {
185    fn get_mapping_rules_for_destination<'a>(
186        mapping_dst: &'a serde_json::Map<String, serde_json::Value>,
187        dst_area_id: &CorticalID,
188    ) -> Option<&'a Vec<serde_json::Value>> {
189        if let Some(rules) = mapping_dst
190            .get(&dst_area_id.as_base_64())
191            .and_then(|value| value.as_array())
192        {
193            return Some(rules);
194        }
195
196        // Compatibility path: some legacy genomes may still store destination IDs
197        // as 6/8-char ASCII keys instead of base64. Resolve by semantic ID equality.
198        for (raw_dst_key, rules_value) in mapping_dst {
199            let parsed_dst = CorticalID::try_from_base_64(raw_dst_key)
200                .or_else(|_| CorticalID::try_from_legacy_ascii(raw_dst_key));
201            if parsed_dst.as_ref().ok() != Some(dst_area_id) {
202                continue;
203            }
204            if let Some(rules) = rules_value.as_array() {
205                return Some(rules);
206            }
207        }
208
209        None
210    }
211
212    /// Create a new ConnectomeManager (private - use `instance()`)
213    fn new() -> Self {
214        Self {
215            cortical_areas: HashMap::new(),
216            cortical_id_to_idx: HashMap::new(),
217            cortical_idx_to_id: HashMap::new(),
218            // CRITICAL: Reserve indices 0 (_death) and 1 (_power) - start regular areas at 2
219            next_cortical_idx: 3, // Reserve 0=_death, 1=_power, 2=_fatigue
220            brain_regions: BrainRegionHierarchy::new(),
221            morphology_registry: feagi_evolutionary::MorphologyRegistry::new(),
222            config: ConnectomeConfig::default(),
223            npu: None,
224            #[cfg(feature = "plasticity")]
225            plasticity_executor: None,
226            cached_neuron_count: Arc::new(AtomicUsize::new(0)),
227            cached_synapse_count: Arc::new(AtomicUsize::new(0)),
228            cached_neuron_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
229            cached_synapse_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
230            initialized: false,
231            last_fatigue_calculation: Arc::new(Mutex::new(
232                std::time::Instant::now() - std::time::Duration::from_secs(10),
233            )), // Initialize to allow first calculation
234        }
235    }
236
237    /// Get the global singleton instance
238    ///
239    /// # Returns
240    ///
241    /// Arc to the ConnectomeManager wrapped in RwLock
242    ///
243    /// # Example
244    ///
245    /// ```ignore
246    /// use feagi_brain_development::ConnectomeManager;
247    ///
248    /// let manager = ConnectomeManager::instance();
249    /// let read_lock = manager.read();
250    /// let area_count = read_lock.get_cortical_area_count();
251    /// ```
252    ///
253    pub fn instance() -> Arc<RwLock<ConnectomeManager>> {
254        // Note: Singleton is always f32 for backward compatibility
255        // New code should use ConnectomeManager::<T>::new_for_testing_with_npu() for custom types
256        Arc::clone(&*INSTANCE)
257    }
258
259    /// Calculate optimal visualization voxel granularity for a cortical area
260    ///
261    /// This function determines the granularity for aggregated rendering based on:
262    /// - Total voxel count (larger areas get larger chunks)
263    /// - Aspect ratio (handles thin dimensions like 1024×900×3)
264    /// - Target chunk count (~2k-10k chunks for manageable message size)
265    ///
266    /// # Arguments
267    ///
268    /// * `dimensions` - The cortical area dimensions (width, height, depth)
269    ///
270    /// # Returns
271    ///
272    /// Tuple of (chunk_x, chunk_y, chunk_z) that divides evenly into dimensions
273    ///
274    ///
275    /// Create a new isolated instance for testing
276    ///
277    /// This bypasses the singleton pattern and creates a fresh instance.
278    /// Use this in tests to avoid conflicts between parallel test runs.
279    ///
280    /// # Example
281    ///
282    /// ```rust
283    /// let manager = ConnectomeManager::new_for_testing();
284    /// // Use manager in isolated test
285    /// ```
286    pub fn new_for_testing() -> Self {
287        Self {
288            cortical_areas: HashMap::new(),
289            cortical_id_to_idx: HashMap::new(),
290            cortical_idx_to_id: HashMap::new(),
291            next_cortical_idx: 0,
292            brain_regions: BrainRegionHierarchy::new(),
293            morphology_registry: feagi_evolutionary::MorphologyRegistry::new(),
294            config: ConnectomeConfig::default(),
295            npu: None,
296            #[cfg(feature = "plasticity")]
297            plasticity_executor: None,
298            cached_neuron_count: Arc::new(AtomicUsize::new(0)),
299            cached_synapse_count: Arc::new(AtomicUsize::new(0)),
300            cached_neuron_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
301            cached_synapse_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
302            initialized: false,
303            last_fatigue_calculation: Arc::new(Mutex::new(
304                std::time::Instant::now() - std::time::Duration::from_secs(10),
305            )),
306        }
307    }
308
309    /// Create a new isolated instance for testing with NPU
310    ///
311    /// This bypasses the singleton pattern and creates a fresh instance with NPU connected.
312    /// Use this in tests to avoid conflicts between parallel test runs.
313    ///
314    /// # Arguments
315    ///
316    /// * `npu` - Arc<TracingMutex<DynamicNPU>> to connect to this manager
317    ///
318    /// # Example
319    ///
320    /// ```rust
321    /// let npu = Arc::new(TracingMutex::new(RustNPU::new(1_000_000, 10_000_000, 10), "NPU"));
322    /// let manager = ConnectomeManager::new_for_testing_with_npu(npu);
323    /// ```
324    pub fn new_for_testing_with_npu(
325        npu: Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
326    ) -> Self {
327        Self {
328            cortical_areas: HashMap::new(),
329            cortical_id_to_idx: HashMap::new(),
330            cortical_idx_to_id: HashMap::new(),
331            next_cortical_idx: 3,
332            brain_regions: BrainRegionHierarchy::new(),
333            morphology_registry: feagi_evolutionary::MorphologyRegistry::new(),
334            config: ConnectomeConfig::default(),
335            npu: Some(npu),
336            #[cfg(feature = "plasticity")]
337            plasticity_executor: None,
338            cached_neuron_count: Arc::new(AtomicUsize::new(0)),
339            cached_synapse_count: Arc::new(AtomicUsize::new(0)),
340            cached_neuron_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
341            cached_synapse_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
342            initialized: false,
343            last_fatigue_calculation: Arc::new(Mutex::new(
344                std::time::Instant::now() - std::time::Duration::from_secs(10),
345            )),
346        }
347    }
348
349    /// Set up core morphologies in the registry (for testing only)
350    ///
351    /// This is a test helper to set up core morphologies (projector, block_to_block, etc.)
352    /// in the morphology registry so that synaptogenesis tests can run.
353    ///
354    /// # Note
355    ///
356    /// This should only be called in tests. Morphologies are typically loaded from genome files.
357    /// This method is public to allow integration tests to access it.
358    pub fn setup_core_morphologies_for_testing(&mut self) {
359        feagi_evolutionary::add_core_morphologies(&mut self.morphology_registry);
360    }
361
362    /// Reset the singleton (for testing only)
363    ///
364    /// # Safety
365    ///
366    /// This should only be called in tests to reset state between test runs.
367    /// Calling this in production code will cause all references to the old
368    /// instance to become stale.
369    ///
370    #[cfg(test)]
371    pub fn reset_for_testing() {
372        let mut instance = INSTANCE.write();
373        *instance = Self::new();
374    }
375
376    // ======================================================================
377    // Data Hashing (event-driven updates for health_check)
378    // ======================================================================
379
380    /// Update stored hashes for data types that have changed.
381    fn update_state_hashes(
382        &self,
383        brain_regions: Option<u64>,
384        cortical_areas: Option<u64>,
385        brain_geometry: Option<u64>,
386        morphologies: Option<u64>,
387        cortical_mappings: Option<u64>,
388    ) {
389        let state_manager = StateManager::instance();
390        let state_manager = state_manager.read();
391        if let Some(value) = brain_regions {
392            state_manager.set_brain_regions_hash(value);
393        }
394        if let Some(value) = cortical_areas {
395            state_manager.set_cortical_areas_hash(value);
396        }
397        if let Some(value) = brain_geometry {
398            state_manager.set_brain_geometry_hash(value);
399        }
400        if let Some(value) = morphologies {
401            state_manager.set_morphologies_hash(value);
402        }
403        if let Some(value) = cortical_mappings {
404            state_manager.set_cortical_mappings_hash(value);
405        }
406    }
407
408    /// Refresh the brain regions hash (hierarchy, membership, and properties).
409    fn refresh_brain_regions_hash(&self) {
410        let hash = self.compute_brain_regions_hash();
411        self.update_state_hashes(Some(hash), None, None, None, None);
412    }
413
414    #[allow(dead_code)]
415    /// Refresh the cortical areas hash (metadata and properties).
416    fn refresh_cortical_areas_hash(&self) {
417        let hash = self.compute_cortical_areas_hash();
418        self.update_state_hashes(None, Some(hash), None, None, None);
419    }
420
421    #[allow(dead_code)]
422    /// Refresh the brain geometry hash (positions, dimensions, 2D coordinates).
423    fn refresh_brain_geometry_hash(&self) {
424        let hash = self.compute_brain_geometry_hash();
425        self.update_state_hashes(None, None, Some(hash), None, None);
426    }
427
428    /// Refresh the morphologies hash.
429    fn refresh_morphologies_hash(&self) {
430        let hash = self.compute_morphologies_hash();
431        self.update_state_hashes(None, None, None, Some(hash), None);
432    }
433
434    /// Refresh the cortical mappings hash.
435    fn refresh_cortical_mappings_hash(&self) {
436        let hash = self.compute_cortical_mappings_hash();
437        self.update_state_hashes(None, None, None, None, Some(hash));
438    }
439
440    /// Refresh cortical area-related hashes based on the affected data.
441    pub fn refresh_cortical_area_hashes(&self, properties_changed: bool, geometry_changed: bool) {
442        let cortical_hash = if properties_changed {
443            Some(self.compute_cortical_areas_hash())
444        } else {
445            None
446        };
447        let geometry_hash = if geometry_changed {
448            Some(self.compute_brain_geometry_hash())
449        } else {
450            None
451        };
452        self.update_state_hashes(None, cortical_hash, geometry_hash, None, None);
453    }
454
455    /// Compute hash for brain regions (hierarchy, membership, and properties).
456    fn compute_brain_regions_hash(&self) -> u64 {
457        let mut hasher = Xxh64::new(DATA_HASH_SEED);
458        let mut region_ids: Vec<String> = self
459            .brain_regions
460            .get_all_region_ids()
461            .into_iter()
462            .cloned()
463            .collect();
464        region_ids.sort();
465
466        for region_id in region_ids {
467            let Some(region) = self.brain_regions.get_region(&region_id) else {
468                continue;
469            };
470            Self::hash_str(&mut hasher, &region_id);
471            Self::hash_str(&mut hasher, &region.name);
472            Self::hash_str(&mut hasher, &region.region_type.to_string());
473            let parent_id = self.brain_regions.get_parent(&region_id);
474            match parent_id {
475                Some(parent) => Self::hash_str(&mut hasher, parent),
476                None => Self::hash_str(&mut hasher, "null"),
477            }
478
479            let mut cortical_ids: Vec<String> = region
480                .cortical_areas
481                .iter()
482                .map(|id| id.as_base_64())
483                .collect();
484            cortical_ids.sort();
485            for cortical_id in cortical_ids {
486                Self::hash_str(&mut hasher, &cortical_id);
487            }
488
489            Self::hash_properties_filtered(&mut hasher, &region.properties, &[]);
490        }
491
492        hasher.finish() & HASH_SAFE_MASK
493    }
494
495    /// Compute hash for cortical areas and properties (excluding mappings).
496    fn compute_cortical_areas_hash(&self) -> u64 {
497        let mut hasher = Xxh64::new(DATA_HASH_SEED);
498        let mut areas: Vec<&CorticalArea> = self.cortical_areas.values().collect();
499        areas.sort_by_key(|area| area.cortical_id.as_base_64());
500
501        for area in areas {
502            let cortical_id = area.cortical_id.as_base_64();
503            Self::hash_str(&mut hasher, &cortical_id);
504            hasher.write_u32(area.cortical_idx);
505            Self::hash_str(&mut hasher, &area.name);
506            Self::hash_str(&mut hasher, &area.cortical_type.to_string());
507
508            let excluded = ["cortical_mapping_dst", "upstream_cortical_areas"];
509            Self::hash_properties_filtered(&mut hasher, &area.properties, &excluded);
510        }
511
512        hasher.finish() & HASH_SAFE_MASK
513    }
514
515    /// Compute hash for brain geometry (area positions, dimensions, and 2D coordinates).
516    fn compute_brain_geometry_hash(&self) -> u64 {
517        let mut hasher = Xxh64::new(DATA_HASH_SEED);
518        let mut areas: Vec<&CorticalArea> = self.cortical_areas.values().collect();
519        areas.sort_by_key(|area| area.cortical_id.as_base_64());
520
521        for area in areas {
522            let cortical_id = area.cortical_id.as_base_64();
523            Self::hash_str(&mut hasher, &cortical_id);
524
525            Self::hash_i32(&mut hasher, area.position.x);
526            Self::hash_i32(&mut hasher, area.position.y);
527            Self::hash_i32(&mut hasher, area.position.z);
528
529            Self::hash_u32(&mut hasher, area.dimensions.width);
530            Self::hash_u32(&mut hasher, area.dimensions.height);
531            Self::hash_u32(&mut hasher, area.dimensions.depth);
532
533            let coord_2d = area
534                .properties
535                .get("coordinate_2d")
536                .or_else(|| area.properties.get("coordinates_2d"));
537            match coord_2d {
538                Some(value) => Self::hash_json_value(&mut hasher, value),
539                None => Self::hash_str(&mut hasher, "null"),
540            }
541        }
542
543        hasher.finish() & HASH_SAFE_MASK
544    }
545
546    /// Compute hash for morphologies.
547    fn compute_morphologies_hash(&self) -> u64 {
548        let mut hasher = Xxh64::new(DATA_HASH_SEED);
549        let mut morphology_ids = self.morphology_registry.morphology_ids();
550        morphology_ids.sort();
551
552        for morphology_id in morphology_ids {
553            if let Some(morphology) = self.morphology_registry.get(&morphology_id) {
554                Self::hash_str(&mut hasher, &morphology_id);
555                Self::hash_str(&mut hasher, &format!("{:?}", morphology.morphology_type));
556                Self::hash_str(&mut hasher, &morphology.class);
557                if let Ok(value) = serde_json::to_value(&morphology.parameters) {
558                    Self::hash_json_value(&mut hasher, &value);
559                }
560            }
561        }
562
563        hasher.finish() & HASH_SAFE_MASK
564    }
565
566    /// Compute hash for cortical mappings (cortical_mapping_dst).
567    fn compute_cortical_mappings_hash(&self) -> u64 {
568        let mut hasher = Xxh64::new(DATA_HASH_SEED);
569        let mut areas: Vec<&CorticalArea> = self.cortical_areas.values().collect();
570        areas.sort_by_key(|area| area.cortical_id.as_base_64());
571
572        for area in areas {
573            let cortical_id = area.cortical_id.as_base_64();
574            Self::hash_str(&mut hasher, &cortical_id);
575            if let Some(serde_json::Value::Object(map)) =
576                area.properties.get("cortical_mapping_dst")
577            {
578                let mut dest_ids: Vec<&String> = map.keys().collect();
579                dest_ids.sort();
580                for dest_id in dest_ids {
581                    Self::hash_str(&mut hasher, dest_id);
582                    if let Some(value) = map.get(dest_id) {
583                        Self::hash_json_value(&mut hasher, value);
584                    }
585                }
586            } else {
587                Self::hash_str(&mut hasher, "null");
588            }
589        }
590
591        hasher.finish() & HASH_SAFE_MASK
592    }
593
594    /// Hash a string with a separator to avoid concatenation collisions.
595    fn hash_str(hasher: &mut Xxh64, value: &str) {
596        hasher.write(value.as_bytes());
597        hasher.write_u8(0);
598    }
599
600    /// Hash a signed 32-bit integer deterministically.
601    fn hash_i32(hasher: &mut Xxh64, value: i32) {
602        hasher.write(&value.to_le_bytes());
603    }
604
605    /// Hash an unsigned 32-bit integer deterministically.
606    fn hash_u32(hasher: &mut Xxh64, value: u32) {
607        hasher.write(&value.to_le_bytes());
608    }
609
610    /// Hash JSON values deterministically with sorted object keys.
611    fn hash_json_value(hasher: &mut Xxh64, value: &serde_json::Value) {
612        match value {
613            serde_json::Value::Null => {
614                hasher.write_u8(0);
615            }
616            serde_json::Value::Bool(val) => {
617                hasher.write_u8(1);
618                hasher.write_u8(*val as u8);
619            }
620            serde_json::Value::Number(num) => {
621                hasher.write_u8(2);
622                Self::hash_str(hasher, &num.to_string());
623            }
624            serde_json::Value::String(val) => {
625                hasher.write_u8(3);
626                Self::hash_str(hasher, val);
627            }
628            serde_json::Value::Array(items) => {
629                hasher.write_u8(4);
630                for item in items {
631                    Self::hash_json_value(hasher, item);
632                }
633            }
634            serde_json::Value::Object(map) => {
635                hasher.write_u8(5);
636                let mut keys: Vec<&String> = map.keys().collect();
637                keys.sort();
638                for key in keys {
639                    Self::hash_str(hasher, key);
640                    if let Some(val) = map.get(key) {
641                        Self::hash_json_value(hasher, val);
642                    }
643                }
644            }
645        }
646    }
647
648    /// Hash JSON properties deterministically, excluding specific keys.
649    fn hash_properties_filtered(
650        hasher: &mut Xxh64,
651        properties: &HashMap<String, serde_json::Value>,
652        excluded_keys: &[&str],
653    ) {
654        let mut keys: Vec<&String> = properties.keys().collect();
655        keys.sort();
656        for key in keys {
657            if excluded_keys.contains(&key.as_str()) {
658                continue;
659            }
660            Self::hash_str(hasher, key);
661            if let Some(value) = properties.get(key) {
662                Self::hash_json_value(hasher, value);
663            }
664        }
665    }
666
667    // ======================================================================
668    // Cortical Area Management
669    // ======================================================================
670
671    /// Add a new cortical area
672    ///
673    /// # Arguments
674    ///
675    /// * `area` - The cortical area to add
676    ///
677    /// # Returns
678    ///
679    /// The assigned cortical index
680    ///
681    /// # Errors
682    ///
683    /// Returns error if:
684    /// - An area with the same cortical_id already exists
685    /// - The area's cortical_idx conflicts with an existing area
686    ///
687    pub fn add_cortical_area(&mut self, mut area: CorticalArea) -> BduResult<u32> {
688        // Check if area already exists
689        if self.cortical_areas.contains_key(&area.cortical_id) {
690            return Err(BduError::InvalidArea(format!(
691                "Cortical area {} already exists",
692                area.cortical_id
693            )));
694        }
695
696        // CRITICAL: Reserve cortical_idx 0 for _death, 1 for _power, 2 for _fatigue
697        // Use feagi-data-processing types as single source of truth
698        use feagi_structures::genomic::cortical_area::CoreCorticalType;
699
700        let death_id = CoreCorticalType::Death.to_cortical_id();
701        let power_id = CoreCorticalType::Power.to_cortical_id();
702        let fatigue_id = CoreCorticalType::Fatigue.to_cortical_id();
703
704        let is_death_area = area.cortical_id == death_id;
705        let is_power_area = area.cortical_id == power_id;
706        let is_fatigue_area = area.cortical_id == fatigue_id;
707
708        if is_death_area {
709            trace!(
710                target: "feagi-bdu",
711                "[CORE-AREA] Assigning RESERVED cortical_idx=0 to _death area (id={})",
712                area.cortical_id
713            );
714            area.cortical_idx = 0;
715        } else if is_power_area {
716            trace!(
717                target: "feagi-bdu",
718                "[CORE-AREA] Assigning RESERVED cortical_idx=1 to _power area (id={})",
719                area.cortical_id
720            );
721            area.cortical_idx = 1;
722        } else if is_fatigue_area {
723            trace!(
724                target: "feagi-bdu",
725                "[CORE-AREA] Assigning RESERVED cortical_idx=2 to _fatigue area (id={})",
726                area.cortical_id
727            );
728            area.cortical_idx = 2;
729        } else {
730            // Regular areas: assign cortical_idx if not set (will be ≥3 due to next_cortical_idx=3 initialization)
731            if area.cortical_idx == 0 {
732                area.cortical_idx = self.next_cortical_idx;
733                self.next_cortical_idx += 1;
734                trace!(
735                    target: "feagi-bdu",
736                    "[REGULAR-AREA] Assigned cortical_idx={} to area '{}' (should be ≥3)",
737                    area.cortical_idx,
738                    area.cortical_id.as_base_64()
739                );
740            } else {
741                // Check for reserved index collision
742                if area.cortical_idx == 0 || area.cortical_idx == 1 || area.cortical_idx == 2 {
743                    warn!(
744                        "Regular area '{}' attempted to use RESERVED cortical_idx={}! Reassigning to next available.",
745                        area.cortical_id, area.cortical_idx);
746                    area.cortical_idx = self.next_cortical_idx;
747                    self.next_cortical_idx += 1;
748                    info!(
749                        "   Reassigned '{}' to cortical_idx={}",
750                        area.cortical_id, area.cortical_idx
751                    );
752                } else if self.cortical_idx_to_id.contains_key(&area.cortical_idx) {
753                    return Err(BduError::InvalidArea(format!(
754                        "Cortical index {} is already in use",
755                        area.cortical_idx
756                    )));
757                }
758
759                // Update next_cortical_idx if needed
760                if area.cortical_idx >= self.next_cortical_idx {
761                    self.next_cortical_idx = area.cortical_idx + 1;
762                }
763            }
764        }
765
766        let cortical_id = area.cortical_id;
767        let cortical_idx = area.cortical_idx;
768
769        // Update lookup maps
770        self.cortical_id_to_idx.insert(cortical_id, cortical_idx);
771        self.cortical_idx_to_id.insert(cortical_idx, cortical_id);
772
773        // Initialize upstream_cortical_areas property (empty array for O(1) lookup)
774        area.properties
775            .insert("upstream_cortical_areas".to_string(), serde_json::json!([]));
776
777        // Default visualization voxel granularity is 1x1x1 (assumed, not stored)
778        // User overrides are stored in properties["visualization_voxel_granularity"] only if != 1x1x1
779
780        // If the caller provided a parent brain region ID, persist the association in the
781        // BrainRegionHierarchy membership set (this drives /v1/region/regions_members).
782        //
783        // IMPORTANT: This is separate from storing "parent_region_id" in the cortical area's
784        // properties. BV may show that property even if the hierarchy isn't updated.
785        let parent_region_id = area
786            .properties
787            .get("parent_region_id")
788            .and_then(|v| v.as_str())
789            .map(|s| s.to_string());
790
791        // Store area
792        self.cortical_areas.insert(cortical_id, area);
793
794        // Update region membership (source of truth for region->areas listing)
795        if let Some(region_id) = parent_region_id {
796            let region = self
797                .brain_regions
798                .get_region_mut(&region_id)
799                .ok_or_else(|| {
800                    BduError::InvalidArea(format!(
801                        "Unknown parent_region_id '{}' for cortical area {}",
802                        region_id,
803                        cortical_id.as_base_64()
804                    ))
805                })?;
806            region.add_area(cortical_id);
807        }
808
809        // CRITICAL: Initialize per-area count caches to 0 (lock-free for readers)
810        // This allows healthcheck endpoints to read counts without NPU lock
811        {
812            let mut neuron_cache = self.cached_neuron_counts_per_area.write();
813            neuron_cache.insert(cortical_id, AtomicUsize::new(0));
814            let mut synapse_cache = self.cached_synapse_counts_per_area.write();
815            synapse_cache.insert(cortical_id, AtomicUsize::new(0));
816        }
817        // @cursor:critical-path - BV pulls per-area stats from StateManager without NPU lock.
818        let state_manager = StateManager::instance();
819        let state_manager = state_manager.read();
820        state_manager.init_cortical_area_stats(&cortical_id.as_base_64());
821
822        // CRITICAL: Register cortical area in NPU during corticogenesis
823        // This must happen BEFORE neurogenesis so neurons can look up their cortical IDs
824        // Use base64 format for proper CorticalID conversion
825        if let Some(ref npu) = self.npu {
826            trace!(target: "feagi-bdu", "[LOCK-TRACE] add_cortical_area: attempting NPU lock for registration");
827            if let Ok(mut npu_lock) = npu.lock() {
828                trace!(target: "feagi-bdu", "[LOCK-TRACE] add_cortical_area: acquired NPU lock for registration");
829                npu_lock.register_cortical_area(cortical_idx, cortical_id.as_base_64());
830                trace!(
831                    target: "feagi-bdu",
832                    "Registered cortical area idx={} -> '{}' in NPU",
833                    cortical_idx,
834                    cortical_id.as_base_64()
835                );
836            }
837        }
838
839        // Synchronize cortical area flags with NPU (psp_uniform_distribution, mp_driven_psp, etc.)
840        self.sync_cortical_area_flags_to_npu()?;
841
842        self.initialized = true;
843
844        self.refresh_cortical_area_hashes(true, true);
845        self.refresh_brain_regions_hash();
846
847        Ok(cortical_idx)
848    }
849
850    /// Remove a cortical area by ID
851    ///
852    /// # Arguments
853    ///
854    /// * `cortical_id` - ID of the cortical area to remove
855    ///
856    /// # Returns
857    ///
858    /// `Ok(())` if removed, error if area doesn't exist
859    ///
860    /// # Note
861    ///
862    /// This does NOT remove neurons from the NPU - that must be done separately.
863    ///
864    pub fn remove_cortical_area(&mut self, cortical_id: &CorticalID) -> BduResult<()> {
865        let area = self.cortical_areas.remove(cortical_id).ok_or_else(|| {
866            BduError::InvalidArea(format!("Cortical area {} does not exist", cortical_id))
867        })?;
868
869        // Remove from lookup maps
870        self.cortical_id_to_idx.remove(cortical_id);
871        self.cortical_idx_to_id.remove(&area.cortical_idx);
872
873        self.refresh_cortical_area_hashes(true, true);
874        Ok(())
875    }
876
877    /// Update a cortical area ID without changing its cortical_idx.
878    ///
879    /// This remaps internal lookup tables, brain-region membership, and mapping keys.
880    pub fn rename_cortical_area_id(
881        &mut self,
882        old_id: &CorticalID,
883        new_id: CorticalID,
884        new_cortical_type: CorticalAreaType,
885    ) -> BduResult<()> {
886        self.rename_cortical_area_id_with_options(old_id, new_id, new_cortical_type, true)
887    }
888
889    /// Update a cortical area ID without changing its cortical_idx, with optional NPU registry update.
890    pub fn rename_cortical_area_id_with_options(
891        &mut self,
892        old_id: &CorticalID,
893        new_id: CorticalID,
894        new_cortical_type: CorticalAreaType,
895        update_npu_registry: bool,
896    ) -> BduResult<()> {
897        if !self.cortical_areas.contains_key(old_id) {
898            return Err(BduError::InvalidArea(format!(
899                "Cortical area {} does not exist",
900                old_id
901            )));
902        }
903        if self.cortical_areas.contains_key(&new_id) {
904            return Err(BduError::InvalidArea(format!(
905                "Cortical area {} already exists",
906                new_id
907            )));
908        }
909
910        let mut area = self.cortical_areas.remove(old_id).ok_or_else(|| {
911            BduError::InvalidArea(format!("Cortical area {} does not exist", old_id))
912        })?;
913        let cortical_idx = area.cortical_idx;
914        area.cortical_id = new_id;
915        area.cortical_type = new_cortical_type;
916
917        self.cortical_areas.insert(new_id, area);
918        self.cortical_id_to_idx.remove(old_id);
919        self.cortical_id_to_idx.insert(new_id, cortical_idx);
920        self.cortical_idx_to_id.insert(cortical_idx, new_id);
921
922        // Update per-area count caches
923        {
924            let mut neuron_cache = self.cached_neuron_counts_per_area.write();
925            if let Some(value) = neuron_cache.remove(old_id) {
926                neuron_cache.insert(new_id, value);
927            }
928            let mut synapse_cache = self.cached_synapse_counts_per_area.write();
929            if let Some(value) = synapse_cache.remove(old_id) {
930                synapse_cache.insert(new_id, value);
931            }
932        }
933
934        // Update brain-region membership
935        self.brain_regions.rename_cortical_area_id(old_id, new_id);
936
937        // Update cortical mapping properties referencing the old ID
938        let old_id_str = old_id.as_base_64();
939        let new_id_str = new_id.as_base_64();
940        for area in self.cortical_areas.values_mut() {
941            if let Some(mapping) = area
942                .properties
943                .get_mut("cortical_mapping_dst")
944                .and_then(|v| v.as_object_mut())
945            {
946                if let Some(value) = mapping.remove(&old_id_str) {
947                    mapping.insert(new_id_str.clone(), value);
948                }
949            }
950        }
951
952        // Update NPU cortical_id registry if requested
953        if update_npu_registry {
954            if let Some(ref npu) = self.npu {
955                if let Ok(mut npu_lock) = npu.lock() {
956                    npu_lock.register_cortical_area(cortical_idx, new_id.as_base_64());
957                }
958            }
959        }
960
961        self.refresh_cortical_area_hashes(true, true);
962        self.refresh_brain_regions_hash();
963        self.refresh_cortical_mappings_hash();
964
965        Ok(())
966    }
967
968    /// Get a cortical area by ID
969    pub fn get_cortical_area(&self, cortical_id: &CorticalID) -> Option<&CorticalArea> {
970        self.cortical_areas.get(cortical_id)
971    }
972
973    /// Get a mutable reference to a cortical area
974    pub fn get_cortical_area_mut(&mut self, cortical_id: &CorticalID) -> Option<&mut CorticalArea> {
975        self.cortical_areas.get_mut(cortical_id)
976    }
977
978    /// Get cortical index by ID
979    pub fn get_cortical_idx(&self, cortical_id: &CorticalID) -> Option<u32> {
980        self.cortical_id_to_idx.get(cortical_id).copied()
981    }
982
983    /// Find which brain region contains a cortical area
984    ///
985    /// This is used to populate `parent_region_id` in API responses for Brain Visualizer.
986    /// Delegates to BrainRegionHierarchy for the actual search.
987    ///
988    /// # Arguments
989    /// * `cortical_id` - Cortical area to search for
990    ///
991    /// # Returns
992    /// * `Option<String>` - Parent region ID (UUID string) if found
993    ///
994    pub fn get_parent_region_id_for_area(&self, cortical_id: &CorticalID) -> Option<String> {
995        self.brain_regions.find_region_containing_area(cortical_id)
996    }
997
998    /// Recompute brain-region `inputs`/`outputs` registries from current cortical mappings.
999    ///
1000    /// This drives `/v1/region/regions_members` (via `BrainRegion.properties["inputs"/"outputs"]`).
1001    ///
1002    /// Semantics (matches Python `_auto_assign_region_io()` and BV expectations):
1003    /// - **outputs**: Any cortical area *in the region* that connects to an area outside the region
1004    /// - **inputs**: Any cortical area *in the region* that receives a connection from outside the region
1005    ///
1006    /// This function updates the hierarchy in-place and returns the computed base64 ID lists
1007    /// for downstream persistence into `RuntimeGenome`.
1008    pub fn recompute_brain_region_io_registry(&mut self) -> BduResult<BrainRegionIoRegistry> {
1009        use std::collections::HashSet;
1010
1011        let region_ids: Vec<String> = self
1012            .brain_regions
1013            .get_all_region_ids()
1014            .into_iter()
1015            .cloned()
1016            .collect();
1017
1018        let mut inputs_by_region: HashMap<String, HashSet<String>> = HashMap::new();
1019        let mut outputs_by_region: HashMap<String, HashSet<String>> = HashMap::new();
1020
1021        // Initialize so regions with no IO still get cleared deterministically.
1022        for rid in &region_ids {
1023            inputs_by_region.insert(rid.clone(), HashSet::new());
1024            outputs_by_region.insert(rid.clone(), HashSet::new());
1025        }
1026
1027        for (src_id, src_area) in &self.cortical_areas {
1028            let Some(dstmap) = src_area
1029                .properties
1030                .get("cortical_mapping_dst")
1031                .and_then(|v| v.as_object())
1032            else {
1033                continue;
1034            };
1035
1036            let Some(src_region_id) = self.brain_regions.find_region_containing_area(src_id) else {
1037                warn!(
1038                    target: "feagi-bdu",
1039                    "Skipping region IO for source area {} (not in any region)",
1040                    src_id.as_base_64()
1041                );
1042                continue;
1043            };
1044
1045            for dst_id_str in dstmap.keys() {
1046                let dst_id = CorticalID::try_from_base_64(dst_id_str).map_err(|e| {
1047                    BduError::InvalidArea(format!(
1048                        "Unable to recompute region IO: invalid destination cortical id '{}' in cortical_mapping_dst for {}: {}",
1049                        dst_id_str,
1050                        src_id.as_base_64(),
1051                        e
1052                    ))
1053                })?;
1054
1055                let Some(dst_region_id) = self.brain_regions.find_region_containing_area(&dst_id)
1056                else {
1057                    warn!(
1058                        target: "feagi-bdu",
1059                        "Skipping region IO for destination area {} (not in any region)",
1060                        dst_id.as_base_64()
1061                    );
1062                    continue;
1063                };
1064
1065                if src_region_id == dst_region_id {
1066                    continue;
1067                }
1068
1069                outputs_by_region
1070                    .entry(src_region_id.clone())
1071                    .or_default()
1072                    .insert(src_id.as_base_64());
1073                inputs_by_region
1074                    .entry(dst_region_id.clone())
1075                    .or_default()
1076                    .insert(dst_id.as_base_64());
1077            }
1078        }
1079
1080        let mut computed: HashMap<String, (Vec<String>, Vec<String>)> = HashMap::new();
1081        for rid in region_ids {
1082            let mut inputs: Vec<String> = inputs_by_region
1083                .remove(&rid)
1084                .unwrap_or_default()
1085                .into_iter()
1086                .collect();
1087            let mut outputs: Vec<String> = outputs_by_region
1088                .remove(&rid)
1089                .unwrap_or_default()
1090                .into_iter()
1091                .collect();
1092
1093            inputs.sort();
1094            outputs.sort();
1095
1096            let region = self.brain_regions.get_region_mut(&rid).ok_or_else(|| {
1097                BduError::InvalidArea(format!(
1098                    "Unable to recompute region IO: region '{}' not found in hierarchy",
1099                    rid
1100                ))
1101            })?;
1102
1103            if inputs.is_empty() {
1104                region.properties.remove("inputs");
1105            } else {
1106                region
1107                    .properties
1108                    .insert("inputs".to_string(), serde_json::json!(inputs.clone()));
1109            }
1110
1111            if outputs.is_empty() {
1112                region.properties.remove("outputs");
1113            } else {
1114                region
1115                    .properties
1116                    .insert("outputs".to_string(), serde_json::json!(outputs.clone()));
1117            }
1118
1119            computed.insert(rid, (inputs, outputs));
1120        }
1121
1122        self.refresh_brain_regions_hash();
1123
1124        Ok(computed)
1125    }
1126
1127    /// Get the root brain region ID (region with no parent)
1128    ///
1129    /// # Returns
1130    /// * `Option<String>` - Root region ID (UUID string) if found
1131    ///
1132    pub fn get_root_region_id(&self) -> Option<String> {
1133        self.brain_regions.get_root_region_id()
1134    }
1135
1136    /// Get cortical ID by index
1137    pub fn get_cortical_id(&self, cortical_idx: u32) -> Option<&CorticalID> {
1138        self.cortical_idx_to_id.get(&cortical_idx)
1139    }
1140
1141    /// Get all cortical_idx -> cortical_id mappings (for burst loop caching)
1142    /// Returns a HashMap of cortical_idx -> cortical_id (base64 string)
1143    pub fn get_all_cortical_idx_to_id_mappings(&self) -> ahash::AHashMap<u32, String> {
1144        self.cortical_idx_to_id
1145            .iter()
1146            .map(|(idx, id)| (*idx, id.as_base_64()))
1147            .collect()
1148    }
1149
1150    /// Get all cortical_idx -> visualization_voxel_granularity mappings
1151    ///
1152    /// Returns a map of cortical_idx to (granularity_x, granularity_y, granularity_z) for areas that have
1153    /// visualization voxel granularity configured.
1154    pub fn get_all_visualization_granularities(&self) -> ahash::AHashMap<u32, (u32, u32, u32)> {
1155        let mut granularities = ahash::AHashMap::new();
1156        for (cortical_id, area) in &self.cortical_areas {
1157            let cortical_idx = self
1158                .cortical_id_to_idx
1159                .get(cortical_id)
1160                .copied()
1161                .unwrap_or(0);
1162
1163            // Extract visualization granularity overrides from properties.
1164            // Default is 1x1x1 (assumed, not stored) so we only include non-default overrides.
1165            if let Some(granularity_json) = area.properties.get("visualization_voxel_granularity") {
1166                if let Some(arr) = granularity_json.as_array() {
1167                    if arr.len() == 3 {
1168                        let x_opt = arr[0]
1169                            .as_u64()
1170                            .or_else(|| arr[0].as_f64().map(|f| f as u64));
1171                        let y_opt = arr[1]
1172                            .as_u64()
1173                            .or_else(|| arr[1].as_f64().map(|f| f as u64));
1174                        let z_opt = arr[2]
1175                            .as_u64()
1176                            .or_else(|| arr[2].as_f64().map(|f| f as u64));
1177
1178                        if let (Some(x), Some(y), Some(z)) = (x_opt, y_opt, z_opt) {
1179                            let granularity = (x as u32, y as u32, z as u32);
1180                            // Only include overrides (non-default)
1181                            if granularity != (1, 1, 1) {
1182                                granularities.insert(cortical_idx, granularity);
1183                            }
1184                        }
1185                    }
1186                }
1187            }
1188        }
1189        granularities
1190    }
1191
1192    /// Get all cortical area IDs
1193    pub fn get_cortical_area_ids(&self) -> Vec<&CorticalID> {
1194        self.cortical_areas.keys().collect()
1195    }
1196
1197    /// Get the number of cortical areas
1198    pub fn get_cortical_area_count(&self) -> usize {
1199        self.cortical_areas.len()
1200    }
1201
1202    /// Get all cortical areas that have synapses targeting the specified area (upstream/afferent areas)
1203    ///
1204    /// Reads from the `upstream_cortical_areas` property stored on the cortical area.
1205    /// This property is maintained by `add_upstream_area()` and `remove_upstream_area()`.
1206    ///
1207    /// # Arguments
1208    ///
1209    /// * `target_cortical_id` - The cortical area ID to find upstream connections for
1210    ///
1211    /// # Returns
1212    ///
1213    /// Vec of cortical_idx values for all upstream areas
1214    ///
1215    pub fn get_upstream_cortical_areas(&self, target_cortical_id: &CorticalID) -> Vec<u32> {
1216        if let Some(area) = self.cortical_areas.get(target_cortical_id) {
1217            if let Some(upstream_prop) = area.properties.get("upstream_cortical_areas") {
1218                if let Some(upstream_array) = upstream_prop.as_array() {
1219                    return upstream_array
1220                        .iter()
1221                        .filter_map(|v| v.as_u64().map(|n| n as u32))
1222                        .collect();
1223                }
1224            }
1225
1226            // Property missing - data integrity issue
1227            warn!(target: "feagi-bdu",
1228                "Cortical area '{}' missing 'upstream_cortical_areas' property - treating as empty",
1229                target_cortical_id.as_base_64()
1230            );
1231        }
1232
1233        Vec::new()
1234    }
1235
1236    /// Filter upstream cortical indices to exclude memory areas.
1237    pub fn filter_non_memory_upstream_areas(&self, upstream: &[u32]) -> Vec<u32> {
1238        upstream
1239            .iter()
1240            .filter_map(|idx| {
1241                let cortical_id = self.cortical_idx_to_id.get(idx)?;
1242                let area = self.cortical_areas.get(cortical_id)?;
1243                if matches!(area.cortical_type, CorticalAreaType::Memory(_)) {
1244                    None
1245                } else {
1246                    Some(*idx)
1247                }
1248            })
1249            .collect()
1250    }
1251
1252    /// Recompute and persist upstream_cortical_areas for a target area from mapping properties.
1253    ///
1254    /// This is a recovery path for stale upstream tracking (e.g., bidirectional mappings
1255    /// that were mirrored into properties but not fully regenerated).
1256    pub fn refresh_upstream_cortical_areas_from_mappings(
1257        &mut self,
1258        target_cortical_id: &CorticalID,
1259    ) -> Vec<u32> {
1260        use std::collections::HashSet;
1261        let target_id_str = target_cortical_id.as_base_64();
1262        let mut upstream_idxs = HashSet::new();
1263        for (src_id, src_area) in &self.cortical_areas {
1264            if src_id == target_cortical_id {
1265                continue;
1266            }
1267            if let Some(mapping) = src_area
1268                .properties
1269                .get("cortical_mapping_dst")
1270                .and_then(|v| v.as_object())
1271            {
1272                if mapping.contains_key(&target_id_str) {
1273                    upstream_idxs.insert(src_area.cortical_idx);
1274                }
1275            }
1276        }
1277
1278        let mut upstream_list: Vec<u32> = upstream_idxs.into_iter().collect();
1279        upstream_list.sort_unstable();
1280
1281        if let Some(target_area) = self.cortical_areas.get_mut(target_cortical_id) {
1282            target_area.properties.insert(
1283                "upstream_cortical_areas".to_string(),
1284                serde_json::json!(upstream_list),
1285            );
1286        }
1287
1288        self.get_upstream_cortical_areas(target_cortical_id)
1289    }
1290
1291    /// Add an upstream cortical area to a target area's upstream list
1292    ///
1293    /// This should be called when synapses are created from src_cortical_idx to target_cortical_id.
1294    ///
1295    /// # Arguments
1296    ///
1297    /// * `target_cortical_id` - The cortical area receiving connections
1298    /// * `src_cortical_idx` - The cortical index of the source area
1299    ///
1300    pub fn add_upstream_area(&mut self, target_cortical_id: &CorticalID, src_cortical_idx: u32) {
1301        if let Some(area) = self.cortical_areas.get_mut(target_cortical_id) {
1302            let upstream_array = area
1303                .properties
1304                .entry("upstream_cortical_areas".to_string())
1305                .or_insert_with(|| serde_json::json!([]));
1306
1307            if let Some(arr) = upstream_array.as_array_mut() {
1308                let src_value = serde_json::json!(src_cortical_idx);
1309                if !arr.contains(&src_value) {
1310                    arr.push(src_value);
1311                    info!(target: "feagi-bdu",
1312                        "✓ Added upstream area idx={} to cortical area '{}'",
1313                        src_cortical_idx, target_cortical_id.as_base_64()
1314                    );
1315                }
1316            }
1317        }
1318    }
1319
1320    /// Get the memory twin cortical ID for a given memory area and upstream area.
1321    pub fn get_memory_twin_for_upstream_idx(
1322        &self,
1323        memory_area_idx: u32,
1324        upstream_idx: u32,
1325    ) -> Option<CorticalID> {
1326        let memory_id = self.cortical_idx_to_id.get(&memory_area_idx)?;
1327        let upstream_id = self.cortical_idx_to_id.get(&upstream_idx)?;
1328        let area = self.cortical_areas.get(memory_id)?;
1329        let mapping = area
1330            .properties
1331            .get("memory_twin_areas")
1332            .and_then(|v| v.as_object())?;
1333        let twin_b64 = mapping.get(&upstream_id.as_base_64())?.as_str()?;
1334        CorticalID::try_from_base_64(twin_b64).ok()
1335    }
1336
1337    /// Ensure a memory twin area exists for the given upstream and memory areas.
1338    pub fn ensure_memory_twin_area(
1339        &mut self,
1340        memory_area_id: &CorticalID,
1341        upstream_area_id: &CorticalID,
1342    ) -> BduResult<CorticalID> {
1343        use crate::models::CorticalAreaExt;
1344
1345        let register_replay_mapping = |manager: &mut ConnectomeManager,
1346                                       twin_id: &CorticalID|
1347         -> BduResult<()> {
1348            let Some(npu) = manager.npu.as_ref() else {
1349                return Ok(());
1350            };
1351            let memory_area_idx =
1352                *manager
1353                    .cortical_id_to_idx
1354                    .get(memory_area_id)
1355                    .ok_or_else(|| {
1356                        BduError::InvalidArea(format!(
1357                            "Memory area idx missing for {}",
1358                            memory_area_id.as_base_64()
1359                        ))
1360                    })?;
1361            let upstream_area_idx = *manager
1362                .cortical_id_to_idx
1363                .get(upstream_area_id)
1364                .ok_or_else(|| {
1365                    BduError::InvalidArea(format!(
1366                        "Upstream area idx missing for {}",
1367                        upstream_area_id.as_base_64()
1368                    ))
1369                })?;
1370            let twin_area_idx = *manager.cortical_id_to_idx.get(twin_id).ok_or_else(|| {
1371                BduError::InvalidArea(format!(
1372                    "Twin area idx missing for {}",
1373                    twin_id.as_base_64()
1374                ))
1375            })?;
1376            let twin_area = manager.cortical_areas.get(twin_id).ok_or_else(|| {
1377                BduError::InvalidArea(format!("Twin area {} not found", twin_id.as_base_64()))
1378            })?;
1379            let potential = twin_area.firing_threshold() + twin_area.firing_threshold_increment();
1380            if let Ok(mut npu_lock) = npu.lock() {
1381                npu_lock.register_memory_twin_mapping(
1382                    memory_area_idx,
1383                    upstream_area_idx,
1384                    twin_area_idx,
1385                    potential,
1386                );
1387            }
1388            Ok(())
1389        };
1390
1391        let memory_area = self.cortical_areas.get(memory_area_id).ok_or_else(|| {
1392            BduError::InvalidArea(format!(
1393                "Memory area {} not found",
1394                memory_area_id.as_base_64()
1395            ))
1396        })?;
1397        let upstream_area = self.cortical_areas.get(upstream_area_id).ok_or_else(|| {
1398            BduError::InvalidArea(format!(
1399                "Upstream area {} not found",
1400                upstream_area_id.as_base_64()
1401            ))
1402        })?;
1403
1404        if matches!(upstream_area.cortical_type, CorticalAreaType::Memory(_)) {
1405            return Err(BduError::InvalidArea(format!(
1406                "Upstream area {} is memory type; twin creation is only for non-memory areas",
1407                upstream_area_id.as_base_64()
1408            )));
1409        }
1410
1411        if let Some(existing) = memory_area
1412            .properties
1413            .get("memory_twin_areas")
1414            .and_then(|v| v.as_object())
1415            .and_then(|map| map.get(&upstream_area_id.as_base_64()))
1416            .and_then(|v| v.as_str())
1417            .and_then(|s| CorticalID::try_from_base_64(s).ok())
1418        {
1419            self.ensure_memory_replay_mapping(memory_area_id, &existing)?;
1420            register_replay_mapping(self, &existing)?;
1421            self.refresh_cortical_mappings_hash();
1422            return Ok(existing);
1423        }
1424
1425        let twin_id = self.build_memory_twin_id(memory_area_id, upstream_area_id)?;
1426        if self.cortical_areas.contains_key(&twin_id) {
1427            if let Some(existing) = self.cortical_areas.get_mut(&twin_id) {
1428                let expected_source = upstream_area_id.as_base_64();
1429                let expected_target = memory_area_id.as_base_64();
1430                let existing_source = existing
1431                    .properties
1432                    .get("memory_twin_of")
1433                    .and_then(|v| v.as_str());
1434                let existing_target = existing
1435                    .properties
1436                    .get("memory_twin_for")
1437                    .and_then(|v| v.as_str());
1438                if existing_source != Some(expected_source.as_str())
1439                    || existing_target != Some(expected_target.as_str())
1440                {
1441                    warn!(
1442                        target: "feagi-bdu",
1443                        "Twin cortical ID properties missing/mismatched for {} -> {}; repairing",
1444                        upstream_area_id.as_base_64(),
1445                        memory_area_id.as_base_64()
1446                    );
1447                    existing.properties.insert(
1448                        "memory_twin_of".to_string(),
1449                        serde_json::json!(expected_source),
1450                    );
1451                    existing.properties.insert(
1452                        "memory_twin_for".to_string(),
1453                        serde_json::json!(expected_target),
1454                    );
1455                }
1456            }
1457            self.set_memory_twin_mapping(memory_area_id, upstream_area_id, &twin_id);
1458            self.ensure_memory_replay_mapping(memory_area_id, &twin_id)?;
1459            register_replay_mapping(self, &twin_id)?;
1460            self.refresh_cortical_mappings_hash();
1461            return Ok(twin_id);
1462        }
1463
1464        let twin_name = format!("{}_twin", upstream_area.name.replace(' ', "_"));
1465        let twin_type = CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire);
1466        let twin_position = self.build_memory_twin_position(memory_area, upstream_area);
1467        let mut twin_area = CorticalArea::new(
1468            twin_id,
1469            0,
1470            twin_name,
1471            upstream_area.dimensions,
1472            twin_position,
1473            twin_type,
1474        )?;
1475        twin_area.properties = self.build_memory_twin_properties(
1476            memory_area,
1477            upstream_area,
1478            memory_area_id,
1479            upstream_area_id,
1480        );
1481
1482        let _twin_idx = self.add_cortical_area(twin_area)?;
1483        let _ = self.create_neurons_for_area(&twin_id);
1484
1485        self.set_memory_twin_mapping(memory_area_id, upstream_area_id, &twin_id);
1486        self.ensure_memory_replay_mapping(memory_area_id, &twin_id)?;
1487        register_replay_mapping(self, &twin_id)?;
1488        self.refresh_cortical_mappings_hash();
1489        Ok(twin_id)
1490    }
1491
1492    fn build_memory_twin_position(
1493        &self,
1494        memory_area: &CorticalArea,
1495        upstream_area: &CorticalArea,
1496    ) -> GenomeCoordinate3D {
1497        let memory_parent = memory_area
1498            .properties
1499            .get("parent_region_id")
1500            .and_then(|v| v.as_str());
1501        let upstream_parent = upstream_area
1502            .properties
1503            .get("parent_region_id")
1504            .and_then(|v| v.as_str());
1505        let same_region = memory_parent.is_some() && memory_parent == upstream_parent;
1506
1507        if !same_region {
1508            return GenomeCoordinate3D::new(
1509                memory_area.position.x + 20,
1510                memory_area.position.y,
1511                memory_area.position.z,
1512            );
1513        }
1514
1515        let width = upstream_area.dimensions.width as f32;
1516        let margin = (width * 0.25).ceil() as i32;
1517        let offset = upstream_area.dimensions.width as i32 + margin;
1518        GenomeCoordinate3D::new(
1519            upstream_area.position.x + offset,
1520            upstream_area.position.y,
1521            upstream_area.position.z,
1522        )
1523    }
1524
1525    fn build_memory_twin_id(
1526        &self,
1527        memory_area_id: &CorticalID,
1528        upstream_area_id: &CorticalID,
1529    ) -> BduResult<CorticalID> {
1530        let mut hasher = Xxh64::new(DATA_HASH_SEED);
1531        hasher.write(memory_area_id.as_base_64().as_bytes());
1532        hasher.write(upstream_area_id.as_base_64().as_bytes());
1533        hasher.write(b"memory_twin");
1534        let hash = hasher.finish();
1535        let mut bytes = hash.to_be_bytes();
1536        bytes[0] = b'c';
1537        CorticalID::try_from_bytes(&bytes)
1538            .map_err(|e| BduError::Internal(format!("Failed to build twin cortical ID: {}", e)))
1539    }
1540
1541    fn build_memory_twin_properties(
1542        &self,
1543        memory_area: &CorticalArea,
1544        upstream_area: &CorticalArea,
1545        memory_area_id: &CorticalID,
1546        upstream_area_id: &CorticalID,
1547    ) -> HashMap<String, serde_json::Value> {
1548        let mut props = upstream_area.properties.clone();
1549        props.remove("cortical_mapping_dst");
1550        props.remove("upstream_cortical_areas");
1551        props.remove("parent_region_id");
1552        props.insert("cortical_group".to_string(), serde_json::json!("CUSTOM"));
1553        props.insert("is_mem_type".to_string(), serde_json::json!(false));
1554        props.insert(
1555            "memory_twin_of".to_string(),
1556            serde_json::json!(upstream_area_id.as_base_64()),
1557        );
1558        props.insert(
1559            "memory_twin_for".to_string(),
1560            serde_json::json!(memory_area_id.as_base_64()),
1561        );
1562        if let Some(parent_region_id) = memory_area
1563            .properties
1564            .get("parent_region_id")
1565            .and_then(|v| v.as_str())
1566        {
1567            props.insert(
1568                "parent_region_id".to_string(),
1569                serde_json::json!(parent_region_id),
1570            );
1571        }
1572        props
1573    }
1574
1575    fn set_memory_twin_mapping(
1576        &mut self,
1577        memory_area_id: &CorticalID,
1578        upstream_area_id: &CorticalID,
1579        twin_id: &CorticalID,
1580    ) {
1581        if let Some(memory_area) = self.cortical_areas.get_mut(memory_area_id) {
1582            let mapping = memory_area
1583                .properties
1584                .entry("memory_twin_areas".to_string())
1585                .or_insert_with(|| serde_json::json!({}));
1586            if let Some(map) = mapping.as_object_mut() {
1587                map.insert(
1588                    upstream_area_id.as_base_64(),
1589                    serde_json::json!(twin_id.as_base_64()),
1590                );
1591            }
1592        }
1593    }
1594
1595    fn ensure_memory_replay_mapping(
1596        &mut self,
1597        memory_area_id: &CorticalID,
1598        twin_id: &CorticalID,
1599    ) -> BduResult<()> {
1600        if !self.morphology_registry.contains("memory_replay") {
1601            feagi_evolutionary::add_core_morphologies(&mut self.morphology_registry);
1602        }
1603        self.refresh_morphologies_hash();
1604        let mapping_data = vec![serde_json::json!({
1605            "morphology_id": "memory_replay",
1606            "morphology_scalar": [1, 1, 1],
1607            "postSynapticCurrent_multiplier": 1,
1608            "plasticity_flag": false,
1609            "plasticity_constant": 0,
1610            "ltp_multiplier": 0,
1611            "ltd_multiplier": 0,
1612            "plasticity_window": 0,
1613        })];
1614        self.update_cortical_mapping(memory_area_id, twin_id, mapping_data)?;
1615        let _ = self.regenerate_synapses_for_mapping(memory_area_id, twin_id)?;
1616        // Update cortical area hash so BV refreshes area details (outgoing mappings).
1617        self.refresh_cortical_area_hashes(true, false);
1618        Ok(())
1619    }
1620
1621    /// Remove an upstream cortical area from a target area's upstream list
1622    ///
1623    /// This should be called when all synapses from src_cortical_idx to target_cortical_id are deleted.
1624    ///
1625    /// # Arguments
1626    ///
1627    /// * `target_cortical_id` - The cortical area that had connections
1628    /// * `src_cortical_idx` - The cortical index of the source area to remove
1629    ///
1630    pub fn remove_upstream_area(&mut self, target_cortical_id: &CorticalID, src_cortical_idx: u32) {
1631        if let Some(area) = self.cortical_areas.get_mut(target_cortical_id) {
1632            if let Some(upstream_prop) = area.properties.get_mut("upstream_cortical_areas") {
1633                if let Some(arr) = upstream_prop.as_array_mut() {
1634                    let src_value = serde_json::json!(src_cortical_idx);
1635                    if let Some(pos) = arr.iter().position(|v| v == &src_value) {
1636                        arr.remove(pos);
1637                        debug!(target: "feagi-bdu",
1638                            "Removed upstream area idx={} from cortical area '{}'",
1639                            src_cortical_idx, target_cortical_id.as_base_64()
1640                        );
1641                    }
1642                }
1643            }
1644        }
1645    }
1646
1647    /// Check if a cortical area exists
1648    pub fn has_cortical_area(&self, cortical_id: &CorticalID) -> bool {
1649        self.cortical_areas.contains_key(cortical_id)
1650    }
1651
1652    /// Check if the connectome is initialized (has areas)
1653    pub fn is_initialized(&self) -> bool {
1654        self.initialized && !self.cortical_areas.is_empty()
1655    }
1656
1657    // ======================================================================
1658    // Brain Region Management
1659    // ======================================================================
1660
1661    /// Add a brain region
1662    pub fn add_brain_region(
1663        &mut self,
1664        region: BrainRegion,
1665        parent_id: Option<String>,
1666    ) -> BduResult<()> {
1667        self.brain_regions.add_region(region, parent_id)?;
1668        self.refresh_brain_regions_hash();
1669        Ok(())
1670    }
1671
1672    /// Remove a brain region
1673    pub fn remove_brain_region(&mut self, region_id: &str) -> BduResult<()> {
1674        self.brain_regions.remove_region(region_id)?;
1675        self.refresh_brain_regions_hash();
1676        Ok(())
1677    }
1678
1679    /// Change the parent of an existing brain region.
1680    pub fn change_brain_region_parent(
1681        &mut self,
1682        region_id: &str,
1683        new_parent_id: &str,
1684    ) -> BduResult<()> {
1685        self.brain_regions.change_parent(region_id, new_parent_id)?;
1686        self.refresh_brain_regions_hash();
1687        Ok(())
1688    }
1689
1690    /// Get a brain region by ID
1691    pub fn get_brain_region(&self, region_id: &str) -> Option<&BrainRegion> {
1692        self.brain_regions.get_region(region_id)
1693    }
1694
1695    /// Get a mutable reference to a brain region
1696    pub fn get_brain_region_mut(&mut self, region_id: &str) -> Option<&mut BrainRegion> {
1697        self.brain_regions.get_region_mut(region_id)
1698    }
1699
1700    /// Get all brain region IDs
1701    pub fn get_brain_region_ids(&self) -> Vec<&String> {
1702        self.brain_regions.get_all_region_ids()
1703    }
1704
1705    /// Get the brain region hierarchy
1706    pub fn get_brain_region_hierarchy(&self) -> &BrainRegionHierarchy {
1707        &self.brain_regions
1708    }
1709
1710    // ========================================================================
1711    // MORPHOLOGY ACCESS
1712    // ========================================================================
1713
1714    /// Get all morphologies from the loaded genome
1715    pub fn get_morphologies(&self) -> &feagi_evolutionary::MorphologyRegistry {
1716        &self.morphology_registry
1717    }
1718
1719    /// Get morphology count
1720    pub fn get_morphology_count(&self) -> usize {
1721        self.morphology_registry.count()
1722    }
1723
1724    /// Insert or overwrite a morphology definition in the in-memory registry.
1725    ///
1726    /// NOTE: This updates the runtime registry used by mapping/synapse generation.
1727    /// Callers that also maintain a RuntimeGenome (source of truth) MUST update it too.
1728    pub fn upsert_morphology(
1729        &mut self,
1730        morphology_id: String,
1731        morphology: feagi_evolutionary::Morphology,
1732    ) {
1733        self.morphology_registry
1734            .add_morphology(morphology_id, morphology);
1735        self.refresh_morphologies_hash();
1736    }
1737
1738    /// Remove a morphology definition from the in-memory registry.
1739    ///
1740    /// Returns true if the morphology existed and was removed.
1741    ///
1742    /// NOTE: Callers that also maintain a RuntimeGenome (source of truth) MUST update it too.
1743    pub fn remove_morphology(&mut self, morphology_id: &str) -> bool {
1744        let removed = self.morphology_registry.remove_morphology(morphology_id);
1745        if removed {
1746            self.refresh_morphologies_hash();
1747        }
1748        removed
1749    }
1750
1751    // ========================================================================
1752    // CORTICAL MAPPING UPDATES
1753    // ========================================================================
1754
1755    /// Update cortical mapping properties between two cortical areas
1756    ///
1757    /// Following Python's update_cortical_mapping_properties() logic:
1758    /// 1. Updates the source area's cortical_mapping_dst property
1759    /// 2. Triggers synapse regeneration for the affected connection
1760    ///
1761    /// # Arguments
1762    /// * `src_area_id` - Source cortical area ID
1763    /// * `dst_area_id` - Destination cortical area ID
1764    /// * `mapping_data` - List of connection specifications
1765    ///
1766    /// # Returns
1767    /// * `BduResult<()>` - Ok if successful, Err otherwise
1768    pub fn update_cortical_mapping(
1769        &mut self,
1770        src_area_id: &CorticalID,
1771        dst_area_id: &CorticalID,
1772        mapping_data: Vec<serde_json::Value>,
1773    ) -> BduResult<()> {
1774        use tracing::info;
1775
1776        info!(target: "feagi-bdu", "Updating cortical mapping: {} -> {}", src_area_id, dst_area_id);
1777
1778        // Helper: detect associative_memory (bi-directional STDP) in a mapping payload
1779        let mapping_has_bidirectional_stdp = |rules: &[serde_json::Value]| -> bool {
1780            for rule in rules {
1781                if let Some(obj) = rule.as_object() {
1782                    if let Some(morphology_id) = obj.get("morphology_id").and_then(|v| v.as_str()) {
1783                        if morphology_id == "associative_memory" {
1784                            return true;
1785                        }
1786                    }
1787                }
1788            }
1789            false
1790        };
1791
1792        let requested_bidirectional = mapping_has_bidirectional_stdp(&mapping_data);
1793        let existing_bidirectional = self
1794            .cortical_areas
1795            .get(src_area_id)
1796            .and_then(|area| area.properties.get("cortical_mapping_dst"))
1797            .and_then(|v| v.as_object())
1798            .and_then(|map| map.get(&dst_area_id.as_base_64()))
1799            .and_then(|v| v.as_array())
1800            .map(|arr| mapping_has_bidirectional_stdp(arr))
1801            .unwrap_or(false);
1802        let should_apply_bidirectional = requested_bidirectional || existing_bidirectional;
1803
1804        {
1805            // Get source area (must exist)
1806            let src_area = self.cortical_areas.get_mut(src_area_id).ok_or_else(|| {
1807                crate::types::BduError::InvalidArea(format!(
1808                    "Source area not found: {}",
1809                    src_area_id
1810                ))
1811            })?;
1812
1813            // Get or create cortical_mapping_dst property
1814            let cortical_mapping_dst =
1815                if let Some(existing) = src_area.properties.get_mut("cortical_mapping_dst") {
1816                    existing.as_object_mut().ok_or_else(|| {
1817                        crate::types::BduError::InvalidMorphology(
1818                            "cortical_mapping_dst is not an object".to_string(),
1819                        )
1820                    })?
1821                } else {
1822                    // Create new cortical_mapping_dst
1823                    src_area
1824                        .properties
1825                        .insert("cortical_mapping_dst".to_string(), serde_json::json!({}));
1826                    src_area
1827                        .properties
1828                        .get_mut("cortical_mapping_dst")
1829                        .unwrap()
1830                        .as_object_mut()
1831                        .unwrap()
1832                };
1833
1834            // Update or add the mapping for this destination
1835            if mapping_data.is_empty() {
1836                // Empty mapping_data = delete the connection
1837                cortical_mapping_dst.remove(&dst_area_id.as_base_64());
1838                info!(target: "feagi-bdu", "Removed mapping from {} to {}", src_area_id, dst_area_id);
1839            } else {
1840                cortical_mapping_dst.insert(
1841                    dst_area_id.as_base_64(),
1842                    serde_json::Value::Array(mapping_data.clone()),
1843                );
1844                info!(target: "feagi-bdu", "Updated mapping from {} to {} with {} connections",
1845                      src_area_id, dst_area_id, mapping_data.len());
1846            }
1847        }
1848
1849        // Bi-directional STDP: mirror the mapping on the reverse pair
1850        if should_apply_bidirectional {
1851            let dst_area = self.cortical_areas.get_mut(dst_area_id).ok_or_else(|| {
1852                crate::types::BduError::InvalidArea(format!(
1853                    "Destination area not found: {}",
1854                    dst_area_id
1855                ))
1856            })?;
1857            let dst_mapping_dst =
1858                if let Some(existing) = dst_area.properties.get_mut("cortical_mapping_dst") {
1859                    existing.as_object_mut().ok_or_else(|| {
1860                        crate::types::BduError::InvalidMorphology(
1861                            "cortical_mapping_dst is not an object".to_string(),
1862                        )
1863                    })?
1864                } else {
1865                    dst_area
1866                        .properties
1867                        .insert("cortical_mapping_dst".to_string(), serde_json::json!({}));
1868                    dst_area
1869                        .properties
1870                        .get_mut("cortical_mapping_dst")
1871                        .unwrap()
1872                        .as_object_mut()
1873                        .unwrap()
1874                };
1875
1876            if mapping_data.is_empty() {
1877                dst_mapping_dst.remove(&src_area_id.as_base_64());
1878                info!(
1879                    target: "feagi-bdu",
1880                    "Removed bi-directional STDP mirror mapping from {} to {}",
1881                    dst_area_id,
1882                    src_area_id
1883                );
1884            } else {
1885                dst_mapping_dst.insert(
1886                    src_area_id.as_base_64(),
1887                    serde_json::Value::Array(mapping_data.clone()),
1888                );
1889                info!(
1890                    target: "feagi-bdu",
1891                    "Updated bi-directional STDP mirror mapping from {} to {} with {} connections",
1892                    dst_area_id,
1893                    src_area_id,
1894                    mapping_data.len()
1895                );
1896            }
1897        }
1898
1899        self.refresh_cortical_mappings_hash();
1900
1901        Ok(())
1902    }
1903
1904    /// Regenerate synapses for a specific cortical mapping
1905    ///
1906    /// Creates new synapses based on mapping rules. Only removes existing synapses if
1907    /// a mapping already existed (update case), not for new mappings (allows multiple
1908    /// synapses between the same neurons).
1909    ///
1910    /// # Arguments
1911    /// * `src_area_id` - Source cortical area ID
1912    /// * `dst_area_id` - Destination cortical area ID
1913    ///
1914    /// # Returns
1915    /// * `BduResult<usize>` - Number of synapses created
1916    pub fn regenerate_synapses_for_mapping(
1917        &mut self,
1918        src_area_id: &CorticalID,
1919        dst_area_id: &CorticalID,
1920    ) -> BduResult<usize> {
1921        use tracing::info;
1922
1923        info!(target: "feagi-bdu", "Regenerating synapses: {} -> {}", src_area_id, dst_area_id);
1924
1925        let mapping_rules_len = self
1926            .cortical_areas
1927            .get(src_area_id)
1928            .and_then(|area| area.properties.get("cortical_mapping_dst"))
1929            .and_then(|v| v.as_object())
1930            .and_then(|map| map.get(&dst_area_id.as_base_64()))
1931            .and_then(|v| v.as_array())
1932            .map(|arr| arr.len())
1933            .unwrap_or(0);
1934        tracing::debug!(
1935            target: "feagi-bdu",
1936            "Mapping rules for {} -> {}: {}",
1937            src_area_id,
1938            dst_area_id,
1939            mapping_rules_len
1940        );
1941
1942        // If NPU is available, regenerate synapses
1943        let Some(npu_arc) = self.npu.clone() else {
1944            info!(target: "feagi-bdu", "NPU not available - skipping synapse regeneration");
1945            return Ok(0);
1946        };
1947
1948        // Mapping regeneration must be deterministic:
1949        // - On mapping deletion: prune all synapses from A→B, then attempt synaptogenesis (which yields 0).
1950        // - On rule removal/updates: prune all synapses from A→B, then re-run synaptogenesis using the *current*
1951        //   mapping rules. This guarantees stale synapses from removed rules do not persist, while preserving
1952        //   other A→B mappings by re-creating them from the remaining rules.
1953        //
1954        // NOTE: This pruning requires retrieving neuron IDs in each area. Today, that can be O(all_neurons)
1955        // via `get_neurons_in_cortical_area()`. This is the safest correctness-first behavior.
1956
1957        let src_idx = *self.cortical_id_to_idx.get(src_area_id).ok_or_else(|| {
1958            BduError::InvalidArea(format!("No cortical idx for source area {}", src_area_id))
1959        })?;
1960        let dst_idx = *self.cortical_id_to_idx.get(dst_area_id).ok_or_else(|| {
1961            BduError::InvalidArea(format!(
1962                "No cortical idx for destination area {}",
1963                dst_area_id
1964            ))
1965        })?;
1966
1967        // Prune all existing synapses from src_area→dst_area before (re)creating based on current rules.
1968        // This prevents stale synapses when rules are removed/edited.
1969        let mut pruned_synapse_count: usize = 0;
1970        use std::time::Instant;
1971        let start = Instant::now();
1972
1973        // Get neuron lists (may be slow; see note above).
1974        //
1975        // IMPORTANT: Do not rely on per-area cached neuron counts here. Pruning must be correct even if
1976        // caches are stale (e.g., in tests or during partial initialization). If either side is empty,
1977        // pruning is a no-op anyway.
1978        let (sources, targets) = {
1979            let lock_start = std::time::Instant::now();
1980            let npu = npu_arc.lock().unwrap();
1981            let lock_wait = lock_start.elapsed();
1982            tracing::debug!(
1983                target: "feagi-bdu",
1984                "[NPU-LOCK] prune list lock wait {:.2}ms for {} -> {}",
1985                lock_wait.as_secs_f64() * 1000.0,
1986                src_area_id,
1987                dst_area_id
1988            );
1989            let sources: Vec<NeuronId> = npu
1990                .get_neurons_in_cortical_area(src_idx)
1991                .into_iter()
1992                .map(NeuronId)
1993                .collect();
1994            let targets: Vec<NeuronId> = npu
1995                .get_neurons_in_cortical_area(dst_idx)
1996                .into_iter()
1997                .map(NeuronId)
1998                .collect();
1999            (sources, targets)
2000        };
2001
2002        tracing::debug!(
2003            target: "feagi-bdu",
2004            "Prune synapses: {} sources, {} targets",
2005            sources.len(),
2006            targets.len()
2007        );
2008
2009        if !sources.is_empty() && !targets.is_empty() {
2010            let remove_start = Instant::now();
2011            pruned_synapse_count = {
2012                let lock_start = std::time::Instant::now();
2013                let mut npu = npu_arc.lock().unwrap();
2014                let lock_wait = lock_start.elapsed();
2015                tracing::debug!(
2016                    target: "feagi-bdu",
2017                    "[NPU-LOCK] prune remove lock wait {:.2}ms for {} -> {}",
2018                    lock_wait.as_secs_f64() * 1000.0,
2019                    src_area_id,
2020                    dst_area_id
2021                );
2022                // Use direct source-target batch removal here rather than index-based removal.
2023                // This avoids false "Pruned 0" outcomes when propagation synapse_index is stale
2024                // during repeated rapid remap operations.
2025                npu.remove_synapses_between(sources, targets)
2026            };
2027            let remove_time = remove_start.elapsed();
2028            let total_time = start.elapsed();
2029
2030            info!(
2031                target: "feagi-bdu",
2032                "Pruned {} existing synapses for mapping {} -> {} (total={}ms, remove={}ms)",
2033                pruned_synapse_count,
2034                src_area_id,
2035                dst_area_id,
2036                total_time.as_millis(),
2037                remove_time.as_millis()
2038            );
2039
2040            // Update StateManager synapse count (health_check endpoint)
2041            if pruned_synapse_count > 0 {
2042                let pruned_u32 = u32::try_from(pruned_synapse_count).map_err(|_| {
2043                    BduError::Internal(format!(
2044                        "Pruned synapse count overflow (usize -> u32): {}",
2045                        pruned_synapse_count
2046                    ))
2047                })?;
2048                let state_manager = StateManager::instance();
2049                let state_manager = state_manager.read();
2050                let core_state = state_manager.get_core_state();
2051                core_state.subtract_synapse_count(pruned_u32);
2052                state_manager.subtract_cortical_area_outgoing_synapses(
2053                    &src_area_id.as_base_64(),
2054                    pruned_synapse_count,
2055                );
2056                state_manager.subtract_cortical_area_incoming_synapses(
2057                    &dst_area_id.as_base_64(),
2058                    pruned_synapse_count,
2059                );
2060
2061                // Best-effort: adjust per-area outgoing synapse count cache for the source area.
2062                // (Cache is used for lock-free health-check reads; correctness is eventually
2063                // consistent via periodic refresh of global count from NPU.)
2064                {
2065                    let mut cache = self.cached_synapse_counts_per_area.write();
2066                    let entry = cache
2067                        .entry(*src_area_id)
2068                        .or_insert_with(|| AtomicUsize::new(0));
2069                    let mut current = entry.load(Ordering::Relaxed);
2070                    loop {
2071                        let next = current.saturating_sub(pruned_synapse_count);
2072                        match entry.compare_exchange(
2073                            current,
2074                            next,
2075                            Ordering::Relaxed,
2076                            Ordering::Relaxed,
2077                        ) {
2078                            Ok(_) => break,
2079                            Err(v) => current = v,
2080                        }
2081                    }
2082                }
2083            }
2084        }
2085
2086        // Apply cortical mapping rules to create synapses (may be 0 for memory areas).
2087        //
2088        // IMPORTANT:
2089        // - We already pruned A→B synapses above to ensure no stale synapses remain after a rule removal/update.
2090        // - `apply_cortical_mapping_for_pair()` returns the created synapse count but does not update
2091        //   StateManager/caches; we do that immediately after the call.
2092        let synapse_count = self.apply_cortical_mapping_for_pair(src_area_id, dst_area_id)?;
2093        tracing::debug!(
2094            target: "feagi-bdu",
2095            "Synaptogenesis created {} synapses for {} -> {}",
2096            synapse_count,
2097            src_area_id,
2098            dst_area_id
2099        );
2100
2101        // Update synapse count caches and StateManager based on synapses created.
2102        // NOTE: apply_cortical_mapping_for_pair() does not touch caches/StateManager.
2103        if synapse_count > 0 {
2104            let created_u32 = u32::try_from(synapse_count).map_err(|_| {
2105                BduError::Internal(format!(
2106                    "Created synapse count overflow (usize -> u32): {}",
2107                    synapse_count
2108                ))
2109            })?;
2110
2111            // Update per-area outgoing synapse count cache (source area)
2112            {
2113                let mut cache = self.cached_synapse_counts_per_area.write();
2114                cache
2115                    .entry(*src_area_id)
2116                    .or_insert_with(|| AtomicUsize::new(0))
2117                    .fetch_add(synapse_count, Ordering::Relaxed);
2118            }
2119
2120            // Update StateManager synapse count (health_check endpoint)
2121            let state_manager = StateManager::instance();
2122            let state_manager = state_manager.read();
2123            let core_state = state_manager.get_core_state();
2124            core_state.add_synapse_count(created_u32);
2125            state_manager
2126                .add_cortical_area_outgoing_synapses(&src_area_id.as_base_64(), synapse_count);
2127            state_manager
2128                .add_cortical_area_incoming_synapses(&dst_area_id.as_base_64(), synapse_count);
2129        }
2130
2131        // Update upstream area tracking based on MAPPING existence, not synapse count
2132        // Memory areas have 0 synapses but still need upstream tracking for pattern detection
2133        let src_idx_for_upstream = src_idx;
2134
2135        // Check if mapping exists by looking at cortical_mapping_dst property (after update)
2136        let has_mapping = self
2137            .cortical_areas
2138            .get(src_area_id)
2139            .and_then(|area| area.properties.get("cortical_mapping_dst"))
2140            .and_then(|v| v.as_object())
2141            .and_then(|map| map.get(&dst_area_id.as_base_64()))
2142            .is_some();
2143
2144        info!(target: "feagi-bdu",
2145            "Mapping result: {} synapses, {} -> {} (mapping_exists={}, will {}update upstream)",
2146            synapse_count,
2147            src_area_id.as_base_64(),
2148            dst_area_id.as_base_64(),
2149            has_mapping,
2150            if has_mapping { "" } else { "NOT " }
2151        );
2152
2153        if has_mapping {
2154            // Mapping exists - add to upstream tracking (for both memory and regular areas)
2155            self.add_upstream_area(dst_area_id, src_idx_for_upstream);
2156
2157            if let Some(dst_area) = self.cortical_areas.get(dst_area_id) {
2158                if matches!(dst_area.cortical_type, CorticalAreaType::Memory(_)) {
2159                    if let Err(e) = self.ensure_memory_twin_area(dst_area_id, src_area_id) {
2160                        warn!(
2161                            target: "feagi-bdu",
2162                            "Failed to ensure memory twin for {} -> {}: {}",
2163                            src_area_id.as_base_64(),
2164                            dst_area_id.as_base_64(),
2165                            e
2166                        );
2167                    }
2168                }
2169            }
2170
2171            // If destination is a memory area, register it with PlasticityExecutor (automatic)
2172            #[cfg(feature = "plasticity")]
2173            if let Some(ref executor) = self.plasticity_executor {
2174                use feagi_evolutionary::extract_memory_properties;
2175
2176                if let Some(dst_area) = self.cortical_areas.get(dst_area_id) {
2177                    if let Some(mem_props) = extract_memory_properties(&dst_area.properties) {
2178                        let upstream_areas = self.get_upstream_cortical_areas(dst_area_id);
2179                        let upstream_non_memory =
2180                            self.filter_non_memory_upstream_areas(&upstream_areas);
2181                        debug!(
2182                            target: "feagi-bdu",
2183                            "Registering memory area idx={} id={} upstream={} depth={}",
2184                            dst_area.cortical_idx,
2185                            dst_area_id.as_base_64(),
2186                            upstream_areas.len(),
2187                            mem_props.temporal_depth
2188                        );
2189
2190                        // Ensure FireLedger tracks the upstream areas with at least the required temporal depth.
2191                        // Dense, burst-aligned tracking is required for correct memory pattern hashing.
2192                        if let Some(ref npu_arc) = self.npu {
2193                            if let Ok(mut npu) = npu_arc.lock() {
2194                                let existing_configs = npu.get_all_fire_ledger_configs();
2195                                for &upstream_idx in &upstream_areas {
2196                                    let existing = existing_configs
2197                                        .iter()
2198                                        .find(|(idx, _)| *idx == upstream_idx)
2199                                        .map(|(_, w)| *w)
2200                                        .unwrap_or(0);
2201
2202                                    let desired = mem_props.temporal_depth as usize;
2203                                    let resolved = existing.max(desired);
2204                                    if resolved != existing {
2205                                        if let Err(e) =
2206                                            npu.configure_fire_ledger_window(upstream_idx, resolved)
2207                                        {
2208                                            warn!(
2209                                                target: "feagi-bdu",
2210                                                "Failed to configure FireLedger window for upstream area idx={} (requested={}): {}",
2211                                                upstream_idx,
2212                                                resolved,
2213                                                e
2214                                            );
2215                                        }
2216                                    }
2217                                }
2218                            } else {
2219                                warn!(target: "feagi-bdu", "Failed to lock NPU for FireLedger configuration");
2220                            }
2221                        }
2222
2223                        if let Ok(exec) = executor.lock() {
2224                            use feagi_npu_plasticity::{
2225                                MemoryNeuronLifecycleConfig, PlasticityExecutor,
2226                            };
2227
2228                            let lifecycle_config = MemoryNeuronLifecycleConfig {
2229                                initial_lifespan: mem_props.init_lifespan,
2230                                lifespan_growth_rate: mem_props.lifespan_growth_rate,
2231                                longterm_threshold: mem_props.longterm_threshold,
2232                                max_reactivations: 1000,
2233                            };
2234
2235                            exec.register_memory_area(
2236                                dst_area.cortical_idx,
2237                                dst_area_id.as_base_64(),
2238                                mem_props.temporal_depth,
2239                                upstream_non_memory,
2240                                Some(lifecycle_config),
2241                            );
2242                        } else {
2243                            warn!(target: "feagi-bdu", "Failed to lock PlasticityExecutor");
2244                        }
2245                    } else {
2246                        debug!(
2247                            target: "feagi-bdu",
2248                            "Skipping plasticity registration: no memory properties for area {}",
2249                            dst_area_id.as_base_64()
2250                        );
2251                    }
2252                } else {
2253                    warn!(target: "feagi-bdu", "Destination area {} not found in cortical_areas", dst_area_id.as_base_64());
2254                }
2255            } else {
2256                warn!(
2257                    target: "feagi-bdu",
2258                    "PlasticityExecutor not available; memory area {} not registered",
2259                    dst_area_id.as_base_64()
2260                );
2261            }
2262
2263            #[cfg(not(feature = "plasticity"))]
2264            {
2265                info!(target: "feagi-bdu", "Plasticity feature disabled at compile time");
2266            }
2267        } else {
2268            // Mapping deleted - remove from upstream tracking
2269            self.remove_upstream_area(dst_area_id, src_idx_for_upstream);
2270
2271            // Ensure any STDP mapping parameters for this pair are removed when the mapping is gone.
2272            let mut npu = npu_arc.lock().unwrap();
2273            let _was_registered = npu.unregister_stdp_mapping(src_idx, dst_idx);
2274        }
2275
2276        info!(
2277            target: "feagi-bdu",
2278            "Created {} new synapses: {} -> {}",
2279            synapse_count,
2280            src_area_id,
2281            dst_area_id
2282        );
2283
2284        // CRITICAL: Rebuild synapse index so removals are reflected in propagation and query paths.
2285        // Many morphology paths rebuild the index after creation, but pruning requires an explicit rebuild.
2286        if pruned_synapse_count > 0 || synapse_count == 0 {
2287            let mut npu = npu_arc.lock().unwrap();
2288            npu.rebuild_synapse_index();
2289            info!(
2290                target: "feagi-bdu",
2291                "Rebuilt synapse index after regenerating {} -> {} (pruned={}, created={})",
2292                src_area_id,
2293                dst_area_id,
2294                pruned_synapse_count,
2295                synapse_count
2296            );
2297        } else {
2298            info!(
2299                target: "feagi-bdu",
2300                "Skipped synapse index rebuild for mapping {} -> {} (created={}, pruned=0; index rebuilt during synaptogenesis)",
2301                src_area_id,
2302                dst_area_id,
2303                synapse_count
2304            );
2305        }
2306
2307        // Refresh the global synapse count cache from NPU (deterministic after prune/create).
2308        {
2309            let npu = npu_arc.lock().unwrap();
2310            let fresh_count = npu.get_synapse_count();
2311            self.cached_synapse_count
2312                .store(fresh_count, Ordering::Relaxed);
2313        }
2314
2315        Ok(synapse_count)
2316    }
2317
2318    /// Register STDP mapping parameters for a plastic rule
2319    #[allow(clippy::too_many_arguments)]
2320    fn register_stdp_mapping_for_rule(
2321        npu: &Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
2322        src_area_id: &CorticalID,
2323        dst_area_id: &CorticalID,
2324        src_cortical_idx: u32,
2325        dst_cortical_idx: u32,
2326        rule_obj: &serde_json::Map<String, serde_json::Value>,
2327        bidirectional_stdp: bool,
2328        synapse_psp: u8,
2329        synapse_type: feagi_npu_neural::SynapseType,
2330    ) -> BduResult<()> {
2331        let plasticity_window = rule_obj
2332            .get("plasticity_window")
2333            .and_then(|v| v.as_u64())
2334            .ok_or_else(|| {
2335                BduError::Internal(format!(
2336                    "Missing plasticity_window in plastic mapping rule {} -> {}",
2337                    src_area_id, dst_area_id
2338                ))
2339            })? as usize;
2340        let plasticity_constant = rule_obj
2341            .get("plasticity_constant")
2342            .and_then(|v| v.as_i64())
2343            .ok_or_else(|| {
2344                BduError::Internal(format!(
2345                    "Missing plasticity_constant in plastic mapping rule {} -> {}",
2346                    src_area_id, dst_area_id
2347                ))
2348            })?;
2349        let ltp_multiplier = rule_obj
2350            .get("ltp_multiplier")
2351            .and_then(|v| v.as_i64())
2352            .ok_or_else(|| {
2353                BduError::Internal(format!(
2354                    "Missing ltp_multiplier in plastic mapping rule {} -> {}",
2355                    src_area_id, dst_area_id
2356                ))
2357            })?;
2358        let ltd_multiplier = rule_obj
2359            .get("ltd_multiplier")
2360            .and_then(|v| v.as_i64())
2361            .ok_or_else(|| {
2362                BduError::Internal(format!(
2363                    "Missing ltd_multiplier in plastic mapping rule {} -> {}",
2364                    src_area_id, dst_area_id
2365                ))
2366            })?;
2367
2368        let params = feagi_npu_burst_engine::npu::StdpMappingParams {
2369            plasticity_window,
2370            plasticity_constant,
2371            ltp_multiplier,
2372            ltd_multiplier,
2373            bidirectional_stdp,
2374            synapse_psp,
2375            synapse_type,
2376        };
2377
2378        trace!(target: "feagi-bdu", "[LOCK-TRACE] create_neurons_for_area: attempting NPU lock");
2379        let mut npu_lock = npu
2380            .lock()
2381            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
2382        trace!(target: "feagi-bdu", "[LOCK-TRACE] create_neurons_for_area: acquired NPU lock");
2383
2384        npu_lock
2385            .register_stdp_mapping(src_cortical_idx, dst_cortical_idx, params)
2386            .map_err(|e| {
2387                BduError::Internal(format!(
2388                    "Failed to register STDP mapping {} -> {}: {}",
2389                    src_area_id, dst_area_id, e
2390                ))
2391            })?;
2392
2393        // FireLedger tracking for STDP (ensure A and B are tracked at least to plasticity_window)
2394        let existing_configs = npu_lock.get_all_fire_ledger_configs();
2395        for area_idx in [src_cortical_idx, dst_cortical_idx] {
2396            let existing = existing_configs
2397                .iter()
2398                .find(|(idx, _)| *idx == area_idx)
2399                .map(|(_, w)| *w)
2400                .unwrap_or(0);
2401            let resolved = existing.max(plasticity_window);
2402            if resolved != existing {
2403                npu_lock
2404                    .configure_fire_ledger_window(area_idx, resolved)
2405                    .map_err(|e| {
2406                        BduError::Internal(format!(
2407                            "Failed to configure FireLedger window for area idx={} (requested={}): {}",
2408                            area_idx, resolved, e
2409                        ))
2410                    })?;
2411            }
2412        }
2413
2414        Ok(())
2415    }
2416
2417    /// Resolve synapse weight, PSP, and type from a mapping rule.
2418    fn resolve_synapse_params_for_rule(
2419        &self,
2420        src_area_id: &CorticalID,
2421        rule: &serde_json::Value,
2422    ) -> BduResult<(u8, u8, feagi_npu_neural::SynapseType)> {
2423        // Get source area to access PSP property
2424        let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2425            crate::types::BduError::InvalidArea(format!("Source area not found: {}", src_area_id))
2426        })?;
2427
2428        // Extract weight from rule (postSynapticCurrent_multiplier)
2429        //
2430        // IMPORTANT:
2431        // - This value represents the synapse "weight" stored in the NPU (u8: 0..255).
2432        // - Do NOT scale by 255 here. A multiplier of 1.0 should remain weight=1 (not 255).
2433        let (weight, synapse_type) = {
2434            // Accept either integer or whole-number float inputs for compatibility with older clients/tests.
2435            let parse_i64 = |v: &serde_json::Value| -> Option<i64> {
2436                if let Some(i) = v.as_i64() {
2437                    return Some(i);
2438                }
2439                let f = v.as_f64()?;
2440                if f.fract() == 0.0 {
2441                    Some(f as i64)
2442                } else {
2443                    None
2444                }
2445            };
2446
2447            let multiplier_i64: i64 = if let Some(obj) = rule.as_object() {
2448                obj.get("postSynapticCurrent_multiplier")
2449                    .and_then(parse_i64)
2450                    .unwrap_or(1) // @architecture:acceptable - rule-level default multiplier
2451            } else if let Some(arr) = rule.as_array() {
2452                // Array format: [morphology_id, scalar, multiplier, ...]
2453                arr.get(2).and_then(parse_i64).unwrap_or(1) // @architecture:acceptable - rule-level default multiplier
2454            } else {
2455                128 // @architecture:acceptable - emergency fallback for malformed rule
2456            };
2457
2458            if multiplier_i64 < 0 {
2459                let abs = if multiplier_i64 == i64::MIN {
2460                    i64::MAX
2461                } else {
2462                    multiplier_i64.abs()
2463                };
2464                (
2465                    abs.clamp(0, 255) as u8,
2466                    feagi_npu_neural::SynapseType::Inhibitory,
2467                )
2468            } else {
2469                (
2470                    multiplier_i64.clamp(0, 255) as u8,
2471                    feagi_npu_neural::SynapseType::Excitatory,
2472                )
2473            }
2474        };
2475
2476        // Get PSP from source cortical area.
2477        //
2478        // IMPORTANT:
2479        // - This value represents the synapse PSP stored in the NPU (u8: 0..255).
2480        // - Treat `postsynaptic_current` as an absolute value in 0..255 units.
2481        // - Do NOT scale by 255 here. A PSP of 1.0 should remain PSP=1 (not 255).
2482        let (psp_f32, psp) = {
2483            use crate::models::cortical_area::CorticalAreaExt;
2484            let psp_f32 = src_area.postsynaptic_current();
2485            (psp_f32, psp_f32.clamp(0.0, 255.0) as u8)
2486        };
2487
2488        tracing::debug!(
2489            target: "feagi-bdu",
2490            "Resolved synapse params src={} weight={} psp={} psp_f32={} type={:?}",
2491            src_area_id.as_base_64(),
2492            weight,
2493            psp,
2494            psp_f32,
2495            synapse_type
2496        );
2497
2498        Ok((weight, psp, synapse_type))
2499    }
2500
2501    /// Apply cortical mapping for a specific area pair
2502    fn apply_cortical_mapping_for_pair(
2503        &mut self,
2504        src_area_id: &CorticalID,
2505        dst_area_id: &CorticalID,
2506    ) -> BduResult<usize> {
2507        // Clone the rules to avoid borrow checker issues.
2508        //
2509        // IMPORTANT: absence of mapping rules is a valid state (e.g. mapping deletion).
2510        // In that case, return Ok(0) rather than an error so API callers can treat
2511        // "deleted mapping" as success (and BV can update its cache/UI).
2512        let rules = {
2513            let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2514                crate::types::BduError::InvalidArea(format!(
2515                    "Source area not found: {}",
2516                    src_area_id
2517                ))
2518            })?;
2519
2520            let Some(mapping_dst) = src_area
2521                .properties
2522                .get("cortical_mapping_dst")
2523                .and_then(|v| v.as_object())
2524            else {
2525                return Ok(0);
2526            };
2527
2528            let Some(rules) = Self::get_mapping_rules_for_destination(mapping_dst, dst_area_id)
2529            else {
2530                return Ok(0);
2531            };
2532
2533            rules.clone()
2534        }; // Borrow ends here
2535
2536        if rules.is_empty() {
2537            return Ok(0);
2538        }
2539
2540        // Get indices for STDP handling
2541        let src_cortical_idx = *self.cortical_id_to_idx.get(src_area_id).ok_or_else(|| {
2542            crate::types::BduError::InvalidArea(format!("No index for {}", src_area_id))
2543        })?;
2544        let dst_cortical_idx = *self.cortical_id_to_idx.get(dst_area_id).ok_or_else(|| {
2545            crate::types::BduError::InvalidArea(format!("No index for {}", dst_area_id))
2546        })?;
2547
2548        // Clone NPU Arc for STDP handling (Arc::clone is cheap - just increments ref count)
2549        let npu_arc = self
2550            .npu
2551            .as_ref()
2552            .ok_or_else(|| crate::types::BduError::Internal("NPU not connected".to_string()))?
2553            .clone();
2554
2555        tracing::debug!(
2556            target: "feagi-bdu",
2557            "Applying {} mapping rule(s) for {} -> {}",
2558            rules.len(),
2559            src_area_id,
2560            dst_area_id
2561        );
2562        // Apply each morphology rule
2563        let mut total_synapses = 0;
2564        for rule in &rules {
2565            let morphology_id = if let Some(rule_obj) = rule.as_object() {
2566                rule_obj
2567                    .get("morphology_id")
2568                    .and_then(|v| v.as_str())
2569                    .unwrap_or("unknown")
2570                    .to_string()
2571            } else if let Some(rule_arr) = rule.as_array() {
2572                rule_arr
2573                    .first()
2574                    .and_then(|v| v.as_str())
2575                    .unwrap_or("unknown")
2576                    .to_string()
2577            } else {
2578                "unknown".to_string()
2579            };
2580
2581            let rule_keys: Vec<String> = rule
2582                .as_object()
2583                .map(|obj| obj.keys().cloned().collect())
2584                .unwrap_or_default();
2585
2586            // Handle STDP/plasticity configuration if needed
2587            let mut plasticity_flag = rule
2588                .as_object()
2589                .and_then(|obj| obj.get("plasticity_flag"))
2590                .and_then(|v| v.as_bool())
2591                .unwrap_or(false);
2592            if morphology_id == "associative_memory" {
2593                plasticity_flag = true;
2594            }
2595            if plasticity_flag {
2596                let Some(rule_obj) = rule.as_object() else {
2597                    return Err(crate::types::BduError::InvalidMorphology(
2598                        "Plasticity mapping rule must be an object format".to_string(),
2599                    ));
2600                };
2601                let (_weight, psp, synapse_type) =
2602                    self.resolve_synapse_params_for_rule(src_area_id, rule)?;
2603                let bidirectional_stdp = morphology_id == "associative_memory";
2604                if let Err(e) = Self::register_stdp_mapping_for_rule(
2605                    &npu_arc,
2606                    src_area_id,
2607                    dst_area_id,
2608                    src_cortical_idx,
2609                    dst_cortical_idx,
2610                    rule_obj,
2611                    bidirectional_stdp,
2612                    psp,
2613                    synapse_type,
2614                ) {
2615                    tracing::error!(
2616                        target: "feagi-bdu",
2617                        "STDP mapping registration failed for {} -> {} (morphology={}, keys={:?}): {}",
2618                        src_area_id,
2619                        dst_area_id,
2620                        morphology_id,
2621                        rule_keys,
2622                        e
2623                    );
2624                    return Err(e);
2625                }
2626            }
2627
2628            // Apply the morphology rule
2629            let synapse_count = match self.apply_single_morphology_rule(
2630                src_area_id,
2631                dst_area_id,
2632                rule,
2633            ) {
2634                Ok(count) => count,
2635                Err(e) => {
2636                    tracing::error!(
2637                        target: "feagi-bdu",
2638                        "Mapping rule application failed for {} -> {} (morphology={}, keys={:?}): {}",
2639                        src_area_id,
2640                        dst_area_id,
2641                        morphology_id,
2642                        rule_keys,
2643                        e
2644                    );
2645                    return Err(e);
2646                }
2647            };
2648            total_synapses += synapse_count;
2649            tracing::debug!(
2650                target: "feagi-bdu",
2651                "Rule {} created {} synapses for {} -> {}",
2652                morphology_id,
2653                synapse_count,
2654                src_area_id,
2655                dst_area_id
2656            );
2657        }
2658
2659        Ok(total_synapses)
2660    }
2661
2662    /// Apply a function-type morphology (projector, memory, block_to_block, etc.)
2663    ///
2664    /// This helper consolidates all function-type morphology logic in one place.
2665    /// Function-type morphologies are code-driven and require code changes to add new ones.
2666    ///
2667    /// # Arguments
2668    /// * `morphology_id` - The morphology ID string (e.g., "projector", "block_to_block")
2669    /// * `rule` - The morphology rule JSON value
2670    /// * `npu_arc` - Arc to the NPU (for batched operations)
2671    /// * `npu` - Locked NPU reference
2672    /// * `src_area_id`, `dst_area_id` - Source and destination area IDs
2673    /// * `src_idx`, `dst_idx` - Source and destination area indices
2674    /// * `weight`, `psp`, `synapse_attractivity` - Synapse parameters
2675    #[allow(clippy::too_many_arguments)]
2676    fn apply_function_morphology(
2677        &self,
2678        morphology_id: &str,
2679        rule: &serde_json::Value,
2680        npu_arc: &Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
2681        npu: &mut feagi_npu_burst_engine::DynamicNPU,
2682        src_area_id: &CorticalID,
2683        dst_area_id: &CorticalID,
2684        src_idx: u32,
2685        dst_idx: u32,
2686        weight: u8,
2687        psp: u8,
2688        synapse_attractivity: u8,
2689        synapse_type: feagi_npu_neural::SynapseType,
2690    ) -> BduResult<usize> {
2691        match morphology_id {
2692            "projector" | "transpose_xy" | "transpose_yz" | "transpose_xz" => {
2693                // Get dimensions from cortical areas (no neuron scanning!)
2694                let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2695                    crate::types::BduError::InvalidArea(format!(
2696                        "Source area not found: {}",
2697                        src_area_id
2698                    ))
2699                })?;
2700                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
2701                    crate::types::BduError::InvalidArea(format!(
2702                        "Destination area not found: {}",
2703                        dst_area_id
2704                    ))
2705                })?;
2706
2707                let src_dimensions = (
2708                    src_area.dimensions.width as usize,
2709                    src_area.dimensions.height as usize,
2710                    src_area.dimensions.depth as usize,
2711                );
2712                let dst_dimensions = (
2713                    dst_area.dimensions.width as usize,
2714                    dst_area.dimensions.height as usize,
2715                    dst_area.dimensions.depth as usize,
2716                );
2717
2718                // Legacy-compatible transpose mappings from Python FEAGI:
2719                // projector_xy -> (y, x, z), projector_yz -> (x, z, y), projector_xz -> (z, y, x)
2720                let transpose = match morphology_id {
2721                    "transpose_xy" => Some((1, 0, 2)),
2722                    "transpose_yz" => Some((0, 2, 1)),
2723                    "transpose_xz" => Some((2, 1, 0)),
2724                    _ => None,
2725                };
2726
2727                use crate::connectivity::core_morphologies::apply_projector_morphology_with_dimensions;
2728                let count = apply_projector_morphology_with_dimensions(
2729                    npu,
2730                    src_idx,
2731                    dst_idx,
2732                    src_dimensions,
2733                    dst_dimensions,
2734                    transpose,
2735                    None, // project_last_layer_of
2736                    weight,
2737                    psp,
2738                    synapse_attractivity,
2739                    synapse_type,
2740                )?;
2741                // Ensure the propagation engine sees the newly created synapses immediately
2742                npu.rebuild_synapse_index();
2743                Ok(count as usize)
2744            }
2745            "episodic_memory" => {
2746                // Episodic memory morphology: No physical synapses created
2747                // Pattern detection and memory neuron creation handled by PlasticityService
2748                use tracing::trace;
2749                trace!(
2750                    target: "feagi-bdu",
2751                    "Episodic memory morphology: {} -> {} (no physical synapses, plasticity-driven)",
2752                    src_idx, dst_idx
2753                );
2754                Ok(0)
2755            }
2756            "memory_replay" => {
2757                // Replay mapping: semantic only, no physical synapses
2758                use tracing::trace;
2759                trace!(
2760                    target: "feagi-bdu",
2761                    "Memory replay morphology: {} -> {} (no physical synapses)",
2762                    src_idx, dst_idx
2763                );
2764                Ok(0)
2765            }
2766            "associative_memory" => {
2767                // Associative memory (bi-directional STDP) mapping:
2768                // If both ends are memory areas, create synapses between LIF twins to enable associations.
2769                // Otherwise, no initial synapses are created (STDP will update existing synapses).
2770                let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2771                    crate::types::BduError::InvalidArea(format!(
2772                        "Source area not found: {}",
2773                        src_area_id
2774                    ))
2775                })?;
2776                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
2777                    crate::types::BduError::InvalidArea(format!(
2778                        "Destination area not found: {}",
2779                        dst_area_id
2780                    ))
2781                })?;
2782
2783                if matches!(src_area.cortical_type, CorticalAreaType::Memory(_))
2784                    && matches!(dst_area.cortical_type, CorticalAreaType::Memory(_))
2785                {
2786                    let src_dimensions = (
2787                        src_area.dimensions.width as usize,
2788                        src_area.dimensions.height as usize,
2789                        src_area.dimensions.depth as usize,
2790                    );
2791                    let dst_dimensions = (
2792                        dst_area.dimensions.width as usize,
2793                        dst_area.dimensions.height as usize,
2794                        dst_area.dimensions.depth as usize,
2795                    );
2796                    use crate::connectivity::core_morphologies::apply_projector_morphology_with_dimensions;
2797                    let count = apply_projector_morphology_with_dimensions(
2798                        npu,
2799                        src_idx,
2800                        dst_idx,
2801                        src_dimensions,
2802                        dst_dimensions,
2803                        None,
2804                        None,
2805                        weight,
2806                        psp,
2807                        synapse_attractivity,
2808                        synapse_type,
2809                    )?;
2810                    npu.rebuild_synapse_index();
2811                    Ok(count as usize)
2812                } else {
2813                    Ok(0)
2814                }
2815            }
2816            "block_to_block" => {
2817                tracing::warn!(
2818                    target: "feagi-bdu",
2819                    "🔍 DEBUG apply_function_morphology: block_to_block case reached with src_idx={}, dst_idx={}",
2820                    src_idx, dst_idx
2821                );
2822                // Get dimensions from cortical areas (no neuron scanning!)
2823                let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2824                    crate::types::BduError::InvalidArea(format!(
2825                        "Source area not found: {}",
2826                        src_area_id
2827                    ))
2828                })?;
2829                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
2830                    crate::types::BduError::InvalidArea(format!(
2831                        "Destination area not found: {}",
2832                        dst_area_id
2833                    ))
2834                })?;
2835
2836                let src_dimensions = (
2837                    src_area.dimensions.width as usize,
2838                    src_area.dimensions.height as usize,
2839                    src_area.dimensions.depth as usize,
2840                );
2841                let dst_dimensions = (
2842                    dst_area.dimensions.width as usize,
2843                    dst_area.dimensions.height as usize,
2844                    dst_area.dimensions.depth as usize,
2845                );
2846
2847                // Extract scalar from rule (morphology_scalar)
2848                let scalar = if let Some(obj) = rule.as_object() {
2849                    // Object format: get from morphology_scalar array
2850                    if let Some(scalar_arr) =
2851                        obj.get("morphology_scalar").and_then(|v| v.as_array())
2852                    {
2853                        // Use first element as scalar (or default to 1)
2854                        scalar_arr.first().and_then(|v| v.as_i64()).unwrap_or(1) as u32
2855                    } else {
2856                        1 // @architecture:acceptable - default scalar
2857                    }
2858                } else if let Some(arr) = rule.as_array() {
2859                    // Array format: [morphology_id, scalar, multiplier, ...]
2860                    arr.get(1).and_then(|v| v.as_i64()).unwrap_or(1) as u32
2861                } else {
2862                    1 // @architecture:acceptable - default scalar
2863                };
2864
2865                // CRITICAL: Do NOT call get_neurons_in_cortical_area to check neuron count!
2866                // Use dimensions to estimate: if area is large, use batched version
2867                let estimated_neurons = src_dimensions.0 * src_dimensions.1 * src_dimensions.2;
2868                let count = if estimated_neurons > 100_000 {
2869                    // Release lock and use batched version
2870                    let _ = npu;
2871
2872                    crate::connectivity::synaptogenesis::apply_block_connection_morphology_batched(
2873                        npu_arc,
2874                        src_idx,
2875                        dst_idx,
2876                        src_dimensions,
2877                        dst_dimensions,
2878                        scalar, // scaling_factor
2879                        weight,
2880                        psp,
2881                        synapse_attractivity,
2882                        synapse_type,
2883                    )? as usize
2884                } else {
2885                    // Small area: use regular version (faster for small counts)
2886                    tracing::warn!(
2887                        target: "feagi-bdu",
2888                        "🔍 DEBUG connectome_manager: Calling apply_block_connection_morphology with src_idx={}, dst_idx={}, src_dim={:?}, dst_dim={:?}",
2889                        src_idx, dst_idx, src_dimensions, dst_dimensions
2890                    );
2891                    let count =
2892                        crate::connectivity::synaptogenesis::apply_block_connection_morphology(
2893                            npu,
2894                            src_idx,
2895                            dst_idx,
2896                            src_dimensions,
2897                            dst_dimensions,
2898                            scalar, // scaling_factor
2899                            weight,
2900                            psp,
2901                            synapse_attractivity,
2902                            synapse_type,
2903                        )? as usize;
2904                    tracing::warn!(
2905                        target: "feagi-bdu",
2906                        "🔍 DEBUG connectome_manager: apply_block_connection_morphology returned count={}",
2907                        count
2908                    );
2909                    // Rebuild synapse index while we still have the lock
2910                    if count > 0 {
2911                        npu.rebuild_synapse_index();
2912                    }
2913                    count
2914                };
2915
2916                // Ensure the propagation engine sees the newly created synapses immediately (batched version only)
2917                if count > 0 && estimated_neurons > 100_000 {
2918                    let mut npu_lock = npu_arc.lock().unwrap();
2919                    npu_lock.rebuild_synapse_index();
2920                }
2921
2922                Ok(count)
2923            }
2924            "bitmask_encoder_x" | "bitmask_encoder_y" | "bitmask_encoder_z"
2925            | "bitmask_decoder_x" | "bitmask_decoder_y" | "bitmask_decoder_z" => {
2926                let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2927                    crate::types::BduError::InvalidArea(format!(
2928                        "Source area not found: {}",
2929                        src_area_id
2930                    ))
2931                })?;
2932                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
2933                    crate::types::BduError::InvalidArea(format!(
2934                        "Destination area not found: {}",
2935                        dst_area_id
2936                    ))
2937                })?;
2938
2939                let src_dimensions = (
2940                    src_area.dimensions.width as usize,
2941                    src_area.dimensions.height as usize,
2942                    src_area.dimensions.depth as usize,
2943                );
2944                let dst_dimensions = (
2945                    dst_area.dimensions.width as usize,
2946                    dst_area.dimensions.height as usize,
2947                    dst_area.dimensions.depth as usize,
2948                );
2949
2950                let (axis, mode) = match morphology_id {
2951                    "bitmask_encoder_x" => (
2952                        crate::connectivity::core_morphologies::BitmaskAxis::X,
2953                        crate::connectivity::core_morphologies::BitmaskMode::Encoder,
2954                    ),
2955                    "bitmask_encoder_y" => (
2956                        crate::connectivity::core_morphologies::BitmaskAxis::Y,
2957                        crate::connectivity::core_morphologies::BitmaskMode::Encoder,
2958                    ),
2959                    "bitmask_encoder_z" => (
2960                        crate::connectivity::core_morphologies::BitmaskAxis::Z,
2961                        crate::connectivity::core_morphologies::BitmaskMode::Encoder,
2962                    ),
2963                    "bitmask_decoder_x" => (
2964                        crate::connectivity::core_morphologies::BitmaskAxis::X,
2965                        crate::connectivity::core_morphologies::BitmaskMode::Decoder,
2966                    ),
2967                    "bitmask_decoder_y" => (
2968                        crate::connectivity::core_morphologies::BitmaskAxis::Y,
2969                        crate::connectivity::core_morphologies::BitmaskMode::Decoder,
2970                    ),
2971                    "bitmask_decoder_z" => (
2972                        crate::connectivity::core_morphologies::BitmaskAxis::Z,
2973                        crate::connectivity::core_morphologies::BitmaskMode::Decoder,
2974                    ),
2975                    _ => unreachable!("matched bitmask morphology above"),
2976                };
2977
2978                let count =
2979                    crate::connectivity::core_morphologies::apply_bitmask_morphology_with_dimensions(
2980                        npu,
2981                        src_idx,
2982                        dst_idx,
2983                        src_dimensions,
2984                        dst_dimensions,
2985                        axis,
2986                        mode,
2987                        weight,
2988                        psp,
2989                        synapse_attractivity,
2990                        synapse_type,
2991                    )?;
2992                if count > 0 {
2993                    npu.rebuild_synapse_index();
2994                }
2995                Ok(count as usize)
2996            }
2997            "sweeper" => {
2998                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
2999                    crate::types::BduError::InvalidArea(format!(
3000                        "Destination area not found: {}",
3001                        dst_area_id
3002                    ))
3003                })?;
3004                let dst_dimensions = (
3005                    dst_area.dimensions.width as usize,
3006                    dst_area.dimensions.height as usize,
3007                    dst_area.dimensions.depth as usize,
3008                );
3009
3010                let count =
3011                    crate::connectivity::core_morphologies::apply_sweeper_morphology_with_dimensions(
3012                        npu,
3013                        src_idx,
3014                        dst_idx,
3015                        dst_dimensions,
3016                        weight,
3017                        psp,
3018                        synapse_attractivity,
3019                        synapse_type,
3020                    )?;
3021                if count > 0 {
3022                    npu.rebuild_synapse_index();
3023                }
3024                Ok(count as usize)
3025            }
3026            "last_to_first" => {
3027                let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3028                    crate::types::BduError::InvalidArea(format!(
3029                        "Source area not found: {}",
3030                        src_area_id
3031                    ))
3032                })?;
3033                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3034                    crate::types::BduError::InvalidArea(format!(
3035                        "Destination area not found: {}",
3036                        dst_area_id
3037                    ))
3038                })?;
3039                let src_dimensions = (
3040                    src_area.dimensions.width as usize,
3041                    src_area.dimensions.height as usize,
3042                    src_area.dimensions.depth as usize,
3043                );
3044                let dst_dimensions = (
3045                    dst_area.dimensions.width as usize,
3046                    dst_area.dimensions.height as usize,
3047                    dst_area.dimensions.depth as usize,
3048                );
3049
3050                let count = crate::connectivity::core_morphologies::apply_last_to_first_morphology_with_dimensions(
3051                    npu,
3052                    src_idx,
3053                    dst_idx,
3054                    src_dimensions,
3055                    dst_dimensions,
3056                    weight,
3057                    psp,
3058                    synapse_attractivity,
3059                    synapse_type,
3060                )?;
3061                if count > 0 {
3062                    npu.rebuild_synapse_index();
3063                }
3064                Ok(count as usize)
3065            }
3066            "rotator_z" => {
3067                let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3068                    crate::types::BduError::InvalidArea(format!(
3069                        "Source area not found: {}",
3070                        src_area_id
3071                    ))
3072                })?;
3073                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3074                    crate::types::BduError::InvalidArea(format!(
3075                        "Destination area not found: {}",
3076                        dst_area_id
3077                    ))
3078                })?;
3079                let src_dimensions = (
3080                    src_area.dimensions.width as usize,
3081                    src_area.dimensions.height as usize,
3082                    src_area.dimensions.depth as usize,
3083                );
3084                let dst_dimensions = (
3085                    dst_area.dimensions.width as usize,
3086                    dst_area.dimensions.height as usize,
3087                    dst_area.dimensions.depth as usize,
3088                );
3089
3090                let count = crate::connectivity::core_morphologies::apply_rotator_z_morphology_with_dimensions(
3091                    npu,
3092                    src_idx,
3093                    dst_idx,
3094                    src_dimensions,
3095                    dst_dimensions,
3096                    weight,
3097                    psp,
3098                    synapse_attractivity,
3099                    synapse_type,
3100                )?;
3101                if count > 0 {
3102                    npu.rebuild_synapse_index();
3103                }
3104                Ok(count as usize)
3105            }
3106            _ => {
3107                // Other function morphologies not yet implemented
3108                // NOTE: To add a new function-type morphology, add a case here
3109                use tracing::debug;
3110                debug!(target: "feagi-bdu", "Function morphology {} not yet implemented", morphology_id);
3111                Ok(0)
3112            }
3113        }
3114    }
3115
3116    /// Apply a single morphology rule
3117    fn apply_single_morphology_rule(
3118        &mut self,
3119        src_area_id: &CorticalID,
3120        dst_area_id: &CorticalID,
3121        rule: &serde_json::Value,
3122    ) -> BduResult<usize> {
3123        // Extract morphology_id from rule (array or dict format)
3124        let morphology_id = if let Some(arr) = rule.as_array() {
3125            arr.first().and_then(|v| v.as_str()).unwrap_or("")
3126        } else if let Some(obj) = rule.as_object() {
3127            obj.get("morphology_id")
3128                .and_then(|v| v.as_str())
3129                .unwrap_or("")
3130        } else {
3131            return Ok(0);
3132        };
3133
3134        if morphology_id.is_empty() {
3135            return Ok(0);
3136        }
3137
3138        // Get morphology from registry
3139        let morphology = self.morphology_registry.get(morphology_id).ok_or_else(|| {
3140            crate::types::BduError::InvalidMorphology(format!(
3141                "Morphology not found: {}",
3142                morphology_id
3143            ))
3144        })?;
3145
3146        // Convert area IDs to cortical indices (required by NPU functions)
3147        let src_idx = self.cortical_id_to_idx.get(src_area_id).ok_or_else(|| {
3148            crate::types::BduError::InvalidArea(format!(
3149                "Source area ID not found: {}",
3150                src_area_id
3151            ))
3152        })?;
3153        let dst_idx = self.cortical_id_to_idx.get(dst_area_id).ok_or_else(|| {
3154            crate::types::BduError::InvalidArea(format!(
3155                "Destination area ID not found: {}",
3156                dst_area_id
3157            ))
3158        })?;
3159
3160        // Apply morphology based on type
3161        if let Some(ref npu_arc) = self.npu {
3162            let lock_start = std::time::Instant::now();
3163            let mut npu = npu_arc.lock().unwrap();
3164            let lock_wait = lock_start.elapsed();
3165            tracing::debug!(
3166                target: "feagi-bdu",
3167                "[NPU-LOCK] synaptogenesis lock wait {:.2}ms for {} -> {} (morphology={})",
3168                lock_wait.as_secs_f64() * 1000.0,
3169                src_area_id,
3170                dst_area_id,
3171                morphology_id
3172            );
3173
3174            let (weight, psp, synapse_type) =
3175                self.resolve_synapse_params_for_rule(src_area_id, rule)?;
3176
3177            // Extract synapse_attractivity from rule (probability 0-100)
3178            let synapse_attractivity = if let Some(obj) = rule.as_object() {
3179                obj.get("synapse_attractivity")
3180                    .and_then(|v| v.as_u64())
3181                    .unwrap_or(100) as u8
3182            } else {
3183                100 // @architecture:acceptable - default to always create when not specified
3184            };
3185
3186            match morphology.morphology_type {
3187                feagi_evolutionary::MorphologyType::Functions => {
3188                    tracing::warn!(
3189                        target: "feagi-bdu",
3190                        "🔍 DEBUG apply_single_morphology_rule: Functions type, morphology_id={}, calling apply_function_morphology",
3191                        morphology_id
3192                    );
3193                    // Function-based morphologies (projector, memory, block_to_block, etc.)
3194                    // Delegate to helper function to consolidate all function-type logic
3195                    self.apply_function_morphology(
3196                        morphology_id,
3197                        rule,
3198                        npu_arc,
3199                        &mut npu,
3200                        src_area_id,
3201                        dst_area_id,
3202                        *src_idx,
3203                        *dst_idx,
3204                        weight,
3205                        psp,
3206                        synapse_attractivity,
3207                        synapse_type,
3208                    )
3209                }
3210                feagi_evolutionary::MorphologyType::Vectors => {
3211                    use crate::connectivity::synaptogenesis::apply_vectors_morphology_with_dimensions;
3212
3213                    // Get dimensions from cortical areas (no neuron scanning!)
3214                    let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3215                        crate::types::BduError::InvalidArea(format!(
3216                            "Destination area not found: {}",
3217                            dst_area_id
3218                        ))
3219                    })?;
3220
3221                    let dst_dimensions = (
3222                        dst_area.dimensions.width as usize,
3223                        dst_area.dimensions.height as usize,
3224                        dst_area.dimensions.depth as usize,
3225                    );
3226
3227                    if let feagi_evolutionary::MorphologyParameters::Vectors { ref vectors } =
3228                        morphology.parameters
3229                    {
3230                        // Convert Vec<[i32; 3]> to Vec<(i32, i32, i32)>
3231                        let vectors_tuples: Vec<(i32, i32, i32)> =
3232                            vectors.iter().map(|v| (v[0], v[1], v[2])).collect();
3233
3234                        let count = apply_vectors_morphology_with_dimensions(
3235                            &mut npu,
3236                            *src_idx,
3237                            *dst_idx,
3238                            vectors_tuples,
3239                            dst_dimensions,
3240                            weight,               // From rule, not hardcoded
3241                            psp,                  // PSP from source area, NOT hardcoded!
3242                            synapse_attractivity, // From rule, not hardcoded
3243                            synapse_type,
3244                        )?;
3245                        // Ensure the propagation engine sees the newly created synapses immediately,
3246                        // and avoid a second outer NPU mutex acquisition later in the mapping update path.
3247                        npu.rebuild_synapse_index();
3248                        Ok(count as usize)
3249                    } else {
3250                        Ok(0)
3251                    }
3252                }
3253                feagi_evolutionary::MorphologyType::Patterns => {
3254                    use crate::connectivity::core_morphologies::apply_patterns_morphology;
3255                    use crate::connectivity::rules::patterns::{
3256                        Pattern3D, PatternElement as RulePatternElement,
3257                    };
3258                    use feagi_evolutionary::PatternElement as EvoPatternElement;
3259
3260                    let feagi_evolutionary::MorphologyParameters::Patterns { ref patterns } =
3261                        morphology.parameters
3262                    else {
3263                        return Ok(0);
3264                    };
3265
3266                    let convert_element =
3267                        |element: &EvoPatternElement|
3268                         -> crate::types::BduResult<RulePatternElement> {
3269                            match element {
3270                                EvoPatternElement::Value(value) => {
3271                                    if *value < 0 {
3272                                        return Err(crate::types::BduError::InvalidMorphology(
3273                                            format!(
3274                                                "Pattern morphology {} contains negative voxel coordinate {}",
3275                                                morphology_id, value
3276                                            ),
3277                                        ));
3278                                    }
3279                                    Ok(RulePatternElement::Exact(*value))
3280                                }
3281                                EvoPatternElement::Wildcard => Ok(RulePatternElement::Wildcard),
3282                                EvoPatternElement::Skip => Ok(RulePatternElement::Skip),
3283                                EvoPatternElement::Exclude => Ok(RulePatternElement::Exclude),
3284                            }
3285                        };
3286
3287                    let mut converted_patterns = Vec::with_capacity(patterns.len());
3288                    for pattern_pair in patterns {
3289                        if pattern_pair.len() != 2 {
3290                            return Err(crate::types::BduError::InvalidMorphology(format!(
3291                                "Pattern morphology {} must contain [src, dst] pairs",
3292                                morphology_id
3293                            )));
3294                        }
3295
3296                        let src_pattern = &pattern_pair[0];
3297                        let dst_pattern = &pattern_pair[1];
3298
3299                        if src_pattern.len() != 3 || dst_pattern.len() != 3 {
3300                            return Err(crate::types::BduError::InvalidMorphology(format!(
3301                                "Pattern morphology {} requires 3-axis patterns",
3302                                morphology_id
3303                            )));
3304                        }
3305
3306                        let src: Pattern3D = (
3307                            convert_element(&src_pattern[0])?,
3308                            convert_element(&src_pattern[1])?,
3309                            convert_element(&src_pattern[2])?,
3310                        );
3311                        let dst: Pattern3D = (
3312                            convert_element(&dst_pattern[0])?,
3313                            convert_element(&dst_pattern[1])?,
3314                            convert_element(&dst_pattern[2])?,
3315                        );
3316
3317                        converted_patterns.push((src, dst));
3318                    }
3319
3320                    let count = apply_patterns_morphology(
3321                        &mut npu,
3322                        *src_idx,
3323                        *dst_idx,
3324                        converted_patterns,
3325                        weight,
3326                        psp,
3327                        synapse_attractivity,
3328                        synapse_type,
3329                    )?;
3330                    if count > 0 {
3331                        npu.rebuild_synapse_index();
3332                    }
3333                    Ok(count as usize)
3334                }
3335                feagi_evolutionary::MorphologyType::Composite => {
3336                    let feagi_evolutionary::MorphologyParameters::Composite { .. } =
3337                        morphology.parameters
3338                    else {
3339                        return Ok(0);
3340                    };
3341
3342                    if morphology_id != "tile" {
3343                        use tracing::debug;
3344                        debug!(
3345                            target: "feagi-bdu",
3346                            "Composite morphology {} not yet implemented",
3347                            morphology_id
3348                        );
3349                        return Ok(0);
3350                    }
3351
3352                    let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3353                        crate::types::BduError::InvalidArea(format!(
3354                            "Source area not found: {}",
3355                            src_area_id
3356                        ))
3357                    })?;
3358                    let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3359                        crate::types::BduError::InvalidArea(format!(
3360                            "Destination area not found: {}",
3361                            dst_area_id
3362                        ))
3363                    })?;
3364                    let src_dimensions = (
3365                        src_area.dimensions.width as usize,
3366                        src_area.dimensions.height as usize,
3367                        src_area.dimensions.depth as usize,
3368                    );
3369                    let dst_dimensions = (
3370                        dst_area.dimensions.width as usize,
3371                        dst_area.dimensions.height as usize,
3372                        dst_area.dimensions.depth as usize,
3373                    );
3374
3375                    let count =
3376                        crate::connectivity::core_morphologies::apply_tile_morphology_with_dimensions(
3377                            &mut npu,
3378                            *src_idx,
3379                            *dst_idx,
3380                            src_dimensions,
3381                            dst_dimensions,
3382                            weight,
3383                            psp,
3384                            synapse_attractivity,
3385                            synapse_type,
3386                        )?;
3387                    if count > 0 {
3388                        npu.rebuild_synapse_index();
3389                    }
3390                    Ok(count as usize)
3391                }
3392            }
3393        } else {
3394            Ok(0) // NPU not available
3395        }
3396    }
3397
3398    // ======================================================================
3399    // NPU Integration
3400    // ======================================================================
3401
3402    /// Set the NPU reference for neuron/synapse queries
3403    ///
3404    /// This should be called once during FEAGI initialization after the NPU is created.
3405    ///
3406    /// # Arguments
3407    ///
3408    /// * `npu` - Arc to the Rust NPU (wrapped in TracingMutex for automatic lock tracing)
3409    ///
3410    pub fn set_npu(
3411        &mut self,
3412        npu: Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
3413    ) {
3414        self.npu = Some(Arc::clone(&npu));
3415        info!(target: "feagi-bdu","🔗 ConnectomeManager: NPU reference set");
3416
3417        // CRITICAL: Update State Manager with capacity values (from config, never changes)
3418        // This ensures health check endpoint can read capacity without acquiring NPU lock
3419        #[cfg(not(feature = "wasm"))]
3420        {
3421            use feagi_state_manager::StateManager;
3422            let state_manager = StateManager::instance();
3423            let state_manager = state_manager.read();
3424            let core_state = state_manager.get_core_state();
3425            // Capacity comes from config (set at initialization, never changes)
3426            core_state.set_neuron_capacity(self.config.max_neurons as u32);
3427            core_state.set_synapse_capacity(self.config.max_synapses as u32);
3428            info!(
3429                target: "feagi-bdu",
3430                "📊 Updated State Manager with capacity: {} neurons, {} synapses",
3431                self.config.max_neurons, self.config.max_synapses
3432            );
3433        }
3434
3435        // CRITICAL: Backfill cortical area registrations into NPU.
3436        //
3437        // Cortical areas can be created/loaded before the NPU is attached (startup ordering).
3438        // Those areas won't be registered via `add_cortical_area()` (it registers only if NPU is present),
3439        // which causes visualization encoding to fall back to "area_{idx}" and subsequently drop the area
3440        // (base64 decode fails), making BV appear to "miss" firing activity for that cortical area.
3441        let existing_area_count = self.cortical_id_to_idx.len();
3442        if existing_area_count > 0 {
3443            match npu.lock() {
3444                Ok(mut npu_lock) => {
3445                    for (cortical_id, cortical_idx) in self.cortical_id_to_idx.iter() {
3446                        npu_lock.register_cortical_area(*cortical_idx, cortical_id.as_base_64());
3447                    }
3448                    info!(
3449                        target: "feagi-bdu",
3450                        "🔁 Backfilled {} cortical area registrations into NPU",
3451                        existing_area_count
3452                    );
3453                }
3454                Err(e) => {
3455                    warn!(
3456                        target: "feagi-bdu",
3457                        "⚠️ Failed to lock NPU for cortical area backfill registration: {}",
3458                        e
3459                    );
3460                }
3461            }
3462        }
3463
3464        // Initialize cached stats immediately
3465        self.update_all_cached_stats();
3466        info!(target: "feagi-bdu","📊 Initialized cached stats: {} neurons, {} synapses",
3467            self.get_neuron_count(), self.get_synapse_count());
3468    }
3469
3470    /// Check if NPU is connected
3471    pub fn has_npu(&self) -> bool {
3472        self.npu.is_some()
3473    }
3474
3475    /// Get NPU reference (read-only access for queries)
3476    ///
3477    /// # Returns
3478    ///
3479    /// * `Option<&Arc<Mutex<RustNPU>>>` - Reference to NPU if connected
3480    ///
3481    pub fn get_npu(
3482        &self,
3483    ) -> Option<&Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>>
3484    {
3485        self.npu.as_ref()
3486    }
3487
3488    /// Set the PlasticityExecutor reference (optional, only if plasticity feature enabled)
3489    /// The executor is passed as Arc<Mutex<dyn Any>> for feature-gating compatibility
3490    #[cfg(feature = "plasticity")]
3491    pub fn set_plasticity_executor(
3492        &mut self,
3493        executor: Arc<std::sync::Mutex<feagi_npu_plasticity::AsyncPlasticityExecutor>>,
3494    ) {
3495        self.plasticity_executor = Some(executor);
3496        info!(target: "feagi-bdu", "🔗 ConnectomeManager: PlasticityExecutor reference set");
3497    }
3498
3499    /// Get the PlasticityExecutor reference (if plasticity feature enabled)
3500    #[cfg(feature = "plasticity")]
3501    pub fn get_plasticity_executor(
3502        &self,
3503    ) -> Option<&Arc<std::sync::Mutex<feagi_npu_plasticity::AsyncPlasticityExecutor>>> {
3504        self.plasticity_executor.as_ref()
3505    }
3506
3507    /// Get neuron capacity from config (lock-free, never acquires NPU lock)
3508    ///
3509    /// # Returns
3510    ///
3511    /// * `usize` - Maximum neuron capacity from config (single source of truth)
3512    ///
3513    /// # Performance
3514    ///
3515    /// This is a lock-free read from config that never blocks, even during burst processing.
3516    /// Capacity values are set at NPU initialization and never change.
3517    ///
3518    pub fn get_neuron_capacity(&self) -> usize {
3519        // CRITICAL: Read from config, NOT NPU - capacity never changes and should not acquire locks
3520        self.config.max_neurons
3521    }
3522
3523    /// Get synapse capacity from config (lock-free, never acquires NPU lock)
3524    ///
3525    /// # Returns
3526    ///
3527    /// * `usize` - Maximum synapse capacity from config (single source of truth)
3528    ///
3529    /// # Performance
3530    ///
3531    /// This is a lock-free read from config that never blocks, even during burst processing.
3532    /// Capacity values are set at NPU initialization and never change.
3533    ///
3534    pub fn get_synapse_capacity(&self) -> usize {
3535        // CRITICAL: Read from config, NOT NPU - capacity never changes and should not acquire locks
3536        self.config.max_synapses
3537    }
3538
3539    /// Update fatigue index based on utilization of neuron and synapse arrays
3540    ///
3541    /// Calculates fatigue index as max(regular_neuron_util%, memory_neuron_util%, synapse_util%)
3542    /// Applies hysteresis: triggers at 85%, clears at 80%
3543    /// Rate limited to max once per 2 seconds to protect against rapid changes
3544    ///
3545    /// # Safety
3546    ///
3547    /// This method is completely non-blocking and safe to call during genome loading.
3548    /// If StateManager is unavailable or locked, it will skip the calculation gracefully.
3549    ///
3550    /// # Returns
3551    ///
3552    /// * `Option<u8>` - New fatigue index (0-100) if calculation was performed, None if rate limited or StateManager unavailable
3553    pub fn update_fatigue_index(&self) -> Option<u8> {
3554        // Rate limiting: max once per 2 seconds
3555        let mut last_calc = match self.last_fatigue_calculation.lock() {
3556            Ok(guard) => guard,
3557            Err(_) => return None, // Lock poisoned, skip calculation
3558        };
3559
3560        let now = std::time::Instant::now();
3561        if now.duration_since(*last_calc).as_secs() < 2 {
3562            return None; // Rate limited
3563        }
3564        *last_calc = now;
3565        drop(last_calc);
3566
3567        // Get regular neuron utilization
3568        let regular_neuron_count = self.get_neuron_count();
3569        let regular_neuron_capacity = self.get_neuron_capacity();
3570        let regular_neuron_util = if regular_neuron_capacity > 0 {
3571            ((regular_neuron_count as f64 / regular_neuron_capacity as f64) * 100.0).round() as u8
3572        } else {
3573            0
3574        };
3575
3576        // Get memory neuron utilization from state manager
3577        // Use try_read() to avoid blocking during neurogenesis
3578        // If StateManager singleton initialization fails or is locked, skip calculation entirely
3579        let memory_neuron_util = match StateManager::instance().try_read() {
3580            Some(state_manager) => state_manager.get_core_state().get_memory_neuron_util(),
3581            None => {
3582                // StateManager is locked or not ready - skip fatigue calculation
3583                return None;
3584            }
3585        };
3586
3587        // Get synapse utilization
3588        let synapse_count = self.get_synapse_count();
3589        let synapse_capacity = self.get_synapse_capacity();
3590        let synapse_util = if synapse_capacity > 0 {
3591            ((synapse_count as f64 / synapse_capacity as f64) * 100.0).round() as u8
3592        } else {
3593            0
3594        };
3595
3596        // Calculate fatigue index as max of all utilizations
3597        let fatigue_index = regular_neuron_util
3598            .max(memory_neuron_util)
3599            .max(synapse_util);
3600
3601        // Apply hysteresis: trigger at 85%, clear at 80%
3602        let current_fatigue_active = {
3603            // Try to read current state - if unavailable, assume false
3604            StateManager::instance()
3605                .try_read()
3606                .map(|m| m.get_core_state().is_fatigue_active())
3607                .unwrap_or(false)
3608        };
3609
3610        let new_fatigue_active = if fatigue_index >= 85 {
3611            true
3612        } else if fatigue_index < 80 {
3613            false
3614        } else {
3615            current_fatigue_active // Keep current state in hysteresis zone
3616        };
3617
3618        // Update state manager with all values
3619        // Use try_write() to avoid blocking during neurogenesis
3620        // If StateManager is unavailable, skip update (non-blocking)
3621        if let Some(state_manager) = StateManager::instance().try_write() {
3622            let core_state = state_manager.get_core_state();
3623            core_state.set_fatigue_index(fatigue_index);
3624            core_state.set_fatigue_active(new_fatigue_active);
3625            core_state.set_regular_neuron_util(regular_neuron_util);
3626            core_state.set_memory_neuron_util(memory_neuron_util);
3627            core_state.set_synapse_util(synapse_util);
3628        } else {
3629            // StateManager is locked or not ready - skip update (non-blocking)
3630            trace!(target: "feagi-bdu", "[FATIGUE] StateManager unavailable, skipping update");
3631        }
3632
3633        // Update NPU's atomic boolean
3634        if let Some(ref npu) = self.npu {
3635            if let Ok(mut npu_lock) = npu.lock() {
3636                npu_lock.set_fatigue_active(new_fatigue_active);
3637            }
3638        }
3639
3640        trace!(
3641            target: "feagi-bdu",
3642            "[FATIGUE] Index={}, Active={}, Regular={}%, Memory={}%, Synapse={}%",
3643            fatigue_index, new_fatigue_active, regular_neuron_util, memory_neuron_util, synapse_util
3644        );
3645
3646        Some(fatigue_index)
3647    }
3648
3649    // ======================================================================
3650    // Neuron/Synapse Creation Methods (Delegates to NPU)
3651    // ======================================================================
3652
3653    /// Create neurons for a cortical area
3654    ///
3655    /// This delegates to the NPU's optimized batch creation function.
3656    ///
3657    /// # Arguments
3658    ///
3659    /// * `cortical_id` - Cortical area ID (6-character string)
3660    ///
3661    /// # Returns
3662    ///
3663    /// Number of neurons created
3664    ///
3665    pub fn create_neurons_for_area(&mut self, cortical_id: &CorticalID) -> BduResult<u32> {
3666        // Get cortical area
3667        let area = self
3668            .cortical_areas
3669            .get(cortical_id)
3670            .ok_or_else(|| {
3671                BduError::InvalidArea(format!("Cortical area {} not found", cortical_id))
3672            })?
3673            .clone();
3674
3675        // Get cortical index
3676        let cortical_idx = self.cortical_id_to_idx.get(cortical_id).ok_or_else(|| {
3677            BduError::InvalidArea(format!("No index for cortical area {}", cortical_id))
3678        })?;
3679
3680        // Get NPU
3681        let npu = self
3682            .npu
3683            .as_ref()
3684            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
3685
3686        // Extract neural parameters from area properties using CorticalAreaExt trait
3687        // This ensures consistent defaults across the codebase
3688        use crate::models::CorticalAreaExt;
3689        let per_voxel_cnt = area.neurons_per_voxel();
3690        let firing_threshold = area.firing_threshold();
3691        let firing_threshold_increment_x = area.firing_threshold_increment_x();
3692        let firing_threshold_increment_y = area.firing_threshold_increment_y();
3693        let firing_threshold_increment_z = area.firing_threshold_increment_z();
3694        // SIMD-friendly encoding: 0.0 means no limit, convert to MAX
3695        let firing_threshold_limit_raw = area.firing_threshold_limit();
3696        let firing_threshold_limit = if firing_threshold_limit_raw == 0.0 {
3697            f32::MAX // SIMD-friendly encoding: MAX = no limit
3698        } else {
3699            firing_threshold_limit_raw
3700        };
3701
3702        // DEBUG: Log the increment values
3703        if firing_threshold_increment_x != 0.0
3704            || firing_threshold_increment_y != 0.0
3705            || firing_threshold_increment_z != 0.0
3706        {
3707            info!(
3708                target: "feagi-bdu",
3709                "🔍 [DEBUG] Area {}: firing_threshold_increment = [{}, {}, {}]",
3710                cortical_id.as_base_64(),
3711                firing_threshold_increment_x,
3712                firing_threshold_increment_y,
3713                firing_threshold_increment_z
3714            );
3715        } else {
3716            // Check if properties exist but are just 0
3717            if area.properties.contains_key("firing_threshold_increment_x")
3718                || area.properties.contains_key("firing_threshold_increment_y")
3719                || area.properties.contains_key("firing_threshold_increment_z")
3720            {
3721                info!(
3722                    target: "feagi-bdu",
3723                    "🔍 [DEBUG] Area {}: INCREMENT PROPERTIES FOUND: x={:?}, y={:?}, z={:?}",
3724                    cortical_id.as_base_64(),
3725                    area.properties.get("firing_threshold_increment_x"),
3726                    area.properties.get("firing_threshold_increment_y"),
3727                    area.properties.get("firing_threshold_increment_z")
3728                );
3729            }
3730        }
3731
3732        let leak_coefficient = area.leak_coefficient();
3733        let excitability = area.neuron_excitability();
3734        let refractory_period = area.refractory_period();
3735        // SIMD-friendly encoding: 0 means no limit, convert to MAX
3736        let consecutive_fire_limit_raw = area.consecutive_fire_count() as u16;
3737        let consecutive_fire_limit = if consecutive_fire_limit_raw == 0 {
3738            u16::MAX // SIMD-friendly encoding: MAX = no limit
3739        } else {
3740            consecutive_fire_limit_raw
3741        };
3742        let snooze_length = area.snooze_period();
3743        let mp_charge_accumulation = area.mp_charge_accumulation();
3744
3745        // Calculate expected neuron count for logging
3746        let voxels = area.dimensions.width as usize
3747            * area.dimensions.height as usize
3748            * area.dimensions.depth as usize;
3749        let expected_neurons = voxels * per_voxel_cnt as usize;
3750
3751        trace!(
3752            target: "feagi-bdu",
3753            "Creating neurons for area {}: {}x{}x{} voxels × {} neurons/voxel = {} total neurons",
3754            cortical_id.as_base_64(),
3755            area.dimensions.width,
3756            area.dimensions.height,
3757            area.dimensions.depth,
3758            per_voxel_cnt,
3759            expected_neurons
3760        );
3761
3762        // Call NPU to create neurons
3763        // NOTE: Cortical area should already be registered in NPU during corticogenesis
3764        let mut npu_lock = npu
3765            .lock()
3766            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
3767
3768        let neuron_count = npu_lock
3769            .create_cortical_area_neurons(
3770                *cortical_idx,
3771                area.dimensions.width,
3772                area.dimensions.height,
3773                area.dimensions.depth,
3774                per_voxel_cnt,
3775                firing_threshold,
3776                firing_threshold_increment_x,
3777                firing_threshold_increment_y,
3778                firing_threshold_increment_z,
3779                firing_threshold_limit,
3780                leak_coefficient,
3781                0.0, // resting_potential (LIF default)
3782                0,   // neuron_type (excitatory)
3783                refractory_period,
3784                excitability,
3785                consecutive_fire_limit,
3786                snooze_length,
3787                mp_charge_accumulation,
3788            )
3789            .map_err(|e| BduError::Internal(format!("NPU neuron creation failed: {}", e)))?;
3790
3791        trace!(
3792            target: "feagi-bdu",
3793            "Created {} neurons for area {} via NPU",
3794            neuron_count,
3795            cortical_id.as_base_64()
3796        );
3797
3798        // CRITICAL: Update per-area neuron count cache (lock-free for readers)
3799        // This allows healthcheck endpoints to read counts without NPU lock
3800        {
3801            let mut cache = self.cached_neuron_counts_per_area.write();
3802            cache
3803                .entry(*cortical_id)
3804                .or_insert_with(|| AtomicUsize::new(0))
3805                .store(neuron_count as usize, Ordering::Relaxed);
3806        }
3807
3808        // @cursor:critical-path - Keep BV-facing stats in StateManager.
3809        let state_manager = StateManager::instance();
3810        let state_manager = state_manager.read();
3811        state_manager
3812            .set_cortical_area_neuron_count(&cortical_id.as_base_64(), neuron_count as usize);
3813
3814        // Update total neuron count cache
3815        self.cached_neuron_count
3816            .fetch_add(neuron_count as usize, Ordering::Relaxed);
3817
3818        // CRITICAL: Update StateManager neuron count (for health_check endpoint)
3819        let state_manager = StateManager::instance();
3820        let state_manager = state_manager.read();
3821        let core_state = state_manager.get_core_state();
3822        core_state.add_neuron_count(neuron_count);
3823        core_state.add_regular_neuron_count(neuron_count);
3824
3825        // Trigger fatigue index recalculation after neuron creation
3826        // NOTE: Disabled during genome loading to prevent blocking
3827        // Fatigue calculation will be enabled after genome loading completes
3828        // if neuron_count > 0 {
3829        //     let _ = self.update_fatigue_index();
3830        // }
3831
3832        Ok(neuron_count)
3833    }
3834
3835    /// Add a single neuron to a cortical area
3836    ///
3837    /// # Arguments
3838    ///
3839    /// * `cortical_id` - Cortical area ID
3840    /// * `x` - X coordinate
3841    /// * `y` - Y coordinate
3842    /// * `z` - Z coordinate
3843    /// * `firing_threshold` - Firing threshold (minimum MP to fire)
3844    /// * `firing_threshold_limit` - Firing threshold limit (maximum MP to fire, 0 = no limit)
3845    /// * `leak_coefficient` - Leak coefficient
3846    /// * `resting_potential` - Resting membrane potential
3847    /// * `neuron_type` - Neuron type (0=excitatory, 1=inhibitory)
3848    /// * `refractory_period` - Refractory period
3849    /// * `excitability` - Excitability multiplier
3850    /// * `consecutive_fire_limit` - Maximum consecutive fires
3851    /// * `snooze_length` - Snooze duration after consecutive fire limit
3852    /// * `mp_charge_accumulation` - Whether membrane potential accumulates
3853    ///
3854    /// # Returns
3855    ///
3856    /// The newly created neuron ID
3857    ///
3858    #[allow(clippy::too_many_arguments)]
3859    pub fn add_neuron(
3860        &mut self,
3861        cortical_id: &CorticalID,
3862        x: u32,
3863        y: u32,
3864        z: u32,
3865        firing_threshold: f32,
3866        firing_threshold_limit: f32,
3867        leak_coefficient: f32,
3868        resting_potential: f32,
3869        neuron_type: u8,
3870        refractory_period: u16,
3871        excitability: f32,
3872        consecutive_fire_limit: u16,
3873        snooze_length: u16,
3874        mp_charge_accumulation: bool,
3875    ) -> BduResult<u64> {
3876        // Validate cortical area exists
3877        if !self.cortical_areas.contains_key(cortical_id) {
3878            return Err(BduError::InvalidArea(format!(
3879                "Cortical area {} not found",
3880                cortical_id
3881            )));
3882        }
3883
3884        let cortical_idx = *self
3885            .cortical_id_to_idx
3886            .get(cortical_id)
3887            .ok_or_else(|| BduError::InvalidArea(format!("No index for {}", cortical_id)))?;
3888
3889        // Get NPU
3890        let npu = self
3891            .npu
3892            .as_ref()
3893            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
3894
3895        let mut npu_lock = npu
3896            .lock()
3897            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
3898
3899        // Add neuron via NPU
3900        let neuron_id = npu_lock
3901            .add_neuron(
3902                firing_threshold,
3903                firing_threshold_limit,
3904                leak_coefficient,
3905                resting_potential,
3906                neuron_type as i32,
3907                refractory_period,
3908                excitability,
3909                consecutive_fire_limit,
3910                snooze_length,
3911                mp_charge_accumulation,
3912                cortical_idx,
3913                x,
3914                y,
3915                z,
3916            )
3917            .map_err(|e| BduError::Internal(format!("Failed to add neuron: {}", e)))?;
3918
3919        trace!(
3920            target: "feagi-bdu",
3921            "Created neuron {} in area {} at ({}, {}, {})",
3922            neuron_id.0,
3923            cortical_id,
3924            x,
3925            y,
3926            z
3927        );
3928
3929        // CRITICAL: Update StateManager neuron count (for health_check endpoint)
3930        let state_manager = StateManager::instance();
3931        let state_manager = state_manager.read();
3932        let core_state = state_manager.get_core_state();
3933        core_state.add_neuron_count(1);
3934        core_state.add_regular_neuron_count(1);
3935        state_manager.add_cortical_area_neuron_count(&cortical_id.as_base_64(), 1);
3936
3937        Ok(neuron_id.0 as u64)
3938    }
3939
3940    /// Delete a neuron by ID
3941    ///
3942    /// # Arguments
3943    ///
3944    /// * `neuron_id` - Global neuron ID
3945    ///
3946    /// # Returns
3947    ///
3948    /// `true` if the neuron was deleted, `false` if it didn't exist
3949    ///
3950    pub fn delete_neuron(&mut self, neuron_id: u64) -> BduResult<bool> {
3951        // Get NPU
3952        let npu = self
3953            .npu
3954            .as_ref()
3955            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
3956
3957        let mut npu_lock = npu
3958            .lock()
3959            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
3960
3961        let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id as u32);
3962        let cortical_id = self.cortical_idx_to_id.get(&cortical_idx).cloned();
3963
3964        let deleted = npu_lock.delete_neuron(neuron_id as u32);
3965
3966        if deleted {
3967            trace!(target: "feagi-bdu", "Deleted neuron {}", neuron_id);
3968
3969            // CRITICAL: Update StateManager neuron count (for health_check endpoint)
3970            let state_manager = StateManager::instance();
3971            let state_manager = state_manager.read();
3972            let core_state = state_manager.get_core_state();
3973            core_state.subtract_neuron_count(1);
3974            core_state.subtract_regular_neuron_count(1);
3975            if let Some(cortical_id) = cortical_id {
3976                state_manager.subtract_cortical_area_neuron_count(&cortical_id.as_base_64(), 1);
3977            }
3978
3979            // Trigger fatigue index recalculation after neuron deletion
3980            // NOTE: Disabled during genome loading to prevent blocking
3981            // let _ = self.update_fatigue_index();
3982        }
3983
3984        Ok(deleted)
3985    }
3986
3987    /// Apply cortical mapping rules (dstmap) to create synapses
3988    ///
3989    /// This parses the destination mapping rules from a source area and
3990    /// creates synapses using the NPU's synaptogenesis functions.
3991    ///
3992    /// # Arguments
3993    ///
3994    /// * `src_cortical_id` - Source cortical area ID
3995    ///
3996    /// # Returns
3997    ///
3998    /// Number of synapses created
3999    ///
4000    pub fn apply_cortical_mapping(&mut self, src_cortical_id: &CorticalID) -> BduResult<u32> {
4001        // Get source area
4002        let src_area = self
4003            .cortical_areas
4004            .get(src_cortical_id)
4005            .ok_or_else(|| {
4006                BduError::InvalidArea(format!("Source area {} not found", src_cortical_id))
4007            })?
4008            .clone();
4009
4010        // Get dstmap from area properties
4011        let dstmap = match src_area.properties.get("cortical_mapping_dst") {
4012            Some(serde_json::Value::Object(map)) if !map.is_empty() => map,
4013            _ => return Ok(0), // No mappings
4014        };
4015
4016        let src_cortical_idx = *self
4017            .cortical_id_to_idx
4018            .get(src_cortical_id)
4019            .ok_or_else(|| BduError::InvalidArea(format!("No index for {}", src_cortical_id)))?;
4020
4021        let mut total_synapses = 0u32;
4022        let mut upstream_updates: Vec<(CorticalID, u32)> = Vec::new(); // Collect updates to apply later
4023
4024        // Process each destination area using the unified path
4025        for (dst_cortical_id_str, _rules) in dstmap {
4026            // Convert string to CorticalID
4027            let dst_cortical_id = match CorticalID::try_from_base_64(dst_cortical_id_str) {
4028                Ok(id) => id,
4029                Err(_) => {
4030                    warn!(target: "feagi-bdu","Invalid cortical ID format: {}, skipping", dst_cortical_id_str);
4031                    continue;
4032                }
4033            };
4034
4035            // Verify destination area exists
4036            if !self.cortical_id_to_idx.contains_key(&dst_cortical_id) {
4037                warn!(target: "feagi-bdu","Destination area {} not found, skipping", dst_cortical_id);
4038                continue;
4039            }
4040
4041            // Apply cortical mapping for this pair (handles STDP and all morphology rules)
4042            let synapse_count =
4043                self.apply_cortical_mapping_for_pair(src_cortical_id, &dst_cortical_id)?;
4044            total_synapses += synapse_count as u32;
4045
4046            // Queue upstream area update for ANY mapping (even if no synapses created)
4047            // This is critical for memory areas which have mappings but no physical synapses
4048            upstream_updates.push((dst_cortical_id, src_cortical_idx));
4049        }
4050
4051        // Apply all upstream area updates now that NPU borrows are complete
4052        for (dst_id, src_idx) in upstream_updates {
4053            self.add_upstream_area(&dst_id, src_idx);
4054        }
4055
4056        trace!(
4057            target: "feagi-bdu",
4058            "Created {} synapses for area {} via NPU",
4059            total_synapses,
4060            src_cortical_id
4061        );
4062
4063        // CRITICAL: Update per-area synapse count cache (lock-free for readers)
4064        // This allows healthcheck endpoints to read counts without NPU lock
4065        if total_synapses > 0 {
4066            let mut cache = self.cached_synapse_counts_per_area.write();
4067            cache
4068                .entry(*src_cortical_id)
4069                .or_insert_with(|| AtomicUsize::new(0))
4070                .fetch_add(total_synapses as usize, Ordering::Relaxed);
4071        }
4072
4073        // Update total synapse count cache
4074        self.cached_synapse_count
4075            .fetch_add(total_synapses as usize, Ordering::Relaxed);
4076
4077        // CRITICAL: Update StateManager synapse count (for health_check endpoint)
4078        if total_synapses > 0 {
4079            let state_manager = StateManager::instance();
4080            let state_manager = state_manager.read();
4081            let core_state = state_manager.get_core_state();
4082            core_state.add_synapse_count(total_synapses);
4083        }
4084
4085        Ok(total_synapses)
4086    }
4087
4088    // ======================================================================
4089    // Neuron Query Methods (Delegates to NPU)
4090    // ======================================================================
4091
4092    /// Check if a neuron exists
4093    ///
4094    /// # Arguments
4095    ///
4096    /// * `neuron_id` - The neuron ID to check
4097    ///
4098    /// # Returns
4099    ///
4100    /// `true` if the neuron exists in the NPU, `false` otherwise
4101    ///
4102    /// # Note
4103    ///
4104    /// Returns `false` if NPU is not connected
4105    ///
4106    pub fn has_neuron(&self, neuron_id: u64) -> bool {
4107        if let Some(ref npu) = self.npu {
4108            if let Ok(npu_lock) = npu.lock() {
4109                // Check if neuron exists AND is valid (not deleted)
4110                npu_lock.is_neuron_valid(neuron_id as u32)
4111            } else {
4112                false
4113            }
4114        } else {
4115            false
4116        }
4117    }
4118
4119    /// Get total number of active neurons (lock-free cached read with opportunistic update)
4120    ///
4121    /// # Returns
4122    ///
4123    /// The total number of neurons (from cache)
4124    ///
4125    /// # Performance
4126    ///
4127    /// This is a lock-free atomic read that never blocks, even during burst processing.
4128    /// Opportunistically updates cache if NPU is available (non-blocking try_lock).
4129    ///
4130    pub fn get_neuron_count(&self) -> usize {
4131        // Opportunistically update cache if NPU is available (non-blocking)
4132        if let Some(ref npu) = self.npu {
4133            if let Ok(npu_lock) = npu.try_lock() {
4134                let fresh_count = npu_lock.get_neuron_count();
4135                self.cached_neuron_count
4136                    .store(fresh_count, Ordering::Relaxed);
4137            }
4138            // If NPU is busy, just use cached value
4139        }
4140
4141        // Always return cached value (never blocks)
4142        self.cached_neuron_count.load(Ordering::Relaxed)
4143    }
4144
4145    /// Update the cached neuron count (explicit update)
4146    ///
4147    /// Use this if you want to force a cache update. Most callers should just
4148    /// use get_neuron_count() which updates opportunistically.
4149    ///
4150    pub fn update_cached_neuron_count(&self) {
4151        if let Some(ref npu) = self.npu {
4152            if let Ok(npu_lock) = npu.try_lock() {
4153                let count = npu_lock.get_neuron_count();
4154                self.cached_neuron_count.store(count, Ordering::Relaxed);
4155            }
4156        }
4157    }
4158
4159    /// Refresh cached neuron count for a single cortical area from the NPU.
4160    ///
4161    /// Returns the refreshed count if successful.
4162    pub fn refresh_neuron_count_for_area(&self, cortical_id: &CorticalID) -> Option<usize> {
4163        let npu = self.npu.as_ref()?;
4164        let cortical_idx = *self.cortical_id_to_idx.get(cortical_id)?;
4165        let npu_lock = npu.lock().ok()?;
4166        let count = npu_lock.get_neurons_in_cortical_area(cortical_idx).len();
4167        drop(npu_lock);
4168
4169        let mut cache = self.cached_neuron_counts_per_area.write();
4170        cache
4171            .entry(*cortical_id)
4172            .or_insert_with(|| AtomicUsize::new(0))
4173            .store(count, Ordering::Relaxed);
4174
4175        // @cursor:critical-path - Keep BV-facing stats in StateManager.
4176        let state_manager = StateManager::instance();
4177        let state_manager = state_manager.read();
4178        state_manager.set_cortical_area_neuron_count(&cortical_id.as_base_64(), count);
4179
4180        self.update_cached_neuron_count();
4181
4182        Some(count)
4183    }
4184
4185    /// Get total number of synapses (lock-free cached read with opportunistic update)
4186    ///
4187    /// # Returns
4188    ///
4189    /// The total number of synapses (from cache)
4190    ///
4191    /// # Performance
4192    ///
4193    /// This is a lock-free atomic read that never blocks, even during burst processing.
4194    /// Opportunistically updates cache if NPU is available (non-blocking try_lock).
4195    ///
4196    pub fn get_synapse_count(&self) -> usize {
4197        // Opportunistically update cache if NPU is available (non-blocking)
4198        if let Some(ref npu) = self.npu {
4199            if let Ok(npu_lock) = npu.try_lock() {
4200                let fresh_count = npu_lock.get_synapse_count();
4201                self.cached_synapse_count
4202                    .store(fresh_count, Ordering::Relaxed);
4203            }
4204            // If NPU is busy, just use cached value
4205        }
4206
4207        // Always return cached value (never blocks)
4208        self.cached_synapse_count.load(Ordering::Relaxed)
4209    }
4210
4211    /// Update the cached synapse count (explicit update)
4212    ///
4213    /// Use this if you want to force a cache update. Most callers should just
4214    /// use get_synapse_count() which updates opportunistically.
4215    ///
4216    pub fn update_cached_synapse_count(&self) {
4217        if let Some(ref npu) = self.npu {
4218            if let Ok(npu_lock) = npu.try_lock() {
4219                let count = npu_lock.get_synapse_count();
4220                self.cached_synapse_count.store(count, Ordering::Relaxed);
4221            }
4222        }
4223    }
4224
4225    /// Update all cached stats (neuron and synapse counts)
4226    ///
4227    /// This is called automatically when NPU is connected and can be called
4228    /// explicitly if you want to force a cache refresh.
4229    ///
4230    pub fn update_all_cached_stats(&self) {
4231        self.update_cached_neuron_count();
4232        self.update_cached_synapse_count();
4233    }
4234
4235    /// Get neuron coordinates (x, y, z)
4236    ///
4237    /// # Arguments
4238    ///
4239    /// * `neuron_id` - The neuron ID to query
4240    ///
4241    /// # Returns
4242    ///
4243    /// Coordinates as (x, y, z), or (0, 0, 0) if neuron doesn't exist or NPU not connected
4244    ///
4245    pub fn get_neuron_coordinates(&self, neuron_id: u64) -> (u32, u32, u32) {
4246        if let Some(ref npu) = self.npu {
4247            if let Ok(npu_lock) = npu.lock() {
4248                npu_lock
4249                    .get_neuron_coordinates(neuron_id as u32)
4250                    .unwrap_or((0, 0, 0))
4251            } else {
4252                (0, 0, 0)
4253            }
4254        } else {
4255            (0, 0, 0)
4256        }
4257    }
4258
4259    /// Get the cortical area index for a neuron
4260    ///
4261    /// # Arguments
4262    ///
4263    /// * `neuron_id` - The neuron ID to query
4264    ///
4265    /// # Returns
4266    ///
4267    /// Cortical area index, or 0 if neuron doesn't exist or NPU not connected
4268    ///
4269    pub fn get_neuron_cortical_idx(&self, neuron_id: u64) -> u32 {
4270        if let Some(ref npu) = self.npu {
4271            if let Ok(npu_lock) = npu.lock() {
4272                npu_lock.get_neuron_cortical_area(neuron_id as u32)
4273            } else {
4274                0
4275            }
4276        } else {
4277            0
4278        }
4279    }
4280
4281    /// Get all neuron IDs in a specific cortical area
4282    ///
4283    /// # Arguments
4284    ///
4285    /// * `cortical_id` - The cortical area ID (string)
4286    ///
4287    /// # Returns
4288    ///
4289    /// Vec of neuron IDs in the area, or empty vec if area doesn't exist or NPU not connected
4290    ///
4291    pub fn get_neurons_in_area(&self, cortical_id: &CorticalID) -> Vec<u64> {
4292        // Get cortical_idx from cortical_id
4293        let cortical_idx = match self.cortical_id_to_idx.get(cortical_id) {
4294            Some(idx) => *idx,
4295            None => return Vec::new(),
4296        };
4297
4298        if let Some(ref npu) = self.npu {
4299            if let Ok(npu_lock) = npu.lock() {
4300                // Convert Vec<u32> to Vec<u64>
4301                npu_lock
4302                    .get_neurons_in_cortical_area(cortical_idx)
4303                    .into_iter()
4304                    .map(|id| id as u64)
4305                    .collect()
4306            } else {
4307                Vec::new()
4308            }
4309        } else {
4310            Vec::new()
4311        }
4312    }
4313
4314    /// Get all outgoing synapses from a source neuron
4315    ///
4316    /// # Arguments
4317    ///
4318    /// * `source_neuron_id` - The source neuron ID
4319    ///
4320    /// # Returns
4321    ///
4322    /// Vec of (target_neuron_id, weight, psp, synapse_type), or empty if NPU not connected
4323    ///
4324    pub fn get_outgoing_synapses(&self, source_neuron_id: u64) -> Vec<(u32, u8, u8, u8)> {
4325        if let Some(ref npu) = self.npu {
4326            if let Ok(npu_lock) = npu.lock() {
4327                npu_lock.get_outgoing_synapses(source_neuron_id as u32)
4328            } else {
4329                Vec::new()
4330            }
4331        } else {
4332            Vec::new()
4333        }
4334    }
4335
4336    /// Get all incoming synapses to a target neuron
4337    ///
4338    /// # Arguments
4339    ///
4340    /// * `target_neuron_id` - The target neuron ID
4341    ///
4342    /// # Returns
4343    ///
4344    /// Vec of (source_neuron_id, weight, psp, synapse_type), or empty if NPU not connected
4345    ///
4346    pub fn get_incoming_synapses(&self, target_neuron_id: u64) -> Vec<(u32, u8, u8, u8)> {
4347        if let Some(ref npu) = self.npu {
4348            if let Ok(npu_lock) = npu.lock() {
4349                npu_lock.get_incoming_synapses(target_neuron_id as u32)
4350            } else {
4351                Vec::new()
4352            }
4353        } else {
4354            Vec::new()
4355        }
4356    }
4357
4358    /// Get neuron count for a specific cortical area
4359    ///
4360    /// # Arguments
4361    ///
4362    /// * `cortical_id` - The cortical area ID (string)
4363    ///
4364    /// # Returns
4365    ///
4366    /// Number of neurons in the area, or 0 if area doesn't exist or NPU not connected
4367    ///
4368    /// Get neuron count for a specific cortical area (lock-free cached read)
4369    ///
4370    /// # Arguments
4371    ///
4372    /// * `cortical_id` - The cortical area ID
4373    ///
4374    /// # Returns
4375    ///
4376    /// The number of neurons in the area (from cache, never blocks on NPU lock)
4377    ///
4378    /// # Performance
4379    ///
4380    /// This is a lock-free atomic read that never blocks, even during burst processing.
4381    /// Count is maintained in ConnectomeManager and updated when neurons are created/deleted.
4382    ///
4383    pub fn get_neuron_count_in_area(&self, cortical_id: &CorticalID) -> usize {
4384        // CRITICAL: Read from cache (lock-free) - never query NPU for healthcheck endpoints
4385        let cache = self.cached_neuron_counts_per_area.read();
4386        let base_count = cache
4387            .get(cortical_id)
4388            .map(|count| count.load(Ordering::Relaxed))
4389            .unwrap_or(0);
4390
4391        // Memory areas maintain neurons outside the NPU; add their count from StateManager.
4392        let memory_count = self
4393            .cortical_areas
4394            .get(cortical_id)
4395            .and_then(|area| feagi_evolutionary::extract_memory_properties(&area.properties))
4396            .and_then(|_| {
4397                StateManager::instance()
4398                    .try_read()
4399                    .and_then(|state_manager| {
4400                        state_manager.get_cortical_area_stats(&cortical_id.as_base_64())
4401                    })
4402            })
4403            .map(|stats| stats.neuron_count)
4404            .unwrap_or(0);
4405
4406        base_count.saturating_add(memory_count)
4407    }
4408
4409    /// Get all cortical areas that have neurons
4410    ///
4411    /// # Returns
4412    ///
4413    /// Vec of (cortical_id, neuron_count) for areas with at least one neuron
4414    ///
4415    pub fn get_populated_areas(&self) -> Vec<(String, usize)> {
4416        let mut result = Vec::new();
4417
4418        for cortical_id in self.cortical_areas.keys() {
4419            let count = self.get_neuron_count_in_area(cortical_id);
4420            if count > 0 {
4421                result.push((cortical_id.to_string(), count));
4422            }
4423        }
4424
4425        result
4426    }
4427
4428    /// Check if a cortical area has any neurons
4429    ///
4430    /// # Arguments
4431    ///
4432    /// * `cortical_id` - The cortical area ID
4433    ///
4434    /// # Returns
4435    ///
4436    /// `true` if the area has at least one neuron, `false` otherwise
4437    ///
4438    pub fn is_area_populated(&self, cortical_id: &CorticalID) -> bool {
4439        self.get_neuron_count_in_area(cortical_id) > 0
4440    }
4441
4442    /// Get total synapse count for a specific cortical area (outgoing only) - lock-free cached read
4443    ///
4444    /// # Arguments
4445    ///
4446    /// * `cortical_id` - The cortical area ID
4447    ///
4448    /// # Returns
4449    ///
4450    /// Total number of outgoing synapses from neurons in this area (from cache, never blocks on NPU lock)
4451    ///
4452    /// # Performance
4453    ///
4454    /// This is a lock-free atomic read that never blocks, even during burst processing.
4455    /// Count is maintained in ConnectomeManager and updated when synapses are created/deleted.
4456    ///
4457    pub fn get_synapse_count_in_area(&self, cortical_id: &CorticalID) -> usize {
4458        // CRITICAL: Read from cache (lock-free) - never query NPU for healthcheck endpoints
4459        let cache = self.cached_synapse_counts_per_area.read();
4460        cache
4461            .get(cortical_id)
4462            .map(|count| count.load(Ordering::Relaxed))
4463            .unwrap_or(0)
4464    }
4465
4466    /// Get total incoming synapse count for a specific cortical area.
4467    ///
4468    /// # Arguments
4469    ///
4470    /// * `cortical_id` - The cortical area ID
4471    ///
4472    /// # Returns
4473    ///
4474    /// Total number of incoming synapses targeting neurons in this area.
4475    pub fn get_incoming_synapse_count_in_area(&self, cortical_id: &CorticalID) -> usize {
4476        if !self.cortical_id_to_idx.contains_key(cortical_id) {
4477            return 0;
4478        }
4479
4480        if let Some(state_manager) = StateManager::instance().try_read() {
4481            if let Some(stats) = state_manager.get_cortical_area_stats(&cortical_id.as_base_64()) {
4482                return stats.incoming_synapse_count;
4483            }
4484        }
4485
4486        0
4487    }
4488
4489    /// Get total outgoing synapse count for a specific cortical area.
4490    ///
4491    /// # Arguments
4492    ///
4493    /// * `cortical_id` - The cortical area ID
4494    ///
4495    /// # Returns
4496    ///
4497    /// Total number of outgoing synapses originating from neurons in this area.
4498    pub fn get_outgoing_synapse_count_in_area(&self, cortical_id: &CorticalID) -> usize {
4499        if !self.cortical_id_to_idx.contains_key(cortical_id) {
4500            return 0;
4501        }
4502
4503        if let Some(state_manager) = StateManager::instance().try_read() {
4504            if let Some(stats) = state_manager.get_cortical_area_stats(&cortical_id.as_base_64()) {
4505                return stats.outgoing_synapse_count;
4506            }
4507        }
4508
4509        0
4510    }
4511
4512    /// Check if two neurons are connected (source → target)
4513    ///
4514    /// # Arguments
4515    ///
4516    /// * `source_neuron_id` - The source neuron ID
4517    /// * `target_neuron_id` - The target neuron ID
4518    ///
4519    /// # Returns
4520    ///
4521    /// `true` if there is a synapse from source to target, `false` otherwise
4522    ///
4523    pub fn are_neurons_connected(&self, source_neuron_id: u64, target_neuron_id: u64) -> bool {
4524        let synapses = self.get_outgoing_synapses(source_neuron_id);
4525        synapses
4526            .iter()
4527            .any(|(target, _, _, _)| *target == target_neuron_id as u32)
4528    }
4529
4530    /// Get connection strength (weight) between two neurons
4531    ///
4532    /// # Arguments
4533    ///
4534    /// * `source_neuron_id` - The source neuron ID
4535    /// * `target_neuron_id` - The target neuron ID
4536    ///
4537    /// # Returns
4538    ///
4539    /// Synapse weight (0-255), or None if no connection exists
4540    ///
4541    pub fn get_connection_weight(
4542        &self,
4543        source_neuron_id: u64,
4544        target_neuron_id: u64,
4545    ) -> Option<u8> {
4546        let synapses = self.get_outgoing_synapses(source_neuron_id);
4547        synapses
4548            .iter()
4549            .find(|(target, _, _, _)| *target == target_neuron_id as u32)
4550            .map(|(_, weight, _, _)| *weight)
4551    }
4552
4553    /// Get connectivity statistics for a cortical area
4554    ///
4555    /// # Arguments
4556    ///
4557    /// * `cortical_id` - The cortical area ID
4558    ///
4559    /// # Returns
4560    ///
4561    /// (neuron_count, total_synapses, avg_synapses_per_neuron)
4562    ///
4563    pub fn get_area_connectivity_stats(&self, cortical_id: &CorticalID) -> (usize, usize, f32) {
4564        let neurons = self.get_neurons_in_area(cortical_id);
4565        let neuron_count = neurons.len();
4566
4567        if neuron_count == 0 {
4568            return (0, 0, 0.0);
4569        }
4570
4571        let mut total_synapses = 0;
4572        for neuron_id in neurons {
4573            total_synapses += self.get_outgoing_synapses(neuron_id).len();
4574        }
4575
4576        let avg_synapses = total_synapses as f32 / neuron_count as f32;
4577
4578        (neuron_count, total_synapses, avg_synapses)
4579    }
4580
4581    /// Get the cortical area ID (string) for a neuron
4582    ///
4583    /// # Arguments
4584    ///
4585    /// * `neuron_id` - The neuron ID
4586    ///
4587    /// # Returns
4588    ///
4589    /// The cortical area ID, or None if neuron doesn't exist
4590    ///
4591    pub fn get_neuron_cortical_id(&self, neuron_id: u64) -> Option<CorticalID> {
4592        let cortical_idx = self.get_neuron_cortical_idx(neuron_id);
4593        self.cortical_idx_to_id.get(&cortical_idx).copied()
4594    }
4595
4596    /// Get neuron density (neurons per voxel) for a cortical area
4597    ///
4598    /// # Arguments
4599    ///
4600    /// * `cortical_id` - The cortical area ID
4601    ///
4602    /// # Returns
4603    ///
4604    /// Neuron density (neurons per voxel), or 0.0 if area doesn't exist
4605    ///
4606    pub fn get_neuron_density(&self, cortical_id: &CorticalID) -> f32 {
4607        let area = match self.cortical_areas.get(cortical_id) {
4608            Some(a) => a,
4609            None => return 0.0,
4610        };
4611
4612        let neuron_count = self.get_neuron_count_in_area(cortical_id);
4613        let volume = area.dimensions.width * area.dimensions.height * area.dimensions.depth;
4614
4615        if volume == 0 {
4616            return 0.0;
4617        }
4618
4619        neuron_count as f32 / volume as f32
4620    }
4621
4622    /// Get all cortical areas with connectivity statistics
4623    ///
4624    /// # Returns
4625    ///
4626    /// Vec of (cortical_id, neuron_count, synapse_count, density)
4627    ///
4628    pub fn get_all_area_stats(&self) -> Vec<(String, usize, usize, f32)> {
4629        let mut stats = Vec::new();
4630
4631        for cortical_id in self.cortical_areas.keys() {
4632            let neuron_count = self.get_neuron_count_in_area(cortical_id);
4633            let synapse_count = self.get_synapse_count_in_area(cortical_id);
4634            let density = self.get_neuron_density(cortical_id);
4635
4636            stats.push((
4637                cortical_id.to_string(),
4638                neuron_count,
4639                synapse_count,
4640                density,
4641            ));
4642        }
4643
4644        stats
4645    }
4646
4647    // ======================================================================
4648    // Configuration
4649    // ======================================================================
4650
4651    /// Get the configuration
4652    pub fn get_config(&self) -> &ConnectomeConfig {
4653        &self.config
4654    }
4655
4656    /// Update configuration
4657    pub fn set_config(&mut self, config: ConnectomeConfig) {
4658        self.config = config;
4659    }
4660
4661    // ======================================================================
4662    // Genome I/O
4663    // ======================================================================
4664
4665    /// Ensure core cortical areas (_death, _power, _fatigue) exist
4666    ///
4667    /// Core areas are required for brain operation:
4668    /// - `_death` (cortical_idx=0): Manages neuron death and cleanup
4669    /// - `_power` (cortical_idx=1): Provides power injection for burst engine
4670    /// - `_fatigue` (cortical_idx=2): Monitors brain fatigue and triggers sleep mode
4671    ///
4672    /// If any core area is missing from the genome, it will be automatically created
4673    /// with default properties (1x1x1 dimensions, minimal configuration).
4674    ///
4675    /// # Returns
4676    ///
4677    /// * `Ok(())` if all core areas exist or were successfully created
4678    /// * `Err(BduError)` if creation fails
4679    pub fn ensure_core_cortical_areas(&mut self) -> BduResult<()> {
4680        info!(target: "feagi-bdu", "🔧 [CORE-AREA] Ensuring core cortical areas exist...");
4681
4682        use feagi_structures::genomic::cortical_area::{
4683            CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalAreaType,
4684        };
4685
4686        // Core areas are always 1x1x1 as per requirements
4687        let core_dimensions = CorticalAreaDimensions::new(1, 1, 1).map_err(|e| {
4688            BduError::Internal(format!("Failed to create core area dimensions: {}", e))
4689        })?;
4690
4691        // Default position for core areas (origin)
4692        let core_position = (0, 0, 0).into();
4693
4694        // Check and create _death (cortical_idx=0)
4695        let death_id = CoreCorticalType::Death.to_cortical_id();
4696        if !self.cortical_areas.contains_key(&death_id) {
4697            info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _death area (cortical_idx=0)");
4698            let death_area = CorticalArea::new(
4699                death_id,
4700                0, // Will be overridden by add_cortical_area to 0
4701                "_death".to_string(),
4702                core_dimensions,
4703                core_position,
4704                CorticalAreaType::Core(CoreCorticalType::Death),
4705            )
4706            .map_err(|e| BduError::Internal(format!("Failed to create _death area: {}", e)))?;
4707            match self.add_cortical_area(death_area) {
4708                Ok(idx) => {
4709                    info!(target: "feagi-bdu", "  ✅ Created _death area with cortical_idx={}", idx);
4710                }
4711                Err(e) => {
4712                    error!(target: "feagi-bdu", "  ❌ Failed to add _death area: {}", e);
4713                    return Err(e);
4714                }
4715            }
4716        } else {
4717            info!(target: "feagi-bdu", "  ✓ _death area already exists");
4718        }
4719
4720        // Check and create _power (cortical_idx=1)
4721        let power_id = CoreCorticalType::Power.to_cortical_id();
4722        if !self.cortical_areas.contains_key(&power_id) {
4723            info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _power area (cortical_idx=1)");
4724            let power_area = CorticalArea::new(
4725                power_id,
4726                1, // Will be overridden by add_cortical_area to 1
4727                "_power".to_string(),
4728                core_dimensions,
4729                core_position,
4730                CorticalAreaType::Core(CoreCorticalType::Power),
4731            )
4732            .map_err(|e| BduError::Internal(format!("Failed to create _power area: {}", e)))?;
4733            match self.add_cortical_area(power_area) {
4734                Ok(idx) => {
4735                    info!(target: "feagi-bdu", "  ✅ Created _power area with cortical_idx={}", idx);
4736                }
4737                Err(e) => {
4738                    error!(target: "feagi-bdu", "  ❌ Failed to add _power area: {}", e);
4739                    return Err(e);
4740                }
4741            }
4742        } else {
4743            info!(target: "feagi-bdu", "  ✓ _power area already exists");
4744        }
4745
4746        // Check and create _fatigue (cortical_idx=2)
4747        let fatigue_id = CoreCorticalType::Fatigue.to_cortical_id();
4748        if !self.cortical_areas.contains_key(&fatigue_id) {
4749            info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _fatigue area (cortical_idx=2)");
4750            let fatigue_area = CorticalArea::new(
4751                fatigue_id,
4752                2, // Will be overridden by add_cortical_area to 2
4753                "_fatigue".to_string(),
4754                core_dimensions,
4755                core_position,
4756                CorticalAreaType::Core(CoreCorticalType::Fatigue),
4757            )
4758            .map_err(|e| BduError::Internal(format!("Failed to create _fatigue area: {}", e)))?;
4759            match self.add_cortical_area(fatigue_area) {
4760                Ok(idx) => {
4761                    info!(target: "feagi-bdu", "  ✅ Created _fatigue area with cortical_idx={}", idx);
4762                }
4763                Err(e) => {
4764                    error!(target: "feagi-bdu", "  ❌ Failed to add _fatigue area: {}", e);
4765                    return Err(e);
4766                }
4767            }
4768        } else {
4769            info!(target: "feagi-bdu", "  ✓ _fatigue area already exists");
4770        }
4771
4772        info!(target: "feagi-bdu", "🔧 [CORE-AREA] Core area check complete");
4773        Ok(())
4774    }
4775
4776    /// Save the connectome as a genome JSON
4777    ///
4778    /// **DEPRECATED**: This method produces incomplete hierarchical format v2.1 without morphologies/physiology.
4779    /// Use `GenomeService::save_genome()` instead, which produces complete flat format v3.0.
4780    ///
4781    /// This method is kept only for legacy tests. Production code MUST use GenomeService.
4782    ///
4783    /// # Arguments
4784    ///
4785    /// * `genome_id` - Optional custom genome ID (generates timestamp-based ID if None)
4786    /// * `genome_title` - Optional custom genome title
4787    ///
4788    /// # Returns
4789    ///
4790    /// JSON string representation of the genome (hierarchical v2.1, incomplete)
4791    ///
4792    #[deprecated(
4793        note = "Use GenomeService::save_genome() instead. This produces incomplete v2.1 format without morphologies/physiology."
4794    )]
4795    #[allow(deprecated)]
4796    pub fn save_genome_to_json(
4797        &self,
4798        genome_id: Option<String>,
4799        genome_title: Option<String>,
4800    ) -> BduResult<String> {
4801        // Build parent map from brain region hierarchy
4802        let mut brain_regions_with_parents = std::collections::HashMap::new();
4803
4804        for region_id in self.brain_regions.get_all_region_ids() {
4805            if let Some(region) = self.brain_regions.get_region(region_id) {
4806                let parent_id = self
4807                    .brain_regions
4808                    .get_parent(region_id)
4809                    .map(|s| s.to_string());
4810                brain_regions_with_parents
4811                    .insert(region_id.to_string(), (region.clone(), parent_id));
4812            }
4813        }
4814
4815        // Generate and return JSON
4816        Ok(feagi_evolutionary::GenomeSaver::save_to_json(
4817            &self.cortical_areas,
4818            &brain_regions_with_parents,
4819            genome_id,
4820            genome_title,
4821        )?)
4822    }
4823
4824    // Load genome from file and develop brain
4825    //
4826    // This was a high-level convenience method that:
4827    // 1. Loads genome from JSON file
4828    // 2. Prepares for new genome (clears existing state)
4829    // 3. Runs neuroembryogenesis to develop the brain
4830    //
4831    // # Arguments
4832    //
4833    // * `genome_path` - Path to genome JSON file
4834    //
4835    // # Returns
4836    //
4837    // Development progress information
4838    //
4839    // NOTE: load_from_genome_file() and load_from_genome() have been REMOVED.
4840    // All genome loading must now go through GenomeService::load_genome() which:
4841    // - Stores RuntimeGenome for persistence
4842    // - Updates genome metadata
4843    // - Provides async/await support
4844    // - Includes timeout protection
4845    // - Ensures core cortical areas exist
4846    //
4847    // See: feagi-services/src/impls/genome_service_impl.rs::load_genome()
4848
4849    /// Prepare for loading a new genome
4850    ///
4851    /// Clears all existing cortical areas, brain regions, and resets state.
4852    /// This is typically called before loading a new genome.
4853    ///
4854    pub fn prepare_for_new_genome(&mut self) -> BduResult<()> {
4855        info!(target: "feagi-bdu","Preparing for new genome (clearing existing state)");
4856
4857        // Clear cortical areas
4858        self.cortical_areas.clear();
4859        self.cortical_id_to_idx.clear();
4860        self.cortical_idx_to_id.clear();
4861        // CRITICAL: Reserve indices 0 (_death) and 1 (_power)
4862        self.next_cortical_idx = 3;
4863        info!("🔧 [BRAIN-RESET] Cortical mapping cleared, next_cortical_idx reset to 3 (reserves 0=_death, 1=_power, 2=_fatigue)");
4864
4865        // Clear brain regions
4866        self.brain_regions = BrainRegionHierarchy::new();
4867
4868        // Reset NPU runtime state to prevent old neurons/synapses from leaking into the next genome.
4869        if let Some(ref npu) = self.npu {
4870            let mut npu_lock = npu
4871                .lock()
4872                .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
4873            npu_lock
4874                .reset_for_new_genome()
4875                .map_err(|e| BduError::Internal(format!("Failed to reset NPU: {}", e)))?;
4876        }
4877
4878        info!(target: "feagi-bdu","✅ Connectome cleared and ready for new genome");
4879        Ok(())
4880    }
4881
4882    /// Calculate and resize memory for a genome
4883    ///
4884    /// Analyzes the genome to determine memory requirements and
4885    /// prepares the NPU for the expected neuron/synapse counts.
4886    ///
4887    /// # Arguments
4888    ///
4889    /// * `genome` - Genome to analyze for memory requirements
4890    ///
4891    pub fn resize_for_genome(
4892        &mut self,
4893        genome: &feagi_evolutionary::RuntimeGenome,
4894    ) -> BduResult<()> {
4895        // Store morphologies from genome
4896        self.morphology_registry = genome.morphologies.clone();
4897        info!(target: "feagi-bdu", "Stored {} morphologies from genome", self.morphology_registry.count());
4898
4899        // Calculate required capacity from genome stats
4900        let required_neurons = genome.stats.innate_neuron_count;
4901        let required_synapses = genome.stats.innate_synapse_count;
4902
4903        info!(target: "feagi-bdu",
4904            "Genome requires: {} neurons, {} synapses",
4905            required_neurons,
4906            required_synapses
4907        );
4908
4909        // Calculate total voxels from all cortical areas
4910        let mut total_voxels = 0;
4911        for area in genome.cortical_areas.values() {
4912            total_voxels += area.dimensions.width * area.dimensions.height * area.dimensions.depth;
4913        }
4914
4915        info!(target: "feagi-bdu",
4916            "Genome has {} cortical areas with {} total voxels",
4917            genome.cortical_areas.len(),
4918            total_voxels
4919        );
4920
4921        // TODO: Resize NPU if needed
4922        // For now, we assume NPU has sufficient capacity
4923        // In the future, we may want to dynamically resize the NPU based on genome requirements
4924
4925        Ok(())
4926    }
4927
4928    // ========================================================================
4929    // SYNAPSE OPERATIONS
4930    // ========================================================================
4931
4932    /// Create a synapse between two neurons
4933    ///
4934    /// # Arguments
4935    ///
4936    /// * `source_neuron_id` - Source neuron ID
4937    /// * `target_neuron_id` - Target neuron ID
4938    /// * `weight` - Synapse weight (0-255)
4939    /// * `psp` - Synapse PSP (0-255)
4940    /// * `synapse_type` - Synapse type (0=excitatory, 1=inhibitory)
4941    ///
4942    /// # Returns
4943    ///
4944    /// `Ok(())` if synapse created successfully
4945    ///
4946    pub fn create_synapse(
4947        &mut self,
4948        source_neuron_id: u64,
4949        target_neuron_id: u64,
4950        weight: u8,
4951        psp: u8,
4952        synapse_type: u8,
4953    ) -> BduResult<()> {
4954        // Get NPU
4955        let npu = self
4956            .npu
4957            .as_ref()
4958            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
4959
4960        let mut npu_lock = npu
4961            .lock()
4962            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
4963
4964        // Verify both neurons exist
4965        let source_exists = (source_neuron_id as u32) < npu_lock.get_neuron_count() as u32;
4966        let target_exists = (target_neuron_id as u32) < npu_lock.get_neuron_count() as u32;
4967
4968        if !source_exists {
4969            return Err(BduError::InvalidNeuron(format!(
4970                "Source neuron {} not found",
4971                source_neuron_id
4972            )));
4973        }
4974        if !target_exists {
4975            return Err(BduError::InvalidNeuron(format!(
4976                "Target neuron {} not found",
4977                target_neuron_id
4978            )));
4979        }
4980
4981        // Create synapse via NPU
4982        let syn_type = if synapse_type == 0 {
4983            feagi_npu_neural::synapse::SynapseType::Excitatory
4984        } else {
4985            feagi_npu_neural::synapse::SynapseType::Inhibitory
4986        };
4987
4988        let synapse_idx = npu_lock
4989            .add_synapse(
4990                NeuronId(source_neuron_id as u32),
4991                NeuronId(target_neuron_id as u32),
4992                feagi_npu_neural::types::SynapticWeight(weight),
4993                feagi_npu_neural::types::SynapticPsp(psp),
4994                syn_type,
4995            )
4996            .map_err(|e| BduError::Internal(format!("Failed to create synapse: {}", e)))?;
4997
4998        debug!(target: "feagi-bdu", "Created synapse: {} -> {} (weight: {}, psp: {}, type: {}, idx: {})",
4999            source_neuron_id, target_neuron_id, weight, psp, synapse_type, synapse_idx);
5000
5001        let source_cortical_idx = npu_lock.get_neuron_cortical_area(source_neuron_id as u32);
5002        let target_cortical_idx = npu_lock.get_neuron_cortical_area(target_neuron_id as u32);
5003        let source_cortical_id = self.cortical_idx_to_id.get(&source_cortical_idx).cloned();
5004        let target_cortical_id = self.cortical_idx_to_id.get(&target_cortical_idx).cloned();
5005
5006        let state_manager = StateManager::instance();
5007        let state_manager = state_manager.read();
5008        let core_state = state_manager.get_core_state();
5009        core_state.add_synapse_count(1);
5010        if let Some(cortical_id) = source_cortical_id {
5011            state_manager.add_cortical_area_outgoing_synapses(&cortical_id.as_base_64(), 1);
5012        }
5013        if let Some(cortical_id) = target_cortical_id {
5014            state_manager.add_cortical_area_incoming_synapses(&cortical_id.as_base_64(), 1);
5015        }
5016
5017        // Trigger fatigue index recalculation after synapse creation
5018        // NOTE: Disabled during genome loading to prevent blocking
5019        // let _ = self.update_fatigue_index();
5020
5021        Ok(())
5022    }
5023
5024    /// Synchronize cortical area flags with NPU
5025    /// This should be called after adding/updating cortical areas
5026    fn sync_cortical_area_flags_to_npu(&mut self) -> BduResult<()> {
5027        if let Some(ref npu) = self.npu {
5028            if let Ok(mut npu_lock) = npu.lock() {
5029                // Build psp_uniform_distribution flags map
5030                let mut psp_uniform_flags = ahash::AHashMap::new();
5031                let mut mp_driven_psp_flags = ahash::AHashMap::new();
5032
5033                for (cortical_id, area) in &self.cortical_areas {
5034                    // Power area defaults to uniform PSP distribution when property is absent.
5035                    let default_psp_uniform =
5036                        *cortical_id == CoreCorticalType::Power.to_cortical_id();
5037                    let psp_uniform = area
5038                        .get_property("psp_uniform_distribution")
5039                        .and_then(|v| v.as_bool())
5040                        .unwrap_or(default_psp_uniform);
5041                    psp_uniform_flags.insert(*cortical_id, psp_uniform);
5042
5043                    // Get mp_driven_psp flag (default to false)
5044                    let mp_driven_psp = area
5045                        .get_property("mp_driven_psp")
5046                        .and_then(|v| v.as_bool())
5047                        .unwrap_or(false);
5048                    mp_driven_psp_flags.insert(*cortical_id, mp_driven_psp);
5049                }
5050
5051                // Update NPU with flags
5052                npu_lock.set_psp_uniform_distribution_flags(psp_uniform_flags);
5053                npu_lock.set_mp_driven_psp_flags(mp_driven_psp_flags);
5054
5055                trace!(
5056                    target: "feagi-bdu",
5057                    "Synchronized cortical area flags to NPU ({} areas)",
5058                    self.cortical_areas.len()
5059                );
5060            }
5061        }
5062
5063        Ok(())
5064    }
5065
5066    /// Get synapse information between two neurons
5067    ///
5068    /// # Arguments
5069    ///
5070    /// * `source_neuron_id` - Source neuron ID
5071    /// * `target_neuron_id` - Target neuron ID
5072    ///
5073    /// # Returns
5074    ///
5075    /// `Some((weight, psp, type))` if synapse exists, `None` otherwise
5076    ///
5077    pub fn get_synapse(
5078        &self,
5079        source_neuron_id: u64,
5080        target_neuron_id: u64,
5081    ) -> Option<(u8, u8, u8)> {
5082        // Get NPU
5083        let npu = self.npu.as_ref()?;
5084        let npu_lock = npu.lock().ok()?;
5085
5086        // Use get_incoming_synapses and filter by source
5087        // (This does O(n) scan of synapse_array, but works even when propagation engine isn't updated)
5088        let incoming = npu_lock.get_incoming_synapses(target_neuron_id as u32);
5089
5090        // Find the synapse from our specific source
5091        for (source_id, weight, psp, synapse_type) in incoming {
5092            if source_id == source_neuron_id as u32 {
5093                return Some((weight, psp, synapse_type));
5094            }
5095        }
5096
5097        None
5098    }
5099
5100    /// Update the weight of an existing synapse
5101    ///
5102    /// # Arguments
5103    ///
5104    /// * `source_neuron_id` - Source neuron ID
5105    /// * `target_neuron_id` - Target neuron ID
5106    /// * `new_weight` - New synapse weight (0-255)
5107    ///
5108    /// # Returns
5109    ///
5110    /// `Ok(())` if synapse updated, `Err` if synapse not found
5111    ///
5112    pub fn update_synapse_weight(
5113        &mut self,
5114        source_neuron_id: u64,
5115        target_neuron_id: u64,
5116        new_weight: u8,
5117    ) -> BduResult<()> {
5118        // Get NPU
5119        let npu = self
5120            .npu
5121            .as_ref()
5122            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5123
5124        let mut npu_lock = npu
5125            .lock()
5126            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5127
5128        // Update synapse weight via NPU
5129        let updated = npu_lock.update_synapse_weight(
5130            NeuronId(source_neuron_id as u32),
5131            NeuronId(target_neuron_id as u32),
5132            feagi_npu_neural::types::SynapticWeight(new_weight),
5133        );
5134
5135        if updated {
5136            debug!(target: "feagi-bdu","Updated synapse weight: {} -> {} = {}", source_neuron_id, target_neuron_id, new_weight);
5137            Ok(())
5138        } else {
5139            Err(BduError::InvalidSynapse(format!(
5140                "Synapse {} -> {} not found",
5141                source_neuron_id, target_neuron_id
5142            )))
5143        }
5144    }
5145
5146    /// Remove a synapse between two neurons
5147    ///
5148    /// # Arguments
5149    ///
5150    /// * `source_neuron_id` - Source neuron ID
5151    /// * `target_neuron_id` - Target neuron ID
5152    ///
5153    /// # Returns
5154    ///
5155    /// `Ok(true)` if synapse removed, `Ok(false)` if synapse didn't exist
5156    ///
5157    pub fn remove_synapse(
5158        &mut self,
5159        source_neuron_id: u64,
5160        target_neuron_id: u64,
5161    ) -> BduResult<bool> {
5162        // Get NPU
5163        let npu = self
5164            .npu
5165            .as_ref()
5166            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5167
5168        let mut npu_lock = npu
5169            .lock()
5170            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5171
5172        let source_cortical_idx = npu_lock.get_neuron_cortical_area(source_neuron_id as u32);
5173        let target_cortical_idx = npu_lock.get_neuron_cortical_area(target_neuron_id as u32);
5174        let source_cortical_id = self.cortical_idx_to_id.get(&source_cortical_idx).cloned();
5175        let target_cortical_id = self.cortical_idx_to_id.get(&target_cortical_idx).cloned();
5176
5177        // Remove synapse via NPU
5178        let removed = npu_lock.remove_synapse(
5179            NeuronId(source_neuron_id as u32),
5180            NeuronId(target_neuron_id as u32),
5181        );
5182
5183        if removed {
5184            debug!(target: "feagi-bdu","Removed synapse: {} -> {}", source_neuron_id, target_neuron_id);
5185
5186            // CRITICAL: Update StateManager synapse count (for health_check endpoint)
5187            let state_manager = StateManager::instance();
5188            let state_manager = state_manager.read();
5189            let core_state = state_manager.get_core_state();
5190            core_state.subtract_synapse_count(1);
5191            if let Some(cortical_id) = source_cortical_id {
5192                state_manager
5193                    .subtract_cortical_area_outgoing_synapses(&cortical_id.as_base_64(), 1);
5194            }
5195            if let Some(cortical_id) = target_cortical_id {
5196                state_manager
5197                    .subtract_cortical_area_incoming_synapses(&cortical_id.as_base_64(), 1);
5198            }
5199        }
5200
5201        Ok(removed)
5202    }
5203
5204    // ========================================================================
5205    // BATCH OPERATIONS
5206    // ========================================================================
5207
5208    /// Batch create multiple neurons at once (SIMD-optimized)
5209    ///
5210    /// This is significantly faster than calling `add_neuron()` in a loop
5211    ///
5212    /// # Arguments
5213    ///
5214    /// * `cortical_id` - Target cortical area
5215    /// * `neurons` - Vector of neuron parameters (x, y, z, firing_threshold, leak, resting_potential, etc.)
5216    ///
5217    /// # Returns
5218    ///
5219    /// Vector of created neuron IDs
5220    ///
5221    pub fn batch_create_neurons(
5222        &mut self,
5223        cortical_id: &CorticalID,
5224        neurons: Vec<NeuronData>,
5225    ) -> BduResult<Vec<u64>> {
5226        // Get NPU
5227        let npu = self
5228            .npu
5229            .as_ref()
5230            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5231
5232        let mut npu_lock = npu
5233            .lock()
5234            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5235
5236        // Get cortical area to verify it exists and get its index
5237        let area = self.get_cortical_area(cortical_id).ok_or_else(|| {
5238            BduError::InvalidArea(format!("Cortical area {} not found", cortical_id))
5239        })?;
5240        let cortical_idx = area.cortical_idx;
5241
5242        let count = neurons.len();
5243
5244        // Extract parameters into separate vectors for batch operation
5245        let mut x_coords = Vec::with_capacity(count);
5246        let mut y_coords = Vec::with_capacity(count);
5247        let mut z_coords = Vec::with_capacity(count);
5248        let mut firing_thresholds = Vec::with_capacity(count);
5249        let mut threshold_limits = Vec::with_capacity(count);
5250        let mut leak_coeffs = Vec::with_capacity(count);
5251        let mut resting_potentials = Vec::with_capacity(count);
5252        let mut neuron_types = Vec::with_capacity(count);
5253        let mut refractory_periods = Vec::with_capacity(count);
5254        let mut excitabilities = Vec::with_capacity(count);
5255        let mut consec_fire_limits = Vec::with_capacity(count);
5256        let mut snooze_lengths = Vec::with_capacity(count);
5257        let mut mp_accums = Vec::with_capacity(count);
5258        let mut cortical_areas = Vec::with_capacity(count);
5259
5260        for (
5261            x,
5262            y,
5263            z,
5264            threshold,
5265            threshold_limit,
5266            leak,
5267            resting,
5268            ntype,
5269            refract,
5270            excit,
5271            consec_limit,
5272            snooze,
5273            mp_accum,
5274        ) in neurons
5275        {
5276            x_coords.push(x);
5277            y_coords.push(y);
5278            z_coords.push(z);
5279            firing_thresholds.push(threshold);
5280            threshold_limits.push(threshold_limit);
5281            leak_coeffs.push(leak);
5282            resting_potentials.push(resting);
5283            neuron_types.push(ntype);
5284            refractory_periods.push(refract);
5285            excitabilities.push(excit);
5286            consec_fire_limits.push(consec_limit);
5287            snooze_lengths.push(snooze);
5288            mp_accums.push(mp_accum);
5289            cortical_areas.push(cortical_idx);
5290        }
5291
5292        // Get the current neuron count - this will be the first ID of our batch
5293        let first_neuron_id = npu_lock.get_neuron_count() as u32;
5294
5295        // Call NPU batch creation (SIMD-optimized)
5296        // Signature: (thresholds, threshold_limits, leak_coeffs, resting_pots, neuron_types, refract, excit, consec_limits, snooze, mp_accums, cortical_areas, x, y, z)
5297        // Convert f32 vectors to T
5298        // DynamicNPU will handle f32 inputs and convert internally based on its precision
5299        let firing_thresholds_t = firing_thresholds;
5300        let threshold_limits_t = threshold_limits;
5301        let resting_potentials_t = resting_potentials;
5302        let (neurons_created, _indices) = npu_lock.add_neurons_batch(
5303            firing_thresholds_t,
5304            threshold_limits_t,
5305            leak_coeffs,
5306            resting_potentials_t,
5307            neuron_types,
5308            refractory_periods,
5309            excitabilities,
5310            consec_fire_limits,
5311            snooze_lengths,
5312            mp_accums,
5313            cortical_areas,
5314            x_coords,
5315            y_coords,
5316            z_coords,
5317        );
5318
5319        // Generate neuron IDs (they are sequential starting from first_neuron_id)
5320        let mut neuron_ids = Vec::with_capacity(count);
5321        for i in 0..neurons_created {
5322            neuron_ids.push((first_neuron_id + i) as u64);
5323        }
5324
5325        info!(target: "feagi-bdu","Batch created {} neurons in cortical area {}", count, cortical_id);
5326
5327        // CRITICAL: Update StateManager neuron count (for health_check endpoint)
5328        let state_manager = StateManager::instance();
5329        let state_manager = state_manager.read();
5330        let core_state = state_manager.get_core_state();
5331        core_state.add_neuron_count(neurons_created);
5332        core_state.add_regular_neuron_count(neurons_created);
5333        state_manager.add_cortical_area_neuron_count(&cortical_id.as_base_64(), count);
5334
5335        // Best-effort: keep per-area cache in sync for lock-free reads.
5336        {
5337            let mut cache = self.cached_neuron_counts_per_area.write();
5338            cache
5339                .entry(*cortical_id)
5340                .or_insert_with(|| AtomicUsize::new(0))
5341                .fetch_add(count, Ordering::Relaxed);
5342        }
5343
5344        Ok(neuron_ids)
5345    }
5346
5347    /// Delete multiple neurons at once (batch operation)
5348    ///
5349    /// # Arguments
5350    ///
5351    /// * `neuron_ids` - Vector of neuron IDs to delete
5352    ///
5353    /// # Returns
5354    ///
5355    /// Number of neurons actually deleted
5356    ///
5357    pub fn delete_neurons_batch(&mut self, neuron_ids: Vec<u64>) -> BduResult<usize> {
5358        // Get NPU
5359        let npu = self
5360            .npu
5361            .as_ref()
5362            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5363
5364        let mut npu_lock = npu
5365            .lock()
5366            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5367
5368        let mut deleted_count = 0;
5369        let mut per_area_deleted: std::collections::HashMap<String, usize> =
5370            std::collections::HashMap::new();
5371
5372        // Delete each neuron
5373        // Note: Could be optimized with a batch delete method in NPU if needed
5374        for neuron_id in neuron_ids {
5375            let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id as u32);
5376            let cortical_id = self.cortical_idx_to_id.get(&cortical_idx).cloned();
5377
5378            if npu_lock.delete_neuron(neuron_id as u32) {
5379                deleted_count += 1;
5380                if let Some(cortical_id) = cortical_id {
5381                    let key = cortical_id.as_base_64();
5382                    *per_area_deleted.entry(key).or_insert(0) += 1;
5383                }
5384            }
5385        }
5386
5387        info!(target: "feagi-bdu","Batch deleted {} neurons", deleted_count);
5388
5389        // CRITICAL: Update StateManager neuron count (for health_check endpoint)
5390        if deleted_count > 0 {
5391            let state_manager = StateManager::instance();
5392            let state_manager = state_manager.read();
5393            let core_state = state_manager.get_core_state();
5394            core_state.subtract_neuron_count(deleted_count as u32);
5395            core_state.subtract_regular_neuron_count(deleted_count as u32);
5396            for (cortical_id, count) in per_area_deleted {
5397                state_manager.subtract_cortical_area_neuron_count(&cortical_id, count);
5398            }
5399        }
5400
5401        // Trigger fatigue index recalculation after batch neuron deletion
5402        // NOTE: Disabled during genome loading to prevent blocking
5403        // if deleted_count > 0 {
5404        //     let _ = self.update_fatigue_index();
5405        // }
5406
5407        Ok(deleted_count)
5408    }
5409
5410    // ========================================================================
5411    // NEURON UPDATE OPERATIONS
5412    // ========================================================================
5413
5414    /// Update properties of an existing neuron
5415    ///
5416    /// # Arguments
5417    ///
5418    /// * `neuron_id` - Target neuron ID
5419    /// * `firing_threshold` - Optional new firing threshold
5420    /// * `leak_coefficient` - Optional new leak coefficient
5421    /// * `resting_potential` - Optional new resting potential
5422    /// * `excitability` - Optional new excitability
5423    ///
5424    /// # Returns
5425    ///
5426    /// `Ok(())` if neuron updated successfully
5427    ///
5428    pub fn update_neuron_properties(
5429        &mut self,
5430        neuron_id: u64,
5431        firing_threshold: Option<f32>,
5432        leak_coefficient: Option<f32>,
5433        resting_potential: Option<f32>,
5434        excitability: Option<f32>,
5435    ) -> BduResult<()> {
5436        // Get NPU
5437        let npu = self
5438            .npu
5439            .as_ref()
5440            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5441
5442        let mut npu_lock = npu
5443            .lock()
5444            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5445
5446        let neuron_id_u32 = neuron_id as u32;
5447
5448        // Verify neuron exists by trying to update at least one property
5449        let mut updated = false;
5450
5451        // Update properties if provided
5452        if let Some(threshold) = firing_threshold {
5453            if npu_lock.update_neuron_threshold(neuron_id_u32, threshold) {
5454                updated = true;
5455                debug!(target: "feagi-bdu","Updated neuron {} firing_threshold = {}", neuron_id, threshold);
5456            } else if !updated {
5457                return Err(BduError::InvalidNeuron(format!(
5458                    "Neuron {} not found",
5459                    neuron_id
5460                )));
5461            }
5462        }
5463
5464        if let Some(leak) = leak_coefficient {
5465            if npu_lock.update_neuron_leak(neuron_id_u32, leak) {
5466                updated = true;
5467                debug!(target: "feagi-bdu","Updated neuron {} leak_coefficient = {}", neuron_id, leak);
5468            } else if !updated {
5469                return Err(BduError::InvalidNeuron(format!(
5470                    "Neuron {} not found",
5471                    neuron_id
5472                )));
5473            }
5474        }
5475
5476        if let Some(resting) = resting_potential {
5477            if npu_lock.update_neuron_resting_potential(neuron_id_u32, resting) {
5478                updated = true;
5479                debug!(target: "feagi-bdu","Updated neuron {} resting_potential = {}", neuron_id, resting);
5480            } else if !updated {
5481                return Err(BduError::InvalidNeuron(format!(
5482                    "Neuron {} not found",
5483                    neuron_id
5484                )));
5485            }
5486        }
5487
5488        if let Some(excit) = excitability {
5489            if npu_lock.update_neuron_excitability(neuron_id_u32, excit) {
5490                updated = true;
5491                debug!(target: "feagi-bdu","Updated neuron {} excitability = {}", neuron_id, excit);
5492            } else if !updated {
5493                return Err(BduError::InvalidNeuron(format!(
5494                    "Neuron {} not found",
5495                    neuron_id
5496                )));
5497            }
5498        }
5499
5500        if !updated {
5501            return Err(BduError::Internal(
5502                "No properties provided for update".to_string(),
5503            ));
5504        }
5505
5506        info!(target: "feagi-bdu","Updated properties for neuron {}", neuron_id);
5507
5508        Ok(())
5509    }
5510
5511    /// Update the firing threshold of a specific neuron
5512    ///
5513    /// # Arguments
5514    ///
5515    /// * `neuron_id` - Target neuron ID
5516    /// * `new_threshold` - New firing threshold value
5517    ///
5518    /// # Returns
5519    ///
5520    /// `Ok(())` if threshold updated successfully
5521    ///
5522    pub fn set_neuron_firing_threshold(
5523        &mut self,
5524        neuron_id: u64,
5525        new_threshold: f32,
5526    ) -> BduResult<()> {
5527        // Get NPU
5528        let npu = self
5529            .npu
5530            .as_ref()
5531            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5532
5533        let mut npu_lock = npu
5534            .lock()
5535            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5536
5537        // Update threshold via NPU
5538        if npu_lock.update_neuron_threshold(neuron_id as u32, new_threshold) {
5539            debug!(target: "feagi-bdu","Set neuron {} firing threshold = {}", neuron_id, new_threshold);
5540            Ok(())
5541        } else {
5542            Err(BduError::InvalidNeuron(format!(
5543                "Neuron {} not found",
5544                neuron_id
5545            )))
5546        }
5547    }
5548
5549    // ========================================================================
5550    // AREA MANAGEMENT & QUERIES
5551    // ========================================================================
5552
5553    /// Get cortical area by name (alternative to ID lookup)
5554    ///
5555    /// # Arguments
5556    ///
5557    /// * `name` - Human-readable area name
5558    ///
5559    /// # Returns
5560    ///
5561    /// `Some(CorticalArea)` if found, `None` otherwise
5562    ///
5563    pub fn get_cortical_area_by_name(&self, name: &str) -> Option<CorticalArea> {
5564        self.cortical_areas
5565            .values()
5566            .find(|area| area.name == name)
5567            .cloned()
5568    }
5569
5570    /// Resize a cortical area (changes dimensions, may require neuron reallocation)
5571    ///
5572    /// # Arguments
5573    ///
5574    /// * `cortical_id` - Target cortical area ID
5575    /// * `new_dimensions` - New dimensions (width, height, depth)
5576    ///
5577    /// # Returns
5578    ///
5579    /// `Ok(())` if resized successfully
5580    ///
5581    /// # Note
5582    ///
5583    /// This does NOT automatically create/delete neurons. It only updates metadata.
5584    /// Caller must handle neuron population separately.
5585    ///
5586    pub fn resize_cortical_area(
5587        &mut self,
5588        cortical_id: &CorticalID,
5589        new_dimensions: CorticalAreaDimensions,
5590    ) -> BduResult<()> {
5591        // Validate dimensions
5592        if new_dimensions.width == 0 || new_dimensions.height == 0 || new_dimensions.depth == 0 {
5593            return Err(BduError::InvalidArea(format!(
5594                "Invalid dimensions: {:?} (all must be > 0)",
5595                new_dimensions
5596            )));
5597        }
5598
5599        // Get and update area
5600        let area = self.cortical_areas.get_mut(cortical_id).ok_or_else(|| {
5601            BduError::InvalidArea(format!("Cortical area {} not found", cortical_id))
5602        })?;
5603
5604        let old_dimensions = area.dimensions;
5605        area.dimensions = new_dimensions;
5606
5607        // Note: Visualization voxel granularity is user-driven, not recalculated on resize
5608        // If user had set a custom value, it remains; otherwise defaults to 1x1x1
5609
5610        info!(target: "feagi-bdu",
5611            "Resized cortical area {} from {:?} to {:?}",
5612            cortical_id,
5613            old_dimensions,
5614            new_dimensions
5615        );
5616
5617        self.refresh_cortical_area_hashes(false, true);
5618
5619        Ok(())
5620    }
5621
5622    /// Get all cortical areas in a brain region
5623    ///
5624    /// # Arguments
5625    ///
5626    /// * `region_id` - Brain region ID
5627    ///
5628    /// # Returns
5629    ///
5630    /// Vector of cortical area IDs in the region
5631    ///
5632    pub fn get_areas_in_region(&self, region_id: &str) -> BduResult<Vec<String>> {
5633        let region = self.brain_regions.get_region(region_id).ok_or_else(|| {
5634            BduError::InvalidArea(format!("Brain region {} not found", region_id))
5635        })?;
5636
5637        // Convert CorticalID to base64 strings
5638        Ok(region
5639            .cortical_areas
5640            .iter()
5641            .map(|id| id.as_base_64())
5642            .collect())
5643    }
5644
5645    /// Update brain region properties
5646    ///
5647    /// # Arguments
5648    ///
5649    /// * `region_id` - Target region ID
5650    /// * `new_name` - Optional new name
5651    /// * `new_description` - Optional new description
5652    ///
5653    /// # Returns
5654    ///
5655    /// `Ok(())` if updated successfully
5656    ///
5657    pub fn update_brain_region(
5658        &mut self,
5659        region_id: &str,
5660        new_name: Option<String>,
5661        new_description: Option<String>,
5662    ) -> BduResult<()> {
5663        let region = self
5664            .brain_regions
5665            .get_region_mut(region_id)
5666            .ok_or_else(|| {
5667                BduError::InvalidArea(format!("Brain region {} not found", region_id))
5668            })?;
5669
5670        if let Some(name) = new_name {
5671            region.name = name;
5672            debug!(target: "feagi-bdu","Updated brain region {} name", region_id);
5673        }
5674
5675        if let Some(desc) = new_description {
5676            // BrainRegion doesn't have a description field in the struct, so we'll store it in properties
5677            region
5678                .properties
5679                .insert("description".to_string(), serde_json::json!(desc));
5680            debug!(target: "feagi-bdu","Updated brain region {} description", region_id);
5681        }
5682
5683        info!(target: "feagi-bdu","Updated brain region {}", region_id);
5684
5685        self.refresh_brain_regions_hash();
5686
5687        Ok(())
5688    }
5689
5690    /// Update brain region properties with generic property map
5691    ///
5692    /// Supports updating any brain region property including coordinates, title, description, etc.
5693    ///
5694    /// # Arguments
5695    ///
5696    /// * `region_id` - Target region ID
5697    /// * `properties` - Map of property names to new values
5698    ///
5699    /// # Returns
5700    ///
5701    /// `Ok(())` if updated successfully
5702    ///
5703    pub fn update_brain_region_properties(
5704        &mut self,
5705        region_id: &str,
5706        properties: std::collections::HashMap<String, serde_json::Value>,
5707    ) -> BduResult<()> {
5708        use tracing::{debug, info};
5709
5710        let region = self
5711            .brain_regions
5712            .get_region_mut(region_id)
5713            .ok_or_else(|| {
5714                BduError::InvalidArea(format!("Brain region {} not found", region_id))
5715            })?;
5716
5717        for (key, value) in properties {
5718            match key.as_str() {
5719                "title" | "name" => {
5720                    if let Some(name) = value.as_str() {
5721                        region.name = name.to_string();
5722                        debug!(target: "feagi-bdu", "Updated brain region {} name = {}", region_id, name);
5723                    }
5724                }
5725                "coordinate_3d" | "coordinates_3d" => {
5726                    region
5727                        .properties
5728                        .insert("coordinate_3d".to_string(), value.clone());
5729                    debug!(target: "feagi-bdu", "Updated brain region {} coordinate_3d = {:?}", region_id, value);
5730                }
5731                "coordinate_2d" | "coordinates_2d" => {
5732                    region
5733                        .properties
5734                        .insert("coordinate_2d".to_string(), value.clone());
5735                    debug!(target: "feagi-bdu", "Updated brain region {} coordinate_2d = {:?}", region_id, value);
5736                }
5737                "description" => {
5738                    region
5739                        .properties
5740                        .insert("description".to_string(), value.clone());
5741                    debug!(target: "feagi-bdu", "Updated brain region {} description", region_id);
5742                }
5743                "region_type" => {
5744                    if let Some(type_str) = value.as_str() {
5745                        // Note: RegionType is currently a placeholder (Undefined only)
5746                        // Specific region types will be added in the future
5747                        region.region_type = feagi_structures::genomic::RegionType::Undefined;
5748                        debug!(target: "feagi-bdu", "Updated brain region {} type = {}", region_id, type_str);
5749                    }
5750                }
5751                // Store any other properties in the properties map
5752                _ => {
5753                    region.properties.insert(key.clone(), value.clone());
5754                    debug!(target: "feagi-bdu", "Updated brain region {} property {} = {:?}", region_id, key, value);
5755                }
5756            }
5757        }
5758
5759        info!(target: "feagi-bdu", "Updated brain region {} properties", region_id);
5760
5761        Ok(())
5762    }
5763
5764    // ========================================================================
5765    // NEURON QUERY METHODS (P6)
5766    // ========================================================================
5767
5768    /// Get neuron by 3D coordinates within a cortical area
5769    ///
5770    /// # Arguments
5771    ///
5772    /// * `cortical_id` - Cortical area ID
5773    /// * `x` - X coordinate
5774    /// * `y` - Y coordinate
5775    /// * `z` - Z coordinate
5776    ///
5777    /// # Returns
5778    ///
5779    /// `Some(neuron_id)` if found, `None` otherwise
5780    ///
5781    pub fn get_neuron_by_coordinates(
5782        &self,
5783        cortical_id: &CorticalID,
5784        x: u32,
5785        y: u32,
5786        z: u32,
5787    ) -> Option<u64> {
5788        // Get cortical area to get its index
5789        let area = self.get_cortical_area(cortical_id)?;
5790        let cortical_idx = area.cortical_idx;
5791
5792        // Query NPU via public method
5793        let npu = self.npu.as_ref()?;
5794        let npu_lock = npu.lock().ok()?;
5795
5796        npu_lock
5797            .get_neuron_id_at_coordinate(cortical_idx, x, y, z)
5798            .map(|id| id as u64)
5799    }
5800
5801    /// Get the position (coordinates) of a neuron
5802    ///
5803    /// # Arguments
5804    ///
5805    /// * `neuron_id` - Neuron ID
5806    ///
5807    /// # Returns
5808    ///
5809    /// `Some((x, y, z))` if found, `None` otherwise
5810    ///
5811    pub fn get_neuron_position(&self, neuron_id: u64) -> Option<(u32, u32, u32)> {
5812        let npu = self.npu.as_ref()?;
5813        let npu_lock = npu.lock().ok()?;
5814
5815        // Verify neuron exists and get coordinates
5816        let neuron_count = npu_lock.get_neuron_count();
5817        if (neuron_id as usize) >= neuron_count {
5818            return None;
5819        }
5820
5821        Some(
5822            npu_lock
5823                .get_neuron_coordinates(neuron_id as u32)
5824                .unwrap_or((0, 0, 0)),
5825        )
5826    }
5827
5828    /// Get which cortical area contains a specific neuron
5829    ///
5830    /// # Arguments
5831    ///
5832    /// * `neuron_id` - Neuron ID
5833    ///
5834    /// # Returns
5835    ///
5836    /// `Some(cortical_id)` if found, `None` otherwise
5837    ///
5838    pub fn get_cortical_area_for_neuron(&self, neuron_id: u64) -> Option<CorticalID> {
5839        let npu = self.npu.as_ref()?;
5840        let npu_lock = npu.lock().ok()?;
5841
5842        // Verify neuron exists
5843        let neuron_count = npu_lock.get_neuron_count();
5844        if (neuron_id as usize) >= neuron_count {
5845            return None;
5846        }
5847
5848        let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id as u32);
5849
5850        // Look up cortical_id from index
5851        self.cortical_areas
5852            .values()
5853            .find(|area| area.cortical_idx == cortical_idx)
5854            .map(|area| area.cortical_id)
5855    }
5856
5857    /// Get all properties of a neuron
5858    ///
5859    /// # Arguments
5860    ///
5861    /// * `neuron_id` - Neuron ID
5862    ///
5863    /// # Returns
5864    ///
5865    /// `Some(properties)` if found, `None` otherwise
5866    ///
5867    pub fn get_neuron_properties(
5868        &self,
5869        neuron_id: u64,
5870    ) -> Option<std::collections::HashMap<String, serde_json::Value>> {
5871        let npu = self.npu.as_ref()?;
5872        let npu_lock = npu.lock().ok()?;
5873
5874        let neuron_id_u32 = neuron_id as u32;
5875        let idx = neuron_id as usize;
5876
5877        // Verify neuron exists
5878        let neuron_count = npu_lock.get_neuron_count();
5879        if idx >= neuron_count {
5880            return None;
5881        }
5882
5883        let mut properties = std::collections::HashMap::new();
5884
5885        // Basic info
5886        properties.insert("neuron_id".to_string(), serde_json::json!(neuron_id));
5887
5888        // Get coordinates
5889        let (x, y, z) = npu_lock.get_neuron_coordinates(neuron_id_u32)?;
5890        properties.insert("x".to_string(), serde_json::json!(x));
5891        properties.insert("y".to_string(), serde_json::json!(y));
5892        properties.insert("z".to_string(), serde_json::json!(z));
5893
5894        // Get cortical area
5895        let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id_u32);
5896        properties.insert("cortical_area".to_string(), serde_json::json!(cortical_idx));
5897
5898        // Get neuron state (returns: consecutive_fire_count, consecutive_fire_limit, snooze_period, membrane_potential, threshold, refractory_countdown)
5899        if let Some((consec_count, consec_limit, snooze, mp, threshold, refract_countdown)) =
5900            npu_lock.get_neuron_state(NeuronId(neuron_id_u32))
5901        {
5902            properties.insert(
5903                "consecutive_fire_count".to_string(),
5904                serde_json::json!(consec_count),
5905            );
5906            properties.insert(
5907                "consecutive_fire_limit".to_string(),
5908                serde_json::json!(consec_limit),
5909            );
5910            properties.insert("snooze_period".to_string(), serde_json::json!(snooze));
5911            properties.insert("membrane_potential".to_string(), serde_json::json!(mp));
5912            properties.insert("threshold".to_string(), serde_json::json!(threshold));
5913            properties.insert(
5914                "refractory_countdown".to_string(),
5915                serde_json::json!(refract_countdown),
5916            );
5917        }
5918
5919        // Get other properties via get_neuron_property_by_index
5920        if let Some(leak) = npu_lock.get_neuron_property_by_index(idx, "leak_coefficient") {
5921            properties.insert("leak_coefficient".to_string(), serde_json::json!(leak));
5922        }
5923        if let Some(resting) = npu_lock.get_neuron_property_by_index(idx, "resting_potential") {
5924            properties.insert("resting_potential".to_string(), serde_json::json!(resting));
5925        }
5926        if let Some(excit) = npu_lock.get_neuron_property_by_index(idx, "excitability") {
5927            properties.insert("excitability".to_string(), serde_json::json!(excit));
5928        }
5929        if let Some(threshold_limit) = npu_lock.get_neuron_property_by_index(idx, "threshold_limit")
5930        {
5931            properties.insert(
5932                "threshold_limit".to_string(),
5933                serde_json::json!(threshold_limit),
5934            );
5935        }
5936
5937        // Get u16 properties
5938        if let Some(refract_period) =
5939            npu_lock.get_neuron_property_u16_by_index(idx, "refractory_period")
5940        {
5941            properties.insert(
5942                "refractory_period".to_string(),
5943                serde_json::json!(refract_period),
5944            );
5945        }
5946
5947        Some(properties)
5948    }
5949
5950    /// Get a specific property of a neuron
5951    ///
5952    /// # Arguments
5953    ///
5954    /// * `neuron_id` - Neuron ID
5955    /// * `property_name` - Name of the property to retrieve
5956    ///
5957    /// # Returns
5958    ///
5959    /// `Some(value)` if found, `None` otherwise
5960    ///
5961    pub fn get_neuron_property(
5962        &self,
5963        neuron_id: u64,
5964        property_name: &str,
5965    ) -> Option<serde_json::Value> {
5966        self.get_neuron_properties(neuron_id)?
5967            .get(property_name)
5968            .cloned()
5969    }
5970
5971    // ========================================================================
5972    // CORTICAL AREA LIST/QUERY METHODS (P6)
5973    // ========================================================================
5974
5975    /// Get all cortical area IDs
5976    ///
5977    /// # Returns
5978    ///
5979    /// Vector of all cortical area IDs
5980    ///
5981    pub fn get_all_cortical_ids(&self) -> Vec<CorticalID> {
5982        self.cortical_areas.keys().copied().collect()
5983    }
5984
5985    /// Get all cortical area indices
5986    ///
5987    /// # Returns
5988    ///
5989    /// Vector of all cortical area indices
5990    ///
5991    pub fn get_all_cortical_indices(&self) -> Vec<u32> {
5992        self.cortical_areas
5993            .values()
5994            .map(|area| area.cortical_idx)
5995            .collect()
5996    }
5997
5998    /// Get all cortical area names
5999    ///
6000    /// # Returns
6001    ///
6002    /// Vector of all cortical area names
6003    ///
6004    pub fn get_cortical_area_names(&self) -> Vec<String> {
6005        self.cortical_areas
6006            .values()
6007            .map(|area| area.name.clone())
6008            .collect()
6009    }
6010
6011    /// List all input (IPU/sensory) cortical areas
6012    ///
6013    /// # Returns
6014    ///
6015    /// Vector of IPU/sensory area IDs
6016    ///
6017    pub fn list_ipu_areas(&self) -> Vec<CorticalID> {
6018        use crate::models::CorticalAreaExt;
6019        self.cortical_areas
6020            .values()
6021            .filter(|area| area.is_input_area())
6022            .map(|area| area.cortical_id)
6023            .collect()
6024    }
6025
6026    /// List all output (OPU/motor) cortical areas
6027    ///
6028    /// # Returns
6029    ///
6030    /// Vector of OPU/motor area IDs
6031    ///
6032    pub fn list_opu_areas(&self) -> Vec<CorticalID> {
6033        use crate::models::CorticalAreaExt;
6034        self.cortical_areas
6035            .values()
6036            .filter(|area| area.is_output_area())
6037            .map(|area| area.cortical_id)
6038            .collect()
6039    }
6040
6041    /// Get maximum dimensions across all cortical areas
6042    ///
6043    /// # Returns
6044    ///
6045    /// (max_width, max_height, max_depth)
6046    ///
6047    pub fn get_max_cortical_area_dimensions(&self) -> (usize, usize, usize) {
6048        self.cortical_areas
6049            .values()
6050            .fold((0, 0, 0), |(max_w, max_h, max_d), area| {
6051                (
6052                    max_w.max(area.dimensions.width as usize),
6053                    max_h.max(area.dimensions.height as usize),
6054                    max_d.max(area.dimensions.depth as usize),
6055                )
6056            })
6057    }
6058
6059    /// Get all properties of a cortical area as a JSON-serializable map
6060    ///
6061    /// # Arguments
6062    ///
6063    /// * `cortical_id` - Cortical area ID
6064    ///
6065    /// # Returns
6066    ///
6067    /// `Some(properties)` if found, `None` otherwise
6068    ///
6069    pub fn get_cortical_area_properties(
6070        &self,
6071        cortical_id: &CorticalID,
6072    ) -> Option<std::collections::HashMap<String, serde_json::Value>> {
6073        let area = self.get_cortical_area(cortical_id)?;
6074
6075        let mut properties = std::collections::HashMap::new();
6076        properties.insert(
6077            "cortical_id".to_string(),
6078            serde_json::json!(area.cortical_id),
6079        );
6080        properties.insert(
6081            "cortical_id_s".to_string(),
6082            serde_json::json!(area.cortical_id.to_string()),
6083        );
6084        properties.insert(
6085            "cortical_idx".to_string(),
6086            serde_json::json!(area.cortical_idx),
6087        );
6088        properties.insert("name".to_string(), serde_json::json!(area.name));
6089        use crate::models::CorticalAreaExt;
6090        properties.insert(
6091            "area_type".to_string(),
6092            serde_json::json!(area.get_cortical_group()),
6093        );
6094        properties.insert(
6095            "dimensions".to_string(),
6096            serde_json::json!({
6097                "width": area.dimensions.width,
6098                "height": area.dimensions.height,
6099                "depth": area.dimensions.depth,
6100            }),
6101        );
6102        properties.insert("position".to_string(), serde_json::json!(area.position));
6103
6104        // Copy all properties from area.properties to the response
6105        for (key, value) in &area.properties {
6106            properties.insert(key.clone(), value.clone());
6107        }
6108
6109        // Add custom properties
6110        properties.extend(area.properties.clone());
6111
6112        Some(properties)
6113    }
6114
6115    /// Get properties of all cortical areas
6116    ///
6117    /// # Returns
6118    ///
6119    /// Vector of property maps for all areas
6120    ///
6121    pub fn get_all_cortical_area_properties(
6122        &self,
6123    ) -> Vec<std::collections::HashMap<String, serde_json::Value>> {
6124        self.cortical_areas
6125            .keys()
6126            .filter_map(|id| self.get_cortical_area_properties(id))
6127            .collect()
6128    }
6129
6130    // ========================================================================
6131    // BRAIN REGION QUERY METHODS (P6)
6132    // ========================================================================
6133
6134    /// Get all brain region IDs
6135    ///
6136    /// # Returns
6137    ///
6138    /// Vector of all brain region IDs
6139    ///
6140    pub fn get_all_brain_region_ids(&self) -> Vec<String> {
6141        self.brain_regions
6142            .get_all_region_ids()
6143            .into_iter()
6144            .cloned()
6145            .collect()
6146    }
6147
6148    /// Get all brain region names
6149    ///
6150    /// # Returns
6151    ///
6152    /// Vector of all brain region names
6153    ///
6154    pub fn get_brain_region_names(&self) -> Vec<String> {
6155        self.brain_regions
6156            .get_all_region_ids()
6157            .iter()
6158            .filter_map(|id| {
6159                self.brain_regions
6160                    .get_region(id)
6161                    .map(|region| region.name.clone())
6162            })
6163            .collect()
6164    }
6165
6166    /// Get properties of a brain region
6167    ///
6168    /// # Arguments
6169    ///
6170    /// * `region_id` - Brain region ID
6171    ///
6172    /// # Returns
6173    ///
6174    /// `Some(properties)` if found, `None` otherwise
6175    ///
6176    pub fn get_brain_region_properties(
6177        &self,
6178        region_id: &str,
6179    ) -> Option<std::collections::HashMap<String, serde_json::Value>> {
6180        let region = self.brain_regions.get_region(region_id)?;
6181
6182        let mut properties = std::collections::HashMap::new();
6183        properties.insert("region_id".to_string(), serde_json::json!(region.region_id));
6184        properties.insert("name".to_string(), serde_json::json!(region.name));
6185        properties.insert(
6186            "region_type".to_string(),
6187            serde_json::json!(format!("{:?}", region.region_type)),
6188        );
6189        properties.insert(
6190            "cortical_areas".to_string(),
6191            serde_json::json!(region.cortical_areas.iter().collect::<Vec<_>>()),
6192        );
6193
6194        // Add custom properties
6195        properties.extend(region.properties.clone());
6196
6197        Some(properties)
6198    }
6199
6200    /// Check if a cortical area exists
6201    ///
6202    /// # Arguments
6203    ///
6204    /// * `cortical_id` - Cortical area ID to check
6205    ///
6206    /// # Returns
6207    ///
6208    /// `true` if area exists, `false` otherwise
6209    ///
6210    pub fn cortical_area_exists(&self, cortical_id: &CorticalID) -> bool {
6211        self.cortical_areas.contains_key(cortical_id)
6212    }
6213
6214    /// Check if a brain region exists
6215    ///
6216    /// # Arguments
6217    ///
6218    /// * `region_id` - Brain region ID to check
6219    ///
6220    /// # Returns
6221    ///
6222    /// `true` if region exists, `false` otherwise
6223    ///
6224    pub fn brain_region_exists(&self, region_id: &str) -> bool {
6225        self.brain_regions.get_region(region_id).is_some()
6226    }
6227
6228    /// Get the total number of brain regions
6229    ///
6230    /// # Returns
6231    ///
6232    /// Number of brain regions
6233    ///
6234    pub fn get_brain_region_count(&self) -> usize {
6235        self.brain_regions.region_count()
6236    }
6237
6238    /// Get neurons by cortical area (alias for get_neurons_in_area for API compatibility)
6239    ///
6240    /// # Arguments
6241    ///
6242    /// * `cortical_id` - Cortical area ID
6243    ///
6244    /// # Returns
6245    ///
6246    /// Vector of neuron IDs in the area
6247    ///
6248    pub fn get_neurons_by_cortical_area(&self, cortical_id: &CorticalID) -> Vec<u64> {
6249        // This is an alias for get_neurons_in_area, which already exists
6250        // Keeping it for Python API compatibility
6251        // Note: The signature says Vec<NeuronId> but implementation returns Vec<u64>
6252        self.get_neurons_in_area(cortical_id)
6253    }
6254}
6255
6256// Manual Debug implementation (RustNPU doesn't implement Debug)
6257impl std::fmt::Debug for ConnectomeManager {
6258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6259        f.debug_struct("ConnectomeManager")
6260            .field("cortical_areas", &self.cortical_areas.len())
6261            .field("next_cortical_idx", &self.next_cortical_idx)
6262            .field("brain_regions", &self.brain_regions)
6263            .field(
6264                "npu",
6265                &if self.npu.is_some() {
6266                    "Connected"
6267                } else {
6268                    "Not connected"
6269                },
6270            )
6271            .field("initialized", &self.initialized)
6272            .finish()
6273    }
6274}
6275
6276#[cfg(test)]
6277mod tests {
6278    use super::*;
6279    use feagi_structures::genomic::cortical_area::CoreCorticalType;
6280
6281    #[test]
6282    fn test_singleton_instance() {
6283        let instance1 = ConnectomeManager::instance();
6284        let instance2 = ConnectomeManager::instance();
6285
6286        // Both should point to the same instance
6287        assert_eq!(Arc::strong_count(&instance1), Arc::strong_count(&instance2));
6288    }
6289
6290    #[test]
6291    fn test_add_cortical_area() {
6292        ConnectomeManager::reset_for_testing();
6293
6294        let instance = ConnectomeManager::instance();
6295        let mut manager = instance.write();
6296
6297        use feagi_structures::genomic::cortical_area::{
6298            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6299        };
6300        let cortical_id = CorticalID::try_from_bytes(b"cst_add_").unwrap(); // Use unique custom ID
6301        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6302        let area = CorticalArea::new(
6303            cortical_id,
6304            0,
6305            "Visual Input".to_string(),
6306            CorticalAreaDimensions::new(128, 128, 20).unwrap(),
6307            (0, 0, 0).into(),
6308            cortical_type,
6309        )
6310        .unwrap();
6311
6312        let initial_count = manager.get_cortical_area_count();
6313        let _cortical_idx = manager.add_cortical_area(area).unwrap();
6314
6315        assert_eq!(manager.get_cortical_area_count(), initial_count + 1);
6316        assert!(manager.has_cortical_area(&cortical_id));
6317        assert!(manager.is_initialized());
6318    }
6319
6320    #[test]
6321    fn test_cortical_area_lookups() {
6322        ConnectomeManager::reset_for_testing();
6323
6324        let instance = ConnectomeManager::instance();
6325        let mut manager = instance.write();
6326
6327        use feagi_structures::genomic::cortical_area::{
6328            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6329        };
6330        let cortical_id = CorticalID::try_from_bytes(b"cst_look").unwrap(); // Use unique custom ID
6331        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6332        let area = CorticalArea::new(
6333            cortical_id,
6334            0,
6335            "Test Area".to_string(),
6336            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
6337            (0, 0, 0).into(),
6338            cortical_type,
6339        )
6340        .unwrap();
6341
6342        let cortical_idx = manager.add_cortical_area(area).unwrap();
6343
6344        // ID -> idx lookup
6345        assert_eq!(manager.get_cortical_idx(&cortical_id), Some(cortical_idx));
6346
6347        // idx -> ID lookup
6348        assert_eq!(manager.get_cortical_id(cortical_idx), Some(&cortical_id));
6349
6350        // Get area
6351        let retrieved_area = manager.get_cortical_area(&cortical_id).unwrap();
6352        assert_eq!(retrieved_area.name, "Test Area");
6353    }
6354
6355    #[test]
6356    fn test_remove_cortical_area() {
6357        ConnectomeManager::reset_for_testing();
6358
6359        let instance = ConnectomeManager::instance();
6360        let mut manager = instance.write();
6361
6362        use feagi_structures::genomic::cortical_area::{
6363            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6364        };
6365        let cortical_id = CoreCorticalType::Power.to_cortical_id();
6366
6367        // Remove area if it already exists from previous tests
6368        if manager.has_cortical_area(&cortical_id) {
6369            manager.remove_cortical_area(&cortical_id).unwrap();
6370        }
6371
6372        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6373        let area = CorticalArea::new(
6374            cortical_id,
6375            0,
6376            "Test".to_string(),
6377            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
6378            (0, 0, 0).into(),
6379            cortical_type,
6380        )
6381        .unwrap();
6382
6383        let initial_count = manager.get_cortical_area_count();
6384        manager.add_cortical_area(area).unwrap();
6385        assert_eq!(manager.get_cortical_area_count(), initial_count + 1);
6386
6387        manager.remove_cortical_area(&cortical_id).unwrap();
6388        assert_eq!(manager.get_cortical_area_count(), initial_count);
6389        assert!(!manager.has_cortical_area(&cortical_id));
6390    }
6391
6392    #[test]
6393    fn test_duplicate_area_error() {
6394        ConnectomeManager::reset_for_testing();
6395
6396        let instance = ConnectomeManager::instance();
6397        let mut manager = instance.write();
6398
6399        use feagi_structures::genomic::cortical_area::{
6400            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6401        };
6402        // Use a unique ID only for this test to avoid collisions with other tests (e.g. Power)
6403        // when tests run in parallel; we still test duplicate by adding the same ID twice.
6404        let cortical_id = CorticalID::try_from_bytes(b"cst_dup1").unwrap();
6405        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6406        let area1 = CorticalArea::new(
6407            cortical_id,
6408            0,
6409            "First".to_string(),
6410            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
6411            (0, 0, 0).into(),
6412            cortical_type,
6413        )
6414        .unwrap();
6415
6416        let area2 = CorticalArea::new(
6417            cortical_id, // Same ID - duplicate
6418            1,
6419            "Second".to_string(),
6420            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
6421            (0, 0, 0).into(),
6422            cortical_type,
6423        )
6424        .unwrap();
6425
6426        manager.add_cortical_area(area1).unwrap();
6427        let result = manager.add_cortical_area(area2);
6428
6429        assert!(result.is_err());
6430    }
6431
6432    #[test]
6433    fn test_brain_region_management() {
6434        ConnectomeManager::reset_for_testing();
6435
6436        let instance = ConnectomeManager::instance();
6437        let mut manager = instance.write();
6438
6439        let region_id = feagi_structures::genomic::brain_regions::RegionID::new();
6440        let region_id_str = region_id.to_string();
6441        let root = BrainRegion::new(
6442            region_id,
6443            "Root".to_string(),
6444            feagi_structures::genomic::brain_regions::RegionType::Undefined,
6445        )
6446        .unwrap();
6447
6448        let initial_count = manager.get_brain_region_ids().len();
6449        manager.add_brain_region(root, None).unwrap();
6450
6451        assert_eq!(manager.get_brain_region_ids().len(), initial_count + 1);
6452        assert!(manager.get_brain_region(&region_id_str).is_some());
6453    }
6454
6455    #[test]
6456    fn test_synapse_operations() {
6457        use feagi_npu_burst_engine::npu::RustNPU;
6458        use feagi_npu_burst_engine::TracingMutex;
6459        use std::sync::Arc;
6460
6461        // Create NPU and manager for isolated test state
6462        use feagi_npu_burst_engine::backend::CPUBackend;
6463        use feagi_npu_burst_engine::DynamicNPU;
6464        use feagi_npu_runtime::StdRuntime;
6465
6466        let runtime = StdRuntime;
6467        let backend = CPUBackend::new();
6468        let npu_result =
6469            RustNPU::new(runtime, backend, 100, 1000, 10).expect("Failed to create NPU");
6470        let npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu_result), "TestNPU"));
6471        let mut manager = ConnectomeManager::new_for_testing_with_npu(npu.clone());
6472
6473        // First create a cortical area to add neurons to
6474        use feagi_structures::genomic::cortical_area::{
6475            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6476        };
6477        let cortical_id = CorticalID::try_from_bytes(b"cst_syn_").unwrap(); // Use unique custom ID
6478        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6479        let area = CorticalArea::new(
6480            cortical_id,
6481            0, // cortical_idx
6482            "Test Area".to_string(),
6483            CorticalAreaDimensions::new(10, 10, 1).unwrap(),
6484            (0, 0, 0).into(), // position
6485            cortical_type,
6486        )
6487        .unwrap();
6488        let cortical_idx = manager.add_cortical_area(area).unwrap();
6489
6490        // Register the cortical area with the NPU using the cortical ID's base64 representation
6491        if let Some(npu_arc) = manager.get_npu() {
6492            if let Ok(mut npu_guard) = npu_arc.try_lock() {
6493                if let DynamicNPU::F32(ref mut npu) = *npu_guard {
6494                    npu.register_cortical_area(cortical_idx, cortical_id.as_base_64());
6495                }
6496            }
6497        }
6498
6499        // Create two neurons
6500        let neuron1_id = manager
6501            .add_neuron(
6502                &cortical_id,
6503                0,
6504                0,
6505                0,     // coordinates
6506                100.0, // firing_threshold
6507                0.0,   // firing_threshold_limit (0 = no limit)
6508                0.1,   // leak_coefficient
6509                -60.0, // resting_potential
6510                0,     // neuron_type
6511                2,     // refractory_period
6512                1.0,   // excitability
6513                5,     // consecutive_fire_limit
6514                10,    // snooze_length
6515                false, // mp_charge_accumulation
6516            )
6517            .unwrap();
6518
6519        let neuron2_id = manager
6520            .add_neuron(
6521                &cortical_id,
6522                1,
6523                0,
6524                0, // coordinates
6525                100.0,
6526                f32::MAX, // firing_threshold_limit (MAX = no limit, SIMD-friendly encoding)
6527                0.1,
6528                -60.0,
6529                0,
6530                2,
6531                1.0,
6532                5,
6533                10,
6534                false,
6535            )
6536            .unwrap();
6537
6538        // Test create_synapse (creation should succeed)
6539        manager
6540            .create_synapse(
6541                neuron1_id, neuron2_id, 128, // weight
6542                64,  // psp
6543                0,   // excitatory
6544            )
6545            .unwrap();
6546
6547        // Note: Synapse retrieval/update/removal tests require full NPU propagation engine initialization
6548        // which is beyond the scope of this unit test. The important part is that create_synapse succeeds.
6549        println!("✅ Synapse creation test passed");
6550    }
6551
6552    #[test]
6553    fn test_apply_cortical_mapping_missing_rules_is_ok() {
6554        // This guards against a regression where deleting a mapping causes a 500 because
6555        // synapse regeneration treats "no mapping rules" as an error.
6556        let mut manager = ConnectomeManager::new_for_testing();
6557
6558        use feagi_structures::genomic::cortical_area::{
6559            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6560        };
6561
6562        let src_id = CorticalID::try_from_bytes(b"map_src_").unwrap();
6563        let dst_id = CorticalID::try_from_bytes(b"map_dst_").unwrap();
6564
6565        let src_area = CorticalArea::new(
6566            src_id,
6567            0,
6568            "src".to_string(),
6569            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6570            (0, 0, 0).into(),
6571            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
6572        )
6573        .unwrap();
6574
6575        let dst_area = CorticalArea::new(
6576            dst_id,
6577            1,
6578            "dst".to_string(),
6579            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6580            (0, 0, 0).into(),
6581            CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
6582        )
6583        .unwrap();
6584
6585        manager.add_cortical_area(src_area).unwrap();
6586        manager.add_cortical_area(dst_area).unwrap();
6587
6588        // No cortical_mapping_dst property set -> should be Ok(0), not an error
6589        let count = manager
6590            .apply_cortical_mapping_for_pair(&src_id, &dst_id)
6591            .unwrap();
6592        assert_eq!(count, 0);
6593
6594        // Now create then delete mapping; missing destination rules should still be Ok(0)
6595        manager
6596            .update_cortical_mapping(
6597                &src_id,
6598                &dst_id,
6599                vec![serde_json::json!({"morphology_id":"m1"})],
6600            )
6601            .unwrap();
6602        manager
6603            .update_cortical_mapping(&src_id, &dst_id, vec![])
6604            .unwrap();
6605
6606        let count2 = manager
6607            .apply_cortical_mapping_for_pair(&src_id, &dst_id)
6608            .unwrap();
6609        assert_eq!(count2, 0);
6610    }
6611
6612    #[test]
6613    fn test_get_mapping_rules_for_destination_supports_legacy_key() {
6614        let dst_id = CorticalID::try_from_bytes(b"csrc0002").unwrap();
6615        let mapping_dst = serde_json::json!({
6616            "csrc0002": [
6617                {"morphology_id": "m1"}
6618            ]
6619        });
6620        let mapping_obj = mapping_dst.as_object().expect("mapping must be an object");
6621
6622        let rules = ConnectomeManager::get_mapping_rules_for_destination(mapping_obj, &dst_id)
6623            .expect("legacy destination key should resolve");
6624        assert_eq!(rules.len(), 1);
6625        assert_eq!(
6626            rules[0].get("morphology_id").and_then(|v| v.as_str()),
6627            Some("m1")
6628        );
6629    }
6630
6631    #[test]
6632    fn test_mapping_deletion_prunes_synapses_between_areas() {
6633        use feagi_npu_burst_engine::backend::CPUBackend;
6634        use feagi_npu_burst_engine::RustNPU;
6635        use feagi_npu_burst_engine::TracingMutex;
6636        use feagi_npu_runtime::StdRuntime;
6637        use feagi_structures::genomic::cortical_area::{
6638            CorticalAreaDimensions, CorticalAreaType, IOCorticalAreaConfigurationFlag,
6639        };
6640        use std::sync::Arc;
6641
6642        // Create NPU and manager (small capacities for a deterministic unit test)
6643        let runtime = StdRuntime;
6644        let backend = CPUBackend::new();
6645        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6646        let dyn_npu = Arc::new(TracingMutex::new(
6647            feagi_npu_burst_engine::DynamicNPU::F32(npu),
6648            "TestNPU",
6649        ));
6650        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6651
6652        // Create two cortical areas
6653        let src_id = CorticalID::try_from_bytes(b"cst_src_").unwrap();
6654        let dst_id = CorticalID::try_from_bytes(b"cst_dst_").unwrap();
6655
6656        let src_area = CorticalArea::new(
6657            src_id,
6658            0,
6659            "src".to_string(),
6660            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6661            (0, 0, 0).into(),
6662            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
6663        )
6664        .unwrap();
6665        let dst_area = CorticalArea::new(
6666            dst_id,
6667            1,
6668            "dst".to_string(),
6669            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6670            (0, 0, 0).into(),
6671            CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
6672        )
6673        .unwrap();
6674
6675        manager.add_cortical_area(src_area).unwrap();
6676        manager.add_cortical_area(dst_area).unwrap();
6677
6678        // Add a couple neurons to each area
6679        let s0 = manager
6680            .add_neuron(&src_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6681            .unwrap();
6682        let s1 = manager
6683            .add_neuron(&src_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6684            .unwrap();
6685        let t0 = manager
6686            .add_neuron(&dst_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6687            .unwrap();
6688        let t1 = manager
6689            .add_neuron(&dst_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6690            .unwrap();
6691
6692        // Create synapses that represent an established mapping between the two areas
6693        manager.create_synapse(s0, t0, 128, 200, 0).unwrap();
6694        manager.create_synapse(s1, t1, 128, 200, 0).unwrap();
6695
6696        // Build index once before pruning
6697        {
6698            let mut npu = dyn_npu.lock().unwrap();
6699            npu.rebuild_synapse_index();
6700            assert_eq!(npu.get_synapse_count(), 2);
6701        }
6702
6703        // Simulate mapping deletion and regeneration: should prune synapses and not re-add any
6704        manager
6705            .update_cortical_mapping(&src_id, &dst_id, vec![])
6706            .unwrap();
6707        let created = manager
6708            .regenerate_synapses_for_mapping(&src_id, &dst_id)
6709            .unwrap();
6710        assert_eq!(created, 0);
6711
6712        // Verify synapses are gone (invalidated) and no outgoing synapses remain from the sources
6713        {
6714            let mut npu = dyn_npu.lock().unwrap();
6715            // Pruning invalidates synapses; rebuild the index so counts/outgoing queries reflect the current state.
6716            npu.rebuild_synapse_index();
6717            assert_eq!(npu.get_synapse_count(), 0);
6718            assert!(npu.get_outgoing_synapses(s0 as u32).is_empty());
6719            assert!(npu.get_outgoing_synapses(s1 as u32).is_empty());
6720        }
6721    }
6722
6723    #[test]
6724    fn test_mapping_update_prunes_synapses_between_areas() {
6725        use feagi_npu_burst_engine::backend::CPUBackend;
6726        use feagi_npu_burst_engine::RustNPU;
6727        use feagi_npu_burst_engine::TracingMutex;
6728        use feagi_npu_runtime::StdRuntime;
6729        use feagi_structures::genomic::cortical_area::{
6730            CorticalAreaDimensions, CorticalAreaType, IOCorticalAreaConfigurationFlag,
6731        };
6732        use std::sync::Arc;
6733
6734        // Create NPU and manager (small capacities for a deterministic unit test)
6735        let runtime = StdRuntime;
6736        let backend = CPUBackend::new();
6737        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6738        let dyn_npu = Arc::new(TracingMutex::new(
6739            feagi_npu_burst_engine::DynamicNPU::F32(npu),
6740            "TestNPU",
6741        ));
6742        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6743
6744        // Seed core morphologies so mapping regeneration can resolve function morphologies (e.g. "episodic_memory").
6745        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
6746
6747        // Create two cortical areas
6748        // Use valid custom cortical IDs (the `cst...` namespace).
6749        let src_id = CorticalID::try_from_bytes(b"cstupds1").unwrap();
6750        let dst_id = CorticalID::try_from_bytes(b"cstupdt1").unwrap();
6751
6752        let src_area = CorticalArea::new(
6753            src_id,
6754            0,
6755            "src".to_string(),
6756            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6757            (0, 0, 0).into(),
6758            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
6759        )
6760        .unwrap();
6761        let dst_area = CorticalArea::new(
6762            dst_id,
6763            0,
6764            "dst".to_string(),
6765            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6766            (0, 0, 0).into(),
6767            CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
6768        )
6769        .unwrap();
6770
6771        manager.add_cortical_area(src_area).unwrap();
6772        manager.add_cortical_area(dst_area).unwrap();
6773
6774        // Add a couple neurons to each area
6775        let s0 = manager
6776            .add_neuron(&src_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6777            .unwrap();
6778        let s1 = manager
6779            .add_neuron(&src_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6780            .unwrap();
6781        let t0 = manager
6782            .add_neuron(&dst_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6783            .unwrap();
6784        let t1 = manager
6785            .add_neuron(&dst_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6786            .unwrap();
6787
6788        // Create synapses that represent an established mapping between the two areas
6789        manager.create_synapse(s0, t0, 128, 200, 0).unwrap();
6790        manager.create_synapse(s1, t1, 128, 200, 0).unwrap();
6791
6792        // Build index once before pruning
6793        {
6794            let mut npu = dyn_npu.lock().unwrap();
6795            npu.rebuild_synapse_index();
6796            assert_eq!(npu.get_synapse_count(), 2);
6797        }
6798
6799        // Update mapping rules (non-empty) and regenerate.
6800        // This should prune the existing A→B synapses before re-applying the mapping.
6801        //
6802        // Use "episodic_memory" morphology to avoid creating physical synapses; the key assertion is that
6803        // the pre-existing synapses were pruned on update.
6804        manager
6805            .update_cortical_mapping(
6806                &src_id,
6807                &dst_id,
6808                vec![serde_json::json!({
6809                    "morphology_id": "episodic_memory",
6810                    "morphology_scalar": [1],
6811                    "postSynapticCurrent_multiplier": 1,
6812                    "plasticity_flag": false,
6813                    "plasticity_constant": 0,
6814                    "ltp_multiplier": 0,
6815                    "ltd_multiplier": 0,
6816                    "plasticity_window": 0,
6817                })],
6818            )
6819            .unwrap();
6820        let created = manager
6821            .regenerate_synapses_for_mapping(&src_id, &dst_id)
6822            .unwrap();
6823        assert_eq!(created, 0);
6824
6825        // Verify synapses are gone and no outgoing synapses remain from the sources
6826        {
6827            let mut npu = dyn_npu.lock().unwrap();
6828            // Pruning invalidates synapses; rebuild the index so counts/outgoing queries reflect the current state.
6829            npu.rebuild_synapse_index();
6830            assert_eq!(npu.get_synapse_count(), 0);
6831            assert!(npu.get_outgoing_synapses(s0 as u32).is_empty());
6832            assert!(npu.get_outgoing_synapses(s1 as u32).is_empty());
6833        }
6834    }
6835
6836    #[test]
6837    fn test_upstream_area_tracking() {
6838        // Test that upstream_cortical_areas property is maintained correctly
6839        use crate::models::cortical_area::CorticalArea;
6840        use feagi_npu_burst_engine::backend::CPUBackend;
6841        use feagi_npu_burst_engine::TracingMutex;
6842        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
6843        use feagi_npu_runtime::StdRuntime;
6844        use feagi_structures::genomic::cortical_area::{
6845            CorticalAreaDimensions, CorticalAreaType, CorticalID,
6846        };
6847
6848        // Create test manager with NPU
6849        let runtime = StdRuntime;
6850        let backend = CPUBackend::new();
6851        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6852        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
6853        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6854
6855        // Seed the morphology registry with core morphologies so mapping regeneration can run.
6856        // (new_for_testing_with_npu() intentionally starts empty.)
6857        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
6858
6859        // Create source area
6860        let src_id = CorticalID::try_from_bytes(b"csrc0000").unwrap();
6861        let src_area = CorticalArea::new(
6862            src_id,
6863            0,
6864            "Source Area".to_string(),
6865            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6866            (0, 0, 0).into(),
6867            CorticalAreaType::Custom(
6868                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
6869            ),
6870        )
6871        .unwrap();
6872        let src_idx = manager.add_cortical_area(src_area).unwrap();
6873
6874        // Create destination area (memory area)
6875        let dst_id = CorticalID::try_from_bytes(b"cdst0000").unwrap();
6876        let dst_area = CorticalArea::new(
6877            dst_id,
6878            0,
6879            "Dest Area".to_string(),
6880            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6881            (0, 0, 0).into(),
6882            CorticalAreaType::Custom(
6883                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
6884            ),
6885        )
6886        .unwrap();
6887        manager.add_cortical_area(dst_area).unwrap();
6888
6889        // Verify upstream_cortical_areas property was initialized to empty array
6890        {
6891            let dst_area = manager.get_cortical_area(&dst_id).unwrap();
6892            let upstream = dst_area.properties.get("upstream_cortical_areas").unwrap();
6893            assert!(
6894                upstream.as_array().unwrap().is_empty(),
6895                "Upstream areas should be empty initially"
6896            );
6897        }
6898
6899        // Create a mapping from src to dst
6900        let mapping_data = vec![serde_json::json!({
6901            "morphology_id": "episodic_memory",
6902            "morphology_scalar": 1,
6903            "postSynapticCurrent_multiplier": 1.0,
6904        })];
6905        manager
6906            .update_cortical_mapping(&src_id, &dst_id, mapping_data)
6907            .unwrap();
6908        manager
6909            .regenerate_synapses_for_mapping(&src_id, &dst_id)
6910            .unwrap();
6911
6912        // Verify src_idx was added to dst's upstream_cortical_areas
6913        {
6914            let upstream_areas = manager.get_upstream_cortical_areas(&dst_id);
6915            assert_eq!(upstream_areas.len(), 1, "Should have 1 upstream area");
6916            assert_eq!(
6917                upstream_areas[0], src_idx,
6918                "Upstream area should be src_idx"
6919            );
6920        }
6921
6922        // Delete the mapping
6923        manager
6924            .update_cortical_mapping(&src_id, &dst_id, vec![])
6925            .unwrap();
6926        manager
6927            .regenerate_synapses_for_mapping(&src_id, &dst_id)
6928            .unwrap();
6929
6930        // Verify src_idx was removed from dst's upstream_cortical_areas
6931        {
6932            let upstream_areas = manager.get_upstream_cortical_areas(&dst_id);
6933            assert_eq!(
6934                upstream_areas.len(),
6935                0,
6936                "Should have 0 upstream areas after deletion"
6937            );
6938        }
6939    }
6940
6941    #[test]
6942    fn test_refresh_upstream_areas_for_associative_memory_pairs() {
6943        use crate::models::cortical_area::CorticalArea;
6944        use feagi_npu_burst_engine::backend::CPUBackend;
6945        use feagi_npu_burst_engine::TracingMutex;
6946        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
6947        use feagi_npu_runtime::StdRuntime;
6948        use feagi_structures::genomic::cortical_area::{
6949            CorticalAreaDimensions, CorticalAreaType, CorticalID, MemoryCorticalType,
6950        };
6951        use std::sync::Arc;
6952
6953        let runtime = StdRuntime;
6954        let backend = CPUBackend::new();
6955        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6956        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
6957        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6958        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
6959
6960        let a1_id = CorticalID::try_from_bytes(b"csrc0002").unwrap();
6961        let a2_id = CorticalID::try_from_bytes(b"csrc0003").unwrap();
6962        let m1_id = CorticalID::try_from_bytes(b"mmem0002").unwrap();
6963        let m2_id = CorticalID::try_from_bytes(b"mmem0003").unwrap();
6964
6965        let a1_area = CorticalArea::new(
6966            a1_id,
6967            0,
6968            "A1".to_string(),
6969            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
6970            (0, 0, 0).into(),
6971            CorticalAreaType::Custom(
6972                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
6973            ),
6974        )
6975        .unwrap();
6976        let a2_area = CorticalArea::new(
6977            a2_id,
6978            0,
6979            "A2".to_string(),
6980            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
6981            (0, 0, 0).into(),
6982            CorticalAreaType::Custom(
6983                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
6984            ),
6985        )
6986        .unwrap();
6987
6988        let mut m1_area = CorticalArea::new(
6989            m1_id,
6990            0,
6991            "M1".to_string(),
6992            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
6993            (0, 0, 0).into(),
6994            CorticalAreaType::Memory(MemoryCorticalType::Memory),
6995        )
6996        .unwrap();
6997        m1_area
6998            .properties
6999            .insert("is_mem_type".to_string(), serde_json::json!(true));
7000        m1_area
7001            .properties
7002            .insert("temporal_depth".to_string(), serde_json::json!(1));
7003
7004        let mut m2_area = CorticalArea::new(
7005            m2_id,
7006            0,
7007            "M2".to_string(),
7008            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
7009            (0, 0, 0).into(),
7010            CorticalAreaType::Memory(MemoryCorticalType::Memory),
7011        )
7012        .unwrap();
7013        m2_area
7014            .properties
7015            .insert("is_mem_type".to_string(), serde_json::json!(true));
7016        m2_area
7017            .properties
7018            .insert("temporal_depth".to_string(), serde_json::json!(1));
7019
7020        let a1_idx = manager.add_cortical_area(a1_area).unwrap();
7021        let a2_idx = manager.add_cortical_area(a2_area).unwrap();
7022        let m1_idx = manager.add_cortical_area(m1_area).unwrap();
7023        let m2_idx = manager.add_cortical_area(m2_area).unwrap();
7024
7025        manager
7026            .add_neuron(&a1_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7027            .unwrap();
7028        manager
7029            .add_neuron(&a2_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7030            .unwrap();
7031
7032        let episodic_mapping = vec![serde_json::json!({
7033            "morphology_id": "episodic_memory",
7034            "morphology_scalar": 1,
7035            "postSynapticCurrent_multiplier": 1.0,
7036        })];
7037        manager
7038            .update_cortical_mapping(&a1_id, &m1_id, episodic_mapping.clone())
7039            .unwrap();
7040        manager
7041            .regenerate_synapses_for_mapping(&a1_id, &m1_id)
7042            .unwrap();
7043        manager
7044            .update_cortical_mapping(&a2_id, &m2_id, episodic_mapping)
7045            .unwrap();
7046        manager
7047            .regenerate_synapses_for_mapping(&a2_id, &m2_id)
7048            .unwrap();
7049
7050        let assoc_mapping = vec![serde_json::json!({
7051            "morphology_id": "associative_memory",
7052            "morphology_scalar": 1,
7053            "postSynapticCurrent_multiplier": 1.0,
7054            "plasticity_flag": true,
7055            "plasticity_constant": 1,
7056            "ltp_multiplier": 1,
7057            "ltd_multiplier": 1,
7058            "plasticity_window": 5,
7059        })];
7060        manager
7061            .update_cortical_mapping(&m1_id, &m2_id, assoc_mapping)
7062            .unwrap();
7063        manager
7064            .regenerate_synapses_for_mapping(&m1_id, &m2_id)
7065            .unwrap();
7066
7067        let upstream_m1 = manager.get_upstream_cortical_areas(&m1_id);
7068        let upstream_m2 = manager.get_upstream_cortical_areas(&m2_id);
7069        assert_eq!(
7070            upstream_m1.len(),
7071            1,
7072            "M1 should have only 1 upstream before refresh"
7073        );
7074        assert_eq!(
7075            upstream_m2.len(),
7076            2,
7077            "M2 should have 2 upstreams before refresh"
7078        );
7079
7080        manager.refresh_upstream_cortical_areas_from_mappings(&m1_id);
7081        manager.refresh_upstream_cortical_areas_from_mappings(&m2_id);
7082
7083        let upstream_m1 = manager.get_upstream_cortical_areas(&m1_id);
7084        let upstream_m2 = manager.get_upstream_cortical_areas(&m2_id);
7085        assert_eq!(
7086            upstream_m1.len(),
7087            2,
7088            "M1 should have 2 upstreams after refresh"
7089        );
7090        assert_eq!(
7091            upstream_m2.len(),
7092            2,
7093            "M2 should have 2 upstreams after refresh"
7094        );
7095        assert!(upstream_m1.contains(&a1_idx));
7096        assert!(upstream_m1.contains(&m2_idx));
7097        assert!(upstream_m2.contains(&a2_idx));
7098        assert!(upstream_m2.contains(&m1_idx));
7099
7100        // Fire upstream neurons and ensure burst processing works without altering upstream tracking.
7101        {
7102            let mut npu_lock = dyn_npu.lock().unwrap();
7103            let injected_a1 = npu_lock.inject_sensory_xyzp_by_id(&a1_id, &[(0, 0, 0, 1.0)]);
7104            let injected_a2 = npu_lock.inject_sensory_xyzp_by_id(&a2_id, &[(0, 0, 0, 1.0)]);
7105            assert_eq!(injected_a1, 1, "Expected A1 injection to match one neuron");
7106            assert_eq!(injected_a2, 1, "Expected A2 injection to match one neuron");
7107            npu_lock.process_burst().expect("Burst processing failed");
7108        }
7109
7110        let upstream_m1 = manager.get_upstream_cortical_areas(&m1_id);
7111        let upstream_m2 = manager.get_upstream_cortical_areas(&m2_id);
7112        assert_eq!(
7113            upstream_m1.len(),
7114            2,
7115            "M1 should keep 2 upstreams after firing"
7116        );
7117        assert_eq!(
7118            upstream_m2.len(),
7119            2,
7120            "M2 should keep 2 upstreams after firing"
7121        );
7122    }
7123
7124    #[test]
7125    fn test_memory_twin_created_for_memory_mapping() {
7126        use crate::models::cortical_area::CorticalArea;
7127        use feagi_npu_burst_engine::backend::CPUBackend;
7128        use feagi_npu_burst_engine::TracingMutex;
7129        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
7130        use feagi_npu_runtime::StdRuntime;
7131        use feagi_structures::genomic::cortical_area::{
7132            CorticalAreaDimensions, CorticalAreaType, CorticalID, IOCorticalAreaConfigurationFlag,
7133            MemoryCorticalType,
7134        };
7135        use std::sync::Arc;
7136
7137        let runtime = StdRuntime;
7138        let backend = CPUBackend::new();
7139        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
7140        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
7141        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
7142        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
7143
7144        let src_id = CorticalID::try_from_bytes(b"csrc0001").unwrap();
7145        let dst_id = CorticalID::try_from_bytes(b"mmem0001").unwrap();
7146
7147        let src_area = CorticalArea::new(
7148            src_id,
7149            0,
7150            "Source Area".to_string(),
7151            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7152            (0, 0, 0).into(),
7153            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
7154        )
7155        .unwrap();
7156        let mut dst_area = CorticalArea::new(
7157            dst_id,
7158            0,
7159            "Memory Area".to_string(),
7160            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7161            (0, 0, 0).into(),
7162            CorticalAreaType::Memory(MemoryCorticalType::Memory),
7163        )
7164        .unwrap();
7165        dst_area
7166            .properties
7167            .insert("is_mem_type".to_string(), serde_json::json!(true));
7168        dst_area
7169            .properties
7170            .insert("temporal_depth".to_string(), serde_json::json!(1));
7171
7172        manager.add_cortical_area(src_area).unwrap();
7173        manager.add_cortical_area(dst_area).unwrap();
7174
7175        let mapping_data = vec![serde_json::json!({
7176            "morphology_id": "episodic_memory",
7177            "morphology_scalar": 1,
7178            "postSynapticCurrent_multiplier": 1.0,
7179        })];
7180        manager
7181            .update_cortical_mapping(&src_id, &dst_id, mapping_data)
7182            .unwrap();
7183        manager
7184            .regenerate_synapses_for_mapping(&src_id, &dst_id)
7185            .unwrap();
7186
7187        let memory_area = manager.get_cortical_area(&dst_id).unwrap();
7188        let twin_map = memory_area
7189            .properties
7190            .get("memory_twin_areas")
7191            .and_then(|v| v.as_object())
7192            .expect("memory_twin_areas should be set");
7193        let twin_id_str = twin_map
7194            .get(&src_id.as_base_64())
7195            .and_then(|v| v.as_str())
7196            .expect("Missing twin entry for upstream area");
7197        let twin_id = CorticalID::try_from_base_64(twin_id_str).unwrap();
7198        let mapping = memory_area
7199            .properties
7200            .get("cortical_mapping_dst")
7201            .and_then(|v| v.as_object())
7202            .and_then(|map| map.get(&twin_id.as_base_64()))
7203            .and_then(|v| v.as_array())
7204            .expect("Missing memory replay mapping for twin area");
7205        let uses_replay = mapping.iter().any(|rule| {
7206            rule.get("morphology_id")
7207                .and_then(|v| v.as_str())
7208                .is_some_and(|id| id == "memory_replay")
7209        });
7210        assert!(uses_replay, "Expected memory_replay mapping for twin area");
7211
7212        let twin_area = manager.get_cortical_area(&twin_id).unwrap();
7213        assert!(matches!(
7214            twin_area.cortical_type,
7215            CorticalAreaType::Custom(_)
7216        ));
7217        assert_eq!(
7218            twin_area
7219                .properties
7220                .get("memory_twin_of")
7221                .and_then(|v| v.as_str()),
7222            Some(src_id.as_base_64().as_str())
7223        );
7224        assert_eq!(
7225            twin_area
7226                .properties
7227                .get("memory_twin_for")
7228                .and_then(|v| v.as_str()),
7229            Some(dst_id.as_base_64().as_str())
7230        );
7231    }
7232
7233    #[test]
7234    fn test_associative_memory_between_memory_areas_creates_synapses() {
7235        use crate::models::cortical_area::CorticalArea;
7236        use feagi_npu_burst_engine::backend::CPUBackend;
7237        use feagi_npu_burst_engine::TracingMutex;
7238        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
7239        use feagi_npu_runtime::StdRuntime;
7240        use feagi_structures::genomic::cortical_area::{
7241            CorticalAreaDimensions, CorticalAreaType, CorticalID, MemoryCorticalType,
7242        };
7243        use std::sync::Arc;
7244
7245        let runtime = StdRuntime;
7246        let backend = CPUBackend::new();
7247        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
7248        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
7249        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
7250        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
7251
7252        let m1_id = CorticalID::try_from_bytes(b"mmem0402").unwrap();
7253        let m2_id = CorticalID::try_from_bytes(b"mmem0403").unwrap();
7254
7255        let mut m1_area = CorticalArea::new(
7256            m1_id,
7257            0,
7258            "Memory M1".to_string(),
7259            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
7260            (0, 0, 0).into(),
7261            CorticalAreaType::Memory(MemoryCorticalType::Memory),
7262        )
7263        .unwrap();
7264        m1_area
7265            .properties
7266            .insert("is_mem_type".to_string(), serde_json::json!(true));
7267        m1_area
7268            .properties
7269            .insert("temporal_depth".to_string(), serde_json::json!(1));
7270
7271        let mut m2_area = CorticalArea::new(
7272            m2_id,
7273            0,
7274            "Memory M2".to_string(),
7275            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
7276            (0, 0, 0).into(),
7277            CorticalAreaType::Memory(MemoryCorticalType::Memory),
7278        )
7279        .unwrap();
7280        m2_area
7281            .properties
7282            .insert("is_mem_type".to_string(), serde_json::json!(true));
7283        m2_area
7284            .properties
7285            .insert("temporal_depth".to_string(), serde_json::json!(1));
7286
7287        manager.add_cortical_area(m1_area).unwrap();
7288        manager.add_cortical_area(m2_area).unwrap();
7289
7290        manager
7291            .add_neuron(&m1_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7292            .unwrap();
7293        manager
7294            .add_neuron(&m2_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7295            .unwrap();
7296
7297        let mapping_data = vec![serde_json::json!({
7298            "morphology_id": "associative_memory",
7299            "morphology_scalar": 1,
7300            "postSynapticCurrent_multiplier": 1.0,
7301            "plasticity_flag": true,
7302            "plasticity_constant": 1,
7303            "ltp_multiplier": 1,
7304            "ltd_multiplier": 1,
7305            "plasticity_window": 5,
7306        })];
7307        manager
7308            .update_cortical_mapping(&m1_id, &m2_id, mapping_data)
7309            .unwrap();
7310        let created = manager
7311            .regenerate_synapses_for_mapping(&m1_id, &m2_id)
7312            .unwrap();
7313        assert!(
7314            created > 0,
7315            "Expected associative memory mapping between memory areas to create synapses"
7316        );
7317    }
7318
7319    #[test]
7320    fn test_memory_twin_repair_on_load_preserves_replay_mapping() {
7321        use crate::models::cortical_area::CorticalArea;
7322        use feagi_npu_burst_engine::backend::CPUBackend;
7323        use feagi_npu_burst_engine::TracingMutex;
7324        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
7325        use feagi_npu_runtime::StdRuntime;
7326        use feagi_structures::genomic::cortical_area::{
7327            CorticalAreaDimensions, CorticalAreaType, CorticalID, IOCorticalAreaConfigurationFlag,
7328            MemoryCorticalType,
7329        };
7330        use std::sync::Arc;
7331
7332        let runtime = StdRuntime;
7333        let backend = CPUBackend::new();
7334        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
7335        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
7336        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
7337        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
7338
7339        let src_id = CorticalID::try_from_bytes(b"csrc0002").unwrap();
7340        let mem_id = CorticalID::try_from_bytes(b"mmem0002").unwrap();
7341
7342        let src_area = CorticalArea::new(
7343            src_id,
7344            0,
7345            "Source Area".to_string(),
7346            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7347            (0, 0, 0).into(),
7348            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
7349        )
7350        .unwrap();
7351        let mut mem_area = CorticalArea::new(
7352            mem_id,
7353            0,
7354            "Memory Area".to_string(),
7355            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7356            (0, 0, 0).into(),
7357            CorticalAreaType::Memory(MemoryCorticalType::Memory),
7358        )
7359        .unwrap();
7360        mem_area
7361            .properties
7362            .insert("is_mem_type".to_string(), serde_json::json!(true));
7363        mem_area
7364            .properties
7365            .insert("temporal_depth".to_string(), serde_json::json!(1));
7366
7367        manager.add_cortical_area(src_area).unwrap();
7368        manager.add_cortical_area(mem_area).unwrap();
7369
7370        let twin_id = manager
7371            .build_memory_twin_id(&mem_id, &src_id)
7372            .expect("Failed to build twin id");
7373        let twin_area = CorticalArea::new(
7374            twin_id,
7375            0,
7376            "Source Area_twin".to_string(),
7377            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7378            (0, 0, 0).into(),
7379            CorticalAreaType::Custom(
7380                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
7381            ),
7382        )
7383        .unwrap();
7384        manager.add_cortical_area(twin_area).unwrap();
7385
7386        let repaired = manager
7387            .ensure_memory_twin_area(&mem_id, &src_id)
7388            .expect("Failed to repair twin");
7389        assert_eq!(repaired, twin_id);
7390
7391        let mem_area = manager.get_cortical_area(&mem_id).unwrap();
7392        let twin_map = mem_area
7393            .properties
7394            .get("memory_twin_areas")
7395            .and_then(|v| v.as_object())
7396            .expect("memory_twin_areas should be set");
7397        let twin_id_str = twin_map
7398            .get(&src_id.as_base_64())
7399            .and_then(|v| v.as_str())
7400            .expect("Missing twin entry for upstream area");
7401        assert_eq!(twin_id_str, twin_id.as_base_64());
7402
7403        let replay_map = mem_area
7404            .properties
7405            .get("cortical_mapping_dst")
7406            .and_then(|v| v.as_object())
7407            .and_then(|map| map.get(&twin_id.as_base_64()))
7408            .and_then(|v| v.as_array())
7409            .expect("Missing memory replay mapping for twin area");
7410        let uses_replay = replay_map.iter().any(|rule| {
7411            rule.get("morphology_id")
7412                .and_then(|v| v.as_str())
7413                .is_some_and(|id| id == "memory_replay")
7414        });
7415        assert!(uses_replay, "Expected memory_replay mapping for twin area");
7416
7417        let twin_area = manager.get_cortical_area(&twin_id).unwrap();
7418        assert_eq!(
7419            twin_area
7420                .properties
7421                .get("memory_twin_of")
7422                .and_then(|v| v.as_str()),
7423            Some(src_id.as_base_64().as_str())
7424        );
7425        assert_eq!(
7426            twin_area
7427                .properties
7428                .get("memory_twin_for")
7429                .and_then(|v| v.as_str()),
7430            Some(mem_id.as_base_64().as_str())
7431        );
7432    }
7433}