1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
use nalgebra::Complex;
use num_traits::{Float, Zero};
use rustfft::FftNum;
///
/// Resample the data to the desired number of samples using the Fourier transform.
///
/// This method is similar but not exactly equivalent to the SciPy method of resampling:
/// <https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.resample.html>
///
/// It skips some complexity of the SciPy method, such as windowing and handling odd vs. even-length signals.
///
/// Procedure:
/// 1. Convert to the frequency-domain.
/// a. If upsampling, pad higher frequency bins with 0
/// b. If downsampling, truncate higher frequency bins
/// 2. Convert back to the time-domain.
///
pub fn resample<F: Float + FftNum>(x: &[F], n: usize) -> Vec<F> {
// SciPy style 'Fourier' resampling
// 1. Compute FFT of x
// 2. Fill vec of zeros with the desired length, y.
// 3. Set the from beginning of y to the first half of x
// 4. Set the from end of y to the second half of x
// 5. Compute IFFT of y
// 6. Multiply y by (n / x.len())
// 7. Take the real part of y
// Compute FFT of x
let mut fft_planner = rustfft::FftPlanner::<F>::new();
let fft = fft_planner.plan_fft_forward(x.len());
let ifft = fft_planner.plan_fft_inverse(n);
let scratch_length = std::cmp::max(
fft.get_inplace_scratch_len(),
ifft.get_inplace_scratch_len(),
);
let mut scratch = vec![Complex::zero(); scratch_length];
let mut x = x
.into_iter()
.map(|x| Complex::new(*x, F::zero()))
.collect::<Vec<_>>();
fft.process_with_scratch(&mut x, &mut scratch);
// Fill y with halfs of x
let mut y = vec![Complex::zero(); n];
let bins = std::cmp::min(x.len(), n);
let half_spectrum = bins / 2;
y[..half_spectrum].copy_from_slice(&x[..half_spectrum]);
y[n - half_spectrum..].copy_from_slice(&x[x.len() - half_spectrum..]);
// Compute iFFT of y
ifft.process_with_scratch(&mut y, &mut scratch);
// Take the scaled real domain as the resampled result
let scale_factor = F::from(1.0 / x.len() as f64).unwrap();
let y = y.iter().map(|x| x.re * scale_factor).collect::<Vec<_>>();
y
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use rand::Rng;
use super::*;
#[test]
#[should_panic]
fn can_resample_like_scipy() {
let x = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.];
let y = resample(&x, 5);
let expected = vec![3., 2.18649851, 5.01849831, 5.98150169, 8.81350149];
assert_eq!(y.len(), expected.len());
// Fails due to slight algorithmic differences
println!("y: {:?}, scipy: {:?}", y, expected);
for (y, expected) in y.iter().zip(expected.iter()) {
assert_relative_eq!(y, expected, epsilon = 1e-10);
}
}
#[test]
fn can_resample_to_exact_number() {
// Randomly generate 1000 vectors of length (10,1000)
// Resample each to length 100
// Check that each resampled vector is length 100
let mut rng = rand::thread_rng();
for i in (0..100) {
let len = rng.gen_range((10..50));
let x = (0..len)
.map(|_| rng.gen_range((-100.0..100.)))
.collect::<Vec<_>>();
let y = resample(&x, 100);
assert_eq!(y.len(), 100);
}
for i in (0..50) {
let len = rng.gen_range((200..10000));
let target_len = rng.gen_range((50..50000));
let x = (0..len)
.map(|_| rng.gen_range((-100.0..100.)))
.collect::<Vec<_>>();
let y = resample(&x, target_len);
assert_eq!(y.len(), target_len);
}
}
}