sc_neurocore_engine/analysis/
temporal.rs1use super::basic::{bin_spike_train, spike_times};
10
11pub fn burst_detection(
14 binary_train: &[i32],
15 dt: f64,
16 max_isi_ms: f64,
17 min_spikes: usize,
18) -> Vec<(f64, f64, usize)> {
19 let times = spike_times(binary_train, dt);
20 if times.len() < min_spikes {
21 return vec![];
22 }
23 let max_isi = max_isi_ms / 1000.0;
24 let intervals: Vec<f64> = times.windows(2).map(|w| w[1] - w[0]).collect();
25 let mut bursts = Vec::new();
26 let mut i = 0;
27 while i < intervals.len() {
28 if intervals[i] < max_isi {
29 let start = i;
30 while i < intervals.len() && intervals[i] < max_isi {
31 i += 1;
32 }
33 let n_spikes = i - start + 1;
34 if n_spikes >= min_spikes {
35 bursts.push((times[start], times[i], n_spikes));
36 }
37 } else {
38 i += 1;
39 }
40 }
41 bursts
42}
43
44pub fn first_spike_latency(binary_train: &[i32], dt: f64) -> f64 {
46 for (i, &v) in binary_train.iter().enumerate() {
47 if v > 0 {
48 return i as f64 * dt;
49 }
50 }
51 f64::NAN
52}
53
54pub fn response_onset(
57 binary_train: &[i32],
58 baseline_steps: usize,
59 dt: f64,
60 threshold_sigma: f64,
61) -> f64 {
62 if binary_train.len() <= baseline_steps {
63 return f64::NAN;
64 }
65 let baseline = &binary_train[..baseline_steps];
66 let mean: f64 = baseline.iter().map(|&v| v as f64).sum::<f64>() / baseline_steps as f64;
67 let std: f64 = (baseline
68 .iter()
69 .map(|&v| (v as f64 - mean).powi(2))
70 .sum::<f64>()
71 / baseline_steps as f64)
72 .sqrt()
73 .max(1e-10);
74 let threshold = mean + threshold_sigma * std;
75 for (i, &v) in binary_train[baseline_steps..].iter().enumerate() {
76 if v as f64 > threshold {
77 return (baseline_steps + i) as f64 * dt;
78 }
79 }
80 f64::NAN
81}
82
83pub fn change_point_detection(binary_train: &[i32], bin_size: usize, threshold: f64) -> Vec<usize> {
86 let counts: Vec<f64> = bin_spike_train(binary_train, bin_size)
87 .iter()
88 .map(|&v| v as f64)
89 .collect();
90 let n = counts.len();
91 if n < 5 {
92 return vec![];
93 }
94 let mean_rate: f64 = counts.iter().sum::<f64>() / n as f64;
95 let std_rate: f64 =
96 (counts.iter().map(|v| (v - mean_rate).powi(2)).sum::<f64>() / n as f64).sqrt();
97 if std_rate < 1e-10 {
98 return vec![];
99 }
100 let mut cusum_pos = 0.0_f64;
101 let mut cusum_neg = 0.0_f64;
102 let mut change_points = Vec::new();
103 for i in 1..n {
104 let z = (counts[i] - mean_rate) / std_rate;
105 cusum_pos = (cusum_pos + z).max(0.0);
106 cusum_neg = (cusum_neg - z).max(0.0);
107 if cusum_pos > threshold || cusum_neg > threshold {
108 change_points.push(i);
109 cusum_pos = 0.0;
110 cusum_neg = 0.0;
111 }
112 }
113 change_points
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn make_train(spikes: &[usize], len: usize) -> Vec<i32> {
121 let mut t = vec![0i32; len];
122 for &s in spikes {
123 t[s] = 1;
124 }
125 t
126 }
127
128 #[test]
129 fn test_burst_detection_regular() {
130 let train = make_train(&[10, 15, 20, 25, 30], 100);
132 let bursts = burst_detection(&train, 0.001, 10.0, 3);
133 assert_eq!(bursts.len(), 1);
134 assert_eq!(bursts[0].2, 5, "5 spikes in burst");
135 }
136
137 #[test]
138 fn test_burst_detection_no_burst() {
139 let train = make_train(&[10, 60, 110, 160], 200);
141 let bursts = burst_detection(&train, 0.001, 10.0, 3);
142 assert!(bursts.is_empty());
143 }
144
145 #[test]
146 fn test_burst_detection_too_few_spikes() {
147 let train = make_train(&[10], 100);
148 assert!(burst_detection(&train, 0.001, 10.0, 3).is_empty());
149 }
150
151 #[test]
152 fn test_first_spike_latency() {
153 let train = make_train(&[50], 100);
154 assert!((first_spike_latency(&train, 0.001) - 0.050).abs() < 1e-10);
155 }
156
157 #[test]
158 fn test_first_spike_latency_no_spike() {
159 let train = vec![0i32; 100];
160 assert!(first_spike_latency(&train, 0.001).is_nan());
161 }
162
163 #[test]
164 fn test_response_onset_detected() {
165 let mut train = vec![0i32; 200];
167 train[100] = 1;
168 let onset = response_onset(&train, 100, 0.001, 1.0);
169 assert!(!onset.is_nan(), "should detect onset");
170 assert!((onset - 0.100).abs() < 1e-10);
171 }
172
173 #[test]
174 fn test_response_onset_no_response() {
175 let train = vec![0i32; 200];
176 assert!(response_onset(&train, 100, 0.001, 3.0).is_nan());
177 }
178
179 #[test]
180 fn test_response_onset_too_short() {
181 let train = vec![0i32; 50];
182 assert!(response_onset(&train, 100, 0.001, 3.0).is_nan());
183 }
184
185 #[test]
186 fn test_change_point_step() {
187 let mut train = vec![0i32; 500];
189 for i in 250..500 {
190 if i % 2 == 0 {
191 train[i] = 1;
192 }
193 }
194 let cps = change_point_detection(&train, 50, 3.0);
195 assert!(!cps.is_empty(), "should detect rate change");
196 }
197
198 #[test]
199 fn test_change_point_constant() {
200 let train = vec![0i32; 500];
201 assert!(change_point_detection(&train, 50, 3.0).is_empty());
202 }
203
204 #[test]
205 fn test_change_point_too_short() {
206 let train = vec![1i32; 10];
207 assert!(change_point_detection(&train, 50, 3.0).is_empty());
208 }
209}