1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum CostFunction {
56 L2,
62 Normal,
69}
70
71impl CostFunction {
72 fn params_per_segment(self) -> usize {
74 match self {
75 CostFunction::L2 => 1,
76 CostFunction::Normal => 2,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy)]
83pub enum Penalty {
84 Bic,
90 Custom(f64),
93}
94
95pub struct Pelt {
121 cost: CostFunction,
123 penalty: Penalty,
125 min_segment_len: usize,
127}
128
129#[derive(Debug, Clone)]
131pub struct PeltResult {
132 pub changepoints: Vec<usize>,
138}
139
140#[derive(Debug, Clone)]
142pub struct MultiPeltResult {
143 pub changepoints: Vec<usize>,
145}
146
147impl Pelt {
148 pub fn new(cost: CostFunction, penalty: Penalty) -> Option<Self> {
161 Self::with_min_segment_len(cost, penalty, 2)
162 }
163
164 pub fn with_min_segment_len(
177 cost: CostFunction,
178 penalty: Penalty,
179 min_segment_len: usize,
180 ) -> Option<Self> {
181 if let Penalty::Custom(p) = penalty {
182 if !p.is_finite() || p <= 0.0 {
183 return None;
184 }
185 }
186 if min_segment_len < 2 {
187 return None;
188 }
189 Some(Self {
190 cost,
191 penalty,
192 min_segment_len,
193 })
194 }
195
196 pub fn detect(&self, data: &[f64]) -> PeltResult {
225 let n = data.len();
226
227 if n < 2 * self.min_segment_len {
228 return PeltResult {
229 changepoints: Vec::new(),
230 };
231 }
232
233 let penalty_value = self.resolve_penalty(n, &[data]);
234 if penalty_value <= 0.0 {
235 return PeltResult {
238 changepoints: Vec::new(),
239 };
240 }
241
242 let (cum_sum, cum_sum_sq) = cumulative_sums(data);
246
247 let mut f = vec![0.0_f64; n + 1];
250 f[0] = -penalty_value;
251
252 let mut last_change = vec![0_usize; n + 1];
254
255 let mut candidates: Vec<usize> = vec![0];
257
258 for t in self.min_segment_len..=n {
259 let mut best_cost = f64::INFINITY;
261 let mut best_tau = 0;
262
263 for &tau in &candidates {
264 let seg_len = t - tau;
265 if seg_len < self.min_segment_len {
266 continue;
267 }
268
269 let cost = self.segment_cost(&cum_sum, &cum_sum_sq, tau, t);
270 let total = f[tau] + cost + penalty_value;
271
272 if total < best_cost {
273 best_cost = total;
274 best_tau = tau;
275 }
276 }
277
278 f[t] = best_cost;
279 last_change[t] = best_tau;
280
281 candidates.retain(|&tau| {
285 let seg_len = t - tau;
286 if seg_len < self.min_segment_len {
287 return true; }
289 let cost = self.segment_cost(&cum_sum, &cum_sum_sq, tau, t);
290 f[tau] + cost < f[t] + penalty_value
291 });
292
293 candidates.push(t);
294 }
295
296 let mut changepoints = Vec::new();
298 let mut t = n;
299 while t > 0 {
300 let tau = last_change[t];
301 if tau > 0 {
302 changepoints.push(tau);
303 }
304 t = tau;
305 }
306
307 changepoints.sort_unstable();
308
309 PeltResult { changepoints }
310 }
311
312 pub fn detect_multi(&self, signals: &[&[f64]]) -> Option<MultiPeltResult> {
352 if signals.is_empty() {
353 return Some(MultiPeltResult {
354 changepoints: Vec::new(),
355 });
356 }
357
358 let n = signals[0].len();
359 if signals.iter().any(|s| s.len() != n) {
360 return None;
361 }
362
363 if n < 2 * self.min_segment_len {
364 return Some(MultiPeltResult {
365 changepoints: Vec::new(),
366 });
367 }
368
369 let n_channels = signals.len();
370 let penalty_value = self.resolve_penalty(n, signals);
371 if penalty_value <= 0.0 {
372 return Some(MultiPeltResult {
373 changepoints: Vec::new(),
374 });
375 }
376
377 let mut cum_sums: Vec<Vec<f64>> = Vec::with_capacity(n_channels);
379 let mut cum_sum_sqs: Vec<Vec<f64>> = Vec::with_capacity(n_channels);
380
381 for signal in signals {
382 let (cs, css) = cumulative_sums(signal);
383 cum_sums.push(cs);
384 cum_sum_sqs.push(css);
385 }
386
387 let mut f = vec![0.0_f64; n + 1];
388 f[0] = -penalty_value;
389 let mut last_change = vec![0_usize; n + 1];
390 let mut candidates: Vec<usize> = vec![0];
391
392 for t in self.min_segment_len..=n {
393 let mut best_cost = f64::INFINITY;
394 let mut best_tau = 0;
395
396 for &tau in &candidates {
397 let seg_len = t - tau;
398 if seg_len < self.min_segment_len {
399 continue;
400 }
401
402 let cost: f64 = (0..n_channels)
403 .map(|ch| self.segment_cost(&cum_sums[ch], &cum_sum_sqs[ch], tau, t))
404 .sum();
405 let total = f[tau] + cost + penalty_value;
406
407 if total < best_cost {
408 best_cost = total;
409 best_tau = tau;
410 }
411 }
412
413 f[t] = best_cost;
414 last_change[t] = best_tau;
415
416 candidates.retain(|&tau| {
417 let seg_len = t - tau;
418 if seg_len < self.min_segment_len {
419 return true;
420 }
421 let cost: f64 = (0..n_channels)
422 .map(|ch| self.segment_cost(&cum_sums[ch], &cum_sum_sqs[ch], tau, t))
423 .sum();
424 f[tau] + cost < f[t] + penalty_value
425 });
426
427 candidates.push(t);
428 }
429
430 let mut changepoints = Vec::new();
431 let mut t = n;
432 while t > 0 {
433 let tau = last_change[t];
434 if tau > 0 {
435 changepoints.push(tau);
436 }
437 t = tau;
438 }
439 changepoints.sort_unstable();
440
441 Some(MultiPeltResult { changepoints })
442 }
443
444 fn resolve_penalty(&self, n: usize, signals: &[&[f64]]) -> f64 {
455 let channels = signals.len() as f64;
456 match self.penalty {
457 Penalty::Bic => {
458 let p = self.cost.params_per_segment() as f64;
459 let per_channel = p * (n as f64).ln();
460 match self.cost {
461 CostFunction::L2 => {
462 per_channel * signals.iter().map(|s| noise_variance(s)).sum::<f64>()
463 }
464 CostFunction::Normal => per_channel * channels,
465 }
466 }
467 Penalty::Custom(val) => val * channels,
468 }
469 }
470
471 fn segment_cost(&self, cum_sum: &[f64], cum_sum_sq: &[f64], start: usize, end: usize) -> f64 {
477 let seg_len = (end - start) as f64;
478 let sum = cum_sum[end] - cum_sum[start];
479 let sum_sq = cum_sum_sq[end] - cum_sum_sq[start];
480 let mean = sum / seg_len;
481
482 match self.cost {
483 CostFunction::L2 => {
484 sum_sq - seg_len * mean * mean
486 }
487 CostFunction::Normal => {
488 let variance = (sum_sq - seg_len * mean * mean) / seg_len;
490 if variance <= 0.0 {
491 seg_len * (f64::MIN_POSITIVE).ln()
494 } else {
495 seg_len * variance.ln()
496 }
497 }
498 }
499 }
500}
501
502fn noise_variance(signal: &[f64]) -> f64 {
509 if signal.len() < 2 {
510 return 0.0;
511 }
512 let mut diffs: Vec<f64> = signal.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
513 diffs.sort_by(f64::total_cmp);
514 let median = if diffs.len() % 2 == 1 {
515 diffs[diffs.len() / 2]
516 } else {
517 0.5 * (diffs[diffs.len() / 2 - 1] + diffs[diffs.len() / 2])
518 };
519 let sigma = median / (0.6745 * std::f64::consts::SQRT_2);
520 let (lo, hi) = signal
521 .iter()
522 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
523 (lo.min(v), hi.max(v))
524 });
525 let floor = 1e-6 * (hi - lo);
526 sigma.max(floor).powi(2)
527}
528
529fn cumulative_sums(signal: &[f64]) -> (Vec<f64>, Vec<f64>) {
538 let n = signal.len();
539 let center = signal.iter().sum::<f64>() / n as f64;
540 let mut cum_sum = vec![0.0_f64; n + 1];
541 let mut cum_sum_sq = vec![0.0_f64; n + 1];
542 for (i, &y) in signal.iter().enumerate() {
543 let y = y - center;
544 cum_sum[i + 1] = cum_sum[i] + y;
545 cum_sum_sq[i + 1] = cum_sum_sq[i] + y * y;
546 }
547 (cum_sum, cum_sum_sq)
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
557 fn test_pelt_valid_construction() {
558 assert!(Pelt::new(CostFunction::L2, Penalty::Bic).is_some());
559 assert!(Pelt::new(CostFunction::Normal, Penalty::Bic).is_some());
560 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(10.0)).is_some());
561 }
562
563 #[test]
564 fn test_pelt_invalid_custom_penalty() {
565 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(0.0)).is_none());
566 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(-1.0)).is_none());
567 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(f64::NAN)).is_none());
568 assert!(Pelt::new(CostFunction::L2, Penalty::Custom(f64::INFINITY)).is_none());
569 }
570
571 #[test]
572 fn test_pelt_invalid_min_segment_len() {
573 assert!(Pelt::with_min_segment_len(CostFunction::L2, Penalty::Bic, 0).is_none());
574 assert!(Pelt::with_min_segment_len(CostFunction::L2, Penalty::Bic, 1).is_none());
575 assert!(Pelt::with_min_segment_len(CostFunction::L2, Penalty::Bic, 2).is_some());
576 }
577
578 #[test]
581 fn test_pelt_empty_data() {
582 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
583 let result = pelt.detect(&[]);
584 assert!(result.changepoints.is_empty());
585 }
586
587 #[test]
588 fn test_pelt_too_short_data() {
589 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
590 let result = pelt.detect(&[1.0, 2.0, 3.0]);
592 assert!(result.changepoints.is_empty());
593 }
594
595 #[test]
598 fn test_pelt_constant_data_no_changepoint() {
599 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
600 let data = vec![5.0; 100];
601 let result = pelt.detect(&data);
602 assert!(
603 result.changepoints.is_empty(),
604 "constant data should have no changepoints, got {:?}",
605 result.changepoints
606 );
607 }
608
609 #[test]
610 fn test_pelt_normal_cost_constant_data() {
611 let pelt = Pelt::new(CostFunction::Normal, Penalty::Bic).expect("valid");
612 let data = vec![5.0; 100];
613 let result = pelt.detect(&data);
614 assert!(
615 result.changepoints.is_empty(),
616 "constant data should have no changepoints with Normal cost, got {:?}",
617 result.changepoints
618 );
619 }
620
621 #[test]
624 fn test_pelt_single_mean_shift_l2() {
625 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
626
627 let mut data = vec![0.0; 50];
628 data.extend(vec![5.0; 50]);
629
630 let result = pelt.detect(&data);
631 assert_eq!(
632 result.changepoints.len(),
633 1,
634 "expected 1 changepoint, got {:?}",
635 result.changepoints
636 );
637 assert!(
638 (result.changepoints[0] as i64 - 50).unsigned_abs() <= 2,
639 "changepoint should be near index 50, got {}",
640 result.changepoints[0]
641 );
642 }
643
644 #[test]
645 fn test_pelt_single_mean_shift_normal() {
646 let pelt = Pelt::new(CostFunction::Normal, Penalty::Bic).expect("valid");
647
648 let mut data = vec![0.0; 50];
649 data.extend(vec![5.0; 50]);
650
651 let result = pelt.detect(&data);
652 assert_eq!(
653 result.changepoints.len(),
654 1,
655 "expected 1 changepoint with Normal cost, got {:?}",
656 result.changepoints
657 );
658 assert!(
659 (result.changepoints[0] as i64 - 50).unsigned_abs() <= 2,
660 "changepoint should be near index 50, got {}",
661 result.changepoints[0]
662 );
663 }
664
665 #[test]
668 fn test_pelt_two_changepoints() {
669 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
670
671 let mut data = vec![0.0; 40];
672 data.extend(vec![5.0; 40]);
673 data.extend(vec![0.0; 40]);
674
675 let result = pelt.detect(&data);
676 assert_eq!(
677 result.changepoints.len(),
678 2,
679 "expected 2 changepoints, got {:?}",
680 result.changepoints
681 );
682
683 assert!(
685 (result.changepoints[0] as i64 - 40).unsigned_abs() <= 2,
686 "first changepoint near 40, got {}",
687 result.changepoints[0]
688 );
689 assert!(
690 (result.changepoints[1] as i64 - 80).unsigned_abs() <= 2,
691 "second changepoint near 80, got {}",
692 result.changepoints[1]
693 );
694 }
695
696 #[test]
697 fn test_pelt_three_changepoints() {
698 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
699
700 let mut data = vec![0.0; 30];
701 data.extend(vec![4.0; 30]);
702 data.extend(vec![-2.0; 30]);
703 data.extend(vec![3.0; 30]);
704
705 let result = pelt.detect(&data);
706 assert_eq!(
707 result.changepoints.len(),
708 3,
709 "expected 3 changepoints, got {:?}",
710 result.changepoints
711 );
712
713 for i in 1..result.changepoints.len() {
715 assert!(
716 result.changepoints[i] > result.changepoints[i - 1],
717 "changepoints should be strictly increasing"
718 );
719 }
720 }
721
722 #[test]
725 fn test_pelt_variance_change_normal_cost() {
726 let pelt = Pelt::new(CostFunction::Normal, Penalty::Bic).expect("valid");
728
729 let mut data = Vec::with_capacity(200);
731 for i in 0..100 {
733 data.push(if i % 2 == 0 { 0.1 } else { -0.1 });
734 }
735 for i in 0..100 {
737 data.push(if i % 2 == 0 { 5.0 } else { -5.0 });
738 }
739
740 let result = pelt.detect(&data);
741 assert!(
742 !result.changepoints.is_empty(),
743 "Normal cost should detect variance change"
744 );
745 let cp = result.changepoints[0];
747 assert!(
748 (cp as i64 - 100).unsigned_abs() <= 5,
749 "variance changepoint should be near 100, got {}",
750 cp
751 );
752 }
753
754 #[test]
757 fn test_pelt_higher_penalty_fewer_changepoints() {
758 let mut data = vec![0.0; 30];
759 data.extend(vec![2.0; 30]);
760 data.extend(vec![0.0; 30]);
761
762 let pelt_low = Pelt::new(CostFunction::L2, Penalty::Custom(1.0)).expect("valid");
763 let pelt_high = Pelt::new(CostFunction::L2, Penalty::Custom(100.0)).expect("valid");
764
765 let result_low = pelt_low.detect(&data);
766 let result_high = pelt_high.detect(&data);
767
768 assert!(
769 result_low.changepoints.len() >= result_high.changepoints.len(),
770 "higher penalty should produce fewer or equal changepoints: low={}, high={}",
771 result_low.changepoints.len(),
772 result_high.changepoints.len()
773 );
774 }
775
776 #[test]
779 fn test_pelt_custom_min_segment_len() {
780 let mut data = vec![0.0; 50];
781 data.extend(vec![10.0; 50]);
782
783 let pelt = Pelt::with_min_segment_len(CostFunction::L2, Penalty::Bic, 10).expect("valid");
784 let result = pelt.detect(&data);
785 assert_eq!(
786 result.changepoints.len(),
787 1,
788 "should detect changepoint with min_segment_len=10"
789 );
790
791 let mut boundaries = vec![0];
793 boundaries.extend_from_slice(&result.changepoints);
794 boundaries.push(data.len());
795 for i in 1..boundaries.len() {
796 let seg_len = boundaries[i] - boundaries[i - 1];
797 assert!(
798 seg_len >= 10,
799 "segment length {} is less than min_segment_len=10",
800 seg_len
801 );
802 }
803 }
804
805 #[test]
816 fn test_pelt_exact_small_example() {
817 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
818 let data = [0.0, 0.0, 10.0, 10.0];
819 let result = pelt.detect(&data);
820
821 assert_eq!(
822 result.changepoints.len(),
823 1,
824 "expected 1 changepoint in [0,0,10,10], got {:?}",
825 result.changepoints
826 );
827 assert_eq!(
828 result.changepoints[0], 2,
829 "changepoint should be at index 2"
830 );
831 }
832
833 #[test]
836 fn test_pelt_changepoints_sorted() {
837 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
838
839 let mut data = vec![0.0; 25];
840 data.extend(vec![5.0; 25]);
841 data.extend(vec![-3.0; 25]);
842 data.extend(vec![7.0; 25]);
843
844 let result = pelt.detect(&data);
845 for i in 1..result.changepoints.len() {
846 assert!(
847 result.changepoints[i] > result.changepoints[i - 1],
848 "changepoints must be strictly increasing: {:?}",
849 result.changepoints
850 );
851 }
852 }
853
854 #[test]
857 fn test_pelt_bic_penalty_scales() {
858 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
859
860 let mut data = vec![0.0; 50];
863 data.extend(vec![0.5; 50]); let result = pelt.detect(&data);
866 let _ = result;
871 }
872
873 #[test]
876 fn test_pelt_segments_cover_data() {
877 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
878
879 let mut data = vec![0.0; 30];
880 data.extend(vec![5.0; 30]);
881 data.extend(vec![0.0; 30]);
882
883 let result = pelt.detect(&data);
884
885 let mut boundaries = vec![0];
887 boundaries.extend_from_slice(&result.changepoints);
888 boundaries.push(data.len());
889
890 for i in 1..boundaries.len() {
891 assert!(
892 boundaries[i] > boundaries[i - 1],
893 "segments must not have zero length"
894 );
895 }
896 assert_eq!(
897 *boundaries.last().expect("non-empty boundaries"),
898 data.len(),
899 "segments must cover entire data"
900 );
901 }
902
903 #[test]
906 fn test_pelt_downward_shift() {
907 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
908
909 let mut data = vec![10.0; 50];
910 data.extend(vec![2.0; 50]); let result = pelt.detect(&data);
913 assert_eq!(result.changepoints.len(), 1, "should detect downward shift");
914 assert!(
915 (result.changepoints[0] as i64 - 50).unsigned_abs() <= 2,
916 "changepoint should be near index 50, got {}",
917 result.changepoints[0]
918 );
919 }
920
921 #[test]
924 fn test_cost_function_params() {
925 assert_eq!(CostFunction::L2.params_per_segment(), 1);
926 assert_eq!(CostFunction::Normal.params_per_segment(), 2);
927 }
928
929 #[test]
932 fn test_pelt_multi_single_channel_matches_univariate() {
933 let pelt = Pelt::new(CostFunction::L2, Penalty::Custom(5.0)).expect("valid");
934 let mut data = vec![0.0; 50];
935 data.extend(vec![5.0; 50]);
936
937 let uni = pelt.detect(&data);
938 let multi = pelt.detect_multi(&[&data]).expect("valid");
939 assert_eq!(uni.changepoints, multi.changepoints);
940 }
941
942 #[test]
943 fn test_pelt_multi_two_channels() {
944 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
945 let a: Vec<f64> = [vec![0.0; 50], vec![5.0; 50]].concat();
946 let b: Vec<f64> = [vec![0.0; 50], vec![3.0; 50]].concat();
947
948 let result = pelt.detect_multi(&[&a, &b]).expect("valid");
949 assert_eq!(
950 result.changepoints.len(),
951 1,
952 "expected 1 changepoint, got {:?}",
953 result.changepoints
954 );
955 assert!(
956 (result.changepoints[0] as i64 - 50).unsigned_abs() <= 2,
957 "changepoint near 50, got {}",
958 result.changepoints[0]
959 );
960 }
961
962 #[test]
963 fn test_pelt_multi_inconsistent_lengths() {
964 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
965 let a = vec![0.0; 50];
966 let b = vec![0.0; 30];
967 assert!(pelt.detect_multi(&[&a[..], &b[..]]).is_none());
968 }
969
970 #[test]
971 fn test_pelt_multi_empty_signals() {
972 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
973 let result = pelt.detect_multi(&[]).expect("valid");
974 assert!(result.changepoints.is_empty());
975 }
976
977 #[test]
978 fn test_pelt_multi_three_channels_two_changepoints() {
979 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
980 let a: Vec<f64> = [vec![0.0; 40], vec![5.0; 40], vec![0.0; 40]].concat();
981 let b: Vec<f64> = [vec![0.0; 40], vec![3.0; 40], vec![0.0; 40]].concat();
982 let c: Vec<f64> = [vec![0.0; 40], vec![4.0; 40], vec![0.0; 40]].concat();
983
984 let result = pelt.detect_multi(&[&a, &b, &c]).expect("valid");
985 assert_eq!(
986 result.changepoints.len(),
987 2,
988 "expected 2 changepoints, got {:?}",
989 result.changepoints
990 );
991 }
992
993 #[test]
994 fn test_pelt_multi_short_data() {
995 let pelt = Pelt::new(CostFunction::L2, Penalty::Bic).expect("valid");
996 let a = [1.0, 2.0];
997 let b = [3.0, 4.0];
998 let result = pelt.detect_multi(&[&a, &b]).expect("valid");
999 assert!(result.changepoints.is_empty());
1000 }
1001}
1002
1003#[cfg(test)]
1009mod exact_dp_reference {
1010 use super::*;
1011
1012 const CASE_0: [f64; 60] = [
1014 1.02, -1.278, 0.209, -0.284, -0.226, -0.108, -1.01, -0.116, -0.433, 1.661, 0.113, -0.176,
1015 -0.141, -0.334, -0.528, -0.195, 0.241, -0.119, 0.479, -0.1, 3.012, 3.773, 3.273, 2.747,
1016 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,
1017 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,
1018 0.397, 0.037, 0.792, 0.791, 0.393, 0.109, 0.615, -0.747, 0.845, 0.746,
1019 ];
1020
1021 const CASE_1: [f64; 80] = [
1023 -0.656, 0.025, -0.386, 0.303, -0.814, -0.366, 0.284, 0.463, -0.863, -0.199, 0.131, -0.244,
1024 0.636, -0.476, 0.142, -0.419, 0.562, -0.009, -0.149, -0.687, 0.673, 0.301, 0.301, 0.455,
1025 0.14, -0.256, -0.32, -0.32, 0.548, -0.584, -0.239, -0.128, 0.09, 0.23, -0.5, -0.692,
1026 -0.002, 0.485, 0.303, 0.086, 0.366, 1.586, 0.513, 2.634, -0.589, 1.268, 0.778, 2.087,
1027 1.449, 6.1, 3.997, 3.993, -3.079, 0.319, -0.217, 2.065, -3.558, 3.349, 3.134, -1.604,
1028 -0.957, -0.602, 1.087, 2.282, 5.096, 0.605, 2.535, 1.311, 4.52, 2.484, 3.737, -1.155,
1029 0.616, -0.628, 4.01, 2.315, 0.39, 0.095, 1.969, -0.403,
1030 ];
1031
1032 const CASE_2: [f64; 50] = [
1034 -0.931, 0.481, 2.463, -0.246, -0.556, -1.171, -1.335, 0.525, 0.851, 0.009, 0.333, 0.116,
1035 0.139, -1.526, -0.458, 0.111, -0.783, -0.476, -0.819, -0.333, 0.853, -0.407, -0.154, 0.814,
1036 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,
1037 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,
1038 ];
1039
1040 const CASE_3: [f64; 60] = [
1042 9999999.337,
1043 10000001.046,
1044 9999999.356,
1045 9999999.039,
1046 9999999.29,
1047 9999998.81,
1048 10000000.146,
1049 10000001.031,
1050 10000000.164,
1051 10000000.624,
1052 10000001.632,
1053 10000000.27,
1054 10000000.195,
1055 9999999.725,
1056 9999998.392,
1057 10000000.76,
1058 9999998.244,
1059 10000000.653,
1060 9999999.986,
1061 10000001.127,
1062 9999999.932,
1063 9999999.177,
1064 10000000.358,
1065 9999999.44,
1066 9999999.819,
1067 10000000.042,
1068 9999999.865,
1069 9999999.811,
1070 9999999.168,
1071 9999999.811,
1072 10000001.862,
1073 10000003.843,
1074 10000002.802,
1075 10000005.12,
1076 10000005.27,
1077 10000002.049,
1078 10000004.145,
1079 10000003.874,
1080 10000002.953,
1081 10000004.532,
1082 10000003.538,
1083 10000002.232,
1084 10000003.733,
1085 10000003.852,
1086 10000004.106,
1087 10000002.769,
1088 10000004.616,
1089 10000004.735,
1090 10000002.854,
1091 10000003.341,
1092 10000003.92,
1093 10000003.434,
1094 10000005.744,
1095 10000004.208,
1096 10000002.989,
1097 10000003.212,
1098 10000003.943,
1099 10000006.296,
1100 10000003.822,
1101 10000004.127,
1102 ];
1103
1104 fn detect(cost: CostFunction, penalty: Penalty, min_len: usize, data: &[f64]) -> Vec<usize> {
1105 Pelt::with_min_segment_len(cost, penalty, min_len)
1106 .expect("valid settings")
1107 .detect(data)
1108 .changepoints
1109 }
1110
1111 const LARGE_LEVEL: [f64; 80] = [
1113 1699999999.198,
1114 1699999998.676,
1115 1699999999.752,
1116 1700000000.42,
1117 1700000001.136,
1118 1700000000.11,
1119 1699999999.447,
1120 1699999999.215,
1121 1700000000.749,
1122 1700000001.635,
1123 1700000000.273,
1124 1699999998.767,
1125 1699999999.042,
1126 1700000001.6,
1127 1700000000.203,
1128 1699999998.268,
1129 1699999999.916,
1130 1699999998.837,
1131 1699999999.371,
1132 1699999999.512,
1133 1699999999.287,
1134 1700000000.553,
1135 1699999999.937,
1136 1699999999.411,
1137 1700000000.41,
1138 1700000000.83,
1139 1699999998.357,
1140 1699999999.743,
1141 1699999999.019,
1142 1699999999.827,
1143 1699999998.711,
1144 1700000000.021,
1145 1699999999.962,
1146 1699999999.696,
1147 1699999998.952,
1148 1699999999.604,
1149 1699999998.909,
1150 1699999998.645,
1151 1700000000.225,
1152 1699999998.891,
1153 1700000006.17,
1154 1700000005.717,
1155 1700000003.002,
1156 1700000005.272,
1157 1700000003.898,
1158 1700000005.033,
1159 1700000005.044,
1160 1700000003.012,
1161 1700000004.767,
1162 1700000004.744,
1163 1700000005.962,
1164 1700000003.819,
1165 1700000005.738,
1166 1700000003.901,
1167 1700000004.669,
1168 1700000004.16,
1169 1700000006.449,
1170 1700000005.568,
1171 1700000007.432,
1172 1700000005.642,
1173 1700000005.845,
1174 1700000005.841,
1175 1700000004.393,
1176 1700000004.93,
1177 1700000006.35,
1178 1700000004.603,
1179 1700000005.189,
1180 1700000004.979,
1181 1700000005.609,
1182 1700000004.635,
1183 1700000004.848,
1184 1700000005.242,
1185 1700000005.103,
1186 1700000004.135,
1187 1700000005.896,
1188 1700000003.702,
1189 1700000003.799,
1190 1700000003.718,
1191 1700000005.967,
1192 1700000004.639,
1193 ];
1194
1195 #[test]
1196 fn a_large_level_does_not_move_the_optimum() {
1197 assert_eq!(
1200 detect(
1201 CostFunction::L2,
1202 Penalty::Custom((LARGE_LEVEL.len() as f64).ln()),
1203 2,
1204 &LARGE_LEVEL
1205 ),
1206 vec![40, 56, 62]
1207 );
1208 }
1209
1210 #[test]
1211 fn pruned_search_returns_the_exact_optimum() {
1212 assert_eq!(
1214 detect(
1215 CostFunction::L2,
1216 Penalty::Custom((CASE_0.len() as f64).ln()),
1217 2,
1218 &CASE_0
1219 ),
1220 vec![20, 45]
1221 );
1222 assert_eq!(
1224 detect(CostFunction::Normal, Penalty::Bic, 5, &CASE_1),
1225 vec![41]
1226 );
1227 assert_eq!(
1229 detect(CostFunction::L2, Penalty::Custom(5.0), 5, &CASE_2),
1230 vec![25]
1231 );
1232 assert_eq!(
1234 detect(
1235 CostFunction::L2,
1236 Penalty::Custom((CASE_3.len() as f64).ln()),
1237 2,
1238 &CASE_3
1239 ),
1240 vec![31]
1241 );
1242 }
1243}
1244
1245#[cfg(test)]
1251mod exact_dp_reference_multi {
1252 use super::*;
1253
1254 fn detect_multi(
1255 cost: CostFunction,
1256 penalty: Penalty,
1257 min_len: usize,
1258 channels: &[&[f64]],
1259 ) -> Vec<usize> {
1260 Pelt::with_min_segment_len(cost, penalty, min_len)
1261 .expect("valid settings")
1262 .detect_multi(channels)
1263 .expect("equal lengths")
1264 .changepoints
1265 }
1266
1267 const A_0: [f64; 60] = [
1268 -0.804, 0.801, -0.66, -0.813, 0.703, 1.326, -0.448, 2.263, -1.25, -2.127, 1.654, -0.384,
1269 0.038, -0.988, 1.559, -1.068, -0.242, -1.431, -1.253, -0.734, -0.485, 3.102, 2.171, -0.177,
1270 -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,
1271 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,
1272 1.201, 3.216, 1.34, 2.219, 3.623, 3.182, 5.136, 2.97, 1.461, 1.545,
1273 ];
1274
1275 const A_1: [f64; 60] = [
1276 1.04, -1.488, 0.963, -0.538, 1.736, 0.943, -0.191, 1.916, 0.606, -1.098, -0.764, 0.437,
1277 -0.276, 0.696, -1.029, -0.504, -0.287, -0.619, 0.587, -0.272, -1.753, 0.125, -2.015,
1278 -0.146, 1.821, 1.502, 1.776, -0.202, 1.056, 3.046, 2.47, 0.695, 1.365, 1.453, 0.705,
1279 -0.373, 1.967, 0.828, 1.686, -0.625, 0.406, 2.587, 1.235, -1.549, 1.573, -0.702, 2.101,
1280 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,
1281 ];
1282
1283 const B_0: [f64; 60] = [
1284 0.549, -0.705, -0.681, -0.052, 0.206, -0.084, -0.413, -0.562, -0.393, 0.281, 0.6, -1.232,
1285 -0.127, 0.072, 0.255, -0.094, -0.273, 0.606, -0.597, 0.615, 0.599, -0.193, 0.08, 1.066,
1286 -0.624, -0.284, 0.675, -0.924, -0.54, -0.564, -1.563, 2.435, -3.255, 0.942, 0.323, 1.455,
1287 -5.14, 1.305, -3.933, -0.982, 1.378, 2.145, -4.895, 0.009, -1.539, 3.119, -0.391, -2.202,
1288 5.382, 3.363, 2.322, -0.958, 1.278, -1.489, -0.671, -3.143, -0.972, 4.385, 1.683, -1.434,
1289 ];
1290
1291 const B_1: [f64; 60] = [
1292 -0.394, -0.653, 0.721, -0.457, -0.009, 0.161, -0.763, 0.533, -0.474, -0.655, -0.423, 0.734,
1293 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,
1294 -0.673, 0.033, 0.267, -0.507, 0.303, 5.49, -4.019, 0.665, 4.472, -0.633, -5.961, 2.979,
1295 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,
1296 0.382, 0.434, -2.124, 3.613, 2.052, 2.66, 2.733, 5.311, 0.163, 5.14,
1297 ];
1298
1299 const B_2: [f64; 60] = [
1300 0.13, -0.45, -0.281, 0.099, -0.849, -0.968, -0.102, 0.034, -0.244, -0.855, -1.248, 0.123,
1301 -0.757, -0.162, 0.304, -0.05, -0.153, 0.633, 0.197, -0.539, 0.398, -0.124, 0.021, 0.168,
1302 -0.909, -0.353, -0.161, -0.339, -0.212, 0.003, 0.043, -0.051, -0.087, -0.065, -0.55,
1303 -0.066, 0.756, 0.312, -0.21, -0.042, -0.518, -0.595, -0.717, -0.091, 0.24, -0.416, -0.179,
1304 0.611, -0.219, -0.528, -0.603, -1.164, -0.22, 0.875, -0.005, 1.255, -0.619, -0.851, 0.107,
1305 0.101,
1306 ];
1307
1308 const C_0: [f64; 60] = [
1309 0.954, 0.369, 0.316, 0.089, -0.008, -0.682, 0.736, -2.132, 2.099, -1.086, -0.896, -0.925,
1310 -0.576, -1.991, -0.338, 0.524, -0.182, 0.678, -0.767, 0.299, 1.874, 3.856, 2.051, 3.948,
1311 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,
1312 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,
1313 0.123, -0.145, -0.717, 0.334, 0.933, -1.102, 0.313, -2.129, 0.467, 0.762,
1314 ];
1315
1316 const C_1: [f64; 60] = [
1317 0.443, -0.016, 0.496, -0.191, 0.405, 0.124, 0.133, -0.853, -0.11, -1.616, -0.619, -0.114,
1318 0.05, 1.525, -1.539, 0.252, 0.402, 1.997, -0.821, 1.855, -3.542, -1.497, -2.465, 0.122,
1319 -3.354, -3.124, -2.577, -2.521, -1.298, -2.091, 0.046, -2.452, -1.855, -2.804, -2.243,
1320 -2.454, -1.28, -0.159, -1.652, -1.664, 0.177, -0.067, 0.107, -0.184, -0.051, 0.917, 1.159,
1321 1.173, 1.039, 0.66, 0.071, -0.928, 0.8, -0.388, 1.233, -0.091, 0.962, 0.423, -1.042,
1322 -0.113,
1323 ];
1324
1325 const D_0: [f64; 60] = [
1326 999998.994,
1327 999998.457,
1328 999997.649,
1329 999999.049,
1330 1000000.222,
1331 1000000.29,
1332 999999.019,
1333 1000000.062,
1334 1000000.761,
1335 999998.951,
1336 999999.22,
1337 999999.29,
1338 999999.563,
1339 1000000.661,
1340 1000000.006,
1341 999999.314,
1342 1000000.983,
1343 999998.466,
1344 1000000.97,
1345 1000002.599,
1346 999999.604,
1347 999998.518,
1348 1000001.488,
1349 1000001.909,
1350 999999.945,
1351 999998.831,
1352 999999.774,
1353 999999.966,
1354 999998.892,
1355 999998.914,
1356 1000003.175,
1357 1000004.168,
1358 1000004.646,
1359 1000003.017,
1360 1000001.941,
1361 1000004.399,
1362 1000003.577,
1363 1000002.784,
1364 1000003.348,
1365 1000002.429,
1366 1000002.519,
1367 1000001.615,
1368 1000002.515,
1369 1000001.867,
1370 1000003.272,
1371 1000004.196,
1372 1000000.7,
1373 1000003.487,
1374 1000003.406,
1375 1000002.157,
1376 1000003.654,
1377 1000002.441,
1378 1000002.837,
1379 1000003.022,
1380 1000003.252,
1381 1000003.838,
1382 1000003.735,
1383 1000002.762,
1384 1000003.664,
1385 1000003.312,
1386 ];
1387
1388 const D_1: [f64; 60] = [
1389 0.323, 1.153, -0.737, -0.765, -0.387, -0.132, -0.38, -0.017, 0.039, -1.855, 1.465, 2.111,
1390 -1.086, 0.568, 0.859, 1.543, 1.12, 0.916, -0.591, -1.247, -0.131, -0.231, -0.639, -0.829,
1391 0.322, -0.727, -1.789, -0.935, -0.092, 0.186, 1.729, 3.461, 2.688, 1.473, 3.507, 1.722,
1392 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,
1393 0.915, 2.022, 2.275, 1.484, 2.071, 3.911, -0.234, 2.667, 1.866, 2.315, 2.401,
1394 ];
1395
1396 const D_2: [f64; 60] = [
1397 -50.107, -50.532, -50.842, -48.33, -49.442, -49.761, -51.681, -52.824, -50.637, -51.821,
1398 -53.9, -49.553, -48.224, -49.598, -48.92, -51.311, -48.414, -46.838, -47.13, -49.746,
1399 -47.412, -51.215, -47.565, -50.883, -50.071, -53.527, -49.043, -48.191, -51.551, -49.062,
1400 -48.796, -49.414, -46.466, -48.3, -49.039, -48.988, -48.978, -48.995, -52.944, -51.869,
1401 -51.183, -47.484, -49.5, -48.72, -48.391, -50.368, -47.708, -47.899, -50.359, -50.786,
1402 -49.405, -49.132, -46.105, -56.126, -49.201, -49.857, -51.277, -51.729, -50.852, -49.224,
1403 ];
1404
1405 #[test]
1406 fn the_pruned_multi_channel_search_returns_the_exact_optimum() {
1407 assert_eq!(
1408 detect_multi(
1409 CostFunction::L2,
1410 Penalty::Custom((A_0.len() as f64).ln()),
1411 2,
1412 &[&A_0, &A_1]
1413 ),
1414 vec![21, 23, 26]
1415 );
1416 assert_eq!(
1417 detect_multi(CostFunction::Normal, Penalty::Bic, 5, &[&B_0, &B_1, &B_2]),
1418 vec![30]
1419 );
1420 assert_eq!(
1421 detect_multi(CostFunction::L2, Penalty::Custom(8.0), 5, &[&C_0, &C_1]),
1422 vec![20, 40]
1423 );
1424 assert_eq!(
1425 detect_multi(
1426 CostFunction::L2,
1427 Penalty::Custom((D_0.len() as f64).ln()),
1428 2,
1429 &[&D_0, &D_1, &D_2]
1430 ),
1431 vec![6, 11, 21, 30, 38, 41, 53]
1432 );
1433 }
1434}