Skip to main content

sc_neurocore_engine/analysis/
basic.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — Basic spike train operations
8
9/// Extract spike times (seconds) from a binary 0/1 array.
10pub fn spike_times(binary_train: &[i32], dt: f64) -> Vec<f64> {
11    binary_train
12        .iter()
13        .enumerate()
14        .filter(|(_, &s)| s > 0)
15        .map(|(i, _)| i as f64 * dt)
16        .collect()
17}
18
19/// Inter-spike intervals (seconds) from a binary train.
20pub fn isi(binary_train: &[i32], dt: f64) -> Vec<f64> {
21    let times = spike_times(binary_train, dt);
22    if times.len() < 2 {
23        return vec![];
24    }
25    times.windows(2).map(|w| w[1] - w[0]).collect()
26}
27
28/// Mean firing rate (Hz).
29pub fn firing_rate(binary_train: &[i32], dt: f64) -> f64 {
30    let duration = binary_train.len() as f64 * dt;
31    if duration <= 0.0 {
32        return 0.0;
33    }
34    let count: i64 = binary_train.iter().map(|&s| s as i64).sum();
35    count as f64 / duration
36}
37
38/// Total spike count.
39pub fn spike_count(binary_train: &[i32]) -> i64 {
40    let mut total = 0_i64;
41    let mut chunks = binary_train.chunks_exact(4);
42    for c in chunks.by_ref() {
43        total += (c[0] + c[1] + c[2] + c[3]) as i64;
44    }
45    for &s in chunks.remainder() {
46        total += s as i64;
47    }
48    total
49}
50
51/// Bin a binary spike train into spike counts per bin.
52pub fn bin_spike_train(binary_train: &[i32], bin_size: usize) -> Vec<i64> {
53    let n = binary_train.len();
54    let n_bins = n / bin_size;
55    if n_bins == 0 {
56        return vec![binary_train.iter().map(|&s| s as i64).sum()];
57    }
58    let mut res = Vec::with_capacity(n_bins);
59    for i in 0..n_bins {
60        let chunk = &binary_train[i * bin_size..(i + 1) * bin_size];
61        let mut total = 0_i64;
62        let mut c_iter = chunk.chunks_exact(4);
63        for c in c_iter.by_ref() {
64            total += (c[0] + c[1] + c[2] + c[3]) as i64;
65        }
66        for &s in c_iter.remainder() {
67            total += s as i64;
68        }
69        res.push(total);
70    }
71    res
72}
73
74// Also accept f64 spike trains (common in Python pipelines)
75
76/// Spike times from f64 binary train (threshold > 0.5).
77pub fn spike_times_f64(binary_train: &[f64], dt: f64) -> Vec<f64> {
78    binary_train
79        .iter()
80        .enumerate()
81        .filter(|(_, &s)| s > 0.5)
82        .map(|(i, _)| i as f64 * dt)
83        .collect()
84}
85
86/// ISI from f64 binary train.
87pub fn isi_f64(binary_train: &[f64], dt: f64) -> Vec<f64> {
88    let times = spike_times_f64(binary_train, dt);
89    if times.len() < 2 {
90        return vec![];
91    }
92    times.windows(2).map(|w| w[1] - w[0]).collect()
93}
94
95/// Firing rate from f64 binary train.
96pub fn firing_rate_f64(binary_train: &[f64], dt: f64) -> f64 {
97    let duration = binary_train.len() as f64 * dt;
98    if duration <= 0.0 {
99        return 0.0;
100    }
101    let count: f64 = binary_train.iter().sum();
102    count / duration
103}
104
105/// Spike count from f64 binary train.
106pub fn spike_count_f64(binary_train: &[f64]) -> i64 {
107    binary_train.iter().filter(|&&s| s > 0.5).count() as i64
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_spike_times_basic() {
116        let train = vec![0, 1, 0, 0, 1, 1, 0];
117        let times = spike_times(&train, 0.001);
118        assert_eq!(times.len(), 3);
119        assert!((times[0] - 0.001).abs() < 1e-12);
120        assert!((times[1] - 0.004).abs() < 1e-12);
121        assert!((times[2] - 0.005).abs() < 1e-12);
122    }
123
124    #[test]
125    fn test_spike_times_empty() {
126        let train = vec![0, 0, 0];
127        assert!(spike_times(&train, 0.001).is_empty());
128    }
129
130    #[test]
131    fn test_isi_basic() {
132        let train = vec![0, 1, 0, 0, 1, 0];
133        let intervals = isi(&train, 0.001);
134        assert_eq!(intervals.len(), 1);
135        assert!((intervals[0] - 0.003).abs() < 1e-12);
136    }
137
138    #[test]
139    fn test_isi_single_spike() {
140        let train = vec![0, 1, 0];
141        assert!(isi(&train, 0.001).is_empty());
142    }
143
144    #[test]
145    fn test_firing_rate() {
146        let train = vec![1, 0, 1, 0, 1, 0, 1, 0, 1, 0];
147        let rate = firing_rate(&train, 0.001);
148        assert!((rate - 500.0).abs() < 0.01);
149    }
150
151    #[test]
152    fn test_firing_rate_zero() {
153        let train = vec![0, 0, 0];
154        assert_eq!(firing_rate(&train, 0.001), 0.0);
155    }
156
157    #[test]
158    fn test_spike_count() {
159        let train = vec![1, 0, 1, 1, 0, 1];
160        assert_eq!(spike_count(&train), 4);
161    }
162
163    #[test]
164    fn test_bin_spike_train() {
165        let train = vec![1, 0, 1, 1, 0, 0, 1, 1, 1, 0];
166        let bins = bin_spike_train(&train, 5);
167        assert_eq!(bins, vec![3, 3]);
168    }
169
170    #[test]
171    fn test_bin_spike_train_remainder() {
172        let train = vec![1, 1, 1, 1, 1, 1, 1];
173        let bins = bin_spike_train(&train, 3);
174        assert_eq!(bins, vec![3, 3]); // last 1 trimmed
175    }
176
177    #[test]
178    fn test_f64_variants() {
179        let train = vec![0.0, 1.0, 0.0, 1.0, 0.0];
180        assert_eq!(spike_count_f64(&train), 2);
181        assert!((firing_rate_f64(&train, 0.001) - 400.0).abs() < 0.01);
182        assert_eq!(spike_times_f64(&train, 0.001).len(), 2);
183        assert_eq!(isi_f64(&train, 0.001).len(), 1);
184    }
185}