feagi-npu-plasticity 0.0.13

FEAGI plasticity algorithms - STDP and memory formation
Documentation
// Copyright 2025 Neuraville Inc.
// SPDX-License-Identifier: Apache-2.0

//! Plasticity Executor Abstraction Layer
//!
//! Provides backend-agnostic execution models for plasticity computation:
//! - AsyncPlasticityExecutor: For std environments (CPU, CUDA, WebGPU)
//! - SyncPlasticityExecutor: For no_std environments (RTOS, embedded) [Future]
//!
//! This abstraction allows FEAGI to run plasticity optimally on different backends
//! without code duplication or runtime overhead.

use crate::memory_neuron_array::MemoryNeuronDetail;
use crate::memory_stats_cache::MemoryStatsCache;
use crate::service::{
    MemoryCorticalAreaRuntimeInfo, PlasticityCommand, PlasticityConfig, PlasticityService,
};
use std::sync::{Arc, Mutex};
use tracing::{debug, warn};

/// Trait for executing plasticity computation
///
/// Implementations of this trait define how plasticity is executed on different backends.
/// - Async implementations run plasticity in a background thread (std environments)
/// - Sync implementations run plasticity inline in burst loop (no_std environments)
pub trait PlasticityExecutor: Send + Sync {
    /// Notify the executor of a completed burst
    ///
    /// # Arguments
    /// * `timestep` - The timestep/burst count that just completed
    fn notify_burst(&self, timestep: u64);

    /// Drain pending commands from the executor
    ///
    /// Commands are generated by plasticity computation and need to be
    /// applied to the NPU (e.g., inject memory neurons to FCL).
    ///
    /// # Returns
    /// Vector of plasticity commands to execute
    fn drain_commands(&self) -> Vec<PlasticityCommand>;

    /// Register a memory area with the plasticity system
    ///
    /// # Arguments
    /// * `area_idx` - Cortical area index
    /// * `area_name` - Human-readable name for the area
    /// * `temporal_depth` - Number of historical timesteps to consider
    /// * `upstream_areas` - Cortical indices of areas that project to this memory area
    /// * `lifecycle_config` - Optional lifecycle configuration for memory neurons
    /// * `mp_learning_enabled` - Whether to capture membrane potentials in replay frames
    fn register_memory_area(
        &self,
        area_idx: u32,
        area_name: String,
        temporal_depth: u32,
        upstream_areas: Vec<u32>,
        lifecycle_config: Option<crate::MemoryNeuronLifecycleConfig>,
        mp_learning_enabled: bool,
    );

    /// Start the executor (for async implementations)
    ///
    /// Default implementation is no-op (for sync implementations).
    fn start(&mut self) {}

    /// Stop the executor (for async implementations)
    ///
    /// Default implementation is no-op (for sync implementations).
    fn stop(&mut self) {}

    /// Check if the executor is running
    fn is_running(&self) -> bool {
        false // Default for sync implementations
    }
}

/// Async plasticity executor for std environments
///
/// Runs plasticity computation in a background thread, allowing the burst loop
/// to continue processing while patterns are detected and memory neurons created.
///
/// This is optimal for CPU/GPU backends where:
/// - FireLedger is CPU-resident
/// - Plasticity computation can overlap with GPU burst processing
/// - Thread overhead is acceptable
pub struct AsyncPlasticityExecutor {
    /// The plasticity service instance
    service: Arc<Mutex<Option<PlasticityService>>>,

    /// Memory stats cache (shared with health check)
    memory_stats_cache: MemoryStatsCache,

    /// Configuration for the plasticity service
    _config: PlasticityConfig,

    /// Running state
    running: bool,
}

impl AsyncPlasticityExecutor {
    /// Create a new async plasticity executor
    pub fn new(
        config: PlasticityConfig,
        memory_stats_cache: MemoryStatsCache,
        npu: Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
    ) -> Self {
        let service = PlasticityService::new(config.clone(), memory_stats_cache.clone(), npu);

        Self {
            service: Arc::new(Mutex::new(Some(service))),
            memory_stats_cache,
            _config: config,
            running: false,
        }
    }

    /// Get the memory stats cache
    pub fn get_memory_stats_cache(&self) -> MemoryStatsCache {
        self.memory_stats_cache.clone()
    }

    /// Get a reference to the underlying PlasticityService (for RuntimeService wiring)
    pub fn get_service(&self) -> Option<PlasticityService> {
        self.service.lock().ok()?.as_ref().cloned()
    }

    pub fn enqueue_commands_for_test(&self, commands: Vec<crate::PlasticityCommand>) {
        if let Some(service) = self.service.lock().unwrap().as_ref() {
            service.enqueue_commands_for_test(commands);
        }
    }

    /// ST/LTM counts and upstream pattern cache size for a memory cortical index.
    pub fn memory_cortical_area_runtime_info(
        &self,
        cortical_idx: u32,
    ) -> Option<MemoryCorticalAreaRuntimeInfo> {
        self.service
            .lock()
            .ok()?
            .as_ref()
            .map(|s| s.memory_cortical_area_runtime_info(cortical_idx))
    }

    /// Plasticity snapshot for a memory neuron by global id.
    pub fn memory_neuron_detail(&self, neuron_id: u32) -> Option<MemoryNeuronDetail> {
        self.service
            .lock()
            .ok()?
            .as_ref()
            .and_then(|s| s.memory_neuron_detail(neuron_id))
    }

    /// Active memory neuron ids for a cortical area (sorted), paginated: `(page, total)`.
    pub fn paginated_memory_neuron_ids_in_area(
        &self,
        cortical_idx: u32,
        offset: usize,
        limit: usize,
    ) -> Option<(Vec<u32>, usize)> {
        let guard = self.service.lock().ok()?;
        let service = guard.as_ref()?;
        let array_arc = service.get_memory_neuron_array();
        let array = array_arc.lock().ok()?;
        Some(array.paginated_neuron_ids_in_area(cortical_idx, offset, limit))
    }
}

impl PlasticityExecutor for AsyncPlasticityExecutor {
    fn notify_burst(&self, timestep: u64) {
        // trace!("[PLASTICITY-EXEC] 🔔 notify_burst({}) called", timestep);
        if let Some(service) = self.service.lock().unwrap().as_ref() {
            service.notify_burst(timestep);
        } else {
            warn!("[PLASTICITY-EXEC] ⚠️ Service is None, cannot notify");
        }
    }

    fn drain_commands(&self) -> Vec<PlasticityCommand> {
        if let Some(service) = self.service.lock().unwrap().as_ref() {
            let drained = service.drain_commands();
            if !drained.is_empty() {
                debug!(
                    target: "plasticity",
                    "[PLASTICITY-EXEC] Drained {} command(s) from executor",
                    drained.len()
                );
                for command in &drained {
                    match command {
                        PlasticityCommand::RegisterMemoryNeuron {
                            neuron_id,
                            area_idx,
                            ..
                        } => {
                            debug!(
                                target: "plasticity",
                                "[PLASTICITY-EXEC] RegisterMemoryNeuron area={} neuron_id={}",
                                area_idx,
                                neuron_id
                            );
                        }
                        PlasticityCommand::MemoryNeuronConvertedToLtm {
                            neuron_id,
                            area_idx,
                            ..
                        } => {
                            debug!(
                                target: "plasticity",
                                "[PLASTICITY-EXEC] MemoryNeuronConvertedToLtm area={} neuron_id={}",
                                area_idx,
                                neuron_id
                            );
                        }
                        PlasticityCommand::InjectMemoryNeuronToFCL {
                            neuron_id,
                            area_idx,
                            is_reactivation,
                            replay_frames,
                            ..
                        } => {
                            debug!(
                                target: "plasticity",
                                "[PLASTICITY-EXEC] InjectMemoryNeuronToFCL area={} neuron_id={} reactivation={} replay_frames={}",
                                area_idx,
                                neuron_id,
                                is_reactivation,
                                replay_frames.len()
                            );
                        }
                        PlasticityCommand::UpdateWeightsDelta { .. } => {}
                        PlasticityCommand::UpdateStateCounters { .. } => {}
                        PlasticityCommand::ResetMemoryNeuronsInArea { cortical_idx } => {
                            debug!(
                                target: "plasticity",
                                "[PLASTICITY-EXEC] ResetMemoryNeuronsInArea cortical_idx={}",
                                cortical_idx
                            );
                        }
                    }
                }
            }
            drained
        } else {
            Vec::new()
        }
    }

    fn register_memory_area(
        &self,
        area_idx: u32,
        area_name: String,
        temporal_depth: u32,
        upstream_areas: Vec<u32>,
        lifecycle_config: Option<crate::MemoryNeuronLifecycleConfig>,
        mp_learning_enabled: bool,
    ) {
        if let Some(service) = self.service.lock().unwrap().as_mut() {
            service.register_memory_area(
                area_idx,
                area_name,
                temporal_depth,
                upstream_areas,
                lifecycle_config,
                mp_learning_enabled,
            );
        }
    }

    fn start(&mut self) {
        if self.running {
            tracing::warn!(target: "plasticity", "⚠️  PlasticityExecutor already running");
            return;
        }

        if let Some(service) = self.service.lock().unwrap().as_ref() {
            tracing::info!(target: "plasticity", "🚀 Initializing AsyncPlasticityExecutor...");
            service.start();
            self.running = true;
            tracing::info!(target: "plasticity",
                "✅ AsyncPlasticityExecutor started successfully - ready to monitor memory areas and process STDP"
            );
        } else {
            tracing::error!(target: "plasticity", "❌ Failed to start PlasticityExecutor - service not initialized");
        }
    }

    fn stop(&mut self) {
        if !self.running {
            return;
        }

        if let Some(service) = self.service.lock().unwrap().as_ref() {
            service.stop();
            self.running = false;
            tracing::info!("Stopped async plasticity executor");
        }
    }

    fn is_running(&self) -> bool {
        self.running
    }
}

// ============================================================================
// FUTURE: Sync Executor for no_std Environments
// ============================================================================

/// Sync plasticity executor for no_std environments
///
/// **NOT YET IMPLEMENTED** - Placeholder for future embedded/RTOS support.
///
/// This would run plasticity computation synchronously in the burst loop,
/// blocking until pattern detection and memory neuron creation completes.
///
/// Optimal for embedded environments where:
/// - No std::thread available
/// - Memory is constrained (no separate thread stack)
/// - Deterministic timing is critical
///
/// To implement:
/// 1. Create no_std-compatible PlasticityService variant
/// 2. Use spin::Mutex or critical_section::Mutex
/// 3. Call compute_plasticity() directly in execute()
#[cfg(not(feature = "std"))]
pub struct SyncPlasticityExecutor {
    // TODO: Implement for embedded support
    _marker: core::marker::PhantomData<()>,
}

#[cfg(not(feature = "std"))]
impl PlasticityExecutor for SyncPlasticityExecutor {
    fn notify_burst(&self, _timestep: u64) {
        unimplemented!("SyncPlasticityExecutor not yet implemented");
    }

    fn drain_commands(&self) -> Vec<PlasticityCommand> {
        unimplemented!("SyncPlasticityExecutor not yet implemented");
    }

    fn register_memory_area(
        &self,
        _area_idx: u32,
        _area_name: String,
        _temporal_depth: u32,
        _upstream_areas: Vec<u32>,
        _lifecycle_config: Option<crate::MemoryNeuronLifecycleConfig>,
        _mp_learning_enabled: bool,
    ) {
        unimplemented!("SyncPlasticityExecutor not yet implemented");
    }
}