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