1fn histogram(values: &[f64], bins: usize) -> Vec<usize> {
17 if values.is_empty() || bins == 0 {
18 return vec![];
19 }
20 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
21 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
22
23 if (max - min).abs() < f64::EPSILON {
24 let mut counts = vec![0usize; bins];
26 if bins > 0 {
27 counts[0] = values.len();
28 }
29 return counts;
30 }
31
32 let width = (max - min) / bins as f64;
33 let mut counts = vec![0usize; bins];
34
35 for &v in values {
36 let idx = ((v - min) / width).floor() as usize;
37 let idx = idx.min(bins - 1);
39 counts[idx] += 1;
40 }
41
42 counts
43}
44
45fn histogram_2d(x: &[f64], y: &[f64], bins: usize) -> Vec<Vec<usize>> {
47 let n = x.len().min(y.len());
48 if n == 0 || bins == 0 {
49 return vec![];
50 }
51
52 let x_min = x[..n].iter().cloned().fold(f64::INFINITY, f64::min);
53 let x_max = x[..n].iter().cloned().fold(f64::NEG_INFINITY, f64::max);
54 let y_min = y[..n].iter().cloned().fold(f64::INFINITY, f64::min);
55 let y_max = y[..n].iter().cloned().fold(f64::NEG_INFINITY, f64::max);
56
57 let x_width = if (x_max - x_min).abs() < f64::EPSILON {
58 1.0
59 } else {
60 (x_max - x_min) / bins as f64
61 };
62 let y_width = if (y_max - y_min).abs() < f64::EPSILON {
63 1.0
64 } else {
65 (y_max - y_min) / bins as f64
66 };
67
68 let mut counts = vec![vec![0usize; bins]; bins];
69
70 for i in 0..n {
71 let xi = if (x_max - x_min).abs() < f64::EPSILON {
72 0
73 } else {
74 (((x[i] - x_min) / x_width).floor() as usize).min(bins - 1)
75 };
76 let yi = if (y_max - y_min).abs() < f64::EPSILON {
77 0
78 } else {
79 (((y[i] - y_min) / y_width).floor() as usize).min(bins - 1)
80 };
81 counts[xi][yi] += 1;
82 }
83
84 counts
85}
86
87pub fn entropy(values: &[f64], bins: usize) -> f64 {
107 if values.is_empty() || bins == 0 {
108 return 0.0;
109 }
110
111 let counts = histogram(values, bins);
112 let total = values.len() as f64;
113
114 let mut h = 0.0;
115 for &c in &counts {
116 if c > 0 {
117 let p = c as f64 / total;
118 h -= p * p.log2();
119 }
120 }
121
122 h
123}
124
125pub fn joint_entropy(x: &[f64], y: &[f64], bins: usize) -> f64 {
129 let n = x.len().min(y.len());
130 if n == 0 || bins == 0 {
131 return 0.0;
132 }
133
134 let counts = histogram_2d(x, y, bins);
135 let total = n as f64;
136
137 let mut h = 0.0;
138 for row in &counts {
139 for &c in row {
140 if c > 0 {
141 let p = c as f64 / total;
142 h -= p * p.log2();
143 }
144 }
145 }
146
147 h
148}
149
150pub fn mutual_information(x: &[f64], y: &[f64], bins: usize) -> f64 {
171 let n = x.len().min(y.len());
172 if n == 0 || bins == 0 {
173 return 0.0;
174 }
175
176 let hx = entropy(x, bins);
177 let hy = entropy(y, bins);
178 let hxy = joint_entropy(x, y, bins);
179
180 (hx + hy - hxy).max(0.0)
182}
183
184pub fn kl_divergence(current: &[f64], baseline: &[f64], bins: usize) -> f64 {
205 if current.is_empty() || baseline.is_empty() || bins == 0 {
206 return 0.0;
207 }
208
209 let p_counts = histogram(current, bins);
210 let q_counts = histogram(baseline, bins);
211
212 let p_total = current.len() as f64;
213 let q_total = baseline.len() as f64;
214
215 let mut kl = 0.0;
216 for i in 0..bins {
217 let p = p_counts[i] as f64 / p_total;
218 let q = q_counts[i] as f64 / q_total;
219
220 if p > 0.0 && q > 0.0 {
221 kl += p * (p / q).log2();
222 } else if p > 0.0 && q <= 0.0 {
223 return f64::INFINITY;
224 }
225 }
227
228 kl
229}
230
231pub fn jsd(p: &[f64], q: &[f64], bins: usize) -> f64 {
248 if p.is_empty() || q.is_empty() || bins == 0 {
249 return 0.0;
250 }
251
252 let p_counts = histogram(p, bins);
253 let q_counts = histogram(q, bins);
254
255 let p_total = p.len() as f64;
256 let q_total = q.len() as f64;
257
258 let mut jsd_val = 0.0;
259
260 for i in 0..bins {
261 let pi = p_counts[i] as f64 / p_total;
262 let qi = q_counts[i] as f64 / q_total;
263 let mi = (pi + qi) / 2.0;
264
265 if pi > 0.0 && mi > 0.0 {
266 jsd_val += 0.5 * pi * (pi / mi).log2();
267 }
268 if qi > 0.0 && mi > 0.0 {
269 jsd_val += 0.5 * qi * (qi / mi).log2();
270 }
271 }
272
273 jsd_val
274}
275
276pub fn transfer_entropy(x: &[f64], y: &[f64], lag: usize, bins: usize) -> f64 {
299 let n = x.len().min(y.len());
300 if n <= lag + 1 || bins == 0 {
301 return 0.0;
302 }
303
304 let m = n - lag;
306 let y_future: Vec<f64> = (lag..n).map(|t| y[t]).collect();
307 let x_past: Vec<f64> = (0..m).map(|t| x[t]).collect();
308 let y_past: Vec<f64> = (0..m).map(|t| y[t]).collect();
309
310 let h_yxy = entropy_3d(&y_future, &x_past, &y_past, bins);
313 let h_xy = joint_entropy(&x_past, &y_past, bins);
314 let h_yy = joint_entropy(&y_future, &y_past, bins);
315 let h_y = entropy(&y_past, bins);
316
317 (h_yxy - h_xy - h_yy + h_y).max(0.0)
318}
319
320fn entropy_3d(a: &[f64], b: &[f64], c: &[f64], bins: usize) -> f64 {
322 let n = a.len().min(b.len()).min(c.len());
323 if n == 0 || bins == 0 {
324 return 0.0;
325 }
326
327 let a_disc = discretize(&a[..n], bins);
329 let b_disc = discretize(&b[..n], bins);
330 let c_disc = discretize(&c[..n], bins);
331
332 let mut counts = std::collections::HashMap::new();
334 for i in 0..n {
335 let key = (a_disc[i], b_disc[i], c_disc[i]);
336 *counts.entry(key).or_insert(0usize) += 1;
337 }
338
339 let total = n as f64;
340 let mut h = 0.0;
341 for &c in counts.values() {
342 let p = c as f64 / total;
343 h -= p * p.log2();
344 }
345
346 h
347}
348
349fn discretize(values: &[f64], bins: usize) -> Vec<usize> {
351 if values.is_empty() || bins == 0 {
352 return vec![];
353 }
354
355 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
356 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
357
358 if (max - min).abs() < f64::EPSILON {
359 return vec![0; values.len()];
360 }
361
362 let width = (max - min) / bins as f64;
363 values
364 .iter()
365 .map(|&v| ((v - min) / width).floor() as usize).map(|idx| idx.min(bins - 1))
366 .collect()
367}
368
369pub fn permutation_entropy(values: &[f64], order: usize) -> f64 {
388 if values.len() < order || order < 2 {
389 return 0.0;
390 }
391
392 let n_patterns = factorial(order);
393 let mut pattern_counts = vec![0usize; n_patterns];
394
395 let n = values.len() - order + 1;
396 for i in 0..n {
397 let window = &values[i..i + order];
398 let idx = ordinal_pattern_index(window);
399 pattern_counts[idx] += 1;
400 }
401
402 let total = n as f64;
403 let mut h = 0.0;
404 for &c in &pattern_counts {
405 if c > 0 {
406 let p = c as f64 / total;
407 h -= p * p.log2();
408 }
409 }
410
411 let max_h = (n_patterns as f64).log2();
413 if max_h > 0.0 {
414 h / max_h
415 } else {
416 0.0
417 }
418}
419
420fn ordinal_pattern_index(window: &[f64]) -> usize {
424 let order = window.len();
425 let mut indices: Vec<usize> = (0..order).collect();
426
427 indices.sort_by(|&a, &b| {
429 window[a]
430 .partial_cmp(&window[b])
431 .unwrap_or(std::cmp::Ordering::Equal)
432 });
433
434 let mut code = 0usize;
436 for i in 0..order {
437 let mut count = 0;
438 for j in (i + 1)..order {
439 if indices[j] < indices[i] {
440 count += 1;
441 }
442 }
443 code += count * factorial(order - i - 1);
444 }
445
446 code
447}
448
449fn factorial(n: usize) -> usize {
451 if n <= 1 {
452 1
453 } else {
454 (2..=n).product()
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
465 fn histogram_basic() {
466 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
467 let counts = histogram(&values, 5);
468 assert_eq!(counts.iter().sum::<usize>(), 5);
469 }
470
471 #[test]
472 fn histogram_empty() {
473 let counts = histogram(&[], 10);
474 assert!(counts.is_empty());
475 }
476
477 #[test]
478 fn histogram_single_value() {
479 let counts = histogram(&[5.0; 10], 5);
480 assert_eq!(counts[0], 10);
481 assert_eq!(counts.iter().sum::<usize>(), 10);
482 }
483
484 #[test]
485 fn histogram_2d_basic() {
486 let x = vec![1.0, 2.0, 3.0];
487 let y = vec![1.0, 2.0, 3.0];
488 let counts = histogram_2d(&x, &y, 3);
489 let total: usize = counts.iter().flat_map(|r| r.iter()).sum();
490 assert_eq!(total, 3);
491 }
492
493 #[test]
494 fn histogram_2d_empty() {
495 let counts = histogram_2d(&[], &[], 3);
496 assert!(counts.is_empty());
497 }
498
499 #[test]
502 fn entropy_uniform_distribution() {
503 let values: Vec<f64> = (0..100).map(|i| (i % 4) as f64).collect();
505 let h = entropy(&values, 4);
506 assert!((h - 2.0).abs() < 0.05, "expected ~2.0, got {}", h);
507 }
508
509 #[test]
510 fn entropy_single_value() {
511 let h = entropy(&[5.0; 100], 10);
512 assert!(h.abs() < 0.001, "expected ~0.0, got {}", h);
513 }
514
515 #[test]
516 fn entropy_empty() {
517 assert_eq!(entropy(&[], 10), 0.0);
518 }
519
520 #[test]
521 fn entropy_zero_bins() {
522 assert_eq!(entropy(&[1.0, 2.0], 0), 0.0);
523 }
524
525 #[test]
526 fn entropy_two_values_different() {
527 let h = entropy(&[1.0, 2.0], 2);
528 assert!((h - 1.0).abs() < 0.01, "expected ~1.0, got {}", h);
530 }
531
532 #[test]
533 fn entropy_monotonic_in_bins() {
534 let values: Vec<f64> = (0..1000).map(|i| (i % 10) as f64).collect();
536 let h5 = entropy(&values, 5);
537 let h20 = entropy(&values, 20);
538 assert!(h20 >= h5 - 0.1);
540 }
541
542 #[test]
545 fn joint_entropy_independent() {
546 let x: Vec<f64> = (0..100).map(|i| (i % 2) as f64).collect();
547 let y: Vec<f64> = (0..100).map(|i| (i % 2) as f64).collect();
548 let hxy = joint_entropy(&x, &y, 2);
549 let hx = entropy(&x, 2);
550 let hy = entropy(&y, 2);
551 assert!((hxy - hx).abs() < 0.1);
553 let _ = hy; }
555
556 #[test]
557 fn joint_entropy_empty() {
558 assert_eq!(joint_entropy(&[], &[], 10), 0.0);
559 }
560
561 #[test]
562 fn joint_entropy_increases_with_independence() {
563 let x: Vec<f64> = (0..100).map(|i| (i % 4) as f64).collect();
565 let y_same: Vec<f64> = x.clone();
566 let y_indep: Vec<f64> = (0..100).map(|i| ((i + 2) % 4) as f64).collect();
567
568 let h_corr = joint_entropy(&x, &y_same, 4);
569 let h_indep = joint_entropy(&x, &y_indep, 4);
570 assert!(h_indep >= h_corr);
572 }
573
574 #[test]
577 fn mi_identical_variables() {
578 let x: Vec<f64> = (0..100).map(|i| (i % 4) as f64).collect();
579 let mi = mutual_information(&x, &x, 4);
580 let hx = entropy(&x, 4);
581 assert!((mi - hx).abs() < 0.1, "MI = {}, H(X) = {}", mi, hx);
583 }
584
585 #[test]
586 fn mi_independent_variables() {
587 let x: Vec<f64> = (0..100).map(|i| (i % 2) as f64).collect();
589 let y: Vec<f64> = (0..100).map(|i| ((i / 2) % 2) as f64).collect();
590 let mi = mutual_information(&x, &y, 2);
591 assert!(mi < 0.5, "expected low MI, got {}", mi);
593 }
594
595 #[test]
596 fn mi_empty() {
597 assert_eq!(mutual_information(&[], &[], 10), 0.0);
598 }
599
600 #[test]
601 fn mi_nonlinear_dependence() {
602 let x: Vec<f64> = (0..50).map(|i| (i as f64 - 25.0) / 5.0).collect();
604 let y: Vec<f64> = x.iter().map(|v| v * v).collect();
605 let mi = mutual_information(&x, &y, 10);
606 assert!(mi > 0.5, "MI should detect nonlinear dependence, got {}", mi);
608 }
609
610 #[test]
611 fn mi_non_negative() {
612 let x = vec![1.0, 2.0, 3.0, 4.0];
613 let y = vec![4.0, 3.0, 2.0, 1.0];
614 let mi = mutual_information(&x, &y, 4);
615 assert!(mi >= 0.0);
616 }
617
618 #[test]
621 fn kl_identical_distributions() {
622 let p = vec![1.0, 2.0, 3.0, 4.0, 5.0];
623 let q = vec![1.0, 2.0, 3.0, 4.0, 5.0];
624 let kl = kl_divergence(&p, &q, 5);
625 assert!(kl.abs() < 0.01, "expected ~0.0, got {}", kl);
626 }
627
628 #[test]
629 fn kl_different_distributions() {
630 let p: Vec<f64> = (0..100).map(|_| 1.0).collect();
631 let q: Vec<f64> = (0..100).map(|i| if i < 50 { 1.0 } else { 10.0 }).collect();
632 let kl = kl_divergence(&p, &q, 10);
633 assert!(kl > 0.0);
634 }
635
636 #[test]
637 fn kl_empty() {
638 assert_eq!(kl_divergence(&[], &[1.0], 10), 0.0);
639 assert_eq!(kl_divergence(&[1.0], &[], 10), 0.0);
640 }
641
642 #[test]
643 fn kl_asymmetric() {
644 let p = vec![1.0, 1.0, 1.0, 10.0];
645 let q = vec![1.0, 1.0, 1.0, 1.0];
646 let kl_pq = kl_divergence(&p, &q, 4);
647 let kl_qp = kl_divergence(&q, &p, 4);
648 assert!((kl_pq - kl_qp).abs() > 0.01 || (kl_pq.is_infinite() || kl_qp.is_infinite()));
650 }
651
652 #[test]
653 fn kl_non_negative() {
654 let p = vec![1.0, 2.0, 3.0];
655 let q = vec![3.0, 2.0, 1.0];
656 let kl = kl_divergence(&p, &q, 3);
657 assert!(kl >= 0.0);
658 }
659
660 #[test]
663 fn jsd_identical() {
664 let p = vec![1.0, 2.0, 3.0, 4.0];
665 let js = jsd(&p, &p, 4);
666 assert!(js.abs() < 0.01, "expected ~0.0, got {}", js);
667 }
668
669 #[test]
670 fn jsd_symmetric() {
671 let p = vec![1.0, 1.0, 1.0, 10.0];
672 let q = vec![1.0, 1.0, 1.0, 1.0];
673 let js_pq = jsd(&p, &q, 4);
674 let js_qp = jsd(&q, &p, 4);
675 assert!((js_pq - js_qp).abs() < 0.01);
676 }
677
678 #[test]
679 fn jsd_different_distributions() {
680 let p: Vec<f64> = (0..100).map(|_| 1.0).collect();
681 let q: Vec<f64> = (0..100).map(|i| if i < 50 { 1.0 } else { 10.0 }).collect();
682 let js = jsd(&p, &q, 10);
683 assert!(js > 0.0);
684 }
685
686 #[test]
687 fn jsd_empty() {
688 assert_eq!(jsd(&[], &[], 10), 0.0);
689 }
690
691 #[test]
692 fn jsd_bounded() {
693 let p = vec![1.0, 1.0, 1.0];
694 let q = vec![10.0, 10.0, 10.0];
695 let js = jsd(&p, &q, 2);
696 assert!(js.is_finite());
698 assert!(js >= 0.0);
699 }
700
701 #[test]
704 fn te_empty() {
705 assert_eq!(transfer_entropy(&[], &[], 1, 10), 0.0);
706 }
707
708 #[test]
709 fn te_too_short() {
710 assert_eq!(transfer_entropy(&[1.0], &[1.0], 1, 10), 0.0);
711 }
712
713 #[test]
714 fn te_causal_direction() {
715 let x: Vec<f64> = (0..50).map(|i| (i % 4) as f64).collect();
717 let mut y = vec![0.0; 50];
718 for i in 1..50 {
719 y[i] = x[i - 1];
720 }
721 let te_xy = transfer_entropy(&x, &y, 1, 4);
722 let te_yx = transfer_entropy(&y, &x, 1, 4);
723 assert!(te_xy >= te_yx, "X→Y = {}, Y→X = {}", te_xy, te_yx);
725 }
726
727 #[test]
728 fn te_independent() {
729 let x: Vec<f64> = (0..100).map(|i| (i % 2) as f64).collect();
731 let y: Vec<f64> = (0..100).map(|i| ((i * 7 + 3) % 5) as f64).collect();
732 let te = transfer_entropy(&x, &y, 1, 5);
733 assert!(te < 1.0, "expected small TE, got {}", te);
735 }
736
737 #[test]
738 fn te_non_negative() {
739 let x: Vec<f64> = (0..20).map(|i| i as f64).collect();
740 let y: Vec<f64> = (0..20).map(|i| (i * 2) as f64).collect();
741 let te = transfer_entropy(&x, &y, 1, 5);
742 assert!(te >= 0.0);
743 }
744
745 #[test]
746 fn te_lag_2() {
747 let x: Vec<f64> = (0..100).map(|i| (i as f64 * 0.3).sin()).collect();
749 let mut y = vec![0.0; 100];
750 for i in 2..100 {
751 y[i] = x[i - 2] + 0.01 * (i as f64).cos();
752 }
753 let te_xy = transfer_entropy(&x, &y, 2, 5);
754 let te_yx = transfer_entropy(&y, &x, 2, 5);
755 assert!(te_xy > 0.0 || te_xy >= te_yx, "X→Y = {}, Y→X = {}", te_xy, te_yx);
757 }
758
759 #[test]
762 fn pe_constant_series() {
763 let values = vec![5.0; 100];
764 let pe = permutation_entropy(&values, 3);
765 assert!(pe.abs() < 0.01, "expected ~0.0, got {}", pe);
767 }
768
769 #[test]
770 fn pe_increasing_series() {
771 let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
772 let pe = permutation_entropy(&values, 3);
773 assert!(pe.abs() < 0.01, "expected ~0.0, got {}", pe);
775 }
776
777 #[test]
778 fn pe_random_like() {
779 let values: Vec<f64> = (0..100).map(|i| ((i * 17 + 3) % 97) as f64).collect();
781 let pe = permutation_entropy(&values, 3);
782 assert!(pe > 0.4, "expected high PE for pseudo-random, got {}", pe);
784 }
785
786 #[test]
787 fn pe_empty() {
788 assert_eq!(permutation_entropy(&[], 3), 0.0);
789 }
790
791 #[test]
792 fn pe_order_too_large() {
793 let values = vec![1.0, 2.0];
794 assert_eq!(permutation_entropy(&values, 5), 0.0);
795 }
796
797 #[test]
798 fn pe_order_1() {
799 assert_eq!(permutation_entropy(&[1.0, 2.0, 3.0], 1), 0.0);
800 }
801
802 #[test]
803 fn pe_normalized_between_0_and_1() {
804 let values: Vec<f64> = (0..50).map(|i| (i as f64).sin()).collect();
805 let pe = permutation_entropy(&values, 4);
806 assert!(pe >= 0.0 && pe <= 1.0);
807 }
808
809 #[test]
810 fn pe_sinusoidal() {
811 let values: Vec<f64> = (0..200).map(|i| (i as f64 * 0.1).sin()).collect();
813 let pe = permutation_entropy(&values, 3);
814 assert!(pe > 0.0 && pe < 1.0);
815 }
816
817 #[test]
820 fn factorial_values() {
821 assert_eq!(factorial(0), 1);
822 assert_eq!(factorial(1), 1);
823 assert_eq!(factorial(2), 2);
824 assert_eq!(factorial(3), 6);
825 assert_eq!(factorial(4), 24);
826 assert_eq!(factorial(5), 120);
827 }
828
829 #[test]
832 fn discretize_basic() {
833 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
834 let disc = discretize(&values, 5);
835 assert_eq!(disc.len(), 5);
836 for &d in &disc {
838 assert!(d < 5);
839 }
840 }
841
842 #[test]
843 fn discretize_constant() {
844 let disc = discretize(&[5.0; 10], 5);
845 assert!(disc.iter().all(|&d| d == 0));
846 }
847
848 #[test]
851 fn ordinal_pattern_increasing() {
852 let idx = ordinal_pattern_index(&[1.0, 2.0, 3.0]);
853 assert_eq!(idx, 0);
855 }
856
857 #[test]
858 fn ordinal_pattern_decreasing() {
859 let idx = ordinal_pattern_index(&[3.0, 2.0, 1.0]);
860 assert_eq!(idx, 5);
862 }
863
864 #[test]
867 fn entropy_3d_basic() {
868 let a = vec![1.0, 2.0, 3.0, 4.0];
869 let b = vec![1.0, 2.0, 3.0, 4.0];
870 let c = vec![1.0, 2.0, 3.0, 4.0];
871 let h = entropy_3d(&a, &b, &c, 4);
872 assert!(h > 0.0);
874 assert!(h.is_finite());
875 }
876
877 #[test]
880 fn entropy_peaked_vs_flat() {
881 let peaked: Vec<f64> = (0..100).map(|_| 5.0).chain(vec![1.0, 2.0, 8.0, 9.0]).collect();
883 let flat: Vec<f64> = (0..100).map(|i| i as f64).collect();
885
886 let h_peaked = entropy(&peaked, 10);
887 let h_flat = entropy(&flat, 10);
888 assert!(h_peaked < h_flat);
889 }
890
891 #[test]
892 fn entropy_bits_units() {
893 let values: Vec<f64> = (0..800).map(|i| (i % 8) as f64).collect();
895 let h = entropy(&values, 8);
896 assert!((h - 3.0).abs() < 0.05);
897 }
898}