1use 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
41pub type BrainRegionIoRegistry = HashMap<String, (Vec<String>, Vec<String>)>;
43
44use crate::models::{BrainRegion, BrainRegionHierarchy, CorticalArea, CorticalAreaDimensions};
45use crate::types::{BduError, BduResult};
46use feagi_npu_neural::synapse::SYNAPSE_EDGE_ASSOCIATIVE_MEMORY;
47use feagi_npu_neural::types::NeuronId;
48use feagi_structures::genomic::cortical_area::{
49 CoreCorticalType, CorticalAreaType, CorticalID, CustomCorticalType,
50};
51use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
52
53use feagi_state_manager::StateManager;
56
57const DATA_HASH_SEED: u64 = 0;
58const HASH_SAFE_MASK: u64 = (1u64 << 53) - 1;
60
61static INSTANCE: Lazy<Arc<RwLock<ConnectomeManager>>> =
66 Lazy::new(|| Arc::new(RwLock::new(ConnectomeManager::new())));
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ConnectomeConfig {
71 pub max_neurons: usize,
73
74 pub max_synapses: usize,
76
77 pub backend: String,
79}
80
81impl Default for ConnectomeConfig {
82 fn default() -> Self {
83 Self {
84 max_neurons: 10_000_000,
85 max_synapses: 100_000_000,
86 backend: "cpu".to_string(),
87 }
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct MemoryTwinDiagnostic {
94 pub src_cortical_area: String,
95 pub dst_cortical_area: String,
96 pub src_cortical_type: String,
97 pub dst_cortical_type: String,
98 pub mapping_exists: bool,
99 pub episodic_rule_count: usize,
100 pub twin_expected: bool,
101 pub twin_present: bool,
102 pub twin_cortical_area: Option<String>,
103 pub reason: Option<String>,
104 pub src_parent_region_id: Option<String>,
105 pub dst_parent_region_id: Option<String>,
106 pub twin_parent_region_id: Option<String>,
107 pub twin_parent_matches_src_parent: Option<bool>,
108}
109
110pub struct ConnectomeManager {
132 cortical_areas: HashMap<CorticalID, CorticalArea>,
134
135 cortical_id_to_idx: HashMap<CorticalID, u32>,
137
138 cortical_idx_to_id: HashMap<u32, CorticalID>,
140
141 next_cortical_idx: u32,
143
144 brain_regions: BrainRegionHierarchy,
146
147 morphology_registry: feagi_evolutionary::MorphologyRegistry,
149
150 config: ConnectomeConfig,
152
153 npu: Option<Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>>,
159
160 #[cfg(feature = "plasticity")]
162 plasticity_executor:
163 Option<Arc<std::sync::Mutex<feagi_npu_plasticity::AsyncPlasticityExecutor>>>,
164
165 cached_neuron_count: Arc<AtomicUsize>,
168
169 cached_synapse_count: Arc<AtomicUsize>,
172
173 cached_neuron_counts_per_area: Arc<RwLock<HashMap<CorticalID, AtomicUsize>>>,
176
177 cached_synapse_counts_per_area: Arc<RwLock<HashMap<CorticalID, AtomicUsize>>>,
180
181 initialized: bool,
183
184 last_fatigue_calculation: Arc<Mutex<std::time::Instant>>,
186}
187
188type NeuronData = (
190 u32,
191 u32,
192 u32,
193 f32,
194 f32,
195 f32,
196 f32,
197 i32,
198 u16,
199 f32,
200 u16,
201 u16,
202 bool,
203);
204
205impl ConnectomeManager {
206 fn get_mapping_rules_for_destination<'a>(
207 mapping_dst: &'a serde_json::Map<String, serde_json::Value>,
208 dst_area_id: &CorticalID,
209 ) -> Option<&'a Vec<serde_json::Value>> {
210 if let Some(rules) = mapping_dst
211 .get(&dst_area_id.as_base_64())
212 .and_then(|value| value.as_array())
213 {
214 return Some(rules);
215 }
216
217 for (raw_dst_key, rules_value) in mapping_dst {
220 let parsed_dst = CorticalID::try_from_base_64(raw_dst_key)
221 .or_else(|_| CorticalID::try_from_legacy_ascii(raw_dst_key));
222 if parsed_dst.as_ref().ok() != Some(dst_area_id) {
223 continue;
224 }
225 if let Some(rules) = rules_value.as_array() {
226 return Some(rules);
227 }
228 }
229
230 None
231 }
232
233 fn new() -> Self {
235 Self {
236 cortical_areas: HashMap::new(),
237 cortical_id_to_idx: HashMap::new(),
238 cortical_idx_to_id: HashMap::new(),
239 next_cortical_idx: 7, brain_regions: BrainRegionHierarchy::new(),
242 morphology_registry: feagi_evolutionary::MorphologyRegistry::new(),
243 config: ConnectomeConfig::default(),
244 npu: None,
245 #[cfg(feature = "plasticity")]
246 plasticity_executor: None,
247 cached_neuron_count: Arc::new(AtomicUsize::new(0)),
248 cached_synapse_count: Arc::new(AtomicUsize::new(0)),
249 cached_neuron_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
250 cached_synapse_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
251 initialized: false,
252 last_fatigue_calculation: Arc::new(Mutex::new(
253 std::time::Instant::now() - std::time::Duration::from_secs(10),
254 )), }
256 }
257
258 pub fn instance() -> Arc<RwLock<ConnectomeManager>> {
275 Arc::clone(&*INSTANCE)
278 }
279
280 pub fn new_for_testing() -> Self {
308 Self {
309 cortical_areas: HashMap::new(),
310 cortical_id_to_idx: HashMap::new(),
311 cortical_idx_to_id: HashMap::new(),
312 next_cortical_idx: 0,
313 brain_regions: BrainRegionHierarchy::new(),
314 morphology_registry: feagi_evolutionary::MorphologyRegistry::new(),
315 config: ConnectomeConfig::default(),
316 npu: None,
317 #[cfg(feature = "plasticity")]
318 plasticity_executor: None,
319 cached_neuron_count: Arc::new(AtomicUsize::new(0)),
320 cached_synapse_count: Arc::new(AtomicUsize::new(0)),
321 cached_neuron_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
322 cached_synapse_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
323 initialized: false,
324 last_fatigue_calculation: Arc::new(Mutex::new(
325 std::time::Instant::now() - std::time::Duration::from_secs(10),
326 )),
327 }
328 }
329
330 pub fn new_for_testing_with_npu(
346 npu: Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
347 ) -> Self {
348 Self {
349 cortical_areas: HashMap::new(),
350 cortical_id_to_idx: HashMap::new(),
351 cortical_idx_to_id: HashMap::new(),
352 next_cortical_idx: 7,
353 brain_regions: BrainRegionHierarchy::new(),
354 morphology_registry: feagi_evolutionary::MorphologyRegistry::new(),
355 config: ConnectomeConfig::default(),
356 npu: Some(npu),
357 #[cfg(feature = "plasticity")]
358 plasticity_executor: None,
359 cached_neuron_count: Arc::new(AtomicUsize::new(0)),
360 cached_synapse_count: Arc::new(AtomicUsize::new(0)),
361 cached_neuron_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
362 cached_synapse_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
363 initialized: false,
364 last_fatigue_calculation: Arc::new(Mutex::new(
365 std::time::Instant::now() - std::time::Duration::from_secs(10),
366 )),
367 }
368 }
369
370 pub fn setup_core_morphologies_for_testing(&mut self) {
380 feagi_evolutionary::add_core_morphologies(&mut self.morphology_registry);
381 }
382
383 #[cfg(test)]
392 pub fn reset_for_testing() {
393 let mut instance = INSTANCE.write();
394 *instance = Self::new();
395 }
396
397 fn update_state_hashes(
403 &self,
404 brain_regions: Option<u64>,
405 cortical_areas: Option<u64>,
406 brain_geometry: Option<u64>,
407 morphologies: Option<u64>,
408 cortical_mappings: Option<u64>,
409 ) {
410 let state_manager = StateManager::instance();
411 let state_manager = state_manager.read();
412 if let Some(value) = brain_regions {
413 state_manager.set_brain_regions_hash(value);
414 }
415 if let Some(value) = cortical_areas {
416 state_manager.set_cortical_areas_hash(value);
417 }
418 if let Some(value) = brain_geometry {
419 state_manager.set_brain_geometry_hash(value);
420 }
421 if let Some(value) = morphologies {
422 state_manager.set_morphologies_hash(value);
423 }
424 if let Some(value) = cortical_mappings {
425 state_manager.set_cortical_mappings_hash(value);
426 }
427 }
428
429 fn refresh_brain_regions_hash(&self) {
431 let hash = self.compute_brain_regions_hash();
432 self.update_state_hashes(Some(hash), None, None, None, None);
433 }
434
435 #[allow(dead_code)]
436 fn refresh_cortical_areas_hash(&self) {
438 let hash = self.compute_cortical_areas_hash();
439 self.update_state_hashes(None, Some(hash), None, None, None);
440 }
441
442 #[allow(dead_code)]
443 fn refresh_brain_geometry_hash(&self) {
445 let hash = self.compute_brain_geometry_hash();
446 self.update_state_hashes(None, None, Some(hash), None, None);
447 }
448
449 fn refresh_morphologies_hash(&self) {
451 let hash = self.compute_morphologies_hash();
452 self.update_state_hashes(None, None, None, Some(hash), None);
453 }
454
455 pub fn refresh_cortical_mappings_hash(&self) {
457 let hash = self.compute_cortical_mappings_hash();
458 self.update_state_hashes(None, None, None, None, Some(hash));
459 }
460
461 pub fn refresh_cortical_area_hashes(&self, properties_changed: bool, geometry_changed: bool) {
463 let cortical_hash = if properties_changed {
464 Some(self.compute_cortical_areas_hash())
465 } else {
466 None
467 };
468 let geometry_hash = if geometry_changed {
469 Some(self.compute_brain_geometry_hash())
470 } else {
471 None
472 };
473 self.update_state_hashes(None, cortical_hash, geometry_hash, None, None);
474 }
475
476 fn compute_brain_regions_hash(&self) -> u64 {
478 let mut hasher = Xxh64::new(DATA_HASH_SEED);
479 let mut region_ids: Vec<String> = self
480 .brain_regions
481 .get_all_region_ids()
482 .into_iter()
483 .cloned()
484 .collect();
485 region_ids.sort();
486
487 for region_id in region_ids {
488 let Some(region) = self.brain_regions.get_region(®ion_id) else {
489 continue;
490 };
491 Self::hash_str(&mut hasher, ®ion_id);
492 Self::hash_str(&mut hasher, ®ion.name);
493 Self::hash_str(&mut hasher, ®ion.region_type.to_string());
494 let parent_id = self.brain_regions.get_parent(®ion_id);
495 match parent_id {
496 Some(parent) => Self::hash_str(&mut hasher, parent),
497 None => Self::hash_str(&mut hasher, "null"),
498 }
499
500 let mut cortical_ids: Vec<String> = region
501 .cortical_areas
502 .iter()
503 .map(|id| id.as_base_64())
504 .collect();
505 cortical_ids.sort();
506 for cortical_id in cortical_ids {
507 Self::hash_str(&mut hasher, &cortical_id);
508 }
509
510 Self::hash_properties_filtered(&mut hasher, ®ion.properties, &[]);
511 }
512
513 hasher.finish() & HASH_SAFE_MASK
514 }
515
516 fn compute_cortical_areas_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 hasher.write_u32(area.cortical_idx);
526 Self::hash_str(&mut hasher, &area.name);
527 Self::hash_str(&mut hasher, &area.cortical_type.to_string());
528
529 let excluded = ["cortical_mapping_dst", "upstream_cortical_areas"];
530 Self::hash_properties_filtered(&mut hasher, &area.properties, &excluded);
531 }
532
533 hasher.finish() & HASH_SAFE_MASK
534 }
535
536 fn compute_brain_geometry_hash(&self) -> u64 {
538 let mut hasher = Xxh64::new(DATA_HASH_SEED);
539 let mut areas: Vec<&CorticalArea> = self.cortical_areas.values().collect();
540 areas.sort_by_key(|area| area.cortical_id.as_base_64());
541
542 for area in areas {
543 let cortical_id = area.cortical_id.as_base_64();
544 Self::hash_str(&mut hasher, &cortical_id);
545
546 Self::hash_i32(&mut hasher, area.position.x);
547 Self::hash_i32(&mut hasher, area.position.y);
548 Self::hash_i32(&mut hasher, area.position.z);
549
550 Self::hash_u32(&mut hasher, area.dimensions.width);
551 Self::hash_u32(&mut hasher, area.dimensions.height);
552 Self::hash_u32(&mut hasher, area.dimensions.depth);
553
554 let coord_2d = area
555 .properties
556 .get("coordinate_2d")
557 .or_else(|| area.properties.get("coordinates_2d"));
558 match coord_2d {
559 Some(value) => Self::hash_json_value(&mut hasher, value),
560 None => Self::hash_str(&mut hasher, "null"),
561 }
562 }
563
564 hasher.finish() & HASH_SAFE_MASK
565 }
566
567 fn compute_morphologies_hash(&self) -> u64 {
569 let mut hasher = Xxh64::new(DATA_HASH_SEED);
570 let mut morphology_ids = self.morphology_registry.morphology_ids();
571 morphology_ids.sort();
572
573 for morphology_id in morphology_ids {
574 if let Some(morphology) = self.morphology_registry.get(&morphology_id) {
575 Self::hash_str(&mut hasher, &morphology_id);
576 Self::hash_str(&mut hasher, &format!("{:?}", morphology.morphology_type));
577 Self::hash_str(&mut hasher, &morphology.class);
578 if let Ok(value) = serde_json::to_value(&morphology.parameters) {
579 Self::hash_json_value(&mut hasher, &value);
580 }
581 }
582 }
583
584 hasher.finish() & HASH_SAFE_MASK
585 }
586
587 fn compute_cortical_mappings_hash(&self) -> u64 {
589 let mut hasher = Xxh64::new(DATA_HASH_SEED);
590 let mut areas: Vec<&CorticalArea> = self.cortical_areas.values().collect();
591 areas.sort_by_key(|area| area.cortical_id.as_base_64());
592
593 for area in areas {
594 let cortical_id = area.cortical_id.as_base_64();
595 Self::hash_str(&mut hasher, &cortical_id);
596 if let Some(serde_json::Value::Object(map)) =
597 area.properties.get("cortical_mapping_dst")
598 {
599 let mut dest_ids: Vec<&String> = map.keys().collect();
600 dest_ids.sort();
601 for dest_id in dest_ids {
602 Self::hash_str(&mut hasher, dest_id);
603 if let Some(value) = map.get(dest_id) {
604 Self::hash_json_value(&mut hasher, value);
605 }
606 }
607 } else {
608 Self::hash_str(&mut hasher, "null");
609 }
610 }
611
612 hasher.finish() & HASH_SAFE_MASK
613 }
614
615 fn hash_str(hasher: &mut Xxh64, value: &str) {
617 hasher.write(value.as_bytes());
618 hasher.write_u8(0);
619 }
620
621 fn hash_i32(hasher: &mut Xxh64, value: i32) {
623 hasher.write(&value.to_le_bytes());
624 }
625
626 fn hash_u32(hasher: &mut Xxh64, value: u32) {
628 hasher.write(&value.to_le_bytes());
629 }
630
631 fn hash_json_value(hasher: &mut Xxh64, value: &serde_json::Value) {
633 match value {
634 serde_json::Value::Null => {
635 hasher.write_u8(0);
636 }
637 serde_json::Value::Bool(val) => {
638 hasher.write_u8(1);
639 hasher.write_u8(*val as u8);
640 }
641 serde_json::Value::Number(num) => {
642 hasher.write_u8(2);
643 Self::hash_str(hasher, &num.to_string());
644 }
645 serde_json::Value::String(val) => {
646 hasher.write_u8(3);
647 Self::hash_str(hasher, val);
648 }
649 serde_json::Value::Array(items) => {
650 hasher.write_u8(4);
651 for item in items {
652 Self::hash_json_value(hasher, item);
653 }
654 }
655 serde_json::Value::Object(map) => {
656 hasher.write_u8(5);
657 let mut keys: Vec<&String> = map.keys().collect();
658 keys.sort();
659 for key in keys {
660 Self::hash_str(hasher, key);
661 if let Some(val) = map.get(key) {
662 Self::hash_json_value(hasher, val);
663 }
664 }
665 }
666 }
667 }
668
669 fn hash_properties_filtered(
671 hasher: &mut Xxh64,
672 properties: &HashMap<String, serde_json::Value>,
673 excluded_keys: &[&str],
674 ) {
675 let mut keys: Vec<&String> = properties.keys().collect();
676 keys.sort();
677 for key in keys {
678 if excluded_keys.contains(&key.as_str()) {
679 continue;
680 }
681 Self::hash_str(hasher, key);
682 if let Some(value) = properties.get(key) {
683 Self::hash_json_value(hasher, value);
684 }
685 }
686 }
687
688 pub fn add_cortical_area(&mut self, mut area: CorticalArea) -> BduResult<u32> {
709 if self.cortical_areas.contains_key(&area.cortical_id) {
711 return Err(BduError::InvalidArea(format!(
712 "Cortical area {} already exists",
713 area.cortical_id
714 )));
715 }
716
717 use feagi_structures::genomic::cortical_area::CoreCorticalType;
720
721 let death_id = CoreCorticalType::Death.to_cortical_id();
722 let power_id = CoreCorticalType::Power.to_cortical_id();
723 let fatigue_id = CoreCorticalType::Fatigue.to_cortical_id();
724 let pain_id = CoreCorticalType::Pain.to_cortical_id();
725 let pleasure_id = CoreCorticalType::Pleasure.to_cortical_id();
726 let fear_id = CoreCorticalType::Fear.to_cortical_id();
727 let hope_id = CoreCorticalType::Hope.to_cortical_id();
728
729 let is_death_area = area.cortical_id == death_id;
730 let is_power_area = area.cortical_id == power_id;
731 let is_fatigue_area = area.cortical_id == fatigue_id;
732 let is_pain_area = area.cortical_id == pain_id;
733 let is_pleasure_area = area.cortical_id == pleasure_id;
734 let is_fear_area = area.cortical_id == fear_id;
735 let is_hope_area = area.cortical_id == hope_id;
736
737 if is_death_area {
738 trace!(
739 target: "feagi-bdu",
740 "[CORE-AREA] Assigning RESERVED cortical_idx=0 to _death area (id={})",
741 area.cortical_id
742 );
743 area.cortical_idx = 0;
744 } else if is_power_area {
745 trace!(
746 target: "feagi-bdu",
747 "[CORE-AREA] Assigning RESERVED cortical_idx=1 to _power area (id={})",
748 area.cortical_id
749 );
750 area.cortical_idx = 1;
751 } else if is_fatigue_area {
752 trace!(
753 target: "feagi-bdu",
754 "[CORE-AREA] Assigning RESERVED cortical_idx=2 to _fatigue area (id={})",
755 area.cortical_id
756 );
757 area.cortical_idx = 2;
758 } else if is_pain_area {
759 trace!(
760 target: "feagi-bdu",
761 "[CORE-AREA] Assigning RESERVED cortical_idx=3 to _pain area (id={})",
762 area.cortical_id
763 );
764 area.cortical_idx = 3;
765 } else if is_pleasure_area {
766 trace!(
767 target: "feagi-bdu",
768 "[CORE-AREA] Assigning RESERVED cortical_idx=4 to _pleasure area (id={})",
769 area.cortical_id
770 );
771 area.cortical_idx = 4;
772 } else if is_fear_area {
773 trace!(
774 target: "feagi-bdu",
775 "[CORE-AREA] Assigning RESERVED cortical_idx=5 to _fear area (id={})",
776 area.cortical_id
777 );
778 area.cortical_idx = 5;
779 } else if is_hope_area {
780 trace!(
781 target: "feagi-bdu",
782 "[CORE-AREA] Assigning RESERVED cortical_idx=6 to _hope area (id={})",
783 area.cortical_id
784 );
785 area.cortical_idx = 6;
786 } else {
787 if area.cortical_idx == 0 {
789 area.cortical_idx = self.next_cortical_idx;
790 self.next_cortical_idx += 1;
791 trace!(
792 target: "feagi-bdu",
793 "[REGULAR-AREA] Assigned cortical_idx={} to area '{}' (should be >=7)",
794 area.cortical_idx,
795 area.cortical_id.as_base_64()
796 );
797 } else {
798 if area.cortical_idx <= 6 {
800 warn!(
801 "Regular area '{}' attempted to use RESERVED cortical_idx={}! Reassigning to next available.",
802 area.cortical_id, area.cortical_idx);
803 area.cortical_idx = self.next_cortical_idx;
804 self.next_cortical_idx += 1;
805 info!(
806 " Reassigned '{}' to cortical_idx={}",
807 area.cortical_id, area.cortical_idx
808 );
809 } else if self.cortical_idx_to_id.contains_key(&area.cortical_idx) {
810 return Err(BduError::InvalidArea(format!(
811 "Cortical index {} is already in use",
812 area.cortical_idx
813 )));
814 }
815
816 if area.cortical_idx >= self.next_cortical_idx {
818 self.next_cortical_idx = area.cortical_idx + 1;
819 }
820 }
821 }
822
823 let cortical_id = area.cortical_id;
824 let cortical_idx = area.cortical_idx;
825
826 self.cortical_id_to_idx.insert(cortical_id, cortical_idx);
828 self.cortical_idx_to_id.insert(cortical_idx, cortical_id);
829
830 area.properties
832 .insert("upstream_cortical_areas".to_string(), serde_json::json!([]));
833
834 let parent_region_id = area
843 .properties
844 .get("parent_region_id")
845 .and_then(|v| v.as_str())
846 .map(|s| s.to_string());
847
848 self.cortical_areas.insert(cortical_id, area);
850
851 if let Some(region_id) = parent_region_id {
853 let region = self
854 .brain_regions
855 .get_region_mut(®ion_id)
856 .ok_or_else(|| {
857 BduError::InvalidArea(format!(
858 "Unknown parent_region_id '{}' for cortical area {}",
859 region_id,
860 cortical_id.as_base_64()
861 ))
862 })?;
863 region.add_area(cortical_id);
864 }
865
866 {
869 let mut neuron_cache = self.cached_neuron_counts_per_area.write();
870 neuron_cache.insert(cortical_id, AtomicUsize::new(0));
871 let mut synapse_cache = self.cached_synapse_counts_per_area.write();
872 synapse_cache.insert(cortical_id, AtomicUsize::new(0));
873 }
874 let state_manager = StateManager::instance();
876 let state_manager = state_manager.read();
877 state_manager.init_cortical_area_stats(&cortical_id.as_base_64());
878
879 if let Some(ref npu) = self.npu {
883 trace!(target: "feagi-bdu", "[LOCK-TRACE] add_cortical_area: attempting NPU lock for registration");
884 if let Ok(mut npu_lock) = npu.lock() {
885 trace!(target: "feagi-bdu", "[LOCK-TRACE] add_cortical_area: acquired NPU lock for registration");
886 npu_lock.register_cortical_area(cortical_idx, cortical_id.as_base_64());
887 trace!(
888 target: "feagi-bdu",
889 "Registered cortical area idx={} -> '{}' in NPU",
890 cortical_idx,
891 cortical_id.as_base_64()
892 );
893 }
894 }
895
896 self.sync_cortical_area_flags_to_npu()?;
898
899 self.initialized = true;
900
901 self.refresh_cortical_area_hashes(true, true);
902 self.refresh_brain_regions_hash();
903
904 Ok(cortical_idx)
905 }
906
907 pub fn remove_cortical_area(&mut self, cortical_id: &CorticalID) -> BduResult<()> {
922 let area = self.cortical_areas.remove(cortical_id).ok_or_else(|| {
923 BduError::InvalidArea(format!("Cortical area {} does not exist", cortical_id))
924 })?;
925
926 self.cortical_id_to_idx.remove(cortical_id);
928 self.cortical_idx_to_id.remove(&area.cortical_idx);
929
930 self.refresh_cortical_area_hashes(true, true);
931 Ok(())
932 }
933
934 pub fn rename_cortical_area_id(
938 &mut self,
939 old_id: &CorticalID,
940 new_id: CorticalID,
941 new_cortical_type: CorticalAreaType,
942 ) -> BduResult<()> {
943 self.rename_cortical_area_id_with_options(old_id, new_id, new_cortical_type, true)
944 }
945
946 pub fn rename_cortical_area_id_with_options(
948 &mut self,
949 old_id: &CorticalID,
950 new_id: CorticalID,
951 new_cortical_type: CorticalAreaType,
952 update_npu_registry: bool,
953 ) -> BduResult<()> {
954 if !self.cortical_areas.contains_key(old_id) {
955 return Err(BduError::InvalidArea(format!(
956 "Cortical area {} does not exist",
957 old_id
958 )));
959 }
960 if self.cortical_areas.contains_key(&new_id) {
961 return Err(BduError::InvalidArea(format!(
962 "Cortical area {} already exists",
963 new_id
964 )));
965 }
966
967 let mut area = self.cortical_areas.remove(old_id).ok_or_else(|| {
968 BduError::InvalidArea(format!("Cortical area {} does not exist", old_id))
969 })?;
970 let cortical_idx = area.cortical_idx;
971 area.cortical_id = new_id;
972 area.cortical_type = new_cortical_type;
973
974 self.cortical_areas.insert(new_id, area);
975 self.cortical_id_to_idx.remove(old_id);
976 self.cortical_id_to_idx.insert(new_id, cortical_idx);
977 self.cortical_idx_to_id.insert(cortical_idx, new_id);
978
979 {
981 let mut neuron_cache = self.cached_neuron_counts_per_area.write();
982 if let Some(value) = neuron_cache.remove(old_id) {
983 neuron_cache.insert(new_id, value);
984 }
985 let mut synapse_cache = self.cached_synapse_counts_per_area.write();
986 if let Some(value) = synapse_cache.remove(old_id) {
987 synapse_cache.insert(new_id, value);
988 }
989 }
990
991 self.brain_regions.rename_cortical_area_id(old_id, new_id);
993
994 let old_id_str = old_id.as_base_64();
996 let new_id_str = new_id.as_base_64();
997 for area in self.cortical_areas.values_mut() {
998 if let Some(mapping) = area
999 .properties
1000 .get_mut("cortical_mapping_dst")
1001 .and_then(|v| v.as_object_mut())
1002 {
1003 if let Some(value) = mapping.remove(&old_id_str) {
1004 mapping.insert(new_id_str.clone(), value);
1005 }
1006 }
1007 }
1008
1009 if update_npu_registry {
1011 if let Some(ref npu) = self.npu {
1012 if let Ok(mut npu_lock) = npu.lock() {
1013 npu_lock.register_cortical_area(cortical_idx, new_id.as_base_64());
1014 }
1015 }
1016 }
1017
1018 self.refresh_cortical_area_hashes(true, true);
1019 self.refresh_brain_regions_hash();
1020 self.refresh_cortical_mappings_hash();
1021
1022 Ok(())
1023 }
1024
1025 pub fn get_cortical_area(&self, cortical_id: &CorticalID) -> Option<&CorticalArea> {
1027 self.cortical_areas.get(cortical_id)
1028 }
1029
1030 pub fn get_cortical_area_mut(&mut self, cortical_id: &CorticalID) -> Option<&mut CorticalArea> {
1032 self.cortical_areas.get_mut(cortical_id)
1033 }
1034
1035 pub fn get_cortical_idx(&self, cortical_id: &CorticalID) -> Option<u32> {
1037 self.cortical_id_to_idx.get(cortical_id).copied()
1038 }
1039
1040 pub fn get_parent_region_id_for_area(&self, cortical_id: &CorticalID) -> Option<String> {
1052 self.brain_regions.find_region_containing_area(cortical_id)
1053 }
1054
1055 pub fn has_cross_region_outgoing(&self, area: &CorticalID) -> bool {
1058 let Some(my_region) = self.brain_regions.find_region_containing_area(area) else {
1059 return false;
1060 };
1061 let Some(src_area) = self.cortical_areas.get(area) else {
1062 return false;
1063 };
1064 let Some(dst_obj) = src_area
1065 .properties
1066 .get("cortical_mapping_dst")
1067 .and_then(|v| v.as_object())
1068 else {
1069 return false;
1070 };
1071 for dst_key in dst_obj.keys() {
1072 let Ok(dst_id) = CorticalID::try_from_base_64(dst_key) else {
1073 continue;
1074 };
1075 match self.brain_regions.find_region_containing_area(&dst_id) {
1076 None => return true,
1077 Some(rid) if rid != my_region => return true,
1078 _ => {}
1079 }
1080 }
1081 false
1082 }
1083
1084 pub fn has_cross_region_incoming(&self, area: &CorticalID) -> bool {
1086 let Some(my_region) = self.brain_regions.find_region_containing_area(area) else {
1087 return false;
1088 };
1089 let my_b64 = area.as_base_64();
1090 for (src_id, src_area) in &self.cortical_areas {
1091 if src_id == area {
1092 continue;
1093 }
1094 let Some(dst_map) = src_area
1095 .properties
1096 .get("cortical_mapping_dst")
1097 .and_then(|v| v.as_object())
1098 else {
1099 continue;
1100 };
1101 if !dst_map.contains_key(&my_b64) {
1102 continue;
1103 }
1104 match self.brain_regions.find_region_containing_area(src_id) {
1105 None => return true,
1106 Some(rid) if rid != my_region => return true,
1107 _ => {}
1108 }
1109 }
1110 false
1111 }
1112
1113 pub fn recompute_brain_region_io_registry(&mut self) -> BduResult<BrainRegionIoRegistry> {
1124 use std::collections::HashSet;
1125
1126 let region_ids: Vec<String> = self
1127 .brain_regions
1128 .get_all_region_ids()
1129 .into_iter()
1130 .cloned()
1131 .collect();
1132
1133 let mut inputs_by_region: HashMap<String, HashSet<String>> = HashMap::new();
1134 let mut outputs_by_region: HashMap<String, HashSet<String>> = HashMap::new();
1135
1136 for rid in ®ion_ids {
1138 inputs_by_region.insert(rid.clone(), HashSet::new());
1139 outputs_by_region.insert(rid.clone(), HashSet::new());
1140 }
1141
1142 for (src_id, src_area) in &self.cortical_areas {
1143 let Some(dstmap) = src_area
1144 .properties
1145 .get("cortical_mapping_dst")
1146 .and_then(|v| v.as_object())
1147 else {
1148 continue;
1149 };
1150
1151 let Some(src_region_id) = self.brain_regions.find_region_containing_area(src_id) else {
1152 debug!(
1153 target: "feagi-bdu",
1154 "Skipping region IO for source area {} (not in any region)",
1155 src_id.as_base_64()
1156 );
1157 continue;
1158 };
1159
1160 for dst_id_str in dstmap.keys() {
1161 let dst_id = CorticalID::try_from_base_64(dst_id_str).map_err(|e| {
1162 BduError::InvalidArea(format!(
1163 "Unable to recompute region IO: invalid destination cortical id '{}' in cortical_mapping_dst for {}: {}",
1164 dst_id_str,
1165 src_id.as_base_64(),
1166 e
1167 ))
1168 })?;
1169
1170 let Some(dst_region_id) = self.brain_regions.find_region_containing_area(&dst_id)
1171 else {
1172 warn!(
1173 target: "feagi-bdu",
1174 "Skipping region IO for destination area {} (not in any region)",
1175 dst_id.as_base_64()
1176 );
1177 continue;
1178 };
1179
1180 if src_region_id == dst_region_id {
1181 continue;
1182 }
1183
1184 outputs_by_region
1185 .entry(src_region_id.clone())
1186 .or_default()
1187 .insert(src_id.as_base_64());
1188 inputs_by_region
1189 .entry(dst_region_id.clone())
1190 .or_default()
1191 .insert(dst_id.as_base_64());
1192 }
1193 }
1194
1195 for rid in ®ion_ids {
1197 let Some(region) = self.brain_regions.get_region(rid) else {
1198 continue;
1199 };
1200 let in_ids = crate::region_io_designation::parse_designated_id_list(
1201 region
1202 .properties
1203 .get(crate::region_io_designation::DESIGNATED_INPUTS_KEY),
1204 )?;
1205 let out_ids = crate::region_io_designation::parse_designated_id_list(
1206 region
1207 .properties
1208 .get(crate::region_io_designation::DESIGNATED_OUTPUTS_KEY),
1209 )?;
1210 for id in in_ids {
1211 inputs_by_region
1212 .entry(rid.clone())
1213 .or_default()
1214 .insert(id.as_base_64());
1215 }
1216 for id in out_ids {
1217 outputs_by_region
1218 .entry(rid.clone())
1219 .or_default()
1220 .insert(id.as_base_64());
1221 }
1222 }
1223
1224 let mut computed: HashMap<String, (Vec<String>, Vec<String>)> = HashMap::new();
1225 for rid in region_ids {
1226 let mut inputs: Vec<String> = inputs_by_region
1227 .remove(&rid)
1228 .unwrap_or_default()
1229 .into_iter()
1230 .collect();
1231 let mut outputs: Vec<String> = outputs_by_region
1232 .remove(&rid)
1233 .unwrap_or_default()
1234 .into_iter()
1235 .collect();
1236
1237 inputs.sort();
1238 outputs.sort();
1239
1240 let region = self.brain_regions.get_region_mut(&rid).ok_or_else(|| {
1241 BduError::InvalidArea(format!(
1242 "Unable to recompute region IO: region '{}' not found in hierarchy",
1243 rid
1244 ))
1245 })?;
1246
1247 if inputs.is_empty() {
1248 region.properties.remove("inputs");
1249 } else {
1250 region
1251 .properties
1252 .insert("inputs".to_string(), serde_json::json!(inputs.clone()));
1253 }
1254
1255 if outputs.is_empty() {
1256 region.properties.remove("outputs");
1257 } else {
1258 region
1259 .properties
1260 .insert("outputs".to_string(), serde_json::json!(outputs.clone()));
1261 }
1262
1263 computed.insert(rid, (inputs, outputs));
1264 }
1265
1266 self.refresh_brain_regions_hash();
1267
1268 Ok(computed)
1269 }
1270
1271 pub fn get_root_region_id(&self) -> Option<String> {
1277 self.brain_regions.get_root_region_id()
1278 }
1279
1280 pub fn get_cortical_id(&self, cortical_idx: u32) -> Option<&CorticalID> {
1282 self.cortical_idx_to_id.get(&cortical_idx)
1283 }
1284
1285 pub fn get_all_cortical_idx_to_id_mappings(&self) -> ahash::AHashMap<u32, String> {
1288 self.cortical_idx_to_id
1289 .iter()
1290 .map(|(idx, id)| (*idx, id.as_base_64()))
1291 .collect()
1292 }
1293
1294 pub fn get_all_visualization_granularities(&self) -> ahash::AHashMap<u32, (u32, u32, u32)> {
1299 let mut granularities = ahash::AHashMap::new();
1300 for (cortical_id, area) in &self.cortical_areas {
1301 let cortical_idx = self
1302 .cortical_id_to_idx
1303 .get(cortical_id)
1304 .copied()
1305 .unwrap_or(0);
1306
1307 if let Some(granularity_json) = area.properties.get("visualization_voxel_granularity") {
1310 if let Some(arr) = granularity_json.as_array() {
1311 if arr.len() == 3 {
1312 let x_opt = arr[0]
1313 .as_u64()
1314 .or_else(|| arr[0].as_f64().map(|f| f as u64));
1315 let y_opt = arr[1]
1316 .as_u64()
1317 .or_else(|| arr[1].as_f64().map(|f| f as u64));
1318 let z_opt = arr[2]
1319 .as_u64()
1320 .or_else(|| arr[2].as_f64().map(|f| f as u64));
1321
1322 if let (Some(x), Some(y), Some(z)) = (x_opt, y_opt, z_opt) {
1323 let granularity = (x as u32, y as u32, z as u32);
1324 if granularity != (1, 1, 1) {
1326 granularities.insert(cortical_idx, granularity);
1327 }
1328 }
1329 }
1330 }
1331 }
1332 }
1333 granularities
1334 }
1335
1336 pub fn get_cortical_area_ids(&self) -> Vec<&CorticalID> {
1338 self.cortical_areas.keys().collect()
1339 }
1340
1341 pub fn get_cortical_area_count(&self) -> usize {
1343 self.cortical_areas.len()
1344 }
1345
1346 pub fn get_upstream_cortical_areas(&self, target_cortical_id: &CorticalID) -> Vec<u32> {
1360 if let Some(area) = self.cortical_areas.get(target_cortical_id) {
1361 if let Some(upstream_prop) = area.properties.get("upstream_cortical_areas") {
1362 if let Some(upstream_array) = upstream_prop.as_array() {
1363 return upstream_array
1364 .iter()
1365 .filter_map(|v| v.as_u64().map(|n| n as u32))
1366 .collect();
1367 }
1368 }
1369
1370 warn!(target: "feagi-bdu",
1372 "Cortical area '{}' missing 'upstream_cortical_areas' property - treating as empty",
1373 target_cortical_id.as_base_64()
1374 );
1375 }
1376
1377 Vec::new()
1378 }
1379
1380 pub fn filter_non_memory_upstream_areas(&self, upstream: &[u32]) -> Vec<u32> {
1382 upstream
1383 .iter()
1384 .filter_map(|idx| {
1385 let cortical_id = self.cortical_idx_to_id.get(idx)?;
1386 let area = self.cortical_areas.get(cortical_id)?;
1387 if matches!(area.cortical_type, CorticalAreaType::Memory(_)) {
1388 None
1389 } else {
1390 Some(*idx)
1391 }
1392 })
1393 .collect()
1394 }
1395
1396 pub fn refresh_upstream_cortical_areas_from_mappings(
1400 &mut self,
1401 target_cortical_id: &CorticalID,
1402 ) -> Vec<u32> {
1403 use std::collections::HashSet;
1404 let target_id_str = target_cortical_id.as_base_64();
1405 let mut upstream_idxs = HashSet::new();
1406 for (src_id, src_area) in &self.cortical_areas {
1407 if src_id == target_cortical_id {
1408 continue;
1409 }
1410 if let Some(mapping) = src_area
1411 .properties
1412 .get("cortical_mapping_dst")
1413 .and_then(|v| v.as_object())
1414 {
1415 if mapping.contains_key(&target_id_str) {
1416 upstream_idxs.insert(src_area.cortical_idx);
1417 }
1418 }
1419 }
1420
1421 let mut upstream_list: Vec<u32> = upstream_idxs.into_iter().collect();
1422 upstream_list.sort_unstable();
1423
1424 if let Some(target_area) = self.cortical_areas.get_mut(target_cortical_id) {
1425 target_area.properties.insert(
1426 "upstream_cortical_areas".to_string(),
1427 serde_json::json!(upstream_list),
1428 );
1429 }
1430
1431 self.get_upstream_cortical_areas(target_cortical_id)
1432 }
1433
1434 pub fn add_upstream_area(&mut self, target_cortical_id: &CorticalID, src_cortical_idx: u32) {
1444 if let Some(area) = self.cortical_areas.get_mut(target_cortical_id) {
1445 let upstream_array = area
1446 .properties
1447 .entry("upstream_cortical_areas".to_string())
1448 .or_insert_with(|| serde_json::json!([]));
1449
1450 if let Some(arr) = upstream_array.as_array_mut() {
1451 let src_value = serde_json::json!(src_cortical_idx);
1452 if !arr.contains(&src_value) {
1453 arr.push(src_value);
1454 debug!(target: "feagi-bdu",
1455 "Added upstream area idx={} to cortical area '{}'",
1456 src_cortical_idx, target_cortical_id.as_base_64()
1457 );
1458 }
1459 }
1460 }
1461 }
1462
1463 pub fn get_memory_twin_for_upstream_idx(
1465 &self,
1466 memory_area_idx: u32,
1467 upstream_idx: u32,
1468 ) -> Option<CorticalID> {
1469 let memory_id = self.cortical_idx_to_id.get(&memory_area_idx)?;
1470 let upstream_id = self.cortical_idx_to_id.get(&upstream_idx)?;
1471 let area = self.cortical_areas.get(memory_id)?;
1472 let mapping = area
1473 .properties
1474 .get("memory_twin_areas")
1475 .and_then(|v| v.as_object())?;
1476 let twin_b64 = mapping.get(&upstream_id.as_base_64())?.as_str()?;
1477 CorticalID::try_from_base_64(twin_b64).ok()
1478 }
1479
1480 pub fn diagnose_memory_twin_for_mapping(
1487 &self,
1488 src_area_id: &CorticalID,
1489 dst_area_id: &CorticalID,
1490 ) -> BduResult<MemoryTwinDiagnostic> {
1491 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
1492 BduError::InvalidArea(format!(
1493 "Source area not found: {}",
1494 src_area_id.as_base_64()
1495 ))
1496 })?;
1497 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
1498 BduError::InvalidArea(format!(
1499 "Destination area not found: {}",
1500 dst_area_id.as_base_64()
1501 ))
1502 })?;
1503
1504 let src_is_memory = matches!(src_area.cortical_type, CorticalAreaType::Memory(_));
1505 let dst_is_memory = matches!(dst_area.cortical_type, CorticalAreaType::Memory(_));
1506
1507 let rules_opt = src_area
1508 .properties
1509 .get("cortical_mapping_dst")
1510 .and_then(|v| v.as_object())
1511 .and_then(|mapping_dst| {
1512 Self::get_mapping_rules_for_destination(mapping_dst, dst_area_id)
1513 });
1514 let mapping_exists = rules_opt.is_some();
1515 let episodic_rule_count = rules_opt
1516 .map(|rules| {
1517 rules
1518 .iter()
1519 .filter(|rule| {
1520 let morphology_id = rule
1521 .as_object()
1522 .and_then(|obj| obj.get("morphology_id"))
1523 .and_then(|v| v.as_str())
1524 .or_else(|| {
1525 rule.as_array()
1526 .and_then(|arr| arr.first())
1527 .and_then(|v| v.as_str())
1528 });
1529 morphology_id == Some("episodic_memory")
1530 })
1531 .count()
1532 })
1533 .unwrap_or(0);
1534
1535 let twin_expected =
1536 mapping_exists && episodic_rule_count > 0 && dst_is_memory && !src_is_memory;
1537 let twin_cortical_area = dst_area
1538 .properties
1539 .get("memory_twin_areas")
1540 .and_then(|v| v.as_object())
1541 .and_then(|map| map.get(&src_area_id.as_base_64()))
1542 .and_then(|v| v.as_str())
1543 .map(ToString::to_string);
1544 let twin_present = twin_cortical_area.is_some();
1545
1546 let twin_parent_region_id = twin_cortical_area.as_ref().and_then(|twin_b64| {
1547 CorticalID::try_from_base_64(twin_b64)
1548 .ok()
1549 .and_then(|twin_id| {
1550 self.cortical_areas.get(&twin_id).and_then(|area| {
1551 area.properties
1552 .get("parent_region_id")
1553 .and_then(|v| v.as_str())
1554 .map(ToString::to_string)
1555 })
1556 })
1557 });
1558 let src_parent_region_id = src_area
1559 .properties
1560 .get("parent_region_id")
1561 .and_then(|v| v.as_str())
1562 .map(ToString::to_string);
1563 let dst_parent_region_id = dst_area
1564 .properties
1565 .get("parent_region_id")
1566 .and_then(|v| v.as_str())
1567 .map(ToString::to_string);
1568 let twin_parent_matches_src_parent = match (&twin_parent_region_id, &src_parent_region_id) {
1569 (Some(twin_parent), Some(src_parent)) => Some(twin_parent == src_parent),
1570 (None, None) if twin_present => Some(true),
1571 _ if twin_present => Some(false),
1572 _ => None,
1573 };
1574
1575 let reason = if !mapping_exists {
1576 Some("no_mapping_rule_between_src_and_dst".to_string())
1577 } else if episodic_rule_count == 0 {
1578 Some("mapping_exists_but_not_episodic_memory".to_string())
1579 } else if !dst_is_memory {
1580 Some("destination_is_not_memory_area".to_string())
1581 } else if src_is_memory {
1582 Some("upstream_is_memory_area_twin_not_supported".to_string())
1583 } else if !twin_present {
1584 Some("twin_expected_but_missing".to_string())
1585 } else if twin_parent_matches_src_parent == Some(false) {
1586 Some("twin_parent_region_mismatch_with_source".to_string())
1587 } else {
1588 None
1589 };
1590
1591 Ok(MemoryTwinDiagnostic {
1592 src_cortical_area: src_area_id.as_base_64(),
1593 dst_cortical_area: dst_area_id.as_base_64(),
1594 src_cortical_type: src_area.cortical_type.to_string(),
1595 dst_cortical_type: dst_area.cortical_type.to_string(),
1596 mapping_exists,
1597 episodic_rule_count,
1598 twin_expected,
1599 twin_present,
1600 twin_cortical_area,
1601 reason,
1602 src_parent_region_id,
1603 dst_parent_region_id,
1604 twin_parent_region_id,
1605 twin_parent_matches_src_parent,
1606 })
1607 }
1608
1609 pub fn ensure_memory_twin_area(
1611 &mut self,
1612 memory_area_id: &CorticalID,
1613 upstream_area_id: &CorticalID,
1614 ) -> BduResult<CorticalID> {
1615 use crate::models::CorticalAreaExt;
1616
1617 let register_replay_mapping = |manager: &mut ConnectomeManager,
1618 twin_id: &CorticalID|
1619 -> BduResult<()> {
1620 let Some(npu) = manager.npu.as_ref() else {
1621 return Ok(());
1622 };
1623 let memory_area_idx =
1624 *manager
1625 .cortical_id_to_idx
1626 .get(memory_area_id)
1627 .ok_or_else(|| {
1628 BduError::InvalidArea(format!(
1629 "Memory area idx missing for {}",
1630 memory_area_id.as_base_64()
1631 ))
1632 })?;
1633 let upstream_area_idx = *manager
1634 .cortical_id_to_idx
1635 .get(upstream_area_id)
1636 .ok_or_else(|| {
1637 BduError::InvalidArea(format!(
1638 "Upstream area idx missing for {}",
1639 upstream_area_id.as_base_64()
1640 ))
1641 })?;
1642 let twin_area_idx = *manager.cortical_id_to_idx.get(twin_id).ok_or_else(|| {
1643 BduError::InvalidArea(format!(
1644 "Twin area idx missing for {}",
1645 twin_id.as_base_64()
1646 ))
1647 })?;
1648 let twin_area = manager.cortical_areas.get(twin_id).ok_or_else(|| {
1649 BduError::InvalidArea(format!("Twin area {} not found", twin_id.as_base_64()))
1650 })?;
1651 let potential = twin_area.firing_threshold() + twin_area.firing_threshold_increment();
1652 if let Ok(mut npu_lock) = npu.lock() {
1653 npu_lock.register_memory_twin_mapping(
1654 memory_area_idx,
1655 upstream_area_idx,
1656 twin_area_idx,
1657 potential,
1658 );
1659 }
1660 Ok(())
1661 };
1662
1663 let memory_area = self.cortical_areas.get(memory_area_id).ok_or_else(|| {
1664 BduError::InvalidArea(format!(
1665 "Memory area {} not found",
1666 memory_area_id.as_base_64()
1667 ))
1668 })?;
1669 let upstream_area = self.cortical_areas.get(upstream_area_id).ok_or_else(|| {
1670 BduError::InvalidArea(format!(
1671 "Upstream area {} not found",
1672 upstream_area_id.as_base_64()
1673 ))
1674 })?;
1675
1676 if matches!(upstream_area.cortical_type, CorticalAreaType::Memory(_)) {
1677 return Err(BduError::InvalidArea(format!(
1678 "Upstream area {} is memory type; twin creation is only for non-memory areas",
1679 upstream_area_id.as_base_64()
1680 )));
1681 }
1682
1683 if let Some(existing) = memory_area
1684 .properties
1685 .get("memory_twin_areas")
1686 .and_then(|v| v.as_object())
1687 .and_then(|map| map.get(&upstream_area_id.as_base_64()))
1688 .and_then(|v| v.as_str())
1689 .and_then(|s| CorticalID::try_from_base_64(s).ok())
1690 {
1691 self.ensure_memory_replay_mapping(memory_area_id, &existing)?;
1692 register_replay_mapping(self, &existing)?;
1693 self.refresh_cortical_mappings_hash();
1694 return Ok(existing);
1695 }
1696
1697 let twin_id = self.build_memory_twin_id(memory_area_id, upstream_area_id)?;
1698 if self.cortical_areas.contains_key(&twin_id) {
1699 if let Some(existing) = self.cortical_areas.get_mut(&twin_id) {
1700 let expected_source = upstream_area_id.as_base_64();
1701 let expected_target = memory_area_id.as_base_64();
1702 let existing_source = existing
1703 .properties
1704 .get("memory_twin_of")
1705 .and_then(|v| v.as_str());
1706 let existing_target = existing
1707 .properties
1708 .get("memory_twin_for")
1709 .and_then(|v| v.as_str());
1710 if existing_source != Some(expected_source.as_str())
1711 || existing_target != Some(expected_target.as_str())
1712 {
1713 warn!(
1714 target: "feagi-bdu",
1715 "Twin cortical ID properties missing/mismatched for {} -> {}; repairing",
1716 upstream_area_id.as_base_64(),
1717 memory_area_id.as_base_64()
1718 );
1719 existing.properties.insert(
1720 "memory_twin_of".to_string(),
1721 serde_json::json!(expected_source),
1722 );
1723 existing.properties.insert(
1724 "memory_twin_for".to_string(),
1725 serde_json::json!(expected_target),
1726 );
1727 }
1728 }
1729 self.set_memory_twin_mapping(memory_area_id, upstream_area_id, &twin_id);
1730 self.ensure_memory_replay_mapping(memory_area_id, &twin_id)?;
1731 register_replay_mapping(self, &twin_id)?;
1732 self.refresh_cortical_mappings_hash();
1733 return Ok(twin_id);
1734 }
1735
1736 let twin_name = format!("{}_twin", upstream_area.name.replace(' ', "_"));
1737 let twin_type = CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire);
1738 let twin_position = self.build_memory_twin_position(memory_area, upstream_area);
1739 let mut twin_area = CorticalArea::new(
1740 twin_id,
1741 0,
1742 twin_name,
1743 upstream_area.dimensions,
1744 twin_position,
1745 twin_type,
1746 )?;
1747 twin_area.properties = self.build_memory_twin_properties(
1748 memory_area,
1749 upstream_area,
1750 memory_area_id,
1751 upstream_area_id,
1752 );
1753
1754 let _twin_idx = self.add_cortical_area(twin_area)?;
1755 let _ = self.create_neurons_for_area(&twin_id);
1756
1757 self.set_memory_twin_mapping(memory_area_id, upstream_area_id, &twin_id);
1758 self.ensure_memory_replay_mapping(memory_area_id, &twin_id)?;
1759 register_replay_mapping(self, &twin_id)?;
1760 self.refresh_cortical_mappings_hash();
1761 Ok(twin_id)
1762 }
1763
1764 fn build_memory_twin_position(
1765 &self,
1766 _memory_area: &CorticalArea,
1767 upstream_area: &CorticalArea,
1768 ) -> GenomeCoordinate3D {
1769 let width = upstream_area.dimensions.width as f32;
1770 let margin = (width * 0.25).ceil() as i32;
1771 let offset = upstream_area.dimensions.width as i32 + margin;
1772 GenomeCoordinate3D::new(
1773 upstream_area.position.x + offset,
1774 upstream_area.position.y,
1775 upstream_area.position.z,
1776 )
1777 }
1778
1779 fn build_memory_twin_id(
1780 &self,
1781 memory_area_id: &CorticalID,
1782 upstream_area_id: &CorticalID,
1783 ) -> BduResult<CorticalID> {
1784 let mut hasher = Xxh64::new(DATA_HASH_SEED);
1785 hasher.write(memory_area_id.as_base_64().as_bytes());
1786 hasher.write(upstream_area_id.as_base_64().as_bytes());
1787 hasher.write(b"memory_twin");
1788 let hash = hasher.finish();
1789 let mut bytes = hash.to_be_bytes();
1790 bytes[0] = b'c';
1791 CorticalID::try_from_bytes(&bytes)
1792 .map_err(|e| BduError::Internal(format!("Failed to build twin cortical ID: {}", e)))
1793 }
1794
1795 fn build_memory_twin_properties(
1796 &self,
1797 _memory_area: &CorticalArea,
1798 upstream_area: &CorticalArea,
1799 memory_area_id: &CorticalID,
1800 upstream_area_id: &CorticalID,
1801 ) -> HashMap<String, serde_json::Value> {
1802 let mut props = upstream_area.properties.clone();
1803 props.remove("cortical_mapping_dst");
1804 props.remove("upstream_cortical_areas");
1805 props.remove("parent_region_id");
1806 props.insert("cortical_group".to_string(), serde_json::json!("CUSTOM"));
1807 props.insert("is_mem_type".to_string(), serde_json::json!(false));
1808 props.insert(
1809 "memory_twin_of".to_string(),
1810 serde_json::json!(upstream_area_id.as_base_64()),
1811 );
1812 props.insert(
1813 "memory_twin_for".to_string(),
1814 serde_json::json!(memory_area_id.as_base_64()),
1815 );
1816 if let Some(parent_region_id) = upstream_area
1817 .properties
1818 .get("parent_region_id")
1819 .and_then(|v| v.as_str())
1820 {
1821 props.insert(
1822 "parent_region_id".to_string(),
1823 serde_json::json!(parent_region_id),
1824 );
1825 }
1826 props
1827 }
1828
1829 fn set_memory_twin_mapping(
1830 &mut self,
1831 memory_area_id: &CorticalID,
1832 upstream_area_id: &CorticalID,
1833 twin_id: &CorticalID,
1834 ) {
1835 if let Some(memory_area) = self.cortical_areas.get_mut(memory_area_id) {
1836 let mapping = memory_area
1837 .properties
1838 .entry("memory_twin_areas".to_string())
1839 .or_insert_with(|| serde_json::json!({}));
1840 if let Some(map) = mapping.as_object_mut() {
1841 map.insert(
1842 upstream_area_id.as_base_64(),
1843 serde_json::json!(twin_id.as_base_64()),
1844 );
1845 }
1846 }
1847 }
1848
1849 fn ensure_memory_replay_mapping(
1850 &mut self,
1851 memory_area_id: &CorticalID,
1852 twin_id: &CorticalID,
1853 ) -> BduResult<()> {
1854 if !self.morphology_registry.contains("memory_replay") {
1855 feagi_evolutionary::add_core_morphologies(&mut self.morphology_registry);
1856 }
1857 self.refresh_morphologies_hash();
1858 let mapping_data = vec![serde_json::json!({
1859 "morphology_id": "memory_replay",
1860 "morphology_scalar": [1, 1, 1],
1861 "postSynapticCurrent_multiplier": 1,
1862 "plasticity_flag": false,
1863 "plasticity_constant": 0,
1864 "ltp_multiplier": 0,
1865 "ltd_multiplier": 0,
1866 "plasticity_window": 0,
1867 })];
1868 self.update_cortical_mapping(memory_area_id, twin_id, mapping_data)?;
1869 let _ = self.regenerate_synapses_for_mapping(memory_area_id, twin_id)?;
1870 self.refresh_cortical_area_hashes(true, false);
1872 Ok(())
1873 }
1874
1875 pub fn remove_upstream_area(&mut self, target_cortical_id: &CorticalID, src_cortical_idx: u32) {
1885 if let Some(area) = self.cortical_areas.get_mut(target_cortical_id) {
1886 if let Some(upstream_prop) = area.properties.get_mut("upstream_cortical_areas") {
1887 if let Some(arr) = upstream_prop.as_array_mut() {
1888 let src_value = serde_json::json!(src_cortical_idx);
1889 if let Some(pos) = arr.iter().position(|v| v == &src_value) {
1890 arr.remove(pos);
1891 debug!(target: "feagi-bdu",
1892 "Removed upstream area idx={} from cortical area '{}'",
1893 src_cortical_idx, target_cortical_id.as_base_64()
1894 );
1895 }
1896 }
1897 }
1898 }
1899 }
1900
1901 pub fn has_cortical_area(&self, cortical_id: &CorticalID) -> bool {
1903 self.cortical_areas.contains_key(cortical_id)
1904 }
1905
1906 pub fn is_initialized(&self) -> bool {
1908 self.initialized && !self.cortical_areas.is_empty()
1909 }
1910
1911 pub fn add_brain_region(
1917 &mut self,
1918 region: BrainRegion,
1919 parent_id: Option<String>,
1920 ) -> BduResult<()> {
1921 self.brain_regions.add_region(region, parent_id)?;
1922 self.refresh_brain_regions_hash();
1923 Ok(())
1924 }
1925
1926 pub fn remove_brain_region(&mut self, region_id: &str) -> BduResult<()> {
1928 self.brain_regions.remove_region(region_id)?;
1929 self.refresh_brain_regions_hash();
1930 Ok(())
1931 }
1932
1933 pub fn change_brain_region_parent(
1935 &mut self,
1936 region_id: &str,
1937 new_parent_id: &str,
1938 ) -> BduResult<()> {
1939 self.brain_regions.change_parent(region_id, new_parent_id)?;
1940 self.refresh_brain_regions_hash();
1941 Ok(())
1942 }
1943
1944 pub fn get_brain_region(&self, region_id: &str) -> Option<&BrainRegion> {
1946 self.brain_regions.get_region(region_id)
1947 }
1948
1949 pub fn get_brain_region_mut(&mut self, region_id: &str) -> Option<&mut BrainRegion> {
1951 self.brain_regions.get_region_mut(region_id)
1952 }
1953
1954 pub fn get_brain_region_ids(&self) -> Vec<&String> {
1956 self.brain_regions.get_all_region_ids()
1957 }
1958
1959 pub fn get_brain_region_hierarchy(&self) -> &BrainRegionHierarchy {
1961 &self.brain_regions
1962 }
1963
1964 pub fn get_morphologies(&self) -> &feagi_evolutionary::MorphologyRegistry {
1970 &self.morphology_registry
1971 }
1972
1973 pub fn get_morphology_count(&self) -> usize {
1975 self.morphology_registry.count()
1976 }
1977
1978 pub fn upsert_morphology(
1983 &mut self,
1984 morphology_id: String,
1985 morphology: feagi_evolutionary::Morphology,
1986 ) {
1987 self.morphology_registry
1988 .add_morphology(morphology_id, morphology);
1989 self.refresh_morphologies_hash();
1990 }
1991
1992 pub fn remove_morphology(&mut self, morphology_id: &str) -> bool {
1998 let removed = self.morphology_registry.remove_morphology(morphology_id);
1999 if removed {
2000 self.refresh_morphologies_hash();
2001 }
2002 removed
2003 }
2004
2005 pub fn update_cortical_mapping(
2023 &mut self,
2024 src_area_id: &CorticalID,
2025 dst_area_id: &CorticalID,
2026 mapping_data: Vec<serde_json::Value>,
2027 ) -> BduResult<()> {
2028 use tracing::info;
2029
2030 crate::region_io_designation::validate_cross_region_mapping_proposal(
2031 self,
2032 src_area_id,
2033 dst_area_id,
2034 &mapping_data,
2035 )?;
2036
2037 debug!(target: "feagi-bdu", "Updating cortical mapping: {} -> {}", src_area_id, dst_area_id);
2038
2039 {
2040 let src_area = self.cortical_areas.get_mut(src_area_id).ok_or_else(|| {
2042 crate::types::BduError::InvalidArea(format!(
2043 "Source area not found: {}",
2044 src_area_id
2045 ))
2046 })?;
2047
2048 let cortical_mapping_dst =
2050 if let Some(existing) = src_area.properties.get_mut("cortical_mapping_dst") {
2051 existing.as_object_mut().ok_or_else(|| {
2052 crate::types::BduError::InvalidMorphology(
2053 "cortical_mapping_dst is not an object".to_string(),
2054 )
2055 })?
2056 } else {
2057 src_area
2059 .properties
2060 .insert("cortical_mapping_dst".to_string(), serde_json::json!({}));
2061 src_area
2062 .properties
2063 .get_mut("cortical_mapping_dst")
2064 .unwrap()
2065 .as_object_mut()
2066 .unwrap()
2067 };
2068
2069 if mapping_data.is_empty() {
2071 cortical_mapping_dst.remove(&dst_area_id.as_base_64());
2073 info!(target: "feagi-bdu", "Removed mapping from {} to {}", src_area_id, dst_area_id);
2074 } else {
2075 cortical_mapping_dst.insert(
2076 dst_area_id.as_base_64(),
2077 serde_json::Value::Array(mapping_data.clone()),
2078 );
2079 debug!(target: "feagi-bdu", "Updated mapping from {} to {} with {} connections",
2080 src_area_id, dst_area_id, mapping_data.len());
2081 }
2082 }
2083
2084 self.refresh_cortical_mappings_hash();
2085
2086 Ok(())
2087 }
2088
2089 pub fn regenerate_synapses_for_mapping(
2102 &mut self,
2103 src_area_id: &CorticalID,
2104 dst_area_id: &CorticalID,
2105 ) -> BduResult<usize> {
2106 use tracing::info;
2107
2108 debug!(target: "feagi-bdu", "Regenerating synapses: {} -> {}", src_area_id, dst_area_id);
2109
2110 let mapping_rules_len = self
2111 .cortical_areas
2112 .get(src_area_id)
2113 .and_then(|area| area.properties.get("cortical_mapping_dst"))
2114 .and_then(|v| v.as_object())
2115 .and_then(|map| map.get(&dst_area_id.as_base_64()))
2116 .and_then(|v| v.as_array())
2117 .map(|arr| arr.len())
2118 .unwrap_or(0);
2119 tracing::debug!(
2120 target: "feagi-bdu",
2121 "Mapping rules for {} -> {}: {}",
2122 src_area_id,
2123 dst_area_id,
2124 mapping_rules_len
2125 );
2126
2127 let Some(npu_arc) = self.npu.clone() else {
2129 info!(target: "feagi-bdu", "NPU not available - skipping synapse regeneration");
2130 return Ok(0);
2131 };
2132
2133 let src_idx = *self.cortical_id_to_idx.get(src_area_id).ok_or_else(|| {
2143 BduError::InvalidArea(format!("No cortical idx for source area {}", src_area_id))
2144 })?;
2145 let dst_idx = *self.cortical_id_to_idx.get(dst_area_id).ok_or_else(|| {
2146 BduError::InvalidArea(format!(
2147 "No cortical idx for destination area {}",
2148 dst_area_id
2149 ))
2150 })?;
2151
2152 let mut pruned_synapse_count: usize = 0;
2155 use std::time::Instant;
2156 let start = Instant::now();
2157
2158 let (sources, targets) = {
2164 let lock_start = std::time::Instant::now();
2165 let npu = npu_arc.lock().unwrap();
2166 let lock_wait = lock_start.elapsed();
2167 tracing::debug!(
2168 target: "feagi-bdu",
2169 "[NPU-LOCK] prune list lock wait {:.2}ms for {} -> {}",
2170 lock_wait.as_secs_f64() * 1000.0,
2171 src_area_id,
2172 dst_area_id
2173 );
2174 let sources: Vec<NeuronId> = npu
2175 .get_neurons_in_cortical_area(src_idx)
2176 .into_iter()
2177 .map(NeuronId)
2178 .collect();
2179 let targets: Vec<NeuronId> = npu
2180 .get_neurons_in_cortical_area(dst_idx)
2181 .into_iter()
2182 .map(NeuronId)
2183 .collect();
2184 (sources, targets)
2185 };
2186
2187 tracing::debug!(
2188 target: "feagi-bdu",
2189 "Prune synapses: {} sources, {} targets",
2190 sources.len(),
2191 targets.len()
2192 );
2193
2194 if !sources.is_empty() && !targets.is_empty() {
2195 let remove_start = Instant::now();
2196 pruned_synapse_count = {
2197 let lock_start = std::time::Instant::now();
2198 let mut npu = npu_arc.lock().unwrap();
2199 let lock_wait = lock_start.elapsed();
2200 tracing::debug!(
2201 target: "feagi-bdu",
2202 "[NPU-LOCK] prune remove lock wait {:.2}ms for {} -> {}",
2203 lock_wait.as_secs_f64() * 1000.0,
2204 src_area_id,
2205 dst_area_id
2206 );
2207 npu.remove_synapses_between(sources, targets)
2211 };
2212 let remove_time = remove_start.elapsed();
2213 let total_time = start.elapsed();
2214
2215 info!(
2216 target: "feagi-bdu",
2217 "Pruned {} existing synapses for mapping {} -> {} (total={}ms, remove={}ms)",
2218 pruned_synapse_count,
2219 src_area_id,
2220 dst_area_id,
2221 total_time.as_millis(),
2222 remove_time.as_millis()
2223 );
2224
2225 if pruned_synapse_count > 0 {
2227 let pruned_u32 = u32::try_from(pruned_synapse_count).map_err(|_| {
2228 BduError::Internal(format!(
2229 "Pruned synapse count overflow (usize -> u32): {}",
2230 pruned_synapse_count
2231 ))
2232 })?;
2233 let state_manager = StateManager::instance();
2234 let state_manager = state_manager.read();
2235 let core_state = state_manager.get_core_state();
2236 core_state.subtract_synapse_count(pruned_u32);
2237 state_manager.subtract_cortical_area_outgoing_synapses(
2238 &src_area_id.as_base_64(),
2239 pruned_synapse_count,
2240 );
2241 state_manager.subtract_cortical_area_incoming_synapses(
2242 &dst_area_id.as_base_64(),
2243 pruned_synapse_count,
2244 );
2245
2246 {
2250 let mut cache = self.cached_synapse_counts_per_area.write();
2251 let entry = cache
2252 .entry(*src_area_id)
2253 .or_insert_with(|| AtomicUsize::new(0));
2254 let mut current = entry.load(Ordering::Relaxed);
2255 loop {
2256 let next = current.saturating_sub(pruned_synapse_count);
2257 match entry.compare_exchange(
2258 current,
2259 next,
2260 Ordering::Relaxed,
2261 Ordering::Relaxed,
2262 ) {
2263 Ok(_) => break,
2264 Err(v) => current = v,
2265 }
2266 }
2267 }
2268 }
2269 }
2270
2271 let synapse_count = self.apply_cortical_mapping_for_pair(src_area_id, dst_area_id)?;
2278 tracing::debug!(
2279 target: "feagi-bdu",
2280 "Synaptogenesis created {} synapses for {} -> {}",
2281 synapse_count,
2282 src_area_id,
2283 dst_area_id
2284 );
2285
2286 if synapse_count > 0 {
2289 let created_u32 = u32::try_from(synapse_count).map_err(|_| {
2290 BduError::Internal(format!(
2291 "Created synapse count overflow (usize -> u32): {}",
2292 synapse_count
2293 ))
2294 })?;
2295
2296 {
2298 let mut cache = self.cached_synapse_counts_per_area.write();
2299 cache
2300 .entry(*src_area_id)
2301 .or_insert_with(|| AtomicUsize::new(0))
2302 .fetch_add(synapse_count, Ordering::Relaxed);
2303 }
2304
2305 let state_manager = StateManager::instance();
2307 let state_manager = state_manager.read();
2308 let core_state = state_manager.get_core_state();
2309 core_state.add_synapse_count(created_u32);
2310 state_manager
2311 .add_cortical_area_outgoing_synapses(&src_area_id.as_base_64(), synapse_count);
2312 state_manager
2313 .add_cortical_area_incoming_synapses(&dst_area_id.as_base_64(), synapse_count);
2314 }
2315
2316 let src_idx_for_upstream = src_idx;
2319
2320 let has_mapping = self
2322 .cortical_areas
2323 .get(src_area_id)
2324 .and_then(|area| area.properties.get("cortical_mapping_dst"))
2325 .and_then(|v| v.as_object())
2326 .and_then(|map| map.get(&dst_area_id.as_base_64()))
2327 .is_some();
2328
2329 debug!(target: "feagi-bdu",
2330 "Mapping result: {} synapses, {} -> {} (mapping_exists={}, will {}update upstream)",
2331 synapse_count,
2332 src_area_id.as_base_64(),
2333 dst_area_id.as_base_64(),
2334 has_mapping,
2335 if has_mapping { "" } else { "NOT " }
2336 );
2337
2338 if has_mapping {
2339 self.add_upstream_area(dst_area_id, src_idx_for_upstream);
2341
2342 if let Some(dst_area) = self.cortical_areas.get(dst_area_id) {
2343 if matches!(dst_area.cortical_type, CorticalAreaType::Memory(_)) {
2344 if let Err(e) = self.ensure_memory_twin_area(dst_area_id, src_area_id) {
2345 warn!(
2346 target: "feagi-bdu",
2347 "Failed to ensure memory twin for {} -> {}: {}",
2348 src_area_id.as_base_64(),
2349 dst_area_id.as_base_64(),
2350 e
2351 );
2352 }
2353 }
2354 }
2355
2356 #[cfg(feature = "plasticity")]
2358 if let Some(ref executor) = self.plasticity_executor {
2359 use feagi_evolutionary::extract_memory_properties;
2360
2361 if let Some(dst_area) = self.cortical_areas.get(dst_area_id) {
2362 if let Some(mem_props) = extract_memory_properties(&dst_area.properties) {
2363 let upstream_areas = self.get_upstream_cortical_areas(dst_area_id);
2364 debug!(
2365 target: "feagi-bdu",
2366 "Registering memory area idx={} id={} upstream={} depth={}",
2367 dst_area.cortical_idx,
2368 dst_area_id.as_base_64(),
2369 upstream_areas.len(),
2370 mem_props.temporal_depth
2371 );
2372
2373 if let Some(ref npu_arc) = self.npu {
2376 if let Ok(mut npu) = npu_arc.lock() {
2377 let existing_configs = npu.get_all_fire_ledger_configs();
2378 for &upstream_idx in &upstream_areas {
2379 let existing = existing_configs
2380 .iter()
2381 .find(|(idx, _)| *idx == upstream_idx)
2382 .map(|(_, w)| *w)
2383 .unwrap_or(0);
2384
2385 let desired = mem_props.temporal_depth as usize;
2386 let resolved = existing.max(desired);
2387 if resolved != existing {
2388 if let Err(e) =
2389 npu.configure_fire_ledger_window(upstream_idx, resolved)
2390 {
2391 warn!(
2392 target: "feagi-bdu",
2393 "Failed to configure FireLedger window for upstream area idx={} (requested={}): {}",
2394 upstream_idx,
2395 resolved,
2396 e
2397 );
2398 }
2399 }
2400 }
2401 } else {
2402 warn!(target: "feagi-bdu", "Failed to lock NPU for FireLedger configuration");
2403 }
2404 }
2405
2406 if let Ok(exec) = executor.lock() {
2407 use feagi_npu_plasticity::{
2408 MemoryNeuronLifecycleConfig, PlasticityExecutor,
2409 };
2410
2411 let lifecycle_config = MemoryNeuronLifecycleConfig {
2412 initial_lifespan: mem_props.init_lifespan,
2413 lifespan_growth_rate: mem_props.lifespan_growth_rate,
2414 longterm_threshold: mem_props.longterm_threshold,
2415 max_reactivations: 1000,
2416 };
2417
2418 exec.register_memory_area(
2419 dst_area.cortical_idx,
2420 dst_area_id.as_base_64(),
2421 mem_props.temporal_depth,
2422 upstream_areas.clone(),
2423 Some(lifecycle_config),
2424 mem_props.mp_learning_enabled,
2425 );
2426 } else {
2427 warn!(target: "feagi-bdu", "Failed to lock PlasticityExecutor");
2428 }
2429 } else {
2430 debug!(
2431 target: "feagi-bdu",
2432 "Skipping plasticity registration: no memory properties for area {}",
2433 dst_area_id.as_base_64()
2434 );
2435 }
2436 } else {
2437 warn!(target: "feagi-bdu", "Destination area {} not found in cortical_areas", dst_area_id.as_base_64());
2438 }
2439 } else {
2440 warn!(
2441 target: "feagi-bdu",
2442 "PlasticityExecutor not available; memory area {} not registered",
2443 dst_area_id.as_base_64()
2444 );
2445 }
2446
2447 #[cfg(not(feature = "plasticity"))]
2448 {
2449 info!(target: "feagi-bdu", "Plasticity feature disabled at compile time");
2450 }
2451 } else {
2452 self.remove_upstream_area(dst_area_id, src_idx_for_upstream);
2454
2455 let mut npu = npu_arc.lock().unwrap();
2457 let _was_registered = npu.unregister_stdp_mapping(src_idx, dst_idx);
2458 }
2459
2460 debug!(
2461 target: "feagi-bdu",
2462 "Created {} new synapses: {} -> {}",
2463 synapse_count,
2464 src_area_id,
2465 dst_area_id
2466 );
2467
2468 if pruned_synapse_count > 0 || synapse_count == 0 {
2471 let mut npu = npu_arc.lock().unwrap();
2472 npu.rebuild_synapse_index();
2473 info!(
2474 target: "feagi-bdu",
2475 "Rebuilt synapse index after regenerating {} -> {} (pruned={}, created={})",
2476 src_area_id,
2477 dst_area_id,
2478 pruned_synapse_count,
2479 synapse_count
2480 );
2481 } else {
2482 debug!(
2483 target: "feagi-bdu",
2484 "Skipped synapse index rebuild for mapping {} -> {} (created={}, pruned=0; index rebuilt during synaptogenesis)",
2485 src_area_id,
2486 dst_area_id,
2487 synapse_count
2488 );
2489 }
2490
2491 {
2493 let npu = npu_arc.lock().unwrap();
2494 let fresh_count = npu.get_synapse_count();
2495 self.cached_synapse_count
2496 .store(fresh_count, Ordering::Relaxed);
2497 }
2498
2499 Ok(synapse_count)
2500 }
2501
2502 fn json_number_as_i64_for_stdp(v: &serde_json::Value) -> Option<i64> {
2504 v.as_i64().or_else(|| v.as_f64().map(|f| f as i64))
2505 }
2506
2507 fn json_number_as_usize_for_stdp(v: &serde_json::Value) -> Option<usize> {
2508 v.as_u64()
2509 .map(|n| n as usize)
2510 .or_else(|| v.as_f64().map(|f| f as usize))
2511 }
2512
2513 #[allow(clippy::too_many_arguments)]
2515 fn register_stdp_mapping_for_rule(
2516 npu: &Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
2517 src_area_id: &CorticalID,
2518 dst_area_id: &CorticalID,
2519 src_cortical_idx: u32,
2520 dst_cortical_idx: u32,
2521 rule_obj: &serde_json::Map<String, serde_json::Value>,
2522 bidirectional_stdp: bool,
2523 synapse_psp: f32,
2524 synapse_type: feagi_npu_neural::SynapseType,
2525 ) -> BduResult<()> {
2526 let plasticity_window = rule_obj
2527 .get("plasticity_window")
2528 .and_then(Self::json_number_as_usize_for_stdp)
2529 .ok_or_else(|| {
2530 BduError::Internal(format!(
2531 "Missing plasticity_window in plastic mapping rule {} -> {}",
2532 src_area_id, dst_area_id
2533 ))
2534 })?;
2535 let plasticity_constant = rule_obj
2536 .get("plasticity_constant")
2537 .and_then(Self::json_number_as_i64_for_stdp)
2538 .ok_or_else(|| {
2539 BduError::Internal(format!(
2540 "Missing plasticity_constant in plastic mapping rule {} -> {}",
2541 src_area_id, dst_area_id
2542 ))
2543 })?;
2544 let ltp_i64 = rule_obj
2545 .get("ltp_multiplier")
2546 .and_then(Self::json_number_as_i64_for_stdp)
2547 .ok_or_else(|| {
2548 BduError::Internal(format!(
2549 "Missing ltp_multiplier in plastic mapping rule {} -> {}",
2550 src_area_id, dst_area_id
2551 ))
2552 })?;
2553 let ltp_multiplier = i8::try_from(ltp_i64).map_err(|_| {
2554 BduError::Internal(format!(
2555 "ltp_multiplier must fit in i8 range {}..={} (got {}) on mapping {} -> {}",
2556 i8::MIN,
2557 i8::MAX,
2558 ltp_i64,
2559 src_area_id,
2560 dst_area_id
2561 ))
2562 })?;
2563 let ltd_i64 = rule_obj
2564 .get("ltd_multiplier")
2565 .and_then(Self::json_number_as_i64_for_stdp)
2566 .ok_or_else(|| {
2567 BduError::Internal(format!(
2568 "Missing ltd_multiplier in plastic mapping rule {} -> {}",
2569 src_area_id, dst_area_id
2570 ))
2571 })?;
2572 let ltd_multiplier = i8::try_from(ltd_i64).map_err(|_| {
2573 BduError::Internal(format!(
2574 "ltd_multiplier must fit in i8 range {}..={} (got {}) on mapping {} -> {}",
2575 i8::MIN,
2576 i8::MAX,
2577 ltd_i64,
2578 src_area_id,
2579 dst_area_id
2580 ))
2581 })?;
2582
2583 let plasticity_mode = match rule_obj.get("plasticity_mode").and_then(|v| v.as_str()) {
2587 Some(s) if s.eq_ignore_ascii_case("rstdp") || s.eq_ignore_ascii_case("r-stdp") => {
2588 feagi_npu_burst_engine::npu::PlasticityMode::RStdp
2589 }
2590 Some(s) if s.eq_ignore_ascii_case("stdp") => {
2591 feagi_npu_burst_engine::npu::PlasticityMode::Stdp
2592 }
2593 Some(s) if s.eq_ignore_ascii_case("off") => {
2594 feagi_npu_burst_engine::npu::PlasticityMode::Off
2595 }
2596 Some(other) => {
2597 return Err(BduError::Internal(format!(
2598 "Unknown plasticity_mode '{}' in mapping rule {} -> {}",
2599 other, src_area_id, dst_area_id
2600 )));
2601 }
2602 None => feagi_npu_burst_engine::npu::PlasticityMode::Stdp,
2603 };
2604
2605 let eligibility_decay_bursts = rule_obj
2607 .get("eligibility_decay_bursts")
2608 .and_then(|v| v.as_u64())
2609 .map(|n| n as u32)
2610 .unwrap_or(0);
2611 let reward_source_area_id = rule_obj
2612 .get("reward_source_area")
2613 .and_then(|v| v.as_str())
2614 .map(str::to_string);
2615 let punishment_source_area_id = rule_obj
2616 .get("punishment_source_area")
2617 .and_then(|v| v.as_str())
2618 .map(str::to_string);
2619
2620 let max_weight_provided = rule_obj.get("max_weight").is_some()
2625 && !rule_obj
2626 .get("max_weight")
2627 .map(|v| v.is_null())
2628 .unwrap_or(true);
2629 let max_weight: f32 = if max_weight_provided {
2630 let raw = rule_obj
2631 .get("max_weight")
2632 .and_then(|v| v.as_f64())
2633 .ok_or_else(|| {
2634 BduError::Internal(format!(
2635 "max_weight must be a number on mapping {} -> {}",
2636 src_area_id, dst_area_id
2637 ))
2638 })?;
2639 if raw.is_nan() || raw <= 0.0 {
2640 return Err(BduError::Internal(format!(
2641 "max_weight must be strictly positive (got {}) on mapping {} -> {}",
2642 raw, src_area_id, dst_area_id
2643 )));
2644 }
2645 raw as f32
2646 } else {
2647 f32::INFINITY
2648 };
2649
2650 let plasticity_eta_provided = rule_obj.get("plasticity_eta").is_some()
2653 && !rule_obj
2654 .get("plasticity_eta")
2655 .map(|v| v.is_null())
2656 .unwrap_or(true);
2657 let plasticity_eta: f32 = if plasticity_eta_provided {
2658 let raw = rule_obj
2659 .get("plasticity_eta")
2660 .and_then(|v| v.as_f64())
2661 .ok_or_else(|| {
2662 BduError::Internal(format!(
2663 "plasticity_eta must be a number on mapping {} -> {}",
2664 src_area_id, dst_area_id
2665 ))
2666 })?;
2667 if raw.is_nan() || raw <= 0.0 || !raw.is_finite() {
2668 return Err(BduError::Internal(format!(
2669 "plasticity_eta must be finite and strictly positive (got {}) on mapping {} -> {}",
2670 raw, src_area_id, dst_area_id
2671 )));
2672 }
2673 raw as f32
2674 } else {
2675 1.0
2676 };
2677
2678 if !matches!(
2680 plasticity_mode,
2681 feagi_npu_burst_engine::npu::PlasticityMode::RStdp
2682 ) && (reward_source_area_id.is_some()
2683 || punishment_source_area_id.is_some()
2684 || eligibility_decay_bursts != 0)
2685 {
2686 return Err(BduError::Internal(format!(
2687 "R-STDP fields (reward_source_area / punishment_source_area / eligibility_decay_bursts) \
2688 only valid when plasticity_mode='rstdp' on mapping {} -> {}",
2689 src_area_id, dst_area_id
2690 )));
2691 }
2692
2693 if matches!(
2697 plasticity_mode,
2698 feagi_npu_burst_engine::npu::PlasticityMode::Off
2699 ) && max_weight_provided
2700 {
2701 return Err(BduError::Internal(format!(
2702 "max_weight is only valid when plasticity_mode is 'stdp' or 'rstdp' (got off) on mapping {} -> {}",
2703 src_area_id, dst_area_id
2704 )));
2705 }
2706
2707 if matches!(
2708 plasticity_mode,
2709 feagi_npu_burst_engine::npu::PlasticityMode::Off
2710 ) && plasticity_eta_provided
2711 {
2712 return Err(BduError::Internal(format!(
2713 "plasticity_eta is only valid when plasticity_mode is 'stdp' or 'rstdp' (got off) on mapping {} -> {}",
2714 src_area_id, dst_area_id
2715 )));
2716 }
2717
2718 trace!(target: "feagi-bdu", "[LOCK-TRACE] create_neurons_for_area: attempting NPU lock");
2719 let mut npu_lock = npu
2720 .lock()
2721 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
2722 trace!(target: "feagi-bdu", "[LOCK-TRACE] create_neurons_for_area: acquired NPU lock");
2723
2724 let resolve_optional_area =
2729 |label: &str, name_opt: &Option<String>| -> BduResult<Option<u32>> {
2730 let Some(name) = name_opt else {
2731 return Ok(None);
2732 };
2733 match npu_lock.get_cortical_area_id(name.as_str()) {
2734 Some(idx) => Ok(Some(idx)),
2735 None => Err(BduError::Internal(format!(
2736 "Unknown {} cortical area '{}' on R-STDP mapping {} -> {}",
2737 label, name, src_area_id, dst_area_id
2738 ))),
2739 }
2740 };
2741 let reward_source_area =
2742 resolve_optional_area("reward_source_area", &reward_source_area_id)?;
2743 let punishment_source_area =
2744 resolve_optional_area("punishment_source_area", &punishment_source_area_id)?;
2745
2746 if matches!(
2748 plasticity_mode,
2749 feagi_npu_burst_engine::npu::PlasticityMode::Off
2750 ) {
2751 return Ok(());
2752 }
2753
2754 let params = feagi_npu_burst_engine::npu::StdpMappingParams {
2755 plasticity_window,
2756 plasticity_constant,
2757 ltp_multiplier,
2758 ltd_multiplier,
2759 bidirectional_stdp,
2760 synapse_psp,
2761 synapse_type,
2762 plasticity_mode,
2763 eligibility_decay_bursts,
2764 reward_source_area,
2765 punishment_source_area,
2766 max_weight,
2767 plasticity_eta,
2768 };
2769
2770 npu_lock
2771 .register_stdp_mapping(src_cortical_idx, dst_cortical_idx, params)
2772 .map_err(|e| {
2773 BduError::Internal(format!(
2774 "Failed to register STDP mapping {} -> {}: {}",
2775 src_area_id, dst_area_id, e
2776 ))
2777 })?;
2778
2779 let mut areas_to_track: Vec<(u32, usize)> = vec![
2783 (src_cortical_idx, plasticity_window),
2784 (dst_cortical_idx, plasticity_window),
2785 ];
2786 if let Some(area) = reward_source_area {
2787 areas_to_track.push((area, 1));
2788 }
2789 if let Some(area) = punishment_source_area {
2790 areas_to_track.push((area, 1));
2791 }
2792
2793 let existing_configs = npu_lock.get_all_fire_ledger_configs();
2794 for (area_idx, required_depth) in areas_to_track {
2795 let existing = existing_configs
2796 .iter()
2797 .find(|(idx, _)| *idx == area_idx)
2798 .map(|(_, w)| *w)
2799 .unwrap_or(0);
2800 let resolved = existing.max(required_depth);
2801 if resolved != existing {
2802 npu_lock
2803 .configure_fire_ledger_window(area_idx, resolved)
2804 .map_err(|e| {
2805 BduError::Internal(format!(
2806 "Failed to configure FireLedger window for area idx={} (requested={}): {}",
2807 area_idx, resolved, e
2808 ))
2809 })?;
2810 }
2811 }
2812
2813 Ok(())
2814 }
2815
2816 fn resolve_synapse_params_for_rule(
2818 &self,
2819 src_area_id: &CorticalID,
2820 rule: &serde_json::Value,
2821 ) -> BduResult<(f32, f32, feagi_npu_neural::SynapseType, u8)> {
2822 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2824 crate::types::BduError::InvalidArea(format!("Source area not found: {}", src_area_id))
2825 })?;
2826
2827 let (weight, synapse_type) = {
2829 let parse_f64 = |v: &serde_json::Value| -> Option<f64> {
2830 if let Some(i) = v.as_i64() {
2831 return Some(i as f64);
2832 }
2833 v.as_f64()
2834 };
2835
2836 let mult: f64 = if let Some(obj) = rule.as_object() {
2837 obj.get("postSynapticCurrent_multiplier")
2838 .and_then(parse_f64)
2839 .unwrap_or(1.0)
2840 } else if let Some(arr) = rule.as_array() {
2841 arr.get(2).and_then(parse_f64).unwrap_or(1.0)
2842 } else {
2843 128.0
2844 };
2845
2846 if mult < 0.0 {
2847 (mult.abs() as f32, feagi_npu_neural::SynapseType::Inhibitory)
2848 } else {
2849 (mult as f32, feagi_npu_neural::SynapseType::Excitatory)
2850 }
2851 };
2852
2853 use crate::models::cortical_area::CorticalAreaExt;
2855 let psp_f32 = src_area.postsynaptic_current();
2856
2857 let delay_bursts: u8 = if let Some(obj) = rule.as_object() {
2858 obj.get("synaptic_delay_bursts")
2859 .and_then(|v| v.as_u64())
2860 .map(|d| u8::try_from(d).unwrap_or(1))
2861 .unwrap_or(1)
2862 } else if let Some(arr) = rule.as_array() {
2863 arr.get(8)
2864 .and_then(|v| v.as_u64())
2865 .map(|d| u8::try_from(d).unwrap_or(1))
2866 .unwrap_or(1)
2867 } else {
2868 1
2869 };
2870 if delay_bursts < 1 {
2871 return Err(crate::types::BduError::Internal(format!(
2872 "synaptic_delay_bursts must be >= 1 (src area {})",
2873 src_area_id.as_base_64()
2874 )));
2875 }
2876
2877 tracing::debug!(
2878 target: "feagi-bdu",
2879 "Resolved synapse params src={} weight={} psp={} type={:?} delay_bursts={}",
2880 src_area_id.as_base_64(),
2881 weight,
2882 psp_f32,
2883 synapse_type,
2884 delay_bursts
2885 );
2886
2887 Ok((weight, psp_f32, synapse_type, delay_bursts))
2888 }
2889
2890 fn apply_cortical_mapping_for_pair(
2892 &mut self,
2893 src_area_id: &CorticalID,
2894 dst_area_id: &CorticalID,
2895 ) -> BduResult<usize> {
2896 let rules = {
2902 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2903 crate::types::BduError::InvalidArea(format!(
2904 "Source area not found: {}",
2905 src_area_id
2906 ))
2907 })?;
2908
2909 let Some(mapping_dst) = src_area
2910 .properties
2911 .get("cortical_mapping_dst")
2912 .and_then(|v| v.as_object())
2913 else {
2914 return Ok(0);
2915 };
2916
2917 let Some(rules) = Self::get_mapping_rules_for_destination(mapping_dst, dst_area_id)
2918 else {
2919 return Ok(0);
2920 };
2921
2922 rules.clone()
2923 }; if rules.is_empty() {
2926 return Ok(0);
2927 }
2928
2929 let src_cortical_idx = *self.cortical_id_to_idx.get(src_area_id).ok_or_else(|| {
2931 crate::types::BduError::InvalidArea(format!("No index for {}", src_area_id))
2932 })?;
2933 let dst_cortical_idx = *self.cortical_id_to_idx.get(dst_area_id).ok_or_else(|| {
2934 crate::types::BduError::InvalidArea(format!("No index for {}", dst_area_id))
2935 })?;
2936
2937 let npu_arc = self
2939 .npu
2940 .as_ref()
2941 .ok_or_else(|| crate::types::BduError::Internal("NPU not connected".to_string()))?
2942 .clone();
2943
2944 tracing::debug!(
2945 target: "feagi-bdu",
2946 "Applying {} mapping rule(s) for {} -> {}",
2947 rules.len(),
2948 src_area_id,
2949 dst_area_id
2950 );
2951 let mut total_synapses = 0;
2953 for rule in &rules {
2954 let morphology_id = if let Some(rule_obj) = rule.as_object() {
2955 rule_obj
2956 .get("morphology_id")
2957 .and_then(|v| v.as_str())
2958 .unwrap_or("unknown")
2959 .to_string()
2960 } else if let Some(rule_arr) = rule.as_array() {
2961 rule_arr
2962 .first()
2963 .and_then(|v| v.as_str())
2964 .unwrap_or("unknown")
2965 .to_string()
2966 } else {
2967 "unknown".to_string()
2968 };
2969
2970 let rule_keys: Vec<String> = rule
2971 .as_object()
2972 .map(|obj| obj.keys().cloned().collect())
2973 .unwrap_or_default();
2974
2975 let mut plasticity_flag = rule
2977 .as_object()
2978 .and_then(|obj| obj.get("plasticity_flag"))
2979 .and_then(|v| v.as_bool())
2980 .unwrap_or(false);
2981 if morphology_id == "associative_memory" {
2982 plasticity_flag = true;
2983 }
2984 if plasticity_flag {
2985 let Some(rule_obj) = rule.as_object() else {
2986 return Err(crate::types::BduError::InvalidMorphology(
2987 "Plasticity mapping rule must be an object format".to_string(),
2988 ));
2989 };
2990 let (_weight, psp, synapse_type, _delay_bursts) =
2991 self.resolve_synapse_params_for_rule(src_area_id, rule)?;
2992 let bidirectional_stdp = false;
2995 if let Err(e) = Self::register_stdp_mapping_for_rule(
2996 &npu_arc,
2997 src_area_id,
2998 dst_area_id,
2999 src_cortical_idx,
3000 dst_cortical_idx,
3001 rule_obj,
3002 bidirectional_stdp,
3003 psp,
3004 synapse_type,
3005 ) {
3006 tracing::error!(
3007 target: "feagi-bdu",
3008 "STDP mapping registration failed for {} -> {} (morphology={}, keys={:?}): {}",
3009 src_area_id,
3010 dst_area_id,
3011 morphology_id,
3012 rule_keys,
3013 e
3014 );
3015 return Err(e);
3016 }
3017 }
3018
3019 if let Some(gate_area_str) = rule
3023 .as_object()
3024 .and_then(|obj| obj.get("gate_source_area"))
3025 .and_then(|v| v.as_str())
3026 {
3027 let gate_area_id = CorticalID::try_from_base_64(gate_area_str).map_err(|_| {
3028 crate::types::BduError::Internal(format!(
3029 "Invalid gate_source_area '{}' on mapping {} -> {}",
3030 gate_area_str, src_area_id, dst_area_id
3031 ))
3032 })?;
3033 let gate_cortical_idx =
3034 self.cortical_id_to_idx.get(&gate_area_id).ok_or_else(|| {
3035 crate::types::BduError::Internal(format!(
3036 "Unknown gate_source_area '{}' on mapping {} -> {}",
3037 gate_area_str, src_area_id, dst_area_id
3038 ))
3039 })?;
3040
3041 let mut npu_lock = npu_arc.lock().map_err(|_| {
3042 crate::types::BduError::Internal(
3043 "Failed to acquire NPU lock for gate registration".to_string(),
3044 )
3045 })?;
3046 if let Err(e) = npu_lock.register_gate_mapping(
3047 src_cortical_idx,
3048 dst_cortical_idx,
3049 *gate_cortical_idx,
3050 ) {
3051 tracing::error!(
3052 target: "feagi-bdu",
3053 "Gate mapping registration failed for {} -> {} (gate={}): {}",
3054 src_area_id,
3055 dst_area_id,
3056 gate_area_str,
3057 e
3058 );
3059 return Err(crate::types::BduError::Internal(format!(
3060 "Gate registration failed: {}",
3061 e
3062 )));
3063 }
3064 }
3065
3066 let synapse_count = match self.apply_single_morphology_rule(
3068 src_area_id,
3069 dst_area_id,
3070 rule,
3071 ) {
3072 Ok(count) => count,
3073 Err(e) => {
3074 tracing::error!(
3075 target: "feagi-bdu",
3076 "Mapping rule application failed for {} -> {} (morphology={}, keys={:?}): {}",
3077 src_area_id,
3078 dst_area_id,
3079 morphology_id,
3080 rule_keys,
3081 e
3082 );
3083 return Err(e);
3084 }
3085 };
3086 total_synapses += synapse_count;
3087 tracing::debug!(
3088 target: "feagi-bdu",
3089 "Rule {} created {} synapses for {} -> {}",
3090 morphology_id,
3091 synapse_count,
3092 src_area_id,
3093 dst_area_id
3094 );
3095 }
3096
3097 Ok(total_synapses)
3098 }
3099
3100 #[allow(clippy::too_many_arguments)]
3114 fn apply_function_morphology(
3115 &self,
3116 morphology_id: &str,
3117 rule: &serde_json::Value,
3118 npu_arc: &Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
3119 npu: &mut feagi_npu_burst_engine::DynamicNPU,
3120 src_area_id: &CorticalID,
3121 dst_area_id: &CorticalID,
3122 src_idx: u32,
3123 dst_idx: u32,
3124 weight: f32,
3125 psp: f32,
3126 synapse_attractivity: u8,
3127 synapse_type: feagi_npu_neural::SynapseType,
3128 delay_bursts: u8,
3129 ) -> BduResult<usize> {
3130 match morphology_id {
3131 "projector" | "transpose_xy" | "transpose_yz" | "transpose_xz" => {
3132 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3134 crate::types::BduError::InvalidArea(format!(
3135 "Source area not found: {}",
3136 src_area_id
3137 ))
3138 })?;
3139 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3140 crate::types::BduError::InvalidArea(format!(
3141 "Destination area not found: {}",
3142 dst_area_id
3143 ))
3144 })?;
3145
3146 let src_dimensions = (
3147 src_area.dimensions.width as usize,
3148 src_area.dimensions.height as usize,
3149 src_area.dimensions.depth as usize,
3150 );
3151 let dst_dimensions = (
3152 dst_area.dimensions.width as usize,
3153 dst_area.dimensions.height as usize,
3154 dst_area.dimensions.depth as usize,
3155 );
3156
3157 let transpose = match morphology_id {
3160 "transpose_xy" => Some((1, 0, 2)),
3161 "transpose_yz" => Some((0, 2, 1)),
3162 "transpose_xz" => Some((2, 1, 0)),
3163 _ => None,
3164 };
3165
3166 use crate::connectivity::core_morphologies::apply_projector_morphology_with_dimensions;
3167 let count = apply_projector_morphology_with_dimensions(
3168 npu,
3169 src_idx,
3170 dst_idx,
3171 src_dimensions,
3172 dst_dimensions,
3173 transpose,
3174 None, weight,
3176 psp,
3177 synapse_attractivity,
3178 synapse_type,
3179 0,
3180 delay_bursts,
3181 )?;
3182 npu.rebuild_synapse_index();
3184 Ok(count as usize)
3185 }
3186 "centered_projector" => {
3187 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3188 crate::types::BduError::InvalidArea(format!(
3189 "Source area not found: {}",
3190 src_area_id
3191 ))
3192 })?;
3193 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3194 crate::types::BduError::InvalidArea(format!(
3195 "Destination area not found: {}",
3196 dst_area_id
3197 ))
3198 })?;
3199
3200 let src_dimensions = (
3201 src_area.dimensions.width as usize,
3202 src_area.dimensions.height as usize,
3203 src_area.dimensions.depth as usize,
3204 );
3205 let dst_dimensions = (
3206 dst_area.dimensions.width as usize,
3207 dst_area.dimensions.height as usize,
3208 dst_area.dimensions.depth as usize,
3209 );
3210
3211 let count = crate::connectivity::core_morphologies::apply_centered_projector_morphology_with_dimensions(
3212 npu,
3213 src_idx,
3214 dst_idx,
3215 src_dimensions,
3216 dst_dimensions,
3217 weight,
3218 psp,
3219 synapse_attractivity,
3220 synapse_type,
3221 delay_bursts,
3222 )?;
3223 if count > 0 {
3224 npu.rebuild_synapse_index();
3225 }
3226 Ok(count as usize)
3227 }
3228 "episodic_memory" => {
3229 use tracing::trace;
3232 trace!(
3233 target: "feagi-bdu",
3234 "Episodic memory morphology: {} -> {} (no physical synapses, plasticity-driven)",
3235 src_idx, dst_idx
3236 );
3237 Ok(0)
3238 }
3239 "memory_replay" => {
3240 use tracing::trace;
3242 trace!(
3243 target: "feagi-bdu",
3244 "Memory replay morphology: {} -> {} (no physical synapses)",
3245 src_idx, dst_idx
3246 );
3247 Ok(0)
3248 }
3249 "associative_memory" => {
3250 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3254 crate::types::BduError::InvalidArea(format!(
3255 "Source area not found: {}",
3256 src_area_id
3257 ))
3258 })?;
3259 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3260 crate::types::BduError::InvalidArea(format!(
3261 "Destination area not found: {}",
3262 dst_area_id
3263 ))
3264 })?;
3265
3266 if matches!(src_area.cortical_type, CorticalAreaType::Memory(_))
3267 && matches!(dst_area.cortical_type, CorticalAreaType::Memory(_))
3268 {
3269 let src_dimensions = (
3270 src_area.dimensions.width as usize,
3271 src_area.dimensions.height as usize,
3272 src_area.dimensions.depth as usize,
3273 );
3274 let dst_dimensions = (
3275 dst_area.dimensions.width as usize,
3276 dst_area.dimensions.height as usize,
3277 dst_area.dimensions.depth as usize,
3278 );
3279 use crate::connectivity::core_morphologies::apply_projector_morphology_with_dimensions;
3280 let count = apply_projector_morphology_with_dimensions(
3281 npu,
3282 src_idx,
3283 dst_idx,
3284 src_dimensions,
3285 dst_dimensions,
3286 None,
3287 None,
3288 weight,
3289 psp,
3290 synapse_attractivity,
3291 synapse_type,
3292 SYNAPSE_EDGE_ASSOCIATIVE_MEMORY,
3293 delay_bursts,
3294 )?;
3295 npu.rebuild_synapse_index();
3296 Ok(count as usize)
3297 } else {
3298 Ok(0)
3299 }
3300 }
3301 "block_to_block" => {
3302 tracing::warn!(
3303 target: "feagi-bdu",
3304 "🔍 DEBUG apply_function_morphology: block_to_block case reached with src_idx={}, dst_idx={}",
3305 src_idx, dst_idx
3306 );
3307 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3309 crate::types::BduError::InvalidArea(format!(
3310 "Source area not found: {}",
3311 src_area_id
3312 ))
3313 })?;
3314 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3315 crate::types::BduError::InvalidArea(format!(
3316 "Destination area not found: {}",
3317 dst_area_id
3318 ))
3319 })?;
3320
3321 let src_dimensions = (
3322 src_area.dimensions.width as usize,
3323 src_area.dimensions.height as usize,
3324 src_area.dimensions.depth as usize,
3325 );
3326 let dst_dimensions = (
3327 dst_area.dimensions.width as usize,
3328 dst_area.dimensions.height as usize,
3329 dst_area.dimensions.depth as usize,
3330 );
3331
3332 let scalar = if let Some(obj) = rule.as_object() {
3334 if let Some(scalar_arr) =
3336 obj.get("morphology_scalar").and_then(|v| v.as_array())
3337 {
3338 scalar_arr.first().and_then(|v| v.as_i64()).unwrap_or(1) as u32
3340 } else {
3341 1 }
3343 } else if let Some(arr) = rule.as_array() {
3344 arr.get(1).and_then(|v| v.as_i64()).unwrap_or(1) as u32
3346 } else {
3347 1 };
3349
3350 let estimated_neurons = src_dimensions.0 * src_dimensions.1 * src_dimensions.2;
3353 let count = if estimated_neurons > 100_000 {
3354 let _ = npu;
3356
3357 crate::connectivity::synaptogenesis::apply_block_connection_morphology_batched(
3358 npu_arc,
3359 src_idx,
3360 dst_idx,
3361 src_dimensions,
3362 dst_dimensions,
3363 scalar, weight,
3365 psp,
3366 synapse_attractivity,
3367 synapse_type,
3368 delay_bursts,
3369 )? as usize
3370 } else {
3371 tracing::warn!(
3373 target: "feagi-bdu",
3374 "🔍 DEBUG connectome_manager: Calling apply_block_connection_morphology with src_idx={}, dst_idx={}, src_dim={:?}, dst_dim={:?}",
3375 src_idx, dst_idx, src_dimensions, dst_dimensions
3376 );
3377 let count =
3378 crate::connectivity::synaptogenesis::apply_block_connection_morphology(
3379 npu,
3380 src_idx,
3381 dst_idx,
3382 src_dimensions,
3383 dst_dimensions,
3384 scalar, weight,
3386 psp,
3387 synapse_attractivity,
3388 synapse_type,
3389 delay_bursts,
3390 )? as usize;
3391 tracing::warn!(
3392 target: "feagi-bdu",
3393 "🔍 DEBUG connectome_manager: apply_block_connection_morphology returned count={}",
3394 count
3395 );
3396 if count > 0 {
3398 npu.rebuild_synapse_index();
3399 }
3400 count
3401 };
3402
3403 if count > 0 && estimated_neurons > 100_000 {
3405 let mut npu_lock = npu_arc.lock().unwrap();
3406 npu_lock.rebuild_synapse_index();
3407 }
3408
3409 Ok(count)
3410 }
3411 "bitmask_encoder_x" | "bitmask_encoder_y" | "bitmask_encoder_z"
3412 | "bitmask_decoder_x" | "bitmask_decoder_y" | "bitmask_decoder_z" => {
3413 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3414 crate::types::BduError::InvalidArea(format!(
3415 "Source area not found: {}",
3416 src_area_id
3417 ))
3418 })?;
3419 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3420 crate::types::BduError::InvalidArea(format!(
3421 "Destination area not found: {}",
3422 dst_area_id
3423 ))
3424 })?;
3425
3426 let src_dimensions = (
3427 src_area.dimensions.width as usize,
3428 src_area.dimensions.height as usize,
3429 src_area.dimensions.depth as usize,
3430 );
3431 let dst_dimensions = (
3432 dst_area.dimensions.width as usize,
3433 dst_area.dimensions.height as usize,
3434 dst_area.dimensions.depth as usize,
3435 );
3436
3437 let (axis, mode) = match morphology_id {
3438 "bitmask_encoder_x" => (
3439 crate::connectivity::core_morphologies::BitmaskAxis::X,
3440 crate::connectivity::core_morphologies::BitmaskMode::Encoder,
3441 ),
3442 "bitmask_encoder_y" => (
3443 crate::connectivity::core_morphologies::BitmaskAxis::Y,
3444 crate::connectivity::core_morphologies::BitmaskMode::Encoder,
3445 ),
3446 "bitmask_encoder_z" => (
3447 crate::connectivity::core_morphologies::BitmaskAxis::Z,
3448 crate::connectivity::core_morphologies::BitmaskMode::Encoder,
3449 ),
3450 "bitmask_decoder_x" => (
3451 crate::connectivity::core_morphologies::BitmaskAxis::X,
3452 crate::connectivity::core_morphologies::BitmaskMode::Decoder,
3453 ),
3454 "bitmask_decoder_y" => (
3455 crate::connectivity::core_morphologies::BitmaskAxis::Y,
3456 crate::connectivity::core_morphologies::BitmaskMode::Decoder,
3457 ),
3458 "bitmask_decoder_z" => (
3459 crate::connectivity::core_morphologies::BitmaskAxis::Z,
3460 crate::connectivity::core_morphologies::BitmaskMode::Decoder,
3461 ),
3462 _ => unreachable!("matched bitmask morphology above"),
3463 };
3464
3465 let count =
3466 crate::connectivity::core_morphologies::apply_bitmask_morphology_with_dimensions(
3467 npu,
3468 src_idx,
3469 dst_idx,
3470 src_dimensions,
3471 dst_dimensions,
3472 axis,
3473 mode,
3474 weight,
3475 psp,
3476 synapse_attractivity,
3477 synapse_type,
3478 delay_bursts,
3479 )?;
3480 if count > 0 {
3481 npu.rebuild_synapse_index();
3482 }
3483 Ok(count as usize)
3484 }
3485 "sweeper" => {
3486 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3487 crate::types::BduError::InvalidArea(format!(
3488 "Destination area not found: {}",
3489 dst_area_id
3490 ))
3491 })?;
3492 let dst_dimensions = (
3493 dst_area.dimensions.width as usize,
3494 dst_area.dimensions.height as usize,
3495 dst_area.dimensions.depth as usize,
3496 );
3497
3498 let count =
3499 crate::connectivity::core_morphologies::apply_sweeper_morphology_with_dimensions(
3500 npu,
3501 src_idx,
3502 dst_idx,
3503 dst_dimensions,
3504 weight,
3505 psp,
3506 synapse_attractivity,
3507 synapse_type,
3508 delay_bursts,
3509 )?;
3510 if count > 0 {
3511 npu.rebuild_synapse_index();
3512 }
3513 Ok(count as usize)
3514 }
3515 "last_to_first" => {
3516 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3517 crate::types::BduError::InvalidArea(format!(
3518 "Source area not found: {}",
3519 src_area_id
3520 ))
3521 })?;
3522 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3523 crate::types::BduError::InvalidArea(format!(
3524 "Destination area not found: {}",
3525 dst_area_id
3526 ))
3527 })?;
3528 let src_dimensions = (
3529 src_area.dimensions.width as usize,
3530 src_area.dimensions.height as usize,
3531 src_area.dimensions.depth as usize,
3532 );
3533 let dst_dimensions = (
3534 dst_area.dimensions.width as usize,
3535 dst_area.dimensions.height as usize,
3536 dst_area.dimensions.depth as usize,
3537 );
3538
3539 let count = crate::connectivity::core_morphologies::apply_last_to_first_morphology_with_dimensions(
3540 npu,
3541 src_idx,
3542 dst_idx,
3543 src_dimensions,
3544 dst_dimensions,
3545 weight,
3546 psp,
3547 synapse_attractivity,
3548 synapse_type,
3549 delay_bursts,
3550 )?;
3551 if count > 0 {
3552 npu.rebuild_synapse_index();
3553 }
3554 Ok(count as usize)
3555 }
3556 "first_to_last" => {
3557 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3558 crate::types::BduError::InvalidArea(format!(
3559 "Source area not found: {}",
3560 src_area_id
3561 ))
3562 })?;
3563 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3564 crate::types::BduError::InvalidArea(format!(
3565 "Destination area not found: {}",
3566 dst_area_id
3567 ))
3568 })?;
3569 let src_dimensions = (
3570 src_area.dimensions.width as usize,
3571 src_area.dimensions.height as usize,
3572 src_area.dimensions.depth as usize,
3573 );
3574 let dst_dimensions = (
3575 dst_area.dimensions.width as usize,
3576 dst_area.dimensions.height as usize,
3577 dst_area.dimensions.depth as usize,
3578 );
3579
3580 let count = crate::connectivity::core_morphologies::apply_first_to_last_morphology_with_dimensions(
3581 npu,
3582 src_idx,
3583 dst_idx,
3584 src_dimensions,
3585 dst_dimensions,
3586 weight,
3587 psp,
3588 synapse_attractivity,
3589 synapse_type,
3590 delay_bursts,
3591 )?;
3592 if count > 0 {
3593 npu.rebuild_synapse_index();
3594 }
3595 Ok(count as usize)
3596 }
3597 "rotator_z" => {
3598 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3599 crate::types::BduError::InvalidArea(format!(
3600 "Source area not found: {}",
3601 src_area_id
3602 ))
3603 })?;
3604 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3605 crate::types::BduError::InvalidArea(format!(
3606 "Destination area not found: {}",
3607 dst_area_id
3608 ))
3609 })?;
3610 let src_dimensions = (
3611 src_area.dimensions.width as usize,
3612 src_area.dimensions.height as usize,
3613 src_area.dimensions.depth as usize,
3614 );
3615 let dst_dimensions = (
3616 dst_area.dimensions.width as usize,
3617 dst_area.dimensions.height as usize,
3618 dst_area.dimensions.depth as usize,
3619 );
3620
3621 let count = crate::connectivity::core_morphologies::apply_rotator_z_morphology_with_dimensions(
3622 npu,
3623 src_idx,
3624 dst_idx,
3625 src_dimensions,
3626 dst_dimensions,
3627 weight,
3628 psp,
3629 synapse_attractivity,
3630 synapse_type,
3631 delay_bursts,
3632 )?;
3633 if count > 0 {
3634 npu.rebuild_synapse_index();
3635 }
3636 Ok(count as usize)
3637 }
3638 _ => {
3639 use tracing::debug;
3642 debug!(target: "feagi-bdu", "Function morphology {} not yet implemented", morphology_id);
3643 Ok(0)
3644 }
3645 }
3646 }
3647
3648 fn apply_single_morphology_rule(
3650 &mut self,
3651 src_area_id: &CorticalID,
3652 dst_area_id: &CorticalID,
3653 rule: &serde_json::Value,
3654 ) -> BduResult<usize> {
3655 let morphology_id = if let Some(arr) = rule.as_array() {
3657 arr.first().and_then(|v| v.as_str()).unwrap_or("")
3658 } else if let Some(obj) = rule.as_object() {
3659 obj.get("morphology_id")
3660 .and_then(|v| v.as_str())
3661 .unwrap_or("")
3662 } else {
3663 return Ok(0);
3664 };
3665
3666 if morphology_id.is_empty() {
3667 return Ok(0);
3668 }
3669
3670 let morphology = self.morphology_registry.get(morphology_id).ok_or_else(|| {
3672 crate::types::BduError::InvalidMorphology(format!(
3673 "Morphology not found: {}",
3674 morphology_id
3675 ))
3676 })?;
3677
3678 let src_idx = self.cortical_id_to_idx.get(src_area_id).ok_or_else(|| {
3680 crate::types::BduError::InvalidArea(format!(
3681 "Source area ID not found: {}",
3682 src_area_id
3683 ))
3684 })?;
3685 let dst_idx = self.cortical_id_to_idx.get(dst_area_id).ok_or_else(|| {
3686 crate::types::BduError::InvalidArea(format!(
3687 "Destination area ID not found: {}",
3688 dst_area_id
3689 ))
3690 })?;
3691
3692 if let Some(ref npu_arc) = self.npu {
3694 let lock_start = std::time::Instant::now();
3695 let mut npu = npu_arc.lock().unwrap();
3696 let lock_wait = lock_start.elapsed();
3697 tracing::debug!(
3698 target: "feagi-bdu",
3699 "[NPU-LOCK] synaptogenesis lock wait {:.2}ms for {} -> {} (morphology={})",
3700 lock_wait.as_secs_f64() * 1000.0,
3701 src_area_id,
3702 dst_area_id,
3703 morphology_id
3704 );
3705
3706 let (weight, psp, synapse_type, delay_bursts) =
3707 self.resolve_synapse_params_for_rule(src_area_id, rule)?;
3708
3709 let synapse_attractivity = if let Some(obj) = rule.as_object() {
3711 obj.get("synapse_attractivity")
3712 .and_then(|v| v.as_u64())
3713 .unwrap_or(100) as u8
3714 } else {
3715 100 };
3717
3718 match morphology.morphology_type {
3719 feagi_evolutionary::MorphologyType::Functions => {
3720 tracing::debug!(
3721 target: "feagi-bdu",
3722 "apply_single_morphology_rule: Functions type, morphology_id={}, calling apply_function_morphology",
3723 morphology_id
3724 );
3725 self.apply_function_morphology(
3728 morphology_id,
3729 rule,
3730 npu_arc,
3731 &mut npu,
3732 src_area_id,
3733 dst_area_id,
3734 *src_idx,
3735 *dst_idx,
3736 weight,
3737 psp,
3738 synapse_attractivity,
3739 synapse_type,
3740 delay_bursts,
3741 )
3742 }
3743 feagi_evolutionary::MorphologyType::Vectors => {
3744 use crate::connectivity::synaptogenesis::apply_vectors_morphology_with_dimensions;
3745
3746 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3748 crate::types::BduError::InvalidArea(format!(
3749 "Destination area not found: {}",
3750 dst_area_id
3751 ))
3752 })?;
3753
3754 let dst_dimensions = (
3755 dst_area.dimensions.width as usize,
3756 dst_area.dimensions.height as usize,
3757 dst_area.dimensions.depth as usize,
3758 );
3759
3760 if let feagi_evolutionary::MorphologyParameters::Vectors { ref vectors } =
3761 morphology.parameters
3762 {
3763 let vectors_tuples: Vec<(i32, i32, i32)> =
3765 vectors.iter().map(|v| (v[0], v[1], v[2])).collect();
3766
3767 let count = apply_vectors_morphology_with_dimensions(
3768 &mut npu,
3769 *src_idx,
3770 *dst_idx,
3771 vectors_tuples,
3772 dst_dimensions,
3773 weight, psp, synapse_attractivity, synapse_type,
3777 delay_bursts,
3778 )?;
3779 npu.rebuild_synapse_index();
3782 Ok(count as usize)
3783 } else {
3784 Ok(0)
3785 }
3786 }
3787 feagi_evolutionary::MorphologyType::Patterns => {
3788 use crate::connectivity::core_morphologies::apply_patterns_morphology;
3789 use crate::connectivity::rules::patterns::{
3790 Pattern3D, PatternElement as RulePatternElement,
3791 };
3792 use feagi_evolutionary::PatternElement as EvoPatternElement;
3793
3794 let feagi_evolutionary::MorphologyParameters::Patterns { ref patterns } =
3795 morphology.parameters
3796 else {
3797 return Ok(0);
3798 };
3799
3800 let convert_element =
3801 |element: &EvoPatternElement|
3802 -> crate::types::BduResult<RulePatternElement> {
3803 match element {
3804 EvoPatternElement::Value(value) => {
3805 if *value < 0 {
3806 return Err(crate::types::BduError::InvalidMorphology(
3807 format!(
3808 "Pattern morphology {} contains negative voxel coordinate {}",
3809 morphology_id, value
3810 ),
3811 ));
3812 }
3813 Ok(RulePatternElement::Exact(*value))
3814 }
3815 EvoPatternElement::Wildcard => Ok(RulePatternElement::Wildcard),
3816 EvoPatternElement::Skip => Ok(RulePatternElement::Skip),
3817 EvoPatternElement::Exclude => Ok(RulePatternElement::Exclude),
3818 EvoPatternElement::DirectionPositive => {
3819 Ok(RulePatternElement::DirectionExclusive(
3820 crate::connectivity::rules::patterns::Direction::Positive,
3821 ))
3822 }
3823 EvoPatternElement::DirectionNegative => {
3824 Ok(RulePatternElement::DirectionExclusive(
3825 crate::connectivity::rules::patterns::Direction::Negative,
3826 ))
3827 }
3828 EvoPatternElement::DirectionPositiveInclusive => {
3829 Ok(RulePatternElement::DirectionInclusive(
3830 crate::connectivity::rules::patterns::Direction::Positive,
3831 ))
3832 }
3833 EvoPatternElement::DirectionNegativeInclusive => {
3834 Ok(RulePatternElement::DirectionInclusive(
3835 crate::connectivity::rules::patterns::Direction::Negative,
3836 ))
3837 }
3838 EvoPatternElement::Offset(off) => {
3839 Ok(RulePatternElement::Offset(*off))
3840 }
3841 EvoPatternElement::Range(lo, hi) => {
3842 Ok(RulePatternElement::Range(*lo, *hi))
3843 }
3844 }
3845 };
3846
3847 let mut converted_patterns = Vec::with_capacity(patterns.len());
3848 for pattern_pair in patterns {
3849 if pattern_pair.len() != 2 {
3850 return Err(crate::types::BduError::InvalidMorphology(format!(
3851 "Pattern morphology {} must contain [src, dst] pairs",
3852 morphology_id
3853 )));
3854 }
3855
3856 let src_pattern = &pattern_pair[0];
3857 let dst_pattern = &pattern_pair[1];
3858
3859 if src_pattern.len() != 3 || dst_pattern.len() != 3 {
3860 return Err(crate::types::BduError::InvalidMorphology(format!(
3861 "Pattern morphology {} requires 3-axis patterns",
3862 morphology_id
3863 )));
3864 }
3865
3866 let src: Pattern3D = (
3867 convert_element(&src_pattern[0])?,
3868 convert_element(&src_pattern[1])?,
3869 convert_element(&src_pattern[2])?,
3870 );
3871 let dst: Pattern3D = (
3872 convert_element(&dst_pattern[0])?,
3873 convert_element(&dst_pattern[1])?,
3874 convert_element(&dst_pattern[2])?,
3875 );
3876
3877 converted_patterns.push((src, dst));
3878 }
3879
3880 let count = apply_patterns_morphology(
3881 &mut npu,
3882 *src_idx,
3883 *dst_idx,
3884 converted_patterns,
3885 weight,
3886 psp,
3887 synapse_attractivity,
3888 synapse_type,
3889 delay_bursts,
3890 )?;
3891 if count > 0 {
3892 npu.rebuild_synapse_index();
3893 }
3894 Ok(count as usize)
3895 }
3896 feagi_evolutionary::MorphologyType::Composite => {
3897 let feagi_evolutionary::MorphologyParameters::Composite { .. } =
3898 morphology.parameters
3899 else {
3900 return Ok(0);
3901 };
3902
3903 if morphology_id != "tile" {
3904 use tracing::debug;
3905 debug!(
3906 target: "feagi-bdu",
3907 "Composite morphology {} not yet implemented",
3908 morphology_id
3909 );
3910 return Ok(0);
3911 }
3912
3913 let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
3914 crate::types::BduError::InvalidArea(format!(
3915 "Source area not found: {}",
3916 src_area_id
3917 ))
3918 })?;
3919 let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
3920 crate::types::BduError::InvalidArea(format!(
3921 "Destination area not found: {}",
3922 dst_area_id
3923 ))
3924 })?;
3925 let src_dimensions = (
3926 src_area.dimensions.width as usize,
3927 src_area.dimensions.height as usize,
3928 src_area.dimensions.depth as usize,
3929 );
3930 let dst_dimensions = (
3931 dst_area.dimensions.width as usize,
3932 dst_area.dimensions.height as usize,
3933 dst_area.dimensions.depth as usize,
3934 );
3935
3936 let count =
3937 crate::connectivity::core_morphologies::apply_tile_morphology_with_dimensions(
3938 &mut npu,
3939 *src_idx,
3940 *dst_idx,
3941 src_dimensions,
3942 dst_dimensions,
3943 weight,
3944 psp,
3945 synapse_attractivity,
3946 synapse_type,
3947 delay_bursts,
3948 )?;
3949 if count > 0 {
3950 npu.rebuild_synapse_index();
3951 }
3952 Ok(count as usize)
3953 }
3954 }
3955 } else {
3956 Ok(0) }
3958 }
3959
3960 pub fn set_npu(
3973 &mut self,
3974 npu: Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
3975 ) {
3976 self.npu = Some(Arc::clone(&npu));
3977 info!(target: "feagi-bdu","🔗 ConnectomeManager: NPU reference set");
3978
3979 #[cfg(not(feature = "wasm"))]
3982 {
3983 use feagi_state_manager::StateManager;
3984 let state_manager = StateManager::instance();
3985 let state_manager = state_manager.read();
3986 let core_state = state_manager.get_core_state();
3987 core_state.set_neuron_capacity(self.config.max_neurons as u32);
3989 core_state.set_synapse_capacity(self.config.max_synapses as u32);
3990 info!(
3991 target: "feagi-bdu",
3992 "📊 Updated State Manager with capacity: {} neurons, {} synapses",
3993 self.config.max_neurons, self.config.max_synapses
3994 );
3995 }
3996
3997 let existing_area_count = self.cortical_id_to_idx.len();
4004 if existing_area_count > 0 {
4005 match npu.lock() {
4006 Ok(mut npu_lock) => {
4007 for (cortical_id, cortical_idx) in self.cortical_id_to_idx.iter() {
4008 npu_lock.register_cortical_area(*cortical_idx, cortical_id.as_base_64());
4009 }
4010 info!(
4011 target: "feagi-bdu",
4012 "🔁 Backfilled {} cortical area registrations into NPU",
4013 existing_area_count
4014 );
4015 }
4016 Err(e) => {
4017 warn!(
4018 target: "feagi-bdu",
4019 "⚠️ Failed to lock NPU for cortical area backfill registration: {}",
4020 e
4021 );
4022 }
4023 }
4024 }
4025
4026 self.update_all_cached_stats();
4028 info!(target: "feagi-bdu","📊 Initialized cached stats: {} neurons, {} synapses",
4029 self.get_neuron_count(), self.get_synapse_count());
4030 }
4031
4032 pub fn has_npu(&self) -> bool {
4034 self.npu.is_some()
4035 }
4036
4037 pub fn get_npu(
4044 &self,
4045 ) -> Option<&Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>>
4046 {
4047 self.npu.as_ref()
4048 }
4049
4050 #[cfg(feature = "plasticity")]
4053 pub fn set_plasticity_executor(
4054 &mut self,
4055 executor: Arc<std::sync::Mutex<feagi_npu_plasticity::AsyncPlasticityExecutor>>,
4056 ) {
4057 if let Ok(mut exec) = executor.lock() {
4058 use feagi_npu_plasticity::executor::PlasticityExecutor;
4059 if !exec.is_running() {
4060 exec.start();
4061 }
4062 } else {
4063 warn!(target: "feagi-bdu", "⚠️ Failed to lock PlasticityExecutor for startup");
4064 }
4065 self.plasticity_executor = Some(executor);
4066 info!(target: "feagi-bdu", "🔗 ConnectomeManager: PlasticityExecutor reference set");
4067 }
4068
4069 #[cfg(feature = "plasticity")]
4071 pub fn get_plasticity_executor(
4072 &self,
4073 ) -> Option<&Arc<std::sync::Mutex<feagi_npu_plasticity::AsyncPlasticityExecutor>>> {
4074 self.plasticity_executor.as_ref()
4075 }
4076
4077 pub fn get_neuron_capacity(&self) -> usize {
4089 self.config.max_neurons
4091 }
4092
4093 pub fn get_synapse_capacity(&self) -> usize {
4105 self.config.max_synapses
4107 }
4108
4109 pub fn update_fatigue_index(&self) -> Option<u8> {
4124 let mut last_calc = match self.last_fatigue_calculation.lock() {
4126 Ok(guard) => guard,
4127 Err(_) => return None, };
4129
4130 let now = std::time::Instant::now();
4131 if now.duration_since(*last_calc).as_secs() < 2 {
4132 return None; }
4134 *last_calc = now;
4135 drop(last_calc);
4136
4137 let regular_neuron_count = self.get_neuron_count();
4139 let regular_neuron_capacity = self.get_neuron_capacity();
4140 let regular_neuron_util = if regular_neuron_capacity > 0 {
4141 ((regular_neuron_count as f64 / regular_neuron_capacity as f64) * 100.0).round() as u8
4142 } else {
4143 0
4144 };
4145
4146 let memory_neuron_util = match StateManager::instance().try_read() {
4150 Some(state_manager) => state_manager.get_core_state().get_memory_neuron_util(),
4151 None => {
4152 return None;
4154 }
4155 };
4156
4157 let synapse_count = self.get_synapse_count();
4159 let synapse_capacity = self.get_synapse_capacity();
4160 let synapse_util = if synapse_capacity > 0 {
4161 ((synapse_count as f64 / synapse_capacity as f64) * 100.0).round() as u8
4162 } else {
4163 0
4164 };
4165
4166 let fatigue_index = regular_neuron_util
4168 .max(memory_neuron_util)
4169 .max(synapse_util);
4170
4171 let current_fatigue_active = {
4173 StateManager::instance()
4175 .try_read()
4176 .map(|m| m.get_core_state().is_fatigue_active())
4177 .unwrap_or(false)
4178 };
4179
4180 let new_fatigue_active = if fatigue_index >= 85 {
4181 true
4182 } else if fatigue_index < 80 {
4183 false
4184 } else {
4185 current_fatigue_active };
4187
4188 if let Some(state_manager) = StateManager::instance().try_write() {
4192 let core_state = state_manager.get_core_state();
4193 core_state.set_fatigue_index(fatigue_index);
4194 core_state.set_fatigue_active(new_fatigue_active);
4195 core_state.set_regular_neuron_util(regular_neuron_util);
4196 core_state.set_memory_neuron_util(memory_neuron_util);
4197 core_state.set_synapse_util(synapse_util);
4198 } else {
4199 trace!(target: "feagi-bdu", "[FATIGUE] StateManager unavailable, skipping update");
4201 }
4202
4203 if let Some(ref npu) = self.npu {
4205 if let Ok(mut npu_lock) = npu.lock() {
4206 npu_lock.set_fatigue_active(new_fatigue_active);
4207 }
4208 }
4209
4210 trace!(
4211 target: "feagi-bdu",
4212 "[FATIGUE] Index={}, Active={}, Regular={}%, Memory={}%, Synapse={}%",
4213 fatigue_index, new_fatigue_active, regular_neuron_util, memory_neuron_util, synapse_util
4214 );
4215
4216 Some(fatigue_index)
4217 }
4218
4219 pub fn create_neurons_for_area(&mut self, cortical_id: &CorticalID) -> BduResult<u32> {
4236 let area = self
4238 .cortical_areas
4239 .get(cortical_id)
4240 .ok_or_else(|| {
4241 BduError::InvalidArea(format!("Cortical area {} not found", cortical_id))
4242 })?
4243 .clone();
4244
4245 let cortical_idx = self.cortical_id_to_idx.get(cortical_id).ok_or_else(|| {
4247 BduError::InvalidArea(format!("No index for cortical area {}", cortical_id))
4248 })?;
4249
4250 let npu = self
4252 .npu
4253 .as_ref()
4254 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
4255
4256 use crate::models::CorticalAreaExt;
4259 let per_voxel_cnt = area.neurons_per_voxel();
4260 let firing_threshold = area.firing_threshold();
4261 let firing_threshold_increment_x = area.firing_threshold_increment_x();
4262 let firing_threshold_increment_y = area.firing_threshold_increment_y();
4263 let firing_threshold_increment_z = area.firing_threshold_increment_z();
4264 let firing_threshold_limit_raw = area.firing_threshold_limit();
4266 let firing_threshold_limit = if firing_threshold_limit_raw == 0.0 {
4267 f32::MAX } else {
4269 firing_threshold_limit_raw
4270 };
4271
4272 if firing_threshold_increment_x != 0.0
4274 || firing_threshold_increment_y != 0.0
4275 || firing_threshold_increment_z != 0.0
4276 {
4277 info!(
4278 target: "feagi-bdu",
4279 "🔍 [DEBUG] Area {}: firing_threshold_increment = [{}, {}, {}]",
4280 cortical_id.as_base_64(),
4281 firing_threshold_increment_x,
4282 firing_threshold_increment_y,
4283 firing_threshold_increment_z
4284 );
4285 } else {
4286 if area.properties.contains_key("firing_threshold_increment_x")
4288 || area.properties.contains_key("firing_threshold_increment_y")
4289 || area.properties.contains_key("firing_threshold_increment_z")
4290 {
4291 info!(
4292 target: "feagi-bdu",
4293 "🔍 [DEBUG] Area {}: INCREMENT PROPERTIES FOUND: x={:?}, y={:?}, z={:?}",
4294 cortical_id.as_base_64(),
4295 area.properties.get("firing_threshold_increment_x"),
4296 area.properties.get("firing_threshold_increment_y"),
4297 area.properties.get("firing_threshold_increment_z")
4298 );
4299 }
4300 }
4301
4302 let leak_coefficient = area.leak_coefficient();
4303 let excitability = area.neuron_excitability();
4304 let refractory_period = area.refractory_period();
4305 let consecutive_fire_limit_raw = area.consecutive_fire_count() as u16;
4307 let consecutive_fire_limit = if consecutive_fire_limit_raw == 0 {
4308 u16::MAX } else {
4310 consecutive_fire_limit_raw
4311 };
4312 let snooze_length = area.snooze_period();
4313 let mp_charge_accumulation = area.mp_charge_accumulation();
4314
4315 let voxels = area.dimensions.width as usize
4317 * area.dimensions.height as usize
4318 * area.dimensions.depth as usize;
4319 let expected_neurons = voxels * per_voxel_cnt as usize;
4320
4321 trace!(
4322 target: "feagi-bdu",
4323 "Creating neurons for area {}: {}x{}x{} voxels × {} neurons/voxel = {} total neurons",
4324 cortical_id.as_base_64(),
4325 area.dimensions.width,
4326 area.dimensions.height,
4327 area.dimensions.depth,
4328 per_voxel_cnt,
4329 expected_neurons
4330 );
4331
4332 let neuron_count: u32 = {
4337 let mut npu_lock = npu
4338 .lock()
4339 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
4340 npu_lock
4341 .create_cortical_area_neurons(
4342 *cortical_idx,
4343 area.dimensions.width,
4344 area.dimensions.height,
4345 area.dimensions.depth,
4346 per_voxel_cnt,
4347 firing_threshold,
4348 firing_threshold_increment_x,
4349 firing_threshold_increment_y,
4350 firing_threshold_increment_z,
4351 firing_threshold_limit,
4352 leak_coefficient,
4353 0.0, 0, refractory_period,
4356 excitability,
4357 consecutive_fire_limit,
4358 snooze_length,
4359 mp_charge_accumulation,
4360 )
4361 .map_err(|e| BduError::Internal(format!("NPU neuron creation failed: {}", e)))?
4362 };
4363
4364 trace!(
4365 target: "feagi-bdu",
4366 "Created {} neurons for area {} via NPU",
4367 neuron_count,
4368 cortical_id.as_base_64()
4369 );
4370
4371 {
4374 let mut cache = self.cached_neuron_counts_per_area.write();
4375 cache
4376 .entry(*cortical_id)
4377 .or_insert_with(|| AtomicUsize::new(0))
4378 .store(neuron_count as usize, Ordering::Relaxed);
4379 }
4380
4381 let state_manager = StateManager::instance();
4383 let state_manager = state_manager.read();
4384 state_manager
4385 .set_cortical_area_neuron_count(&cortical_id.as_base_64(), neuron_count as usize);
4386
4387 self.cached_neuron_count
4389 .fetch_add(neuron_count as usize, Ordering::Relaxed);
4390
4391 let state_manager = StateManager::instance();
4393 let state_manager = state_manager.read();
4394 let core_state = state_manager.get_core_state();
4395 core_state.add_neuron_count(neuron_count);
4396 core_state.add_regular_neuron_count(neuron_count);
4397
4398 if let Some(npu) = &self.npu {
4400 if let Ok(mut npl) = npu.lock() {
4401 if let Some(v) = area.properties.get("rate_modulated_leak") {
4402 use crate::models::CorticalAreaExt;
4403 let idxs: Vec<usize> = npl
4404 .get_neurons_in_cortical_area(*cortical_idx)
4405 .into_iter()
4406 .map(|id| id as usize)
4407 .collect();
4408 npl.sync_rate_modulated_leak_from_cortical_property(
4409 *cortical_idx,
4410 v,
4411 area.leak_coefficient(),
4412 idxs,
4413 );
4414 } else {
4415 npl.remove_rate_modulated_leak(*cortical_idx);
4416 }
4417 }
4418 }
4419
4420 Ok(neuron_count)
4428 }
4429
4430 #[allow(clippy::too_many_arguments)]
4454 pub fn add_neuron(
4455 &mut self,
4456 cortical_id: &CorticalID,
4457 x: u32,
4458 y: u32,
4459 z: u32,
4460 firing_threshold: f32,
4461 firing_threshold_limit: f32,
4462 leak_coefficient: f32,
4463 resting_potential: f32,
4464 neuron_type: u8,
4465 refractory_period: u16,
4466 excitability: f32,
4467 consecutive_fire_limit: u16,
4468 snooze_length: u16,
4469 mp_charge_accumulation: bool,
4470 ) -> BduResult<u64> {
4471 if !self.cortical_areas.contains_key(cortical_id) {
4473 return Err(BduError::InvalidArea(format!(
4474 "Cortical area {} not found",
4475 cortical_id
4476 )));
4477 }
4478
4479 let cortical_idx = *self
4480 .cortical_id_to_idx
4481 .get(cortical_id)
4482 .ok_or_else(|| BduError::InvalidArea(format!("No index for {}", cortical_id)))?;
4483
4484 let npu = self
4486 .npu
4487 .as_ref()
4488 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
4489
4490 let mut npu_lock = npu
4491 .lock()
4492 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
4493
4494 let neuron_id = npu_lock
4496 .add_neuron(
4497 firing_threshold,
4498 firing_threshold_limit,
4499 leak_coefficient,
4500 resting_potential,
4501 neuron_type as i32,
4502 refractory_period,
4503 excitability,
4504 consecutive_fire_limit,
4505 snooze_length,
4506 mp_charge_accumulation,
4507 cortical_idx,
4508 x,
4509 y,
4510 z,
4511 )
4512 .map_err(|e| BduError::Internal(format!("Failed to add neuron: {}", e)))?;
4513
4514 trace!(
4515 target: "feagi-bdu",
4516 "Created neuron {} in area {} at ({}, {}, {})",
4517 neuron_id.0,
4518 cortical_id,
4519 x,
4520 y,
4521 z
4522 );
4523
4524 let state_manager = StateManager::instance();
4526 let state_manager = state_manager.read();
4527 let core_state = state_manager.get_core_state();
4528 core_state.add_neuron_count(1);
4529 core_state.add_regular_neuron_count(1);
4530 state_manager.add_cortical_area_neuron_count(&cortical_id.as_base_64(), 1);
4531
4532 Ok(neuron_id.0 as u64)
4533 }
4534
4535 pub fn delete_neuron(&mut self, neuron_id: u64) -> BduResult<bool> {
4546 let npu = self
4548 .npu
4549 .as_ref()
4550 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
4551
4552 let mut npu_lock = npu
4553 .lock()
4554 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
4555
4556 let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id as u32);
4557 let cortical_id = cortical_idx.and_then(|idx| self.cortical_idx_to_id.get(&idx).cloned());
4558
4559 let deleted = npu_lock.delete_neuron(neuron_id as u32);
4560
4561 if deleted {
4562 trace!(target: "feagi-bdu", "Deleted neuron {}", neuron_id);
4563
4564 let state_manager = StateManager::instance();
4566 let state_manager = state_manager.read();
4567 let core_state = state_manager.get_core_state();
4568 core_state.subtract_neuron_count(1);
4569 core_state.subtract_regular_neuron_count(1);
4570 if let Some(cortical_id) = cortical_id {
4571 state_manager.subtract_cortical_area_neuron_count(&cortical_id.as_base_64(), 1);
4572 }
4573
4574 }
4578
4579 Ok(deleted)
4580 }
4581
4582 pub fn apply_cortical_mapping(&mut self, src_cortical_id: &CorticalID) -> BduResult<u32> {
4596 let src_area = self
4598 .cortical_areas
4599 .get(src_cortical_id)
4600 .ok_or_else(|| {
4601 BduError::InvalidArea(format!("Source area {} not found", src_cortical_id))
4602 })?
4603 .clone();
4604
4605 let dstmap = match src_area.properties.get("cortical_mapping_dst") {
4607 Some(serde_json::Value::Object(map)) if !map.is_empty() => map,
4608 _ => return Ok(0), };
4610
4611 let src_cortical_idx = *self
4612 .cortical_id_to_idx
4613 .get(src_cortical_id)
4614 .ok_or_else(|| BduError::InvalidArea(format!("No index for {}", src_cortical_id)))?;
4615
4616 let mut total_synapses = 0u32;
4617 let mut upstream_updates: Vec<(CorticalID, u32)> = Vec::new(); for (dst_cortical_id_str, _rules) in dstmap {
4621 let dst_cortical_id = match CorticalID::try_from_base_64(dst_cortical_id_str) {
4623 Ok(id) => id,
4624 Err(_) => {
4625 warn!(target: "feagi-bdu","Invalid cortical ID format: {}, skipping", dst_cortical_id_str);
4626 continue;
4627 }
4628 };
4629
4630 if !self.cortical_id_to_idx.contains_key(&dst_cortical_id) {
4632 warn!(target: "feagi-bdu","Destination area {} not found, skipping", dst_cortical_id);
4633 continue;
4634 }
4635
4636 let synapse_count =
4638 self.apply_cortical_mapping_for_pair(src_cortical_id, &dst_cortical_id)?;
4639 total_synapses += synapse_count as u32;
4640
4641 upstream_updates.push((dst_cortical_id, src_cortical_idx));
4644 }
4645
4646 for (dst_id, src_idx) in upstream_updates {
4648 self.add_upstream_area(&dst_id, src_idx);
4649 }
4650
4651 trace!(
4652 target: "feagi-bdu",
4653 "Created {} synapses for area {} via NPU",
4654 total_synapses,
4655 src_cortical_id
4656 );
4657
4658 if total_synapses > 0 {
4661 let mut cache = self.cached_synapse_counts_per_area.write();
4662 cache
4663 .entry(*src_cortical_id)
4664 .or_insert_with(|| AtomicUsize::new(0))
4665 .fetch_add(total_synapses as usize, Ordering::Relaxed);
4666 }
4667
4668 self.cached_synapse_count
4670 .fetch_add(total_synapses as usize, Ordering::Relaxed);
4671
4672 if total_synapses > 0 {
4674 let state_manager = StateManager::instance();
4675 let state_manager = state_manager.read();
4676 let core_state = state_manager.get_core_state();
4677 core_state.add_synapse_count(total_synapses);
4678 }
4679
4680 Ok(total_synapses)
4681 }
4682
4683 pub fn has_neuron(&self, neuron_id: u64) -> bool {
4702 if let Some(ref npu) = self.npu {
4703 if let Ok(npu_lock) = npu.lock() {
4704 npu_lock.is_neuron_valid(neuron_id as u32)
4706 } else {
4707 false
4708 }
4709 } else {
4710 false
4711 }
4712 }
4713
4714 pub fn get_neuron_count(&self) -> usize {
4726 if let Some(ref npu) = self.npu {
4728 if let Ok(npu_lock) = npu.try_lock() {
4729 let fresh_count = npu_lock.get_neuron_count();
4730 self.cached_neuron_count
4731 .store(fresh_count, Ordering::Relaxed);
4732 }
4733 }
4735
4736 self.cached_neuron_count.load(Ordering::Relaxed)
4738 }
4739
4740 pub fn update_cached_neuron_count(&self) {
4746 if let Some(ref npu) = self.npu {
4747 if let Ok(npu_lock) = npu.try_lock() {
4748 let count = npu_lock.get_neuron_count();
4749 self.cached_neuron_count.store(count, Ordering::Relaxed);
4750 }
4751 }
4752 }
4753
4754 pub fn refresh_neuron_count_for_area(&self, cortical_id: &CorticalID) -> Option<usize> {
4758 let npu = self.npu.as_ref()?;
4759 let cortical_idx = *self.cortical_id_to_idx.get(cortical_id)?;
4760 let npu_lock = npu.lock().ok()?;
4761 let count = npu_lock.get_neurons_in_cortical_area(cortical_idx).len();
4762 drop(npu_lock);
4763
4764 let mut cache = self.cached_neuron_counts_per_area.write();
4765 cache
4766 .entry(*cortical_id)
4767 .or_insert_with(|| AtomicUsize::new(0))
4768 .store(count, Ordering::Relaxed);
4769
4770 let state_manager = StateManager::instance();
4772 let state_manager = state_manager.read();
4773 state_manager.set_cortical_area_neuron_count(&cortical_id.as_base_64(), count);
4774
4775 self.update_cached_neuron_count();
4776
4777 Some(count)
4778 }
4779
4780 pub fn get_synapse_count(&self) -> usize {
4792 if let Some(ref npu) = self.npu {
4794 if let Ok(npu_lock) = npu.try_lock() {
4795 let fresh_count = npu_lock.get_synapse_count();
4796 self.cached_synapse_count
4797 .store(fresh_count, Ordering::Relaxed);
4798 }
4799 }
4801
4802 self.cached_synapse_count.load(Ordering::Relaxed)
4804 }
4805
4806 pub fn update_cached_synapse_count(&self) {
4812 if let Some(ref npu) = self.npu {
4813 if let Ok(npu_lock) = npu.try_lock() {
4814 let count = npu_lock.get_synapse_count();
4815 self.cached_synapse_count.store(count, Ordering::Relaxed);
4816 }
4817 }
4818 }
4819
4820 pub fn update_all_cached_stats(&self) {
4826 self.update_cached_neuron_count();
4827 self.update_cached_synapse_count();
4828 }
4829
4830 pub fn get_neuron_coordinates(&self, neuron_id: u64) -> (u32, u32, u32) {
4841 #[cfg(feature = "plasticity")]
4846 {
4847 if feagi_npu_plasticity::NeuronIdManager::is_memory_neuron_id(neuron_id as u32) {
4848 return (0, 0, 0);
4849 }
4850 }
4851 if let Some(ref npu) = self.npu {
4852 if let Ok(npu_lock) = npu.lock() {
4853 npu_lock
4854 .get_neuron_coordinates(neuron_id as u32)
4855 .unwrap_or((0, 0, 0))
4856 } else {
4857 (0, 0, 0)
4858 }
4859 } else {
4860 (0, 0, 0)
4861 }
4862 }
4863
4864 pub fn get_neuron_cortical_idx(&self, neuron_id: u64) -> u32 {
4875 self.get_neuron_cortical_idx_opt(neuron_id).unwrap_or(0)
4876 }
4877
4878 pub fn get_neuron_cortical_idx_opt(&self, neuron_id: u64) -> Option<u32> {
4884 #[cfg(feature = "plasticity")]
4885 {
4886 if feagi_npu_plasticity::NeuronIdManager::is_memory_neuron_id(neuron_id as u32) {
4887 return self.memory_neuron_cortical_idx_opt(neuron_id as u32);
4888 }
4889 }
4890 if let Some(ref npu) = self.npu {
4891 if let Ok(npu_lock) = npu.lock() {
4892 npu_lock.get_neuron_cortical_area(neuron_id as u32)
4893 } else {
4894 None
4895 }
4896 } else {
4897 None
4898 }
4899 }
4900
4901 #[cfg(feature = "plasticity")]
4903 fn memory_neuron_cortical_idx_opt(&self, neuron_id: u32) -> Option<u32> {
4904 let exec = self.get_plasticity_executor()?;
4905 let guard = exec.lock().ok()?;
4906 guard
4907 .memory_neuron_detail(neuron_id)
4908 .map(|d| d.cortical_area_idx)
4909 }
4910
4911 pub fn get_neurons_in_area(&self, cortical_id: &CorticalID) -> Vec<u64> {
4922 let cortical_idx = match self.cortical_id_to_idx.get(cortical_id) {
4924 Some(idx) => *idx,
4925 None => return Vec::new(),
4926 };
4927
4928 if let Some(ref npu) = self.npu {
4929 if let Ok(npu_lock) = npu.lock() {
4930 npu_lock
4932 .get_neurons_in_cortical_area(cortical_idx)
4933 .into_iter()
4934 .map(|id| id as u64)
4935 .collect()
4936 } else {
4937 Vec::new()
4938 }
4939 } else {
4940 Vec::new()
4941 }
4942 }
4943
4944 pub fn get_outgoing_synapses(&self, source_neuron_id: u64) -> Vec<(u32, f32, f32, u8)> {
4955 if let Some(ref npu) = self.npu {
4956 if let Ok(npu_lock) = npu.lock() {
4957 npu_lock.get_outgoing_synapses(source_neuron_id as u32)
4958 } else {
4959 Vec::new()
4960 }
4961 } else {
4962 Vec::new()
4963 }
4964 }
4965
4966 pub fn get_incoming_synapses(&self, target_neuron_id: u64) -> Vec<(u32, f32, f32, u8)> {
4977 if let Some(ref npu) = self.npu {
4978 if let Ok(npu_lock) = npu.lock() {
4979 npu_lock.get_incoming_synapses(target_neuron_id as u32)
4980 } else {
4981 Vec::new()
4982 }
4983 } else {
4984 Vec::new()
4985 }
4986 }
4987
4988 pub fn get_neuron_count_in_area(&self, cortical_id: &CorticalID) -> usize {
5014 let cache = self.cached_neuron_counts_per_area.read();
5016 let base_count = cache
5017 .get(cortical_id)
5018 .map(|count| count.load(Ordering::Relaxed))
5019 .unwrap_or(0);
5020
5021 let memory_count = self
5023 .cortical_areas
5024 .get(cortical_id)
5025 .and_then(|area| feagi_evolutionary::extract_memory_properties(&area.properties))
5026 .and_then(|_| {
5027 StateManager::instance()
5028 .try_read()
5029 .and_then(|state_manager| {
5030 state_manager.get_cortical_area_stats(&cortical_id.as_base_64())
5031 })
5032 })
5033 .map(|stats| stats.neuron_count)
5034 .unwrap_or(0);
5035
5036 base_count.saturating_add(memory_count)
5037 }
5038
5039 pub fn get_populated_areas(&self) -> Vec<(String, usize)> {
5046 let mut result = Vec::new();
5047
5048 for cortical_id in self.cortical_areas.keys() {
5049 let count = self.get_neuron_count_in_area(cortical_id);
5050 if count > 0 {
5051 result.push((cortical_id.to_string(), count));
5052 }
5053 }
5054
5055 result
5056 }
5057
5058 pub fn is_area_populated(&self, cortical_id: &CorticalID) -> bool {
5069 self.get_neuron_count_in_area(cortical_id) > 0
5070 }
5071
5072 pub fn get_synapse_count_in_area(&self, cortical_id: &CorticalID) -> usize {
5088 let cache = self.cached_synapse_counts_per_area.read();
5090 cache
5091 .get(cortical_id)
5092 .map(|count| count.load(Ordering::Relaxed))
5093 .unwrap_or(0)
5094 }
5095
5096 pub fn get_incoming_synapse_count_in_area(&self, cortical_id: &CorticalID) -> usize {
5106 if !self.cortical_id_to_idx.contains_key(cortical_id) {
5107 return 0;
5108 }
5109
5110 if let Some(state_manager) = StateManager::instance().try_read() {
5111 if let Some(stats) = state_manager.get_cortical_area_stats(&cortical_id.as_base_64()) {
5112 return stats.incoming_synapse_count;
5113 }
5114 }
5115
5116 0
5117 }
5118
5119 pub fn get_outgoing_synapse_count_in_area(&self, cortical_id: &CorticalID) -> usize {
5129 if !self.cortical_id_to_idx.contains_key(cortical_id) {
5130 return 0;
5131 }
5132
5133 if let Some(state_manager) = StateManager::instance().try_read() {
5134 if let Some(stats) = state_manager.get_cortical_area_stats(&cortical_id.as_base_64()) {
5135 return stats.outgoing_synapse_count;
5136 }
5137 }
5138
5139 0
5140 }
5141
5142 pub fn are_neurons_connected(&self, source_neuron_id: u64, target_neuron_id: u64) -> bool {
5154 let synapses = self.get_outgoing_synapses(source_neuron_id);
5155 synapses
5156 .iter()
5157 .any(|(target, _, _, _)| *target == target_neuron_id as u32)
5158 }
5159
5160 pub fn get_connection_weight(
5172 &self,
5173 source_neuron_id: u64,
5174 target_neuron_id: u64,
5175 ) -> Option<f32> {
5176 let synapses = self.get_outgoing_synapses(source_neuron_id);
5177 synapses
5178 .iter()
5179 .find(|(target, _, _, _)| *target == target_neuron_id as u32)
5180 .map(|(_, weight, _, _)| *weight)
5181 }
5182
5183 pub fn get_area_connectivity_stats(&self, cortical_id: &CorticalID) -> (usize, usize, f32) {
5194 let neurons = self.get_neurons_in_area(cortical_id);
5195 let neuron_count = neurons.len();
5196
5197 if neuron_count == 0 {
5198 return (0, 0, 0.0);
5199 }
5200
5201 let mut total_synapses = 0;
5202 for neuron_id in neurons {
5203 total_synapses += self.get_outgoing_synapses(neuron_id).len();
5204 }
5205
5206 let avg_synapses = total_synapses as f32 / neuron_count as f32;
5207
5208 (neuron_count, total_synapses, avg_synapses)
5209 }
5210
5211 pub fn get_neuron_cortical_id(&self, neuron_id: u64) -> Option<CorticalID> {
5222 let cortical_idx = self.get_neuron_cortical_idx_opt(neuron_id)?;
5223 self.cortical_idx_to_id.get(&cortical_idx).copied()
5224 }
5225
5226 pub fn get_neuron_density(&self, cortical_id: &CorticalID) -> f32 {
5237 let area = match self.cortical_areas.get(cortical_id) {
5238 Some(a) => a,
5239 None => return 0.0,
5240 };
5241
5242 let neuron_count = self.get_neuron_count_in_area(cortical_id);
5243 let volume = area.dimensions.width * area.dimensions.height * area.dimensions.depth;
5244
5245 if volume == 0 {
5246 return 0.0;
5247 }
5248
5249 neuron_count as f32 / volume as f32
5250 }
5251
5252 pub fn get_all_area_stats(&self) -> Vec<(String, usize, usize, f32)> {
5259 let mut stats = Vec::new();
5260
5261 for cortical_id in self.cortical_areas.keys() {
5262 let neuron_count = self.get_neuron_count_in_area(cortical_id);
5263 let synapse_count = self.get_synapse_count_in_area(cortical_id);
5264 let density = self.get_neuron_density(cortical_id);
5265
5266 stats.push((
5267 cortical_id.to_string(),
5268 neuron_count,
5269 synapse_count,
5270 density,
5271 ));
5272 }
5273
5274 stats
5275 }
5276
5277 pub fn get_config(&self) -> &ConnectomeConfig {
5283 &self.config
5284 }
5285
5286 pub fn set_config(&mut self, config: ConnectomeConfig) {
5288 self.config = config;
5289 }
5290
5291 pub fn ensure_core_cortical_areas(&mut self) -> BduResult<()> {
5314 info!(target: "feagi-bdu", "🔧 [CORE-AREA] Ensuring core cortical areas exist...");
5315
5316 use feagi_structures::genomic::cortical_area::{
5317 CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalAreaType,
5318 };
5319
5320 let core_dimensions = CorticalAreaDimensions::new(1, 1, 1).map_err(|e| {
5322 BduError::Internal(format!("Failed to create core area dimensions: {}", e))
5323 })?;
5324
5325 let core_position = (0, 0, 0).into();
5327
5328 let death_id = CoreCorticalType::Death.to_cortical_id();
5330 if !self.cortical_areas.contains_key(&death_id) {
5331 info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _death area (cortical_idx=0)");
5332 let death_area = CorticalArea::new(
5333 death_id,
5334 0, "_death".to_string(),
5336 core_dimensions,
5337 core_position,
5338 CorticalAreaType::Core(CoreCorticalType::Death),
5339 )
5340 .map_err(|e| BduError::Internal(format!("Failed to create _death area: {}", e)))?;
5341 match self.add_cortical_area(death_area) {
5342 Ok(idx) => {
5343 info!(target: "feagi-bdu", " ✅ Created _death area with cortical_idx={}", idx);
5344 }
5345 Err(e) => {
5346 error!(target: "feagi-bdu", " ❌ Failed to add _death area: {}", e);
5347 return Err(e);
5348 }
5349 }
5350 } else {
5351 info!(target: "feagi-bdu", " ✓ _death area already exists");
5352 }
5353
5354 let power_id = CoreCorticalType::Power.to_cortical_id();
5356 if !self.cortical_areas.contains_key(&power_id) {
5357 info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _power area (cortical_idx=1)");
5358 let power_area = CorticalArea::new(
5359 power_id,
5360 1, "_power".to_string(),
5362 core_dimensions,
5363 core_position,
5364 CorticalAreaType::Core(CoreCorticalType::Power),
5365 )
5366 .map_err(|e| BduError::Internal(format!("Failed to create _power area: {}", e)))?;
5367 match self.add_cortical_area(power_area) {
5368 Ok(idx) => {
5369 info!(target: "feagi-bdu", " ✅ Created _power area with cortical_idx={}", idx);
5370 }
5371 Err(e) => {
5372 error!(target: "feagi-bdu", " ❌ Failed to add _power area: {}", e);
5373 return Err(e);
5374 }
5375 }
5376 } else {
5377 info!(target: "feagi-bdu", " ✓ _power area already exists");
5378 }
5379
5380 let fatigue_id = CoreCorticalType::Fatigue.to_cortical_id();
5382 let pain_id = CoreCorticalType::Pain.to_cortical_id();
5383 let pleasure_id = CoreCorticalType::Pleasure.to_cortical_id();
5384 let fear_id = CoreCorticalType::Fear.to_cortical_id();
5385 let hope_id = CoreCorticalType::Hope.to_cortical_id();
5386 if !self.cortical_areas.contains_key(&fatigue_id) {
5387 info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _fatigue area (cortical_idx=2)");
5388 let fatigue_area = CorticalArea::new(
5389 fatigue_id,
5390 2, "_fatigue".to_string(),
5392 core_dimensions,
5393 core_position,
5394 CorticalAreaType::Core(CoreCorticalType::Fatigue),
5395 )
5396 .map_err(|e| BduError::Internal(format!("Failed to create _fatigue area: {}", e)))?;
5397 match self.add_cortical_area(fatigue_area) {
5398 Ok(idx) => {
5399 info!(target: "feagi-bdu", " ✅ Created _fatigue area with cortical_idx={}", idx);
5400 }
5401 Err(e) => {
5402 error!(target: "feagi-bdu", " ❌ Failed to add _fatigue area: {}", e);
5403 return Err(e);
5404 }
5405 }
5406 } else {
5407 info!(target: "feagi-bdu", " ✓ _fatigue area already exists");
5408 }
5409
5410 if !self.cortical_areas.contains_key(&pain_id) {
5412 info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _pain area (cortical_idx=3)");
5413 let pain_area = CorticalArea::new(
5414 pain_id,
5415 3, "_pain".to_string(),
5417 core_dimensions,
5418 core_position,
5419 CorticalAreaType::Core(CoreCorticalType::Pain),
5420 )
5421 .map_err(|e| BduError::Internal(format!("Failed to create _pain area: {}", e)))?;
5422 match self.add_cortical_area(pain_area) {
5423 Ok(idx) => {
5424 info!(target: "feagi-bdu", " ✅ Created _pain area with cortical_idx={}", idx);
5425 }
5426 Err(e) => {
5427 error!(target: "feagi-bdu", " ❌ Failed to add _pain area: {}", e);
5428 return Err(e);
5429 }
5430 }
5431 } else {
5432 info!(target: "feagi-bdu", " ✓ _pain area already exists");
5433 }
5434
5435 if !self.cortical_areas.contains_key(&pleasure_id) {
5437 info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _pleasure area (cortical_idx=4)");
5438 let pleasure_area = CorticalArea::new(
5439 pleasure_id,
5440 4, "_pleasure".to_string(),
5442 core_dimensions,
5443 core_position,
5444 CorticalAreaType::Core(CoreCorticalType::Pleasure),
5445 )
5446 .map_err(|e| BduError::Internal(format!("Failed to create _pleasure area: {}", e)))?;
5447 match self.add_cortical_area(pleasure_area) {
5448 Ok(idx) => {
5449 info!(target: "feagi-bdu", " ✅ Created _pleasure area with cortical_idx={}", idx);
5450 }
5451 Err(e) => {
5452 error!(target: "feagi-bdu", " ❌ Failed to add _pleasure area: {}", e);
5453 return Err(e);
5454 }
5455 }
5456 } else {
5457 info!(target: "feagi-bdu", " ✓ _pleasure area already exists");
5458 }
5459
5460 if !self.cortical_areas.contains_key(&fear_id) {
5462 info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _fear area (cortical_idx=5)");
5463 let fear_area = CorticalArea::new(
5464 fear_id,
5465 5, "_fear".to_string(),
5467 core_dimensions,
5468 core_position,
5469 CorticalAreaType::Core(CoreCorticalType::Fear),
5470 )
5471 .map_err(|e| BduError::Internal(format!("Failed to create _fear area: {}", e)))?;
5472 match self.add_cortical_area(fear_area) {
5473 Ok(idx) => {
5474 info!(target: "feagi-bdu", " ✅ Created _fear area with cortical_idx={}", idx);
5475 }
5476 Err(e) => {
5477 error!(target: "feagi-bdu", " ❌ Failed to add _fear area: {}", e);
5478 return Err(e);
5479 }
5480 }
5481 } else {
5482 info!(target: "feagi-bdu", " ✓ _fear area already exists");
5483 }
5484
5485 if !self.cortical_areas.contains_key(&hope_id) {
5487 info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _hope area (cortical_idx=6)");
5488 let hope_area = CorticalArea::new(
5489 hope_id,
5490 6, "_hope".to_string(),
5492 core_dimensions,
5493 core_position,
5494 CorticalAreaType::Core(CoreCorticalType::Hope),
5495 )
5496 .map_err(|e| BduError::Internal(format!("Failed to create _hope area: {}", e)))?;
5497 match self.add_cortical_area(hope_area) {
5498 Ok(idx) => {
5499 info!(target: "feagi-bdu", " ✅ Created _hope area with cortical_idx={}", idx);
5500 }
5501 Err(e) => {
5502 error!(target: "feagi-bdu", " ❌ Failed to add _hope area: {}", e);
5503 return Err(e);
5504 }
5505 }
5506 } else {
5507 info!(target: "feagi-bdu", " ✓ _hope area already exists");
5508 }
5509
5510 info!(target: "feagi-bdu", "🔧 [CORE-AREA] Core area check complete");
5511 Ok(())
5512 }
5513
5514 #[deprecated(
5531 note = "Use GenomeService::save_genome() instead. This produces incomplete v2.1 format without morphologies/physiology."
5532 )]
5533 #[allow(deprecated)]
5534 pub fn save_genome_to_json(
5535 &self,
5536 genome_id: Option<String>,
5537 genome_title: Option<String>,
5538 ) -> BduResult<String> {
5539 let mut brain_regions_with_parents = std::collections::HashMap::new();
5541
5542 for region_id in self.brain_regions.get_all_region_ids() {
5543 if let Some(region) = self.brain_regions.get_region(region_id) {
5544 let parent_id = self
5545 .brain_regions
5546 .get_parent(region_id)
5547 .map(|s| s.to_string());
5548 brain_regions_with_parents
5549 .insert(region_id.to_string(), (region.clone(), parent_id));
5550 }
5551 }
5552
5553 Ok(feagi_evolutionary::GenomeSaver::save_to_json(
5555 &self.cortical_areas,
5556 &brain_regions_with_parents,
5557 genome_id,
5558 genome_title,
5559 )?)
5560 }
5561
5562 pub fn prepare_for_new_genome(&mut self) -> BduResult<()> {
5593 info!(target: "feagi-bdu","Preparing for new genome (clearing existing state)");
5594
5595 self.cortical_areas.clear();
5597 self.cortical_id_to_idx.clear();
5598 self.cortical_idx_to_id.clear();
5599 self.next_cortical_idx = 7;
5601 info!("🔧 [BRAIN-RESET] Cortical mapping cleared, next_cortical_idx reset to 7 (reserves 0=_death, 1=_power, 2=_fatigue, 3=_pain, 4=_pleasure, 5=_fear, 6=_hope)");
5602
5603 self.brain_regions = BrainRegionHierarchy::new();
5605
5606 if let Some(ref npu) = self.npu {
5608 let mut npu_lock = npu
5609 .lock()
5610 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5611 npu_lock
5612 .reset_for_new_genome()
5613 .map_err(|e| BduError::Internal(format!("Failed to reset NPU: {}", e)))?;
5614 }
5615
5616 info!(target: "feagi-bdu","✅ Connectome cleared and ready for new genome");
5617 Ok(())
5618 }
5619
5620 pub fn resize_for_genome(
5630 &mut self,
5631 genome: &feagi_evolutionary::RuntimeGenome,
5632 ) -> BduResult<()> {
5633 self.morphology_registry = genome.morphologies.clone();
5635 info!(target: "feagi-bdu", "Stored {} morphologies from genome", self.morphology_registry.count());
5636
5637 let required_neurons = genome.stats.innate_neuron_count;
5639 let required_synapses = genome.stats.innate_synapse_count;
5640
5641 info!(target: "feagi-bdu",
5642 "Genome requires: {} neurons, {} synapses",
5643 required_neurons,
5644 required_synapses
5645 );
5646
5647 let mut total_voxels = 0;
5649 for area in genome.cortical_areas.values() {
5650 total_voxels += area.dimensions.width * area.dimensions.height * area.dimensions.depth;
5651 }
5652
5653 info!(target: "feagi-bdu",
5654 "Genome has {} cortical areas with {} total voxels",
5655 genome.cortical_areas.len(),
5656 total_voxels
5657 );
5658
5659 Ok(())
5664 }
5665
5666 pub fn create_synapse(
5685 &mut self,
5686 source_neuron_id: u64,
5687 target_neuron_id: u64,
5688 weight: f32,
5689 psp: f32,
5690 synapse_type: u8,
5691 ) -> BduResult<()> {
5692 let npu = self
5694 .npu
5695 .as_ref()
5696 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5697
5698 let mut npu_lock = npu
5699 .lock()
5700 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5701
5702 let source_exists = (source_neuron_id as u32) < npu_lock.get_neuron_count() as u32;
5704 let target_exists = (target_neuron_id as u32) < npu_lock.get_neuron_count() as u32;
5705
5706 if !source_exists {
5707 return Err(BduError::InvalidNeuron(format!(
5708 "Source neuron {} not found",
5709 source_neuron_id
5710 )));
5711 }
5712 if !target_exists {
5713 return Err(BduError::InvalidNeuron(format!(
5714 "Target neuron {} not found",
5715 target_neuron_id
5716 )));
5717 }
5718
5719 let syn_type = if synapse_type == 0 {
5721 feagi_npu_neural::synapse::SynapseType::Excitatory
5722 } else {
5723 feagi_npu_neural::synapse::SynapseType::Inhibitory
5724 };
5725
5726 let synapse_idx = npu_lock
5727 .add_synapse(
5728 NeuronId(source_neuron_id as u32),
5729 NeuronId(target_neuron_id as u32),
5730 feagi_npu_neural::types::SynapticWeight(weight),
5731 feagi_npu_neural::types::SynapticPsp(psp),
5732 syn_type,
5733 0,
5734 1,
5735 )
5736 .map_err(|e| BduError::Internal(format!("Failed to create synapse: {}", e)))?;
5737
5738 debug!(target: "feagi-bdu", "Created synapse: {} -> {} (weight: {}, psp: {}, type: {}, idx: {})",
5739 source_neuron_id, target_neuron_id, weight, psp, synapse_type, synapse_idx);
5740
5741 let source_cortical_idx = npu_lock.get_neuron_cortical_area(source_neuron_id as u32);
5742 let target_cortical_idx = npu_lock.get_neuron_cortical_area(target_neuron_id as u32);
5743 let source_cortical_id =
5744 source_cortical_idx.and_then(|idx| self.cortical_idx_to_id.get(&idx).cloned());
5745 let target_cortical_id =
5746 target_cortical_idx.and_then(|idx| self.cortical_idx_to_id.get(&idx).cloned());
5747
5748 let state_manager = StateManager::instance();
5749 let state_manager = state_manager.read();
5750 let core_state = state_manager.get_core_state();
5751 core_state.add_synapse_count(1);
5752 if let Some(cortical_id) = source_cortical_id {
5753 state_manager.add_cortical_area_outgoing_synapses(&cortical_id.as_base_64(), 1);
5754 }
5755 if let Some(cortical_id) = target_cortical_id {
5756 state_manager.add_cortical_area_incoming_synapses(&cortical_id.as_base_64(), 1);
5757 }
5758
5759 Ok(())
5764 }
5765
5766 fn sync_cortical_area_flags_to_npu(&mut self) -> BduResult<()> {
5769 if let Some(ref npu) = self.npu {
5770 if let Ok(mut npu_lock) = npu.lock() {
5771 let mut psp_uniform_flags = ahash::AHashMap::new();
5773 let mut mp_driven_psp_flags = ahash::AHashMap::new();
5774 let mut postsynaptic_current_flags = ahash::AHashMap::new();
5775 let mut degeneration_flags = ahash::AHashMap::new();
5776
5777 for (cortical_id, area) in &self.cortical_areas {
5778 let default_psp_uniform = *cortical_id
5781 == CoreCorticalType::Power.to_cortical_id()
5782 || matches!(area.cortical_type, CorticalAreaType::Memory(_));
5783 let psp_uniform = area
5784 .get_property("psp_uniform_distribution")
5785 .and_then(|v| v.as_bool())
5786 .unwrap_or(default_psp_uniform);
5787 psp_uniform_flags.insert(*cortical_id, psp_uniform);
5788
5789 let mp_driven_psp = area
5791 .get_property("mp_driven_psp")
5792 .and_then(|v| v.as_bool())
5793 .unwrap_or(false);
5794 mp_driven_psp_flags.insert(*cortical_id, mp_driven_psp);
5795
5796 let postsynaptic_current = area
5798 .get_property("postsynaptic_current")
5799 .and_then(|v| v.as_f64())
5800 .unwrap_or(1.0) as f32;
5801 postsynaptic_current_flags.insert(*cortical_id, postsynaptic_current);
5802
5803 let degeneration = area
5805 .get_property("degeneration")
5806 .and_then(|v| v.as_f64())
5807 .unwrap_or(0.0) as f32;
5808 if degeneration > 0.0 {
5809 degeneration_flags.insert(*cortical_id, degeneration);
5810 }
5811 }
5812
5813 npu_lock.set_psp_uniform_distribution_flags(psp_uniform_flags);
5815 npu_lock.set_mp_driven_psp_flags(mp_driven_psp_flags);
5816 npu_lock.set_postsynaptic_current_flags(postsynaptic_current_flags);
5817 npu_lock.set_degeneration_flags(degeneration_flags);
5818
5819 trace!(
5820 target: "feagi-bdu",
5821 "Synchronized cortical area flags to NPU ({} areas)",
5822 self.cortical_areas.len()
5823 );
5824 }
5825 }
5826
5827 Ok(())
5828 }
5829
5830 pub fn get_synapse(
5842 &self,
5843 source_neuron_id: u64,
5844 target_neuron_id: u64,
5845 ) -> Option<(f32, f32, u8)> {
5846 let npu = self.npu.as_ref()?;
5848 let npu_lock = npu.lock().ok()?;
5849
5850 let incoming = npu_lock.get_incoming_synapses(target_neuron_id as u32);
5853
5854 for (source_id, weight, psp, synapse_type) in incoming {
5856 if source_id == source_neuron_id as u32 {
5857 return Some((weight, psp, synapse_type));
5858 }
5859 }
5860
5861 None
5862 }
5863
5864 pub fn update_synapse_weight(
5877 &mut self,
5878 source_neuron_id: u64,
5879 target_neuron_id: u64,
5880 new_weight: f32,
5881 ) -> BduResult<()> {
5882 let npu = self
5884 .npu
5885 .as_ref()
5886 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5887
5888 let mut npu_lock = npu
5889 .lock()
5890 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5891
5892 let updated = npu_lock.update_synapse_weight(
5894 NeuronId(source_neuron_id as u32),
5895 NeuronId(target_neuron_id as u32),
5896 feagi_npu_neural::types::SynapticWeight(new_weight),
5897 );
5898
5899 if updated {
5900 debug!(target: "feagi-bdu","Updated synapse weight: {} -> {} = {}", source_neuron_id, target_neuron_id, new_weight);
5901 Ok(())
5902 } else {
5903 Err(BduError::InvalidSynapse(format!(
5904 "Synapse {} -> {} not found",
5905 source_neuron_id, target_neuron_id
5906 )))
5907 }
5908 }
5909
5910 pub fn remove_synapse(
5922 &mut self,
5923 source_neuron_id: u64,
5924 target_neuron_id: u64,
5925 ) -> BduResult<bool> {
5926 let npu = self
5928 .npu
5929 .as_ref()
5930 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5931
5932 let mut npu_lock = npu
5933 .lock()
5934 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5935
5936 let source_cortical_idx = npu_lock.get_neuron_cortical_area(source_neuron_id as u32);
5937 let target_cortical_idx = npu_lock.get_neuron_cortical_area(target_neuron_id as u32);
5938 let source_cortical_id =
5939 source_cortical_idx.and_then(|idx| self.cortical_idx_to_id.get(&idx).cloned());
5940 let target_cortical_id =
5941 target_cortical_idx.and_then(|idx| self.cortical_idx_to_id.get(&idx).cloned());
5942
5943 let removed = npu_lock.remove_synapse(
5945 NeuronId(source_neuron_id as u32),
5946 NeuronId(target_neuron_id as u32),
5947 );
5948
5949 if removed {
5950 debug!(target: "feagi-bdu","Removed synapse: {} -> {}", source_neuron_id, target_neuron_id);
5951
5952 let state_manager = StateManager::instance();
5954 let state_manager = state_manager.read();
5955 let core_state = state_manager.get_core_state();
5956 core_state.subtract_synapse_count(1);
5957 if let Some(cortical_id) = source_cortical_id {
5958 state_manager
5959 .subtract_cortical_area_outgoing_synapses(&cortical_id.as_base_64(), 1);
5960 }
5961 if let Some(cortical_id) = target_cortical_id {
5962 state_manager
5963 .subtract_cortical_area_incoming_synapses(&cortical_id.as_base_64(), 1);
5964 }
5965 }
5966
5967 Ok(removed)
5968 }
5969
5970 pub fn batch_create_neurons(
5988 &mut self,
5989 cortical_id: &CorticalID,
5990 neurons: Vec<NeuronData>,
5991 ) -> BduResult<Vec<u64>> {
5992 let npu = self
5994 .npu
5995 .as_ref()
5996 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5997
5998 let mut npu_lock = npu
5999 .lock()
6000 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
6001
6002 let area = self.get_cortical_area(cortical_id).ok_or_else(|| {
6004 BduError::InvalidArea(format!("Cortical area {} not found", cortical_id))
6005 })?;
6006 let cortical_idx = area.cortical_idx;
6007
6008 let count = neurons.len();
6009
6010 let mut x_coords = Vec::with_capacity(count);
6012 let mut y_coords = Vec::with_capacity(count);
6013 let mut z_coords = Vec::with_capacity(count);
6014 let mut firing_thresholds = Vec::with_capacity(count);
6015 let mut threshold_limits = Vec::with_capacity(count);
6016 let mut leak_coeffs = Vec::with_capacity(count);
6017 let mut resting_potentials = Vec::with_capacity(count);
6018 let mut neuron_types = Vec::with_capacity(count);
6019 let mut refractory_periods = Vec::with_capacity(count);
6020 let mut excitabilities = Vec::with_capacity(count);
6021 let mut consec_fire_limits = Vec::with_capacity(count);
6022 let mut snooze_lengths = Vec::with_capacity(count);
6023 let mut mp_accums = Vec::with_capacity(count);
6024 let mut cortical_areas = Vec::with_capacity(count);
6025
6026 for (
6027 x,
6028 y,
6029 z,
6030 threshold,
6031 threshold_limit,
6032 leak,
6033 resting,
6034 ntype,
6035 refract,
6036 excit,
6037 consec_limit,
6038 snooze,
6039 mp_accum,
6040 ) in neurons
6041 {
6042 x_coords.push(x);
6043 y_coords.push(y);
6044 z_coords.push(z);
6045 firing_thresholds.push(threshold);
6046 threshold_limits.push(threshold_limit);
6047 leak_coeffs.push(leak);
6048 resting_potentials.push(resting);
6049 neuron_types.push(ntype);
6050 refractory_periods.push(refract);
6051 excitabilities.push(excit);
6052 consec_fire_limits.push(consec_limit);
6053 snooze_lengths.push(snooze);
6054 mp_accums.push(mp_accum);
6055 cortical_areas.push(cortical_idx);
6056 }
6057
6058 let first_neuron_id = npu_lock.get_neuron_count() as u32;
6060
6061 let firing_thresholds_t = firing_thresholds;
6066 let threshold_limits_t = threshold_limits;
6067 let resting_potentials_t = resting_potentials;
6068 let (neurons_created, _indices) = npu_lock.add_neurons_batch(
6069 firing_thresholds_t,
6070 threshold_limits_t,
6071 leak_coeffs,
6072 resting_potentials_t,
6073 neuron_types,
6074 refractory_periods,
6075 excitabilities,
6076 consec_fire_limits,
6077 snooze_lengths,
6078 mp_accums,
6079 cortical_areas,
6080 x_coords,
6081 y_coords,
6082 z_coords,
6083 );
6084
6085 let mut neuron_ids = Vec::with_capacity(count);
6087 for i in 0..neurons_created {
6088 neuron_ids.push((first_neuron_id + i) as u64);
6089 }
6090
6091 info!(target: "feagi-bdu","Batch created {} neurons in cortical area {}", count, cortical_id);
6092
6093 let state_manager = StateManager::instance();
6095 let state_manager = state_manager.read();
6096 let core_state = state_manager.get_core_state();
6097 core_state.add_neuron_count(neurons_created);
6098 core_state.add_regular_neuron_count(neurons_created);
6099 state_manager.add_cortical_area_neuron_count(&cortical_id.as_base_64(), count);
6100
6101 {
6103 let mut cache = self.cached_neuron_counts_per_area.write();
6104 cache
6105 .entry(*cortical_id)
6106 .or_insert_with(|| AtomicUsize::new(0))
6107 .fetch_add(count, Ordering::Relaxed);
6108 }
6109
6110 Ok(neuron_ids)
6111 }
6112
6113 pub fn delete_neurons_batch(&mut self, neuron_ids: Vec<u64>) -> BduResult<usize> {
6124 let npu = self
6126 .npu
6127 .as_ref()
6128 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
6129
6130 let mut npu_lock = npu
6131 .lock()
6132 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
6133
6134 let mut deleted_count = 0;
6135 let mut per_area_deleted: std::collections::HashMap<String, usize> =
6136 std::collections::HashMap::new();
6137
6138 for neuron_id in neuron_ids {
6141 let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id as u32);
6142 let cortical_id =
6143 cortical_idx.and_then(|idx| self.cortical_idx_to_id.get(&idx).cloned());
6144
6145 if npu_lock.delete_neuron(neuron_id as u32) {
6146 deleted_count += 1;
6147 if let Some(cortical_id) = cortical_id {
6148 let key = cortical_id.as_base_64();
6149 *per_area_deleted.entry(key).or_insert(0) += 1;
6150 }
6151 }
6152 }
6153
6154 info!(target: "feagi-bdu","Batch deleted {} neurons", deleted_count);
6155
6156 if deleted_count > 0 {
6158 let state_manager = StateManager::instance();
6159 let state_manager = state_manager.read();
6160 let core_state = state_manager.get_core_state();
6161 core_state.subtract_neuron_count(deleted_count as u32);
6162 core_state.subtract_regular_neuron_count(deleted_count as u32);
6163 for (cortical_id, count) in per_area_deleted {
6164 state_manager.subtract_cortical_area_neuron_count(&cortical_id, count);
6165 }
6166 }
6167
6168 Ok(deleted_count)
6175 }
6176
6177 pub fn update_neuron_properties(
6196 &mut self,
6197 neuron_id: u64,
6198 firing_threshold: Option<f32>,
6199 leak_coefficient: Option<f32>,
6200 resting_potential: Option<f32>,
6201 excitability: Option<f32>,
6202 ) -> BduResult<()> {
6203 let npu = self
6205 .npu
6206 .as_ref()
6207 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
6208
6209 let mut npu_lock = npu
6210 .lock()
6211 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
6212
6213 let neuron_id_u32 = neuron_id as u32;
6214
6215 let mut updated = false;
6217
6218 if let Some(threshold) = firing_threshold {
6220 if npu_lock.update_neuron_threshold(neuron_id_u32, threshold) {
6221 updated = true;
6222 debug!(target: "feagi-bdu","Updated neuron {} firing_threshold = {}", neuron_id, threshold);
6223 } else if !updated {
6224 return Err(BduError::InvalidNeuron(format!(
6225 "Neuron {} not found",
6226 neuron_id
6227 )));
6228 }
6229 }
6230
6231 if let Some(leak) = leak_coefficient {
6232 if npu_lock.update_neuron_leak(neuron_id_u32, leak) {
6233 updated = true;
6234 debug!(target: "feagi-bdu","Updated neuron {} leak_coefficient = {}", neuron_id, leak);
6235 } else if !updated {
6236 return Err(BduError::InvalidNeuron(format!(
6237 "Neuron {} not found",
6238 neuron_id
6239 )));
6240 }
6241 }
6242
6243 if let Some(resting) = resting_potential {
6244 if npu_lock.update_neuron_resting_potential(neuron_id_u32, resting) {
6245 updated = true;
6246 debug!(target: "feagi-bdu","Updated neuron {} resting_potential = {}", neuron_id, resting);
6247 } else if !updated {
6248 return Err(BduError::InvalidNeuron(format!(
6249 "Neuron {} not found",
6250 neuron_id
6251 )));
6252 }
6253 }
6254
6255 if let Some(excit) = excitability {
6256 if npu_lock.update_neuron_excitability(neuron_id_u32, excit) {
6257 updated = true;
6258 debug!(target: "feagi-bdu","Updated neuron {} excitability = {}", neuron_id, excit);
6259 } else if !updated {
6260 return Err(BduError::InvalidNeuron(format!(
6261 "Neuron {} not found",
6262 neuron_id
6263 )));
6264 }
6265 }
6266
6267 if !updated {
6268 return Err(BduError::Internal(
6269 "No properties provided for update".to_string(),
6270 ));
6271 }
6272
6273 info!(target: "feagi-bdu","Updated properties for neuron {}", neuron_id);
6274
6275 Ok(())
6276 }
6277
6278 pub fn set_neuron_firing_threshold(
6290 &mut self,
6291 neuron_id: u64,
6292 new_threshold: f32,
6293 ) -> BduResult<()> {
6294 let npu = self
6296 .npu
6297 .as_ref()
6298 .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
6299
6300 let mut npu_lock = npu
6301 .lock()
6302 .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
6303
6304 if npu_lock.update_neuron_threshold(neuron_id as u32, new_threshold) {
6306 debug!(target: "feagi-bdu","Set neuron {} firing threshold = {}", neuron_id, new_threshold);
6307 Ok(())
6308 } else {
6309 Err(BduError::InvalidNeuron(format!(
6310 "Neuron {} not found",
6311 neuron_id
6312 )))
6313 }
6314 }
6315
6316 pub fn get_cortical_area_by_name(&self, name: &str) -> Option<CorticalArea> {
6331 self.cortical_areas
6332 .values()
6333 .find(|area| area.name == name)
6334 .cloned()
6335 }
6336
6337 pub fn resize_cortical_area(
6354 &mut self,
6355 cortical_id: &CorticalID,
6356 new_dimensions: CorticalAreaDimensions,
6357 ) -> BduResult<()> {
6358 if new_dimensions.width == 0 || new_dimensions.height == 0 || new_dimensions.depth == 0 {
6360 return Err(BduError::InvalidArea(format!(
6361 "Invalid dimensions: {:?} (all must be > 0)",
6362 new_dimensions
6363 )));
6364 }
6365
6366 let area = self.cortical_areas.get_mut(cortical_id).ok_or_else(|| {
6368 BduError::InvalidArea(format!("Cortical area {} not found", cortical_id))
6369 })?;
6370
6371 let old_dimensions = area.dimensions;
6372 area.dimensions = new_dimensions;
6373
6374 info!(target: "feagi-bdu",
6378 "Resized cortical area {} from {:?} to {:?}",
6379 cortical_id,
6380 old_dimensions,
6381 new_dimensions
6382 );
6383
6384 self.refresh_cortical_area_hashes(false, true);
6385
6386 Ok(())
6387 }
6388
6389 pub fn get_areas_in_region(&self, region_id: &str) -> BduResult<Vec<String>> {
6400 let region = self.brain_regions.get_region(region_id).ok_or_else(|| {
6401 BduError::InvalidArea(format!("Brain region {} not found", region_id))
6402 })?;
6403
6404 Ok(region
6406 .cortical_areas
6407 .iter()
6408 .map(|id| id.as_base_64())
6409 .collect())
6410 }
6411
6412 pub fn update_brain_region(
6425 &mut self,
6426 region_id: &str,
6427 new_name: Option<String>,
6428 new_description: Option<String>,
6429 ) -> BduResult<()> {
6430 let region = self
6431 .brain_regions
6432 .get_region_mut(region_id)
6433 .ok_or_else(|| {
6434 BduError::InvalidArea(format!("Brain region {} not found", region_id))
6435 })?;
6436
6437 if let Some(name) = new_name {
6438 region.name = name;
6439 debug!(target: "feagi-bdu","Updated brain region {} name", region_id);
6440 }
6441
6442 if let Some(desc) = new_description {
6443 region
6445 .properties
6446 .insert("description".to_string(), serde_json::json!(desc));
6447 debug!(target: "feagi-bdu","Updated brain region {} description", region_id);
6448 }
6449
6450 info!(target: "feagi-bdu","Updated brain region {}", region_id);
6451
6452 self.refresh_brain_regions_hash();
6453
6454 Ok(())
6455 }
6456
6457 pub fn update_brain_region_properties(
6471 &mut self,
6472 region_id: &str,
6473 properties: std::collections::HashMap<String, serde_json::Value>,
6474 ) -> BduResult<Option<BrainRegionIoRegistry>> {
6475 use tracing::{debug, info};
6476
6477 let should_recompute_io = properties
6478 .contains_key(crate::region_io_designation::DESIGNATED_INPUTS_KEY)
6479 || properties.contains_key(crate::region_io_designation::DESIGNATED_OUTPUTS_KEY);
6480
6481 if properties.contains_key(crate::region_io_designation::DESIGNATED_INPUTS_KEY)
6482 || properties.contains_key(crate::region_io_designation::DESIGNATED_OUTPUTS_KEY)
6483 {
6484 let region_snapshot = self
6485 .brain_regions
6486 .get_region(region_id)
6487 .ok_or_else(|| {
6488 BduError::InvalidArea(format!("Brain region {} not found", region_id))
6489 })?
6490 .clone();
6491 let (merged_in, merged_out) = crate::region_io_designation::merged_designated_lists(
6492 ®ion_snapshot,
6493 &properties,
6494 )?;
6495 crate::region_io_designation::validate_merged_designations_against_connectivity(
6496 self,
6497 ®ion_snapshot,
6498 &merged_in,
6499 &merged_out,
6500 )?;
6501 }
6502
6503 let region = self
6504 .brain_regions
6505 .get_region_mut(region_id)
6506 .ok_or_else(|| {
6507 BduError::InvalidArea(format!("Brain region {} not found", region_id))
6508 })?;
6509
6510 for (key, value) in properties {
6511 match key.as_str() {
6512 "title" | "name" | "region_title" => {
6514 if let Some(name) = value.as_str() {
6515 region.name = name.to_string();
6516 debug!(target: "feagi-bdu", "Updated brain region {} name = {}", region_id, name);
6517 }
6518 }
6519 "coordinate_3d" | "coordinates_3d" => {
6520 region
6521 .properties
6522 .insert("coordinate_3d".to_string(), value.clone());
6523 debug!(target: "feagi-bdu", "Updated brain region {} coordinate_3d = {:?}", region_id, value);
6524 }
6525 "coordinate_2d" | "coordinates_2d" => {
6526 region
6527 .properties
6528 .insert("coordinate_2d".to_string(), value.clone());
6529 debug!(target: "feagi-bdu", "Updated brain region {} coordinate_2d = {:?}", region_id, value);
6530 }
6531 "description" => {
6532 region
6533 .properties
6534 .insert("description".to_string(), value.clone());
6535 debug!(target: "feagi-bdu", "Updated brain region {} description", region_id);
6536 }
6537 "region_type" => {
6538 if let Some(type_str) = value.as_str() {
6539 region.region_type = feagi_structures::genomic::RegionType::Undefined;
6542 debug!(target: "feagi-bdu", "Updated brain region {} type = {}", region_id, type_str);
6543 }
6544 }
6545 _ => {
6547 region.properties.insert(key.clone(), value.clone());
6548 debug!(target: "feagi-bdu", "Updated brain region {} property {} = {:?}", region_id, key, value);
6549 }
6550 }
6551 }
6552
6553 info!(target: "feagi-bdu", "Updated brain region {} properties", region_id);
6554
6555 if should_recompute_io {
6558 let registry = self.recompute_brain_region_io_registry()?;
6559 return Ok(Some(registry));
6560 }
6561
6562 self.refresh_brain_regions_hash();
6566
6567 Ok(None)
6568 }
6569
6570 pub fn get_neuron_by_coordinates(
6588 &self,
6589 cortical_id: &CorticalID,
6590 x: u32,
6591 y: u32,
6592 z: u32,
6593 ) -> Option<u64> {
6594 let area = self.get_cortical_area(cortical_id)?;
6596 let cortical_idx = area.cortical_idx;
6597
6598 let npu = self.npu.as_ref()?;
6600 let npu_lock = npu.lock().ok()?;
6601
6602 npu_lock
6603 .get_neuron_id_at_coordinate(cortical_idx, x, y, z)
6604 .map(|id| id as u64)
6605 }
6606
6607 pub fn get_neuron_position(&self, neuron_id: u64) -> Option<(u32, u32, u32)> {
6618 let npu = self.npu.as_ref()?;
6619 let npu_lock = npu.lock().ok()?;
6620
6621 let neuron_count = npu_lock.get_neuron_count();
6623 if (neuron_id as usize) >= neuron_count {
6624 return None;
6625 }
6626
6627 Some(
6628 npu_lock
6629 .get_neuron_coordinates(neuron_id as u32)
6630 .unwrap_or((0, 0, 0)),
6631 )
6632 }
6633
6634 pub fn get_cortical_area_for_neuron(&self, neuron_id: u64) -> Option<CorticalID> {
6645 let npu = self.npu.as_ref()?;
6646 let npu_lock = npu.lock().ok()?;
6647
6648 let neuron_count = npu_lock.get_neuron_count();
6650 if (neuron_id as usize) >= neuron_count {
6651 return None;
6652 }
6653
6654 let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id as u32)?;
6655
6656 self.cortical_areas
6658 .values()
6659 .find(|area| area.cortical_idx == cortical_idx)
6660 .map(|area| area.cortical_id)
6661 }
6662
6663 pub fn get_neuron_properties(
6674 &self,
6675 neuron_id: u64,
6676 ) -> Option<std::collections::HashMap<String, serde_json::Value>> {
6677 let npu = self.npu.as_ref()?;
6678 let npu_lock = npu.lock().ok()?;
6679
6680 let neuron_id_u32 = neuron_id as u32;
6681 let idx = neuron_id as usize;
6682
6683 let neuron_count = npu_lock.get_neuron_count();
6685 if idx >= neuron_count {
6686 return None;
6687 }
6688
6689 let mut properties = std::collections::HashMap::new();
6690
6691 properties.insert("neuron_id".to_string(), serde_json::json!(neuron_id));
6693
6694 let (x, y, z) = npu_lock.get_neuron_coordinates(neuron_id_u32)?;
6696 properties.insert("x".to_string(), serde_json::json!(x));
6697 properties.insert("y".to_string(), serde_json::json!(y));
6698 properties.insert("z".to_string(), serde_json::json!(z));
6699
6700 let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id_u32)?;
6702 properties.insert("cortical_area".to_string(), serde_json::json!(cortical_idx));
6703
6704 properties.insert(
6706 "mp_charge_accumulation".to_string(),
6707 serde_json::json!(npu_lock.get_mp_charge_accumulation_at(idx).unwrap_or(false)),
6708 );
6709 properties.insert(
6710 "neuron_type".to_string(),
6711 serde_json::json!(npu_lock.get_neuron_type_at(idx).unwrap_or(0)),
6712 );
6713 let (mp_drv, psp_uni) = self
6714 .cortical_idx_to_id
6715 .get(&cortical_idx)
6716 .map(|cid| {
6717 (
6718 npu_lock.get_mp_driven_psp_for_cortical(cid),
6719 npu_lock.get_psp_uniform_distribution_for_cortical(cid),
6720 )
6721 })
6722 .unwrap_or((false, false));
6723 properties.insert("mp_driven_psp".to_string(), serde_json::json!(mp_drv));
6724 properties.insert(
6725 "psp_uniform_distribution".to_string(),
6726 serde_json::json!(psp_uni),
6727 );
6728
6729 let (consec_count, consec_limit, snooze, mp, threshold, refract_countdown) = npu_lock
6732 .get_neuron_state(NeuronId(neuron_id_u32))
6733 .unwrap_or((0u16, 0u16, 0u16, 0.0f32, 0.0f32, 0u16));
6734 properties.insert(
6735 "consecutive_fire_count".to_string(),
6736 serde_json::json!(consec_count),
6737 );
6738 properties.insert(
6739 "consecutive_fire_limit".to_string(),
6740 serde_json::json!(consec_limit),
6741 );
6742 properties.insert("snooze_period".to_string(), serde_json::json!(snooze));
6743 properties.insert("membrane_potential".to_string(), serde_json::json!(mp));
6744 properties.insert("threshold".to_string(), serde_json::json!(threshold));
6745 properties.insert(
6746 "refractory_countdown".to_string(),
6747 serde_json::json!(refract_countdown),
6748 );
6749
6750 properties.insert(
6752 "leak_coefficient".to_string(),
6753 serde_json::json!(npu_lock
6754 .get_neuron_property_by_index(idx, "leak_coefficient")
6755 .unwrap_or(0.0)),
6756 );
6757 properties.insert(
6758 "resting_potential".to_string(),
6759 serde_json::json!(npu_lock
6760 .get_neuron_property_by_index(idx, "resting_potential")
6761 .unwrap_or(0.0)),
6762 );
6763 properties.insert(
6764 "excitability".to_string(),
6765 serde_json::json!(npu_lock
6766 .get_neuron_property_by_index(idx, "excitability")
6767 .unwrap_or(0.0)),
6768 );
6769 properties.insert(
6770 "threshold_limit".to_string(),
6771 serde_json::json!(npu_lock
6772 .get_neuron_property_by_index(idx, "threshold_limit")
6773 .unwrap_or(0.0)),
6774 );
6775 properties.insert(
6776 "refractory_period".to_string(),
6777 serde_json::json!(npu_lock
6778 .get_neuron_property_u16_by_index(idx, "refractory_period")
6779 .unwrap_or(0)),
6780 );
6781
6782 Some(properties)
6783 }
6784
6785 pub fn get_neuron_property(
6797 &self,
6798 neuron_id: u64,
6799 property_name: &str,
6800 ) -> Option<serde_json::Value> {
6801 self.get_neuron_properties(neuron_id)?
6802 .get(property_name)
6803 .cloned()
6804 }
6805
6806 pub fn get_all_cortical_ids(&self) -> Vec<CorticalID> {
6817 self.cortical_areas.keys().copied().collect()
6818 }
6819
6820 pub fn get_all_cortical_indices(&self) -> Vec<u32> {
6827 self.cortical_areas
6828 .values()
6829 .map(|area| area.cortical_idx)
6830 .collect()
6831 }
6832
6833 pub fn get_cortical_area_names(&self) -> Vec<String> {
6840 self.cortical_areas
6841 .values()
6842 .map(|area| area.name.clone())
6843 .collect()
6844 }
6845
6846 pub fn list_ipu_areas(&self) -> Vec<CorticalID> {
6853 use crate::models::CorticalAreaExt;
6854 self.cortical_areas
6855 .values()
6856 .filter(|area| area.is_input_area())
6857 .map(|area| area.cortical_id)
6858 .collect()
6859 }
6860
6861 pub fn list_opu_areas(&self) -> Vec<CorticalID> {
6868 use crate::models::CorticalAreaExt;
6869 self.cortical_areas
6870 .values()
6871 .filter(|area| area.is_output_area())
6872 .map(|area| area.cortical_id)
6873 .collect()
6874 }
6875
6876 pub fn get_max_cortical_area_dimensions(&self) -> (usize, usize, usize) {
6883 self.cortical_areas
6884 .values()
6885 .fold((0, 0, 0), |(max_w, max_h, max_d), area| {
6886 (
6887 max_w.max(area.dimensions.width as usize),
6888 max_h.max(area.dimensions.height as usize),
6889 max_d.max(area.dimensions.depth as usize),
6890 )
6891 })
6892 }
6893
6894 pub fn get_cortical_area_properties(
6905 &self,
6906 cortical_id: &CorticalID,
6907 ) -> Option<std::collections::HashMap<String, serde_json::Value>> {
6908 let area = self.get_cortical_area(cortical_id)?;
6909
6910 let mut properties = std::collections::HashMap::new();
6911 properties.insert(
6912 "cortical_id".to_string(),
6913 serde_json::json!(area.cortical_id),
6914 );
6915 properties.insert(
6916 "cortical_id_s".to_string(),
6917 serde_json::json!(area.cortical_id.to_string()),
6918 );
6919 properties.insert(
6920 "cortical_idx".to_string(),
6921 serde_json::json!(area.cortical_idx),
6922 );
6923 properties.insert("name".to_string(), serde_json::json!(area.name));
6924 use crate::models::CorticalAreaExt;
6925 properties.insert(
6926 "area_type".to_string(),
6927 serde_json::json!(area.get_cortical_group()),
6928 );
6929 properties.insert(
6930 "dimensions".to_string(),
6931 serde_json::json!({
6932 "width": area.dimensions.width,
6933 "height": area.dimensions.height,
6934 "depth": area.dimensions.depth,
6935 }),
6936 );
6937 properties.insert("position".to_string(), serde_json::json!(area.position));
6938
6939 for (key, value) in &area.properties {
6941 properties.insert(key.clone(), value.clone());
6942 }
6943
6944 properties.extend(area.properties.clone());
6946
6947 Some(properties)
6948 }
6949
6950 pub fn get_all_cortical_area_properties(
6957 &self,
6958 ) -> Vec<std::collections::HashMap<String, serde_json::Value>> {
6959 self.cortical_areas
6960 .keys()
6961 .filter_map(|id| self.get_cortical_area_properties(id))
6962 .collect()
6963 }
6964
6965 pub fn get_all_brain_region_ids(&self) -> Vec<String> {
6976 self.brain_regions
6977 .get_all_region_ids()
6978 .into_iter()
6979 .cloned()
6980 .collect()
6981 }
6982
6983 pub fn get_brain_region_names(&self) -> Vec<String> {
6990 self.brain_regions
6991 .get_all_region_ids()
6992 .iter()
6993 .filter_map(|id| {
6994 self.brain_regions
6995 .get_region(id)
6996 .map(|region| region.name.clone())
6997 })
6998 .collect()
6999 }
7000
7001 pub fn get_brain_region_properties(
7012 &self,
7013 region_id: &str,
7014 ) -> Option<std::collections::HashMap<String, serde_json::Value>> {
7015 let region = self.brain_regions.get_region(region_id)?;
7016
7017 let mut properties = std::collections::HashMap::new();
7018 properties.insert("region_id".to_string(), serde_json::json!(region.region_id));
7019 properties.insert("name".to_string(), serde_json::json!(region.name));
7020 properties.insert(
7021 "region_type".to_string(),
7022 serde_json::json!(format!("{:?}", region.region_type)),
7023 );
7024 properties.insert(
7025 "cortical_areas".to_string(),
7026 serde_json::json!(region.cortical_areas.iter().collect::<Vec<_>>()),
7027 );
7028
7029 properties.extend(region.properties.clone());
7031
7032 Some(properties)
7033 }
7034
7035 pub fn cortical_area_exists(&self, cortical_id: &CorticalID) -> bool {
7046 self.cortical_areas.contains_key(cortical_id)
7047 }
7048
7049 pub fn brain_region_exists(&self, region_id: &str) -> bool {
7060 self.brain_regions.get_region(region_id).is_some()
7061 }
7062
7063 pub fn get_brain_region_count(&self) -> usize {
7070 self.brain_regions.region_count()
7071 }
7072
7073 pub fn get_neurons_by_cortical_area(&self, cortical_id: &CorticalID) -> Vec<u64> {
7084 self.get_neurons_in_area(cortical_id)
7088 }
7089}
7090
7091impl std::fmt::Debug for ConnectomeManager {
7093 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7094 f.debug_struct("ConnectomeManager")
7095 .field("cortical_areas", &self.cortical_areas.len())
7096 .field("next_cortical_idx", &self.next_cortical_idx)
7097 .field("brain_regions", &self.brain_regions)
7098 .field(
7099 "npu",
7100 &if self.npu.is_some() {
7101 "Connected"
7102 } else {
7103 "Not connected"
7104 },
7105 )
7106 .field("initialized", &self.initialized)
7107 .finish()
7108 }
7109}
7110
7111#[cfg(test)]
7112mod tests {
7113 use super::*;
7114 use feagi_structures::genomic::cortical_area::CoreCorticalType;
7115
7116 #[test]
7117 fn test_singleton_instance() {
7118 let instance1 = ConnectomeManager::instance();
7119 let instance2 = ConnectomeManager::instance();
7120
7121 assert_eq!(Arc::strong_count(&instance1), Arc::strong_count(&instance2));
7123 }
7124
7125 #[test]
7126 fn test_add_cortical_area() {
7127 ConnectomeManager::reset_for_testing();
7128
7129 let instance = ConnectomeManager::instance();
7130 let mut manager = instance.write();
7131
7132 use feagi_structures::genomic::cortical_area::{
7133 CorticalAreaType, IOCorticalAreaConfigurationFlag,
7134 };
7135 let cortical_id = CorticalID::try_from_bytes(b"cst_add_").unwrap(); let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
7137 let area = CorticalArea::new(
7138 cortical_id,
7139 0,
7140 "Visual Input".to_string(),
7141 CorticalAreaDimensions::new(128, 128, 20).unwrap(),
7142 (0, 0, 0).into(),
7143 cortical_type,
7144 )
7145 .unwrap();
7146
7147 let initial_count = manager.get_cortical_area_count();
7148 let _cortical_idx = manager.add_cortical_area(area).unwrap();
7149
7150 assert_eq!(manager.get_cortical_area_count(), initial_count + 1);
7151 assert!(manager.has_cortical_area(&cortical_id));
7152 assert!(manager.is_initialized());
7153 }
7154
7155 #[test]
7156 fn test_cortical_area_lookups() {
7157 ConnectomeManager::reset_for_testing();
7158
7159 let instance = ConnectomeManager::instance();
7160 let mut manager = instance.write();
7161
7162 use feagi_structures::genomic::cortical_area::{
7163 CorticalAreaType, IOCorticalAreaConfigurationFlag,
7164 };
7165 let cortical_id = CorticalID::try_from_bytes(b"cst_look").unwrap(); let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
7167 let area = CorticalArea::new(
7168 cortical_id,
7169 0,
7170 "Test Area".to_string(),
7171 CorticalAreaDimensions::new(10, 10, 10).unwrap(),
7172 (0, 0, 0).into(),
7173 cortical_type,
7174 )
7175 .unwrap();
7176
7177 let cortical_idx = manager.add_cortical_area(area).unwrap();
7178
7179 assert_eq!(manager.get_cortical_idx(&cortical_id), Some(cortical_idx));
7181
7182 assert_eq!(manager.get_cortical_id(cortical_idx), Some(&cortical_id));
7184
7185 let retrieved_area = manager.get_cortical_area(&cortical_id).unwrap();
7187 assert_eq!(retrieved_area.name, "Test Area");
7188 }
7189
7190 #[test]
7191 fn test_remove_cortical_area() {
7192 ConnectomeManager::reset_for_testing();
7193
7194 let instance = ConnectomeManager::instance();
7195 let mut manager = instance.write();
7196
7197 use feagi_structures::genomic::cortical_area::{
7198 CorticalAreaType, IOCorticalAreaConfigurationFlag,
7199 };
7200 let cortical_id = CoreCorticalType::Power.to_cortical_id();
7201
7202 if manager.has_cortical_area(&cortical_id) {
7204 manager.remove_cortical_area(&cortical_id).unwrap();
7205 }
7206
7207 let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
7208 let area = CorticalArea::new(
7209 cortical_id,
7210 0,
7211 "Test".to_string(),
7212 CorticalAreaDimensions::new(10, 10, 10).unwrap(),
7213 (0, 0, 0).into(),
7214 cortical_type,
7215 )
7216 .unwrap();
7217
7218 let initial_count = manager.get_cortical_area_count();
7219 manager.add_cortical_area(area).unwrap();
7220 assert_eq!(manager.get_cortical_area_count(), initial_count + 1);
7221
7222 manager.remove_cortical_area(&cortical_id).unwrap();
7223 assert_eq!(manager.get_cortical_area_count(), initial_count);
7224 assert!(!manager.has_cortical_area(&cortical_id));
7225 }
7226
7227 #[test]
7228 fn test_duplicate_area_error() {
7229 ConnectomeManager::reset_for_testing();
7230
7231 let instance = ConnectomeManager::instance();
7232 let mut manager = instance.write();
7233
7234 use feagi_structures::genomic::cortical_area::{
7235 CorticalAreaType, IOCorticalAreaConfigurationFlag,
7236 };
7237 let cortical_id = CorticalID::try_from_bytes(b"cst_dup1").unwrap();
7240 let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
7241 let area1 = CorticalArea::new(
7242 cortical_id,
7243 0,
7244 "First".to_string(),
7245 CorticalAreaDimensions::new(10, 10, 10).unwrap(),
7246 (0, 0, 0).into(),
7247 cortical_type,
7248 )
7249 .unwrap();
7250
7251 let area2 = CorticalArea::new(
7252 cortical_id, 1,
7254 "Second".to_string(),
7255 CorticalAreaDimensions::new(10, 10, 10).unwrap(),
7256 (0, 0, 0).into(),
7257 cortical_type,
7258 )
7259 .unwrap();
7260
7261 manager.add_cortical_area(area1).unwrap();
7262 let result = manager.add_cortical_area(area2);
7263
7264 assert!(result.is_err());
7265 }
7266
7267 #[test]
7268 fn test_brain_region_management() {
7269 ConnectomeManager::reset_for_testing();
7270
7271 let instance = ConnectomeManager::instance();
7272 let mut manager = instance.write();
7273
7274 let region_id = feagi_structures::genomic::brain_regions::RegionID::new();
7275 let region_id_str = region_id.to_string();
7276 let root = BrainRegion::new(
7277 region_id,
7278 "Root".to_string(),
7279 feagi_structures::genomic::brain_regions::RegionType::Undefined,
7280 )
7281 .unwrap();
7282
7283 let initial_count = manager.get_brain_region_ids().len();
7284 manager.add_brain_region(root, None).unwrap();
7285
7286 assert_eq!(manager.get_brain_region_ids().len(), initial_count + 1);
7287 assert!(manager.get_brain_region(®ion_id_str).is_some());
7288 }
7289
7290 #[test]
7291 fn test_synapse_operations() {
7292 use feagi_npu_burst_engine::npu::RustNPU;
7293 use feagi_npu_burst_engine::TracingMutex;
7294 use std::sync::Arc;
7295
7296 use feagi_npu_burst_engine::backend::CPUBackend;
7298 use feagi_npu_burst_engine::DynamicNPU;
7299 use feagi_npu_runtime::StdRuntime;
7300
7301 let runtime = StdRuntime;
7302 let backend = CPUBackend::new();
7303 let npu_result =
7304 RustNPU::new(runtime, backend, 100, 1000, 10).expect("Failed to create NPU");
7305 let npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu_result), "TestNPU"));
7306 let mut manager = ConnectomeManager::new_for_testing_with_npu(npu.clone());
7307
7308 use feagi_structures::genomic::cortical_area::{
7310 CorticalAreaType, IOCorticalAreaConfigurationFlag,
7311 };
7312 let cortical_id = CorticalID::try_from_bytes(b"cst_syn_").unwrap(); let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
7314 let area = CorticalArea::new(
7315 cortical_id,
7316 0, "Test Area".to_string(),
7318 CorticalAreaDimensions::new(10, 10, 1).unwrap(),
7319 (0, 0, 0).into(), cortical_type,
7321 )
7322 .unwrap();
7323 let cortical_idx = manager.add_cortical_area(area).unwrap();
7324
7325 if let Some(npu_arc) = manager.get_npu() {
7327 if let Ok(mut npu_guard) = npu_arc.try_lock() {
7328 if let DynamicNPU::F32(ref mut npu) = *npu_guard {
7329 npu.register_cortical_area(cortical_idx, cortical_id.as_base_64());
7330 }
7331 }
7332 }
7333
7334 let neuron1_id = manager
7336 .add_neuron(
7337 &cortical_id,
7338 0,
7339 0,
7340 0, 100.0, 0.0, 0.1, -60.0, 0, 2, 1.0, 5, 10, false, )
7352 .unwrap();
7353
7354 let neuron2_id = manager
7355 .add_neuron(
7356 &cortical_id,
7357 1,
7358 0,
7359 0, 100.0,
7361 f32::MAX, 0.1,
7363 -60.0,
7364 0,
7365 2,
7366 1.0,
7367 5,
7368 10,
7369 false,
7370 )
7371 .unwrap();
7372
7373 manager
7375 .create_synapse(
7376 neuron1_id, neuron2_id, 128.0, 64.0, 0, )
7380 .unwrap();
7381
7382 println!("✅ Synapse creation test passed");
7385 }
7386
7387 #[test]
7388 fn test_apply_cortical_mapping_missing_rules_is_ok() {
7389 let mut manager = ConnectomeManager::new_for_testing();
7392
7393 use feagi_structures::genomic::cortical_area::{
7394 CorticalAreaType, IOCorticalAreaConfigurationFlag,
7395 };
7396
7397 let src_id = CorticalID::try_from_bytes(b"map_src_").unwrap();
7398 let dst_id = CorticalID::try_from_bytes(b"map_dst_").unwrap();
7399
7400 let src_area = CorticalArea::new(
7401 src_id,
7402 0,
7403 "src".to_string(),
7404 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7405 (0, 0, 0).into(),
7406 CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
7407 )
7408 .unwrap();
7409
7410 let dst_area = CorticalArea::new(
7411 dst_id,
7412 1,
7413 "dst".to_string(),
7414 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7415 (0, 0, 0).into(),
7416 CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
7417 )
7418 .unwrap();
7419
7420 manager.add_cortical_area(src_area).unwrap();
7421 manager.add_cortical_area(dst_area).unwrap();
7422
7423 let count = manager
7425 .apply_cortical_mapping_for_pair(&src_id, &dst_id)
7426 .unwrap();
7427 assert_eq!(count, 0);
7428
7429 manager
7431 .update_cortical_mapping(
7432 &src_id,
7433 &dst_id,
7434 vec![serde_json::json!({"morphology_id":"m1"})],
7435 )
7436 .unwrap();
7437 manager
7438 .update_cortical_mapping(&src_id, &dst_id, vec![])
7439 .unwrap();
7440
7441 let count2 = manager
7442 .apply_cortical_mapping_for_pair(&src_id, &dst_id)
7443 .unwrap();
7444 assert_eq!(count2, 0);
7445 }
7446
7447 #[test]
7448 fn test_get_mapping_rules_for_destination_supports_legacy_key() {
7449 let dst_id = CorticalID::try_from_bytes(b"csrc0002").unwrap();
7450 let mapping_dst = serde_json::json!({
7451 "csrc0002": [
7452 {"morphology_id": "m1"}
7453 ]
7454 });
7455 let mapping_obj = mapping_dst.as_object().expect("mapping must be an object");
7456
7457 let rules = ConnectomeManager::get_mapping_rules_for_destination(mapping_obj, &dst_id)
7458 .expect("legacy destination key should resolve");
7459 assert_eq!(rules.len(), 1);
7460 assert_eq!(
7461 rules[0].get("morphology_id").and_then(|v| v.as_str()),
7462 Some("m1")
7463 );
7464 }
7465
7466 #[test]
7467 fn test_get_neuron_properties_always_includes_neuron_state_keys() {
7468 use feagi_npu_burst_engine::backend::CPUBackend;
7469 use feagi_npu_burst_engine::RustNPU;
7470 use feagi_npu_burst_engine::TracingMutex;
7471 use feagi_npu_runtime::StdRuntime;
7472 use feagi_structures::genomic::cortical_area::{
7473 CorticalAreaDimensions, CorticalAreaType, IOCorticalAreaConfigurationFlag,
7474 };
7475 use std::sync::Arc;
7476
7477 let runtime = StdRuntime;
7478 let backend = CPUBackend::new();
7479 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
7480 let dyn_npu = Arc::new(TracingMutex::new(
7481 feagi_npu_burst_engine::DynamicNPU::F32(npu),
7482 "TestNPU",
7483 ));
7484 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
7485
7486 let area_id = CorticalID::try_from_bytes(b"cst_nsp_").unwrap();
7487 let area = CorticalArea::new(
7488 area_id,
7489 0,
7490 "n".to_string(),
7491 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7492 (0, 0, 0).into(),
7493 CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
7494 )
7495 .unwrap();
7496
7497 manager.add_cortical_area(area).unwrap();
7498 let nid = manager
7499 .add_neuron(
7500 &area_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false,
7501 )
7502 .unwrap();
7503
7504 let props = manager
7505 .get_neuron_properties(nid)
7506 .expect("neuron properties");
7507 for key in [
7508 "consecutive_fire_count",
7509 "consecutive_fire_limit",
7510 "snooze_period",
7511 "membrane_potential",
7512 "threshold",
7513 "refractory_countdown",
7514 "mp_charge_accumulation",
7515 "neuron_type",
7516 "mp_driven_psp",
7517 "psp_uniform_distribution",
7518 "leak_coefficient",
7519 "resting_potential",
7520 "excitability",
7521 "threshold_limit",
7522 "refractory_period",
7523 ] {
7524 assert!(props.contains_key(key), "missing neuron state key: {key}");
7525 }
7526 }
7527
7528 #[test]
7529 fn test_mapping_deletion_prunes_synapses_between_areas() {
7530 use feagi_npu_burst_engine::backend::CPUBackend;
7531 use feagi_npu_burst_engine::RustNPU;
7532 use feagi_npu_burst_engine::TracingMutex;
7533 use feagi_npu_runtime::StdRuntime;
7534 use feagi_structures::genomic::cortical_area::{
7535 CorticalAreaDimensions, CorticalAreaType, IOCorticalAreaConfigurationFlag,
7536 };
7537 use std::sync::Arc;
7538
7539 let runtime = StdRuntime;
7541 let backend = CPUBackend::new();
7542 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
7543 let dyn_npu = Arc::new(TracingMutex::new(
7544 feagi_npu_burst_engine::DynamicNPU::F32(npu),
7545 "TestNPU",
7546 ));
7547 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
7548
7549 let src_id = CorticalID::try_from_bytes(b"cst_src_").unwrap();
7551 let dst_id = CorticalID::try_from_bytes(b"cst_dst_").unwrap();
7552
7553 let src_area = CorticalArea::new(
7554 src_id,
7555 0,
7556 "src".to_string(),
7557 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7558 (0, 0, 0).into(),
7559 CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
7560 )
7561 .unwrap();
7562 let dst_area = CorticalArea::new(
7563 dst_id,
7564 1,
7565 "dst".to_string(),
7566 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7567 (0, 0, 0).into(),
7568 CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
7569 )
7570 .unwrap();
7571
7572 manager.add_cortical_area(src_area).unwrap();
7573 manager.add_cortical_area(dst_area).unwrap();
7574
7575 let s0 = manager
7577 .add_neuron(&src_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7578 .unwrap();
7579 let s1 = manager
7580 .add_neuron(&src_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7581 .unwrap();
7582 let t0 = manager
7583 .add_neuron(&dst_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7584 .unwrap();
7585 let t1 = manager
7586 .add_neuron(&dst_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7587 .unwrap();
7588
7589 manager.create_synapse(s0, t0, 128.0, 200.0, 0).unwrap();
7591 manager.create_synapse(s1, t1, 128.0, 200.0, 0).unwrap();
7592
7593 {
7595 let mut npu = dyn_npu.lock().unwrap();
7596 npu.rebuild_synapse_index();
7597 assert_eq!(npu.get_synapse_count(), 2);
7598 }
7599
7600 manager
7602 .update_cortical_mapping(&src_id, &dst_id, vec![])
7603 .unwrap();
7604 let created = manager
7605 .regenerate_synapses_for_mapping(&src_id, &dst_id)
7606 .unwrap();
7607 assert_eq!(created, 0);
7608
7609 {
7611 let mut npu = dyn_npu.lock().unwrap();
7612 npu.rebuild_synapse_index();
7614 assert_eq!(npu.get_synapse_count(), 0);
7615 assert!(npu.get_outgoing_synapses(s0 as u32).is_empty());
7616 assert!(npu.get_outgoing_synapses(s1 as u32).is_empty());
7617 }
7618 }
7619
7620 #[test]
7621 fn test_mapping_update_prunes_synapses_between_areas() {
7622 use feagi_npu_burst_engine::backend::CPUBackend;
7623 use feagi_npu_burst_engine::RustNPU;
7624 use feagi_npu_burst_engine::TracingMutex;
7625 use feagi_npu_runtime::StdRuntime;
7626 use feagi_structures::genomic::cortical_area::{
7627 CorticalAreaDimensions, CorticalAreaType, IOCorticalAreaConfigurationFlag,
7628 };
7629 use std::sync::Arc;
7630
7631 let runtime = StdRuntime;
7633 let backend = CPUBackend::new();
7634 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
7635 let dyn_npu = Arc::new(TracingMutex::new(
7636 feagi_npu_burst_engine::DynamicNPU::F32(npu),
7637 "TestNPU",
7638 ));
7639 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
7640
7641 feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
7643
7644 let src_id = CorticalID::try_from_bytes(b"cstupds1").unwrap();
7647 let dst_id = CorticalID::try_from_bytes(b"cstupdt1").unwrap();
7648
7649 let src_area = CorticalArea::new(
7650 src_id,
7651 0,
7652 "src".to_string(),
7653 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7654 (0, 0, 0).into(),
7655 CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
7656 )
7657 .unwrap();
7658 let dst_area = CorticalArea::new(
7659 dst_id,
7660 0,
7661 "dst".to_string(),
7662 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7663 (0, 0, 0).into(),
7664 CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
7665 )
7666 .unwrap();
7667
7668 manager.add_cortical_area(src_area).unwrap();
7669 manager.add_cortical_area(dst_area).unwrap();
7670
7671 let s0 = manager
7673 .add_neuron(&src_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7674 .unwrap();
7675 let s1 = manager
7676 .add_neuron(&src_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7677 .unwrap();
7678 let t0 = manager
7679 .add_neuron(&dst_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7680 .unwrap();
7681 let t1 = manager
7682 .add_neuron(&dst_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7683 .unwrap();
7684
7685 manager.create_synapse(s0, t0, 128.0, 200.0, 0).unwrap();
7687 manager.create_synapse(s1, t1, 128.0, 200.0, 0).unwrap();
7688
7689 {
7691 let mut npu = dyn_npu.lock().unwrap();
7692 npu.rebuild_synapse_index();
7693 assert_eq!(npu.get_synapse_count(), 2);
7694 }
7695
7696 manager
7702 .update_cortical_mapping(
7703 &src_id,
7704 &dst_id,
7705 vec![serde_json::json!({
7706 "morphology_id": "episodic_memory",
7707 "morphology_scalar": [1],
7708 "postSynapticCurrent_multiplier": 1,
7709 "plasticity_flag": false,
7710 "plasticity_constant": 0,
7711 "ltp_multiplier": 0,
7712 "ltd_multiplier": 0,
7713 "plasticity_window": 0,
7714 })],
7715 )
7716 .unwrap();
7717 let created = manager
7718 .regenerate_synapses_for_mapping(&src_id, &dst_id)
7719 .unwrap();
7720 assert_eq!(created, 0);
7721
7722 {
7724 let mut npu = dyn_npu.lock().unwrap();
7725 npu.rebuild_synapse_index();
7727 assert_eq!(npu.get_synapse_count(), 0);
7728 assert!(npu.get_outgoing_synapses(s0 as u32).is_empty());
7729 assert!(npu.get_outgoing_synapses(s1 as u32).is_empty());
7730 }
7731 }
7732
7733 #[test]
7734 fn test_upstream_area_tracking() {
7735 use crate::models::cortical_area::CorticalArea;
7737 use feagi_npu_burst_engine::backend::CPUBackend;
7738 use feagi_npu_burst_engine::TracingMutex;
7739 use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
7740 use feagi_npu_runtime::StdRuntime;
7741 use feagi_structures::genomic::cortical_area::{
7742 CorticalAreaDimensions, CorticalAreaType, CorticalID,
7743 };
7744
7745 let runtime = StdRuntime;
7747 let backend = CPUBackend::new();
7748 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
7749 let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
7750 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
7751
7752 feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
7755
7756 let src_id = CorticalID::try_from_bytes(b"csrc0000").unwrap();
7758 let src_area = CorticalArea::new(
7759 src_id,
7760 0,
7761 "Source Area".to_string(),
7762 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7763 (0, 0, 0).into(),
7764 CorticalAreaType::Custom(
7765 feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
7766 ),
7767 )
7768 .unwrap();
7769 let src_idx = manager.add_cortical_area(src_area).unwrap();
7770
7771 let dst_id = CorticalID::try_from_bytes(b"cdst0000").unwrap();
7773 let dst_area = CorticalArea::new(
7774 dst_id,
7775 0,
7776 "Dest Area".to_string(),
7777 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7778 (0, 0, 0).into(),
7779 CorticalAreaType::Custom(
7780 feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
7781 ),
7782 )
7783 .unwrap();
7784 manager.add_cortical_area(dst_area).unwrap();
7785
7786 {
7788 let dst_area = manager.get_cortical_area(&dst_id).unwrap();
7789 let upstream = dst_area.properties.get("upstream_cortical_areas").unwrap();
7790 assert!(
7791 upstream.as_array().unwrap().is_empty(),
7792 "Upstream areas should be empty initially"
7793 );
7794 }
7795
7796 let mapping_data = vec![serde_json::json!({
7798 "morphology_id": "episodic_memory",
7799 "morphology_scalar": 1,
7800 "postSynapticCurrent_multiplier": 1.0,
7801 })];
7802 manager
7803 .update_cortical_mapping(&src_id, &dst_id, mapping_data)
7804 .unwrap();
7805 manager
7806 .regenerate_synapses_for_mapping(&src_id, &dst_id)
7807 .unwrap();
7808
7809 {
7811 let upstream_areas = manager.get_upstream_cortical_areas(&dst_id);
7812 assert_eq!(upstream_areas.len(), 1, "Should have 1 upstream area");
7813 assert_eq!(
7814 upstream_areas[0], src_idx,
7815 "Upstream area should be src_idx"
7816 );
7817 }
7818
7819 manager
7821 .update_cortical_mapping(&src_id, &dst_id, vec![])
7822 .unwrap();
7823 manager
7824 .regenerate_synapses_for_mapping(&src_id, &dst_id)
7825 .unwrap();
7826
7827 {
7829 let upstream_areas = manager.get_upstream_cortical_areas(&dst_id);
7830 assert_eq!(
7831 upstream_areas.len(),
7832 0,
7833 "Should have 0 upstream areas after deletion"
7834 );
7835 }
7836 }
7837
7838 #[test]
7839 fn test_refresh_upstream_areas_for_associative_memory_pairs() {
7840 use crate::models::cortical_area::CorticalArea;
7841 use feagi_npu_burst_engine::backend::CPUBackend;
7842 use feagi_npu_burst_engine::TracingMutex;
7843 use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
7844 use feagi_npu_runtime::StdRuntime;
7845 use feagi_structures::genomic::cortical_area::{
7846 CorticalAreaDimensions, CorticalAreaType, CorticalID, MemoryCorticalType,
7847 };
7848 use std::sync::Arc;
7849
7850 let runtime = StdRuntime;
7851 let backend = CPUBackend::new();
7852 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
7853 let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
7854 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
7855 feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
7856
7857 let a1_id = CorticalID::try_from_bytes(b"csrc0002").unwrap();
7858 let a2_id = CorticalID::try_from_bytes(b"csrc0003").unwrap();
7859 let m1_id = CorticalID::try_from_bytes(b"mmem0002").unwrap();
7860 let m2_id = CorticalID::try_from_bytes(b"mmem0003").unwrap();
7861
7862 let a1_area = CorticalArea::new(
7863 a1_id,
7864 0,
7865 "A1".to_string(),
7866 CorticalAreaDimensions::new(1, 1, 1).unwrap(),
7867 (0, 0, 0).into(),
7868 CorticalAreaType::Custom(
7869 feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
7870 ),
7871 )
7872 .unwrap();
7873 let a2_area = CorticalArea::new(
7874 a2_id,
7875 0,
7876 "A2".to_string(),
7877 CorticalAreaDimensions::new(1, 1, 1).unwrap(),
7878 (0, 0, 0).into(),
7879 CorticalAreaType::Custom(
7880 feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
7881 ),
7882 )
7883 .unwrap();
7884
7885 let mut m1_area = CorticalArea::new(
7886 m1_id,
7887 0,
7888 "M1".to_string(),
7889 CorticalAreaDimensions::new(1, 1, 1).unwrap(),
7890 (0, 0, 0).into(),
7891 CorticalAreaType::Memory(MemoryCorticalType::Memory),
7892 )
7893 .unwrap();
7894 m1_area
7895 .properties
7896 .insert("is_mem_type".to_string(), serde_json::json!(true));
7897 m1_area
7898 .properties
7899 .insert("temporal_depth".to_string(), serde_json::json!(1));
7900
7901 let mut m2_area = CorticalArea::new(
7902 m2_id,
7903 0,
7904 "M2".to_string(),
7905 CorticalAreaDimensions::new(1, 1, 1).unwrap(),
7906 (0, 0, 0).into(),
7907 CorticalAreaType::Memory(MemoryCorticalType::Memory),
7908 )
7909 .unwrap();
7910 m2_area
7911 .properties
7912 .insert("is_mem_type".to_string(), serde_json::json!(true));
7913 m2_area
7914 .properties
7915 .insert("temporal_depth".to_string(), serde_json::json!(1));
7916
7917 let a1_idx = manager.add_cortical_area(a1_area).unwrap();
7918 let a2_idx = manager.add_cortical_area(a2_area).unwrap();
7919 let m1_idx = manager.add_cortical_area(m1_area).unwrap();
7920 let m2_idx = manager.add_cortical_area(m2_area).unwrap();
7921
7922 manager
7923 .add_neuron(&a1_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7924 .unwrap();
7925 manager
7926 .add_neuron(&a2_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
7927 .unwrap();
7928
7929 let episodic_mapping = vec![serde_json::json!({
7930 "morphology_id": "episodic_memory",
7931 "morphology_scalar": 1,
7932 "postSynapticCurrent_multiplier": 1.0,
7933 })];
7934 manager
7935 .update_cortical_mapping(&a1_id, &m1_id, episodic_mapping.clone())
7936 .unwrap();
7937 manager
7938 .regenerate_synapses_for_mapping(&a1_id, &m1_id)
7939 .unwrap();
7940 manager
7941 .update_cortical_mapping(&a2_id, &m2_id, episodic_mapping)
7942 .unwrap();
7943 manager
7944 .regenerate_synapses_for_mapping(&a2_id, &m2_id)
7945 .unwrap();
7946
7947 let assoc_mapping = vec![serde_json::json!({
7948 "morphology_id": "associative_memory",
7949 "morphology_scalar": 1,
7950 "postSynapticCurrent_multiplier": 1.0,
7951 "plasticity_flag": true,
7952 "plasticity_constant": 1,
7953 "ltp_multiplier": 1,
7954 "ltd_multiplier": 1,
7955 "plasticity_window": 5,
7956 })];
7957 manager
7958 .update_cortical_mapping(&m1_id, &m2_id, assoc_mapping.clone())
7959 .unwrap();
7960 manager
7961 .regenerate_synapses_for_mapping(&m1_id, &m2_id)
7962 .unwrap();
7963 manager
7965 .update_cortical_mapping(&m2_id, &m1_id, assoc_mapping)
7966 .unwrap();
7967 manager
7968 .regenerate_synapses_for_mapping(&m2_id, &m1_id)
7969 .unwrap();
7970
7971 let upstream_m1 = manager.get_upstream_cortical_areas(&m1_id);
7972 let upstream_m2 = manager.get_upstream_cortical_areas(&m2_id);
7973 assert_eq!(
7974 upstream_m1.len(),
7975 2,
7976 "M1 should have A1 and M2 as upstreams once both directed associative edges exist"
7977 );
7978 assert_eq!(
7979 upstream_m2.len(),
7980 2,
7981 "M2 should have A2 and M1 as upstreams"
7982 );
7983
7984 manager.refresh_upstream_cortical_areas_from_mappings(&m1_id);
7985 manager.refresh_upstream_cortical_areas_from_mappings(&m2_id);
7986
7987 let upstream_m1 = manager.get_upstream_cortical_areas(&m1_id);
7988 let upstream_m2 = manager.get_upstream_cortical_areas(&m2_id);
7989 assert_eq!(upstream_m1.len(), 2, "M1 upstreams unchanged after refresh");
7990 assert_eq!(upstream_m2.len(), 2, "M2 upstreams unchanged after refresh");
7991 assert!(upstream_m1.contains(&a1_idx));
7992 assert!(upstream_m1.contains(&m2_idx));
7993 assert!(upstream_m2.contains(&a2_idx));
7994 assert!(upstream_m2.contains(&m1_idx));
7995
7996 {
7998 let mut npu_lock = dyn_npu.lock().unwrap();
7999 let injected_a1 = npu_lock.inject_sensory_xyzp_by_id(&a1_id, &[(0, 0, 0, 1.0)]);
8000 let injected_a2 = npu_lock.inject_sensory_xyzp_by_id(&a2_id, &[(0, 0, 0, 1.0)]);
8001 assert_eq!(injected_a1, 1, "Expected A1 injection to match one neuron");
8002 assert_eq!(injected_a2, 1, "Expected A2 injection to match one neuron");
8003 npu_lock.process_burst().expect("Burst processing failed");
8004 }
8005
8006 let upstream_m1 = manager.get_upstream_cortical_areas(&m1_id);
8007 let upstream_m2 = manager.get_upstream_cortical_areas(&m2_id);
8008 assert_eq!(
8009 upstream_m1.len(),
8010 2,
8011 "M1 should keep 2 upstreams after firing"
8012 );
8013 assert_eq!(
8014 upstream_m2.len(),
8015 2,
8016 "M2 should keep 2 upstreams after firing"
8017 );
8018 }
8019
8020 #[test]
8021 fn test_memory_twin_created_for_memory_mapping() {
8022 use crate::models::cortical_area::CorticalArea;
8023 use feagi_npu_burst_engine::backend::CPUBackend;
8024 use feagi_npu_burst_engine::TracingMutex;
8025 use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
8026 use feagi_npu_runtime::StdRuntime;
8027 use feagi_structures::genomic::cortical_area::{
8028 CorticalAreaDimensions, CorticalAreaType, CorticalID, IOCorticalAreaConfigurationFlag,
8029 MemoryCorticalType,
8030 };
8031 use std::sync::Arc;
8032
8033 let runtime = StdRuntime;
8034 let backend = CPUBackend::new();
8035 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
8036 let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
8037 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
8038 feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
8039
8040 let src_id = CorticalID::try_from_bytes(b"csrc0001").unwrap();
8041 let dst_id = CorticalID::try_from_bytes(b"mmem0001").unwrap();
8042
8043 let src_area = CorticalArea::new(
8044 src_id,
8045 0,
8046 "Source Area".to_string(),
8047 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
8048 (0, 0, 0).into(),
8049 CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
8050 )
8051 .unwrap();
8052 let mut dst_area = CorticalArea::new(
8053 dst_id,
8054 0,
8055 "Memory Area".to_string(),
8056 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
8057 (0, 0, 0).into(),
8058 CorticalAreaType::Memory(MemoryCorticalType::Memory),
8059 )
8060 .unwrap();
8061 dst_area
8062 .properties
8063 .insert("is_mem_type".to_string(), serde_json::json!(true));
8064 dst_area
8065 .properties
8066 .insert("temporal_depth".to_string(), serde_json::json!(1));
8067
8068 manager.add_cortical_area(src_area).unwrap();
8069 manager.add_cortical_area(dst_area).unwrap();
8070
8071 let mapping_data = vec![serde_json::json!({
8072 "morphology_id": "episodic_memory",
8073 "morphology_scalar": 1,
8074 "postSynapticCurrent_multiplier": 1.0,
8075 })];
8076 manager
8077 .update_cortical_mapping(&src_id, &dst_id, mapping_data)
8078 .unwrap();
8079 manager
8080 .regenerate_synapses_for_mapping(&src_id, &dst_id)
8081 .unwrap();
8082
8083 let memory_area = manager.get_cortical_area(&dst_id).unwrap();
8084 let twin_map = memory_area
8085 .properties
8086 .get("memory_twin_areas")
8087 .and_then(|v| v.as_object())
8088 .expect("memory_twin_areas should be set");
8089 let twin_id_str = twin_map
8090 .get(&src_id.as_base_64())
8091 .and_then(|v| v.as_str())
8092 .expect("Missing twin entry for upstream area");
8093 let twin_id = CorticalID::try_from_base_64(twin_id_str).unwrap();
8094 let mapping = memory_area
8095 .properties
8096 .get("cortical_mapping_dst")
8097 .and_then(|v| v.as_object())
8098 .and_then(|map| map.get(&twin_id.as_base_64()))
8099 .and_then(|v| v.as_array())
8100 .expect("Missing memory replay mapping for twin area");
8101 let uses_replay = mapping.iter().any(|rule| {
8102 rule.get("morphology_id")
8103 .and_then(|v| v.as_str())
8104 .is_some_and(|id| id == "memory_replay")
8105 });
8106 assert!(uses_replay, "Expected memory_replay mapping for twin area");
8107
8108 let twin_area = manager.get_cortical_area(&twin_id).unwrap();
8109 assert!(matches!(
8110 twin_area.cortical_type,
8111 CorticalAreaType::Custom(_)
8112 ));
8113 assert_eq!(
8114 twin_area
8115 .properties
8116 .get("memory_twin_of")
8117 .and_then(|v| v.as_str()),
8118 Some(src_id.as_base_64().as_str())
8119 );
8120 assert_eq!(
8121 twin_area
8122 .properties
8123 .get("memory_twin_for")
8124 .and_then(|v| v.as_str()),
8125 Some(dst_id.as_base_64().as_str())
8126 );
8127 }
8128
8129 #[test]
8130 fn test_associative_memory_between_memory_areas_creates_synapses() {
8131 use crate::models::cortical_area::CorticalArea;
8132 use feagi_npu_burst_engine::backend::CPUBackend;
8133 use feagi_npu_burst_engine::TracingMutex;
8134 use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
8135 use feagi_npu_runtime::StdRuntime;
8136 use feagi_structures::genomic::cortical_area::{
8137 CorticalAreaDimensions, CorticalAreaType, CorticalID, MemoryCorticalType,
8138 };
8139 use std::sync::Arc;
8140
8141 let runtime = StdRuntime;
8142 let backend = CPUBackend::new();
8143 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
8144 let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
8145 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
8146 feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
8147
8148 let m1_id = CorticalID::try_from_bytes(b"mmem0402").unwrap();
8149 let m2_id = CorticalID::try_from_bytes(b"mmem0403").unwrap();
8150
8151 let mut m1_area = CorticalArea::new(
8152 m1_id,
8153 0,
8154 "Memory M1".to_string(),
8155 CorticalAreaDimensions::new(1, 1, 1).unwrap(),
8156 (0, 0, 0).into(),
8157 CorticalAreaType::Memory(MemoryCorticalType::Memory),
8158 )
8159 .unwrap();
8160 m1_area
8161 .properties
8162 .insert("is_mem_type".to_string(), serde_json::json!(true));
8163 m1_area
8164 .properties
8165 .insert("temporal_depth".to_string(), serde_json::json!(1));
8166
8167 let mut m2_area = CorticalArea::new(
8168 m2_id,
8169 0,
8170 "Memory M2".to_string(),
8171 CorticalAreaDimensions::new(1, 1, 1).unwrap(),
8172 (0, 0, 0).into(),
8173 CorticalAreaType::Memory(MemoryCorticalType::Memory),
8174 )
8175 .unwrap();
8176 m2_area
8177 .properties
8178 .insert("is_mem_type".to_string(), serde_json::json!(true));
8179 m2_area
8180 .properties
8181 .insert("temporal_depth".to_string(), serde_json::json!(1));
8182
8183 manager.add_cortical_area(m1_area).unwrap();
8184 manager.add_cortical_area(m2_area).unwrap();
8185
8186 manager
8187 .add_neuron(&m1_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
8188 .unwrap();
8189 manager
8190 .add_neuron(&m2_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
8191 .unwrap();
8192
8193 let mapping_data = vec![serde_json::json!({
8194 "morphology_id": "associative_memory",
8195 "morphology_scalar": 1,
8196 "postSynapticCurrent_multiplier": 1.0,
8197 "plasticity_flag": true,
8198 "plasticity_constant": 1,
8199 "ltp_multiplier": 1,
8200 "ltd_multiplier": 1,
8201 "plasticity_window": 5,
8202 })];
8203 manager
8204 .update_cortical_mapping(&m1_id, &m2_id, mapping_data)
8205 .unwrap();
8206 let created = manager
8207 .regenerate_synapses_for_mapping(&m1_id, &m2_id)
8208 .unwrap();
8209 assert!(
8210 created > 0,
8211 "Expected associative memory mapping between memory areas to create synapses"
8212 );
8213 let npu_guard = dyn_npu.lock().unwrap();
8214 let assoc_tagged =
8215 npu_guard.count_synapses_with_edge_flag_bits(SYNAPSE_EDGE_ASSOCIATIVE_MEMORY);
8216 assert!(
8217 assoc_tagged >= 1,
8218 "associative_memory connectome path should stamp SYNAPSE_EDGE_ASSOCIATIVE_MEMORY on created synapses"
8219 );
8220 }
8221
8222 #[test]
8223 fn test_memory_twin_repair_on_load_preserves_replay_mapping() {
8224 use crate::models::cortical_area::CorticalArea;
8225 use feagi_npu_burst_engine::backend::CPUBackend;
8226 use feagi_npu_burst_engine::TracingMutex;
8227 use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
8228 use feagi_npu_runtime::StdRuntime;
8229 use feagi_structures::genomic::cortical_area::{
8230 CorticalAreaDimensions, CorticalAreaType, CorticalID, IOCorticalAreaConfigurationFlag,
8231 MemoryCorticalType,
8232 };
8233 use std::sync::Arc;
8234
8235 let runtime = StdRuntime;
8236 let backend = CPUBackend::new();
8237 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
8238 let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
8239 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
8240 feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
8241
8242 let src_id = CorticalID::try_from_bytes(b"csrc0002").unwrap();
8243 let mem_id = CorticalID::try_from_bytes(b"mmem0002").unwrap();
8244
8245 let src_area = CorticalArea::new(
8246 src_id,
8247 0,
8248 "Source Area".to_string(),
8249 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
8250 (0, 0, 0).into(),
8251 CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
8252 )
8253 .unwrap();
8254 let mut mem_area = CorticalArea::new(
8255 mem_id,
8256 0,
8257 "Memory Area".to_string(),
8258 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
8259 (0, 0, 0).into(),
8260 CorticalAreaType::Memory(MemoryCorticalType::Memory),
8261 )
8262 .unwrap();
8263 mem_area
8264 .properties
8265 .insert("is_mem_type".to_string(), serde_json::json!(true));
8266 mem_area
8267 .properties
8268 .insert("temporal_depth".to_string(), serde_json::json!(1));
8269
8270 manager.add_cortical_area(src_area).unwrap();
8271 manager.add_cortical_area(mem_area).unwrap();
8272
8273 let twin_id = manager
8274 .build_memory_twin_id(&mem_id, &src_id)
8275 .expect("Failed to build twin id");
8276 let twin_area = CorticalArea::new(
8277 twin_id,
8278 0,
8279 "Source Area_twin".to_string(),
8280 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
8281 (0, 0, 0).into(),
8282 CorticalAreaType::Custom(
8283 feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
8284 ),
8285 )
8286 .unwrap();
8287 manager.add_cortical_area(twin_area).unwrap();
8288
8289 let repaired = manager
8290 .ensure_memory_twin_area(&mem_id, &src_id)
8291 .expect("Failed to repair twin");
8292 assert_eq!(repaired, twin_id);
8293
8294 let mem_area = manager.get_cortical_area(&mem_id).unwrap();
8295 let twin_map = mem_area
8296 .properties
8297 .get("memory_twin_areas")
8298 .and_then(|v| v.as_object())
8299 .expect("memory_twin_areas should be set");
8300 let twin_id_str = twin_map
8301 .get(&src_id.as_base_64())
8302 .and_then(|v| v.as_str())
8303 .expect("Missing twin entry for upstream area");
8304 assert_eq!(twin_id_str, twin_id.as_base_64());
8305
8306 let replay_map = mem_area
8307 .properties
8308 .get("cortical_mapping_dst")
8309 .and_then(|v| v.as_object())
8310 .and_then(|map| map.get(&twin_id.as_base_64()))
8311 .and_then(|v| v.as_array())
8312 .expect("Missing memory replay mapping for twin area");
8313 let uses_replay = replay_map.iter().any(|rule| {
8314 rule.get("morphology_id")
8315 .and_then(|v| v.as_str())
8316 .is_some_and(|id| id == "memory_replay")
8317 });
8318 assert!(uses_replay, "Expected memory_replay mapping for twin area");
8319
8320 let twin_area = manager.get_cortical_area(&twin_id).unwrap();
8321 assert_eq!(
8322 twin_area
8323 .properties
8324 .get("memory_twin_of")
8325 .and_then(|v| v.as_str()),
8326 Some(src_id.as_base_64().as_str())
8327 );
8328 assert_eq!(
8329 twin_area
8330 .properties
8331 .get("memory_twin_for")
8332 .and_then(|v| v.as_str()),
8333 Some(mem_id.as_base_64().as_str())
8334 );
8335 }
8336
8337 #[test]
8338 fn test_memory_twin_inherits_upstream_parent_region() {
8339 use crate::models::cortical_area::CorticalArea;
8340 use crate::models::BrainRegion;
8341 use feagi_npu_burst_engine::backend::CPUBackend;
8342 use feagi_npu_burst_engine::TracingMutex;
8343 use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
8344 use feagi_npu_runtime::StdRuntime;
8345 use feagi_structures::genomic::brain_regions::{RegionID, RegionType};
8346 use feagi_structures::genomic::cortical_area::{
8347 CorticalAreaDimensions, CorticalAreaType, CorticalID, IOCorticalAreaConfigurationFlag,
8348 MemoryCorticalType,
8349 };
8350 use std::sync::Arc;
8351
8352 let runtime = StdRuntime;
8353 let backend = CPUBackend::new();
8354 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
8355 let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
8356 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
8357 feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
8358 let src_region_id = RegionID::new();
8359 let src_region_id_str = src_region_id.to_string();
8360 manager
8361 .add_brain_region(
8362 BrainRegion::new(
8363 src_region_id,
8364 "Src Region".to_string(),
8365 RegionType::Undefined,
8366 )
8367 .unwrap(),
8368 None,
8369 )
8370 .unwrap();
8371 let mem_region_id = RegionID::new();
8372 let mem_region_id_str = mem_region_id.to_string();
8373 manager
8374 .add_brain_region(
8375 BrainRegion::new(
8376 mem_region_id,
8377 "Mem Region".to_string(),
8378 RegionType::Undefined,
8379 )
8380 .unwrap(),
8381 None,
8382 )
8383 .unwrap();
8384
8385 let src_id = CorticalID::try_from_bytes(b"csrc1001").unwrap();
8386 let mem_id = CorticalID::try_from_bytes(b"mmem1001").unwrap();
8387
8388 let mut src_area = CorticalArea::new(
8389 src_id,
8390 0,
8391 "Origin".to_string(),
8392 CorticalAreaDimensions::new(4, 4, 1).unwrap(),
8393 (10, 20, 0).into(),
8394 CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
8395 )
8396 .unwrap();
8397 src_area.properties.insert(
8398 "parent_region_id".to_string(),
8399 serde_json::json!(src_region_id_str.clone()),
8400 );
8401
8402 let mut mem_area = CorticalArea::new(
8403 mem_id,
8404 0,
8405 "Memory".to_string(),
8406 CorticalAreaDimensions::new(2, 2, 1).unwrap(),
8407 (200, 300, 0).into(),
8408 CorticalAreaType::Memory(MemoryCorticalType::Memory),
8409 )
8410 .unwrap();
8411 mem_area
8412 .properties
8413 .insert("is_mem_type".to_string(), serde_json::json!(true));
8414 mem_area
8415 .properties
8416 .insert("temporal_depth".to_string(), serde_json::json!(1));
8417 mem_area.properties.insert(
8418 "parent_region_id".to_string(),
8419 serde_json::json!(mem_region_id_str.clone()),
8420 );
8421
8422 manager.add_cortical_area(src_area).unwrap();
8423 manager.add_cortical_area(mem_area).unwrap();
8424
8425 let mapping_data = vec![serde_json::json!({
8426 "morphology_id": "episodic_memory",
8427 "morphology_scalar": 1,
8428 "postSynapticCurrent_multiplier": 1.0,
8429 })];
8430 manager
8431 .update_cortical_mapping(&src_id, &mem_id, mapping_data)
8432 .unwrap();
8433 manager
8434 .regenerate_synapses_for_mapping(&src_id, &mem_id)
8435 .unwrap();
8436
8437 let memory_area = manager.get_cortical_area(&mem_id).unwrap();
8438 let twin_map = memory_area
8439 .properties
8440 .get("memory_twin_areas")
8441 .and_then(|v| v.as_object())
8442 .expect("memory_twin_areas should be set");
8443 let twin_id = CorticalID::try_from_base_64(
8444 twin_map
8445 .get(&src_id.as_base_64())
8446 .and_then(|v| v.as_str())
8447 .expect("missing twin for source"),
8448 )
8449 .unwrap();
8450 let twin_area = manager.get_cortical_area(&twin_id).unwrap();
8451
8452 assert_eq!(
8453 twin_area
8454 .properties
8455 .get("parent_region_id")
8456 .and_then(|v| v.as_str()),
8457 Some(src_region_id_str.as_str()),
8458 "Twin should inherit upstream/source parent region"
8459 );
8460 assert_ne!(
8461 twin_area
8462 .properties
8463 .get("parent_region_id")
8464 .and_then(|v| v.as_str()),
8465 Some(mem_region_id_str.as_str()),
8466 "Twin must not inherit memory area's parent region"
8467 );
8468 }
8469
8470 #[test]
8471 fn test_memory_twin_diagnostic_explains_memory_upstream_blocker() {
8472 use crate::models::cortical_area::CorticalArea;
8473 use feagi_npu_burst_engine::backend::CPUBackend;
8474 use feagi_npu_burst_engine::TracingMutex;
8475 use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
8476 use feagi_npu_runtime::StdRuntime;
8477 use feagi_structures::genomic::cortical_area::{
8478 CorticalAreaDimensions, CorticalAreaType, CorticalID, MemoryCorticalType,
8479 };
8480 use std::sync::Arc;
8481
8482 let runtime = StdRuntime;
8483 let backend = CPUBackend::new();
8484 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
8485 let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
8486 let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
8487 feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
8488
8489 let m1_id = CorticalID::try_from_bytes(b"mmem2001").unwrap();
8490 let m2_id = CorticalID::try_from_bytes(b"mmem2002").unwrap();
8491
8492 let mut m1_area = CorticalArea::new(
8493 m1_id,
8494 0,
8495 "Memory A".to_string(),
8496 CorticalAreaDimensions::new(1, 1, 1).unwrap(),
8497 (0, 0, 0).into(),
8498 CorticalAreaType::Memory(MemoryCorticalType::Memory),
8499 )
8500 .unwrap();
8501 m1_area
8502 .properties
8503 .insert("is_mem_type".to_string(), serde_json::json!(true));
8504 m1_area
8505 .properties
8506 .insert("temporal_depth".to_string(), serde_json::json!(1));
8507
8508 let mut m2_area = CorticalArea::new(
8509 m2_id,
8510 0,
8511 "Memory B".to_string(),
8512 CorticalAreaDimensions::new(1, 1, 1).unwrap(),
8513 (0, 0, 0).into(),
8514 CorticalAreaType::Memory(MemoryCorticalType::Memory),
8515 )
8516 .unwrap();
8517 m2_area
8518 .properties
8519 .insert("is_mem_type".to_string(), serde_json::json!(true));
8520 m2_area
8521 .properties
8522 .insert("temporal_depth".to_string(), serde_json::json!(1));
8523
8524 manager.add_cortical_area(m1_area).unwrap();
8525 manager.add_cortical_area(m2_area).unwrap();
8526
8527 let mapping_data = vec![serde_json::json!({
8528 "morphology_id": "episodic_memory",
8529 "morphology_scalar": 1,
8530 "postSynapticCurrent_multiplier": 1.0,
8531 })];
8532 manager
8533 .update_cortical_mapping(&m1_id, &m2_id, mapping_data)
8534 .unwrap();
8535 manager
8536 .regenerate_synapses_for_mapping(&m1_id, &m2_id)
8537 .unwrap();
8538
8539 let diagnostic = manager
8540 .diagnose_memory_twin_for_mapping(&m1_id, &m2_id)
8541 .expect("diagnostic should succeed");
8542 assert!(diagnostic.mapping_exists);
8543 assert_eq!(diagnostic.episodic_rule_count, 1);
8544 assert!(!diagnostic.twin_expected);
8545 assert!(!diagnostic.twin_present);
8546 assert_eq!(
8547 diagnostic.reason.as_deref(),
8548 Some("upstream_is_memory_area_twin_not_supported")
8549 );
8550 }
8551
8552 fn build_max_weight_test_manager() -> (
8557 ConnectomeManager,
8558 CorticalID,
8559 CorticalID,
8560 CorticalID,
8561 CorticalID,
8562 ) {
8563 use feagi_npu_burst_engine::backend::CPUBackend;
8564 use feagi_npu_burst_engine::TracingMutex;
8565 use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
8566 use feagi_npu_runtime::StdRuntime;
8567 use feagi_structures::genomic::cortical_area::{
8568 CorticalAreaType, IOCorticalAreaConfigurationFlag,
8569 };
8570
8571 let runtime = StdRuntime;
8572 let backend = CPUBackend::new();
8573 let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("npu");
8574 let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
8575 let mut mgr = ConnectomeManager::new_for_testing_with_npu(dyn_npu);
8576 feagi_evolutionary::templates::add_core_morphologies(&mut mgr.morphology_registry);
8580
8581 let src = CorticalID::try_from_bytes(b"cstmwsrc").unwrap();
8582 let dst = CorticalID::try_from_bytes(b"cstmwdst").unwrap();
8583 let reward = CorticalID::try_from_bytes(b"cstmwrwd").unwrap();
8584 let pain = CorticalID::try_from_bytes(b"cstmwpan").unwrap();
8585
8586 for (id, label, kind) in [
8587 (
8588 src,
8589 "src",
8590 CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
8591 ),
8592 (
8593 dst,
8594 "dst",
8595 CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
8596 ),
8597 (
8598 reward,
8599 "reward",
8600 CorticalAreaType::Custom(
8601 feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
8602 ),
8603 ),
8604 (
8605 pain,
8606 "pain",
8607 CorticalAreaType::Custom(
8608 feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
8609 ),
8610 ),
8611 ] {
8612 mgr.add_cortical_area(
8613 CorticalArea::new(
8614 id,
8615 0,
8616 label.to_string(),
8617 CorticalAreaDimensions::new(1, 1, 1).unwrap(),
8618 (0, 0, 0).into(),
8619 kind,
8620 )
8621 .unwrap(),
8622 )
8623 .unwrap();
8624 mgr.add_neuron(&id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
8625 .unwrap();
8626 }
8627 (mgr, src, dst, reward, pain)
8628 }
8629
8630 fn write_and_regenerate_mapping(
8635 mgr: &mut ConnectomeManager,
8636 src: &CorticalID,
8637 dst: &CorticalID,
8638 rule: serde_json::Value,
8639 ) -> BduResult<usize> {
8640 mgr.update_cortical_mapping(src, dst, vec![rule])?;
8641 mgr.regenerate_synapses_for_mapping(src, dst)
8642 }
8643
8644 #[test]
8648 fn test_max_weight_finite_positive_accepted_on_rstdp_rule() {
8649 let (mut mgr, src, dst, reward, pain) = build_max_weight_test_manager();
8650
8651 let result = write_and_regenerate_mapping(
8652 &mut mgr,
8653 &src,
8654 &dst,
8655 serde_json::json!({
8656 "morphology_id": "block_to_block",
8657 "morphology_scalar": [1, 1, 1],
8658 "postSynapticCurrent_multiplier": 1,
8659 "plasticity_flag": true,
8660 "plasticity_constant": 1,
8661 "ltp_multiplier": 1,
8662 "ltd_multiplier": 1,
8663 "plasticity_window": 10,
8664 "synaptic_delay_bursts": 1,
8665 "plasticity_mode": "rstdp",
8666 "eligibility_decay_bursts": 50,
8667 "reward_source_area": reward.as_base_64(),
8668 "punishment_source_area": pain.as_base_64(),
8669 "max_weight": 12.5,
8670 }),
8671 );
8672 assert!(
8673 result.is_ok(),
8674 "valid max_weight=12.5 must be accepted, got {:?}",
8675 result
8676 );
8677 }
8678
8679 #[test]
8684 fn test_max_weight_invalid_values_rejected() {
8685 for bad in &[
8686 serde_json::json!(0.0),
8687 serde_json::json!(-1.5),
8688 serde_json::json!("not_a_number"),
8689 ] {
8690 let (mut mgr, src, dst, reward, pain) = build_max_weight_test_manager();
8691 let result = write_and_regenerate_mapping(
8692 &mut mgr,
8693 &src,
8694 &dst,
8695 serde_json::json!({
8696 "morphology_id": "block_to_block",
8697 "morphology_scalar": [1, 1, 1],
8698 "postSynapticCurrent_multiplier": 1,
8699 "plasticity_flag": true,
8700 "plasticity_constant": 1,
8701 "ltp_multiplier": 1,
8702 "ltd_multiplier": 1,
8703 "plasticity_window": 10,
8704 "synaptic_delay_bursts": 1,
8705 "plasticity_mode": "rstdp",
8706 "eligibility_decay_bursts": 50,
8707 "reward_source_area": reward.as_base_64(),
8708 "punishment_source_area": pain.as_base_64(),
8709 "max_weight": bad,
8710 }),
8711 );
8712 assert!(
8713 result.is_err(),
8714 "max_weight={:?} should have been rejected, got {:?}",
8715 bad,
8716 result
8717 );
8718 }
8719 }
8720
8721 #[test]
8724 fn test_ltp_ltd_multiplier_out_of_i8_range_rejected() {
8725 let (mut mgr, src, dst, reward, pain) = build_max_weight_test_manager();
8726 let result = write_and_regenerate_mapping(
8727 &mut mgr,
8728 &src,
8729 &dst,
8730 serde_json::json!({
8731 "morphology_id": "block_to_block",
8732 "morphology_scalar": [1, 1, 1],
8733 "postSynapticCurrent_multiplier": 1,
8734 "plasticity_flag": true,
8735 "plasticity_constant": 1,
8736 "ltp_multiplier": 200,
8737 "ltd_multiplier": 1,
8738 "plasticity_window": 10,
8739 "synaptic_delay_bursts": 1,
8740 "plasticity_mode": "rstdp",
8741 "eligibility_decay_bursts": 50,
8742 "reward_source_area": reward.as_base_64(),
8743 "punishment_source_area": pain.as_base_64(),
8744 }),
8745 );
8746 assert!(
8747 result.is_err(),
8748 "ltp_multiplier=200 must be rejected (i8 range); got {:?}",
8749 result
8750 );
8751 }
8752
8753 #[test]
8756 fn test_max_weight_rejected_when_plasticity_off() {
8757 let (mut mgr, src, dst, _reward, _pain) = build_max_weight_test_manager();
8758
8759 let result = write_and_regenerate_mapping(
8760 &mut mgr,
8761 &src,
8762 &dst,
8763 serde_json::json!({
8764 "morphology_id": "block_to_block",
8765 "morphology_scalar": [1, 1, 1],
8766 "postSynapticCurrent_multiplier": 1,
8767 "plasticity_flag": true,
8772 "plasticity_constant": 0,
8773 "ltp_multiplier": 0,
8774 "ltd_multiplier": 0,
8775 "plasticity_window": 0,
8776 "synaptic_delay_bursts": 1,
8777 "plasticity_mode": "off",
8778 "max_weight": 10.0,
8779 }),
8780 );
8781 assert!(
8782 result.is_err(),
8783 "max_weight on off-mode rule must be rejected; got {:?}",
8784 result
8785 );
8786 }
8787
8788 #[test]
8789 fn test_plasticity_eta_rejected_when_plasticity_off() {
8790 let (mut mgr, src, dst, _reward, _pain) = build_max_weight_test_manager();
8791
8792 let result = write_and_regenerate_mapping(
8793 &mut mgr,
8794 &src,
8795 &dst,
8796 serde_json::json!({
8797 "morphology_id": "block_to_block",
8798 "morphology_scalar": [1, 1, 1],
8799 "postSynapticCurrent_multiplier": 1,
8800 "plasticity_flag": true,
8801 "plasticity_constant": 0,
8802 "ltp_multiplier": 0,
8803 "ltd_multiplier": 0,
8804 "plasticity_window": 0,
8805 "synaptic_delay_bursts": 1,
8806 "plasticity_mode": "off",
8807 "plasticity_eta": 0.5,
8808 }),
8809 );
8810 assert!(
8811 result.is_err(),
8812 "plasticity_eta on off-mode rule must be rejected; got {:?}",
8813 result
8814 );
8815 }
8816
8817 #[test]
8818 fn test_plasticity_eta_non_positive_rejected() {
8819 let (mut mgr, src, dst, reward, pain) = build_max_weight_test_manager();
8820
8821 let result = write_and_regenerate_mapping(
8822 &mut mgr,
8823 &src,
8824 &dst,
8825 serde_json::json!({
8826 "morphology_id": "block_to_block",
8827 "morphology_scalar": [1, 1, 1],
8828 "postSynapticCurrent_multiplier": 1,
8829 "plasticity_flag": true,
8830 "plasticity_constant": 1,
8831 "ltp_multiplier": 1,
8832 "ltd_multiplier": 1,
8833 "plasticity_window": 10,
8834 "synaptic_delay_bursts": 1,
8835 "plasticity_mode": "rstdp",
8836 "eligibility_decay_bursts": 50,
8837 "reward_source_area": reward.as_base_64(),
8838 "punishment_source_area": pain.as_base_64(),
8839 "plasticity_eta": 0.0,
8840 }),
8841 );
8842 assert!(
8843 result.is_err(),
8844 "plasticity_eta=0 must be rejected; got {:?}",
8845 result
8846 );
8847 }
8848}