1#![allow(clippy::needless_range_loop, clippy::ptr_arg)]
2#![allow(dead_code)]
9
10use std::f64::consts::PI;
11
12#[derive(Clone, Copy, Debug)]
17struct C64 {
18 re: f64,
19 im: f64,
20}
21
22impl C64 {
23 #[inline]
24 fn new(re: f64, im: f64) -> Self {
25 Self { re, im }
26 }
27 #[inline]
28 fn exp_i(theta: f64) -> Self {
29 Self {
30 re: theta.cos(),
31 im: theta.sin(),
32 }
33 }
34}
35
36impl std::ops::Add for C64 {
37 type Output = Self;
38 fn add(self, r: Self) -> Self {
39 Self::new(self.re + r.re, self.im + r.im)
40 }
41}
42impl std::ops::Sub for C64 {
43 type Output = Self;
44 fn sub(self, r: Self) -> Self {
45 Self::new(self.re - r.re, self.im - r.im)
46 }
47}
48impl std::ops::Mul for C64 {
49 type Output = Self;
50 fn mul(self, r: Self) -> Self {
51 Self::new(
52 self.re * r.re - self.im * r.im,
53 self.re * r.im + self.im * r.re,
54 )
55 }
56}
57
58fn fft(a: &mut Vec<C64>, invert: bool) {
59 let n = a.len();
60 let mut j = 0usize;
62 for i in 1..n {
63 let mut bit = n >> 1;
64 while j & bit != 0 {
65 j ^= bit;
66 bit >>= 1;
67 }
68 j ^= bit;
69 if i < j {
70 a.swap(i, j);
71 }
72 }
73 let mut len = 2usize;
74 while len <= n {
75 let ang = if invert {
76 2.0 * PI / len as f64
77 } else {
78 -2.0 * PI / len as f64
79 };
80 let wlen = C64::exp_i(ang);
81 let mut i = 0;
82 while i < n {
83 let mut w = C64::new(1.0, 0.0);
84 for jj in 0..(len / 2) {
85 let u = a[i + jj];
86 let v = a[i + jj + len / 2] * w;
87 a[i + jj] = u + v;
88 a[i + jj + len / 2] = u - v;
89 w = w * wlen;
90 }
91 i += len;
92 }
93 len <<= 1;
94 }
95 if invert {
96 let nf = n as f64;
97 for x in a.iter_mut() {
98 x.re /= nf;
99 x.im /= nf;
100 }
101 }
102}
103
104fn next_pow2(n: usize) -> usize {
106 let mut p = 1;
107 while p < n {
108 p <<= 1;
109 }
110 p
111}
112
113#[derive(Debug, Clone)]
121pub struct AutoCorrelation {
122 pub values: Vec<f64>,
124 pub max_lag: usize,
126 pub unbiased: bool,
128}
129
130impl AutoCorrelation {
131 pub fn compute(signal: &[f64], max_lag: usize, unbiased: bool, normalize: bool) -> Self {
136 let n = signal.len();
137 let mean = signal.iter().sum::<f64>() / n as f64;
138 let centered: Vec<f64> = signal.iter().map(|&x| x - mean).collect();
139 let lag_limit = max_lag.min(n.saturating_sub(1));
140 let mut values = vec![0.0_f64; lag_limit + 1];
141 for k in 0..=lag_limit {
142 let sum: f64 = (0..(n - k)).map(|i| centered[i] * centered[i + k]).sum();
143 let denom = if unbiased { (n - k) as f64 } else { n as f64 };
144 values[k] = sum / denom;
145 }
146 if normalize && values[0].abs() > 1e-15 {
147 let r0 = values[0];
148 for v in values.iter_mut() {
149 *v /= r0;
150 }
151 }
152 Self {
153 values,
154 max_lag: lag_limit,
155 unbiased,
156 }
157 }
158
159 pub fn at_lag(&self, k: usize) -> f64 {
161 self.values.get(k).copied().unwrap_or(0.0)
162 }
163
164 pub fn dominant_lag(&self) -> usize {
166 self.values[1..]
167 .iter()
168 .enumerate()
169 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
170 .map(|(i, _)| i + 1)
171 .unwrap_or(0)
172 }
173}
174
175#[derive(Debug, Clone)]
183pub struct CrossCorrelation {
184 pub values: Vec<f64>,
186 pub max_lag: usize,
188}
189
190impl CrossCorrelation {
191 pub fn compute(x: &[f64], y: &[f64], max_lag: usize) -> Self {
195 let n = x.len().min(y.len());
196 let mx = x[..n].iter().sum::<f64>() / n as f64;
197 let my = y[..n].iter().sum::<f64>() / n as f64;
198 let lag_limit = max_lag.min(n.saturating_sub(1));
199 let total_lags = 2 * lag_limit + 1;
200 let mut values = vec![0.0_f64; total_lags];
201 for (idx, lag) in (-(lag_limit as i64)..=(lag_limit as i64)).enumerate() {
202 let sum: f64 = (0..n)
203 .filter_map(|i| {
204 let j = i as i64 + lag;
205 if j >= 0 && (j as usize) < n {
206 Some((x[i] - mx) * (y[j as usize] - my))
207 } else {
208 None
209 }
210 })
211 .sum();
212 values[idx] = sum / n as f64;
213 }
214 Self {
215 values,
216 max_lag: lag_limit,
217 }
218 }
219
220 pub fn peak_lag(&self) -> i64 {
224 let (idx, _) = self
225 .values
226 .iter()
227 .enumerate()
228 .max_by(|(_, a), (_, b)| {
229 a.abs()
230 .partial_cmp(&b.abs())
231 .unwrap_or(std::cmp::Ordering::Equal)
232 })
233 .unwrap_or((self.max_lag, &0.0));
234 idx as i64 - self.max_lag as i64
235 }
236
237 pub fn at_lag(&self, lag: i64) -> f64 {
239 let idx = lag + self.max_lag as i64;
240 if idx < 0 || idx as usize >= self.values.len() {
241 return 0.0;
242 }
243 self.values[idx as usize]
244 }
245}
246
247#[derive(Debug, Clone)]
256pub struct PowerSpectralDensity {
257 pub frequencies: Vec<f64>,
259 pub power: Vec<f64>,
261 pub fs: f64,
263 pub window_len: usize,
265}
266
267impl PowerSpectralDensity {
268 pub fn welch(signal: &[f64], fs: f64, window_len: usize, overlap: f64) -> Self {
274 let n = signal.len();
275 let seg_len = next_pow2(window_len.min(n));
276 let step = ((seg_len as f64 * (1.0 - overlap.clamp(0.0, 0.99))) as usize).max(1);
277 let hann: Vec<f64> = (0..seg_len)
278 .map(|i| 0.5 * (1.0 - (2.0 * PI * i as f64 / (seg_len - 1) as f64).cos()))
279 .collect();
280 let win_power: f64 = hann.iter().map(|&w| w * w).sum::<f64>();
281
282 let half = seg_len / 2 + 1;
283 let mut psd_sum = vec![0.0_f64; half];
284 let mut n_segs = 0usize;
285
286 let mut start = 0;
287 while start + seg_len <= n {
288 let seg = &signal[start..start + seg_len];
289 let mut buf: Vec<C64> = seg
290 .iter()
291 .zip(hann.iter())
292 .map(|(&x, &w)| C64::new(x * w, 0.0))
293 .collect();
294 fft(&mut buf, false);
295 for k in 0..half {
296 let power = buf[k].re * buf[k].re + buf[k].im * buf[k].im;
297 psd_sum[k] += if k == 0 || k == half - 1 {
299 power
300 } else {
301 2.0 * power
302 };
303 }
304 n_segs += 1;
305 start += step;
306 }
307 if n_segs == 0 {
308 n_segs = 1;
309 }
310 let scale = 1.0 / (fs * win_power * n_segs as f64);
311 let power: Vec<f64> = psd_sum.iter().map(|&p| p * scale).collect();
312 let frequencies: Vec<f64> = (0..half).map(|k| k as f64 * fs / seg_len as f64).collect();
313 Self {
314 frequencies,
315 power,
316 fs,
317 window_len: seg_len,
318 }
319 }
320
321 pub fn freq_resolution(&self) -> f64 {
323 if self.window_len > 0 {
324 self.fs / self.window_len as f64
325 } else {
326 0.0
327 }
328 }
329
330 pub fn dominant_frequency(&self) -> f64 {
332 self.frequencies
333 .iter()
334 .zip(self.power.iter())
335 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
336 .map(|(&f, _)| f)
337 .unwrap_or(0.0)
338 }
339
340 pub fn total_power(&self) -> f64 {
342 let df = self.freq_resolution();
343 self.power.iter().sum::<f64>() * df
344 }
345}
346
347#[derive(Debug, Clone)]
357pub struct HilbertTransform {
358 pub real_part: Vec<f64>,
360 pub imaginary_part: Vec<f64>,
362 pub amplitude: Vec<f64>,
364 pub phase: Vec<f64>,
366}
367
368impl HilbertTransform {
369 pub fn compute(signal: &[f64]) -> Self {
373 let n = signal.len();
374 let n_fft = next_pow2(n);
375 let mut buf: Vec<C64> = signal
376 .iter()
377 .map(|&x| C64::new(x, 0.0))
378 .chain(std::iter::repeat(C64::new(0.0, 0.0)))
379 .take(n_fft)
380 .collect();
381 fft(&mut buf, false);
382
383 for k in 0..n_fft {
385 if k == 0 || (n_fft.is_multiple_of(2) && k == n_fft / 2) {
386 } else if k < n_fft / 2 {
388 buf[k].re *= 2.0;
389 buf[k].im *= 2.0;
390 } else {
391 buf[k] = C64::new(0.0, 0.0);
392 }
393 }
394 fft(&mut buf, true); let real_part: Vec<f64> = buf[..n].iter().map(|c| c.re).collect();
397 let imaginary_part: Vec<f64> = buf[..n].iter().map(|c| c.im).collect();
398 let amplitude: Vec<f64> = real_part
399 .iter()
400 .zip(imaginary_part.iter())
401 .map(|(&r, &i)| (r * r + i * i).sqrt())
402 .collect();
403 let phase: Vec<f64> = real_part
404 .iter()
405 .zip(imaginary_part.iter())
406 .map(|(&r, &i)| i.atan2(r))
407 .collect();
408 Self {
409 real_part,
410 imaginary_part,
411 amplitude,
412 phase,
413 }
414 }
415
416 pub fn instantaneous_frequency(&self, fs: f64) -> Vec<f64> {
421 let n = self.phase.len();
422 if n < 2 {
423 return vec![0.0; n];
424 }
425 let mut unwrapped = self.phase.clone();
427 for i in 1..n {
428 let diff = unwrapped[i] - unwrapped[i - 1];
429 if diff > PI {
430 for v in unwrapped[i..].iter_mut() {
431 *v -= 2.0 * PI;
432 }
433 } else if diff < -PI {
434 for v in unwrapped[i..].iter_mut() {
435 *v += 2.0 * PI;
436 }
437 }
438 }
439 let mut freq = vec![0.0_f64; n];
440 for i in 1..(n - 1) {
441 freq[i] = (unwrapped[i + 1] - unwrapped[i - 1]) * fs / (4.0 * PI);
442 }
443 freq[0] = freq[1];
444 freq[n - 1] = freq[n - 2];
445 freq
446 }
447}
448
449#[derive(Debug, Clone)]
459pub struct EMD {
460 pub imfs: Vec<Vec<f64>>,
462 pub residual: Vec<f64>,
464}
465
466impl EMD {
467 pub fn decompose(
473 signal: &[f64],
474 max_imfs: usize,
475 max_sift_iter: usize,
476 sift_threshold: f64,
477 ) -> Self {
478 let n = signal.len();
479 let mut residual = signal.to_vec();
480 let mut imfs = Vec::new();
481
482 for _ in 0..max_imfs {
483 if residual.iter().all(|&x| x.abs() < sift_threshold) {
484 break;
485 }
486 let (maxima_idx, _) = find_local_maxima(&residual);
487 let (minima_idx, _) = find_local_minima(&residual);
488 if maxima_idx.len() < 2 || minima_idx.len() < 2 {
489 break;
490 }
491 let mut h = residual.clone();
492 for _ in 0..max_sift_iter {
493 let upper = cubic_spline_envelope(&h, &find_local_maxima(&h).0, true);
494 let lower = cubic_spline_envelope(&h, &find_local_minima(&h).0, false);
495 let mean_env: Vec<f64> = upper
496 .iter()
497 .zip(lower.iter())
498 .map(|(u, l)| (u + l) / 2.0)
499 .collect();
500 let prev = h.clone();
501 for i in 0..n {
502 h[i] -= mean_env[i];
503 }
504 let sd: f64 = prev
506 .iter()
507 .zip(h.iter())
508 .map(|(p, c)| (p - c) * (p - c) / (p * p + 1e-15))
509 .sum::<f64>()
510 / n as f64;
511 if sd < sift_threshold {
512 break;
513 }
514 }
515 for i in 0..n {
516 residual[i] -= h[i];
517 }
518 imfs.push(h);
519 }
520 Self { imfs, residual }
521 }
522
523 pub fn hilbert_huang_spectrum(&self, fs: f64) -> Vec<Vec<f64>> {
528 self.imfs
529 .iter()
530 .map(|imf| {
531 let ht = HilbertTransform::compute(imf);
532 ht.instantaneous_frequency(fs)
533 })
534 .collect()
535 }
536}
537
538fn find_local_maxima(x: &[f64]) -> (Vec<usize>, Vec<f64>) {
540 let n = x.len();
541 let mut idx = Vec::new();
542 let mut vals = Vec::new();
543 for i in 1..(n.saturating_sub(1)) {
544 if x[i] > x[i - 1] && x[i] > x[i + 1] {
545 idx.push(i);
546 vals.push(x[i]);
547 }
548 }
549 (idx, vals)
550}
551
552fn find_local_minima(x: &[f64]) -> (Vec<usize>, Vec<f64>) {
554 let n = x.len();
555 let mut idx = Vec::new();
556 let mut vals = Vec::new();
557 for i in 1..(n.saturating_sub(1)) {
558 if x[i] < x[i - 1] && x[i] < x[i + 1] {
559 idx.push(i);
560 vals.push(x[i]);
561 }
562 }
563 (idx, vals)
564}
565
566fn cubic_spline_envelope(signal: &[f64], extrema_idx: &[usize], upper: bool) -> Vec<f64> {
568 let n = signal.len();
569 if extrema_idx.is_empty() {
570 let fill = if upper {
571 signal.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
572 } else {
573 signal.iter().cloned().fold(f64::INFINITY, f64::min)
574 };
575 return vec![fill; n];
576 }
577 let mut xs: Vec<f64> = Vec::new();
579 let mut ys: Vec<f64> = Vec::new();
580 xs.push(0.0);
581 ys.push(signal[extrema_idx[0]]);
582 for &i in extrema_idx {
583 xs.push(i as f64);
584 ys.push(signal[i]);
585 }
586 xs.push((n - 1) as f64);
587 ys.push(signal[extrema_idx[extrema_idx.len() - 1]]);
588
589 let mut env = vec![0.0_f64; n];
591 for i in 0..n {
592 let xi = i as f64;
593 let mut lo = 0usize;
595 let mut hi = xs.len() - 1;
596 for k in 0..(xs.len() - 1) {
597 if xs[k] <= xi && xi <= xs[k + 1] {
598 lo = k;
599 hi = k + 1;
600 break;
601 }
602 }
603 let t = if (xs[hi] - xs[lo]).abs() < 1e-15 {
604 0.0
605 } else {
606 (xi - xs[lo]) / (xs[hi] - xs[lo])
607 };
608 env[i] = ys[lo] + t * (ys[hi] - ys[lo]);
609 }
610 env
611}
612
613#[derive(Debug, Clone, PartialEq)]
619pub struct Peak {
620 pub index: usize,
622 pub value: f64,
624 pub prominence: f64,
626 pub width: f64,
628}
629
630#[derive(Debug, Clone)]
632pub struct PeakDetection {
633 pub peaks: Vec<Peak>,
635}
636
637impl PeakDetection {
638 pub fn find_peaks(
643 signal: &[f64],
644 min_height: f64,
645 min_distance: usize,
646 min_prominence: f64,
647 ) -> Self {
648 let n = signal.len();
649 let mut candidates: Vec<usize> = (1..(n.saturating_sub(1)))
651 .filter(|&i| {
652 signal[i] > signal[i - 1] && signal[i] > signal[i + 1] && signal[i] >= min_height
653 })
654 .collect();
655
656 if min_distance > 0 {
658 candidates = apply_min_distance(signal, &candidates, min_distance);
659 }
660
661 let peaks: Vec<Peak> = candidates
663 .iter()
664 .filter_map(|&i| {
665 let prom = prominence(signal, i);
666 if prom < min_prominence {
667 return None;
668 }
669 let w = half_peak_width(signal, i, signal[i] - prom / 2.0);
670 Some(Peak {
671 index: i,
672 value: signal[i],
673 prominence: prom,
674 width: w,
675 })
676 })
677 .collect();
678
679 Self { peaks }
680 }
681
682 pub fn sorted_by_prominence(&self) -> Vec<usize> {
684 let mut sorted = self.peaks.clone();
685 sorted.sort_by(|a, b| {
686 b.prominence
687 .partial_cmp(&a.prominence)
688 .unwrap_or(std::cmp::Ordering::Equal)
689 });
690 sorted.iter().map(|p| p.index).collect()
691 }
692}
693
694fn apply_min_distance(signal: &[f64], candidates: &[usize], min_dist: usize) -> Vec<usize> {
695 let mut result: Vec<usize> = Vec::new();
696 for &c in candidates {
697 if result
698 .iter()
699 .all(|&r| (c as i64 - r as i64).unsigned_abs() as usize >= min_dist)
700 {
701 result.push(c);
702 } else {
703 if let Some(close) = result
705 .iter_mut()
706 .find(|r| ((c as i64 - **r as i64).unsigned_abs() as usize) < min_dist)
707 && signal[c] > signal[*close]
708 {
709 *close = c;
710 }
711 }
712 }
713 result
714}
715
716fn prominence(signal: &[f64], peak_idx: usize) -> f64 {
717 let n = signal.len();
718 let val = signal[peak_idx];
719 let left_min = (0..peak_idx)
721 .rev()
722 .take(100)
723 .map(|i| signal[i])
724 .fold(f64::INFINITY, f64::min);
725 let right_min = ((peak_idx + 1)..n.min(peak_idx + 101))
726 .map(|i| signal[i])
727 .fold(f64::INFINITY, f64::min);
728 let base = left_min.min(right_min);
729 if base.is_infinite() { val } else { val - base }
730}
731
732fn half_peak_width(signal: &[f64], peak_idx: usize, level: f64) -> f64 {
733 let n = signal.len();
734 let mut left = peak_idx;
735 let mut right = peak_idx;
736 while left > 0 && signal[left] > level {
737 left -= 1;
738 }
739 while right < n - 1 && signal[right] > level {
740 right += 1;
741 }
742 (right - left) as f64
743}
744
745#[derive(Debug, Clone)]
751pub struct SignalFilter;
752
753impl SignalFilter {
754 pub fn moving_average(signal: &[f64], window: usize) -> Vec<f64> {
759 let n = signal.len();
760 let hw = window / 2;
761 (0..n)
762 .map(|i| {
763 let lo = i.saturating_sub(hw);
764 let hi = (i + hw + 1).min(n);
765 let count = hi - lo;
766 signal[lo..hi].iter().sum::<f64>() / count as f64
767 })
768 .collect()
769 }
770
771 pub fn exponential_smoothing(signal: &[f64], alpha: f64) -> Vec<f64> {
775 let n = signal.len();
776 if n == 0 {
777 return vec![];
778 }
779 let alpha = alpha.clamp(1e-6, 1.0);
780 let mut out = vec![0.0_f64; n];
781 out[0] = signal[0];
782 for i in 1..n {
783 out[i] = alpha * signal[i] + (1.0 - alpha) * out[i - 1];
784 }
785 out
786 }
787
788 pub fn savitzky_golay(signal: &[f64], window: usize, poly_order: usize) -> Vec<f64> {
795 let n = signal.len();
796 if n == 0 || window > n {
797 return signal.to_vec();
798 }
799 let window = if window.is_multiple_of(2) {
800 window + 1
801 } else {
802 window
803 };
804 let hw = window / 2;
805 let poly_order = poly_order.min(window - 1);
806
807 let mut out = vec![0.0_f64; n];
808 for i in 0..n {
809 let lo = i.saturating_sub(hw);
810 let hi = (i + hw + 1).min(n);
811 let seg = &signal[lo..hi];
812 let m = seg.len();
813 let center = (i - lo) as f64;
814 let xs: Vec<f64> = (0..m).map(|k| k as f64 - center).collect();
817 out[i] = poly_fit_center(seg, &xs, poly_order);
818 }
819 out
820 }
821
822 pub fn lowpass_fir(signal: &[f64], cutoff: f64, num_taps: usize) -> Vec<f64> {
827 let num_taps = if num_taps.is_multiple_of(2) {
828 num_taps + 1
829 } else {
830 num_taps
831 };
832 let hw = num_taps / 2;
833 let kernel: Vec<f64> = (0..num_taps)
835 .map(|i| {
836 let k = i as f64 - hw as f64;
837 let sinc = if k.abs() < 1e-10 {
838 2.0 * cutoff
839 } else {
840 (2.0 * PI * cutoff * k).sin() / (PI * k)
841 };
842 let hann = 0.5 * (1.0 - (2.0 * PI * i as f64 / (num_taps - 1) as f64).cos());
843 sinc * hann
844 })
845 .collect();
846 let sum: f64 = kernel.iter().sum();
848 let kernel: Vec<f64> = kernel.iter().map(|&k| k / sum.max(1e-15)).collect();
849 let n = signal.len();
851 (0..n)
852 .map(|i| {
853 kernel
854 .iter()
855 .enumerate()
856 .map(|(k, &w)| {
857 let j = i as i64 + k as i64 - hw as i64;
858 let j = j.clamp(0, n as i64 - 1) as usize;
859 w * signal[j]
860 })
861 .sum()
862 })
863 .collect()
864 }
865}
866
867fn poly_fit_center(ys: &[f64], xs: &[f64], p: usize) -> f64 {
869 let m = ys.len();
870 let deg = p + 1;
871 let mut vt: Vec<Vec<f64>> = vec![vec![0.0; m]; deg];
873 for (j, &x) in xs.iter().enumerate() {
874 let mut xp = 1.0_f64;
875 for i in 0..deg {
876 vt[i][j] = xp;
877 xp *= x;
878 }
879 }
880 let mut vtv = vec![vec![0.0_f64; deg]; deg];
882 let mut vty = vec![0.0_f64; deg];
883 for i in 0..deg {
884 for k in 0..m {
885 vty[i] += vt[i][k] * ys[k];
886 for j in 0..deg {
887 vtv[i][j] += vt[i][k] * vt[j][k];
888 }
889 }
890 }
891 let coeffs = gaussian_elimination(&mut vtv, &mut vty);
893 coeffs.first().copied().unwrap_or(ys[m / 2])
895}
896
897fn gaussian_elimination(a: &mut Vec<Vec<f64>>, b: &mut Vec<f64>) -> Vec<f64> {
899 let n = b.len();
900 for col in 0..n {
901 let pivot = (col..n)
903 .max_by(|&i, &j| {
904 a[i][col]
905 .abs()
906 .partial_cmp(&a[j][col].abs())
907 .unwrap_or(std::cmp::Ordering::Equal)
908 })
909 .unwrap_or(col);
910 a.swap(col, pivot);
911 b.swap(col, pivot);
912 if a[col][col].abs() < 1e-15 {
913 continue;
914 }
915 let diag = a[col][col];
916 for row in (col + 1)..n {
917 let factor = a[row][col] / diag;
918 for k in col..n {
919 let v = a[col][k];
920 a[row][k] -= factor * v;
921 }
922 let bv = b[col];
923 b[row] -= factor * bv;
924 }
925 }
926 let mut x = vec![0.0_f64; n];
928 for i in (0..n).rev() {
929 let s: f64 = (i + 1..n).map(|j| a[i][j] * x[j]).sum();
930 x[i] = if a[i][i].abs() > 1e-15 {
931 (b[i] - s) / a[i][i]
932 } else {
933 0.0
934 };
935 }
936 x
937}
938
939#[cfg(test)]
944mod tests {
945 use super::*;
946
947 fn sine_wave(n: usize, freq: f64, fs: f64) -> Vec<f64> {
948 (0..n)
949 .map(|i| (2.0 * PI * freq * i as f64 / fs).sin())
950 .collect()
951 }
952
953 fn ramp(n: usize) -> Vec<f64> {
954 (0..n).map(|i| i as f64).collect()
955 }
956
957 #[test]
960 fn test_autocorr_lag0_equals_variance() {
961 let sig = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0];
962 let ac = AutoCorrelation::compute(&sig, 2, false, false);
963 let mean = sig.iter().sum::<f64>() / sig.len() as f64;
964 let var: f64 = sig.iter().map(|&x| (x - mean) * (x - mean)).sum::<f64>() / sig.len() as f64;
965 assert!((ac.at_lag(0) - var).abs() < 1e-10);
966 }
967
968 #[test]
969 fn test_autocorr_normalized_lag0_is_one() {
970 let sig: Vec<f64> = (0..20).map(|i| (i as f64).sin()).collect();
971 let ac = AutoCorrelation::compute(&sig, 5, false, true);
972 assert!((ac.at_lag(0) - 1.0_f64).abs() < 1e-10);
973 }
974
975 #[test]
976 fn test_autocorr_periodic_signal() {
977 let n = 128;
978 let sig = sine_wave(n, 8.0, n as f64);
979 let ac = AutoCorrelation::compute(&sig, n / 2, false, true);
980 let period = (n as f64 / 8.0) as usize;
982 assert!(ac.at_lag(period).abs() > 0.5);
983 }
984
985 #[test]
986 fn test_autocorr_unbiased_vs_biased() {
987 let sig = vec![1.0_f64, 2.0, 3.0, 4.0];
988 let biased = AutoCorrelation::compute(&sig, 1, false, false);
989 let unbiased = AutoCorrelation::compute(&sig, 1, true, false);
990 assert!(unbiased.at_lag(1).abs() >= biased.at_lag(1).abs());
992 }
993
994 #[test]
995 fn test_autocorr_constant_signal_zero_lag_positive() {
996 let sig = vec![3.0_f64; 10];
997 let ac = AutoCorrelation::compute(&sig, 3, false, false);
998 assert!(ac.at_lag(0) >= 0.0);
999 }
1000
1001 #[test]
1002 fn test_autocorr_dominant_lag_returns_index() {
1003 let sig: Vec<f64> = (0..20).map(|i| (i as f64).sin()).collect();
1004 let ac = AutoCorrelation::compute(&sig, 10, false, false);
1005 let dl = ac.dominant_lag();
1006 assert!((1..=10).contains(&dl));
1007 }
1008
1009 #[test]
1012 fn test_crosscorr_identical_signals() {
1013 let sig = vec![1.0_f64, 2.0, 3.0, 2.0, 1.0];
1014 let cc = CrossCorrelation::compute(&sig, &sig, 2);
1015 assert_eq!(cc.peak_lag(), 0);
1017 }
1018
1019 #[test]
1020 fn test_crosscorr_shifted_signal() {
1021 let n = 32;
1022 let delay = 2usize;
1026 let x: Vec<f64> = (0..n).map(|i| (2.0 * PI * i as f64 / 8.0).sin()).collect();
1027 let y: Vec<f64> = (0..n)
1028 .map(|i| (2.0 * PI * (i + delay) as f64 / 8.0).sin())
1029 .collect();
1030 let cc = CrossCorrelation::compute(&x, &y, 5);
1031 let lag = cc.peak_lag();
1032 assert!(
1034 (lag + delay as i64).abs() <= 1,
1035 "lag={lag}, expected near -{delay}"
1036 );
1037 }
1038
1039 #[test]
1040 fn test_crosscorr_at_lag() {
1041 let x = vec![1.0_f64, 0.0, -1.0, 0.0, 1.0];
1042 let y = vec![0.0_f64, 1.0, 0.0, -1.0, 0.0];
1043 let cc = CrossCorrelation::compute(&x, &y, 2);
1044 assert!(cc.at_lag(0).is_finite());
1046 }
1047
1048 #[test]
1049 fn test_crosscorr_out_of_range_lag() {
1050 let x = vec![1.0_f64, 2.0];
1051 let y = vec![1.0_f64, 2.0];
1052 let cc = CrossCorrelation::compute(&x, &y, 1);
1053 assert_eq!(cc.at_lag(100), 0.0);
1054 }
1055
1056 #[test]
1057 fn test_crosscorr_values_length() {
1058 let x = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0];
1059 let y = vec![5.0_f64, 4.0, 3.0, 2.0, 1.0];
1060 let cc = CrossCorrelation::compute(&x, &y, 3);
1061 assert_eq!(cc.values.len(), 7); }
1063
1064 #[test]
1067 fn test_psd_frequencies_start_at_zero() {
1068 let sig = vec![0.0_f64; 64];
1069 let psd = PowerSpectralDensity::welch(&sig, 100.0, 32, 0.5);
1070 assert!((psd.frequencies[0]).abs() < 1e-12);
1071 }
1072
1073 #[test]
1074 fn test_psd_power_nonneg() {
1075 let sig: Vec<f64> = (0..128)
1076 .map(|i| (2.0 * PI * 10.0 * i as f64 / 128.0).sin())
1077 .collect();
1078 let psd = PowerSpectralDensity::welch(&sig, 128.0, 32, 0.5);
1079 assert!(psd.power.iter().all(|&p| p >= 0.0));
1080 }
1081
1082 #[test]
1083 fn test_psd_dominant_freq() {
1084 let fs = 256.0_f64;
1085 let f0 = 16.0_f64;
1086 let sig: Vec<f64> = (0..256)
1087 .map(|i| (2.0 * PI * f0 * i as f64 / fs).sin())
1088 .collect();
1089 let psd = PowerSpectralDensity::welch(&sig, fs, 64, 0.5);
1090 let dominant = psd.dominant_frequency();
1091 assert!(
1092 (dominant - f0).abs() < 5.0,
1093 "expected ~{f0} Hz, got {dominant}"
1094 );
1095 }
1096
1097 #[test]
1098 fn test_psd_freq_resolution() {
1099 let sig = vec![0.0_f64; 64];
1100 let psd = PowerSpectralDensity::welch(&sig, 100.0, 32, 0.0);
1101 let df = psd.freq_resolution();
1102 assert!(df > 0.0);
1103 }
1104
1105 #[test]
1106 fn test_psd_total_power_nonneg() {
1107 let sig: Vec<f64> = (0..64).map(|i| i as f64).collect();
1108 let psd = PowerSpectralDensity::welch(&sig, 64.0, 16, 0.0);
1109 assert!(psd.total_power() >= 0.0);
1110 }
1111
1112 #[test]
1115 fn test_hilbert_real_part_matches_input() {
1116 let sig: Vec<f64> = (0..32).map(|i| (2.0 * PI * i as f64 / 8.0).sin()).collect();
1117 let ht = HilbertTransform::compute(&sig);
1118 for (r, s) in ht.real_part.iter().zip(sig.iter()) {
1120 assert!((r - s).abs() < 0.2, "real part deviation: {} vs {}", r, s);
1121 }
1122 }
1123
1124 #[test]
1125 fn test_hilbert_amplitude_nonneg() {
1126 let sig: Vec<f64> = (0..64).map(|i| (2.0 * PI * i as f64 / 8.0).sin()).collect();
1127 let ht = HilbertTransform::compute(&sig);
1128 assert!(ht.amplitude.iter().all(|&a| a >= 0.0));
1129 }
1130
1131 #[test]
1132 fn test_hilbert_phase_range() {
1133 let sig: Vec<f64> = (0..64).map(|i| (2.0 * PI * i as f64 / 8.0).sin()).collect();
1134 let ht = HilbertTransform::compute(&sig);
1135 assert!(
1136 ht.phase
1137 .iter()
1138 .all(|&p| (-PI - 1e-9..=PI + 1e-9).contains(&p))
1139 );
1140 }
1141
1142 #[test]
1143 fn test_hilbert_instantaneous_freq() {
1144 let fs = 64.0_f64;
1145 let f0 = 4.0_f64;
1146 let sig: Vec<f64> = (0..64)
1147 .map(|i| (2.0 * PI * f0 * i as f64 / fs).sin())
1148 .collect();
1149 let ht = HilbertTransform::compute(&sig);
1150 let ifreq = ht.instantaneous_frequency(fs);
1151 let mid_freq: f64 = ifreq[16..48].iter().sum::<f64>() / 32.0;
1153 assert!(
1154 (mid_freq - f0).abs() < 2.0,
1155 "instantaneous freq {mid_freq} vs expected {f0}"
1156 );
1157 }
1158
1159 #[test]
1160 fn test_hilbert_analytic_length() {
1161 let n = 50;
1162 let sig = vec![1.0_f64; n];
1163 let ht = HilbertTransform::compute(&sig);
1164 assert_eq!(ht.real_part.len(), n);
1165 assert_eq!(ht.imaginary_part.len(), n);
1166 assert_eq!(ht.amplitude.len(), n);
1167 assert_eq!(ht.phase.len(), n);
1168 }
1169
1170 #[test]
1173 fn test_emd_sum_of_imfs_plus_residual() {
1174 let sig: Vec<f64> = (0..64)
1175 .map(|i| (2.0 * PI * i as f64 / 8.0).sin() + 0.5 * (2.0 * PI * i as f64 / 16.0).sin())
1176 .collect();
1177 let emd = EMD::decompose(&sig, 3, 10, 0.05);
1178 let mut recon = emd.residual.clone();
1179 for imf in &emd.imfs {
1180 for (i, &v) in imf.iter().enumerate() {
1181 recon[i] += v;
1182 }
1183 }
1184 let err: f64 = sig
1185 .iter()
1186 .zip(recon.iter())
1187 .map(|(a, b)| (a - b) * (a - b))
1188 .sum::<f64>();
1189 assert!(err.sqrt() < 1.0, "reconstruction error: {err}");
1190 }
1191
1192 #[test]
1193 fn test_emd_imfs_not_empty_for_rich_signal() {
1194 let sig: Vec<f64> = (0..64)
1195 .map(|i| (2.0 * PI * i as f64 / 8.0).sin() + 0.3 * (i as f64).cos())
1196 .collect();
1197 let emd = EMD::decompose(&sig, 4, 10, 0.01);
1198 assert!(!emd.imfs.is_empty());
1199 }
1200
1201 #[test]
1202 fn test_emd_hilbert_huang_spectrum_shape() {
1203 let sig: Vec<f64> = (0..32).map(|i| (2.0 * PI * i as f64 / 8.0).sin()).collect();
1204 let emd = EMD::decompose(&sig, 2, 5, 0.1);
1205 let hhs = emd.hilbert_huang_spectrum(32.0);
1206 for row in &hhs {
1207 assert_eq!(row.len(), 32);
1208 }
1209 }
1210
1211 #[test]
1214 fn test_peaks_simple_sine() {
1215 let n = 64;
1216 let sig: Vec<f64> = (0..n).map(|i| (2.0 * PI * i as f64 / 8.0).sin()).collect();
1217 let pd = PeakDetection::find_peaks(&sig, 0.5, 4, 0.0);
1218 assert!(!pd.peaks.is_empty());
1219 }
1220
1221 #[test]
1222 fn test_peaks_heights_above_threshold() {
1223 let sig = vec![0.0_f64, 1.0, 0.5, 2.0, 0.3, 1.5, 0.0];
1224 let pd = PeakDetection::find_peaks(&sig, 0.8, 1, 0.0);
1225 for p in &pd.peaks {
1226 assert!(p.value >= 0.8_f64);
1227 }
1228 }
1229
1230 #[test]
1231 fn test_peaks_no_peaks_in_flat_signal() {
1232 let sig = vec![1.0_f64; 20];
1233 let pd = PeakDetection::find_peaks(&sig, 0.5, 1, 0.0);
1234 assert!(pd.peaks.is_empty());
1235 }
1236
1237 #[test]
1238 fn test_peaks_prominence_positive() {
1239 let sig = vec![0.0_f64, 1.0, 0.0, 2.0, 0.0];
1240 let pd = PeakDetection::find_peaks(&sig, 0.0, 1, 0.0);
1241 for p in &pd.peaks {
1242 assert!(p.prominence >= 0.0);
1243 }
1244 }
1245
1246 #[test]
1247 fn test_peaks_sorted_by_prominence() {
1248 let sig = vec![0.0_f64, 1.0, 0.0, 3.0, 0.0, 2.0, 0.0];
1249 let pd = PeakDetection::find_peaks(&sig, 0.0, 1, 0.0);
1250 let sorted = pd.sorted_by_prominence();
1251 if !sorted.is_empty() {
1253 assert_eq!(sorted[0], 3);
1254 }
1255 }
1256
1257 #[test]
1258 fn test_peaks_width_nonneg() {
1259 let sig: Vec<f64> = (0..32).map(|i| (2.0 * PI * i as f64 / 8.0).sin()).collect();
1260 let pd = PeakDetection::find_peaks(&sig, 0.0, 2, 0.0);
1261 for p in &pd.peaks {
1262 assert!(p.width >= 0.0);
1263 }
1264 }
1265
1266 #[test]
1269 fn test_moving_average_constant() {
1270 let sig = vec![5.0_f64; 20];
1271 let filtered = SignalFilter::moving_average(&sig, 5);
1272 for &v in &filtered {
1273 assert!((v - 5.0_f64).abs() < 1e-12);
1274 }
1275 }
1276
1277 #[test]
1278 fn test_moving_average_length_preserved() {
1279 let sig = ramp(30);
1280 let out = SignalFilter::moving_average(&sig, 5);
1281 assert_eq!(out.len(), 30);
1282 }
1283
1284 #[test]
1285 fn test_exp_smoothing_constant_signal() {
1286 let sig = vec![3.0_f64; 10];
1287 let out = SignalFilter::exponential_smoothing(&sig, 0.3);
1288 assert!((out.last().unwrap() - 3.0_f64).abs() < 0.1);
1290 }
1291
1292 #[test]
1293 fn test_exp_smoothing_length_preserved() {
1294 let sig = ramp(15);
1295 let out = SignalFilter::exponential_smoothing(&sig, 0.5);
1296 assert_eq!(out.len(), 15);
1297 }
1298
1299 #[test]
1300 fn test_exp_smoothing_smooths_noise() {
1301 let sig: Vec<f64> = (0..20)
1302 .map(|i| if i % 2 == 0 { 1.0_f64 } else { -1.0_f64 })
1303 .collect();
1304 let out = SignalFilter::exponential_smoothing(&sig, 0.1);
1305 let range_out = out.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
1307 - out.iter().cloned().fold(f64::INFINITY, f64::min);
1308 let range_in = 2.0_f64;
1309 assert!(range_out < range_in);
1310 }
1311
1312 #[test]
1313 fn test_savitzky_golay_constant() {
1314 let sig = vec![7.0_f64; 20];
1315 let out = SignalFilter::savitzky_golay(&sig, 5, 2);
1316 for &v in &out {
1317 assert!((v - 7.0_f64).abs() < 1e-8);
1318 }
1319 }
1320
1321 #[test]
1322 fn test_savitzky_golay_length_preserved() {
1323 let sig = ramp(25);
1324 let out = SignalFilter::savitzky_golay(&sig, 5, 2);
1325 assert_eq!(out.len(), 25);
1326 }
1327
1328 #[test]
1329 fn test_lowpass_fir_length_preserved() {
1330 let sig: Vec<f64> = (0..64).map(|i| i as f64).collect();
1331 let out = SignalFilter::lowpass_fir(&sig, 0.1, 15);
1332 assert_eq!(out.len(), 64);
1333 }
1334
1335 #[test]
1336 fn test_lowpass_fir_attenuates_high_freq() {
1337 let n = 256;
1338 let fs = 256.0_f64;
1339 let sig: Vec<f64> = (0..n)
1341 .map(|i| {
1342 (2.0 * PI * 4.0 * i as f64 / fs).sin() + (2.0 * PI * 80.0 * i as f64 / fs).sin()
1343 })
1344 .collect();
1345 let cutoff = 0.1_f64; let out = SignalFilter::lowpass_fir(&sig, cutoff, 31);
1347 let p_in: f64 = sig.iter().map(|x| x * x).sum::<f64>();
1349 let p_out: f64 = out.iter().map(|x| x * x).sum::<f64>();
1350 assert!(p_out < p_in);
1351 }
1352}