1use crate::basis::fourier_basis_with_period;
13use crate::fdata::deriv_1d;
14use num_complex::Complex;
15use rayon::prelude::*;
16use rustfft::FftPlanner;
17use std::f64::consts::PI;
18
19#[derive(Debug, Clone)]
21pub struct PeriodEstimate {
22 pub period: f64,
24 pub frequency: f64,
26 pub power: f64,
28 pub confidence: f64,
30}
31
32#[derive(Debug, Clone)]
34pub struct Peak {
35 pub time: f64,
37 pub value: f64,
39 pub prominence: f64,
41}
42
43#[derive(Debug, Clone)]
45pub struct PeakDetectionResult {
46 pub peaks: Vec<Vec<Peak>>,
48 pub inter_peak_distances: Vec<Vec<f64>>,
50 pub mean_period: f64,
52}
53
54#[derive(Debug, Clone)]
56pub struct DetectedPeriod {
57 pub period: f64,
59 pub confidence: f64,
61 pub strength: f64,
63 pub amplitude: f64,
65 pub phase: f64,
67 pub iteration: usize,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum StrengthMethod {
74 Variance,
76 Spectral,
78}
79
80#[derive(Debug, Clone)]
82pub struct ChangePoint {
83 pub time: f64,
85 pub change_type: ChangeType,
87 pub strength_before: f64,
89 pub strength_after: f64,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum ChangeType {
96 Onset,
98 Cessation,
100}
101
102#[derive(Debug, Clone)]
104pub struct ChangeDetectionResult {
105 pub change_points: Vec<ChangePoint>,
107 pub strength_curve: Vec<f64>,
109}
110
111#[derive(Debug, Clone)]
113pub struct InstantaneousPeriod {
114 pub period: Vec<f64>,
116 pub frequency: Vec<f64>,
118 pub amplitude: Vec<f64>,
120}
121
122#[derive(Debug, Clone)]
124pub struct PeakTimingResult {
125 pub peak_times: Vec<f64>,
127 pub peak_values: Vec<f64>,
129 pub normalized_timing: Vec<f64>,
131 pub mean_timing: f64,
133 pub std_timing: f64,
135 pub range_timing: f64,
137 pub variability_score: f64,
139 pub timing_trend: f64,
141 pub cycle_indices: Vec<usize>,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum SeasonalType {
148 StableSeasonal,
150 VariableTiming,
152 IntermittentSeasonal,
154 NonSeasonal,
156}
157
158#[derive(Debug, Clone)]
160pub struct SeasonalityClassification {
161 pub is_seasonal: bool,
163 pub has_stable_timing: bool,
165 pub timing_variability: f64,
167 pub seasonal_strength: f64,
169 pub cycle_strengths: Vec<f64>,
171 pub weak_seasons: Vec<usize>,
173 pub classification: SeasonalType,
175 pub peak_timing: Option<PeakTimingResult>,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq)]
181pub enum ThresholdMethod {
182 Fixed(f64),
184 Percentile(f64),
186 Otsu,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum ModulationType {
193 Stable,
195 Emerging,
197 Fading,
199 Oscillating,
201 NonSeasonal,
203}
204
205#[derive(Debug, Clone)]
207pub struct AmplitudeModulationResult {
208 pub is_seasonal: bool,
210 pub seasonal_strength: f64,
212 pub has_modulation: bool,
214 pub modulation_type: ModulationType,
216 pub modulation_score: f64,
218 pub amplitude_trend: f64,
220 pub strength_curve: Vec<f64>,
222 pub time_points: Vec<f64>,
224 pub min_strength: f64,
226 pub max_strength: f64,
228}
229
230#[derive(Debug, Clone)]
232pub struct WaveletAmplitudeResult {
233 pub is_seasonal: bool,
235 pub seasonal_strength: f64,
237 pub has_modulation: bool,
239 pub modulation_type: ModulationType,
241 pub modulation_score: f64,
243 pub amplitude_trend: f64,
245 pub wavelet_amplitude: Vec<f64>,
247 pub time_points: Vec<f64>,
249 pub scale: f64,
251}
252
253#[inline]
268fn compute_mean_curve_impl(data: &[f64], n: usize, m: usize, parallel: bool) -> Vec<f64> {
269 if parallel && m >= 100 {
270 (0..m)
272 .into_par_iter()
273 .map(|j| {
274 let mut sum = 0.0;
275 for i in 0..n {
276 sum += data[i + j * n];
277 }
278 sum / n as f64
279 })
280 .collect()
281 } else {
282 (0..m)
284 .map(|j| {
285 let mut sum = 0.0;
286 for i in 0..n {
287 sum += data[i + j * n];
288 }
289 sum / n as f64
290 })
291 .collect()
292 }
293}
294
295#[inline]
297fn compute_mean_curve(data: &[f64], n: usize, m: usize) -> Vec<f64> {
298 compute_mean_curve_impl(data, n, m, true)
299}
300
301#[inline]
311fn interior_bounds(m: usize) -> Option<(usize, usize)> {
312 let edge_skip = (m as f64 * 0.1) as usize;
313 let interior_start = edge_skip.min(m / 4);
314 let interior_end = m.saturating_sub(edge_skip).max(m * 3 / 4);
315
316 if interior_end <= interior_start {
317 None
318 } else {
319 Some((interior_start, interior_end))
320 }
321}
322
323fn periodogram(data: &[f64], argvals: &[f64]) -> (Vec<f64>, Vec<f64>) {
326 let n = data.len();
327 if n < 2 || argvals.len() != n {
328 return (Vec::new(), Vec::new());
329 }
330
331 let dt = (argvals[n - 1] - argvals[0]) / (n - 1) as f64;
332 let fs = 1.0 / dt; let mut planner = FftPlanner::<f64>::new();
335 let fft = planner.plan_fft_forward(n);
336
337 let mut buffer: Vec<Complex<f64>> = data.iter().map(|&x| Complex::new(x, 0.0)).collect();
338 fft.process(&mut buffer);
339
340 let n_freq = n / 2 + 1;
342 let mut power = Vec::with_capacity(n_freq);
343 let mut frequencies = Vec::with_capacity(n_freq);
344
345 for k in 0..n_freq {
346 let freq = k as f64 * fs / n as f64;
347 frequencies.push(freq);
348
349 let p = buffer[k].norm_sqr() / (n as f64 * n as f64);
350 let p = if k > 0 && k < n / 2 { 2.0 * p } else { p };
352 power.push(p);
353 }
354
355 (frequencies, power)
356}
357
358fn autocorrelation(data: &[f64], max_lag: usize) -> Vec<f64> {
360 let n = data.len();
361 if n == 0 {
362 return Vec::new();
363 }
364
365 let mean: f64 = data.iter().sum::<f64>() / n as f64;
366 let var: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
367
368 if var < 1e-15 {
369 return vec![1.0; max_lag.min(n) + 1];
370 }
371
372 let max_lag = max_lag.min(n - 1);
373 let mut acf = Vec::with_capacity(max_lag + 1);
374
375 for lag in 0..=max_lag {
376 let mut sum = 0.0;
377 for i in 0..(n - lag) {
378 sum += (data[i] - mean) * (data[i + lag] - mean);
379 }
380 acf.push(sum / (n as f64 * var));
381 }
382
383 acf
384}
385
386fn find_peaks_1d(signal: &[f64], min_distance: usize) -> Vec<usize> {
388 let n = signal.len();
389 if n < 3 {
390 return Vec::new();
391 }
392
393 let mut peaks = Vec::new();
394
395 for i in 1..(n - 1) {
396 if signal[i] > signal[i - 1] && signal[i] > signal[i + 1] {
397 if peaks.is_empty() || i - peaks[peaks.len() - 1] >= min_distance {
399 peaks.push(i);
400 } else if signal[i] > signal[peaks[peaks.len() - 1]] {
401 peaks.pop();
403 peaks.push(i);
404 }
405 }
406 }
407
408 peaks
409}
410
411fn compute_prominence(signal: &[f64], peak_idx: usize) -> f64 {
413 let n = signal.len();
414 let peak_val = signal[peak_idx];
415
416 let mut left_min = peak_val;
418 for i in (0..peak_idx).rev() {
419 if signal[i] >= peak_val {
420 break;
421 }
422 left_min = left_min.min(signal[i]);
423 }
424
425 let mut right_min = peak_val;
426 for i in (peak_idx + 1)..n {
427 if signal[i] >= peak_val {
428 break;
429 }
430 right_min = right_min.min(signal[i]);
431 }
432
433 peak_val - left_min.max(right_min)
434}
435
436pub fn hilbert_transform(signal: &[f64]) -> Vec<Complex<f64>> {
444 let n = signal.len();
445 if n == 0 {
446 return Vec::new();
447 }
448
449 let mut planner = FftPlanner::<f64>::new();
450 let fft_forward = planner.plan_fft_forward(n);
451 let fft_inverse = planner.plan_fft_inverse(n);
452
453 let mut buffer: Vec<Complex<f64>> = signal.iter().map(|&x| Complex::new(x, 0.0)).collect();
455 fft_forward.process(&mut buffer);
456
457 let half = n / 2;
460 for k in 1..half {
461 buffer[k] *= 2.0;
462 }
463 for k in (half + 1)..n {
464 buffer[k] = Complex::new(0.0, 0.0);
465 }
466
467 fft_inverse.process(&mut buffer);
469
470 for c in buffer.iter_mut() {
472 *c /= n as f64;
473 }
474
475 buffer
476}
477
478fn unwrap_phase(phase: &[f64]) -> Vec<f64> {
480 if phase.is_empty() {
481 return Vec::new();
482 }
483
484 let mut unwrapped = vec![phase[0]];
485 let mut cumulative_correction = 0.0;
486
487 for i in 1..phase.len() {
488 let diff = phase[i] - phase[i - 1];
489
490 if diff > PI {
492 cumulative_correction -= 2.0 * PI;
493 } else if diff < -PI {
494 cumulative_correction += 2.0 * PI;
495 }
496
497 unwrapped.push(phase[i] + cumulative_correction);
498 }
499
500 unwrapped
501}
502
503fn morlet_wavelet(t: f64, omega0: f64) -> Complex<f64> {
510 let gaussian = (-t * t / 2.0).exp();
511 let oscillation = Complex::new((omega0 * t).cos(), (omega0 * t).sin());
512 oscillation * gaussian
513}
514
515#[allow(dead_code)]
529fn cwt_morlet(signal: &[f64], argvals: &[f64], scale: f64, omega0: f64) -> Vec<Complex<f64>> {
530 let n = signal.len();
531 if n == 0 || scale <= 0.0 {
532 return Vec::new();
533 }
534
535 let dt = (argvals[n - 1] - argvals[0]) / (n - 1) as f64;
536
537 let norm = 1.0 / scale.sqrt();
540
541 (0..n)
542 .map(|b| {
543 let mut sum = Complex::new(0.0, 0.0);
544 for k in 0..n {
545 let t_normalized = (argvals[k] - argvals[b]) / scale;
546 if t_normalized.abs() < 6.0 {
548 let wavelet = morlet_wavelet(t_normalized, omega0);
549 sum += signal[k] * wavelet.conj();
550 }
551 }
552 sum * norm * dt
553 })
554 .collect()
555}
556
557fn cwt_morlet_fft(signal: &[f64], argvals: &[f64], scale: f64, omega0: f64) -> Vec<Complex<f64>> {
561 let n = signal.len();
562 if n == 0 || scale <= 0.0 {
563 return Vec::new();
564 }
565
566 let dt = (argvals[n - 1] - argvals[0]) / (n - 1) as f64;
567 let norm = 1.0 / scale.sqrt();
568
569 let wavelet_time: Vec<Complex<f64>> = (0..n)
571 .map(|k| {
572 let t = if k <= n / 2 {
574 k as f64 * dt / scale
575 } else {
576 (k as f64 - n as f64) * dt / scale
577 };
578 morlet_wavelet(t, omega0) * norm
579 })
580 .collect();
581
582 let mut planner = FftPlanner::<f64>::new();
583 let fft_forward = planner.plan_fft_forward(n);
584 let fft_inverse = planner.plan_fft_inverse(n);
585
586 let mut signal_fft: Vec<Complex<f64>> = signal.iter().map(|&x| Complex::new(x, 0.0)).collect();
588 fft_forward.process(&mut signal_fft);
589
590 let mut wavelet_fft = wavelet_time;
592 fft_forward.process(&mut wavelet_fft);
593
594 let mut result: Vec<Complex<f64>> = signal_fft
596 .iter()
597 .zip(wavelet_fft.iter())
598 .map(|(s, w)| *s * w.conj())
599 .collect();
600
601 fft_inverse.process(&mut result);
603
604 for c in result.iter_mut() {
606 *c *= dt / n as f64;
607 }
608
609 result
610}
611
612fn otsu_threshold(values: &[f64]) -> f64 {
617 if values.is_empty() {
618 return 0.5;
619 }
620
621 let valid: Vec<f64> = values.iter().copied().filter(|x| x.is_finite()).collect();
623 if valid.is_empty() {
624 return 0.5;
625 }
626
627 let min_val = valid.iter().cloned().fold(f64::INFINITY, f64::min);
628 let max_val = valid.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
629
630 if (max_val - min_val).abs() < 1e-10 {
631 return (min_val + max_val) / 2.0;
632 }
633
634 let n_bins = 256;
636 let bin_width = (max_val - min_val) / n_bins as f64;
637 let mut histogram = vec![0usize; n_bins];
638
639 for &v in &valid {
640 let bin = ((v - min_val) / bin_width).min(n_bins as f64 - 1.0) as usize;
641 histogram[bin] += 1;
642 }
643
644 let total = valid.len() as f64;
645 let mut sum_total = 0.0;
646 for (i, &count) in histogram.iter().enumerate() {
647 sum_total += i as f64 * count as f64;
648 }
649
650 let mut best_threshold = min_val;
651 let mut best_variance = 0.0;
652
653 let mut sum_b = 0.0;
654 let mut weight_b = 0.0;
655
656 for t in 0..n_bins {
657 weight_b += histogram[t] as f64;
658 if weight_b == 0.0 {
659 continue;
660 }
661
662 let weight_f = total - weight_b;
663 if weight_f == 0.0 {
664 break;
665 }
666
667 sum_b += t as f64 * histogram[t] as f64;
668
669 let mean_b = sum_b / weight_b;
670 let mean_f = (sum_total - sum_b) / weight_f;
671
672 let variance = weight_b * weight_f * (mean_b - mean_f).powi(2);
674
675 if variance > best_variance {
676 best_variance = variance;
677 best_threshold = min_val + (t as f64 + 0.5) * bin_width;
678 }
679 }
680
681 best_threshold
682}
683
684fn linear_slope(x: &[f64], y: &[f64]) -> f64 {
686 if x.len() != y.len() || x.len() < 2 {
687 return 0.0;
688 }
689
690 let n = x.len() as f64;
691 let mean_x: f64 = x.iter().sum::<f64>() / n;
692 let mean_y: f64 = y.iter().sum::<f64>() / n;
693
694 let mut num = 0.0;
695 let mut den = 0.0;
696
697 for (&xi, &yi) in x.iter().zip(y.iter()) {
698 num += (xi - mean_x) * (yi - mean_y);
699 den += (xi - mean_x).powi(2);
700 }
701
702 if den.abs() < 1e-15 {
703 0.0
704 } else {
705 num / den
706 }
707}
708
709pub fn estimate_period_fft(data: &[f64], n: usize, m: usize, argvals: &[f64]) -> PeriodEstimate {
727 if n == 0 || m < 4 || argvals.len() != m {
728 return PeriodEstimate {
729 period: f64::NAN,
730 frequency: f64::NAN,
731 power: 0.0,
732 confidence: 0.0,
733 };
734 }
735
736 let mean_curve = compute_mean_curve(data, n, m);
738
739 let (frequencies, power) = periodogram(&mean_curve, argvals);
740
741 if frequencies.len() < 2 {
742 return PeriodEstimate {
743 period: f64::NAN,
744 frequency: f64::NAN,
745 power: 0.0,
746 confidence: 0.0,
747 };
748 }
749
750 let mut max_power = 0.0;
752 let mut max_idx = 1;
753 for (i, &p) in power.iter().enumerate().skip(1) {
754 if p > max_power {
755 max_power = p;
756 max_idx = i;
757 }
758 }
759
760 let dominant_freq = frequencies[max_idx];
761 let period = if dominant_freq > 1e-15 {
762 1.0 / dominant_freq
763 } else {
764 f64::INFINITY
765 };
766
767 let mean_power: f64 = power.iter().skip(1).sum::<f64>() / (power.len() - 1) as f64;
769 let confidence = if mean_power > 1e-15 {
770 max_power / mean_power
771 } else {
772 0.0
773 };
774
775 PeriodEstimate {
776 period,
777 frequency: dominant_freq,
778 power: max_power,
779 confidence,
780 }
781}
782
783pub fn estimate_period_acf(
794 data: &[f64],
795 n: usize,
796 m: usize,
797 argvals: &[f64],
798 max_lag: usize,
799) -> PeriodEstimate {
800 if n == 0 || m < 4 || argvals.len() != m {
801 return PeriodEstimate {
802 period: f64::NAN,
803 frequency: f64::NAN,
804 power: 0.0,
805 confidence: 0.0,
806 };
807 }
808
809 let mean_curve = compute_mean_curve(data, n, m);
811
812 let acf = autocorrelation(&mean_curve, max_lag);
813
814 let min_lag = 2;
816 let peaks = find_peaks_1d(&acf[min_lag..], 1);
817
818 if peaks.is_empty() {
819 return PeriodEstimate {
820 period: f64::NAN,
821 frequency: f64::NAN,
822 power: 0.0,
823 confidence: 0.0,
824 };
825 }
826
827 let peak_lag = peaks[0] + min_lag;
828 let dt = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
829 let period = peak_lag as f64 * dt;
830 let frequency = if period > 1e-15 { 1.0 / period } else { 0.0 };
831
832 PeriodEstimate {
833 period,
834 frequency,
835 power: acf[peak_lag],
836 confidence: acf[peak_lag].abs(),
837 }
838}
839
840pub fn estimate_period_regression(
855 data: &[f64],
856 n: usize,
857 m: usize,
858 argvals: &[f64],
859 period_min: f64,
860 period_max: f64,
861 n_candidates: usize,
862 n_harmonics: usize,
863) -> PeriodEstimate {
864 if n == 0 || m < 4 || argvals.len() != m || period_min >= period_max || n_candidates < 2 {
865 return PeriodEstimate {
866 period: f64::NAN,
867 frequency: f64::NAN,
868 power: 0.0,
869 confidence: 0.0,
870 };
871 }
872
873 let mean_curve = compute_mean_curve(data, n, m);
875
876 let nbasis = 1 + 2 * n_harmonics;
877
878 let candidates: Vec<f64> = (0..n_candidates)
880 .map(|i| period_min + (period_max - period_min) * i as f64 / (n_candidates - 1) as f64)
881 .collect();
882
883 let results: Vec<(f64, f64)> = candidates
884 .par_iter()
885 .map(|&period| {
886 let basis = fourier_basis_with_period(argvals, nbasis, period);
887
888 let mut rss = 0.0;
890 for j in 0..m {
891 let mut fitted = 0.0;
892 for k in 0..nbasis {
894 let b_val = basis[j + k * m];
895 let coef: f64 = (0..m)
896 .map(|l| mean_curve[l] * basis[l + k * m])
897 .sum::<f64>()
898 / (0..m)
899 .map(|l| basis[l + k * m].powi(2))
900 .sum::<f64>()
901 .max(1e-15);
902 fitted += coef * b_val;
903 }
904 let resid = mean_curve[j] - fitted;
905 rss += resid * resid;
906 }
907
908 (period, rss)
909 })
910 .collect();
911
912 let (best_period, min_rss) = results
914 .iter()
915 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
916 .cloned()
917 .unwrap_or((f64::NAN, f64::INFINITY));
918
919 let mean_rss: f64 = results.iter().map(|(_, r)| r).sum::<f64>() / results.len() as f64;
921 let confidence = if min_rss > 1e-15 {
922 (mean_rss / min_rss).min(10.0)
923 } else {
924 10.0
925 };
926
927 PeriodEstimate {
928 period: best_period,
929 frequency: if best_period > 1e-15 {
930 1.0 / best_period
931 } else {
932 0.0
933 },
934 power: 1.0 - min_rss / mean_rss,
935 confidence,
936 }
937}
938
939pub fn detect_multiple_periods(
960 data: &[f64],
961 n: usize,
962 m: usize,
963 argvals: &[f64],
964 max_periods: usize,
965 min_confidence: f64,
966 min_strength: f64,
967) -> Vec<DetectedPeriod> {
968 if n == 0 || m < 4 || argvals.len() != m || max_periods == 0 {
969 return Vec::new();
970 }
971
972 let mean_curve = compute_mean_curve(data, n, m);
974
975 let mut residual = mean_curve.clone();
976 let mut detected = Vec::with_capacity(max_periods);
977
978 for iteration in 1..=max_periods {
979 let residual_data: Vec<f64> = residual.clone();
981
982 let est = estimate_period_fft(&residual_data, 1, m, argvals);
984
985 if est.confidence < min_confidence || est.period.is_nan() || est.period.is_infinite() {
986 break;
987 }
988
989 let strength = seasonal_strength_variance(&residual_data, 1, m, argvals, est.period, 3);
991
992 if strength < min_strength || strength.is_nan() {
993 break;
994 }
995
996 let omega = 2.0 * PI / est.period;
998 let mut cos_sum = 0.0;
999 let mut sin_sum = 0.0;
1000
1001 for (j, &t) in argvals.iter().enumerate() {
1002 cos_sum += residual[j] * (omega * t).cos();
1003 sin_sum += residual[j] * (omega * t).sin();
1004 }
1005
1006 let a = 2.0 * cos_sum / m as f64; let b = 2.0 * sin_sum / m as f64; let amplitude = (a * a + b * b).sqrt();
1009 let phase = b.atan2(a);
1010
1011 detected.push(DetectedPeriod {
1012 period: est.period,
1013 confidence: est.confidence,
1014 strength,
1015 amplitude,
1016 phase,
1017 iteration,
1018 });
1019
1020 for (j, &t) in argvals.iter().enumerate() {
1022 let fitted = a * (omega * t).cos() + b * (omega * t).sin();
1023 residual[j] -= fitted;
1024 }
1025 }
1026
1027 detected
1028}
1029
1030pub fn detect_peaks(
1050 data: &[f64],
1051 n: usize,
1052 m: usize,
1053 argvals: &[f64],
1054 min_distance: Option<f64>,
1055 min_prominence: Option<f64>,
1056 smooth_first: bool,
1057 smooth_nbasis: Option<usize>,
1058) -> PeakDetectionResult {
1059 if n == 0 || m < 3 || argvals.len() != m {
1060 return PeakDetectionResult {
1061 peaks: Vec::new(),
1062 inter_peak_distances: Vec::new(),
1063 mean_period: f64::NAN,
1064 };
1065 }
1066
1067 let dt = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
1068 let min_dist_points = min_distance.map(|d| (d / dt).round() as usize).unwrap_or(1);
1069
1070 let work_data = if smooth_first {
1072 let nbasis = smooth_nbasis
1074 .unwrap_or_else(|| crate::basis::select_fourier_nbasis_gcv(data, n, m, argvals, 5, 25));
1075
1076 if let Some(result) = crate::basis::fourier_fit_1d(data, n, m, argvals, nbasis) {
1078 result.fitted
1079 } else {
1080 data.to_vec()
1081 }
1082 } else {
1083 data.to_vec()
1084 };
1085
1086 let deriv1 = deriv_1d(&work_data, n, m, argvals, 1);
1088
1089 let data_range: f64 = {
1091 let mut min_val = f64::INFINITY;
1092 let mut max_val = f64::NEG_INFINITY;
1093 for &v in work_data.iter() {
1094 min_val = min_val.min(v);
1095 max_val = max_val.max(v);
1096 }
1097 (max_val - min_val).max(1e-15)
1098 };
1099
1100 let results: Vec<(Vec<Peak>, Vec<f64>)> = (0..n)
1102 .into_par_iter()
1103 .map(|i| {
1104 let curve: Vec<f64> = (0..m).map(|j| work_data[i + j * n]).collect();
1106 let d1: Vec<f64> = (0..m).map(|j| deriv1[i + j * n]).collect();
1107
1108 let mut peak_indices = Vec::new();
1110 for j in 1..m {
1111 if d1[j - 1] > 0.0 && d1[j] <= 0.0 {
1112 let idx = if (d1[j - 1] - d1[j]).abs() > 1e-15 {
1114 j - 1
1115 } else {
1116 j
1117 };
1118
1119 if peak_indices.is_empty()
1121 || idx - peak_indices[peak_indices.len() - 1] >= min_dist_points
1122 {
1123 peak_indices.push(idx);
1124 }
1125 }
1126 }
1127
1128 let mut peaks: Vec<Peak> = peak_indices
1130 .iter()
1131 .map(|&idx| {
1132 let prominence = compute_prominence(&curve, idx) / data_range;
1133 Peak {
1134 time: argvals[idx],
1135 value: curve[idx],
1136 prominence,
1137 }
1138 })
1139 .collect();
1140
1141 if let Some(min_prom) = min_prominence {
1143 peaks.retain(|p| p.prominence >= min_prom);
1144 }
1145
1146 let distances: Vec<f64> = peaks.windows(2).map(|w| w[1].time - w[0].time).collect();
1148
1149 (peaks, distances)
1150 })
1151 .collect();
1152
1153 let peaks: Vec<Vec<Peak>> = results.iter().map(|(p, _)| p.clone()).collect();
1154 let inter_peak_distances: Vec<Vec<f64>> = results.iter().map(|(_, d)| d.clone()).collect();
1155
1156 let all_distances: Vec<f64> = inter_peak_distances.iter().flatten().cloned().collect();
1158 let mean_period = if all_distances.is_empty() {
1159 f64::NAN
1160 } else {
1161 all_distances.iter().sum::<f64>() / all_distances.len() as f64
1162 };
1163
1164 PeakDetectionResult {
1165 peaks,
1166 inter_peak_distances,
1167 mean_period,
1168 }
1169}
1170
1171pub fn seasonal_strength_variance(
1191 data: &[f64],
1192 n: usize,
1193 m: usize,
1194 argvals: &[f64],
1195 period: f64,
1196 n_harmonics: usize,
1197) -> f64 {
1198 if n == 0 || m < 4 || argvals.len() != m || period <= 0.0 {
1199 return f64::NAN;
1200 }
1201
1202 let mean_curve = compute_mean_curve(data, n, m);
1204
1205 let global_mean: f64 = mean_curve.iter().sum::<f64>() / m as f64;
1207 let total_var: f64 = mean_curve
1208 .iter()
1209 .map(|&x| (x - global_mean).powi(2))
1210 .sum::<f64>()
1211 / m as f64;
1212
1213 if total_var < 1e-15 {
1214 return 0.0;
1215 }
1216
1217 let nbasis = 1 + 2 * n_harmonics;
1219 let basis = fourier_basis_with_period(argvals, nbasis, period);
1220
1221 let mut seasonal = vec![0.0; m];
1223 for k in 1..nbasis {
1224 let b_sum: f64 = (0..m).map(|j| basis[j + k * m].powi(2)).sum();
1226 if b_sum > 1e-15 {
1227 let coef: f64 = (0..m)
1228 .map(|j| mean_curve[j] * basis[j + k * m])
1229 .sum::<f64>()
1230 / b_sum;
1231 for j in 0..m {
1232 seasonal[j] += coef * basis[j + k * m];
1233 }
1234 }
1235 }
1236
1237 let seasonal_mean: f64 = seasonal.iter().sum::<f64>() / m as f64;
1239 let seasonal_var: f64 = seasonal
1240 .iter()
1241 .map(|&x| (x - seasonal_mean).powi(2))
1242 .sum::<f64>()
1243 / m as f64;
1244
1245 (seasonal_var / total_var).min(1.0)
1246}
1247
1248pub fn seasonal_strength_spectral(
1259 data: &[f64],
1260 n: usize,
1261 m: usize,
1262 argvals: &[f64],
1263 period: f64,
1264) -> f64 {
1265 if n == 0 || m < 4 || argvals.len() != m || period <= 0.0 {
1266 return f64::NAN;
1267 }
1268
1269 let mean_curve = compute_mean_curve(data, n, m);
1271
1272 let (frequencies, power) = periodogram(&mean_curve, argvals);
1273
1274 if frequencies.len() < 2 {
1275 return f64::NAN;
1276 }
1277
1278 let fundamental_freq = 1.0 / period;
1279
1280 let _freq_tol = fundamental_freq * 0.1; let mut seasonal_power = 0.0;
1283 let mut total_power = 0.0;
1284
1285 for (i, (&freq, &p)) in frequencies.iter().zip(power.iter()).enumerate() {
1286 if i == 0 {
1287 continue;
1288 } total_power += p;
1291
1292 let ratio = freq / fundamental_freq;
1294 let nearest_harmonic = ratio.round();
1295 if (ratio - nearest_harmonic).abs() < 0.1 && nearest_harmonic >= 1.0 {
1296 seasonal_power += p;
1297 }
1298 }
1299
1300 if total_power < 1e-15 {
1301 return 0.0;
1302 }
1303
1304 (seasonal_power / total_power).min(1.0)
1305}
1306
1307pub fn seasonal_strength_wavelet(
1328 data: &[f64],
1329 n: usize,
1330 m: usize,
1331 argvals: &[f64],
1332 period: f64,
1333) -> f64 {
1334 if n == 0 || m < 4 || argvals.len() != m || period <= 0.0 {
1335 return f64::NAN;
1336 }
1337
1338 let mean_curve = compute_mean_curve(data, n, m);
1340
1341 let dc: f64 = mean_curve.iter().sum::<f64>() / m as f64;
1343 let detrended: Vec<f64> = mean_curve.iter().map(|&x| x - dc).collect();
1344
1345 let total_variance: f64 = detrended.iter().map(|&x| x * x).sum::<f64>() / m as f64;
1347
1348 if total_variance < 1e-15 {
1349 return 0.0;
1350 }
1351
1352 let omega0 = 6.0;
1354 let scale = period * omega0 / (2.0 * PI);
1355 let wavelet_coeffs = cwt_morlet_fft(&detrended, argvals, scale, omega0);
1356
1357 if wavelet_coeffs.is_empty() {
1358 return f64::NAN;
1359 }
1360
1361 let (interior_start, interior_end) = match interior_bounds(m) {
1363 Some(bounds) => bounds,
1364 None => return f64::NAN,
1365 };
1366
1367 let wavelet_power: f64 = wavelet_coeffs[interior_start..interior_end]
1368 .iter()
1369 .map(|c| c.norm_sqr())
1370 .sum::<f64>()
1371 / (interior_end - interior_start) as f64;
1372
1373 (wavelet_power / total_variance).sqrt().min(1.0)
1376}
1377
1378pub fn seasonal_strength_windowed(
1392 data: &[f64],
1393 n: usize,
1394 m: usize,
1395 argvals: &[f64],
1396 period: f64,
1397 window_size: f64,
1398 method: StrengthMethod,
1399) -> Vec<f64> {
1400 if n == 0 || m < 4 || argvals.len() != m || period <= 0.0 || window_size <= 0.0 {
1401 return Vec::new();
1402 }
1403
1404 let dt = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
1405 let half_window_points = ((window_size / 2.0) / dt).round() as usize;
1406
1407 let mean_curve = compute_mean_curve(data, n, m);
1409
1410 (0..m)
1411 .into_par_iter()
1412 .map(|center| {
1413 let start = center.saturating_sub(half_window_points);
1414 let end = (center + half_window_points + 1).min(m);
1415 let window_m = end - start;
1416
1417 if window_m < 4 {
1418 return f64::NAN;
1419 }
1420
1421 let window_data: Vec<f64> = mean_curve[start..end].to_vec();
1422 let window_argvals: Vec<f64> = argvals[start..end].to_vec();
1423
1424 let single_data = window_data.clone();
1426
1427 match method {
1428 StrengthMethod::Variance => seasonal_strength_variance(
1429 &single_data,
1430 1,
1431 window_m,
1432 &window_argvals,
1433 period,
1434 3,
1435 ),
1436 StrengthMethod::Spectral => {
1437 seasonal_strength_spectral(&single_data, 1, window_m, &window_argvals, period)
1438 }
1439 }
1440 })
1441 .collect()
1442}
1443
1444pub fn detect_seasonality_changes(
1463 data: &[f64],
1464 n: usize,
1465 m: usize,
1466 argvals: &[f64],
1467 period: f64,
1468 threshold: f64,
1469 window_size: f64,
1470 min_duration: f64,
1471) -> ChangeDetectionResult {
1472 if n == 0 || m < 4 || argvals.len() != m {
1473 return ChangeDetectionResult {
1474 change_points: Vec::new(),
1475 strength_curve: Vec::new(),
1476 };
1477 }
1478
1479 let strength_curve = seasonal_strength_windowed(
1481 data,
1482 n,
1483 m,
1484 argvals,
1485 period,
1486 window_size,
1487 StrengthMethod::Variance,
1488 );
1489
1490 if strength_curve.is_empty() {
1491 return ChangeDetectionResult {
1492 change_points: Vec::new(),
1493 strength_curve: Vec::new(),
1494 };
1495 }
1496
1497 let dt = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
1498 let min_dur_points = (min_duration / dt).round() as usize;
1499
1500 let mut change_points = Vec::new();
1502 let mut in_seasonal = strength_curve[0] > threshold;
1503 let mut last_change_idx: Option<usize> = None;
1504
1505 for (i, &ss) in strength_curve.iter().enumerate().skip(1) {
1506 if ss.is_nan() {
1507 continue;
1508 }
1509
1510 let now_seasonal = ss > threshold;
1511
1512 if now_seasonal != in_seasonal {
1513 if let Some(last_idx) = last_change_idx {
1515 if i - last_idx < min_dur_points {
1516 continue; }
1518 }
1519
1520 let change_type = if now_seasonal {
1521 ChangeType::Onset
1522 } else {
1523 ChangeType::Cessation
1524 };
1525
1526 let strength_before = if i > 0 && !strength_curve[i - 1].is_nan() {
1527 strength_curve[i - 1]
1528 } else {
1529 ss
1530 };
1531
1532 change_points.push(ChangePoint {
1533 time: argvals[i],
1534 change_type,
1535 strength_before,
1536 strength_after: ss,
1537 });
1538
1539 in_seasonal = now_seasonal;
1540 last_change_idx = Some(i);
1541 }
1542 }
1543
1544 ChangeDetectionResult {
1545 change_points,
1546 strength_curve,
1547 }
1548}
1549
1550pub fn detect_amplitude_modulation(
1589 data: &[f64],
1590 n: usize,
1591 m: usize,
1592 argvals: &[f64],
1593 period: f64,
1594 modulation_threshold: f64,
1595 seasonality_threshold: f64,
1596) -> AmplitudeModulationResult {
1597 let empty_result = AmplitudeModulationResult {
1599 is_seasonal: false,
1600 seasonal_strength: 0.0,
1601 has_modulation: false,
1602 modulation_type: ModulationType::NonSeasonal,
1603 modulation_score: 0.0,
1604 amplitude_trend: 0.0,
1605 strength_curve: Vec::new(),
1606 time_points: Vec::new(),
1607 min_strength: 0.0,
1608 max_strength: 0.0,
1609 };
1610
1611 if n == 0 || m < 4 || argvals.len() != m || period <= 0.0 {
1612 return empty_result;
1613 }
1614
1615 let overall_strength = seasonal_strength_spectral(data, n, m, argvals, period);
1617
1618 if overall_strength < seasonality_threshold {
1619 return AmplitudeModulationResult {
1620 is_seasonal: false,
1621 seasonal_strength: overall_strength,
1622 has_modulation: false,
1623 modulation_type: ModulationType::NonSeasonal,
1624 modulation_score: 0.0,
1625 amplitude_trend: 0.0,
1626 strength_curve: Vec::new(),
1627 time_points: argvals.to_vec(),
1628 min_strength: 0.0,
1629 max_strength: 0.0,
1630 };
1631 }
1632
1633 let mean_curve = compute_mean_curve(data, n, m);
1635
1636 let dc: f64 = mean_curve.iter().sum::<f64>() / m as f64;
1638 let detrended: Vec<f64> = mean_curve.iter().map(|&x| x - dc).collect();
1639 let analytic = hilbert_transform(&detrended);
1640 let envelope: Vec<f64> = analytic.iter().map(|c| c.norm()).collect();
1641
1642 if envelope.is_empty() {
1643 return AmplitudeModulationResult {
1644 is_seasonal: true,
1645 seasonal_strength: overall_strength,
1646 has_modulation: false,
1647 modulation_type: ModulationType::Stable,
1648 modulation_score: 0.0,
1649 amplitude_trend: 0.0,
1650 strength_curve: Vec::new(),
1651 time_points: argvals.to_vec(),
1652 min_strength: 0.0,
1653 max_strength: 0.0,
1654 };
1655 }
1656
1657 let dt = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
1660 let smooth_window = ((period / dt) as usize).max(3);
1661 let half_window = smooth_window / 2;
1662
1663 let smoothed_envelope: Vec<f64> = (0..m)
1664 .map(|i| {
1665 let start = i.saturating_sub(half_window);
1666 let end = (i + half_window + 1).min(m);
1667 let sum: f64 = envelope[start..end].iter().sum();
1668 sum / (end - start) as f64
1669 })
1670 .collect();
1671
1672 let (interior_start, interior_end) = match interior_bounds(m) {
1675 Some((s, e)) if e > s + 4 => (s, e),
1676 _ => {
1677 return AmplitudeModulationResult {
1678 is_seasonal: true,
1679 seasonal_strength: overall_strength,
1680 has_modulation: false,
1681 modulation_type: ModulationType::Stable,
1682 modulation_score: 0.0,
1683 amplitude_trend: 0.0,
1684 strength_curve: envelope,
1685 time_points: argvals.to_vec(),
1686 min_strength: 0.0,
1687 max_strength: 0.0,
1688 };
1689 }
1690 };
1691
1692 let interior_envelope = &smoothed_envelope[interior_start..interior_end];
1693 let interior_times = &argvals[interior_start..interior_end];
1694 let n_interior = interior_envelope.len() as f64;
1695
1696 let mean_amp = interior_envelope.iter().sum::<f64>() / n_interior;
1697 let min_amp = interior_envelope
1698 .iter()
1699 .cloned()
1700 .fold(f64::INFINITY, f64::min);
1701 let max_amp = interior_envelope
1702 .iter()
1703 .cloned()
1704 .fold(f64::NEG_INFINITY, f64::max);
1705
1706 let variance = interior_envelope
1708 .iter()
1709 .map(|&a| (a - mean_amp).powi(2))
1710 .sum::<f64>()
1711 / n_interior;
1712 let std_amp = variance.sqrt();
1713 let modulation_score = if mean_amp > 1e-10 {
1714 std_amp / mean_amp
1715 } else {
1716 0.0
1717 };
1718
1719 let t_mean = interior_times.iter().sum::<f64>() / n_interior;
1721 let mut cov_ta = 0.0;
1722 let mut var_t = 0.0;
1723 for (&t, &a) in interior_times.iter().zip(interior_envelope.iter()) {
1724 cov_ta += (t - t_mean) * (a - mean_amp);
1725 var_t += (t - t_mean).powi(2);
1726 }
1727 let slope = if var_t > 1e-10 { cov_ta / var_t } else { 0.0 };
1728
1729 let time_span = interior_times.last().unwrap_or(&1.0) - interior_times.first().unwrap_or(&0.0);
1731 let amp_range = max_amp - min_amp;
1732 let amplitude_trend = if amp_range > 1e-10 && time_span > 1e-10 && mean_amp > 1e-10 {
1733 (slope * time_span / mean_amp).clamp(-1.0, 1.0)
1735 } else {
1736 0.0
1737 };
1738
1739 let has_modulation = modulation_score > modulation_threshold;
1741 let modulation_type = if !has_modulation {
1742 ModulationType::Stable
1743 } else if amplitude_trend > 0.3 {
1744 ModulationType::Emerging
1745 } else if amplitude_trend < -0.3 {
1746 ModulationType::Fading
1747 } else {
1748 ModulationType::Oscillating
1749 };
1750
1751 AmplitudeModulationResult {
1752 is_seasonal: true,
1753 seasonal_strength: overall_strength,
1754 has_modulation,
1755 modulation_type,
1756 modulation_score,
1757 amplitude_trend,
1758 strength_curve: envelope,
1759 time_points: argvals.to_vec(),
1760 min_strength: min_amp,
1761 max_strength: max_amp,
1762 }
1763}
1764
1765pub fn detect_amplitude_modulation_wavelet(
1788 data: &[f64],
1789 n: usize,
1790 m: usize,
1791 argvals: &[f64],
1792 period: f64,
1793 modulation_threshold: f64,
1794 seasonality_threshold: f64,
1795) -> WaveletAmplitudeResult {
1796 let empty_result = WaveletAmplitudeResult {
1797 is_seasonal: false,
1798 seasonal_strength: 0.0,
1799 has_modulation: false,
1800 modulation_type: ModulationType::NonSeasonal,
1801 modulation_score: 0.0,
1802 amplitude_trend: 0.0,
1803 wavelet_amplitude: Vec::new(),
1804 time_points: Vec::new(),
1805 scale: 0.0,
1806 };
1807
1808 if n == 0 || m < 4 || argvals.len() != m || period <= 0.0 {
1809 return empty_result;
1810 }
1811
1812 let overall_strength = seasonal_strength_spectral(data, n, m, argvals, period);
1814
1815 if overall_strength < seasonality_threshold {
1816 return WaveletAmplitudeResult {
1817 is_seasonal: false,
1818 seasonal_strength: overall_strength,
1819 has_modulation: false,
1820 modulation_type: ModulationType::NonSeasonal,
1821 modulation_score: 0.0,
1822 amplitude_trend: 0.0,
1823 wavelet_amplitude: Vec::new(),
1824 time_points: argvals.to_vec(),
1825 scale: 0.0,
1826 };
1827 }
1828
1829 let mean_curve = compute_mean_curve(data, n, m);
1831
1832 let dc: f64 = mean_curve.iter().sum::<f64>() / m as f64;
1834 let detrended: Vec<f64> = mean_curve.iter().map(|&x| x - dc).collect();
1835
1836 let omega0 = 6.0; let scale = period * omega0 / (2.0 * PI);
1840
1841 let wavelet_coeffs = cwt_morlet_fft(&detrended, argvals, scale, omega0);
1843
1844 if wavelet_coeffs.is_empty() {
1845 return WaveletAmplitudeResult {
1846 is_seasonal: true,
1847 seasonal_strength: overall_strength,
1848 has_modulation: false,
1849 modulation_type: ModulationType::Stable,
1850 modulation_score: 0.0,
1851 amplitude_trend: 0.0,
1852 wavelet_amplitude: Vec::new(),
1853 time_points: argvals.to_vec(),
1854 scale,
1855 };
1856 }
1857
1858 let wavelet_amplitude: Vec<f64> = wavelet_coeffs.iter().map(|c| c.norm()).collect();
1860
1861 let (interior_start, interior_end) = match interior_bounds(m) {
1863 Some((s, e)) if e > s + 4 => (s, e),
1864 _ => {
1865 return WaveletAmplitudeResult {
1866 is_seasonal: true,
1867 seasonal_strength: overall_strength,
1868 has_modulation: false,
1869 modulation_type: ModulationType::Stable,
1870 modulation_score: 0.0,
1871 amplitude_trend: 0.0,
1872 wavelet_amplitude,
1873 time_points: argvals.to_vec(),
1874 scale,
1875 };
1876 }
1877 };
1878
1879 let interior_amp = &wavelet_amplitude[interior_start..interior_end];
1880 let interior_times = &argvals[interior_start..interior_end];
1881 let n_interior = interior_amp.len() as f64;
1882
1883 let mean_amp = interior_amp.iter().sum::<f64>() / n_interior;
1884
1885 let variance = interior_amp
1887 .iter()
1888 .map(|&a| (a - mean_amp).powi(2))
1889 .sum::<f64>()
1890 / n_interior;
1891 let std_amp = variance.sqrt();
1892 let modulation_score = if mean_amp > 1e-10 {
1893 std_amp / mean_amp
1894 } else {
1895 0.0
1896 };
1897
1898 let t_mean = interior_times.iter().sum::<f64>() / n_interior;
1900 let mut cov_ta = 0.0;
1901 let mut var_t = 0.0;
1902 for (&t, &a) in interior_times.iter().zip(interior_amp.iter()) {
1903 cov_ta += (t - t_mean) * (a - mean_amp);
1904 var_t += (t - t_mean).powi(2);
1905 }
1906 let slope = if var_t > 1e-10 { cov_ta / var_t } else { 0.0 };
1907
1908 let time_span = interior_times.last().unwrap_or(&1.0) - interior_times.first().unwrap_or(&0.0);
1909 let amplitude_trend = if mean_amp > 1e-10 && time_span > 1e-10 {
1910 (slope * time_span / mean_amp).clamp(-1.0, 1.0)
1911 } else {
1912 0.0
1913 };
1914
1915 let has_modulation = modulation_score > modulation_threshold;
1917 let modulation_type = if !has_modulation {
1918 ModulationType::Stable
1919 } else if amplitude_trend > 0.3 {
1920 ModulationType::Emerging
1921 } else if amplitude_trend < -0.3 {
1922 ModulationType::Fading
1923 } else {
1924 ModulationType::Oscillating
1925 };
1926
1927 WaveletAmplitudeResult {
1928 is_seasonal: true,
1929 seasonal_strength: overall_strength,
1930 has_modulation,
1931 modulation_type,
1932 modulation_score,
1933 amplitude_trend,
1934 wavelet_amplitude,
1935 time_points: argvals.to_vec(),
1936 scale,
1937 }
1938}
1939
1940pub fn instantaneous_period(
1955 data: &[f64],
1956 n: usize,
1957 m: usize,
1958 argvals: &[f64],
1959) -> InstantaneousPeriod {
1960 if n == 0 || m < 4 || argvals.len() != m {
1961 return InstantaneousPeriod {
1962 period: Vec::new(),
1963 frequency: Vec::new(),
1964 amplitude: Vec::new(),
1965 };
1966 }
1967
1968 let mean_curve = compute_mean_curve(data, n, m);
1970
1971 let dc: f64 = mean_curve.iter().sum::<f64>() / m as f64;
1973 let detrended: Vec<f64> = mean_curve.iter().map(|&x| x - dc).collect();
1974
1975 let analytic = hilbert_transform(&detrended);
1977
1978 let amplitude: Vec<f64> = analytic.iter().map(|c| c.norm()).collect();
1980
1981 let phase: Vec<f64> = analytic.iter().map(|c| c.im.atan2(c.re)).collect();
1982
1983 let unwrapped_phase = unwrap_phase(&phase);
1985
1986 let dt = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
1988 let mut inst_freq = vec![0.0; m];
1989
1990 if m > 1 {
1992 inst_freq[0] = (unwrapped_phase[1] - unwrapped_phase[0]) / dt / (2.0 * PI);
1993 }
1994 for j in 1..(m - 1) {
1995 inst_freq[j] = (unwrapped_phase[j + 1] - unwrapped_phase[j - 1]) / (2.0 * dt) / (2.0 * PI);
1996 }
1997 if m > 1 {
1998 inst_freq[m - 1] = (unwrapped_phase[m - 1] - unwrapped_phase[m - 2]) / dt / (2.0 * PI);
1999 }
2000
2001 let period: Vec<f64> = inst_freq
2003 .iter()
2004 .map(|&f| {
2005 if f.abs() > 1e-10 {
2006 (1.0 / f).abs()
2007 } else {
2008 f64::INFINITY
2009 }
2010 })
2011 .collect();
2012
2013 InstantaneousPeriod {
2014 period,
2015 frequency: inst_freq,
2016 amplitude,
2017 }
2018}
2019
2020pub fn analyze_peak_timing(
2041 data: &[f64],
2042 n: usize,
2043 m: usize,
2044 argvals: &[f64],
2045 period: f64,
2046 smooth_nbasis: Option<usize>,
2047) -> PeakTimingResult {
2048 if n == 0 || m < 3 || argvals.len() != m || period <= 0.0 {
2049 return PeakTimingResult {
2050 peak_times: Vec::new(),
2051 peak_values: Vec::new(),
2052 normalized_timing: Vec::new(),
2053 mean_timing: f64::NAN,
2054 std_timing: f64::NAN,
2055 range_timing: f64::NAN,
2056 variability_score: f64::NAN,
2057 timing_trend: f64::NAN,
2058 cycle_indices: Vec::new(),
2059 };
2060 }
2061
2062 let min_distance = period * 0.7;
2065 let peaks = detect_peaks(
2066 data,
2067 n,
2068 m,
2069 argvals,
2070 Some(min_distance),
2071 None, true, smooth_nbasis,
2074 );
2075
2076 let sample_peaks = if peaks.peaks.is_empty() {
2079 Vec::new()
2080 } else {
2081 peaks.peaks[0].clone()
2082 };
2083
2084 if sample_peaks.is_empty() {
2085 return PeakTimingResult {
2086 peak_times: Vec::new(),
2087 peak_values: Vec::new(),
2088 normalized_timing: Vec::new(),
2089 mean_timing: f64::NAN,
2090 std_timing: f64::NAN,
2091 range_timing: f64::NAN,
2092 variability_score: f64::NAN,
2093 timing_trend: f64::NAN,
2094 cycle_indices: Vec::new(),
2095 };
2096 }
2097
2098 let peak_times: Vec<f64> = sample_peaks.iter().map(|p| p.time).collect();
2099 let peak_values: Vec<f64> = sample_peaks.iter().map(|p| p.value).collect();
2100
2101 let t_start = argvals[0];
2103 let normalized_timing: Vec<f64> = peak_times
2104 .iter()
2105 .map(|&t| {
2106 let cycle_pos = (t - t_start) % period;
2107 cycle_pos / period
2108 })
2109 .collect();
2110
2111 let cycle_indices: Vec<usize> = peak_times
2113 .iter()
2114 .map(|&t| ((t - t_start) / period).floor() as usize + 1)
2115 .collect();
2116
2117 let n_peaks = normalized_timing.len() as f64;
2119 let mean_timing = normalized_timing.iter().sum::<f64>() / n_peaks;
2120
2121 let variance: f64 = normalized_timing
2122 .iter()
2123 .map(|&x| (x - mean_timing).powi(2))
2124 .sum::<f64>()
2125 / n_peaks;
2126 let std_timing = variance.sqrt();
2127
2128 let min_timing = normalized_timing
2129 .iter()
2130 .cloned()
2131 .fold(f64::INFINITY, f64::min);
2132 let max_timing = normalized_timing
2133 .iter()
2134 .cloned()
2135 .fold(f64::NEG_INFINITY, f64::max);
2136 let range_timing = max_timing - min_timing;
2137
2138 let variability_score = (std_timing / 0.1).min(1.0);
2142
2143 let cycle_idx_f64: Vec<f64> = cycle_indices.iter().map(|&i| i as f64).collect();
2145 let timing_trend = linear_slope(&cycle_idx_f64, &normalized_timing);
2146
2147 PeakTimingResult {
2148 peak_times,
2149 peak_values,
2150 normalized_timing,
2151 mean_timing,
2152 std_timing,
2153 range_timing,
2154 variability_score,
2155 timing_trend,
2156 cycle_indices,
2157 }
2158}
2159
2160pub fn classify_seasonality(
2184 data: &[f64],
2185 n: usize,
2186 m: usize,
2187 argvals: &[f64],
2188 period: f64,
2189 strength_threshold: Option<f64>,
2190 timing_threshold: Option<f64>,
2191) -> SeasonalityClassification {
2192 let strength_thresh = strength_threshold.unwrap_or(0.3);
2193 let timing_thresh = timing_threshold.unwrap_or(0.05);
2194
2195 if n == 0 || m < 4 || argvals.len() != m || period <= 0.0 {
2196 return SeasonalityClassification {
2197 is_seasonal: false,
2198 has_stable_timing: false,
2199 timing_variability: f64::NAN,
2200 seasonal_strength: f64::NAN,
2201 cycle_strengths: Vec::new(),
2202 weak_seasons: Vec::new(),
2203 classification: SeasonalType::NonSeasonal,
2204 peak_timing: None,
2205 };
2206 }
2207
2208 let overall_strength = seasonal_strength_variance(data, n, m, argvals, period, 3);
2210
2211 let dt = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
2213 let _points_per_cycle = (period / dt).round() as usize;
2214 let t_start = argvals[0];
2215 let t_end = argvals[m - 1];
2216 let n_cycles = ((t_end - t_start) / period).floor() as usize;
2217
2218 let mut cycle_strengths = Vec::with_capacity(n_cycles);
2219 let mut weak_seasons = Vec::new();
2220
2221 for cycle in 0..n_cycles {
2222 let cycle_start = t_start + cycle as f64 * period;
2223 let cycle_end = cycle_start + period;
2224
2225 let start_idx = argvals.iter().position(|&t| t >= cycle_start).unwrap_or(0);
2227 let end_idx = argvals.iter().position(|&t| t > cycle_end).unwrap_or(m);
2228
2229 let cycle_m = end_idx - start_idx;
2230 if cycle_m < 4 {
2231 cycle_strengths.push(f64::NAN);
2232 continue;
2233 }
2234
2235 let cycle_data: Vec<f64> = (start_idx..end_idx)
2237 .flat_map(|j| (0..n).map(move |i| data[i + j * n]))
2238 .collect();
2239 let cycle_argvals: Vec<f64> = argvals[start_idx..end_idx].to_vec();
2240
2241 let strength =
2242 seasonal_strength_variance(&cycle_data, n, cycle_m, &cycle_argvals, period, 3);
2243
2244 cycle_strengths.push(strength);
2245
2246 if strength < strength_thresh {
2247 weak_seasons.push(cycle);
2248 }
2249 }
2250
2251 let peak_timing = analyze_peak_timing(data, n, m, argvals, period, None);
2253
2254 let is_seasonal = overall_strength >= strength_thresh;
2256 let has_stable_timing = peak_timing.std_timing <= timing_thresh;
2257 let timing_variability = peak_timing.variability_score;
2258
2259 let n_weak = weak_seasons.len();
2261 let classification = if !is_seasonal {
2262 SeasonalType::NonSeasonal
2263 } else if n_cycles > 0 && n_weak as f64 / n_cycles as f64 > 0.3 {
2264 SeasonalType::IntermittentSeasonal
2266 } else if !has_stable_timing {
2267 SeasonalType::VariableTiming
2268 } else {
2269 SeasonalType::StableSeasonal
2270 };
2271
2272 SeasonalityClassification {
2273 is_seasonal,
2274 has_stable_timing,
2275 timing_variability,
2276 seasonal_strength: overall_strength,
2277 cycle_strengths,
2278 weak_seasons,
2279 classification,
2280 peak_timing: Some(peak_timing),
2281 }
2282}
2283
2284pub fn detect_seasonality_changes_auto(
2298 data: &[f64],
2299 n: usize,
2300 m: usize,
2301 argvals: &[f64],
2302 period: f64,
2303 threshold_method: ThresholdMethod,
2304 window_size: f64,
2305 min_duration: f64,
2306) -> ChangeDetectionResult {
2307 if n == 0 || m < 4 || argvals.len() != m {
2308 return ChangeDetectionResult {
2309 change_points: Vec::new(),
2310 strength_curve: Vec::new(),
2311 };
2312 }
2313
2314 let strength_curve = seasonal_strength_windowed(
2316 data,
2317 n,
2318 m,
2319 argvals,
2320 period,
2321 window_size,
2322 StrengthMethod::Variance,
2323 );
2324
2325 if strength_curve.is_empty() {
2326 return ChangeDetectionResult {
2327 change_points: Vec::new(),
2328 strength_curve: Vec::new(),
2329 };
2330 }
2331
2332 let threshold = match threshold_method {
2334 ThresholdMethod::Fixed(t) => t,
2335 ThresholdMethod::Percentile(p) => {
2336 let mut sorted: Vec<f64> = strength_curve
2337 .iter()
2338 .copied()
2339 .filter(|x| x.is_finite())
2340 .collect();
2341 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2342 if sorted.is_empty() {
2343 0.5
2344 } else {
2345 let idx = ((p / 100.0) * sorted.len() as f64) as usize;
2346 sorted[idx.min(sorted.len() - 1)]
2347 }
2348 }
2349 ThresholdMethod::Otsu => otsu_threshold(&strength_curve),
2350 };
2351
2352 detect_seasonality_changes(
2354 data,
2355 n,
2356 m,
2357 argvals,
2358 period,
2359 threshold,
2360 window_size,
2361 min_duration,
2362 )
2363}
2364
2365#[cfg(test)]
2366mod tests {
2367 use super::*;
2368 use std::f64::consts::PI;
2369
2370 fn generate_sine(n: usize, m: usize, period: f64, argvals: &[f64]) -> Vec<f64> {
2371 let mut data = vec![0.0; n * m];
2372 for i in 0..n {
2373 for j in 0..m {
2374 data[i + j * n] = (2.0 * PI * argvals[j] / period).sin();
2375 }
2376 }
2377 data
2378 }
2379
2380 #[test]
2381 fn test_period_estimation_fft() {
2382 let m = 200;
2383 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 0.1).collect();
2384 let period = 2.0;
2385 let data = generate_sine(1, m, period, &argvals);
2386
2387 let estimate = estimate_period_fft(&data, 1, m, &argvals);
2388 assert!((estimate.period - period).abs() < 0.2);
2389 assert!(estimate.confidence > 1.0);
2390 }
2391
2392 #[test]
2393 fn test_peak_detection() {
2394 let m = 100;
2395 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 0.1).collect();
2396 let period = 2.0;
2397 let data = generate_sine(1, m, period, &argvals);
2398
2399 let result = detect_peaks(&data, 1, m, &argvals, Some(1.5), None, false, None);
2400
2401 assert!(!result.peaks[0].is_empty());
2403 assert!((result.mean_period - period).abs() < 0.3);
2404 }
2405
2406 #[test]
2407 fn test_peak_detection_known_sine() {
2408 let m = 200; let period = 2.0;
2412 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 10.0 / (m - 1) as f64).collect();
2413 let data: Vec<f64> = argvals
2414 .iter()
2415 .map(|&t| (2.0 * std::f64::consts::PI * t / period).sin())
2416 .collect();
2417
2418 let result = detect_peaks(&data, 1, m, &argvals, None, None, false, None);
2419
2420 assert_eq!(
2422 result.peaks[0].len(),
2423 5,
2424 "Expected 5 peaks, got {}. Peak times: {:?}",
2425 result.peaks[0].len(),
2426 result.peaks[0].iter().map(|p| p.time).collect::<Vec<_>>()
2427 );
2428
2429 let expected_times = [0.5, 2.5, 4.5, 6.5, 8.5];
2431 for (peak, expected) in result.peaks[0].iter().zip(expected_times.iter()) {
2432 assert!(
2433 (peak.time - expected).abs() < 0.15,
2434 "Peak at {:.3} not close to expected {:.3}",
2435 peak.time,
2436 expected
2437 );
2438 }
2439
2440 assert!(
2442 (result.mean_period - period).abs() < 0.1,
2443 "Mean period {:.3} not close to expected {:.3}",
2444 result.mean_period,
2445 period
2446 );
2447 }
2448
2449 #[test]
2450 fn test_peak_detection_with_min_distance() {
2451 let m = 200;
2453 let period = 2.0;
2454 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 10.0 / (m - 1) as f64).collect();
2455 let data: Vec<f64> = argvals
2456 .iter()
2457 .map(|&t| (2.0 * std::f64::consts::PI * t / period).sin())
2458 .collect();
2459
2460 let result = detect_peaks(&data, 1, m, &argvals, Some(1.5), None, false, None);
2462 assert_eq!(
2463 result.peaks[0].len(),
2464 5,
2465 "With min_distance=1.5, expected 5 peaks, got {}",
2466 result.peaks[0].len()
2467 );
2468
2469 let result2 = detect_peaks(&data, 1, m, &argvals, Some(2.5), None, false, None);
2471 assert!(
2472 result2.peaks[0].len() < 5,
2473 "With min_distance=2.5, expected fewer than 5 peaks, got {}",
2474 result2.peaks[0].len()
2475 );
2476 }
2477
2478 #[test]
2479 fn test_peak_detection_period_1() {
2480 let m = 400; let period = 1.0;
2484 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 10.0 / (m - 1) as f64).collect();
2485 let data: Vec<f64> = argvals
2486 .iter()
2487 .map(|&t| (2.0 * std::f64::consts::PI * t / period).sin())
2488 .collect();
2489
2490 let result = detect_peaks(&data, 1, m, &argvals, None, None, false, None);
2491
2492 assert_eq!(
2494 result.peaks[0].len(),
2495 10,
2496 "Expected 10 peaks, got {}",
2497 result.peaks[0].len()
2498 );
2499
2500 assert!(
2502 (result.mean_period - period).abs() < 0.1,
2503 "Mean period {:.3} not close to expected {:.3}",
2504 result.mean_period,
2505 period
2506 );
2507 }
2508
2509 #[test]
2510 fn test_peak_detection_shifted_sine() {
2511 let m = 200;
2514 let period = 2.0;
2515 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 10.0 / (m - 1) as f64).collect();
2516 let data: Vec<f64> = argvals
2517 .iter()
2518 .map(|&t| (2.0 * std::f64::consts::PI * t / period).sin() + 1.0)
2519 .collect();
2520
2521 let result = detect_peaks(&data, 1, m, &argvals, None, None, false, None);
2522
2523 assert_eq!(
2525 result.peaks[0].len(),
2526 5,
2527 "Expected 5 peaks for shifted sine, got {}",
2528 result.peaks[0].len()
2529 );
2530
2531 for peak in &result.peaks[0] {
2533 assert!(
2534 (peak.value - 2.0).abs() < 0.05,
2535 "Peak value {:.3} not close to expected 2.0",
2536 peak.value
2537 );
2538 }
2539 }
2540
2541 #[test]
2542 fn test_peak_detection_prominence() {
2543 let m = 200;
2546 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 10.0 / (m - 1) as f64).collect();
2547 let data: Vec<f64> = argvals
2548 .iter()
2549 .map(|&t| {
2550 let base = (2.0 * std::f64::consts::PI * t / 2.0).sin();
2551 let ripple = 0.1 * (2.0 * std::f64::consts::PI * t * 4.0).sin();
2553 base + ripple
2554 })
2555 .collect();
2556
2557 let result_no_filter = detect_peaks(&data, 1, m, &argvals, None, None, false, None);
2559
2560 let result_filtered = detect_peaks(&data, 1, m, &argvals, None, Some(0.5), false, None);
2562
2563 assert!(
2565 result_filtered.peaks[0].len() <= result_no_filter.peaks[0].len(),
2566 "Prominence filter should reduce peak count"
2567 );
2568 }
2569
2570 #[test]
2571 fn test_peak_detection_different_amplitudes() {
2572 let m = 200;
2574 let period = 2.0;
2575 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 10.0 / (m - 1) as f64).collect();
2576
2577 for amplitude in [0.5, 1.0, 2.0, 5.0] {
2578 let data: Vec<f64> = argvals
2579 .iter()
2580 .map(|&t| amplitude * (2.0 * std::f64::consts::PI * t / period).sin())
2581 .collect();
2582
2583 let result = detect_peaks(&data, 1, m, &argvals, None, None, false, None);
2584
2585 assert_eq!(
2586 result.peaks[0].len(),
2587 5,
2588 "Amplitude {} should still find 5 peaks",
2589 amplitude
2590 );
2591
2592 for peak in &result.peaks[0] {
2594 assert!(
2595 (peak.value - amplitude).abs() < 0.1,
2596 "Peak value {:.3} should be close to amplitude {}",
2597 peak.value,
2598 amplitude
2599 );
2600 }
2601 }
2602 }
2603
2604 #[test]
2605 fn test_peak_detection_varying_frequency() {
2606 let m = 400;
2609 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 10.0 / (m - 1) as f64).collect();
2610
2611 let data: Vec<f64> = argvals
2614 .iter()
2615 .map(|&t| {
2616 let phase = 2.0 * std::f64::consts::PI * (0.5 * t + 0.05 * t * t);
2617 phase.sin()
2618 })
2619 .collect();
2620
2621 let result = detect_peaks(&data, 1, m, &argvals, None, None, false, None);
2622
2623 assert!(
2625 result.peaks[0].len() >= 5,
2626 "Should find at least 5 peaks, got {}",
2627 result.peaks[0].len()
2628 );
2629
2630 let distances = &result.inter_peak_distances[0];
2632 if distances.len() >= 3 {
2633 let early_avg = (distances[0] + distances[1]) / 2.0;
2635 let late_avg = (distances[distances.len() - 2] + distances[distances.len() - 1]) / 2.0;
2636 assert!(
2637 late_avg < early_avg,
2638 "Later peaks should be closer: early avg={:.3}, late avg={:.3}",
2639 early_avg,
2640 late_avg
2641 );
2642 }
2643 }
2644
2645 #[test]
2646 fn test_peak_detection_sum_of_sines() {
2647 let m = 300;
2650 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 12.0 / (m - 1) as f64).collect();
2651
2652 let data: Vec<f64> = argvals
2653 .iter()
2654 .map(|&t| {
2655 (2.0 * std::f64::consts::PI * t / 2.0).sin()
2656 + 0.5 * (2.0 * std::f64::consts::PI * t / 3.0).sin()
2657 })
2658 .collect();
2659
2660 let result = detect_peaks(&data, 1, m, &argvals, Some(1.0), None, false, None);
2661
2662 assert!(
2664 result.peaks[0].len() >= 4,
2665 "Should find at least 4 peaks, got {}",
2666 result.peaks[0].len()
2667 );
2668
2669 let distances = &result.inter_peak_distances[0];
2671 if distances.len() >= 2 {
2672 let min_dist = distances.iter().cloned().fold(f64::INFINITY, f64::min);
2673 let max_dist = distances.iter().cloned().fold(0.0, f64::max);
2674 assert!(
2675 max_dist > min_dist * 1.1,
2676 "Distances should vary: min={:.3}, max={:.3}",
2677 min_dist,
2678 max_dist
2679 );
2680 }
2681 }
2682
2683 #[test]
2684 fn test_seasonal_strength() {
2685 let m = 200;
2686 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 0.1).collect();
2687 let period = 2.0;
2688 let data = generate_sine(1, m, period, &argvals);
2689
2690 let strength = seasonal_strength_variance(&data, 1, m, &argvals, period, 3);
2691 assert!(strength > 0.8);
2693
2694 let strength_spectral = seasonal_strength_spectral(&data, 1, m, &argvals, period);
2695 assert!(strength_spectral > 0.5);
2696 }
2697
2698 #[test]
2699 fn test_instantaneous_period() {
2700 let m = 200;
2701 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 0.1).collect();
2702 let period = 2.0;
2703 let data = generate_sine(1, m, period, &argvals);
2704
2705 let result = instantaneous_period(&data, 1, m, &argvals);
2706
2707 let mid_period = result.period[m / 2];
2709 assert!(
2710 (mid_period - period).abs() < 0.5,
2711 "Expected period ~{}, got {}",
2712 period,
2713 mid_period
2714 );
2715 }
2716
2717 #[test]
2718 fn test_peak_timing_analysis() {
2719 let m = 500;
2721 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 0.02).collect();
2722 let period = 2.0;
2723 let data = generate_sine(1, m, period, &argvals);
2724
2725 let result = analyze_peak_timing(&data, 1, m, &argvals, period, Some(11));
2726
2727 assert!(!result.peak_times.is_empty());
2729 assert!(result.mean_timing.is_finite());
2731 assert!(result.std_timing < 0.1 || result.std_timing.is_nan());
2733 }
2734
2735 #[test]
2736 fn test_seasonality_classification() {
2737 let m = 400;
2738 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 0.05).collect();
2739 let period = 2.0;
2740 let data = generate_sine(1, m, period, &argvals);
2741
2742 let result = classify_seasonality(&data, 1, m, &argvals, period, None, None);
2743
2744 assert!(result.is_seasonal);
2745 assert!(result.seasonal_strength > 0.5);
2746 assert!(matches!(
2747 result.classification,
2748 SeasonalType::StableSeasonal | SeasonalType::VariableTiming
2749 ));
2750 }
2751
2752 #[test]
2753 fn test_otsu_threshold() {
2754 let values = vec![
2756 0.1, 0.12, 0.15, 0.18, 0.11, 0.14, 0.7, 0.75, 0.8, 0.85, 0.9, 0.72,
2757 ];
2758
2759 let threshold = otsu_threshold(&values);
2760
2761 assert!(threshold >= 0.1, "Threshold {} should be >= 0.1", threshold);
2765 assert!(threshold <= 0.9, "Threshold {} should be <= 0.9", threshold);
2766 }
2767
2768 #[test]
2769 fn test_gcv_fourier_nbasis_selection() {
2770 let m = 100;
2771 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 0.1).collect();
2772
2773 let mut data = vec![0.0; m];
2775 for j in 0..m {
2776 data[j] = (2.0 * PI * argvals[j] / 2.0).sin() + 0.1 * (j as f64 * 0.3).sin();
2777 }
2778
2779 let nbasis = crate::basis::select_fourier_nbasis_gcv(&data, 1, m, &argvals, 5, 25);
2780
2781 assert!(nbasis >= 5);
2783 assert!(nbasis <= 25);
2784 }
2785
2786 #[test]
2787 fn test_detect_multiple_periods() {
2788 let m = 400;
2789 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 0.05).collect(); let period1 = 2.0;
2793 let period2 = 7.0;
2794 let mut data = vec![0.0; m];
2795 for j in 0..m {
2796 data[j] = (2.0 * PI * argvals[j] / period1).sin()
2797 + 0.6 * (2.0 * PI * argvals[j] / period2).sin();
2798 }
2799
2800 let detected = detect_multiple_periods(&data, 1, m, &argvals, 5, 0.4, 0.20);
2802
2803 assert!(
2805 detected.len() >= 2,
2806 "Expected at least 2 periods, found {}",
2807 detected.len()
2808 );
2809
2810 let periods: Vec<f64> = detected.iter().map(|d| d.period).collect();
2812 let has_period1 = periods.iter().any(|&p| (p - period1).abs() < 0.3);
2813 let has_period2 = periods.iter().any(|&p| (p - period2).abs() < 0.5);
2814
2815 assert!(
2816 has_period1,
2817 "Expected to find period ~{}, got {:?}",
2818 period1, periods
2819 );
2820 assert!(
2821 has_period2,
2822 "Expected to find period ~{}, got {:?}",
2823 period2, periods
2824 );
2825
2826 assert!(
2828 detected[0].amplitude > detected[1].amplitude,
2829 "First detected should have higher amplitude"
2830 );
2831
2832 for d in &detected {
2834 assert!(
2835 d.strength > 0.0,
2836 "Detected period should have positive strength"
2837 );
2838 assert!(
2839 d.confidence > 0.0,
2840 "Detected period should have positive confidence"
2841 );
2842 assert!(
2843 d.amplitude > 0.0,
2844 "Detected period should have positive amplitude"
2845 );
2846 }
2847 }
2848
2849 #[test]
2854 fn test_amplitude_modulation_stable() {
2855 let m = 200;
2857 let period = 0.2;
2858 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
2859
2860 let data: Vec<f64> = argvals
2862 .iter()
2863 .map(|&t| (2.0 * PI * t / period).sin())
2864 .collect();
2865
2866 let result = detect_amplitude_modulation(
2867 &data, 1, m, &argvals, period, 0.15, 0.3, );
2870
2871 eprintln!(
2872 "Stable test: is_seasonal={}, has_modulation={}, modulation_score={:.4}, amplitude_trend={:.4}, type={:?}",
2873 result.is_seasonal, result.has_modulation, result.modulation_score, result.amplitude_trend, result.modulation_type
2874 );
2875
2876 assert!(result.is_seasonal, "Should detect seasonality");
2877 assert!(
2878 !result.has_modulation,
2879 "Constant amplitude should not have modulation, got score={:.4}",
2880 result.modulation_score
2881 );
2882 assert_eq!(
2883 result.modulation_type,
2884 ModulationType::Stable,
2885 "Should be classified as Stable"
2886 );
2887 }
2888
2889 #[test]
2890 fn test_amplitude_modulation_emerging() {
2891 let m = 200;
2893 let period = 0.2;
2894 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
2895
2896 let data: Vec<f64> = argvals
2898 .iter()
2899 .map(|&t| {
2900 let amplitude = 0.2 + 0.8 * t; amplitude * (2.0 * PI * t / period).sin()
2902 })
2903 .collect();
2904
2905 let result = detect_amplitude_modulation(&data, 1, m, &argvals, period, 0.15, 0.2);
2906
2907 eprintln!(
2908 "Emerging test: is_seasonal={}, has_modulation={}, modulation_score={:.4}, amplitude_trend={:.4}, type={:?}",
2909 result.is_seasonal, result.has_modulation, result.modulation_score, result.amplitude_trend, result.modulation_type
2910 );
2911
2912 assert!(result.is_seasonal, "Should detect seasonality");
2913 assert!(
2914 result.has_modulation,
2915 "Growing amplitude should have modulation, score={:.4}",
2916 result.modulation_score
2917 );
2918 assert_eq!(
2919 result.modulation_type,
2920 ModulationType::Emerging,
2921 "Should be classified as Emerging, trend={:.4}",
2922 result.amplitude_trend
2923 );
2924 assert!(
2925 result.amplitude_trend > 0.0,
2926 "Trend should be positive for emerging"
2927 );
2928 }
2929
2930 #[test]
2931 fn test_amplitude_modulation_fading() {
2932 let m = 200;
2934 let period = 0.2;
2935 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
2936
2937 let data: Vec<f64> = argvals
2939 .iter()
2940 .map(|&t| {
2941 let amplitude = 1.0 - 0.8 * t; amplitude * (2.0 * PI * t / period).sin()
2943 })
2944 .collect();
2945
2946 let result = detect_amplitude_modulation(&data, 1, m, &argvals, period, 0.15, 0.2);
2947
2948 eprintln!(
2949 "Fading test: is_seasonal={}, has_modulation={}, modulation_score={:.4}, amplitude_trend={:.4}, type={:?}",
2950 result.is_seasonal, result.has_modulation, result.modulation_score, result.amplitude_trend, result.modulation_type
2951 );
2952
2953 assert!(result.is_seasonal, "Should detect seasonality");
2954 assert!(
2955 result.has_modulation,
2956 "Fading amplitude should have modulation"
2957 );
2958 assert_eq!(
2959 result.modulation_type,
2960 ModulationType::Fading,
2961 "Should be classified as Fading, trend={:.4}",
2962 result.amplitude_trend
2963 );
2964 assert!(
2965 result.amplitude_trend < 0.0,
2966 "Trend should be negative for fading"
2967 );
2968 }
2969
2970 #[test]
2971 fn test_amplitude_modulation_oscillating() {
2972 let m = 200;
2974 let period = 0.1;
2975 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
2976
2977 let data: Vec<f64> = argvals
2979 .iter()
2980 .map(|&t| {
2981 let amplitude = 0.5 + 0.4 * (2.0 * PI * t * 2.0).sin(); amplitude * (2.0 * PI * t / period).sin()
2983 })
2984 .collect();
2985
2986 let result = detect_amplitude_modulation(&data, 1, m, &argvals, period, 0.15, 0.2);
2987
2988 eprintln!(
2989 "Oscillating test: is_seasonal={}, has_modulation={}, modulation_score={:.4}, amplitude_trend={:.4}, type={:?}",
2990 result.is_seasonal, result.has_modulation, result.modulation_score, result.amplitude_trend, result.modulation_type
2991 );
2992
2993 assert!(result.is_seasonal, "Should detect seasonality");
2994 if result.has_modulation {
2996 assert!(
2998 result.amplitude_trend.abs() < 0.5,
2999 "Trend should be small for oscillating"
3000 );
3001 }
3002 }
3003
3004 #[test]
3005 fn test_amplitude_modulation_non_seasonal() {
3006 let m = 100;
3008 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
3009
3010 let data: Vec<f64> = (0..m)
3012 .map(|i| ((i as f64 * 1.618).sin() * 100.0).fract())
3013 .collect();
3014
3015 let result = detect_amplitude_modulation(
3016 &data, 1, m, &argvals, 0.2, 0.15, 0.3,
3018 );
3019
3020 assert!(
3021 !result.is_seasonal,
3022 "Noise should not be detected as seasonal"
3023 );
3024 assert_eq!(
3025 result.modulation_type,
3026 ModulationType::NonSeasonal,
3027 "Should be classified as NonSeasonal"
3028 );
3029 }
3030
3031 #[test]
3036 fn test_wavelet_amplitude_modulation_stable() {
3037 let m = 200;
3038 let period = 0.2;
3039 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
3040
3041 let data: Vec<f64> = argvals
3042 .iter()
3043 .map(|&t| (2.0 * PI * t / period).sin())
3044 .collect();
3045
3046 let result = detect_amplitude_modulation_wavelet(&data, 1, m, &argvals, period, 0.15, 0.3);
3047
3048 eprintln!(
3049 "Wavelet stable: is_seasonal={}, has_modulation={}, modulation_score={:.4}, amplitude_trend={:.4}, type={:?}",
3050 result.is_seasonal, result.has_modulation, result.modulation_score, result.amplitude_trend, result.modulation_type
3051 );
3052
3053 assert!(result.is_seasonal, "Should detect seasonality");
3054 assert!(
3055 !result.has_modulation,
3056 "Constant amplitude should not have modulation, got score={:.4}",
3057 result.modulation_score
3058 );
3059 }
3060
3061 #[test]
3062 fn test_wavelet_amplitude_modulation_emerging() {
3063 let m = 200;
3064 let period = 0.2;
3065 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
3066
3067 let data: Vec<f64> = argvals
3069 .iter()
3070 .map(|&t| {
3071 let amplitude = 0.2 + 0.8 * t;
3072 amplitude * (2.0 * PI * t / period).sin()
3073 })
3074 .collect();
3075
3076 let result = detect_amplitude_modulation_wavelet(&data, 1, m, &argvals, period, 0.15, 0.2);
3077
3078 eprintln!(
3079 "Wavelet emerging: is_seasonal={}, has_modulation={}, modulation_score={:.4}, amplitude_trend={:.4}, type={:?}",
3080 result.is_seasonal, result.has_modulation, result.modulation_score, result.amplitude_trend, result.modulation_type
3081 );
3082
3083 assert!(result.is_seasonal, "Should detect seasonality");
3084 assert!(
3085 result.has_modulation,
3086 "Growing amplitude should have modulation"
3087 );
3088 assert!(
3089 result.amplitude_trend > 0.0,
3090 "Trend should be positive for emerging"
3091 );
3092 }
3093
3094 #[test]
3095 fn test_wavelet_amplitude_modulation_fading() {
3096 let m = 200;
3097 let period = 0.2;
3098 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
3099
3100 let data: Vec<f64> = argvals
3102 .iter()
3103 .map(|&t| {
3104 let amplitude = 1.0 - 0.8 * t;
3105 amplitude * (2.0 * PI * t / period).sin()
3106 })
3107 .collect();
3108
3109 let result = detect_amplitude_modulation_wavelet(&data, 1, m, &argvals, period, 0.15, 0.2);
3110
3111 eprintln!(
3112 "Wavelet fading: is_seasonal={}, has_modulation={}, modulation_score={:.4}, amplitude_trend={:.4}, type={:?}",
3113 result.is_seasonal, result.has_modulation, result.modulation_score, result.amplitude_trend, result.modulation_type
3114 );
3115
3116 assert!(result.is_seasonal, "Should detect seasonality");
3117 assert!(
3118 result.has_modulation,
3119 "Fading amplitude should have modulation"
3120 );
3121 assert!(
3122 result.amplitude_trend < 0.0,
3123 "Trend should be negative for fading"
3124 );
3125 }
3126
3127 #[test]
3128 fn test_seasonal_strength_wavelet() {
3129 let m = 200;
3130 let period = 0.2;
3131 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
3132
3133 let seasonal_data: Vec<f64> = argvals
3135 .iter()
3136 .map(|&t| (2.0 * PI * t / period).sin())
3137 .collect();
3138
3139 let strength = seasonal_strength_wavelet(&seasonal_data, 1, m, &argvals, period);
3140 eprintln!("Wavelet strength (pure sine): {:.4}", strength);
3141 assert!(
3142 strength > 0.5,
3143 "Pure sine should have high wavelet strength"
3144 );
3145
3146 let noise_data: Vec<f64> = (0..m)
3148 .map(|i| ((i * 12345 + 67890) % 1000) as f64 / 1000.0 - 0.5)
3149 .collect();
3150
3151 let noise_strength = seasonal_strength_wavelet(&noise_data, 1, m, &argvals, period);
3152 eprintln!("Wavelet strength (noise): {:.4}", noise_strength);
3153 assert!(
3154 noise_strength < 0.3,
3155 "Noise should have low wavelet strength"
3156 );
3157
3158 let wrong_period_strength =
3160 seasonal_strength_wavelet(&seasonal_data, 1, m, &argvals, period * 2.0);
3161 eprintln!(
3162 "Wavelet strength (wrong period): {:.4}",
3163 wrong_period_strength
3164 );
3165 assert!(
3166 wrong_period_strength < strength,
3167 "Wrong period should have lower strength"
3168 );
3169 }
3170
3171 #[test]
3172 fn test_compute_mean_curve() {
3173 let data = vec![1.0, 2.0, 2.0, 4.0, 3.0, 6.0]; let mean = compute_mean_curve(&data, 2, 3);
3179 assert_eq!(mean.len(), 3);
3180 assert!((mean[0] - 1.5).abs() < 1e-10);
3181 assert!((mean[1] - 3.0).abs() < 1e-10);
3182 assert!((mean[2] - 4.5).abs() < 1e-10);
3183 }
3184
3185 #[test]
3186 fn test_compute_mean_curve_parallel_consistency() {
3187 let n = 10;
3189 let m = 200;
3190 let data: Vec<f64> = (0..n * m).map(|i| (i as f64 * 0.1).sin()).collect();
3191
3192 let seq_result = compute_mean_curve_impl(&data, n, m, false);
3193 let par_result = compute_mean_curve_impl(&data, n, m, true);
3194
3195 assert_eq!(seq_result.len(), par_result.len());
3196 for (s, p) in seq_result.iter().zip(par_result.iter()) {
3197 assert!(
3198 (s - p).abs() < 1e-10,
3199 "Sequential and parallel results differ"
3200 );
3201 }
3202 }
3203
3204 #[test]
3205 fn test_interior_bounds() {
3206 let bounds = interior_bounds(100);
3208 assert!(bounds.is_some());
3209 let (start, end) = bounds.unwrap();
3210 assert_eq!(start, 10);
3211 assert_eq!(end, 90);
3212
3213 let bounds = interior_bounds(10);
3215 assert!(bounds.is_some());
3216 let (start, end) = bounds.unwrap();
3217 assert!(start < end);
3218
3219 let bounds = interior_bounds(2);
3221 assert!(bounds.is_some() || bounds.is_none());
3223 }
3224
3225 #[test]
3226 fn test_hilbert_transform_pure_sine() {
3227 let m = 200;
3229 let argvals: Vec<f64> = (0..m).map(|i| i as f64 * 0.1).collect();
3230 let signal: Vec<f64> = argvals.iter().map(|&t| (2.0 * PI * t).sin()).collect();
3231
3232 let analytic = hilbert_transform(&signal);
3233 assert_eq!(analytic.len(), m);
3234
3235 for c in analytic.iter().skip(10).take(m - 20) {
3237 let amp = c.norm();
3238 assert!(
3239 (amp - 1.0).abs() < 0.1,
3240 "Amplitude should be ~1, got {}",
3241 amp
3242 );
3243 }
3244 }
3245}