scirs2_spatial/neuromorphic/algorithms/
processing.rs

1//! Neuromorphic Event Processing
2//!
3//! This module implements general-purpose neuromorphic processing capabilities
4//! including event-driven computation, memristive crossbar arrays, and temporal
5//! dynamics for spatial data processing.
6
7use crate::error::{SpatialError, SpatialResult};
8use scirs2_core::ndarray::{Array2, ArrayView2};
9use scirs2_core::random::Rng;
10use std::collections::{HashMap, VecDeque};
11
12// Import core neuromorphic components
13use super::super::core::SpikeEvent;
14
15/// Neuromorphic processor for general spatial computations
16///
17/// This processor implements event-driven neuromorphic computing paradigms
18/// for spatial data processing. It supports memristive crossbar arrays for
19/// in-memory computation and temporal coding schemes for efficient information
20/// processing.
21///
22/// # Features
23/// - Event-driven processing pipeline
24/// - Memristive crossbar arrays for in-memory computation
25/// - Temporal and rate-based coding schemes
26/// - Spike timing dynamics and correlation detection
27/// - Configurable processing parameters
28///
29/// # Example
30/// ```rust
31/// use scirs2_core::ndarray::Array2;
32/// use scirs2_spatial::neuromorphic::algorithms::NeuromorphicProcessor;
33///
34/// let points = Array2::from_shape_vec((3, 2), vec![
35///     0.0, 0.0, 1.0, 1.0, 2.0, 2.0
36/// ]).expect("Operation failed");
37///
38/// let mut processor = NeuromorphicProcessor::new()
39///     .with_memristive_crossbar(true)
40///     .with_temporal_coding(true)
41///     .with_crossbar_size(64, 64);
42///
43/// // Encode spatial data as neuromorphic events
44/// let events = processor.encode_spatial_events(&points.view()).expect("Operation failed");
45///
46/// // Process events through neuromorphic pipeline
47/// let processed_events = processor.process_events(&events).expect("Operation failed");
48/// ```
49#[derive(Debug, Clone)]
50pub struct NeuromorphicProcessor {
51    /// Enable memristive crossbar arrays
52    memristive_crossbar: bool,
53    /// Enable temporal coding
54    temporal_coding: bool,
55    /// Crossbar array dimensions
56    crossbar_size: (usize, usize),
57    /// Memristive device conductances
58    conductances: Array2<f64>,
59    /// Event processing pipeline
60    event_pipeline: VecDeque<SpikeEvent>,
61    /// Maximum pipeline length
62    max_pipeline_length: usize,
63    /// Crossbar threshold for spike generation
64    crossbar_threshold: f64,
65    /// Memristive learning rate
66    memristive_learning_rate: f64,
67}
68
69impl Default for NeuromorphicProcessor {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl NeuromorphicProcessor {
76    /// Create new neuromorphic processor
77    ///
78    /// # Returns
79    /// A new `NeuromorphicProcessor` with default configuration
80    pub fn new() -> Self {
81        Self {
82            memristive_crossbar: false,
83            temporal_coding: false,
84            crossbar_size: (64, 64),
85            conductances: Array2::zeros((64, 64)),
86            event_pipeline: VecDeque::new(),
87            max_pipeline_length: 1000,
88            crossbar_threshold: 0.5,
89            memristive_learning_rate: 0.001,
90        }
91    }
92
93    /// Enable memristive crossbar arrays
94    ///
95    /// When enabled, events are processed through a memristive crossbar array
96    /// that provides in-memory computation capabilities.
97    ///
98    /// # Arguments
99    /// * `enabled` - Whether to enable memristive crossbar processing
100    pub fn with_memristive_crossbar(mut self, enabled: bool) -> Self {
101        self.memristive_crossbar = enabled;
102        if enabled {
103            self.initialize_crossbar();
104        }
105        self
106    }
107
108    /// Enable temporal coding
109    ///
110    /// When enabled, spatial coordinates are encoded using spike timing
111    /// rather than spike rates.
112    ///
113    /// # Arguments
114    /// * `enabled` - Whether to enable temporal coding
115    pub fn with_temporal_coding(mut self, enabled: bool) -> Self {
116        self.temporal_coding = enabled;
117        self
118    }
119
120    /// Configure crossbar size
121    ///
122    /// Sets the dimensions of the memristive crossbar array used for
123    /// in-memory computation.
124    ///
125    /// # Arguments
126    /// * `rows` - Number of rows in crossbar array
127    /// * `cols` - Number of columns in crossbar array
128    pub fn with_crossbar_size(mut self, rows: usize, cols: usize) -> Self {
129        self.crossbar_size = (rows, cols);
130        self.conductances = Array2::zeros((rows, cols));
131        if self.memristive_crossbar {
132            self.initialize_crossbar();
133        }
134        self
135    }
136
137    /// Configure processing parameters
138    ///
139    /// # Arguments
140    /// * `max_pipeline_length` - Maximum length of event pipeline
141    /// * `crossbar_threshold` - Threshold for crossbar spike generation
142    /// * `learning_rate` - Learning rate for memristive adaptation
143    pub fn with_processing_params(
144        mut self,
145        max_pipeline_length: usize,
146        crossbar_threshold: f64,
147        learning_rate: f64,
148    ) -> Self {
149        self.max_pipeline_length = max_pipeline_length;
150        self.crossbar_threshold = crossbar_threshold;
151        self.memristive_learning_rate = learning_rate;
152        self
153    }
154
155    /// Encode spatial data as neuromorphic events
156    ///
157    /// Converts spatial data points into spike events using either temporal
158    /// coding (spike timing) or rate coding (spike frequency) depending on
159    /// the processor configuration.
160    ///
161    /// # Arguments
162    /// * `points` - Input spatial points (n_points × n_dims)
163    ///
164    /// # Returns
165    /// Vector of spike events representing the spatial data
166    pub fn encode_spatial_events(
167        &self,
168        points: &ArrayView2<'_, f64>,
169    ) -> SpatialResult<Vec<SpikeEvent>> {
170        let (n_points, n_dims) = points.dim();
171        let mut events = Vec::new();
172
173        if n_points == 0 || n_dims == 0 {
174            return Ok(events);
175        }
176
177        for (point_idx, point) in points.outer_iter().enumerate() {
178            for (dim, &coord) in point.iter().enumerate() {
179                // Temporal coding: encode coordinate as spike timing
180                let normalized_coord = (coord + 10.0) / 20.0; // Normalize to [0, 1]
181                let normalized_coord = normalized_coord.clamp(0.0, 1.0);
182
183                if self.temporal_coding {
184                    // Timing-based encoding
185                    let spike_time = normalized_coord * 100.0; // Map to [0, 100] time units
186                    let event =
187                        SpikeEvent::new(point_idx * n_dims + dim, spike_time, 1.0, point.to_vec());
188                    events.push(event);
189                } else {
190                    // Rate-based encoding
191                    let spike_rate = normalized_coord * 50.0; // Max 50 Hz
192                    let num_spikes = spike_rate as usize;
193
194                    for spike_idx in 0..num_spikes {
195                        let spike_time = if spike_rate > 0.0 {
196                            (spike_idx as f64) * (1.0 / spike_rate)
197                        } else {
198                            0.0
199                        };
200                        let event = SpikeEvent::new(
201                            point_idx * n_dims + dim,
202                            spike_time,
203                            1.0,
204                            point.to_vec(),
205                        );
206                        events.push(event);
207                    }
208                }
209            }
210        }
211
212        // Sort events by timestamp
213        events.sort_by(|a, b| {
214            a.timestamp()
215                .partial_cmp(&b.timestamp())
216                .expect("Operation failed")
217        });
218
219        Ok(events)
220    }
221
222    /// Process events through neuromorphic pipeline
223    ///
224    /// Processes spike events through the neuromorphic computing pipeline,
225    /// applying memristive crossbar processing and temporal dynamics as
226    /// configured.
227    ///
228    /// # Arguments
229    /// * `events` - Input spike events to process
230    ///
231    /// # Returns
232    /// Vector of processed spike events
233    pub fn process_events(&mut self, events: &[SpikeEvent]) -> SpatialResult<Vec<SpikeEvent>> {
234        let mut processed_events = Vec::new();
235
236        for event in events {
237            self.event_pipeline.push_back(event.clone());
238
239            // Process through memristive crossbar if enabled
240            if self.memristive_crossbar {
241                let crossbar_output = self.process_through_crossbar(event)?;
242                processed_events.extend(crossbar_output);
243            } else {
244                processed_events.push(event.clone());
245            }
246
247            // Apply temporal dynamics
248            if self.temporal_coding {
249                Self::apply_temporal_dynamics(&mut processed_events)?;
250            }
251
252            // Maintain event pipeline size
253            if self.event_pipeline.len() > self.max_pipeline_length {
254                self.event_pipeline.pop_front();
255            }
256        }
257
258        Ok(processed_events)
259    }
260
261    /// Initialize memristive crossbar array
262    ///
263    /// Initializes the conductance values of the memristive crossbar array
264    /// with random values representing the initial device states.
265    fn initialize_crossbar(&mut self) {
266        let (rows, cols) = self.crossbar_size;
267        let mut rng = scirs2_core::random::rng();
268
269        // Initialize conductances with random values
270        for i in 0..rows {
271            for j in 0..cols {
272                // Random conductance between 0.1 and 1.0 (normalized)
273                self.conductances[[i, j]] = 0.1 + rng.gen_range(0.0..0.9);
274            }
275        }
276    }
277
278    /// Process event through memristive crossbar
279    ///
280    /// Processes a spike event through the memristive crossbar array,
281    /// generating output spikes based on the crossbar conductances and
282    /// updating the memristive devices.
283    fn process_through_crossbar(&mut self, event: &SpikeEvent) -> SpatialResult<Vec<SpikeEvent>> {
284        let (rows, cols) = self.crossbar_size;
285        let mut output_events = Vec::new();
286
287        // Map input neuron to crossbar row
288        let input_row = event.neuron_id() % rows;
289
290        // Compute crossbar outputs
291        for col in 0..cols {
292            let conductance = self.conductances[[input_row, col]];
293            let output_current = event.amplitude() * conductance;
294
295            // Generate output spike if current exceeds threshold
296            if output_current > self.crossbar_threshold {
297                let output_event = SpikeEvent::new(
298                    rows + col,              // Offset for output neurons
299                    event.timestamp() + 0.1, // Small delay
300                    output_current,
301                    event.spatial_coords().to_vec(),
302                );
303                output_events.push(output_event);
304
305                // Update memristive device (Hebbian-like plasticity)
306                self.update_memristive_device(input_row, col, event.amplitude())?;
307            }
308        }
309
310        Ok(output_events)
311    }
312
313    /// Update memristive device conductance
314    ///
315    /// Updates the conductance of a memristive device based on the input
316    /// spike amplitude using a simple Hebbian-like learning rule.
317    fn update_memristive_device(
318        &mut self,
319        row: usize,
320        col: usize,
321        spike_amplitude: f64,
322    ) -> SpatialResult<()> {
323        let current_conductance = self.conductances[[row, col]];
324
325        // Simple memristive update rule
326        let conductance_change =
327            self.memristive_learning_rate * spike_amplitude * (1.0 - current_conductance);
328
329        self.conductances[[row, col]] += conductance_change;
330        self.conductances[[row, col]] = self.conductances[[row, col]].clamp(0.0, 1.0);
331
332        Ok(())
333    }
334
335    /// Apply temporal dynamics to event processing
336    ///
337    /// Applies temporal filtering and spike-timing dependent processing
338    /// to enhance temporal correlations and implement refractory periods.
339    fn apply_temporal_dynamics(events: &mut Vec<SpikeEvent>) -> SpatialResult<()> {
340        // Apply temporal filtering and spike-timing dependent processing
341        let mut filtered_events = Vec::new();
342
343        for (i, event) in events.iter().enumerate() {
344            let mut should_include = true;
345            let mut modified_event = event.clone();
346
347            // Check for temporal correlations with recent events
348            for other_event in events.iter().skip(i + 1) {
349                let time_diff = (other_event.timestamp() - event.timestamp()).abs();
350
351                if time_diff < 5.0 {
352                    // Within temporal window
353                    // Apply temporal correlation enhancement
354                    let new_amplitude = modified_event.amplitude() * 1.1;
355                    modified_event = SpikeEvent::new(
356                        modified_event.neuron_id(),
357                        modified_event.timestamp(),
358                        new_amplitude,
359                        modified_event.spatial_coords().to_vec(),
360                    );
361
362                    // Coincidence detection
363                    if time_diff < 1.0 {
364                        let enhanced_amplitude = modified_event.amplitude() * 1.5;
365                        modified_event = SpikeEvent::new(
366                            modified_event.neuron_id(),
367                            modified_event.timestamp(),
368                            enhanced_amplitude,
369                            modified_event.spatial_coords().to_vec(),
370                        );
371                    }
372                }
373
374                // Refractory period simulation
375                if time_diff < 0.5 && event.neuron_id() == other_event.neuron_id() {
376                    should_include = false; // Suppress due to refractory period
377                    break;
378                }
379            }
380
381            if should_include {
382                filtered_events.push(modified_event);
383            }
384        }
385
386        *events = filtered_events;
387        Ok(())
388    }
389
390    /// Get crossbar statistics
391    ///
392    /// Returns statistics about the current state of the memristive crossbar
393    /// array and event processing pipeline.
394    ///
395    /// # Returns
396    /// HashMap containing various statistics about the processor state
397    pub fn get_crossbar_statistics(&self) -> HashMap<String, f64> {
398        let mut stats = HashMap::new();
399
400        if self.memristive_crossbar {
401            let total_conductance: f64 = self.conductances.sum();
402            let avg_conductance =
403                total_conductance / (self.crossbar_size.0 * self.crossbar_size.1) as f64;
404            let max_conductance = self.conductances.fold(0.0f64, |acc, &x| acc.max(x));
405            let min_conductance = self.conductances.fold(1.0f64, |acc, &x| acc.min(x));
406
407            stats.insert("total_conductance".to_string(), total_conductance);
408            stats.insert("avg_conductance".to_string(), avg_conductance);
409            stats.insert("max_conductance".to_string(), max_conductance);
410            stats.insert("min_conductance".to_string(), min_conductance);
411        }
412
413        stats.insert(
414            "event_pipeline_length".to_string(),
415            self.event_pipeline.len() as f64,
416        );
417        stats
418    }
419
420    /// Get crossbar size
421    pub fn crossbar_size(&self) -> (usize, usize) {
422        self.crossbar_size
423    }
424
425    /// Check if memristive crossbar is enabled
426    pub fn is_memristive_enabled(&self) -> bool {
427        self.memristive_crossbar
428    }
429
430    /// Check if temporal coding is enabled
431    pub fn is_temporal_coding_enabled(&self) -> bool {
432        self.temporal_coding
433    }
434
435    /// Get current crossbar threshold
436    pub fn crossbar_threshold(&self) -> f64 {
437        self.crossbar_threshold
438    }
439
440    /// Get memristive learning rate
441    pub fn learning_rate(&self) -> f64 {
442        self.memristive_learning_rate
443    }
444
445    /// Get number of events in pipeline
446    pub fn pipeline_length(&self) -> usize {
447        self.event_pipeline.len()
448    }
449
450    /// Clear event pipeline
451    pub fn clear_pipeline(&mut self) {
452        self.event_pipeline.clear();
453    }
454
455    /// Reset crossbar to initial state
456    pub fn reset_crossbar(&mut self) {
457        if self.memristive_crossbar {
458            self.initialize_crossbar();
459        }
460    }
461
462    /// Get conductance matrix (for analysis)
463    pub fn conductance_matrix(&self) -> &Array2<f64> {
464        &self.conductances
465    }
466
467    /// Set specific conductance value
468    pub fn set_conductance(&mut self, row: usize, col: usize, value: f64) -> SpatialResult<()> {
469        let (rows, cols) = self.crossbar_size;
470        if row >= rows || col >= cols {
471            return Err(SpatialError::InvalidInput(
472                "Crossbar indices out of bounds".to_string(),
473            ));
474        }
475
476        self.conductances[[row, col]] = value.clamp(0.0, 1.0);
477        Ok(())
478    }
479
480    /// Get specific conductance value
481    pub fn get_conductance(&self, row: usize, col: usize) -> Option<f64> {
482        let (rows, cols) = self.crossbar_size;
483        if row >= rows || col >= cols {
484            None
485        } else {
486            Some(self.conductances[[row, col]])
487        }
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use scirs2_core::ndarray::Array2;
495
496    #[test]
497    fn test_processor_creation() {
498        let processor = NeuromorphicProcessor::new();
499        assert!(!processor.is_memristive_enabled());
500        assert!(!processor.is_temporal_coding_enabled());
501        assert_eq!(processor.crossbar_size(), (64, 64));
502        assert_eq!(processor.pipeline_length(), 0);
503    }
504
505    #[test]
506    fn test_processor_configuration() {
507        let processor = NeuromorphicProcessor::new()
508            .with_memristive_crossbar(true)
509            .with_temporal_coding(true)
510            .with_crossbar_size(32, 32)
511            .with_processing_params(500, 0.7, 0.01);
512
513        assert!(processor.is_memristive_enabled());
514        assert!(processor.is_temporal_coding_enabled());
515        assert_eq!(processor.crossbar_size(), (32, 32));
516        assert_eq!(processor.crossbar_threshold(), 0.7);
517        assert_eq!(processor.learning_rate(), 0.01);
518        assert_eq!(processor.max_pipeline_length, 500);
519    }
520
521    #[test]
522    fn test_spatial_event_encoding() {
523        let points =
524            Array2::from_shape_vec((2, 2), vec![0.0, 0.0, 1.0, 1.0]).expect("Operation failed");
525        let processor = NeuromorphicProcessor::new();
526
527        let events = processor
528            .encode_spatial_events(&points.view())
529            .expect("Operation failed");
530
531        // Should generate events for each coordinate
532        assert!(!events.is_empty());
533
534        // Events should be sorted by timestamp
535        for i in 1..events.len() {
536            assert!(events[i - 1].timestamp() <= events[i].timestamp());
537        }
538    }
539
540    #[test]
541    fn test_temporal_vs_rate_coding() {
542        let points = Array2::from_shape_vec((1, 2), vec![1.0, 2.0]).expect("Operation failed");
543
544        // Rate coding
545        let processor_rate = NeuromorphicProcessor::new().with_temporal_coding(false);
546        let events_rate = processor_rate
547            .encode_spatial_events(&points.view())
548            .expect("Operation failed");
549
550        // Temporal coding
551        let processor_temporal = NeuromorphicProcessor::new().with_temporal_coding(true);
552        let events_temporal = processor_temporal
553            .encode_spatial_events(&points.view())
554            .expect("Operation failed");
555
556        // Different coding schemes should produce different numbers of events
557        // (temporal coding typically produces fewer events)
558        assert!(events_temporal.len() <= events_rate.len());
559    }
560
561    #[test]
562    fn test_event_processing() {
563        let points =
564            Array2::from_shape_vec((2, 2), vec![0.0, 0.0, 1.0, 1.0]).expect("Operation failed");
565        let mut processor = NeuromorphicProcessor::new()
566            .with_memristive_crossbar(false)
567            .with_temporal_coding(false);
568
569        let events = processor
570            .encode_spatial_events(&points.view())
571            .expect("Operation failed");
572        let processed_events = processor.process_events(&events).expect("Operation failed");
573
574        // Without crossbar, should have same number of events
575        assert_eq!(events.len(), processed_events.len());
576        assert!(processor.pipeline_length() > 0);
577    }
578
579    #[test]
580    fn test_memristive_crossbar() {
581        let points = Array2::from_shape_vec((1, 1), vec![1.0]).expect("Operation failed");
582        let mut processor = NeuromorphicProcessor::new()
583            .with_memristive_crossbar(true)
584            .with_crossbar_size(4, 4)
585            .with_processing_params(1000, 0.2, 0.001); // Lower threshold to ensure output
586
587        let events = processor
588            .encode_spatial_events(&points.view())
589            .expect("Operation failed");
590        let processed_events = processor.process_events(&events).expect("Operation failed");
591
592        // Memristive crossbar might generate additional output events
593        // With lower threshold (0.2) and conductances [0.1, 1.0], we should get outputs
594        assert!(!processed_events.is_empty());
595
596        let stats = processor.get_crossbar_statistics();
597        assert!(stats.contains_key("avg_conductance"));
598        assert!(stats.contains_key("max_conductance"));
599    }
600
601    #[test]
602    fn test_conductance_operations() {
603        let mut processor = NeuromorphicProcessor::new()
604            .with_memristive_crossbar(true)
605            .with_crossbar_size(4, 4);
606
607        // Test setting and getting conductance
608        processor
609            .set_conductance(0, 0, 0.8)
610            .expect("Operation failed");
611        assert_eq!(processor.get_conductance(0, 0), Some(0.8));
612
613        // Test bounds checking
614        assert!(processor.set_conductance(10, 10, 0.5).is_err());
615        assert_eq!(processor.get_conductance(10, 10), None);
616
617        // Test clamping
618        processor
619            .set_conductance(1, 1, 2.0)
620            .expect("Operation failed"); // Should be clamped to 1.0
621        assert_eq!(processor.get_conductance(1, 1), Some(1.0));
622    }
623
624    #[test]
625    fn test_processor_reset() {
626        let mut processor = NeuromorphicProcessor::new().with_memristive_crossbar(true);
627
628        // Process some events to change state
629        let points = Array2::from_shape_vec((1, 1), vec![1.0]).expect("Operation failed");
630        let events = processor
631            .encode_spatial_events(&points.view())
632            .expect("Operation failed");
633        processor.process_events(&events).expect("Operation failed");
634
635        assert!(processor.pipeline_length() > 0);
636
637        // Reset should clear pipeline
638        processor.clear_pipeline();
639        assert_eq!(processor.pipeline_length(), 0);
640
641        // Reset crossbar should reinitialize conductances
642        let initial_stats = processor.get_crossbar_statistics();
643        processor.reset_crossbar();
644        let reset_stats = processor.get_crossbar_statistics();
645
646        // Conductances might be different after reset
647        assert!(initial_stats.contains_key("avg_conductance"));
648        assert!(reset_stats.contains_key("avg_conductance"));
649    }
650
651    #[test]
652    fn test_empty_input() {
653        let points = Array2::zeros((0, 2));
654        let processor = NeuromorphicProcessor::new();
655
656        let events = processor
657            .encode_spatial_events(&points.view())
658            .expect("Operation failed");
659        assert!(events.is_empty());
660    }
661}