1use super::basic::isi;
10use super::rate::instantaneous_rate;
11
12pub fn van_rossum_distance(train_a: &[i32], train_b: &[i32], dt: f64, tau_ms: f64) -> f64 {
14 let tau = tau_ms / 1000.0;
15 let n = train_a.len().min(train_b.len());
16 if n == 0 || tau <= 0.0 {
17 return 0.0;
18 }
19
20 let decay: Vec<f64> = (0..n).map(|i| (-((i as f64) * dt) / tau).exp()).collect();
22
23 let fa = convolve_full_truncated(train_a, &decay, n);
25 let fb = convolve_full_truncated(train_b, &decay, n);
26
27 let sum_sq: f64 = fa.iter().zip(fb.iter()).map(|(a, b)| (a - b).powi(2)).sum();
28 (sum_sq * dt / tau).sqrt()
29}
30
31fn convolve_full_truncated(signal: &[i32], kernel: &[f64], out_len: usize) -> Vec<f64> {
32 let n = signal.len().min(out_len);
33 let mut result = vec![0.0_f64; out_len];
34 for i in 0..n {
35 if signal[i] == 0 {
36 continue;
37 }
38 let s = signal[i] as f64;
39 for (j, &k) in kernel.iter().enumerate() {
40 let idx = i + j;
41 if idx >= out_len {
42 break;
43 }
44 result[idx] += s * k;
45 }
46 }
47 result
48}
49
50pub fn victor_purpura_distance(times_a: &[f64], times_b: &[f64], cost_per_s: f64) -> f64 {
53 let na = times_a.len();
54 let nb = times_b.len();
55 if na == 0 {
56 return nb as f64;
57 }
58 if nb == 0 {
59 return na as f64;
60 }
61
62 let mut d = vec![vec![0.0_f64; nb + 1]; na + 1];
64 for i in 0..=na {
65 d[i][0] = i as f64;
66 }
67 for j in 0..=nb {
68 d[0][j] = j as f64;
69 }
70 for i in 1..=na {
71 for j in 1..=nb {
72 let shift_cost = cost_per_s * (times_a[i - 1] - times_b[j - 1]).abs();
73 d[i][j] = (d[i - 1][j] + 1.0)
74 .min(d[i][j - 1] + 1.0)
75 .min(d[i - 1][j - 1] + shift_cost);
76 }
77 }
78 d[na][nb]
79}
80
81pub fn isi_distance(train_a: &[i32], train_b: &[i32], dt: f64) -> f64 {
83 let isi_a = isi(train_a, dt);
84 let isi_b = isi(train_b, dt);
85 let n = isi_a.len().min(isi_b.len());
86 if n == 0 {
87 return f64::NAN;
88 }
89 let sum: f64 = (0..n)
90 .map(|i| {
91 let a = isi_a[i];
92 let b = isi_b[i];
93 if a == 0.0 && b == 0.0 {
94 0.0
95 } else if a <= b {
96 if b > 0.0 {
97 (a / b - 1.0).abs()
98 } else {
99 0.0
100 }
101 } else if a > 0.0 {
102 (b / a - 1.0).abs()
103 } else {
104 0.0
105 }
106 })
107 .sum();
108 sum / n as f64
109}
110
111pub fn spike_distance(times_a: &[f64], times_b: &[f64], t_start: f64, t_end: f64) -> f64 {
114 let mut ta: Vec<f64> = times_a
115 .iter()
116 .copied()
117 .filter(|&t| t >= t_start && t <= t_end)
118 .collect();
119 let mut tb: Vec<f64> = times_b
120 .iter()
121 .copied()
122 .filter(|&t| t >= t_start && t <= t_end)
123 .collect();
124 ta.sort_by(|a, b| a.partial_cmp(b).unwrap());
125 tb.sort_by(|a, b| a.partial_cmp(b).unwrap());
126
127 if ta.is_empty() && tb.is_empty() {
128 return 0.0;
129 }
130 if ta.is_empty() || tb.is_empty() {
131 return 1.0;
132 }
133
134 let n_eval = 100_usize;
135 let step = (t_end - t_start) / (n_eval - 1).max(1) as f64;
136 let mut sum = 0.0_f64;
137
138 for k in 0..n_eval {
139 let t = t_start + k as f64 * step;
140 let idx_a = ta.partition_point(|&x| x <= t);
141 let idx_b = tb.partition_point(|&x| x <= t);
142
143 let prev_a = if idx_a > 0 { ta[idx_a - 1] } else { t_start };
144 let next_a = if idx_a < ta.len() { ta[idx_a] } else { t_end };
145 let prev_b = if idx_b > 0 { tb[idx_b - 1] } else { t_start };
146 let next_b = if idx_b < tb.len() { tb[idx_b] } else { t_end };
147
148 let isi_a = (next_a - prev_a).max(1e-30);
149 let isi_b = (next_b - prev_b).max(1e-30);
150
151 let da = (t - prev_a).abs().min((t - next_a).abs());
152 let db = (t - prev_b).abs().min((t - next_b).abs());
153
154 sum += (da / isi_a - db / isi_b).abs();
155 }
156 sum / n_eval as f64
157}
158
159fn local_isi(times: &[f64], idx: usize) -> f64 {
160 if times.len() < 2 {
161 return 1.0;
162 }
163 if idx == 0 {
164 return times[1] - times[0];
165 }
166 if idx >= times.len() - 1 {
167 return times[times.len() - 1] - times[times.len() - 2];
168 }
169 (times[idx] - times[idx - 1]).min(times[idx + 1] - times[idx])
170}
171
172pub fn spike_sync(times_a: &[f64], times_b: &[f64], t_start: f64, t_end: f64) -> f64 {
174 let mut ta: Vec<f64> = times_a
175 .iter()
176 .copied()
177 .filter(|&t| t >= t_start && t <= t_end)
178 .collect();
179 let mut tb: Vec<f64> = times_b
180 .iter()
181 .copied()
182 .filter(|&t| t >= t_start && t <= t_end)
183 .collect();
184 ta.sort_by(|a, b| a.partial_cmp(b).unwrap());
185 tb.sort_by(|a, b| a.partial_cmp(b).unwrap());
186
187 if ta.is_empty() || tb.is_empty() {
188 return 0.0;
189 }
190
191 let total_possible = ta.len() + tb.len();
192 let mut total_coincidences = 0_usize;
193
194 for (i, &t) in ta.iter().enumerate() {
196 let j = nearest_idx(&tb, t);
197 let isi_a = local_isi(&ta, i);
198 let isi_b = local_isi(&tb, j);
199 let tau = isi_a.min(isi_b) / 2.0;
200 if tau > 0.0 && (tb[j] - t).abs() < tau {
201 total_coincidences += 1;
202 }
203 }
204
205 for (j, &t) in tb.iter().enumerate() {
207 let i = nearest_idx(&ta, t);
208 let isi_a = local_isi(&ta, i);
209 let isi_b = local_isi(&tb, j);
210 let tau = isi_a.min(isi_b) / 2.0;
211 if tau > 0.0 && (ta[i] - t).abs() < tau {
212 total_coincidences += 1;
213 }
214 }
215
216 if total_possible == 0 {
217 return 0.0;
218 }
219 total_coincidences as f64 / total_possible as f64
220}
221
222fn nearest_idx(sorted: &[f64], target: f64) -> usize {
223 if sorted.is_empty() {
224 return 0;
225 }
226 let pos = sorted.partition_point(|&x| x < target);
227 if pos == 0 {
228 return 0;
229 }
230 if pos >= sorted.len() {
231 return sorted.len() - 1;
232 }
233 if (sorted[pos] - target).abs() < (sorted[pos - 1] - target).abs() {
234 pos
235 } else {
236 pos - 1
237 }
238}
239
240pub fn spike_sync_profile(
242 times_a: &[f64],
243 times_b: &[f64],
244 n_bins: usize,
245 t_start: f64,
246 t_end: f64,
247) -> Vec<f64> {
248 let mut profile = vec![0.0_f64; n_bins];
249 let bin_width = (t_end - t_start) / n_bins as f64;
250 for k in 0..n_bins {
251 let lo = t_start + k as f64 * bin_width;
252 let hi = lo + bin_width;
253 let sub_a: Vec<f64> = times_a
254 .iter()
255 .copied()
256 .filter(|&t| t >= lo && t < hi)
257 .collect();
258 let sub_b: Vec<f64> = times_b
259 .iter()
260 .copied()
261 .filter(|&t| t >= lo && t < hi)
262 .collect();
263 if !sub_a.is_empty() || !sub_b.is_empty() {
264 profile[k] = spike_sync(&sub_a, &sub_b, lo, hi);
265 }
266 }
267 profile
268}
269
270pub fn spike_profile(
272 times_a: &[f64],
273 times_b: &[f64],
274 n_bins: usize,
275 t_start: f64,
276 t_end: f64,
277) -> Vec<f64> {
278 let mut profile = vec![0.0_f64; n_bins];
279 let bin_width = (t_end - t_start) / n_bins as f64;
280 for k in 0..n_bins {
281 let lo = t_start + k as f64 * bin_width;
282 let hi = lo + bin_width;
283 let sub_a: Vec<f64> = times_a
284 .iter()
285 .copied()
286 .filter(|&t| t >= lo && t < hi)
287 .collect();
288 let sub_b: Vec<f64> = times_b
289 .iter()
290 .copied()
291 .filter(|&t| t >= lo && t < hi)
292 .collect();
293 profile[k] = spike_distance(&sub_a, &sub_b, lo, hi);
294 }
295 profile
296}
297
298pub fn isi_profile(
300 binary_train_a: &[i32],
301 binary_train_b: &[i32],
302 dt: f64,
303 n_bins: usize,
304) -> Vec<f64> {
305 let n = binary_train_a.len().min(binary_train_b.len());
306 let bin_size = (n / n_bins).max(1);
307 let mut profile = vec![0.0_f64; n_bins];
308 for k in 0..n_bins {
309 let start = k * bin_size;
310 let end = (start + bin_size).min(n);
311 if start >= n {
312 break;
313 }
314 profile[k] = isi_distance(&binary_train_a[start..end], &binary_train_b[start..end], dt);
315 }
316 profile
317}
318
319pub fn adaptive_spike_distance(
322 times_a: &[f64],
323 times_b: &[f64],
324 t_start: f64,
325 t_end: f64,
326 cost: f64,
327) -> f64 {
328 let sd = spike_distance(times_a, times_b, t_start, t_end);
329 let ta: Vec<f64> = times_a
330 .iter()
331 .copied()
332 .filter(|&t| t >= t_start && t <= t_end)
333 .collect();
334 let tb: Vec<f64> = times_b
335 .iter()
336 .copied()
337 .filter(|&t| t >= t_start && t <= t_end)
338 .collect();
339
340 let mean_isi = |times: &[f64]| -> f64 {
341 if times.len() > 1 {
342 let mut sorted = times.to_vec();
343 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
344 let diffs: Vec<f64> = sorted.windows(2).map(|w| w[1] - w[0]).collect();
345 diffs.iter().sum::<f64>() / diffs.len() as f64
346 } else {
347 t_end - t_start
348 }
349 };
350
351 let mean_a = mean_isi(&ta);
352 let mean_b = mean_isi(&tb);
353 let ratio = (mean_a - mean_b).abs() / (mean_a + mean_b).max(1e-30);
354 (1.0 - cost) * sd + cost * ratio
355}
356
357pub fn schreiber_similarity(train_a: &[i32], train_b: &[i32], dt: f64, sigma_ms: f64) -> f64 {
359 let fa: Vec<f64> = train_a.iter().map(|&v| v as f64).collect();
360 let fb: Vec<f64> = train_b.iter().map(|&v| v as f64).collect();
361 let ra = instantaneous_rate(&fa, dt, "gaussian", sigma_ms);
362 let rb = instantaneous_rate(&fb, dt, "gaussian", sigma_ms);
363 let n = ra.len().min(rb.len());
364 if n == 0 {
365 return 0.0;
366 }
367
368 let mean_a: f64 = ra[..n].iter().sum::<f64>() / n as f64;
369 let mean_b: f64 = rb[..n].iter().sum::<f64>() / n as f64;
370
371 let mut sum_ab = 0.0_f64;
372 let mut sum_aa = 0.0_f64;
373 let mut sum_bb = 0.0_f64;
374 for i in 0..n {
375 let a = ra[i] - mean_a;
376 let b = rb[i] - mean_b;
377 sum_ab += a * b;
378 sum_aa += a * a;
379 sum_bb += b * b;
380 }
381 let denom = (sum_aa * sum_bb).sqrt();
382 if denom == 0.0 {
383 return 0.0;
384 }
385 sum_ab / denom
386}
387
388pub fn hunter_milton_similarity(times_a: &[f64], times_b: &[f64], dt_max: f64) -> f64 {
390 if times_a.is_empty() || times_b.is_empty() {
391 return 0.0;
392 }
393 let total = times_a.len() + times_b.len();
394 let mut count = 0_usize;
395 for &t in times_a {
396 if times_b.iter().any(|&tb| (tb - t).abs() < dt_max) {
397 count += 1;
398 }
399 }
400 for &t in times_b {
401 if times_a.iter().any(|&ta| (ta - t).abs() < dt_max) {
402 count += 1;
403 }
404 }
405 count as f64 / total as f64
406}
407
408pub fn earth_movers_distance(
410 times_a: &[f64],
411 times_b: &[f64],
412 t_start: f64,
413 t_end: f64,
414 n_bins: usize,
415) -> f64 {
416 let bin_width = (t_end - t_start) / n_bins as f64;
417
418 let histogram = |times: &[f64]| -> Vec<f64> {
419 let mut h = vec![0.0_f64; n_bins];
420 for &t in times {
421 let idx = ((t - t_start) / bin_width) as usize;
422 if idx < n_bins {
423 h[idx] += 1.0;
424 }
425 }
426 let s: f64 = h.iter().sum();
427 if s > 0.0 {
428 for v in &mut h {
429 *v /= s;
430 }
431 }
432 h
433 };
434
435 let ha = histogram(times_a);
436 let hb = histogram(times_b);
437
438 let mut cum_a = 0.0_f64;
440 let mut cum_b = 0.0_f64;
441 let mut emd = 0.0_f64;
442 for i in 0..n_bins {
443 cum_a += ha[i];
444 cum_b += hb[i];
445 emd += (cum_a - cum_b).abs();
446 }
447 emd * bin_width
448}
449
450pub fn multi_neuron_victor_purpura(spike_times_list: &[&[f64]], cost_per_s: f64) -> Vec<Vec<f64>> {
452 let n = spike_times_list.len();
453 let mut mat = vec![vec![0.0_f64; n]; n];
454 for i in 0..n {
455 for j in (i + 1)..n {
456 let d = victor_purpura_distance(spike_times_list[i], spike_times_list[j], cost_per_s);
457 mat[i][j] = d;
458 mat[j][i] = d;
459 }
460 }
461 mat
462}
463
464pub fn generalized_victor_purpura(
467 times_a: &[f64],
468 times_b: &[f64],
469 cost_func: fn(f64) -> f64,
470) -> f64 {
471 let na = times_a.len();
472 let nb = times_b.len();
473 if na == 0 {
474 return nb as f64;
475 }
476 if nb == 0 {
477 return na as f64;
478 }
479 let mut d = vec![vec![0.0_f64; nb + 1]; na + 1];
480 for i in 0..=na {
481 d[i][0] = i as f64;
482 }
483 for j in 0..=nb {
484 d[0][j] = j as f64;
485 }
486 for i in 1..=na {
487 for j in 1..=nb {
488 let shift = cost_func(times_a[i - 1] - times_b[j - 1]);
489 d[i][j] = (d[i - 1][j] + 1.0)
490 .min(d[i][j - 1] + 1.0)
491 .min(d[i - 1][j - 1] + shift);
492 }
493 }
494 d[na][nb]
495}
496
497pub fn spike_distance_matrix(
500 spike_times_list: &[&[f64]],
501 metric: &str,
502 t_start: f64,
503 t_end: f64,
504) -> Vec<Vec<f64>> {
505 let n = spike_times_list.len();
506 let mut mat = vec![vec![0.0_f64; n]; n];
507 for i in 0..n {
508 for j in (i + 1)..n {
509 let d = match metric {
510 "spike_sync" => {
511 1.0 - spike_sync(spike_times_list[i], spike_times_list[j], t_start, t_end)
512 }
513 "victor_purpura" => {
514 victor_purpura_distance(spike_times_list[i], spike_times_list[j], 1000.0)
515 }
516 _ => spike_distance(spike_times_list[i], spike_times_list[j], t_start, t_end),
517 };
518 mat[i][j] = d;
519 mat[j][i] = d;
520 }
521 }
522 mat
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 fn make_train(spikes: &[usize], len: usize) -> Vec<i32> {
530 let mut t = vec![0i32; len];
531 for &s in spikes {
532 t[s] = 1;
533 }
534 t
535 }
536
537 #[test]
540 fn test_van_rossum_identical() {
541 let train = make_train(&[10, 30, 50], 100);
542 let d = van_rossum_distance(&train, &train, 0.001, 10.0);
543 assert!(d.abs() < 1e-10, "identical trains → distance 0, got {d}");
544 }
545
546 #[test]
547 fn test_van_rossum_different() {
548 let a = make_train(&[10, 30, 50], 100);
549 let b = make_train(&[20, 40, 60], 100);
550 let d = van_rossum_distance(&a, &b, 0.001, 10.0);
551 assert!(d > 0.0, "different trains → positive distance");
552 }
553
554 #[test]
555 fn test_van_rossum_symmetry() {
556 let a = make_train(&[10, 30], 100);
557 let b = make_train(&[20, 40], 100);
558 let d1 = van_rossum_distance(&a, &b, 0.001, 10.0);
559 let d2 = van_rossum_distance(&b, &a, 0.001, 10.0);
560 assert!((d1 - d2).abs() < 1e-10, "distance should be symmetric");
561 }
562
563 #[test]
564 fn test_van_rossum_empty() {
565 let a = vec![0i32; 100];
566 let b = make_train(&[50], 100);
567 let d = van_rossum_distance(&a, &b, 0.001, 10.0);
568 assert!(d > 0.0, "empty vs non-empty → positive distance");
569 }
570
571 #[test]
574 fn test_vp_identical() {
575 let times = vec![0.1, 0.3, 0.5];
576 let d = victor_purpura_distance(×, ×, 1000.0);
577 assert!(d < 1e-10, "identical spike times → 0");
578 }
579
580 #[test]
581 fn test_vp_empty_vs_spikes() {
582 let d = victor_purpura_distance(&[], &[0.1, 0.2, 0.3], 1000.0);
583 assert!((d - 3.0).abs() < 1e-10, "3 insertions → cost 3");
584 }
585
586 #[test]
587 fn test_vp_close_spikes() {
588 let a = vec![0.1];
589 let b = vec![0.1001];
590 let d = victor_purpura_distance(&a, &b, 1000.0);
591 assert!(d < 0.2, "close spikes → small shift cost, got {d}");
593 }
594
595 #[test]
596 fn test_vp_symmetry() {
597 let a = vec![0.1, 0.5];
598 let b = vec![0.3, 0.7];
599 let d1 = victor_purpura_distance(&a, &b, 1000.0);
600 let d2 = victor_purpura_distance(&b, &a, 1000.0);
601 assert!((d1 - d2).abs() < 1e-10);
602 }
603
604 #[test]
607 fn test_isi_distance_identical() {
608 let train = make_train(&[10, 30, 50, 70], 100);
609 let d = isi_distance(&train, &train, 0.001);
610 assert!(d.abs() < 1e-10, "identical trains → ISI distance 0");
611 }
612
613 #[test]
614 fn test_isi_distance_empty() {
615 let a = make_train(&[50], 100);
616 let b = make_train(&[50], 100);
617 let d = isi_distance(&a, &b, 0.001);
619 assert!(d.is_nan(), "single spike → NaN ISI distance");
620 }
621
622 #[test]
623 fn test_isi_distance_different() {
624 let a = make_train(&[10, 20, 30], 100); let b = make_train(&[10, 15, 30], 100); let d = isi_distance(&a, &b, 0.001);
627 assert!(d > 0.0, "different ISI patterns → positive distance");
628 }
629
630 #[test]
633 fn test_spike_distance_identical() {
634 let times = vec![0.1, 0.3, 0.5, 0.7, 0.9];
635 let d = spike_distance(×, ×, 0.0, 1.0);
636 assert!(d < 1e-10, "identical → 0, got {d}");
637 }
638
639 #[test]
640 fn test_spike_distance_empty_both() {
641 let d = spike_distance(&[], &[], 0.0, 1.0);
642 assert_eq!(d, 0.0);
643 }
644
645 #[test]
646 fn test_spike_distance_one_empty() {
647 let d = spike_distance(&[0.5], &[], 0.0, 1.0);
648 assert_eq!(d, 1.0);
649 }
650
651 #[test]
652 fn test_spike_distance_symmetry() {
653 let a = vec![0.2, 0.6];
654 let b = vec![0.4, 0.8];
655 let d1 = spike_distance(&a, &b, 0.0, 1.0);
656 let d2 = spike_distance(&b, &a, 0.0, 1.0);
657 assert!((d1 - d2).abs() < 1e-10);
658 }
659
660 #[test]
663 fn test_spike_sync_identical() {
664 let times = vec![0.1, 0.3, 0.5, 0.7, 0.9];
665 let s = spike_sync(×, ×, 0.0, 1.0);
666 assert!(
667 (s - 1.0).abs() < 1e-10,
668 "identical trains → sync=1.0, got {s}"
669 );
670 }
671
672 #[test]
673 fn test_spike_sync_empty() {
674 let s = spike_sync(&[], &[0.5], 0.0, 1.0);
675 assert_eq!(s, 0.0);
676 }
677
678 #[test]
679 fn test_spike_sync_far_apart() {
680 let a = vec![0.1];
681 let b = vec![0.9];
682 let s = spike_sync(&a, &b, 0.0, 1.0);
683 assert!(s < 0.5, "far apart → low sync, got {s}");
684 }
685
686 #[test]
689 fn test_sync_profile_length() {
690 let a = vec![0.1, 0.3, 0.5];
691 let b = vec![0.1, 0.3, 0.5];
692 let p = spike_sync_profile(&a, &b, 10, 0.0, 1.0);
693 assert_eq!(p.len(), 10);
694 }
695
696 #[test]
699 fn test_spike_profile_length() {
700 let a = vec![0.2, 0.6];
701 let b = vec![0.4, 0.8];
702 let p = spike_profile(&a, &b, 5, 0.0, 1.0);
703 assert_eq!(p.len(), 5);
704 }
705
706 #[test]
709 fn test_isi_profile_length() {
710 let a = make_train(&[5, 15, 25, 35, 45], 50);
711 let b = make_train(&[5, 15, 25, 35, 45], 50);
712 let p = isi_profile(&a, &b, 0.001, 5);
713 assert_eq!(p.len(), 5);
714 }
715
716 #[test]
719 fn test_adaptive_cost_zero_equals_spike() {
720 let a = vec![0.2, 0.6];
721 let b = vec![0.4, 0.8];
722 let sd = spike_distance(&a, &b, 0.0, 1.0);
723 let ad = adaptive_spike_distance(&a, &b, 0.0, 1.0, 0.0);
724 assert!((sd - ad).abs() < 1e-10, "cost=0 → pure spike distance");
725 }
726
727 #[test]
728 fn test_adaptive_cost_one() {
729 let a = vec![0.2, 0.6];
730 let b = vec![0.4, 0.8];
731 let ad = adaptive_spike_distance(&a, &b, 0.0, 1.0, 1.0);
732 assert!(ad >= 0.0, "cost=1 → non-negative");
733 }
734
735 #[test]
738 fn test_schreiber_identical() {
739 let train = make_train(&[10, 30, 50, 70, 90], 100);
740 let s = schreiber_similarity(&train, &train, 0.001, 5.0);
741 assert!(
742 (s - 1.0).abs() < 1e-6,
743 "identical trains → similarity 1.0, got {s}"
744 );
745 }
746
747 #[test]
748 fn test_schreiber_empty() {
749 let a = vec![0i32; 100];
750 let b = vec![0i32; 100];
751 let s = schreiber_similarity(&a, &b, 0.001, 5.0);
752 assert_eq!(s, 0.0, "zero trains → 0.0");
753 }
754
755 #[test]
758 fn test_hunter_identical() {
759 let times = vec![0.1, 0.3, 0.5];
760 let s = hunter_milton_similarity(×, ×, 0.01);
761 assert!((s - 1.0).abs() < 1e-10, "identical → 1.0, got {s}");
762 }
763
764 #[test]
765 fn test_hunter_empty() {
766 let s = hunter_milton_similarity(&[], &[0.5], 0.01);
767 assert_eq!(s, 0.0);
768 }
769
770 #[test]
771 fn test_hunter_far_apart() {
772 let a = vec![0.1];
773 let b = vec![0.9];
774 let s = hunter_milton_similarity(&a, &b, 0.01);
775 assert_eq!(s, 0.0, "far apart → 0.0");
776 }
777
778 #[test]
781 fn test_emd_identical() {
782 let times = vec![0.1, 0.3, 0.5, 0.7, 0.9];
783 let d = earth_movers_distance(×, ×, 0.0, 1.0, 100);
784 assert!(d < 1e-10, "identical → 0, got {d}");
785 }
786
787 #[test]
788 fn test_emd_different() {
789 let a = vec![0.1, 0.2];
790 let b = vec![0.8, 0.9];
791 let d = earth_movers_distance(&a, &b, 0.0, 1.0, 100);
792 assert!(d > 0.0, "different distributions → positive EMD");
793 }
794
795 #[test]
796 fn test_emd_symmetry() {
797 let a = vec![0.1, 0.2];
798 let b = vec![0.8, 0.9];
799 let d1 = earth_movers_distance(&a, &b, 0.0, 1.0, 100);
800 let d2 = earth_movers_distance(&b, &a, 0.0, 1.0, 100);
801 assert!((d1 - d2).abs() < 1e-10);
802 }
803
804 #[test]
807 fn test_multi_vp_diagonal_zero() {
808 let t1 = vec![0.1, 0.3];
809 let t2 = vec![0.2, 0.4];
810 let trains: Vec<&[f64]> = vec![&t1, &t2];
811 let mat = multi_neuron_victor_purpura(&trains, 1000.0);
812 assert_eq!(mat[0][0], 0.0);
813 assert_eq!(mat[1][1], 0.0);
814 assert!(mat[0][1] > 0.0);
815 assert!((mat[0][1] - mat[1][0]).abs() < 1e-10);
816 }
817
818 #[test]
821 fn test_gen_vp_linear_matches_standard() {
822 fn linear_cost(dt: f64) -> f64 {
823 1000.0 * dt.abs()
824 }
825 let a = vec![0.1, 0.5];
826 let b = vec![0.2, 0.6];
827 let d_gen = generalized_victor_purpura(&a, &b, linear_cost);
828 let d_std = victor_purpura_distance(&a, &b, 1000.0);
829 assert!((d_gen - d_std).abs() < 1e-10);
830 }
831
832 #[test]
833 fn test_gen_vp_quadratic_cost() {
834 fn quad_cost(dt: f64) -> f64 {
835 1000.0 * dt * dt
836 }
837 let a = vec![0.1];
838 let b = vec![0.2];
839 let d = generalized_victor_purpura(&a, &b, quad_cost);
840 assert!(
842 (d - 2.0).abs() < 1e-10,
843 "high shift cost → delete+insert, got {d}"
844 );
845 }
846
847 #[test]
850 fn test_distance_matrix_shape() {
851 let t1 = vec![0.1, 0.5];
852 let t2 = vec![0.2, 0.6];
853 let t3 = vec![0.3, 0.7];
854 let trains: Vec<&[f64]> = vec![&t1, &t2, &t3];
855 let mat = spike_distance_matrix(&trains, "spike_distance", 0.0, 1.0);
856 assert_eq!(mat.len(), 3);
857 assert_eq!(mat[0].len(), 3);
858 assert_eq!(mat[0][0], 0.0);
859 assert!((mat[0][1] - mat[1][0]).abs() < 1e-10);
860 }
861
862 #[test]
863 fn test_distance_matrix_vp_metric() {
864 let t1 = vec![0.1];
865 let t2 = vec![0.9];
866 let trains: Vec<&[f64]> = vec![&t1, &t2];
867 let mat = spike_distance_matrix(&trains, "victor_purpura", 0.0, 1.0);
868 assert!(mat[0][1] > 0.0);
869 }
870
871 #[test]
874 fn test_local_isi_boundaries() {
875 let times = vec![0.1, 0.3, 0.7];
876 assert!((local_isi(×, 0) - 0.2).abs() < 1e-10);
877 assert!((local_isi(×, 2) - 0.4).abs() < 1e-10);
878 assert!((local_isi(×, 1) - 0.2).abs() < 1e-10);
880 }
881
882 #[test]
883 fn test_local_isi_single() {
884 assert_eq!(local_isi(&[0.5], 0), 1.0);
885 }
886
887 #[test]
888 fn test_nearest_idx() {
889 let sorted = vec![0.1, 0.3, 0.5, 0.7];
890 assert_eq!(nearest_idx(&sorted, 0.0), 0);
891 assert_eq!(nearest_idx(&sorted, 0.29), 1);
892 assert_eq!(nearest_idx(&sorted, 0.9), 3);
893 }
894}