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
//! Spiking Neural Network Clustering
//!
//! This module implements clustering algorithms based on spiking neural networks (SNNs).
//! These algorithms use spike-timing dependent plasticity (STDP) and competitive learning
//! to discover patterns in spatial data through biologically-inspired neural dynamics.

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

// Import core neuromorphic components
use super::super::core::{SpikeEvent, SpikingNeuron, Synapse};

/// Spiking neural network clusterer
///
/// This clusterer uses a network of spiking neurons with STDP learning to perform
/// unsupervised clustering of spatial data. Input points are encoded as spike trains
/// and presented to the network, which learns to respond selectively to different
/// input patterns through competitive dynamics.
///
/// # Features
/// - Rate coding for spatial data encoding
/// - STDP learning for adaptive weights
/// - Lateral inhibition for competitive dynamics
/// - Configurable network architecture
/// - Spike timing analysis
///
/// # Example
/// ```rust
/// use scirs2_core::ndarray::Array2;
/// use scirs2_spatial::neuromorphic::algorithms::SpikingNeuralClusterer;
///
/// let points = Array2::from_shape_vec((4, 2), vec![
///     0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0
/// ]).expect("Operation failed");
///
/// let mut clusterer = SpikingNeuralClusterer::new(2)
///     .with_spike_threshold(0.8)
///     .with_stdp_learning(true)
///     .with_lateral_inhibition(true);
///
/// let (assignments, spike_events) = clusterer.fit(&points.view()).expect("Operation failed");
/// println!("Cluster assignments: {:?}", assignments);
/// ```
#[derive(Debug, Clone)]
pub struct SpikingNeuralClusterer {
    /// Network of spiking neurons
    neurons: Vec<SpikingNeuron>,
    /// Synaptic connections
    synapses: Vec<Synapse>,
    /// Number of clusters (output neurons)
    num_clusters: usize,
    /// Spike threshold
    spike_threshold: f64,
    /// Enable STDP learning
    stdp_learning: bool,
    /// Enable lateral inhibition
    lateral_inhibition: bool,
    /// Simulation time step
    dt: f64,
    /// Current simulation time
    current_time: f64,
    /// Spike history
    spike_history: Vec<SpikeEvent>,
    /// Number of training epochs
    max_epochs: usize,
    /// Simulation duration per data point
    simulation_duration: f64,
}

impl SpikingNeuralClusterer {
    /// Create new spiking neural clusterer
    ///
    /// # Arguments
    /// * `num_clusters` - Number of clusters to discover
    ///
    /// # Returns
    /// A new `SpikingNeuralClusterer` with default parameters
    pub fn new(num_clusters: usize) -> Self {
        Self {
            neurons: Vec::new(),
            synapses: Vec::new(),
            num_clusters,
            spike_threshold: 1.0,
            stdp_learning: true,
            lateral_inhibition: true,
            dt: 0.1,
            current_time: 0.0,
            spike_history: Vec::new(),
            max_epochs: 100,
            simulation_duration: 10.0,
        }
    }

    /// Configure spike threshold
    ///
    /// # Arguments
    /// * `threshold` - Spike threshold for neurons
    pub fn with_spike_threshold(mut self, threshold: f64) -> Self {
        self.spike_threshold = threshold;
        self
    }

    /// Enable/disable STDP learning
    ///
    /// # Arguments
    /// * `enabled` - Whether to enable STDP learning
    pub fn with_stdp_learning(mut self, enabled: bool) -> Self {
        self.stdp_learning = enabled;
        self
    }

    /// Enable/disable lateral inhibition
    ///
    /// # Arguments
    /// * `enabled` - Whether to enable lateral inhibition
    pub fn with_lateral_inhibition(mut self, enabled: bool) -> Self {
        self.lateral_inhibition = enabled;
        self
    }

    /// Configure training parameters
    ///
    /// # Arguments
    /// * `max_epochs` - Maximum number of training epochs
    /// * `simulation_duration` - Duration to simulate per data point
    pub fn with_training_params(mut self, max_epochs: usize, simulation_duration: f64) -> Self {
        self.max_epochs = max_epochs;
        self.simulation_duration = simulation_duration;
        self
    }

    /// Configure simulation time step
    ///
    /// # Arguments
    /// * `dt` - Time step for simulation
    pub fn with_time_step(mut self, dt: f64) -> Self {
        self.dt = dt;
        self
    }

    /// Fit clustering to spatial data
    ///
    /// Trains the spiking neural network on the provided spatial data using
    /// STDP learning and competitive dynamics to discover cluster structure.
    ///
    /// # Arguments
    /// * `points` - Input points to cluster (n_points × n_dims)
    ///
    /// # Returns
    /// Tuple of (cluster assignments, spike events) where assignments
    /// maps each point to its cluster and spike_events contains the
    /// complete spike timing history.
    pub fn fit(
        &mut self,
        points: &ArrayView2<'_, f64>,
    ) -> SpatialResult<(Array1<usize>, Vec<SpikeEvent>)> {
        let (n_points, n_dims) = points.dim();

        if n_points == 0 || n_dims == 0 {
            return Err(SpatialError::InvalidInput(
                "Input data cannot be empty".to_string(),
            ));
        }

        // Initialize neural network
        self.initialize_network(n_dims)?;

        // Present data points as spike trains
        let mut assignments = Array1::zeros(n_points);

        for epoch in 0..self.max_epochs {
            self.current_time = epoch as f64 * 100.0;

            for (point_idx, point) in points.outer_iter().enumerate() {
                // Encode spatial point as spike train
                let spike_train = self.encode_point_as_spikes(&point.to_owned())?;

                // Process spike train through network
                let winning_neuron = self.process_spike_train(&spike_train)?;
                assignments[point_idx] = winning_neuron;

                // Apply learning if enabled
                if self.stdp_learning {
                    self.apply_stdp_learning(&spike_train)?;
                }
            }

            // Apply lateral inhibition
            if self.lateral_inhibition {
                self.apply_lateral_inhibition()?;
            }
        }

        Ok((assignments, self.spike_history.clone()))
    }

    /// Initialize spiking neural network
    ///
    /// Creates the network topology with input neurons, output neurons,
    /// and synaptic connections between them.
    fn initialize_network(&mut self, input_dims: usize) -> SpatialResult<()> {
        self.neurons.clear();
        self.synapses.clear();
        self.spike_history.clear();

        // Create input neurons (one per dimension)
        for i in 0..input_dims {
            let position = vec![i as f64];
            let mut neuron = SpikingNeuron::new(position);
            neuron.set_threshold(self.spike_threshold);
            self.neurons.push(neuron);
        }

        // Create output neurons (cluster centers)
        let mut rng = scirs2_core::random::rng();
        for _i in 0..self.num_clusters {
            let position = (0..input_dims)
                .map(|_| rng.random_range(0.0..1.0))
                .collect();
            let mut neuron = SpikingNeuron::new(position);
            neuron.set_threshold(self.spike_threshold);
            self.neurons.push(neuron);
        }

        // Create synaptic connections (input to output)
        for i in 0..input_dims {
            for j in 0..self.num_clusters {
                let output_idx = input_dims + j;
                let weight = rng.random_range(0.0..0.5);
                let synapse = Synapse::new(i, output_idx, weight);
                self.synapses.push(synapse);
            }
        }

        // Create lateral inhibitory connections between output neurons
        if self.lateral_inhibition {
            for i in 0..self.num_clusters {
                for j in 0..self.num_clusters {
                    if i != j {
                        let neuron_i = input_dims + i;
                        let neuron_j = input_dims + j;
                        let synapse = Synapse::new(neuron_i, neuron_j, -0.5);
                        self.synapses.push(synapse);
                    }
                }
            }
        }

        Ok(())
    }

    /// Encode spatial point as spike train
    ///
    /// Converts a spatial data point into a spike train using rate coding,
    /// where the firing rate of each input neuron is proportional to the
    /// corresponding coordinate value.
    fn encode_point_as_spikes(&self, point: &Array1<f64>) -> SpatialResult<Vec<SpikeEvent>> {
        let mut spike_train = Vec::new();

        // Rate coding: spike frequency proportional to coordinate value
        for (dim, &coord) in point.iter().enumerate() {
            // Normalize coordinate to [0, 1] and scale to spike rate
            let normalized_coord = (coord + 10.0) / 20.0; // Assume data in [-10, 10]
            let spike_rate = normalized_coord.clamp(0.0, 1.0) * 50.0; // Max 50 Hz

            // Generate Poisson spike train
            let num_spikes = (spike_rate * 1.0) as usize; // 1 second duration
            for spike_idx in 0..num_spikes {
                let timestamp =
                    self.current_time + (spike_idx as f64) * (1.0 / spike_rate.max(1.0));
                let spike = SpikeEvent::new(dim, timestamp, 1.0, point.to_vec());
                spike_train.push(spike);
            }
        }

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

        Ok(spike_train)
    }

    /// Process spike train through network
    ///
    /// Simulates the network dynamics when presented with a spike train,
    /// determining which output neuron responds most strongly.
    fn process_spike_train(&mut self, spike_train: &[SpikeEvent]) -> SpatialResult<usize> {
        let input_dims = self.neurons.len() - self.num_clusters;
        let mut neuron_spike_counts = vec![0; self.num_clusters];

        // Simulate network for duration of spike train
        let mut t = self.current_time;
        let mut spike_idx = 0;

        while t < self.current_time + self.simulation_duration {
            // Apply input spikes
            let mut input_currents = vec![0.0; self.neurons.len()];

            while spike_idx < spike_train.len() && spike_train[spike_idx].timestamp() <= t {
                let spike = &spike_train[spike_idx];
                if spike.neuron_id() < input_dims {
                    input_currents[spike.neuron_id()] += spike.amplitude();
                }
                spike_idx += 1;
            }

            // Calculate synaptic currents
            for synapse in &self.synapses {
                if synapse.pre_neuron() < self.neurons.len()
                    && synapse.post_neuron() < self.neurons.len()
                {
                    let pre_current = input_currents[synapse.pre_neuron()];
                    let synaptic_current = synapse.synaptic_current(pre_current);
                    input_currents[synapse.post_neuron()] += synaptic_current;
                }
            }

            // Update neurons and check for spikes
            for (neuron_idx, neuron) in self.neurons.iter_mut().enumerate() {
                let spiked = neuron.update(self.dt, input_currents[neuron_idx]);

                if spiked && neuron_idx >= input_dims {
                    let cluster_idx = neuron_idx - input_dims;
                    neuron_spike_counts[cluster_idx] += 1;

                    // Record spike event
                    let spike_event =
                        SpikeEvent::new(neuron_idx, t, 1.0, neuron.position().to_vec());
                    self.spike_history.push(spike_event);
                }
            }

            t += self.dt;
        }

        // Find winning neuron (cluster with most spikes)
        let winning_cluster = neuron_spike_counts
            .iter()
            .enumerate()
            .max_by(|(_, a), (_, b)| a.cmp(b))
            .map(|(idx, _)| idx)
            .unwrap_or(0);

        Ok(winning_cluster)
    }

    /// Apply STDP learning to synapses
    ///
    /// Updates synaptic weights based on the relative timing of pre- and
    /// post-synaptic spikes using the STDP learning rule.
    fn apply_stdp_learning(&mut self, spike_train: &[SpikeEvent]) -> SpatialResult<()> {
        // Create spike timing map
        let mut spike_times: HashMap<usize, Vec<f64>> = HashMap::new();
        for spike in spike_train {
            spike_times
                .entry(spike.neuron_id())
                .or_default()
                .push(spike.timestamp());
        }

        // Add output neuron spikes from history
        for spike in &self.spike_history {
            spike_times
                .entry(spike.neuron_id())
                .or_default()
                .push(spike.timestamp());
        }

        // Update synaptic weights using STDP
        let empty_spikes = Vec::new();
        for synapse in &mut self.synapses {
            let pre_spikes = spike_times
                .get(&synapse.pre_neuron())
                .unwrap_or(&empty_spikes);
            let post_spikes = spike_times
                .get(&synapse.post_neuron())
                .unwrap_or(&empty_spikes);

            // Check for coincident spikes
            for &pre_time in pre_spikes {
                for &post_time in post_spikes {
                    let dt = post_time - pre_time;
                    if dt.abs() < 50.0 {
                        // Within STDP window
                        let current_weight = synapse.weight();
                        if dt > 0.0 {
                            // Potentiation
                            let delta_w = synapse.stdp_rate() * (-dt / synapse.stdp_tau()).exp();
                            synapse.set_weight(current_weight + delta_w);
                        } else {
                            // Depression
                            let delta_w = synapse.stdp_rate() * (dt / synapse.stdp_tau()).exp();
                            synapse.set_weight(current_weight - delta_w);
                        }
                    }
                }
            }
        }

        Ok(())
    }

    /// Apply lateral inhibition between output neurons
    ///
    /// Strengthens inhibitory connections between neurons based on their
    /// relative activity levels to promote competition.
    fn apply_lateral_inhibition(&mut self) -> SpatialResult<()> {
        let input_dims = self.neurons.len() - self.num_clusters;

        // Strengthen inhibitory connections between active neurons
        for i in 0..self.num_clusters {
            for j in 0..self.num_clusters {
                if i != j {
                    let neuron_i_idx = input_dims + i;
                    let neuron_j_idx = input_dims + j;

                    // Find inhibitory synapse
                    for synapse in &mut self.synapses {
                        if synapse.pre_neuron() == neuron_i_idx
                            && synapse.post_neuron() == neuron_j_idx
                        {
                            // Strengthen inhibition based on activity
                            let activity_i = self.neurons[neuron_i_idx].membrane_potential();
                            let activity_j = self.neurons[neuron_j_idx].membrane_potential();

                            if activity_i > activity_j {
                                let current_weight = synapse.weight();
                                synapse.set_weight(current_weight - 0.01); // Strengthen inhibition
                            }
                        }
                    }
                }
            }
        }

        Ok(())
    }

    /// Get number of clusters
    pub fn num_clusters(&self) -> usize {
        self.num_clusters
    }

    /// Get spike threshold
    pub fn spike_threshold(&self) -> f64 {
        self.spike_threshold
    }

    /// Check if STDP learning is enabled
    pub fn is_stdp_enabled(&self) -> bool {
        self.stdp_learning
    }

    /// Check if lateral inhibition is enabled
    pub fn is_lateral_inhibition_enabled(&self) -> bool {
        self.lateral_inhibition
    }

    /// Get current spike history
    pub fn spike_history(&self) -> &[SpikeEvent] {
        &self.spike_history
    }

    /// Get network statistics
    pub fn network_stats(&self) -> NetworkStats {
        NetworkStats {
            num_neurons: self.neurons.len(),
            num_synapses: self.synapses.len(),
            num_spikes: self.spike_history.len(),
            average_weight: if self.synapses.is_empty() {
                0.0
            } else {
                self.synapses.iter().map(|s| s.weight()).sum::<f64>() / self.synapses.len() as f64
            },
        }
    }

    /// Reset the network to initial state
    pub fn reset(&mut self) {
        for neuron in &mut self.neurons {
            neuron.reset();
        }
        for synapse in &mut self.synapses {
            synapse.reset_spike_history();
        }
        self.spike_history.clear();
        self.current_time = 0.0;
    }
}

/// Network statistics for analysis
#[derive(Debug, Clone)]
pub struct NetworkStats {
    /// Total number of neurons
    pub num_neurons: usize,
    /// Total number of synapses
    pub num_synapses: usize,
    /// Total number of spikes recorded
    pub num_spikes: usize,
    /// Average synaptic weight
    pub average_weight: f64,
}

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

    #[test]
    fn test_spiking_clusterer_creation() {
        let clusterer = SpikingNeuralClusterer::new(3);
        assert_eq!(clusterer.num_clusters(), 3);
        assert_eq!(clusterer.spike_threshold(), 1.0);
        assert!(clusterer.is_stdp_enabled());
        assert!(clusterer.is_lateral_inhibition_enabled());
    }

    #[test]
    fn test_clusterer_configuration() {
        let clusterer = SpikingNeuralClusterer::new(2)
            .with_spike_threshold(0.8)
            .with_stdp_learning(false)
            .with_lateral_inhibition(false)
            .with_training_params(50, 5.0);

        assert_eq!(clusterer.spike_threshold(), 0.8);
        assert!(!clusterer.is_stdp_enabled());
        assert!(!clusterer.is_lateral_inhibition_enabled());
        assert_eq!(clusterer.max_epochs, 50);
        assert_eq!(clusterer.simulation_duration, 5.0);
    }

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

        let mut clusterer = SpikingNeuralClusterer::new(2).with_training_params(5, 1.0); // Reduced for test speed

        let result = clusterer.fit(&points.view());
        assert!(result.is_ok());

        let (assignments, spike_events) = result.expect("Operation failed");
        assert_eq!(assignments.len(), 4);

        // Should have recorded some spike events
        assert!(!spike_events.is_empty());
    }

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

        let result = clusterer.fit(&points.view());
        assert!(result.is_err());
    }

    #[test]
    fn test_network_initialization() {
        let mut clusterer = SpikingNeuralClusterer::new(2);
        clusterer.initialize_network(3).expect("Operation failed");

        let stats = clusterer.network_stats();
        assert_eq!(stats.num_neurons, 5); // 3 input + 2 output

        // Should have input-to-output connections
        let expected_connections = 3 * 2; // input_dims * num_clusters
                                          // Plus lateral inhibition connections: num_clusters * (num_clusters - 1)
        let lateral_connections = 2;
        assert_eq!(
            stats.num_synapses,
            expected_connections + lateral_connections
        );
    }

    #[test]
    fn test_spike_encoding() {
        let clusterer = SpikingNeuralClusterer::new(2);
        let point = Array1::from_vec(vec![1.0, -1.0]);

        let spike_train = clusterer
            .encode_point_as_spikes(&point)
            .expect("Operation failed");

        // Should generate spikes for each dimension
        assert!(!spike_train.is_empty());

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

    #[test]
    fn test_network_reset() {
        let mut clusterer = SpikingNeuralClusterer::new(2);
        clusterer.initialize_network(2).expect("Operation failed");

        // Add some activity
        clusterer
            .spike_history
            .push(SpikeEvent::new(0, 1.0, 1.0, vec![0.0, 0.0]));
        clusterer.current_time = 100.0;

        // Reset should clear history and time
        clusterer.reset();
        assert!(clusterer.spike_history().is_empty());
        assert_eq!(clusterer.current_time, 0.0);
    }

    #[test]
    fn test_network_stats() {
        let mut clusterer = SpikingNeuralClusterer::new(2);
        clusterer.initialize_network(3).expect("Operation failed");

        let stats = clusterer.network_stats();
        assert_eq!(stats.num_neurons, 5);
        assert!(stats.num_synapses > 0);
        assert_eq!(stats.num_spikes, 0); // No activity yet
        assert!(stats.average_weight.is_finite()); // Allow negative weights for inhibitory synapses
    }
}