1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum CostFunction {
69 L2,
75 Normal,
82}
83
84impl CostFunction {
85 fn params_per_segment(self) -> usize {
87 match self {
88 CostFunction::L2 => 1,
89 CostFunction::Normal => 2,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy)]
96pub enum Penalty {
97 Bic,
103 Custom(f64),
106}
107
108pub struct Pelt {
134 cost: CostFunction,
136 penalty: Penalty,
138 min_segment_len: usize,
140}
141
142#[derive(Debug, Clone)]
144pub struct PeltResult {
145 pub changepoints: Vec<usize>,
151}
152
153#[derive(Debug, Clone)]
155pub struct MultiPeltResult {
156 pub changepoints: Vec<usize>,
158}
159
160impl Pelt {
161 pub fn new(cost: CostFunction, penalty: Penalty) -> Option<Self> {
174 Self::with_min_segment_len(cost, penalty, 2)
175 }
176
177 pub fn with_min_segment_len(
190 cost: CostFunction,
191 penalty: Penalty,
192 min_segment_len: usize,
193 ) -> Option<Self> {
194 if let Penalty::Custom(p) = penalty {
195 if !p.is_finite() || p <= 0.0 {
196 return None;
197 }
198 }
199 if min_segment_len < 2 {
200 return None;
201 }
202 Some(Self {
203 cost,
204 penalty,
205 min_segment_len,
206 })
207 }
208
209 pub fn detect(&self, data: &[f64]) -> PeltResult {
241 PeltResult {
242 changepoints: self.segment(&[data]),
243 }
244 }
245
246 pub fn detect_multi(&self, signals: &[&[f64]]) -> Option<MultiPeltResult> {
289 let n = signals.first().map_or(0, |s| s.len());
290 if signals.iter().any(|s| s.len() != n) {
291 return None;
292 }
293 Some(MultiPeltResult {
294 changepoints: self.segment(signals),
295 })
296 }
297
298 fn segment(&self, signals: &[&[f64]]) -> Vec<usize> {
302 let n = signals.first().map_or(0, |s| s.len());
303 if n < 2 * self.min_segment_len {
304 return Vec::new();
305 }
306
307 let penalty_value = self.resolve_penalty(n, signals);
308 if penalty_value <= 0.0 {
309 return Vec::new();
312 }
313
314 let channels: Vec<Channel> = signals.iter().map(|s| Channel::new(s)).collect();
316
317 let last_change = match self.cost {
318 CostFunction::L2 if channels.len() <= FUNCTIONAL_MAX_CHANNELS => {
319 self.partition_functional(&channels, n, penalty_value)
320 }
321 CostFunction::L2 | CostFunction::Normal => {
322 self.partition_pruned(&channels, n, penalty_value)
323 }
324 };
325
326 let mut changepoints = Vec::new();
328 let mut t = n;
329 while t > 0 {
330 let tau = last_change[t];
331 if tau > 0 {
332 changepoints.push(tau);
333 }
334 t = tau;
335 }
336 changepoints.sort_unstable();
337 changepoints
338 }
339
340 fn cost_of(&self, channels: &[Channel], start: usize, end: usize) -> f64 {
342 channels
343 .iter()
344 .map(|c| self.segment_cost(&c.cum_sum, &c.cum_sum_sq, start, end))
345 .sum()
346 }
347
348 fn partition_pruned(&self, channels: &[Channel], n: usize, penalty_value: f64) -> Vec<usize> {
358 let mut f = vec![0.0_f64; n + 1];
359 f[0] = -penalty_value;
360 let mut last_change = vec![0_usize; n + 1];
361 let mut candidates: Vec<usize> = vec![0];
362 let mut partial: Vec<f64> = Vec::new();
365
366 for t in self.min_segment_len..=n {
367 let mut best_cost = f64::INFINITY;
368 let mut best_tau = 0;
369 partial.clear();
370 for &tau in &candidates {
371 if t - tau < self.min_segment_len {
372 partial.push(f64::NEG_INFINITY); continue;
374 }
375 let part = f[tau] + self.cost_of(channels, tau, t);
376 partial.push(part);
377 let total = part + penalty_value;
378 if total < best_cost {
379 best_cost = total;
380 best_tau = tau;
381 }
382 }
383 f[t] = best_cost;
384 last_change[t] = best_tau;
385
386 let bound = f[t] + penalty_value;
387 let mut kept = 0;
388 for i in 0..candidates.len() {
389 if partial[i] < bound {
390 candidates[kept] = candidates[i];
391 kept += 1;
392 }
393 }
394 candidates.truncate(kept);
395 candidates.push(t);
396 }
397 last_change
398 }
399
400 fn partition_functional(
427 &self,
428 channels: &[Channel],
429 n: usize,
430 penalty_value: f64,
431 ) -> Vec<usize> {
432 let m = self.min_segment_len;
433 let dim = channels.len();
434 let mut f = vec![0.0_f64; n + 1];
435 f[0] = -penalty_value;
436 let mut last_change = vec![0_usize; n + 1];
437
438 let range_lo: Vec<f64> = channels.iter().map(|c| c.lo).collect();
439 let range_hi: Vec<f64> = channels.iter().map(|c| c.hi).collect();
440 let mut alive: Vec<Candidate> = vec![Candidate::new(0, &range_lo, &range_hi)];
441 let mut centre = vec![0.0_f64; dim];
442
443 for t in m..=n {
444 let mut best_cost = f64::INFINITY;
447 let mut best_tau = 0;
448 for cand in &alive {
449 let tau = cand.tau;
450 let total = f[tau] + self.cost_of(channels, tau, t) + penalty_value;
451 if total < best_cost {
452 best_cost = total;
453 best_tau = tau;
454 }
455 }
456 f[t] = best_cost;
457 last_change[t] = best_tau;
458
459 let Some(b) = (t + 1).checked_sub(m).filter(|&b| b >= m) else {
461 continue;
462 };
463 let mut born = Candidate::new(b, &range_lo, &range_hi);
464 for cand in &mut alive {
465 let a = cand.tau;
466 let len = (b - a) as f64;
467 let cost = self.cost_of(channels, a, b);
468 let gap = f[b] - f[a] - cost;
469 let slack = 1e-9 * (f[b].abs() + f[a].abs() + cost.abs() + penalty_value) / len;
471 for (k, c) in channels.iter().enumerate() {
472 centre[k] = (c.cum_sum[b] - c.cum_sum[a]) / len;
473 }
474 cand.restrict(¢re, gap / len + slack);
476 born.lose_inside(¢re, gap / len - slack);
478 }
479 for cand in &mut alive {
480 cand.cut_losing();
481 }
482 alive.retain(|cand| !cand.is_dominated());
483 born.cut_losing();
484 if !born.is_dominated() {
485 alive.push(born);
486 }
487 }
488 last_change
489 }
490
491 fn resolve_penalty(&self, n: usize, signals: &[&[f64]]) -> f64 {
502 let channels = signals.len() as f64;
503 match self.penalty {
504 Penalty::Bic => {
505 let p = self.cost.params_per_segment() as f64;
506 let per_channel = p * (n as f64).ln();
507 match self.cost {
508 CostFunction::L2 => {
509 per_channel * signals.iter().map(|s| noise_variance(s)).sum::<f64>()
510 }
511 CostFunction::Normal => per_channel * channels,
512 }
513 }
514 Penalty::Custom(val) => val * channels,
515 }
516 }
517
518 fn segment_cost(&self, cum_sum: &[f64], cum_sum_sq: &[f64], start: usize, end: usize) -> f64 {
524 let seg_len = (end - start) as f64;
525 let sum = cum_sum[end] - cum_sum[start];
526 let sum_sq = cum_sum_sq[end] - cum_sum_sq[start];
527 let mean = sum / seg_len;
528
529 match self.cost {
530 CostFunction::L2 => {
531 sum_sq - seg_len * mean * mean
533 }
534 CostFunction::Normal => {
535 let variance = (sum_sq - seg_len * mean * mean) / seg_len;
537 if variance <= 0.0 {
538 seg_len * (f64::MIN_POSITIVE).ln()
541 } else {
542 seg_len * variance.ln()
543 }
544 }
545 }
546 }
547}
548
549fn noise_variance(signal: &[f64]) -> f64 {
556 if signal.len() < 2 {
557 return 0.0;
558 }
559 let mut diffs: Vec<f64> = signal.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
560 diffs.sort_by(f64::total_cmp);
561 let median = if diffs.len() % 2 == 1 {
562 diffs[diffs.len() / 2]
563 } else {
564 0.5 * (diffs[diffs.len() / 2 - 1] + diffs[diffs.len() / 2])
565 };
566 let sigma = median / (0.6745 * std::f64::consts::SQRT_2);
567 let (lo, hi) = signal
568 .iter()
569 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
570 (lo.min(v), hi.max(v))
571 });
572 let floor = 1e-6 * (hi - lo);
573 sigma.max(floor).powi(2)
574}
575
576fn cumulative_sums(signal: &[f64]) -> (Vec<f64>, Vec<f64>, f64) {
585 let n = signal.len();
586 let center = signal.iter().sum::<f64>() / n as f64;
587 let mut cum_sum = vec![0.0_f64; n + 1];
588 let mut cum_sum_sq = vec![0.0_f64; n + 1];
589 for (i, &y) in signal.iter().enumerate() {
590 let y = y - center;
591 cum_sum[i + 1] = cum_sum[i] + y;
592 cum_sum_sq[i + 1] = cum_sum_sq[i] + y * y;
593 }
594 (cum_sum, cum_sum_sq, center)
595}
596struct Channel {
599 cum_sum: Vec<f64>,
600 cum_sum_sq: Vec<f64>,
601 lo: f64,
602 hi: f64,
603}
604
605impl Channel {
606 fn new(signal: &[f64]) -> Self {
607 let (cum_sum, cum_sum_sq, center) = cumulative_sums(signal);
608 let (lo, hi) = signal
609 .iter()
610 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
611 (lo.min(v), hi.max(v))
612 });
613 Self {
614 cum_sum,
615 cum_sum_sq,
616 lo: lo - center,
617 hi: hi - center,
618 }
619 }
620}
621
622const LOSING_BALLS: usize = 64;
626
627const FUNCTIONAL_MAX_CHANNELS: usize = 2;
635
636struct Candidate {
641 tau: usize,
642 lo: Vec<f64>,
643 hi: Vec<f64>,
644 losing: Vec<(Vec<f64>, f64)>,
645}
646
647impl Candidate {
648 fn new(tau: usize, lo: &[f64], hi: &[f64]) -> Self {
650 Self {
651 tau,
652 lo: lo.to_vec(),
653 hi: hi.to_vec(),
654 losing: Vec::new(),
655 }
656 }
657
658 fn empty(&mut self) {
659 self.lo[0] = f64::INFINITY;
660 self.hi[0] = f64::NEG_INFINITY;
661 }
662
663 fn restrict(&mut self, centre: &[f64], r2: f64) {
667 if r2 < 0.0 {
668 self.empty();
669 return;
670 }
671 let gap2 = |k: usize, lo: &[f64], hi: &[f64]| {
672 let c = centre[k];
673 (lo[k] - c).max(c - hi[k]).max(0.0).powi(2)
674 };
675 let total: f64 = (0..centre.len()).map(|k| gap2(k, &self.lo, &self.hi)).sum();
676 if total > r2 {
677 self.empty();
678 return;
679 }
680 for (k, &c) in centre.iter().enumerate() {
681 let w = (r2 - (total - gap2(k, &self.lo, &self.hi))).max(0.0).sqrt();
682 self.lo[k] = self.lo[k].max(c - w);
683 self.hi[k] = self.hi[k].min(c + w);
684 }
685 }
686
687 fn lose_inside(&mut self, centre: &[f64], r2: f64) {
690 if r2 <= 0.0 {
691 return;
692 }
693 if self.losing.len() == LOSING_BALLS {
694 let (narrowest, &(_, r2_min)) = self
695 .losing
696 .iter()
697 .enumerate()
698 .min_by(|x, y| x.1 .1.total_cmp(&y.1 .1))
699 .expect("the list is full, so not empty");
700 if r2 <= r2_min {
701 return;
702 }
703 self.losing.swap_remove(narrowest);
704 }
705 self.losing.push((centre.to_vec(), r2));
706 }
707
708 fn cut_losing(&mut self) {
712 for (centre, r2) in &self.losing {
713 if self.lo[0] > self.hi[0] {
714 return;
715 }
716 let far2 = |k: usize, lo: &[f64], hi: &[f64]| {
717 (lo[k] - centre[k]).powi(2).max((hi[k] - centre[k]).powi(2))
718 };
719 let total: f64 = (0..centre.len()).map(|k| far2(k, &self.lo, &self.hi)).sum();
720 for (k, &c) in centre.iter().enumerate() {
721 let rest = total - far2(k, &self.lo, &self.hi);
722 if rest > *r2 {
723 continue;
724 }
725 let w = (r2 - rest).sqrt();
726 if (c - w..=c + w).contains(&self.lo[k]) {
727 self.lo[k] = c + w;
728 }
729 if (c - w..=c + w).contains(&self.hi[k]) {
730 self.hi[k] = c - w;
731 }
732 }
733 }
734 }
735
736 fn is_dominated(&self) -> bool {
738 self.lo.iter().zip(&self.hi).any(|(lo, hi)| lo > hi)
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745
746 fn brute_force(pelt: &Pelt, signals: &[&[f64]]) -> Vec<usize> {
750 let n = signals[0].len();
751 if n < 2 * pelt.min_segment_len {
752 return Vec::new();
753 }
754 let beta = pelt.resolve_penalty(n, signals);
755 if beta <= 0.0 {
756 return Vec::new();
757 }
758 let channels: Vec<Channel> = signals.iter().map(|s| Channel::new(s)).collect();
759 let m = pelt.min_segment_len;
760 let mut f = vec![f64::INFINITY; n + 1];
761 f[0] = -beta;
762 let mut last = vec![0_usize; n + 1];
763 for t in m..=n {
764 for tau in (0..=t - m).filter(|&tau| tau == 0 || tau >= m) {
765 let total = f[tau] + pelt.cost_of(&channels, tau, t) + beta;
766 if total < f[t] {
767 f[t] = total;
768 last[t] = tau;
769 }
770 }
771 }
772 let mut cps = Vec::new();
773 let mut t = n;
774 while t > 0 {
775 if last[t] > 0 {
776 cps.push(last[t]);
777 }
778 t = last[t];
779 }
780 cps.sort_unstable();
781 cps
782 }
783
784 fn noisy_series(seed: u64, n: usize, jumps: usize, step: f64) -> Vec<f64> {
787 let mut state = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ 0xD1B5_4A32_D192_ED03;
789 let mut uniform = move || {
790 state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
791 let mut z = state;
792 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
793 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
794 ((z ^ (z >> 31)) >> 11) as f64 / (1u64 << 53) as f64
795 };
796 let mut level = 0.0;
797 let cut = n / (jumps + 1);
798 (0..n)
799 .map(|i| {
800 if jumps > 0 && i > 0 && i % cut == 0 && i / cut <= jumps {
801 level += step;
802 }
803 let u1: f64 = uniform().max(1e-300);
805 let u2: f64 = uniform();
806 level + (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
807 })
808 .collect()
809 }
810
811 fn noise_series_third(seed: u64, n: usize, jumps: usize, step: f64) -> Vec<f64> {
812 noisy_series(seed + 2000, n, jumps, 0.5 * step)
813 }
814
815 #[test]
816 fn functional_pruning_matches_the_unpruned_recurrence() {
817 for seed in 0..40_u64 {
818 for &(n, jumps, step) in &[(200, 0, 0.0), (300, 3, 1.5), (250, 8, 0.8), (150, 2, 4.0)] {
819 for &m in &[2_usize, 5] {
820 for penalty in [Penalty::Bic, Penalty::Custom(3.0)] {
821 let pelt = Pelt::with_min_segment_len(CostFunction::L2, penalty, m)
822 .expect("valid");
823 let a = noisy_series(seed, n, jumps, step);
824 let b = noisy_series(seed + 1000, n, jumps, -step);
825 assert_eq!(
826 pelt.detect(&a).changepoints,
827 brute_force(&pelt, &[&a]),
828 "seed {seed} n {n} jumps {jumps} m {m} {penalty:?}"
829 );
830 assert_eq!(
831 pelt.detect_multi(&[&a, &b])
832 .expect("same length")
833 .changepoints,
834 brute_force(&pelt, &[&a, &b]),
835 "multi seed {seed} n {n} jumps {jumps} m {m} {penalty:?}"
836 );
837 let c = noise_series_third(seed, n, jumps, step);
839 assert_eq!(
840 pelt.detect_multi(&[&a, &b, &c])
841 .expect("same length")
842 .changepoints,
843 brute_force(&pelt, &[&a, &b, &c]),
844 "three seed {seed} n {n} jumps {jumps} m {m} {penalty:?}"
845 );
846 }
847 }
848 }
849 }
850 }
851
852 #[test]
853 fn inequality_pruning_matches_the_unpruned_recurrence_for_the_normal_cost() {
854 for seed in 0..10_u64 {
855 for &m in &[2_usize, 5] {
856 let pelt = Pelt::with_min_segment_len(CostFunction::Normal, Penalty::Bic, m)
857 .expect("valid");
858 let a = noisy_series(seed, 200, 3, 2.0);
859 assert_eq!(
860 pelt.detect(&a).changepoints,
861 brute_force(&pelt, &[&a]),
862 "seed {seed} m {m}"
863 );
864 }
865 }
866 }
867
868 #[test]
871 fn test_pelt_valid_construction() {
872 assert!(Pelt::new(CostFunction::L2, Penalty::Bic).is_some());
873 assert!(Pelt::new(CostFunction::Normal, Penalty::Bic).is_some());
874 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(10.0)).is_some());
875 }
876
877 #[test]
878 fn test_pelt_invalid_custom_penalty() {
879 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(0.0)).is_none());
880 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(-1.0)).is_none());
881 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(f64::NAN)).is_none());
882 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(f64::INFINITY)).is_none());
883 }
884
885 #[test]
886 fn test_pelt_invalid_min_segment_len() {
887 assert!(Pelt::with_min_segment_len(CostFunction::L2, Penalty::Bic, 0).is_none());
888 assert!(Pelt::with_min_segment_len(CostFunction::L2, Penalty::Bic, 1).is_none());
889 assert!(Pelt::with_min_segment_len(CostFunction::L2, Penalty::Bic, 2).is_some());
890 }
891
892 #[test]
895 fn test_pelt_empty_data() {
896 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
897 let result = pelt.detect(&[]);
898 assert!(result.changepoints.is_empty());
899 }
900
901 #[test]
902 fn test_pelt_too_short_data() {
903 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
904 let result = pelt.detect(&[1.0, 2.0, 3.0]);
906 assert!(result.changepoints.is_empty());
907 }
908
909 #[test]
912 fn test_pelt_constant_data_no_changepoint() {
913 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
914 let data = vec![5.0; 100];
915 let result = pelt.detect(&data);
916 assert!(
917 result.changepoints.is_empty(),
918 "constant data should have no changepoints, got {:?}",
919 result.changepoints
920 );
921 }
922
923 #[test]
924 fn test_pelt_normal_cost_constant_data() {
925 let pelt = Pelt::new(CostFunction::Normal, Penalty::Bic).expect("valid");
926 let data = vec![5.0; 100];
927 let result = pelt.detect(&data);
928 assert!(
929 result.changepoints.is_empty(),
930 "constant data should have no changepoints with Normal cost, got {:?}",
931 result.changepoints
932 );
933 }
934
935 #[test]
938 fn test_pelt_single_mean_shift_l2() {
939 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
940
941 let mut data = vec![0.0; 50];
942 data.extend(vec![5.0; 50]);
943
944 let result = pelt.detect(&data);
945 assert_eq!(
946 result.changepoints.len(),
947 1,
948 "expected 1 changepoint, got {:?}",
949 result.changepoints
950 );
951 assert!(
952 (result.changepoints[0] as i64 - 50).unsigned_abs() <= 2,
953 "changepoint should be near index 50, got {}",
954 result.changepoints[0]
955 );
956 }
957
958 #[test]
959 fn test_pelt_single_mean_shift_normal() {
960 let pelt = Pelt::new(CostFunction::Normal, Penalty::Bic).expect("valid");
961
962 let mut data = vec![0.0; 50];
963 data.extend(vec![5.0; 50]);
964
965 let result = pelt.detect(&data);
966 assert_eq!(
967 result.changepoints.len(),
968 1,
969 "expected 1 changepoint with Normal cost, got {:?}",
970 result.changepoints
971 );
972 assert!(
973 (result.changepoints[0] as i64 - 50).unsigned_abs() <= 2,
974 "changepoint should be near index 50, got {}",
975 result.changepoints[0]
976 );
977 }
978
979 #[test]
982 fn test_pelt_two_changepoints() {
983 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
984
985 let mut data = vec![0.0; 40];
986 data.extend(vec![5.0; 40]);
987 data.extend(vec![0.0; 40]);
988
989 let result = pelt.detect(&data);
990 assert_eq!(
991 result.changepoints.len(),
992 2,
993 "expected 2 changepoints, got {:?}",
994 result.changepoints
995 );
996
997 assert!(
999 (result.changepoints[0] as i64 - 40).unsigned_abs() <= 2,
1000 "first changepoint near 40, got {}",
1001 result.changepoints[0]
1002 );
1003 assert!(
1004 (result.changepoints[1] as i64 - 80).unsigned_abs() <= 2,
1005 "second changepoint near 80, got {}",
1006 result.changepoints[1]
1007 );
1008 }
1009
1010 #[test]
1011 fn test_pelt_three_changepoints() {
1012 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1013
1014 let mut data = vec![0.0; 30];
1015 data.extend(vec![4.0; 30]);
1016 data.extend(vec![-2.0; 30]);
1017 data.extend(vec![3.0; 30]);
1018
1019 let result = pelt.detect(&data);
1020 assert_eq!(
1021 result.changepoints.len(),
1022 3,
1023 "expected 3 changepoints, got {:?}",
1024 result.changepoints
1025 );
1026
1027 for i in 1..result.changepoints.len() {
1029 assert!(
1030 result.changepoints[i] > result.changepoints[i - 1],
1031 "changepoints should be strictly increasing"
1032 );
1033 }
1034 }
1035
1036 #[test]
1039 fn test_pelt_variance_change_normal_cost() {
1040 let pelt = Pelt::new(CostFunction::Normal, Penalty::Bic).expect("valid");
1042
1043 let mut data = Vec::with_capacity(200);
1045 for i in 0..100 {
1047 data.push(if i % 2 == 0 { 0.1 } else { -0.1 });
1048 }
1049 for i in 0..100 {
1051 data.push(if i % 2 == 0 { 5.0 } else { -5.0 });
1052 }
1053
1054 let result = pelt.detect(&data);
1055 assert!(
1056 !result.changepoints.is_empty(),
1057 "Normal cost should detect variance change"
1058 );
1059 let cp = result.changepoints[0];
1061 assert!(
1062 (cp as i64 - 100).unsigned_abs() <= 5,
1063 "variance changepoint should be near 100, got {}",
1064 cp
1065 );
1066 }
1067
1068 #[test]
1071 fn test_pelt_higher_penalty_fewer_changepoints() {
1072 let mut data = vec![0.0; 30];
1073 data.extend(vec![2.0; 30]);
1074 data.extend(vec![0.0; 30]);
1075
1076 let pelt_low = Pelt::new(CostFunction::L2, Penalty::Custom(1.0)).expect("valid");
1077 let pelt_high = Pelt::new(CostFunction::L2, Penalty::Custom(100.0)).expect("valid");
1078
1079 let result_low = pelt_low.detect(&data);
1080 let result_high = pelt_high.detect(&data);
1081
1082 assert!(
1083 result_low.changepoints.len() >= result_high.changepoints.len(),
1084 "higher penalty should produce fewer or equal changepoints: low={}, high={}",
1085 result_low.changepoints.len(),
1086 result_high.changepoints.len()
1087 );
1088 }
1089
1090 #[test]
1093 fn test_pelt_custom_min_segment_len() {
1094 let mut data = vec![0.0; 50];
1095 data.extend(vec![10.0; 50]);
1096
1097 let pelt = Pelt::with_min_segment_len(CostFunction::L2, Penalty::Bic, 10).expect("valid");
1098 let result = pelt.detect(&data);
1099 assert_eq!(
1100 result.changepoints.len(),
1101 1,
1102 "should detect changepoint with min_segment_len=10"
1103 );
1104
1105 let mut boundaries = vec![0];
1107 boundaries.extend_from_slice(&result.changepoints);
1108 boundaries.push(data.len());
1109 for i in 1..boundaries.len() {
1110 let seg_len = boundaries[i] - boundaries[i - 1];
1111 assert!(
1112 seg_len >= 10,
1113 "segment length {} is less than min_segment_len=10",
1114 seg_len
1115 );
1116 }
1117 }
1118
1119 #[test]
1130 fn test_pelt_exact_small_example() {
1131 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1132 let data = [0.0, 0.0, 10.0, 10.0];
1133 let result = pelt.detect(&data);
1134
1135 assert_eq!(
1136 result.changepoints.len(),
1137 1,
1138 "expected 1 changepoint in [0,0,10,10], got {:?}",
1139 result.changepoints
1140 );
1141 assert_eq!(
1142 result.changepoints[0], 2,
1143 "changepoint should be at index 2"
1144 );
1145 }
1146
1147 #[test]
1150 fn test_pelt_changepoints_sorted() {
1151 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1152
1153 let mut data = vec![0.0; 25];
1154 data.extend(vec![5.0; 25]);
1155 data.extend(vec![-3.0; 25]);
1156 data.extend(vec![7.0; 25]);
1157
1158 let result = pelt.detect(&data);
1159 for i in 1..result.changepoints.len() {
1160 assert!(
1161 result.changepoints[i] > result.changepoints[i - 1],
1162 "changepoints must be strictly increasing: {:?}",
1163 result.changepoints
1164 );
1165 }
1166 }
1167
1168 #[test]
1171 fn test_pelt_bic_penalty_scales() {
1172 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1173
1174 let mut data = vec![0.0; 50];
1177 data.extend(vec![0.5; 50]); let result = pelt.detect(&data);
1180 let _ = result;
1185 }
1186
1187 #[test]
1190 fn test_pelt_segments_cover_data() {
1191 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1192
1193 let mut data = vec![0.0; 30];
1194 data.extend(vec![5.0; 30]);
1195 data.extend(vec![0.0; 30]);
1196
1197 let result = pelt.detect(&data);
1198
1199 let mut boundaries = vec![0];
1201 boundaries.extend_from_slice(&result.changepoints);
1202 boundaries.push(data.len());
1203
1204 for i in 1..boundaries.len() {
1205 assert!(
1206 boundaries[i] > boundaries[i - 1],
1207 "segments must not have zero length"
1208 );
1209 }
1210 assert_eq!(
1211 *boundaries.last().expect("non-empty boundaries"),
1212 data.len(),
1213 "segments must cover entire data"
1214 );
1215 }
1216
1217 #[test]
1220 fn test_pelt_downward_shift() {
1221 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1222
1223 let mut data = vec![10.0; 50];
1224 data.extend(vec![2.0; 50]); let result = pelt.detect(&data);
1227 assert_eq!(result.changepoints.len(), 1, "should detect downward shift");
1228 assert!(
1229 (result.changepoints[0] as i64 - 50).unsigned_abs() <= 2,
1230 "changepoint should be near index 50, got {}",
1231 result.changepoints[0]
1232 );
1233 }
1234
1235 #[test]
1238 fn test_cost_function_params() {
1239 assert_eq!(CostFunction::L2.params_per_segment(), 1);
1240 assert_eq!(CostFunction::Normal.params_per_segment(), 2);
1241 }
1242
1243 #[test]
1246 fn test_pelt_multi_single_channel_matches_univariate() {
1247 let pelt = Pelt::new(CostFunction::L2, Penalty::Custom(5.0)).expect("valid");
1248 let mut data = vec![0.0; 50];
1249 data.extend(vec![5.0; 50]);
1250
1251 let uni = pelt.detect(&data);
1252 let multi = pelt.detect_multi(&[&data]).expect("valid");
1253 assert_eq!(uni.changepoints, multi.changepoints);
1254 }
1255
1256 #[test]
1257 fn test_pelt_multi_two_channels() {
1258 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1259 let a: Vec<f64> = [vec![0.0; 50], vec![5.0; 50]].concat();
1260 let b: Vec<f64> = [vec![0.0; 50], vec![3.0; 50]].concat();
1261
1262 let result = pelt.detect_multi(&[&a, &b]).expect("valid");
1263 assert_eq!(
1264 result.changepoints.len(),
1265 1,
1266 "expected 1 changepoint, got {:?}",
1267 result.changepoints
1268 );
1269 assert!(
1270 (result.changepoints[0] as i64 - 50).unsigned_abs() <= 2,
1271 "changepoint near 50, got {}",
1272 result.changepoints[0]
1273 );
1274 }
1275
1276 #[test]
1277 fn test_pelt_multi_inconsistent_lengths() {
1278 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1279 let a = vec![0.0; 50];
1280 let b = vec![0.0; 30];
1281 assert!(pelt.detect_multi(&[&a[..], &b[..]]).is_none());
1282 }
1283
1284 #[test]
1285 fn test_pelt_multi_empty_signals() {
1286 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1287 let result = pelt.detect_multi(&[]).expect("valid");
1288 assert!(result.changepoints.is_empty());
1289 }
1290
1291 #[test]
1292 fn test_pelt_multi_three_channels_two_changepoints() {
1293 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1294 let a: Vec<f64> = [vec![0.0; 40], vec![5.0; 40], vec![0.0; 40]].concat();
1295 let b: Vec<f64> = [vec![0.0; 40], vec![3.0; 40], vec![0.0; 40]].concat();
1296 let c: Vec<f64> = [vec![0.0; 40], vec![4.0; 40], vec![0.0; 40]].concat();
1297
1298 let result = pelt.detect_multi(&[&a, &b, &c]).expect("valid");
1299 assert_eq!(
1300 result.changepoints.len(),
1301 2,
1302 "expected 2 changepoints, got {:?}",
1303 result.changepoints
1304 );
1305 }
1306
1307 #[test]
1308 fn test_pelt_multi_short_data() {
1309 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
1310 let a = [1.0, 2.0];
1311 let b = [3.0, 4.0];
1312 let result = pelt.detect_multi(&[&a, &b]).expect("valid");
1313 assert!(result.changepoints.is_empty());
1314 }
1315}
1316
1317#[cfg(test)]
1323mod exact_dp_reference {
1324 use super::*;
1325
1326 const CASE_0: [f64; 60] = [
1328 1.02, -1.278, 0.209, -0.284, -0.226, -0.108, -1.01, -0.116, -0.433, 1.661, 0.113, -0.176,
1329 -0.141, -0.334, -0.528, -0.195, 0.241, -0.119, 0.479, -0.1, 3.012, 3.773, 3.273, 2.747,
1330 2.909, 3.27, 3.968, 2.865, 2.878, 3.501, 2.557, 2.854, 3.441, 3.29, 3.046, 3.335, 1.586,
1331 3.511, 2.52, 2.166, 3.138, 3.35, 2.778, 2.462, 3.013, 0.474, 1.203, 0.874, 0.597, 1.056,
1332 0.397, 0.037, 0.792, 0.791, 0.393, 0.109, 0.615, -0.747, 0.845, 0.746,
1333 ];
1334
1335 const CASE_1: [f64; 80] = [
1337 -0.656, 0.025, -0.386, 0.303, -0.814, -0.366, 0.284, 0.463, -0.863, -0.199, 0.131, -0.244,
1338 0.636, -0.476, 0.142, -0.419, 0.562, -0.009, -0.149, -0.687, 0.673, 0.301, 0.301, 0.455,
1339 0.14, -0.256, -0.32, -0.32, 0.548, -0.584, -0.239, -0.128, 0.09, 0.23, -0.5, -0.692,
1340 -0.002, 0.485, 0.303, 0.086, 0.366, 1.586, 0.513, 2.634, -0.589, 1.268, 0.778, 2.087,
1341 1.449, 6.1, 3.997, 3.993, -3.079, 0.319, -0.217, 2.065, -3.558, 3.349, 3.134, -1.604,
1342 -0.957, -0.602, 1.087, 2.282, 5.096, 0.605, 2.535, 1.311, 4.52, 2.484, 3.737, -1.155,
1343 0.616, -0.628, 4.01, 2.315, 0.39, 0.095, 1.969, -0.403,
1344 ];
1345
1346 const CASE_2: [f64; 50] = [
1348 -0.931, 0.481, 2.463, -0.246, -0.556, -1.171, -1.335, 0.525, 0.851, 0.009, 0.333, 0.116,
1349 0.139, -1.526, -0.458, 0.111, -0.783, -0.476, -0.819, -0.333, 0.853, -0.407, -0.154, 0.814,
1350 0.645, 2.895, -0.89, 2.057, 0.718, 1.335, 2.038, 2.283, 2.239, 1.355, 2.81, 0.917, 1.059,
1351 1.999, 0.649, 3.361, 2.219, 3.376, 1.173, 0.817, 1.367, 1.935, 0.613, 1.58, 1.183, 2.816,
1352 ];
1353
1354 const CASE_3: [f64; 60] = [
1356 9999999.337,
1357 10000001.046,
1358 9999999.356,
1359 9999999.039,
1360 9999999.29,
1361 9999998.81,
1362 10000000.146,
1363 10000001.031,
1364 10000000.164,
1365 10000000.624,
1366 10000001.632,
1367 10000000.27,
1368 10000000.195,
1369 9999999.725,
1370 9999998.392,
1371 10000000.76,
1372 9999998.244,
1373 10000000.653,
1374 9999999.986,
1375 10000001.127,
1376 9999999.932,
1377 9999999.177,
1378 10000000.358,
1379 9999999.44,
1380 9999999.819,
1381 10000000.042,
1382 9999999.865,
1383 9999999.811,
1384 9999999.168,
1385 9999999.811,
1386 10000001.862,
1387 10000003.843,
1388 10000002.802,
1389 10000005.12,
1390 10000005.27,
1391 10000002.049,
1392 10000004.145,
1393 10000003.874,
1394 10000002.953,
1395 10000004.532,
1396 10000003.538,
1397 10000002.232,
1398 10000003.733,
1399 10000003.852,
1400 10000004.106,
1401 10000002.769,
1402 10000004.616,
1403 10000004.735,
1404 10000002.854,
1405 10000003.341,
1406 10000003.92,
1407 10000003.434,
1408 10000005.744,
1409 10000004.208,
1410 10000002.989,
1411 10000003.212,
1412 10000003.943,
1413 10000006.296,
1414 10000003.822,
1415 10000004.127,
1416 ];
1417
1418 fn detect(cost: CostFunction, penalty: Penalty, min_len: usize, data: &[f64]) -> Vec<usize> {
1419 Pelt::with_min_segment_len(cost, penalty, min_len)
1420 .expect("valid settings")
1421 .detect(data)
1422 .changepoints
1423 }
1424
1425 const LARGE_LEVEL: [f64; 80] = [
1427 1699999999.198,
1428 1699999998.676,
1429 1699999999.752,
1430 1700000000.42,
1431 1700000001.136,
1432 1700000000.11,
1433 1699999999.447,
1434 1699999999.215,
1435 1700000000.749,
1436 1700000001.635,
1437 1700000000.273,
1438 1699999998.767,
1439 1699999999.042,
1440 1700000001.6,
1441 1700000000.203,
1442 1699999998.268,
1443 1699999999.916,
1444 1699999998.837,
1445 1699999999.371,
1446 1699999999.512,
1447 1699999999.287,
1448 1700000000.553,
1449 1699999999.937,
1450 1699999999.411,
1451 1700000000.41,
1452 1700000000.83,
1453 1699999998.357,
1454 1699999999.743,
1455 1699999999.019,
1456 1699999999.827,
1457 1699999998.711,
1458 1700000000.021,
1459 1699999999.962,
1460 1699999999.696,
1461 1699999998.952,
1462 1699999999.604,
1463 1699999998.909,
1464 1699999998.645,
1465 1700000000.225,
1466 1699999998.891,
1467 1700000006.17,
1468 1700000005.717,
1469 1700000003.002,
1470 1700000005.272,
1471 1700000003.898,
1472 1700000005.033,
1473 1700000005.044,
1474 1700000003.012,
1475 1700000004.767,
1476 1700000004.744,
1477 1700000005.962,
1478 1700000003.819,
1479 1700000005.738,
1480 1700000003.901,
1481 1700000004.669,
1482 1700000004.16,
1483 1700000006.449,
1484 1700000005.568,
1485 1700000007.432,
1486 1700000005.642,
1487 1700000005.845,
1488 1700000005.841,
1489 1700000004.393,
1490 1700000004.93,
1491 1700000006.35,
1492 1700000004.603,
1493 1700000005.189,
1494 1700000004.979,
1495 1700000005.609,
1496 1700000004.635,
1497 1700000004.848,
1498 1700000005.242,
1499 1700000005.103,
1500 1700000004.135,
1501 1700000005.896,
1502 1700000003.702,
1503 1700000003.799,
1504 1700000003.718,
1505 1700000005.967,
1506 1700000004.639,
1507 ];
1508
1509 #[test]
1510 fn a_large_level_does_not_move_the_optimum() {
1511 assert_eq!(
1514 detect(
1515 CostFunction::L2,
1516 Penalty::Custom((LARGE_LEVEL.len() as f64).ln()),
1517 2,
1518 &LARGE_LEVEL
1519 ),
1520 vec![40, 56, 62]
1521 );
1522 }
1523
1524 #[test]
1525 fn pruned_search_returns_the_exact_optimum() {
1526 assert_eq!(
1528 detect(
1529 CostFunction::L2,
1530 Penalty::Custom((CASE_0.len() as f64).ln()),
1531 2,
1532 &CASE_0
1533 ),
1534 vec![20, 45]
1535 );
1536 assert_eq!(
1538 detect(CostFunction::Normal, Penalty::Bic, 5, &CASE_1),
1539 vec![41]
1540 );
1541 assert_eq!(
1543 detect(CostFunction::L2, Penalty::Custom(5.0), 5, &CASE_2),
1544 vec![25]
1545 );
1546 assert_eq!(
1548 detect(
1549 CostFunction::L2,
1550 Penalty::Custom((CASE_3.len() as f64).ln()),
1551 2,
1552 &CASE_3
1553 ),
1554 vec![31]
1555 );
1556 }
1557}
1558
1559#[cfg(test)]
1565mod exact_dp_reference_multi {
1566 use super::*;
1567
1568 fn detect_multi(
1569 cost: CostFunction,
1570 penalty: Penalty,
1571 min_len: usize,
1572 channels: &[&[f64]],
1573 ) -> Vec<usize> {
1574 Pelt::with_min_segment_len(cost, penalty, min_len)
1575 .expect("valid settings")
1576 .detect_multi(channels)
1577 .expect("equal lengths")
1578 .changepoints
1579 }
1580
1581 const A_0: [f64; 60] = [
1582 -0.804, 0.801, -0.66, -0.813, 0.703, 1.326, -0.448, 2.263, -1.25, -2.127, 1.654, -0.384,
1583 0.038, -0.988, 1.559, -1.068, -0.242, -1.431, -1.253, -0.734, -0.485, 3.102, 2.171, -0.177,
1584 -1.051, 0.913, 3.976, 2.577, 1.26, 2.31, 4.25, 2.975, 2.82, 1.342, 3.658, 2.263, 4.546,
1585 0.756, 1.575, 0.993, 2.598, 3.666, 3.249, 2.378, 2.049, 3.232, 2.173, 1.678, 2.103, 2.626,
1586 1.201, 3.216, 1.34, 2.219, 3.623, 3.182, 5.136, 2.97, 1.461, 1.545,
1587 ];
1588
1589 const A_1: [f64; 60] = [
1590 1.04, -1.488, 0.963, -0.538, 1.736, 0.943, -0.191, 1.916, 0.606, -1.098, -0.764, 0.437,
1591 -0.276, 0.696, -1.029, -0.504, -0.287, -0.619, 0.587, -0.272, -1.753, 0.125, -2.015,
1592 -0.146, 1.821, 1.502, 1.776, -0.202, 1.056, 3.046, 2.47, 0.695, 1.365, 1.453, 0.705,
1593 -0.373, 1.967, 0.828, 1.686, -0.625, 0.406, 2.587, 1.235, -1.549, 1.573, -0.702, 2.101,
1594 2.461, 2.297, 1.882, 2.46, 2.129, 2.153, 3.022, -0.409, 0.904, 2.0, 2.557, 0.326, 1.061,
1595 ];
1596
1597 const B_0: [f64; 60] = [
1598 0.549, -0.705, -0.681, -0.052, 0.206, -0.084, -0.413, -0.562, -0.393, 0.281, 0.6, -1.232,
1599 -0.127, 0.072, 0.255, -0.094, -0.273, 0.606, -0.597, 0.615, 0.599, -0.193, 0.08, 1.066,
1600 -0.624, -0.284, 0.675, -0.924, -0.54, -0.564, -1.563, 2.435, -3.255, 0.942, 0.323, 1.455,
1601 -5.14, 1.305, -3.933, -0.982, 1.378, 2.145, -4.895, 0.009, -1.539, 3.119, -0.391, -2.202,
1602 5.382, 3.363, 2.322, -0.958, 1.278, -1.489, -0.671, -3.143, -0.972, 4.385, 1.683, -1.434,
1603 ];
1604
1605 const B_1: [f64; 60] = [
1606 -0.394, -0.653, 0.721, -0.457, -0.009, 0.161, -0.763, 0.533, -0.474, -0.655, -0.423, 0.734,
1607 0.24, 0.621, -0.094, -0.334, 0.193, 0.01, 0.27, 0.499, 0.814, -1.284, 0.793, 0.584, -0.022,
1608 -0.673, 0.033, 0.267, -0.507, 0.303, 5.49, -4.019, 0.665, 4.472, -0.633, -5.961, 2.979,
1609 1.608, 2.534, -0.708, -2.226, 1.057, 7.403, 1.97, 2.913, 3.112, 1.576, 4.229, 3.941, 1.798,
1610 0.382, 0.434, -2.124, 3.613, 2.052, 2.66, 2.733, 5.311, 0.163, 5.14,
1611 ];
1612
1613 const B_2: [f64; 60] = [
1614 0.13, -0.45, -0.281, 0.099, -0.849, -0.968, -0.102, 0.034, -0.244, -0.855, -1.248, 0.123,
1615 -0.757, -0.162, 0.304, -0.05, -0.153, 0.633, 0.197, -0.539, 0.398, -0.124, 0.021, 0.168,
1616 -0.909, -0.353, -0.161, -0.339, -0.212, 0.003, 0.043, -0.051, -0.087, -0.065, -0.55,
1617 -0.066, 0.756, 0.312, -0.21, -0.042, -0.518, -0.595, -0.717, -0.091, 0.24, -0.416, -0.179,
1618 0.611, -0.219, -0.528, -0.603, -1.164, -0.22, 0.875, -0.005, 1.255, -0.619, -0.851, 0.107,
1619 0.101,
1620 ];
1621
1622 const C_0: [f64; 60] = [
1623 0.954, 0.369, 0.316, 0.089, -0.008, -0.682, 0.736, -2.132, 2.099, -1.086, -0.896, -0.925,
1624 -0.576, -1.991, -0.338, 0.524, -0.182, 0.678, -0.767, 0.299, 1.874, 3.856, 2.051, 3.948,
1625 3.904, 2.532, 1.485, 2.418, 4.804, 3.011, 3.46, 3.401, 3.144, 4.83, 2.15, 1.129, 1.494,
1626 5.09, 3.588, 3.541, 0.143, -1.055, 1.09, 1.263, 0.046, -0.552, 1.53, 1.03, 1.19, -0.342,
1627 0.123, -0.145, -0.717, 0.334, 0.933, -1.102, 0.313, -2.129, 0.467, 0.762,
1628 ];
1629
1630 const C_1: [f64; 60] = [
1631 0.443, -0.016, 0.496, -0.191, 0.405, 0.124, 0.133, -0.853, -0.11, -1.616, -0.619, -0.114,
1632 0.05, 1.525, -1.539, 0.252, 0.402, 1.997, -0.821, 1.855, -3.542, -1.497, -2.465, 0.122,
1633 -3.354, -3.124, -2.577, -2.521, -1.298, -2.091, 0.046, -2.452, -1.855, -2.804, -2.243,
1634 -2.454, -1.28, -0.159, -1.652, -1.664, 0.177, -0.067, 0.107, -0.184, -0.051, 0.917, 1.159,
1635 1.173, 1.039, 0.66, 0.071, -0.928, 0.8, -0.388, 1.233, -0.091, 0.962, 0.423, -1.042,
1636 -0.113,
1637 ];
1638
1639 const D_0: [f64; 60] = [
1640 999998.994,
1641 999998.457,
1642 999997.649,
1643 999999.049,
1644 1000000.222,
1645 1000000.29,
1646 999999.019,
1647 1000000.062,
1648 1000000.761,
1649 999998.951,
1650 999999.22,
1651 999999.29,
1652 999999.563,
1653 1000000.661,
1654 1000000.006,
1655 999999.314,
1656 1000000.983,
1657 999998.466,
1658 1000000.97,
1659 1000002.599,
1660 999999.604,
1661 999998.518,
1662 1000001.488,
1663 1000001.909,
1664 999999.945,
1665 999998.831,
1666 999999.774,
1667 999999.966,
1668 999998.892,
1669 999998.914,
1670 1000003.175,
1671 1000004.168,
1672 1000004.646,
1673 1000003.017,
1674 1000001.941,
1675 1000004.399,
1676 1000003.577,
1677 1000002.784,
1678 1000003.348,
1679 1000002.429,
1680 1000002.519,
1681 1000001.615,
1682 1000002.515,
1683 1000001.867,
1684 1000003.272,
1685 1000004.196,
1686 1000000.7,
1687 1000003.487,
1688 1000003.406,
1689 1000002.157,
1690 1000003.654,
1691 1000002.441,
1692 1000002.837,
1693 1000003.022,
1694 1000003.252,
1695 1000003.838,
1696 1000003.735,
1697 1000002.762,
1698 1000003.664,
1699 1000003.312,
1700 ];
1701
1702 const D_1: [f64; 60] = [
1703 0.323, 1.153, -0.737, -0.765, -0.387, -0.132, -0.38, -0.017, 0.039, -1.855, 1.465, 2.111,
1704 -1.086, 0.568, 0.859, 1.543, 1.12, 0.916, -0.591, -1.247, -0.131, -0.231, -0.639, -0.829,
1705 0.322, -0.727, -1.789, -0.935, -0.092, 0.186, 1.729, 3.461, 2.688, 1.473, 3.507, 1.722,
1706 1.093, 0.833, 1.715, 1.832, 3.849, 1.504, 1.669, 3.159, 1.839, 2.265, 2.845, 1.986, 1.832,
1707 0.915, 2.022, 2.275, 1.484, 2.071, 3.911, -0.234, 2.667, 1.866, 2.315, 2.401,
1708 ];
1709
1710 const D_2: [f64; 60] = [
1711 -50.107, -50.532, -50.842, -48.33, -49.442, -49.761, -51.681, -52.824, -50.637, -51.821,
1712 -53.9, -49.553, -48.224, -49.598, -48.92, -51.311, -48.414, -46.838, -47.13, -49.746,
1713 -47.412, -51.215, -47.565, -50.883, -50.071, -53.527, -49.043, -48.191, -51.551, -49.062,
1714 -48.796, -49.414, -46.466, -48.3, -49.039, -48.988, -48.978, -48.995, -52.944, -51.869,
1715 -51.183, -47.484, -49.5, -48.72, -48.391, -50.368, -47.708, -47.899, -50.359, -50.786,
1716 -49.405, -49.132, -46.105, -56.126, -49.201, -49.857, -51.277, -51.729, -50.852, -49.224,
1717 ];
1718
1719 #[test]
1720 fn the_pruned_multi_channel_search_returns_the_exact_optimum() {
1721 assert_eq!(
1722 detect_multi(
1723 CostFunction::L2,
1724 Penalty::Custom((A_0.len() as f64).ln()),
1725 2,
1726 &[&A_0, &A_1]
1727 ),
1728 vec![21, 23, 26]
1729 );
1730 assert_eq!(
1731 detect_multi(CostFunction::Normal, Penalty::Bic, 5, &[&B_0, &B_1, &B_2]),
1732 vec![30]
1733 );
1734 assert_eq!(
1735 detect_multi(CostFunction::L2, Penalty::Custom(8.0), 5, &[&C_0, &C_1]),
1736 vec![20, 40]
1737 );
1738 assert_eq!(
1739 detect_multi(
1740 CostFunction::L2,
1741 Penalty::Custom((D_0.len() as f64).ln()),
1742 2,
1743 &[&D_0, &D_1, &D_2]
1744 ),
1745 vec![6, 11, 21, 30, 38, 41, 53]
1746 );
1747 }
1748}