sc_neurocore_engine/analysis/
dimensionality.rs1use super::basic;
10use rayon::prelude::*;
11
12fn symmetric_eigen(a: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
18 let mut mat = a.to_vec();
19 let mut vecs = vec![0.0f64; n * n];
20 for i in 0..n {
21 vecs[i * n + i] = 1.0;
22 }
23 let max_iter = 100 * n * n;
24 for _ in 0..max_iter {
25 let mut p = 0;
27 let mut q = 1;
28 let mut max_val = 0.0f64;
29 for i in 0..n {
30 for j in i + 1..n {
31 let v = mat[i * n + j].abs();
32 if v > max_val {
33 max_val = v;
34 p = i;
35 q = j;
36 }
37 }
38 }
39 if max_val < 1e-15 {
40 break;
41 }
42 let app = mat[p * n + p];
43 let aqq = mat[q * n + q];
44 let apq = mat[p * n + q];
45 let theta = if (app - aqq).abs() < 1e-30 {
46 std::f64::consts::FRAC_PI_4
47 } else {
48 0.5 * (2.0 * apq / (app - aqq)).atan()
49 };
50 let c = theta.cos();
51 let s = theta.sin();
52 for i in 0..n {
54 let i_off = i * n;
55 let ip = mat[i_off + p];
56 let iq = mat[i_off + q];
57 mat[i_off + p] = c * ip + s * iq;
58 mat[i_off + q] = -s * ip + c * iq;
59 }
60 let p_off = p * n;
61 let q_off = q * n;
62 for j in 0..n {
63 let pj = mat[p_off + j];
64 let qj = mat[q_off + j];
65 mat[p_off + j] = c * pj + s * qj;
66 mat[q_off + j] = -s * pj + c * qj;
67 }
68 for i in 0..n {
70 let i_off = i * n;
71 let vip = vecs[i_off + p];
72 let viq = vecs[i_off + q];
73 vecs[i_off + p] = c * vip + s * viq;
74 vecs[i_off + q] = -s * vip + c * viq;
75 }
76 }
77 let eigenvalues: Vec<f64> = (0..n).map(|i| mat[i * n + i]).collect();
78 let mut idx: Vec<usize> = (0..n).collect();
80 idx.sort_by(|&a, &b| eigenvalues[b].partial_cmp(&eigenvalues[a]).unwrap());
81 let sorted_vals: Vec<f64> = idx.iter().map(|&i| eigenvalues[i]).collect();
82 let mut sorted_vecs = vec![0.0f64; n * n];
83 for (new_col, &old_col) in idx.iter().enumerate() {
84 for row in 0..n {
85 sorted_vecs[row * n + new_col] = vecs[row * n + old_col];
86 }
87 }
88 (sorted_vals, sorted_vecs)
89}
90
91pub fn spike_train_pca(
97 trains: &[&[i32]],
98 n_components: usize,
99 bin_size: usize,
100) -> (Vec<f64>, Vec<f64>) {
101 if trains.is_empty() {
102 return (vec![], vec![]);
103 }
104 let binned: Vec<Vec<f64>> = trains
105 .iter()
106 .map(|t| {
107 basic::bin_spike_train(t, bin_size)
108 .into_iter()
109 .map(|c| c as f64)
110 .collect()
111 })
112 .collect();
113 let min_bins = binned.iter().map(|b| b.len()).min().unwrap_or(0);
114 if min_bins == 0 {
115 return (vec![], vec![]);
116 }
117 let d = trains.len(); let mut mat = vec![0.0f64; d * min_bins];
120 for i in 0..d {
121 let mean: f64 = binned[i][..min_bins].iter().sum::<f64>() / min_bins as f64;
122 for j in 0..min_bins {
123 mat[i * min_bins + j] = binned[i][j] - mean;
124 }
125 }
126 if d < 2 {
127 return (mat[..min_bins].to_vec(), vec![1.0]);
128 }
129 let mut cov = vec![0.0f64; d * d];
131 for i in 0..d {
132 for j in i..d {
133 let mut s = 0.0;
134 for t in 0..min_bins {
135 s += mat[i * min_bins + t] * mat[j * min_bins + t];
136 }
137 s /= (min_bins - 1).max(1) as f64;
138 cov[i * d + j] = s;
139 cov[j * d + i] = s;
140 }
141 }
142 let (eigvals, eigvecs) = symmetric_eigen(&cov, d);
143 let nc = n_components.min(d);
144 let total: f64 = eigvals.iter().sum();
145 let explained: Vec<f64> = eigvals[..nc]
146 .iter()
147 .map(|&v| if total > 0.0 { v / total } else { v })
148 .collect();
149 let mut projected = vec![0.0f64; nc * min_bins];
152 for c in 0..nc {
153 for t in 0..min_bins {
154 let mut s = 0.0;
155 for i in 0..d {
156 s += eigvecs[i * d + c] * mat[i * min_bins + t];
157 }
158 projected[c * min_bins + t] = s;
159 }
160 }
161 (projected, explained)
162}
163
164pub fn demixed_pca(
169 conditions: &[Vec<&[i32]>],
170 n_components: usize,
171 bin_size: usize,
172) -> (Vec<f64>, Vec<f64>) {
173 if conditions.len() < 2 {
174 return (vec![], vec![]);
175 }
176 let mut all_means: Vec<Vec<f64>> = Vec::new();
178 for trains in conditions {
179 let binned: Vec<Vec<f64>> = trains
180 .iter()
181 .map(|t| {
182 basic::bin_spike_train(t, bin_size)
183 .into_iter()
184 .map(|c| c as f64)
185 .collect()
186 })
187 .collect();
188 let min_bins = binned.iter().map(|b| b.len()).min().unwrap_or(0);
189 if min_bins == 0 {
190 continue;
191 }
192 let n = binned.len();
193 let mut mean = vec![0.0f64; min_bins];
194 for b in &binned {
195 for j in 0..min_bins {
196 mean[j] += b[j];
197 }
198 }
199 for v in &mut mean {
200 *v /= n as f64;
201 }
202 all_means.push(mean);
203 }
204 if all_means.len() < 2 {
205 return (vec![], vec![]);
206 }
207 let min_bins = all_means.iter().map(|m| m.len()).min().unwrap();
208 let n_cond = all_means.len();
209 let mut grand = vec![0.0f64; min_bins];
211 for m in &all_means {
212 for j in 0..min_bins {
213 grand[j] += m[j];
214 }
215 }
216 for v in &mut grand {
217 *v /= n_cond as f64;
218 }
219 let mut mean_mat = vec![0.0f64; n_cond * min_bins];
221 for i in 0..n_cond {
222 for j in 0..min_bins {
223 mean_mat[i * min_bins + j] = all_means[i][j] - grand[j];
224 }
225 }
226 let t = min_bins;
228 let mut cov = vec![0.0f64; t * t];
229 let n_cond_f = n_cond as f64;
230
231 let mut m_cols = vec![vec![0.0_f64; n_cond]; t];
233 for c in 0..n_cond {
234 for i in 0..t {
235 m_cols[i][c] = mean_mat[c * t + i];
236 }
237 }
238
239 cov.par_chunks_exact_mut(t)
240 .enumerate()
241 .for_each(|(i, row)| {
242 for j in i..t {
243 let dot = crate::simd::dot_f64_dispatch(&m_cols[i], &m_cols[j]);
244 row[j] = dot / n_cond_f;
245 }
246 });
247 for i in 0..t {
249 for j in (i + 1)..t {
250 cov[j * t + i] = cov[i * t + j];
251 }
252 }
253 let (eigvals, eigvecs) = symmetric_eigen(&cov, t);
254 let nc = n_components.min(t);
255 let total: f64 = eigvals.iter().sum();
256 let explained: Vec<f64> = eigvals[..nc]
257 .iter()
258 .map(|&v| if total > 0.0 { v / total } else { v })
259 .collect();
260 let mut projected = vec![0.0f64; n_cond * nc];
262 for c in 0..n_cond {
263 for k in 0..nc {
264 let mut s = 0.0;
265 for j in 0..t {
266 s += mean_mat[c * t + j] * eigvecs[j * t + k];
267 }
268 projected[c * nc + k] = s;
269 }
270 }
271 (projected, explained)
272}
273
274pub fn factor_analysis(
278 trains: &[&[i32]],
279 n_factors: usize,
280 bin_size: usize,
281 n_iter: usize,
282) -> (Vec<f64>, Vec<f64>) {
283 let d = trains.len();
284 if d == 0 {
285 return (vec![], vec![]);
286 }
287 let binned: Vec<Vec<f64>> = trains
288 .iter()
289 .map(|t| {
290 basic::bin_spike_train(t, bin_size)
291 .into_iter()
292 .map(|c| c as f64)
293 .collect()
294 })
295 .collect();
296 let t = binned.iter().map(|b| b.len()).min().unwrap_or(0);
297 if t == 0 {
298 return (vec![0.0; d * n_factors], vec![1.0; d]);
299 }
300 let mut mat = vec![0.0f64; d * t];
302 for i in 0..d {
303 let mean: f64 = binned[i][..t].iter().sum::<f64>() / t as f64;
304 for j in 0..t {
305 mat[i * t + j] = binned[i][j] - mean;
306 }
307 }
308 let mut cov = vec![0.0f64; d * d];
310 for i in 0..d {
311 for j in i..d {
312 let mut s = 0.0;
313 for k in 0..t {
314 s += mat[i * t + k] * mat[j * t + k];
315 }
316 s /= t as f64;
317 cov[i * d + j] = s;
318 cov[j * d + i] = s;
319 }
320 }
321 let nf = n_factors.min(d);
322 let mut psi: Vec<f64> = (0..d).map(|i| cov[i * d + i]).collect();
323 let mut loadings = vec![0.0f64; d * nf];
325 let mut rng = 42u64;
326 for v in &mut loadings {
327 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
328 *v = ((rng >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.2;
329 }
330
331 for _ in 0..n_iter {
332 let psi_inv: Vec<f64> = psi.iter().map(|&p| 1.0 / (p + 1e-10)).collect();
334
335 let mut m = vec![0.0f64; nf * nf];
337 for i in 0..nf {
338 for j in 0..nf {
339 let mut s = 0.0;
340 for k in 0..d {
341 s += loadings[k * nf + i] * psi_inv[k] * loadings[k * nf + j];
342 }
343 m[i * nf + j] = s + if i == j { 1.0 } else { 0.0 };
344 }
345 }
346 let m_inv = mat_inv_small(&m, nf);
347
348 let mut beta = vec![0.0f64; nf * d];
350 for i in 0..nf {
351 for j in 0..d {
352 let mut s = 0.0;
353 for k in 0..nf {
354 s += m_inv[i * nf + k] * loadings[j * nf + k] * psi_inv[j];
355 }
356 beta[i * d + j] = s;
357 }
358 }
359
360 let mut ez = vec![0.0f64; nf * t];
362 for i in 0..nf {
363 for j in 0..t {
364 let mut s = 0.0;
365 for k in 0..d {
366 s += beta[i * d + k] * mat[k * t + j];
367 }
368 ez[i * t + j] = s;
369 }
370 }
371
372 let mut ezzt = vec![0.0f64; nf * nf];
374 for i in 0..nf {
375 for j in 0..nf {
376 let mut s = 0.0;
377 for k in 0..t {
378 s += ez[i * t + k] * ez[j * t + k];
379 }
380 ezzt[i * nf + j] = nf as f64 * m_inv[i * nf + j] + s / t as f64;
384 }
385 }
386
387 let mut mat_ez_t = vec![0.0f64; d * nf];
393 for i in 0..d {
394 for j in 0..nf {
395 let mut s = 0.0;
396 for k in 0..t {
397 s += mat[i * t + k] * ez[j * t + k];
398 }
399 mat_ez_t[i * nf + j] = s / t as f64;
400 }
401 }
402 let ezzt_inv = mat_inv_small(&ezzt, nf);
404 for i in 0..d {
406 for j in 0..nf {
407 let mut s = 0.0;
408 for k in 0..nf {
409 s += mat_ez_t[i * nf + k] * ezzt_inv[k * nf + j];
410 }
411 loadings[i * nf + j] = s;
412 }
413 }
414
415 let mut l_ez = vec![0.0f64; d * t];
418 for i in 0..d {
419 for j in 0..t {
420 let mut s = 0.0;
421 for k in 0..nf {
422 s += loadings[i * nf + k] * ez[k * t + j];
423 }
424 l_ez[i * t + j] = s;
425 }
426 }
427 for i in 0..d {
428 let mut s = 0.0;
429 for k in 0..t {
430 s += l_ez[i * t + k] * mat[i * t + k];
431 }
432 psi[i] = (cov[i * d + i] - s / t as f64).max(1e-6);
433 }
434 }
435
436 (loadings, psi)
437}
438
439fn mat_inv_small(a: &[f64], n: usize) -> Vec<f64> {
441 let mut aug = vec![0.0f64; n * 2 * n];
442 for i in 0..n {
443 for j in 0..n {
444 aug[i * 2 * n + j] = a[i * n + j];
445 }
446 aug[i * 2 * n + n + i] = 1.0;
447 }
448 for col in 0..n {
449 let mut max_row = col;
450 let mut max_val = aug[col * 2 * n + col].abs();
451 for row in col + 1..n {
452 let v = aug[row * 2 * n + col].abs();
453 if v > max_val {
454 max_val = v;
455 max_row = row;
456 }
457 }
458 if max_val < 1e-30 {
459 continue;
460 }
461 if max_row != col {
462 for k in 0..2 * n {
463 aug.swap(col * 2 * n + k, max_row * 2 * n + k);
464 }
465 }
466 let pivot = aug[col * 2 * n + col];
467 for k in 0..2 * n {
468 aug[col * 2 * n + k] /= pivot;
469 }
470 for row in 0..n {
471 if row == col {
472 continue;
473 }
474 let factor = aug[row * 2 * n + col];
475 for k in 0..2 * n {
476 aug[row * 2 * n + k] -= factor * aug[col * 2 * n + k];
477 }
478 }
479 }
480 let mut inv = vec![0.0f64; n * n];
481 for i in 0..n {
482 for j in 0..n {
483 inv[i * n + j] = aug[i * 2 * n + n + j];
484 }
485 }
486 inv
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 fn make_trains() -> Vec<Vec<i32>> {
494 let mut trains = Vec::new();
496 for n in 0..5 {
497 let mut t = vec![0i32; 200];
498 let step = 5 + n * 3;
499 for i in (0..200).step_by(step) {
500 t[i] = 1;
501 }
502 trains.push(t);
503 }
504 trains
505 }
506
507 #[test]
508 fn test_spike_train_pca_basic() {
509 let trains = make_trains();
510 let refs: Vec<&[i32]> = trains.iter().map(|t| t.as_slice()).collect();
511 let (proj, explained) = spike_train_pca(&refs, 3, 10);
512 assert_eq!(explained.len(), 3);
513 let total: f64 = explained.iter().sum();
515 assert!(total <= 1.0 + 1e-6, "Total explained {total} > 1");
516 assert!(explained[0] >= explained[1]);
518 assert!(!proj.is_empty());
519 }
520
521 #[test]
522 fn test_spike_train_pca_empty() {
523 let (proj, expl) = spike_train_pca(&[], 3, 10);
524 assert!(proj.is_empty());
525 assert!(expl.is_empty());
526 }
527
528 #[test]
529 fn test_spike_train_pca_single_neuron() {
530 let train = vec![1, 0, 1, 0, 1, 0, 1, 0, 1, 0];
531 let refs = vec![train.as_slice()];
532 let (proj, expl) = spike_train_pca(&refs, 1, 2);
533 assert_eq!(expl.len(), 1);
535 assert!(!proj.is_empty());
536 }
537
538 #[test]
539 fn test_demixed_pca_basic() {
540 let trains_a = make_trains();
541 let trains_b: Vec<Vec<i32>> = (0..5)
542 .map(|n| {
543 let mut t = vec![0i32; 200];
544 let step = 3 + n * 2;
545 for i in (0..200).step_by(step) {
546 t[i] = 1;
547 }
548 t
549 })
550 .collect();
551 let cond_a: Vec<&[i32]> = trains_a.iter().map(|t| t.as_slice()).collect();
552 let cond_b: Vec<&[i32]> = trains_b.iter().map(|t| t.as_slice()).collect();
553 let conditions = vec![cond_a, cond_b];
554 let (proj, expl) = demixed_pca(&conditions, 2, 10);
555 assert!(!expl.is_empty());
556 assert!(!proj.is_empty());
557 }
558
559 #[test]
560 fn test_demixed_pca_single_condition() {
561 let t = [vec![1, 0, 1, 0]];
562 let refs: Vec<&[i32]> = t.iter().map(|v| v.as_slice()).collect();
563 let (proj, expl) = demixed_pca(&[refs], 2, 2);
564 assert!(proj.is_empty());
565 assert!(expl.is_empty());
566 }
567
568 #[test]
569 fn test_factor_analysis_basic() {
570 let trains = make_trains();
571 let refs: Vec<&[i32]> = trains.iter().map(|t| t.as_slice()).collect();
572 let (loadings, psi) = factor_analysis(&refs, 2, 10, 20);
573 assert_eq!(loadings.len(), 5 * 2);
574 assert_eq!(psi.len(), 5);
575 assert!(psi.iter().all(|&p| p > 0.0));
577 }
578
579 #[test]
580 fn test_factor_analysis_empty() {
581 let (l, p) = factor_analysis(&[], 2, 10, 20);
582 assert!(l.is_empty());
583 assert!(p.is_empty());
584 }
585
586 #[test]
587 fn test_symmetric_eigen_identity() {
588 let eye = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
589 let (vals, _) = symmetric_eigen(&eye, 3);
590 for v in &vals {
591 assert!((v - 1.0).abs() < 1e-10);
592 }
593 }
594
595 #[test]
596 fn test_symmetric_eigen_known() {
597 let a = vec![2.0, 1.0, 1.0, 2.0];
599 let (vals, _) = symmetric_eigen(&a, 2);
600 assert!((vals[0] - 3.0).abs() < 1e-10);
601 assert!((vals[1] - 1.0).abs() < 1e-10);
602 }
603
604 #[test]
605 fn test_pca_explains_variance() {
606 let trains = make_trains();
607 let refs: Vec<&[i32]> = trains.iter().map(|t| t.as_slice()).collect();
608 let (_, explained) = spike_train_pca(&refs, 5, 10);
609 let total: f64 = explained.iter().sum();
611 assert!(
612 (total - 1.0).abs() < 0.05,
613 "Total explained {total} should be ~1.0"
614 );
615 }
616}