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