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