sc_neurocore_engine/analysis/
patterns.rs1use rayon::prelude::*;
12
13pub fn spike_directionality(times_a: &[f64], times_b: &[f64], t_start: f64, t_end: f64) -> f64 {
14 let mut ta: Vec<f64> = times_a
15 .iter()
16 .copied()
17 .filter(|&t| t >= t_start && t <= t_end)
18 .collect();
19 let tb: Vec<f64> = times_b
20 .iter()
21 .copied()
22 .filter(|&t| t >= t_start && t <= t_end)
23 .collect();
24 ta.sort_by(|a, b| a.partial_cmp(b).unwrap());
25
26 if ta.is_empty() || tb.is_empty() {
27 return 0.0;
28 }
29
30 let mut lead_a = 0_usize;
31 let mut lead_b = 0_usize;
32
33 let mut tb_sorted = tb.clone();
35 tb_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
36
37 for &t in &ta {
38 let idx = tb_sorted.partition_point(|&x| x < t);
39
40 let nearest_after = if idx < tb_sorted.len() {
41 Some(tb_sorted[idx] - t)
42 } else {
43 None
44 };
45
46 let nearest_before = if idx > 0 {
47 Some(t - tb_sorted[idx - 1])
48 } else {
49 None
50 };
51
52 if let (Some(nb), Some(na)) = (nearest_before, nearest_after) {
53 if nb < na {
54 lead_b += 1;
55 } else {
56 lead_a += 1;
57 }
58 }
59 }
60
61 let total = lead_a + lead_b;
62 if total == 0 {
63 return 0.0;
64 }
65 (lead_a as f64 - lead_b as f64) / total as f64
66}
67
68pub fn spike_train_order(times_list: &[&[f64]], t_start: f64, t_end: f64) -> Vec<f64> {
71 let n = times_list.len();
72 let mut mat = vec![0.0_f64; n * n];
73 mat.par_chunks_exact_mut(n)
74 .enumerate()
75 .for_each(|(i, row)| {
76 for j in 0..n {
77 if i == j {
78 continue;
79 }
80 if j > i {
83 let d = spike_directionality(times_list[i], times_list[j], t_start, t_end);
84 row[j] = d;
85 } else {
86 let d = spike_directionality(times_list[j], times_list[i], t_start, t_end);
87 row[j] = -d;
88 }
89 }
90 });
91 mat
92}
93
94pub fn cubic_higher_order(binary_train: &[i32], dt: f64, max_lag: usize) -> Vec<f64> {
97 let _ = dt;
98 let n = binary_train.len();
99 let mean: f64 = binary_train.iter().map(|&v| v as f64).sum::<f64>() / n as f64;
100 let x: Vec<f64> = binary_train.iter().map(|&v| v as f64 - mean).collect();
101
102 let mut c3 = vec![0.0_f64; max_lag * max_lag];
103 c3.par_chunks_exact_mut(max_lag)
104 .enumerate()
105 .for_each(|(t1, row)| {
106 for t2 in 0..max_lag {
107 let valid_n = n.saturating_sub(t1.max(t2));
108 if valid_n == 0 {
109 continue;
110 }
111 let mut sum = 0.0_f64;
112 let mut k = 0;
113 while k + 3 < valid_n {
114 sum += x[k] * x[k + t1] * x[k + t2];
115 sum += x[k + 1] * x[k + 1 + t1] * x[k + 1 + t2];
116 sum += x[k + 2] * x[k + 2 + t1] * x[k + 2 + t2];
117 sum += x[k + 3] * x[k + 3 + t1] * x[k + 3 + t2];
118 k += 4;
119 }
120 while k < valid_n {
121 sum += x[k] * x[k + t1] * x[k + t2];
122 k += 1;
123 }
124 row[t2] = sum / valid_n as f64;
125 }
126 });
127 c3
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
137 fn test_directionality_a_leads() {
138 let ta = vec![0.1, 0.3, 0.5, 0.7];
140 let tb = vec![0.15, 0.35, 0.55, 0.75];
141 let d = spike_directionality(&ta, &tb, 0.0, 1.0);
142 assert!(d > 0.0, "A leads B → positive, got {d}");
143 }
144
145 #[test]
146 fn test_directionality_b_leads() {
147 let ta = vec![0.15, 0.35, 0.55, 0.75];
148 let tb = vec![0.1, 0.3, 0.5, 0.7];
149 let d = spike_directionality(&ta, &tb, 0.0, 1.0);
150 assert!(d < 0.0, "B leads A → negative, got {d}");
151 }
152
153 #[test]
154 fn test_directionality_empty() {
155 assert_eq!(spike_directionality(&[], &[0.5], 0.0, 1.0), 0.0);
156 }
157
158 #[test]
159 fn test_directionality_antisymmetric() {
160 let ta = vec![0.1, 0.3, 0.5];
161 let tb = vec![0.15, 0.35, 0.55];
162 let d_ab = spike_directionality(&ta, &tb, 0.0, 1.0);
163 let d_ba = spike_directionality(&tb, &ta, 0.0, 1.0);
164 assert!(d_ab > 0.0 && d_ba < 0.0, "should have opposite signs");
166 }
167
168 #[test]
171 fn test_order_antisymmetric() {
172 let ta = vec![0.1, 0.3, 0.5];
173 let tb = vec![0.15, 0.35, 0.55];
174 let trains: Vec<&[f64]> = vec![&ta, &tb];
175 let mat = spike_train_order(&trains, 0.0, 1.0);
176 assert!((mat[1] + mat[2]).abs() < 1e-10, "antisymmetric");
177 assert_eq!(mat[0], 0.0, "diagonal = 0");
178 }
179
180 #[test]
181 fn test_order_shape() {
182 let ta = vec![0.1];
183 let tb = vec![0.2];
184 let tc = vec![0.3];
185 let trains: Vec<&[f64]> = vec![&ta, &tb, &tc];
186 let mat = spike_train_order(&trains, 0.0, 1.0);
187 assert_eq!(mat.len(), 9);
188 }
189
190 #[test]
193 fn test_c3_shape() {
194 let train = vec![0i32, 1, 0, 1, 0, 0, 1, 0, 1, 0];
195 let c3 = cubic_higher_order(&train, 0.001, 5);
196 assert_eq!(c3.len(), 25);
197 }
198
199 #[test]
200 fn test_c3_zero_lag_positive() {
201 let train = vec![0i32, 0, 0, 0, 0, 0, 0, 0, 0, 1];
203 let c3 = cubic_higher_order(&train, 0.001, 3);
204 assert!(
205 c3[0] > 0.0,
206 "C3(0,0) should be positive for skewed data, got {}",
207 c3[0]
208 );
209 }
210
211 #[test]
212 fn test_c3_constant_zero() {
213 let train = vec![0i32; 100];
214 let c3 = cubic_higher_order(&train, 0.001, 5);
215 assert!(c3.iter().all(|&v| v.abs() < 1e-10), "constant → C3 = 0");
216 }
217}