scirs2-spatial 0.4.2

Spatial algorithms module for SciRS2 (scirs2-spatial)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
//! Neuromorphic Event Processing
//!
//! This module implements general-purpose neuromorphic processing capabilities
//! including event-driven computation, memristive crossbar arrays, and temporal
//! dynamics for spatial data processing.

use crate::error::{SpatialError, SpatialResult};
use scirs2_core::ndarray::{Array2, ArrayView2};
use scirs2_core::random::{Rng, RngExt};
use std::collections::{HashMap, VecDeque};

// Import core neuromorphic components
use super::super::core::SpikeEvent;

/// Neuromorphic processor for general spatial computations
///
/// This processor implements event-driven neuromorphic computing paradigms
/// for spatial data processing. It supports memristive crossbar arrays for
/// in-memory computation and temporal coding schemes for efficient information
/// processing.
///
/// # Features
/// - Event-driven processing pipeline
/// - Memristive crossbar arrays for in-memory computation
/// - Temporal and rate-based coding schemes
/// - Spike timing dynamics and correlation detection
/// - Configurable processing parameters
///
/// # Example
/// ```rust
/// use scirs2_core::ndarray::Array2;
/// use scirs2_spatial::neuromorphic::algorithms::NeuromorphicProcessor;
///
/// let points = Array2::from_shape_vec((3, 2), vec![
///     0.0, 0.0, 1.0, 1.0, 2.0, 2.0
/// ]).expect("Operation failed");
///
/// let mut processor = NeuromorphicProcessor::new()
///     .with_memristive_crossbar(true)
///     .with_temporal_coding(true)
///     .with_crossbar_size(64, 64);
///
/// // Encode spatial data as neuromorphic events
/// let events = processor.encode_spatial_events(&points.view()).expect("Operation failed");
///
/// // Process events through neuromorphic pipeline
/// let processed_events = processor.process_events(&events).expect("Operation failed");
/// ```
#[derive(Debug, Clone)]
pub struct NeuromorphicProcessor {
    /// Enable memristive crossbar arrays
    memristive_crossbar: bool,
    /// Enable temporal coding
    temporal_coding: bool,
    /// Crossbar array dimensions
    crossbar_size: (usize, usize),
    /// Memristive device conductances
    conductances: Array2<f64>,
    /// Event processing pipeline
    event_pipeline: VecDeque<SpikeEvent>,
    /// Maximum pipeline length
    max_pipeline_length: usize,
    /// Crossbar threshold for spike generation
    crossbar_threshold: f64,
    /// Memristive learning rate
    memristive_learning_rate: f64,
}

impl Default for NeuromorphicProcessor {
    fn default() -> Self {
        Self::new()
    }
}

impl NeuromorphicProcessor {
    /// Create new neuromorphic processor
    ///
    /// # Returns
    /// A new `NeuromorphicProcessor` with default configuration
    pub fn new() -> Self {
        Self {
            memristive_crossbar: false,
            temporal_coding: false,
            crossbar_size: (64, 64),
            conductances: Array2::zeros((64, 64)),
            event_pipeline: VecDeque::new(),
            max_pipeline_length: 1000,
            crossbar_threshold: 0.5,
            memristive_learning_rate: 0.001,
        }
    }

    /// Enable memristive crossbar arrays
    ///
    /// When enabled, events are processed through a memristive crossbar array
    /// that provides in-memory computation capabilities.
    ///
    /// # Arguments
    /// * `enabled` - Whether to enable memristive crossbar processing
    pub fn with_memristive_crossbar(mut self, enabled: bool) -> Self {
        self.memristive_crossbar = enabled;
        if enabled {
            self.initialize_crossbar();
        }
        self
    }

    /// Enable temporal coding
    ///
    /// When enabled, spatial coordinates are encoded using spike timing
    /// rather than spike rates.
    ///
    /// # Arguments
    /// * `enabled` - Whether to enable temporal coding
    pub fn with_temporal_coding(mut self, enabled: bool) -> Self {
        self.temporal_coding = enabled;
        self
    }

    /// Configure crossbar size
    ///
    /// Sets the dimensions of the memristive crossbar array used for
    /// in-memory computation.
    ///
    /// # Arguments
    /// * `rows` - Number of rows in crossbar array
    /// * `cols` - Number of columns in crossbar array
    pub fn with_crossbar_size(mut self, rows: usize, cols: usize) -> Self {
        self.crossbar_size = (rows, cols);
        self.conductances = Array2::zeros((rows, cols));
        if self.memristive_crossbar {
            self.initialize_crossbar();
        }
        self
    }

    /// Configure processing parameters
    ///
    /// # Arguments
    /// * `max_pipeline_length` - Maximum length of event pipeline
    /// * `crossbar_threshold` - Threshold for crossbar spike generation
    /// * `learning_rate` - Learning rate for memristive adaptation
    pub fn with_processing_params(
        mut self,
        max_pipeline_length: usize,
        crossbar_threshold: f64,
        learning_rate: f64,
    ) -> Self {
        self.max_pipeline_length = max_pipeline_length;
        self.crossbar_threshold = crossbar_threshold;
        self.memristive_learning_rate = learning_rate;
        self
    }

    /// Encode spatial data as neuromorphic events
    ///
    /// Converts spatial data points into spike events using either temporal
    /// coding (spike timing) or rate coding (spike frequency) depending on
    /// the processor configuration.
    ///
    /// # Arguments
    /// * `points` - Input spatial points (n_points × n_dims)
    ///
    /// # Returns
    /// Vector of spike events representing the spatial data
    pub fn encode_spatial_events(
        &self,
        points: &ArrayView2<'_, f64>,
    ) -> SpatialResult<Vec<SpikeEvent>> {
        let (n_points, n_dims) = points.dim();
        let mut events = Vec::new();

        if n_points == 0 || n_dims == 0 {
            return Ok(events);
        }

        for (point_idx, point) in points.outer_iter().enumerate() {
            for (dim, &coord) in point.iter().enumerate() {
                // Temporal coding: encode coordinate as spike timing
                let normalized_coord = (coord + 10.0) / 20.0; // Normalize to [0, 1]
                let normalized_coord = normalized_coord.clamp(0.0, 1.0);

                if self.temporal_coding {
                    // Timing-based encoding
                    let spike_time = normalized_coord * 100.0; // Map to [0, 100] time units
                    let event =
                        SpikeEvent::new(point_idx * n_dims + dim, spike_time, 1.0, point.to_vec());
                    events.push(event);
                } else {
                    // Rate-based encoding
                    let spike_rate = normalized_coord * 50.0; // Max 50 Hz
                    let num_spikes = spike_rate as usize;

                    for spike_idx in 0..num_spikes {
                        let spike_time = if spike_rate > 0.0 {
                            (spike_idx as f64) * (1.0 / spike_rate)
                        } else {
                            0.0
                        };
                        let event = SpikeEvent::new(
                            point_idx * n_dims + dim,
                            spike_time,
                            1.0,
                            point.to_vec(),
                        );
                        events.push(event);
                    }
                }
            }
        }

        // Sort events by timestamp
        events.sort_by(|a, b| {
            a.timestamp()
                .partial_cmp(&b.timestamp())
                .expect("Operation failed")
        });

        Ok(events)
    }

    /// Process events through neuromorphic pipeline
    ///
    /// Processes spike events through the neuromorphic computing pipeline,
    /// applying memristive crossbar processing and temporal dynamics as
    /// configured.
    ///
    /// # Arguments
    /// * `events` - Input spike events to process
    ///
    /// # Returns
    /// Vector of processed spike events
    pub fn process_events(&mut self, events: &[SpikeEvent]) -> SpatialResult<Vec<SpikeEvent>> {
        let mut processed_events = Vec::new();

        for event in events {
            self.event_pipeline.push_back(event.clone());

            // Process through memristive crossbar if enabled
            if self.memristive_crossbar {
                let crossbar_output = self.process_through_crossbar(event)?;
                processed_events.extend(crossbar_output);
            } else {
                processed_events.push(event.clone());
            }

            // Apply temporal dynamics
            if self.temporal_coding {
                Self::apply_temporal_dynamics(&mut processed_events)?;
            }

            // Maintain event pipeline size
            if self.event_pipeline.len() > self.max_pipeline_length {
                self.event_pipeline.pop_front();
            }
        }

        Ok(processed_events)
    }

    /// Initialize memristive crossbar array
    ///
    /// Initializes the conductance values of the memristive crossbar array
    /// with random values representing the initial device states.
    fn initialize_crossbar(&mut self) {
        let (rows, cols) = self.crossbar_size;
        let mut rng = scirs2_core::random::rng();

        // Initialize conductances with random values
        for i in 0..rows {
            for j in 0..cols {
                // Random conductance between 0.1 and 1.0 (normalized)
                self.conductances[[i, j]] = 0.1 + rng.random_range(0.0..0.9);
            }
        }
    }

    /// Process event through memristive crossbar
    ///
    /// Processes a spike event through the memristive crossbar array,
    /// generating output spikes based on the crossbar conductances and
    /// updating the memristive devices.
    fn process_through_crossbar(&mut self, event: &SpikeEvent) -> SpatialResult<Vec<SpikeEvent>> {
        let (rows, cols) = self.crossbar_size;
        let mut output_events = Vec::new();

        // Map input neuron to crossbar row
        let input_row = event.neuron_id() % rows;

        // Compute crossbar outputs
        for col in 0..cols {
            let conductance = self.conductances[[input_row, col]];
            let output_current = event.amplitude() * conductance;

            // Generate output spike if current exceeds threshold
            if output_current > self.crossbar_threshold {
                let output_event = SpikeEvent::new(
                    rows + col,              // Offset for output neurons
                    event.timestamp() + 0.1, // Small delay
                    output_current,
                    event.spatial_coords().to_vec(),
                );
                output_events.push(output_event);

                // Update memristive device (Hebbian-like plasticity)
                self.update_memristive_device(input_row, col, event.amplitude())?;
            }
        }

        Ok(output_events)
    }

    /// Update memristive device conductance
    ///
    /// Updates the conductance of a memristive device based on the input
    /// spike amplitude using a simple Hebbian-like learning rule.
    fn update_memristive_device(
        &mut self,
        row: usize,
        col: usize,
        spike_amplitude: f64,
    ) -> SpatialResult<()> {
        let current_conductance = self.conductances[[row, col]];

        // Simple memristive update rule
        let conductance_change =
            self.memristive_learning_rate * spike_amplitude * (1.0 - current_conductance);

        self.conductances[[row, col]] += conductance_change;
        self.conductances[[row, col]] = self.conductances[[row, col]].clamp(0.0, 1.0);

        Ok(())
    }

    /// Apply temporal dynamics to event processing
    ///
    /// Applies temporal filtering and spike-timing dependent processing
    /// to enhance temporal correlations and implement refractory periods.
    fn apply_temporal_dynamics(events: &mut Vec<SpikeEvent>) -> SpatialResult<()> {
        // Apply temporal filtering and spike-timing dependent processing
        let mut filtered_events = Vec::new();

        for (i, event) in events.iter().enumerate() {
            let mut should_include = true;
            let mut modified_event = event.clone();

            // Check for temporal correlations with recent events
            for other_event in events.iter().skip(i + 1) {
                let time_diff = (other_event.timestamp() - event.timestamp()).abs();

                if time_diff < 5.0 {
                    // Within temporal window
                    // Apply temporal correlation enhancement
                    let new_amplitude = modified_event.amplitude() * 1.1;
                    modified_event = SpikeEvent::new(
                        modified_event.neuron_id(),
                        modified_event.timestamp(),
                        new_amplitude,
                        modified_event.spatial_coords().to_vec(),
                    );

                    // Coincidence detection
                    if time_diff < 1.0 {
                        let enhanced_amplitude = modified_event.amplitude() * 1.5;
                        modified_event = SpikeEvent::new(
                            modified_event.neuron_id(),
                            modified_event.timestamp(),
                            enhanced_amplitude,
                            modified_event.spatial_coords().to_vec(),
                        );
                    }
                }

                // Refractory period simulation
                if time_diff < 0.5 && event.neuron_id() == other_event.neuron_id() {
                    should_include = false; // Suppress due to refractory period
                    break;
                }
            }

            if should_include {
                filtered_events.push(modified_event);
            }
        }

        *events = filtered_events;
        Ok(())
    }

    /// Get crossbar statistics
    ///
    /// Returns statistics about the current state of the memristive crossbar
    /// array and event processing pipeline.
    ///
    /// # Returns
    /// HashMap containing various statistics about the processor state
    pub fn get_crossbar_statistics(&self) -> HashMap<String, f64> {
        let mut stats = HashMap::new();

        if self.memristive_crossbar {
            let total_conductance: f64 = self.conductances.sum();
            let avg_conductance =
                total_conductance / (self.crossbar_size.0 * self.crossbar_size.1) as f64;
            let max_conductance = self.conductances.fold(0.0f64, |acc, &x| acc.max(x));
            let min_conductance = self.conductances.fold(1.0f64, |acc, &x| acc.min(x));

            stats.insert("total_conductance".to_string(), total_conductance);
            stats.insert("avg_conductance".to_string(), avg_conductance);
            stats.insert("max_conductance".to_string(), max_conductance);
            stats.insert("min_conductance".to_string(), min_conductance);
        }

        stats.insert(
            "event_pipeline_length".to_string(),
            self.event_pipeline.len() as f64,
        );
        stats
    }

    /// Get crossbar size
    pub fn crossbar_size(&self) -> (usize, usize) {
        self.crossbar_size
    }

    /// Check if memristive crossbar is enabled
    pub fn is_memristive_enabled(&self) -> bool {
        self.memristive_crossbar
    }

    /// Check if temporal coding is enabled
    pub fn is_temporal_coding_enabled(&self) -> bool {
        self.temporal_coding
    }

    /// Get current crossbar threshold
    pub fn crossbar_threshold(&self) -> f64 {
        self.crossbar_threshold
    }

    /// Get memristive learning rate
    pub fn learning_rate(&self) -> f64 {
        self.memristive_learning_rate
    }

    /// Get number of events in pipeline
    pub fn pipeline_length(&self) -> usize {
        self.event_pipeline.len()
    }

    /// Clear event pipeline
    pub fn clear_pipeline(&mut self) {
        self.event_pipeline.clear();
    }

    /// Reset crossbar to initial state
    pub fn reset_crossbar(&mut self) {
        if self.memristive_crossbar {
            self.initialize_crossbar();
        }
    }

    /// Get conductance matrix (for analysis)
    pub fn conductance_matrix(&self) -> &Array2<f64> {
        &self.conductances
    }

    /// Set specific conductance value
    pub fn set_conductance(&mut self, row: usize, col: usize, value: f64) -> SpatialResult<()> {
        let (rows, cols) = self.crossbar_size;
        if row >= rows || col >= cols {
            return Err(SpatialError::InvalidInput(
                "Crossbar indices out of bounds".to_string(),
            ));
        }

        self.conductances[[row, col]] = value.clamp(0.0, 1.0);
        Ok(())
    }

    /// Get specific conductance value
    pub fn get_conductance(&self, row: usize, col: usize) -> Option<f64> {
        let (rows, cols) = self.crossbar_size;
        if row >= rows || col >= cols {
            None
        } else {
            Some(self.conductances[[row, col]])
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use scirs2_core::ndarray::Array2;

    #[test]
    fn test_processor_creation() {
        let processor = NeuromorphicProcessor::new();
        assert!(!processor.is_memristive_enabled());
        assert!(!processor.is_temporal_coding_enabled());
        assert_eq!(processor.crossbar_size(), (64, 64));
        assert_eq!(processor.pipeline_length(), 0);
    }

    #[test]
    fn test_processor_configuration() {
        let processor = NeuromorphicProcessor::new()
            .with_memristive_crossbar(true)
            .with_temporal_coding(true)
            .with_crossbar_size(32, 32)
            .with_processing_params(500, 0.7, 0.01);

        assert!(processor.is_memristive_enabled());
        assert!(processor.is_temporal_coding_enabled());
        assert_eq!(processor.crossbar_size(), (32, 32));
        assert_eq!(processor.crossbar_threshold(), 0.7);
        assert_eq!(processor.learning_rate(), 0.01);
        assert_eq!(processor.max_pipeline_length, 500);
    }

    #[test]
    fn test_spatial_event_encoding() {
        let points =
            Array2::from_shape_vec((2, 2), vec![0.0, 0.0, 1.0, 1.0]).expect("Operation failed");
        let processor = NeuromorphicProcessor::new();

        let events = processor
            .encode_spatial_events(&points.view())
            .expect("Operation failed");

        // Should generate events for each coordinate
        assert!(!events.is_empty());

        // Events should be sorted by timestamp
        for i in 1..events.len() {
            assert!(events[i - 1].timestamp() <= events[i].timestamp());
        }
    }

    #[test]
    fn test_temporal_vs_rate_coding() {
        let points = Array2::from_shape_vec((1, 2), vec![1.0, 2.0]).expect("Operation failed");

        // Rate coding
        let processor_rate = NeuromorphicProcessor::new().with_temporal_coding(false);
        let events_rate = processor_rate
            .encode_spatial_events(&points.view())
            .expect("Operation failed");

        // Temporal coding
        let processor_temporal = NeuromorphicProcessor::new().with_temporal_coding(true);
        let events_temporal = processor_temporal
            .encode_spatial_events(&points.view())
            .expect("Operation failed");

        // Different coding schemes should produce different numbers of events
        // (temporal coding typically produces fewer events)
        assert!(events_temporal.len() <= events_rate.len());
    }

    #[test]
    fn test_event_processing() {
        let points =
            Array2::from_shape_vec((2, 2), vec![0.0, 0.0, 1.0, 1.0]).expect("Operation failed");
        let mut processor = NeuromorphicProcessor::new()
            .with_memristive_crossbar(false)
            .with_temporal_coding(false);

        let events = processor
            .encode_spatial_events(&points.view())
            .expect("Operation failed");
        let processed_events = processor.process_events(&events).expect("Operation failed");

        // Without crossbar, should have same number of events
        assert_eq!(events.len(), processed_events.len());
        assert!(processor.pipeline_length() > 0);
    }

    #[test]
    fn test_memristive_crossbar() {
        let points = Array2::from_shape_vec((1, 1), vec![1.0]).expect("Operation failed");
        let mut processor = NeuromorphicProcessor::new()
            .with_memristive_crossbar(true)
            .with_crossbar_size(4, 4)
            .with_processing_params(1000, 0.2, 0.001); // Lower threshold to ensure output

        let events = processor
            .encode_spatial_events(&points.view())
            .expect("Operation failed");
        let processed_events = processor.process_events(&events).expect("Operation failed");

        // Memristive crossbar might generate additional output events
        // With lower threshold (0.2) and conductances [0.1, 1.0], we should get outputs
        assert!(!processed_events.is_empty());

        let stats = processor.get_crossbar_statistics();
        assert!(stats.contains_key("avg_conductance"));
        assert!(stats.contains_key("max_conductance"));
    }

    #[test]
    fn test_conductance_operations() {
        let mut processor = NeuromorphicProcessor::new()
            .with_memristive_crossbar(true)
            .with_crossbar_size(4, 4);

        // Test setting and getting conductance
        processor
            .set_conductance(0, 0, 0.8)
            .expect("Operation failed");
        assert_eq!(processor.get_conductance(0, 0), Some(0.8));

        // Test bounds checking
        assert!(processor.set_conductance(10, 10, 0.5).is_err());
        assert_eq!(processor.get_conductance(10, 10), None);

        // Test clamping
        processor
            .set_conductance(1, 1, 2.0)
            .expect("Operation failed"); // Should be clamped to 1.0
        assert_eq!(processor.get_conductance(1, 1), Some(1.0));
    }

    #[test]
    fn test_processor_reset() {
        let mut processor = NeuromorphicProcessor::new().with_memristive_crossbar(true);

        // Process some events to change state
        let points = Array2::from_shape_vec((1, 1), vec![1.0]).expect("Operation failed");
        let events = processor
            .encode_spatial_events(&points.view())
            .expect("Operation failed");
        processor.process_events(&events).expect("Operation failed");

        assert!(processor.pipeline_length() > 0);

        // Reset should clear pipeline
        processor.clear_pipeline();
        assert_eq!(processor.pipeline_length(), 0);

        // Reset crossbar should reinitialize conductances
        let initial_stats = processor.get_crossbar_statistics();
        processor.reset_crossbar();
        let reset_stats = processor.get_crossbar_statistics();

        // Conductances might be different after reset
        assert!(initial_stats.contains_key("avg_conductance"));
        assert!(reset_stats.contains_key("avg_conductance"));
    }

    #[test]
    fn test_empty_input() {
        let points = Array2::zeros((0, 2));
        let processor = NeuromorphicProcessor::new();

        let events = processor
            .encode_spatial_events(&points.view())
            .expect("Operation failed");
        assert!(events.is_empty());
    }
}