Skip to main content

feagi_brain_development/
connectome_manager.rs

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