From 5159c10c48f0e8efc489acc5ea99d79dcbb5de33 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 29 Apr 2024 22:13:00 -0400 Subject: [PATCH 01/38] Add draft implmentation of R2C FFT --- src/lib.rs | 3 +- src/r2c_kernels.rs | 178 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/r2c_kernels.rs diff --git a/src/lib.rs b/src/lib.rs index c65a74f..235279c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ pub mod cobra; mod kernels; pub mod options; pub mod planner; +mod r2c_kernels; mod twiddles; macro_rules! impl_fft_for { @@ -156,8 +157,8 @@ mod tests { use std::ops::Range; use utilities::assert_float_closeness; - use utilities::rustfft::num_complex::Complex; use utilities::rustfft::FftPlanner; + use utilities::rustfft::num_complex::Complex; use super::*; diff --git a/src/r2c_kernels.rs b/src/r2c_kernels.rs new file mode 100644 index 0000000..e8c0ec5 --- /dev/null +++ b/src/r2c_kernels.rs @@ -0,0 +1,178 @@ +use crate::cobra::cobra_apply; +use crate::Direction; +use crate::filter_twiddles; +use crate::options::Options; +use crate::planner::{Planner32, Planner64}; + +use crate::planner::{Planner32, Planner64}; +macro_rules! impl_r2c_fft_for { + ($func_name:ident, $precision:ty, $planner:ty, $opts_and_plan:ident) => { + /// FFT -- Decimation in Frequency. This is just the decimation-in-time algorithm, reversed. + /// This call to FFT is run, in-place. + /// The input should be provided in normal order, and then the modified input is bit-reversed. + /// + /// # Panics + /// + /// Panics if `reals.len() != imags.len()`, or if the input length is _not_ a power of 2. + /// + /// ## References + /// + pub fn $func_name(reals: &mut [$precision], direction: Direction) -> Vec<$precision> { + assert_eq!( + reals.len(), + imags.len(), + "real and imaginary inputs must be of equal size, but got: {} {}", + reals.len(), + imags.len() + ); + + let mut planner = <$planner>::new(reals.len(), direction); + assert!( + planner.num_twiddles().is_power_of_two() + && planner.num_twiddles() == reals.len() / 2 + ); + + let opts = Options::guess_options(reals.len()); + $opts_and_plan(reals, imags, &opts, &mut planner); + } + }; +} + +macro_rules! impl_r2c_fft_with_opts_and_plan_for { + ($func_name:ident, $precision:ty, $planner:ty, $simd_butterfly_kernel:ident, $lanes:literal) => { + /// Same as [fft], but also accepts [`Options`] that control optimization strategies, as well as + /// a [`Planner`] in the case that this FFT will need to be run multiple times. + /// + /// `fft` automatically guesses the best strategy for a given input, + /// so you only need to call this if you are tuning performance for a specific hardware platform. + /// + /// In addition, `fft` automatically creates a planner to be used. In the case that you plan + /// on running an FFT many times on inputs of the same size, use this function with the pre-built + /// [`Planner`]. + /// + /// # Panics + /// + /// Panics if `reals.len() != imags.len()`, or if the input length is _not_ a power of 2. + #[multiversion::multiversion( + targets("x86_64+avx512f+avx512bw+avx512cd+avx512dq+avx512vl", // x86_64-v4 + "x86_64+avx2+fma", // x86_64-v3 + "x86_64+sse4.2", // x86_64-v2 + "x86+avx512f+avx512bw+avx512cd+avx512dq+avx512vl", + "x86+avx2+fma", + "x86+sse4.2", + "x86+sse2", + ))] + pub fn $func_name( + reals: &mut [$precision], + imags: &mut [$precision], + opts: &Options, + planner: &mut $planner, + ) { + assert!(reals.len() == imags.len() && reals.len().is_power_of_two()); + let n: usize = reals.len().ilog2() as usize; + + let twiddles_re = &mut planner.twiddles_re; + let twiddles_im = &mut planner.twiddles_im; + + // We shouldn't be able to execute FFT if the # of twiddles isn't equal to the distance + // between pairs + assert!(twiddles_re.len() == reals.len() / 2 && twiddles_im.len() == imags.len() / 2); + + for t in (0..n).rev() { + let dist = 1 << t; + let chunk_size = dist << 1; + + if chunk_size > 4 { + if t < n - 1 { + filter_twiddles(twiddles_re, twiddles_im); + } + if chunk_size >= $lanes * 2 { + $simd_butterfly_kernel(reals, imags, twiddles_re, twiddles_im, dist); + } else { + fft_r2c_chunk_n(reals, imags, twiddles_re, twiddles_im, dist); + } + } else if chunk_size == 2 { + fft_r2c_chunk_2(reals, imags); + } else if chunk_size == 4 { + fft_r2c_chunk_4(reals, imags); + } + } + + if opts.multithreaded_bit_reversal { + std::thread::scope(|s| { + s.spawn(|| cobra_apply(reals, n)); + s.spawn(|| cobra_apply(imags, n)); + }); + } else { + cobra_apply(reals, n); + cobra_apply(imags, n); + } + } + }; +} + +impl_r2c_fft_with_opts_and_plan_for!( + fft_64_r2c_with_opts_and_plan, + f64, + Planner64, + fft_64_r2c_chunk_n_simd, + 8 +); + +impl_r2c_fft_with_opts_and_plan_for!( + fft_32_r2c_with_opts_and_plan, + f32, + Planner32, + fft_32_r2c_chunk_n_simd, + 16 +); + +impl_r2c_fft_for!(fft_64_r2c, f64, Planner64, fft_64_r2c_with_opts_and_plan); +impl_r2c_fft_for!(fft_32_r2c, f32, Planner32, fft_32_r2c_with_opts_and_plan); + +macro_rules! fft_r2c_butterfly_n_simd { + ($func_name:ident, $precision:ty, $lanes:literal, $simd_vector:ty) => { + #[inline] + pub fn $func_name( + reals: &mut [$precision], + imags: &mut [$precision], + twiddles_re: &[$precision], + twiddles_im: &[$precision], + dist: usize, + ) { + let chunk_size = dist << 1; + assert!(chunk_size >= $lanes * 2); + reals + .chunks_exact_mut(chunk_size) + .zip(imags.chunks_exact_mut(chunk_size)) + .for_each(|(reals_chunk, imags_chunk)| { + let (reals_s0, reals_s1) = reals_chunk.split_at_mut(dist); + let (imags_s0, imags_s1) = imags_chunk.split_at_mut(dist); + + reals_s0 + .chunks_exact_mut($lanes) + .zip(reals_s1.chunks_exact_mut($lanes)) + .zip(imags_s0.chunks_exact_mut($lanes)) + .zip(imags_s1.chunks_exact_mut($lanes)) + .zip(twiddles_re.chunks_exact($lanes)) + .zip(twiddles_im.chunks_exact($lanes)) + .for_each(|(((((re_s0, re_s1), im_s0), im_s1), w_re), w_im)| { + let real_c0 = <$simd_vector>::from_slice(re_s0); + let real_c1 = <$simd_vector>::from_slice(re_s1); + let tw_re = <$simd_vector>::from_slice(tw_re); + let tw_im = <$simd_vector>::from_slice(tw_im); + + re_s0.copy_from_slice((real_c0 + real_c1).as_array()); + + let temp_real = real_c0 - real_c1; + let twiddled_real = temp_real * tw_re; + let twiddled_imag = temp_real * tw_im; + + re_s1.copy_from_slice(twiddled_real.as_array()); + im_s0.copy_from_slice(twiddled_imag.as_array()); + im_s1.copy_from_slice((-twiddled_imag).as_array()); // Negative due to symmetry + }); + }); + } + }; +} From a227dad9bdd9919d36ea7631723e9e8f0aa19d4c Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 13 May 2024 11:32:03 -0400 Subject: [PATCH 02/38] Add skeleton of impl based on packing complex nums --- src/lib.rs | 32 ++++++++ src/r2c_kernels.rs | 178 --------------------------------------------- 2 files changed, 32 insertions(+), 178 deletions(-) delete mode 100644 src/r2c_kernels.rs diff --git a/src/lib.rs b/src/lib.rs index 5756aba..5ccd6f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,6 +173,20 @@ impl_fft_with_opts_and_plan_for!( 16 ); +// TODO: make this generic over f64/f32 using macro +/// Real-to-Complex FFT `f64`. Note the input is a real-valued signal. +pub fn fft_64_r2c(signal: &mut [f64]) -> (Vec, Vec) { + let n = signal.len(); + let (mut reals, mut imags): (Vec, Vec) = + signal.chunks_exact(2).map(|c| (c[0], c[1])).unzip(); + + fft_64(&mut reals, &mut imags, Direction::Forward); + + // TODO: implement/fix untangle + todo!(); + (reals, imags) +} + #[cfg(test)] mod tests { use std::ops::Range; @@ -308,4 +322,22 @@ mod tests { } } } + + #[test] + fn fft_r2c_vs_c2c() { + let n = 4; + let big_n = 1 << n; + let mut reals: Vec = (1..=big_n).map(|i| i as f64).collect(); + + let (signal_re, signal_im) = fft_64_r2c(&mut reals); + println!("{:?}", signal_re); + println!("{:?}\n", signal_im); + + let mut reals: Vec = (1..=big_n).map(|i| i as f64).collect(); + let mut imags = vec![0.0; big_n]; + fft_64(&mut reals, &mut imags, Direction::Forward); + + println!("{:?}", reals); + println!("{:?}\n", imags); + } } diff --git a/src/r2c_kernels.rs b/src/r2c_kernels.rs deleted file mode 100644 index e8c0ec5..0000000 --- a/src/r2c_kernels.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::cobra::cobra_apply; -use crate::Direction; -use crate::filter_twiddles; -use crate::options::Options; -use crate::planner::{Planner32, Planner64}; - -use crate::planner::{Planner32, Planner64}; -macro_rules! impl_r2c_fft_for { - ($func_name:ident, $precision:ty, $planner:ty, $opts_and_plan:ident) => { - /// FFT -- Decimation in Frequency. This is just the decimation-in-time algorithm, reversed. - /// This call to FFT is run, in-place. - /// The input should be provided in normal order, and then the modified input is bit-reversed. - /// - /// # Panics - /// - /// Panics if `reals.len() != imags.len()`, or if the input length is _not_ a power of 2. - /// - /// ## References - /// - pub fn $func_name(reals: &mut [$precision], direction: Direction) -> Vec<$precision> { - assert_eq!( - reals.len(), - imags.len(), - "real and imaginary inputs must be of equal size, but got: {} {}", - reals.len(), - imags.len() - ); - - let mut planner = <$planner>::new(reals.len(), direction); - assert!( - planner.num_twiddles().is_power_of_two() - && planner.num_twiddles() == reals.len() / 2 - ); - - let opts = Options::guess_options(reals.len()); - $opts_and_plan(reals, imags, &opts, &mut planner); - } - }; -} - -macro_rules! impl_r2c_fft_with_opts_and_plan_for { - ($func_name:ident, $precision:ty, $planner:ty, $simd_butterfly_kernel:ident, $lanes:literal) => { - /// Same as [fft], but also accepts [`Options`] that control optimization strategies, as well as - /// a [`Planner`] in the case that this FFT will need to be run multiple times. - /// - /// `fft` automatically guesses the best strategy for a given input, - /// so you only need to call this if you are tuning performance for a specific hardware platform. - /// - /// In addition, `fft` automatically creates a planner to be used. In the case that you plan - /// on running an FFT many times on inputs of the same size, use this function with the pre-built - /// [`Planner`]. - /// - /// # Panics - /// - /// Panics if `reals.len() != imags.len()`, or if the input length is _not_ a power of 2. - #[multiversion::multiversion( - targets("x86_64+avx512f+avx512bw+avx512cd+avx512dq+avx512vl", // x86_64-v4 - "x86_64+avx2+fma", // x86_64-v3 - "x86_64+sse4.2", // x86_64-v2 - "x86+avx512f+avx512bw+avx512cd+avx512dq+avx512vl", - "x86+avx2+fma", - "x86+sse4.2", - "x86+sse2", - ))] - pub fn $func_name( - reals: &mut [$precision], - imags: &mut [$precision], - opts: &Options, - planner: &mut $planner, - ) { - assert!(reals.len() == imags.len() && reals.len().is_power_of_two()); - let n: usize = reals.len().ilog2() as usize; - - let twiddles_re = &mut planner.twiddles_re; - let twiddles_im = &mut planner.twiddles_im; - - // We shouldn't be able to execute FFT if the # of twiddles isn't equal to the distance - // between pairs - assert!(twiddles_re.len() == reals.len() / 2 && twiddles_im.len() == imags.len() / 2); - - for t in (0..n).rev() { - let dist = 1 << t; - let chunk_size = dist << 1; - - if chunk_size > 4 { - if t < n - 1 { - filter_twiddles(twiddles_re, twiddles_im); - } - if chunk_size >= $lanes * 2 { - $simd_butterfly_kernel(reals, imags, twiddles_re, twiddles_im, dist); - } else { - fft_r2c_chunk_n(reals, imags, twiddles_re, twiddles_im, dist); - } - } else if chunk_size == 2 { - fft_r2c_chunk_2(reals, imags); - } else if chunk_size == 4 { - fft_r2c_chunk_4(reals, imags); - } - } - - if opts.multithreaded_bit_reversal { - std::thread::scope(|s| { - s.spawn(|| cobra_apply(reals, n)); - s.spawn(|| cobra_apply(imags, n)); - }); - } else { - cobra_apply(reals, n); - cobra_apply(imags, n); - } - } - }; -} - -impl_r2c_fft_with_opts_and_plan_for!( - fft_64_r2c_with_opts_and_plan, - f64, - Planner64, - fft_64_r2c_chunk_n_simd, - 8 -); - -impl_r2c_fft_with_opts_and_plan_for!( - fft_32_r2c_with_opts_and_plan, - f32, - Planner32, - fft_32_r2c_chunk_n_simd, - 16 -); - -impl_r2c_fft_for!(fft_64_r2c, f64, Planner64, fft_64_r2c_with_opts_and_plan); -impl_r2c_fft_for!(fft_32_r2c, f32, Planner32, fft_32_r2c_with_opts_and_plan); - -macro_rules! fft_r2c_butterfly_n_simd { - ($func_name:ident, $precision:ty, $lanes:literal, $simd_vector:ty) => { - #[inline] - pub fn $func_name( - reals: &mut [$precision], - imags: &mut [$precision], - twiddles_re: &[$precision], - twiddles_im: &[$precision], - dist: usize, - ) { - let chunk_size = dist << 1; - assert!(chunk_size >= $lanes * 2); - reals - .chunks_exact_mut(chunk_size) - .zip(imags.chunks_exact_mut(chunk_size)) - .for_each(|(reals_chunk, imags_chunk)| { - let (reals_s0, reals_s1) = reals_chunk.split_at_mut(dist); - let (imags_s0, imags_s1) = imags_chunk.split_at_mut(dist); - - reals_s0 - .chunks_exact_mut($lanes) - .zip(reals_s1.chunks_exact_mut($lanes)) - .zip(imags_s0.chunks_exact_mut($lanes)) - .zip(imags_s1.chunks_exact_mut($lanes)) - .zip(twiddles_re.chunks_exact($lanes)) - .zip(twiddles_im.chunks_exact($lanes)) - .for_each(|(((((re_s0, re_s1), im_s0), im_s1), w_re), w_im)| { - let real_c0 = <$simd_vector>::from_slice(re_s0); - let real_c1 = <$simd_vector>::from_slice(re_s1); - let tw_re = <$simd_vector>::from_slice(tw_re); - let tw_im = <$simd_vector>::from_slice(tw_im); - - re_s0.copy_from_slice((real_c0 + real_c1).as_array()); - - let temp_real = real_c0 - real_c1; - let twiddled_real = temp_real * tw_re; - let twiddled_imag = temp_real * tw_im; - - re_s1.copy_from_slice(twiddled_real.as_array()); - im_s0.copy_from_slice(twiddled_imag.as_array()); - im_s1.copy_from_slice((-twiddled_imag).as_array()); // Negative due to symmetry - }); - }); - } - }; -} From 22ed8c9dd3e07c31243ea84fad3555bf86823fa8 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 13 May 2024 11:35:30 -0400 Subject: [PATCH 03/38] Remove old mod --- src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5ccd6f1..2a704ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,6 @@ pub mod cobra; mod kernels; pub mod options; pub mod planner; -mod r2c_kernels; mod twiddles; macro_rules! impl_fft_for { @@ -191,9 +190,9 @@ pub fn fft_64_r2c(signal: &mut [f64]) -> (Vec, Vec) { mod tests { use std::ops::Range; - use utilities::{assert_float_closeness, gen_random_signal}; - use utilities::rustfft::FftPlanner; use utilities::rustfft::num_complex::Complex; + use utilities::rustfft::FftPlanner; + use utilities::{assert_float_closeness, gen_random_signal}; use super::*; From de600addec12288e899956ecbaf3e01923f20f8f Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 20 May 2024 10:49:51 -0400 Subject: [PATCH 04/38] Initial implementation of r2c fft --- src/lib.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2a704ff..1db5150 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,8 @@ #![forbid(unsafe_code)] #![feature(portable_simd, avx512_target_feature)] +use std::f64::consts::PI; + use crate::cobra::cobra_apply; use crate::kernels::{ fft_32_chunk_n_simd, fft_64_chunk_n_simd, fft_chunk_2, fft_chunk_4, fft_chunk_n, @@ -172,17 +174,80 @@ impl_fft_with_opts_and_plan_for!( 16 ); +fn compute_twiddle_factors(big_n: usize) -> (Vec, Vec) { + let half_n = big_n / 2; + let mut real_parts = Vec::with_capacity(half_n); + let mut imag_parts = Vec::with_capacity(half_n); + + for k in 0..half_n { + let angle = -2.0 * PI * (k as f64) / (big_n as f64); + real_parts.push(angle.cos()); + imag_parts.push(angle.sin()); + } + + (real_parts, imag_parts) +} + // TODO: make this generic over f64/f32 using macro /// Real-to-Complex FFT `f64`. Note the input is a real-valued signal. -pub fn fft_64_r2c(signal: &mut [f64]) -> (Vec, Vec) { - let n = signal.len(); +pub fn fft_64_r2c(signal: &[f64]) -> (Vec, Vec) { + let big_n = signal.len(); + + // z[n] = x_{e}[n] + j * x_{o}[n] let (mut reals, mut imags): (Vec, Vec) = signal.chunks_exact(2).map(|c| (c[0], c[1])).unzip(); + // Z[k] = DFT{z} fft_64(&mut reals, &mut imags, Direction::Forward); - // TODO: implement/fix untangle - todo!(); + let mut x_evens_re = vec![0.0; big_n / 2]; + let mut x_evens_im = vec![0.0; big_n / 2]; + + for k in 0..big_n / 2 { + let re = 0.5 * (reals[k] + reals[big_n / 2 - 1 - k]); + let im = 0.5 * (imags[k] - imags[big_n / 2 - 1 - k]); + x_evens_re[k] = re; + x_evens_im[k] = im; + } + + let mut x_odds_re = vec![0.0; big_n / 2]; + let mut x_odds_im = vec![0.0; big_n / 2]; + + // -i * ((a + ib) - (c - id)) = -(b + d) - i(a - c) + for k in 0..big_n / 2 { + let re = 0.5 * (-imags[k] - imags[big_n / 2 - 1 - k]); + let im = 0.5 * (-reals[k] + reals[k]); + x_odds_re[k] = re; + x_odds_im[k] = im; + } + + // 7. X[k] = X_{e}[k] + X_{o}[e] * e^{-2*j*pi*(k/N)}, for k \in {0, ..., N/2 - 1} + let (twiddles_re, twiddles_im) = compute_twiddle_factors(big_n); + for k in 0..big_n / 2 { + let a = x_evens_re[k]; + let b = x_evens_im[k]; + let c = x_odds_re[k]; + let d = x_odds_im[k]; + let g = twiddles_re[k]; + let h = twiddles_im[k]; + + // (a + ib) + (c + id) * (g + ih) = (a + cg - dh) + i(b + ch + dg) + reals[k] = a + c * g - d * h; + imags[k] = b + c * h + d * g; + } + + // 8. X[k] = X_e[k] - X_{o}[k], for k = N/2 + let k = big_n / 2 - 1; + reals[k] = x_evens_re[k] - x_odds_re[k]; + imags[k] = x_evens_im[k] - x_odds_im[k]; + + // 9. X[k] = X*[N - k], for k \in {N/2 + 1, ..., N - 1} + for k in (big_n / 2 + 1)..big_n { + eprintln!("k: {k} and {}", big_n - k - 1); + reals[k] = reals[big_n - k - 1]; + imags[k] = -imags[big_n - k - 1]; + } + (reals, imags) } @@ -190,9 +255,9 @@ pub fn fft_64_r2c(signal: &mut [f64]) -> (Vec, Vec) { mod tests { use std::ops::Range; - use utilities::rustfft::num_complex::Complex; - use utilities::rustfft::FftPlanner; use utilities::{assert_float_closeness, gen_random_signal}; + use utilities::rustfft::FftPlanner; + use utilities::rustfft::num_complex::Complex; use super::*; From aaab05c209faff03441711077002fee6646cbb92 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Fri, 24 May 2024 12:24:37 -0400 Subject: [PATCH 05/38] Add implementation based on working python version --- src/fft.rs | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 135 insertions(+) create mode 100644 src/fft.rs diff --git a/src/fft.rs b/src/fft.rs new file mode 100644 index 0000000..793eed9 --- /dev/null +++ b/src/fft.rs @@ -0,0 +1,134 @@ +//! Implementation of Real valued FFT +use std::f64::consts::PI; + +use crate::{compute_twiddle_factors, Direction}; +use crate::fft_64; + +fn precompute_chirp(n: usize, m: usize) -> (Vec, Vec) { + let mut chirp_re = vec![0.0; m]; + let mut chirp_im = vec![0.0; m]; + for k in 0..n { + let angle = PI * (k * k) as f64 / n as f64; + chirp_re[k] = angle.cos(); + chirp_im[k] = -angle.sin(); + } + for k in n..m { + chirp_re[k] = 0.0; + chirp_im[k] = 0.0; + } + (chirp_re, chirp_im) +} + +pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { + let n = input_re.len(); + + // Splitting odd and even + let (mut z_even, mut z_odd): (Vec<_>, Vec<_>) = + input_re.chunks_exact(2).map(|c| (c[0], c[1])).unzip(); + + // Z = np.fft.fft(z) + fft_64(&mut z_even, &mut z_odd, Direction::Forward); + + // take care of the np.flip() + let mut z_even_min_conj: Vec<_> = z_even.iter().copied().rev().collect(); + + let mut z_odd_min_conj: Vec<_> = z_odd.iter().copied().rev().collect(); + + // Zminconj = np.roll(np.flip(Z), 1).conj() + // now roll both by 1 + z_even_min_conj.rotate_right(1); + z_odd_min_conj.rotate_right(1); + + // the conj() call can be resolved by negating every imaginary component + for zo in z_odd_min_conj.iter_mut() { + *zo = -(*zo); + } + + // Zx = 0.5 * (Z + Zminconj) + let (z_x_re, z_x_im): (Vec<_>, Vec<_>) = z_even + .iter() + .zip(z_odd.iter()) + .zip(z_even_min_conj.iter()) + .zip(z_odd_min_conj.iter()) + .map(|(((ze, zo), ze_mc), zo_mc)| { + let a = 0.5 * (ze + ze_mc); + let b = 0.5 * (zo + zo_mc); + (a, b) + }) + .unzip(); + + // Zy = -0.5j * (Z - Zminconj) + let (z_y_re, z_y_im): (Vec<_>, Vec<_>) = z_even + .iter() + .zip(z_odd.iter()) + .zip(z_even_min_conj.iter()) + .zip(z_odd_min_conj.iter()) + .map(|(((ze, zo), ze_mc), zo_mc)| { + let a = ze - ze_mc; + let b = zo - zo_mc; + + // 0.5i (a + ib) = 0.5i * a - 0.5 * b + (-0.5 * b, 0.5 * a) + }) + .unzip(); + + let (twiddle_re, twiddle_im) = compute_twiddle_factors(n); // np.exp(-1j * 2 * math.pi * np.arange(N//2) / N) + + // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) + let mut z_all_re = Vec::new(); + let mut z_all_im = Vec::new(); + + for i in 0..n / 2 { + let zx_re = z_x_re[i]; + let zx_im = z_x_im[i]; + let zy_re = z_y_re[i]; + let zy_im = z_y_im[i]; + let w_re = twiddle_re[i]; + let w_im = twiddle_im[i]; + + let wz_re = w_re * zy_re - w_im * zy_im; + let wz_im = w_re * zy_im + w_im * zy_re; + + // Zx + W * Zy + z_all_re.push(zx_re + wz_re); + z_all_im.push(zx_im + wz_im); + } + + for i in 0..n / 2 { + let zx_re = z_x_re[i]; + let zx_im = z_x_im[i]; + let zy_re = z_y_re[i]; + let zy_im = z_y_im[i]; + let w_re = twiddle_re[i]; + let w_im = twiddle_im[i]; + + let wz_re = w_re * zy_re - w_im * zy_im; + let wz_im = w_re * zy_im + w_im * zy_re; + + // Zx - W * Zy + z_all_re.push(zx_re - wz_re); + z_all_im.push(zx_im - wz_im); + } + + // return Zall + (z_all_re, z_all_im) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn r2c_vs_c2c() { + let mut input_re: Vec<_> = (1..=16).map(|i| i as f64).collect(); // Length is 7, which is a prime number + let mut input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example + + let (re, im) = real_fft(&mut input_re); + println!("actual:\n{:?}\n{:?}\n", re, im); + + input_re = (1..=16).map(|i| i as f64).collect(); + input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example + fft_64(&mut input_re, &mut input_im, Direction::Forward); + println!("expected:\n{:?}\n{:?}\n", input_re, input_im); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1db5150..758e887 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ use crate::planner::{Direction, Planner32, Planner64}; use crate::twiddles::filter_twiddles; pub mod cobra; +pub mod fft; mod kernels; pub mod options; pub mod planner; From 85438e328b0aecd64a081d846525bc14cd37075e Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Fri, 24 May 2024 12:25:41 -0400 Subject: [PATCH 06/38] Fix formatting --- src/fft.rs | 2 +- src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 793eed9..759fc7d 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,8 +1,8 @@ //! Implementation of Real valued FFT use std::f64::consts::PI; -use crate::{compute_twiddle_factors, Direction}; use crate::fft_64; +use crate::{compute_twiddle_factors, Direction}; fn precompute_chirp(n: usize, m: usize) -> (Vec, Vec) { let mut chirp_re = vec![0.0; m]; diff --git a/src/lib.rs b/src/lib.rs index 758e887..6f43313 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -256,9 +256,9 @@ pub fn fft_64_r2c(signal: &[f64]) -> (Vec, Vec) { mod tests { use std::ops::Range; - use utilities::{assert_float_closeness, gen_random_signal}; - use utilities::rustfft::FftPlanner; use utilities::rustfft::num_complex::Complex; + use utilities::rustfft::FftPlanner; + use utilities::{assert_float_closeness, gen_random_signal}; use super::*; From c9f79cb378b45c7071cbabcc939f7783525a0cd2 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Tue, 28 May 2024 11:12:02 -0400 Subject: [PATCH 07/38] Add working implementation of R2C FFT --- src/fft.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 759fc7d..b1c3a36 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -19,6 +19,7 @@ fn precompute_chirp(n: usize, m: usize) -> (Vec, Vec) { (chirp_re, chirp_im) } +/// Implementation of Real-Valued FFT pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { let n = input_re.len(); @@ -44,6 +45,20 @@ pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { *zo = -(*zo); } + println!("Z even: {z_even:?}\nZ odd: {z_odd:?}\n"); + println!("Z even min conj : {z_even_min_conj:?}\nZ odd min conj: {z_odd_min_conj:?}"); + + /* + [ 64. +72.j -27.3137085+11.3137085j -16. +0.j + -11.3137085 -4.6862915j -8. -8.j -4.6862915-11.3137085j + 0. -16.j 11.3137085-27.3137085j] + + [ 64. -72.j 11.3137085+27.3137085j 0. +16.j + -4.6862915+11.3137085j -8. +8.j -11.3137085 +4.6862915j + -16. -0.j -27.3137085-11.3137085j] + + */ + // Zx = 0.5 * (Z + Zminconj) let (z_x_re, z_x_im): (Vec<_>, Vec<_>) = z_even .iter() @@ -57,6 +72,8 @@ pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { }) .unzip(); + println!("Zx reals: {z_x_re:?}\nZx imags: {z_x_im:?}\n"); + // Zy = -0.5j * (Z - Zminconj) let (z_y_re, z_y_im): (Vec<_>, Vec<_>) = z_even .iter() @@ -67,11 +84,25 @@ pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { let a = ze - ze_mc; let b = zo - zo_mc; - // 0.5i (a + ib) = 0.5i * a - 0.5 * b - (-0.5 * b, 0.5 * a) + // -0.5i (a + ib) = -0.5i * a + 0.5 * b + (0.5 * b, -0.5 * a) }) .unzip(); + /* + Zx: + + [64. +0.j -8.+19.3137085j -8. +8.j -8. +3.3137085j + -8. +0.j -8. -3.3137085j -8. -8.j -8.-19.3137085j] + + Zy: + + [72. -0.j -8.+19.3137085j -8. +8.j -8. +3.3137085j + -8. +0.j -8. -3.3137085j -8. -8.j -8.-19.3137085j] + */ + + println!("Zy reals: {z_y_re:?}\nZy imags: {z_y_im:?}"); + let (twiddle_re, twiddle_im) = compute_twiddle_factors(n); // np.exp(-1j * 2 * math.pi * np.arange(N//2) / N) // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) @@ -116,19 +147,30 @@ pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { #[cfg(test)] mod tests { + use utilities::assert_float_closeness; + use super::*; #[test] fn r2c_vs_c2c() { - let mut input_re: Vec<_> = (1..=16).map(|i| i as f64).collect(); // Length is 7, which is a prime number - let mut input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example + let n = 4; + let big_n = 1 << 4; + let mut input_re: Vec<_> = (1..=big_n).map(|i| i as f64).collect(); // Length is 7, which is a prime number - let (re, im) = real_fft(&mut input_re); - println!("actual:\n{:?}\n{:?}\n", re, im); + let (r2c_res_reals, r2c_res_imags) = real_fft(&mut input_re); - input_re = (1..=16).map(|i| i as f64).collect(); - input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example + input_re = (1..=big_n).map(|i| i as f64).collect(); + let mut input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example fft_64(&mut input_re, &mut input_im, Direction::Forward); - println!("expected:\n{:?}\n{:?}\n", input_re, input_im); + + r2c_res_reals + .iter() + .zip(r2c_res_imags.iter()) + .zip(input_re.iter()) + .zip(input_im.iter()) + .for_each(|(((a_re, a_im), e_re), e_im)| { + assert_float_closeness(*a_re, *e_re, 1e-6); + assert_float_closeness(*a_im, *e_im, 1e-6); + }); } } From 22a7f1d2f8e88da483235683b4b39aa511583e31 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Wed, 29 May 2024 11:23:03 -0400 Subject: [PATCH 08/38] Implement r2c test macro for f32 and f64 --- src/fft.rs | 137 ++++++++++++++++++++++++----------------------------- src/lib.rs | 97 ------------------------------------- 2 files changed, 63 insertions(+), 171 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index b1c3a36..7751850 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,27 +1,28 @@ //! Implementation of Real valued FFT -use std::f64::consts::PI; +use num_traits::Float; use crate::fft_64; -use crate::{compute_twiddle_factors, Direction}; - -fn precompute_chirp(n: usize, m: usize) -> (Vec, Vec) { - let mut chirp_re = vec![0.0; m]; - let mut chirp_im = vec![0.0; m]; - for k in 0..n { - let angle = PI * (k * k) as f64 / n as f64; - chirp_re[k] = angle.cos(); - chirp_im[k] = -angle.sin(); +use crate::Direction; + +fn compute_twiddle_factors(big_n: usize) -> (Vec, Vec) { + let half_n = big_n / 2; + let mut real_parts = Vec::with_capacity(half_n); + let mut imag_parts = Vec::with_capacity(half_n); + let two: T = T::from(2.0).unwrap(); + let pi: T = T::from(std::f64::consts::PI).unwrap(); + + for k in 0..half_n { + let angle = -two * pi * T::from(k).unwrap() / T::from(big_n).unwrap(); + real_parts.push(angle.cos()); + imag_parts.push(angle.sin()); } - for k in n..m { - chirp_re[k] = 0.0; - chirp_im[k] = 0.0; - } - (chirp_re, chirp_im) + + (real_parts, imag_parts) } /// Implementation of Real-Valued FFT -pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { - let n = input_re.len(); +pub fn r2c_fft_f64(input_re: &[f64]) -> (Vec, Vec) { + let big_n = input_re.len(); // Splitting odd and even let (mut z_even, mut z_odd): (Vec<_>, Vec<_>) = @@ -45,20 +46,6 @@ pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { *zo = -(*zo); } - println!("Z even: {z_even:?}\nZ odd: {z_odd:?}\n"); - println!("Z even min conj : {z_even_min_conj:?}\nZ odd min conj: {z_odd_min_conj:?}"); - - /* - [ 64. +72.j -27.3137085+11.3137085j -16. +0.j - -11.3137085 -4.6862915j -8. -8.j -4.6862915-11.3137085j - 0. -16.j 11.3137085-27.3137085j] - - [ 64. -72.j 11.3137085+27.3137085j 0. +16.j - -4.6862915+11.3137085j -8. +8.j -11.3137085 +4.6862915j - -16. -0.j -27.3137085-11.3137085j] - - */ - // Zx = 0.5 * (Z + Zminconj) let (z_x_re, z_x_im): (Vec<_>, Vec<_>) = z_even .iter() @@ -72,8 +59,6 @@ pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { }) .unzip(); - println!("Zx reals: {z_x_re:?}\nZx imags: {z_x_im:?}\n"); - // Zy = -0.5j * (Z - Zminconj) let (z_y_re, z_y_im): (Vec<_>, Vec<_>) = z_even .iter() @@ -84,32 +69,20 @@ pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { let a = ze - ze_mc; let b = zo - zo_mc; - // -0.5i (a + ib) = -0.5i * a + 0.5 * b + // -0.5i (a + ib) = -0.5i * a - 0.5i * ib + // = -0.5i * a + 0.5 * b + // = 0.5 * b - 0.5 * ia (0.5 * b, -0.5 * a) }) .unzip(); - /* - Zx: - - [64. +0.j -8.+19.3137085j -8. +8.j -8. +3.3137085j - -8. +0.j -8. -3.3137085j -8. -8.j -8.-19.3137085j] - - Zy: - - [72. -0.j -8.+19.3137085j -8. +8.j -8. +3.3137085j - -8. +0.j -8. -3.3137085j -8. -8.j -8.-19.3137085j] - */ - - println!("Zy reals: {z_y_re:?}\nZy imags: {z_y_im:?}"); - - let (twiddle_re, twiddle_im) = compute_twiddle_factors(n); // np.exp(-1j * 2 * math.pi * np.arange(N//2) / N) + let (twiddle_re, twiddle_im): (Vec, Vec) = compute_twiddle_factors(big_n); // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) - let mut z_all_re = Vec::new(); - let mut z_all_im = Vec::new(); + let mut z_all_re = Vec::with_capacity(z_x_re.len() + z_y_re.len()); + let mut z_all_im = Vec::with_capacity(z_x_im.len() + z_y_im.len()); - for i in 0..n / 2 { + for i in 0..big_n / 2 { let zx_re = z_x_re[i]; let zx_im = z_x_im[i]; let zy_re = z_y_re[i]; @@ -125,7 +98,7 @@ pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { z_all_im.push(zx_im + wz_im); } - for i in 0..n / 2 { + for i in 0..big_n / 2 { let zx_re = z_x_re[i]; let zx_im = z_x_im[i]; let zy_re = z_y_re[i]; @@ -141,6 +114,14 @@ pub fn real_fft(input_re: &[f64]) -> (Vec, Vec) { z_all_im.push(zx_im - wz_im); } + // println!( + // "z_x_re: {}\nz_x_im: {}\nz_all_re len: {}\nz_all_im len: {}\n", + // z_x_re.len(), + // z_x_im.len(), + // z_all_re.len(), + // z_all_im.len() + // ); + // return Zall (z_all_re, z_all_im) } @@ -151,26 +132,34 @@ mod tests { use super::*; - #[test] - fn r2c_vs_c2c() { - let n = 4; - let big_n = 1 << 4; - let mut input_re: Vec<_> = (1..=big_n).map(|i| i as f64).collect(); // Length is 7, which is a prime number - - let (r2c_res_reals, r2c_res_imags) = real_fft(&mut input_re); - - input_re = (1..=big_n).map(|i| i as f64).collect(); - let mut input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example - fft_64(&mut input_re, &mut input_im, Direction::Forward); - - r2c_res_reals - .iter() - .zip(r2c_res_imags.iter()) - .zip(input_re.iter()) - .zip(input_im.iter()) - .for_each(|(((a_re, a_im), e_re), e_im)| { - assert_float_closeness(*a_re, *e_re, 1e-6); - assert_float_closeness(*a_im, *e_im, 1e-6); - }); + macro_rules! impl_r2c_vs_c2c_test { + ($func:ident, $precision:ty, $fft_precision:ident, $rftt_funct:ident) => { + #[test] + fn $func() { + for n in 4..=10 { + let big_n = 1 << n; + let input_re: Vec<$precision> = (1..=big_n).map(|i| i as $precision).collect(); // Length is 7, which is a prime number + + let (r2c_res_reals, r2c_res_imags) = $rftt_funct(&input_re); + + let mut input_re: Vec<_> = (1..=big_n).map(|i| i as $precision).collect(); + let mut input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example + $fft_precision(&mut input_re, &mut input_im, Direction::Forward); + + r2c_res_reals + .iter() + .zip(r2c_res_imags.iter()) + .zip(input_re.iter()) + .zip(input_im.iter()) + .for_each(|(((a_re, a_im), e_re), e_im)| { + assert_float_closeness(*a_re, *e_re, 1e-6); + assert_float_closeness(*a_im, *e_im, 1e-6); + }); + } + } + }; } + + impl_r2c_vs_c2c_test!(r2c_vs_c2c_f64, f64, fft_64, r2c_fft_f64); + // impl_r2c_vs_c2c_test!(r2c_vs_c2c_f32, fft_32, r2c_fft_f32); } diff --git a/src/lib.rs b/src/lib.rs index 6f43313..7eeacd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,8 +8,6 @@ #![forbid(unsafe_code)] #![feature(portable_simd, avx512_target_feature)] -use std::f64::consts::PI; - use crate::cobra::cobra_apply; use crate::kernels::{ fft_32_chunk_n_simd, fft_64_chunk_n_simd, fft_chunk_2, fft_chunk_4, fft_chunk_n, @@ -175,83 +173,6 @@ impl_fft_with_opts_and_plan_for!( 16 ); -fn compute_twiddle_factors(big_n: usize) -> (Vec, Vec) { - let half_n = big_n / 2; - let mut real_parts = Vec::with_capacity(half_n); - let mut imag_parts = Vec::with_capacity(half_n); - - for k in 0..half_n { - let angle = -2.0 * PI * (k as f64) / (big_n as f64); - real_parts.push(angle.cos()); - imag_parts.push(angle.sin()); - } - - (real_parts, imag_parts) -} - -// TODO: make this generic over f64/f32 using macro -/// Real-to-Complex FFT `f64`. Note the input is a real-valued signal. -pub fn fft_64_r2c(signal: &[f64]) -> (Vec, Vec) { - let big_n = signal.len(); - - // z[n] = x_{e}[n] + j * x_{o}[n] - let (mut reals, mut imags): (Vec, Vec) = - signal.chunks_exact(2).map(|c| (c[0], c[1])).unzip(); - - // Z[k] = DFT{z} - fft_64(&mut reals, &mut imags, Direction::Forward); - - let mut x_evens_re = vec![0.0; big_n / 2]; - let mut x_evens_im = vec![0.0; big_n / 2]; - - for k in 0..big_n / 2 { - let re = 0.5 * (reals[k] + reals[big_n / 2 - 1 - k]); - let im = 0.5 * (imags[k] - imags[big_n / 2 - 1 - k]); - x_evens_re[k] = re; - x_evens_im[k] = im; - } - - let mut x_odds_re = vec![0.0; big_n / 2]; - let mut x_odds_im = vec![0.0; big_n / 2]; - - // -i * ((a + ib) - (c - id)) = -(b + d) - i(a - c) - for k in 0..big_n / 2 { - let re = 0.5 * (-imags[k] - imags[big_n / 2 - 1 - k]); - let im = 0.5 * (-reals[k] + reals[k]); - x_odds_re[k] = re; - x_odds_im[k] = im; - } - - // 7. X[k] = X_{e}[k] + X_{o}[e] * e^{-2*j*pi*(k/N)}, for k \in {0, ..., N/2 - 1} - let (twiddles_re, twiddles_im) = compute_twiddle_factors(big_n); - for k in 0..big_n / 2 { - let a = x_evens_re[k]; - let b = x_evens_im[k]; - let c = x_odds_re[k]; - let d = x_odds_im[k]; - let g = twiddles_re[k]; - let h = twiddles_im[k]; - - // (a + ib) + (c + id) * (g + ih) = (a + cg - dh) + i(b + ch + dg) - reals[k] = a + c * g - d * h; - imags[k] = b + c * h + d * g; - } - - // 8. X[k] = X_e[k] - X_{o}[k], for k = N/2 - let k = big_n / 2 - 1; - reals[k] = x_evens_re[k] - x_odds_re[k]; - imags[k] = x_evens_im[k] - x_odds_im[k]; - - // 9. X[k] = X*[N - k], for k \in {N/2 + 1, ..., N - 1} - for k in (big_n / 2 + 1)..big_n { - eprintln!("k: {k} and {}", big_n - k - 1); - reals[k] = reals[big_n - k - 1]; - imags[k] = -imags[big_n - k - 1]; - } - - (reals, imags) -} - #[cfg(test)] mod tests { use std::ops::Range; @@ -387,22 +308,4 @@ mod tests { } } } - - #[test] - fn fft_r2c_vs_c2c() { - let n = 4; - let big_n = 1 << n; - let mut reals: Vec = (1..=big_n).map(|i| i as f64).collect(); - - let (signal_re, signal_im) = fft_64_r2c(&mut reals); - println!("{:?}", signal_re); - println!("{:?}\n", signal_im); - - let mut reals: Vec = (1..=big_n).map(|i| i as f64).collect(); - let mut imags = vec![0.0; big_n]; - fft_64(&mut reals, &mut imags, Direction::Forward); - - println!("{:?}", reals); - println!("{:?}\n", imags); - } } From f847e8f1cdd4aa4621c46a754cbf9087a30543cf Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Thu, 30 May 2024 11:54:59 -0400 Subject: [PATCH 09/38] Use preallocated output arrays --- src/fft.rs | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 7751850..7cb28bf 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,8 +1,8 @@ //! Implementation of Real valued FFT use num_traits::Float; -use crate::fft_64; use crate::Direction; +use crate::fft_64; fn compute_twiddle_factors(big_n: usize) -> (Vec, Vec) { let half_n = big_n / 2; @@ -21,7 +21,12 @@ fn compute_twiddle_factors(big_n: usize) -> (Vec, Vec) { } /// Implementation of Real-Valued FFT -pub fn r2c_fft_f64(input_re: &[f64]) -> (Vec, Vec) { +/// +/// # Panics +/// +/// Panics if `output_re.len() != output_im.len()` and `input_re.len()` == `output_re.len()` +pub fn r2c_fft_f64(input_re: &[f64], output_re: &mut [f64], output_im: &mut [f64]) { + assert!(output_re.len() == output_im.len() && input_re.len() == output_re.len()); let big_n = input_re.len(); // Splitting odd and even @@ -33,7 +38,6 @@ pub fn r2c_fft_f64(input_re: &[f64]) -> (Vec, Vec) { // take care of the np.flip() let mut z_even_min_conj: Vec<_> = z_even.iter().copied().rev().collect(); - let mut z_odd_min_conj: Vec<_> = z_odd.iter().copied().rev().collect(); // Zminconj = np.roll(np.flip(Z), 1).conj() @@ -79,8 +83,6 @@ pub fn r2c_fft_f64(input_re: &[f64]) -> (Vec, Vec) { let (twiddle_re, twiddle_im): (Vec, Vec) = compute_twiddle_factors(big_n); // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) - let mut z_all_re = Vec::with_capacity(z_x_re.len() + z_y_re.len()); - let mut z_all_im = Vec::with_capacity(z_x_im.len() + z_y_im.len()); for i in 0..big_n / 2 { let zx_re = z_x_re[i]; @@ -94,8 +96,8 @@ pub fn r2c_fft_f64(input_re: &[f64]) -> (Vec, Vec) { let wz_im = w_re * zy_im + w_im * zy_re; // Zx + W * Zy - z_all_re.push(zx_re + wz_re); - z_all_im.push(zx_im + wz_im); + output_re[i] = zx_re + wz_re; + output_im[i] = zx_im + wz_im; } for i in 0..big_n / 2 { @@ -110,20 +112,9 @@ pub fn r2c_fft_f64(input_re: &[f64]) -> (Vec, Vec) { let wz_im = w_re * zy_im + w_im * zy_re; // Zx - W * Zy - z_all_re.push(zx_re - wz_re); - z_all_im.push(zx_im - wz_im); + output_re[i + big_n / 2] = zx_re - wz_re; + output_im[i + big_n / 2] = zx_im - wz_im; } - - // println!( - // "z_x_re: {}\nz_x_im: {}\nz_all_re len: {}\nz_all_im len: {}\n", - // z_x_re.len(), - // z_x_im.len(), - // z_all_re.len(), - // z_all_im.len() - // ); - - // return Zall - (z_all_re, z_all_im) } #[cfg(test)] @@ -136,19 +127,20 @@ mod tests { ($func:ident, $precision:ty, $fft_precision:ident, $rftt_funct:ident) => { #[test] fn $func() { - for n in 4..=10 { + for n in 4..=11 { let big_n = 1 << n; let input_re: Vec<$precision> = (1..=big_n).map(|i| i as $precision).collect(); // Length is 7, which is a prime number + let (mut output_re, mut output_im) = (vec![0.0; big_n], vec![0.0; big_n]); - let (r2c_res_reals, r2c_res_imags) = $rftt_funct(&input_re); + $rftt_funct(&input_re, &mut output_re, &mut output_im); let mut input_re: Vec<_> = (1..=big_n).map(|i| i as $precision).collect(); let mut input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example $fft_precision(&mut input_re, &mut input_im, Direction::Forward); - r2c_res_reals + output_re .iter() - .zip(r2c_res_imags.iter()) + .zip(output_im.iter()) .zip(input_re.iter()) .zip(input_im.iter()) .for_each(|(((a_re, a_im), e_re), e_im)| { From e5ea31cd097264d28e0512c0ce730aeb7ad3fcd3 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Thu, 30 May 2024 11:55:59 -0400 Subject: [PATCH 10/38] Fix formatting --- src/fft.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fft.rs b/src/fft.rs index 7cb28bf..5976420 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,8 +1,8 @@ //! Implementation of Real valued FFT use num_traits::Float; -use crate::Direction; use crate::fft_64; +use crate::Direction; fn compute_twiddle_factors(big_n: usize) -> (Vec, Vec) { let half_n = big_n / 2; From 957d56f7ed73f54ff3eddac5f756166262ca48c9 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 3 Jun 2024 20:15:47 -0400 Subject: [PATCH 11/38] Remove extra memory allocations in R2C FFT --- src/fft.rs | 87 +++++++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 5976420..7655f48 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,8 +1,8 @@ //! Implementation of Real valued FFT use num_traits::Float; -use crate::fft_64; use crate::Direction; +use crate::fft_64; fn compute_twiddle_factors(big_n: usize) -> (Vec, Vec) { let half_n = big_n / 2; @@ -36,49 +36,56 @@ pub fn r2c_fft_f64(input_re: &[f64], output_re: &mut [f64], output_im: &mut [f64 // Z = np.fft.fft(z) fft_64(&mut z_even, &mut z_odd, Direction::Forward); - // take care of the np.flip() - let mut z_even_min_conj: Vec<_> = z_even.iter().copied().rev().collect(); - let mut z_odd_min_conj: Vec<_> = z_odd.iter().copied().rev().collect(); - - // Zminconj = np.roll(np.flip(Z), 1).conj() - // now roll both by 1 - z_even_min_conj.rotate_right(1); - z_odd_min_conj.rotate_right(1); - - // the conj() call can be resolved by negating every imaginary component - for zo in z_odd_min_conj.iter_mut() { - *zo = -(*zo); - } + let mut z_x_re = Vec::with_capacity(z_even.len()); + let mut z_x_im = Vec::with_capacity(z_even.len()); + let mut z_y_re = Vec::with_capacity(z_even.len()); + let mut z_y_im = Vec::with_capacity(z_even.len()); - // Zx = 0.5 * (Z + Zminconj) - let (z_x_re, z_x_im): (Vec<_>, Vec<_>) = z_even - .iter() - .zip(z_odd.iter()) - .zip(z_even_min_conj.iter()) - .zip(z_odd_min_conj.iter()) - .map(|(((ze, zo), ze_mc), zo_mc)| { - let a = 0.5 * (ze + ze_mc); - let b = 0.5 * (zo + zo_mc); - (a, b) - }) - .unzip(); + z_x_re.push(0.0); + z_x_im.push(0.0); + z_y_re.push(0.0); + z_y_im.push(0.0); + // Zminconj = np.roll(np.flip(Z), 1).conj() + // Zx = 0.5 * (Z + Zminconj) // Zy = -0.5j * (Z - Zminconj) - let (z_y_re, z_y_im): (Vec<_>, Vec<_>) = z_even + z_even .iter() - .zip(z_odd.iter()) - .zip(z_even_min_conj.iter()) - .zip(z_odd_min_conj.iter()) - .map(|(((ze, zo), ze_mc), zo_mc)| { - let a = ze - ze_mc; - let b = zo - zo_mc; - - // -0.5i (a + ib) = -0.5i * a - 0.5i * ib - // = -0.5i * a + 0.5 * b - // = 0.5 * b - 0.5 * ia - (0.5 * b, -0.5 * a) - }) - .unzip(); + .skip(1) + .zip(z_odd.iter().skip(1)) + .zip(z_even.iter().skip(1).rev()) + .zip(z_odd.iter().skip(1).rev()) + .for_each(|(((z_e, z_o), z_e_mc), z_o_mc)| { + let a = *z_e; + let b = *z_o; + let c = *z_e_mc; + let d = -(*z_o_mc); + + let t = 0.5 * (a + c); + let u = 0.5 * (b + d); + let v = -0.5 * (a - c); + let w = 0.5 * (b - d); + + z_x_re.push(t); + z_x_im.push(u); + z_y_re.push(w); + z_y_im.push(v); + }); + + let a = z_even[0]; + let b = z_odd[0]; + let c = z_even[0]; + let d = -z_odd[0]; + + let t = 0.5 * (a + c); + let u = 0.5 * (b + d); + let v = -0.5 * (a - c); + let w = 0.5 * (b - d); + + z_x_re[0] = t; + z_x_im[0] = u; + z_y_re[0] = w; + z_y_im[0] = v; let (twiddle_re, twiddle_im): (Vec, Vec) = compute_twiddle_factors(big_n); From e41da367532fec7f58bdc60d875f669bcd20ba13 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 3 Jun 2024 20:16:36 -0400 Subject: [PATCH 12/38] Fix formatting --- src/fft.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fft.rs b/src/fft.rs index 7655f48..e38c9f3 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,8 +1,8 @@ //! Implementation of Real valued FFT use num_traits::Float; -use crate::Direction; use crate::fft_64; +use crate::Direction; fn compute_twiddle_factors(big_n: usize) -> (Vec, Vec) { let half_n = big_n / 2; From 88433b538e479df4c4f4831c8b9e6d7d1d7d9bf3 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sat, 8 Jun 2024 14:48:09 -0400 Subject: [PATCH 13/38] Add criterion and benchmark - Benchmark throughput/time of Real-to-Complex (R2C) versus the Complex-to-Complex (C2C) FFT on real input data --- Cargo.toml | 5 + src/fft.rs | 233 +++++++++++++++++++++---------------------- utilities/src/lib.rs | 2 +- 3 files changed, 118 insertions(+), 122 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 12d50ef..d9c39de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,11 @@ multiversion = "0.7" [dev-dependencies] utilities = { path = "utilities" } fftw = "0.8.0" +criterion = "0.5.1" + +[[bench]] +name = "bench" +harness = false [profile.release] codegen-units = 1 diff --git a/src/fft.rs b/src/fft.rs index e38c9f3..8a6de9e 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,129 +1,120 @@ //! Implementation of Real valued FFT -use num_traits::Float; - -use crate::fft_64; -use crate::Direction; - -fn compute_twiddle_factors(big_n: usize) -> (Vec, Vec) { - let half_n = big_n / 2; - let mut real_parts = Vec::with_capacity(half_n); - let mut imag_parts = Vec::with_capacity(half_n); - let two: T = T::from(2.0).unwrap(); - let pi: T = T::from(std::f64::consts::PI).unwrap(); - - for k in 0..half_n { - let angle = -two * pi * T::from(k).unwrap() / T::from(big_n).unwrap(); - real_parts.push(angle.cos()); - imag_parts.push(angle.sin()); - } - - (real_parts, imag_parts) -} - -/// Implementation of Real-Valued FFT -/// -/// # Panics -/// -/// Panics if `output_re.len() != output_im.len()` and `input_re.len()` == `output_re.len()` -pub fn r2c_fft_f64(input_re: &[f64], output_re: &mut [f64], output_im: &mut [f64]) { - assert!(output_re.len() == output_im.len() && input_re.len() == output_re.len()); - let big_n = input_re.len(); - - // Splitting odd and even - let (mut z_even, mut z_odd): (Vec<_>, Vec<_>) = - input_re.chunks_exact(2).map(|c| (c[0], c[1])).unzip(); - - // Z = np.fft.fft(z) - fft_64(&mut z_even, &mut z_odd, Direction::Forward); - - let mut z_x_re = Vec::with_capacity(z_even.len()); - let mut z_x_im = Vec::with_capacity(z_even.len()); - let mut z_y_re = Vec::with_capacity(z_even.len()); - let mut z_y_im = Vec::with_capacity(z_even.len()); - - z_x_re.push(0.0); - z_x_im.push(0.0); - z_y_re.push(0.0); - z_y_im.push(0.0); - - // Zminconj = np.roll(np.flip(Z), 1).conj() - // Zx = 0.5 * (Z + Zminconj) - // Zy = -0.5j * (Z - Zminconj) - z_even - .iter() - .skip(1) - .zip(z_odd.iter().skip(1)) - .zip(z_even.iter().skip(1).rev()) - .zip(z_odd.iter().skip(1).rev()) - .for_each(|(((z_e, z_o), z_e_mc), z_o_mc)| { - let a = *z_e; - let b = *z_o; - let c = *z_e_mc; - let d = -(*z_o_mc); +use crate::{fft_32, fft_64, Direction, twiddles::generate_twiddles}; + +#[macro_export] +macro_rules! impl_r2c_fft { + ($func_name:ident, $precision:ty, $fft_func:ident) => { + /// Implementation of Real-Valued FFT + /// + /// # Panics + /// + /// Panics if `output_re.len() != output_im.len()` and `input_re.len()` == `output_re.len()` + pub fn $func_name(input_re: &[$precision], output_re: &mut [$precision], output_im: &mut [$precision]) { + assert!(output_re.len() == output_im.len() && input_re.len() == output_re.len()); + let big_n = input_re.len(); + + // Splitting odd and even + let (mut z_even, mut z_odd): (Vec<_>, Vec<_>) = + input_re.chunks_exact(2).map(|c| (c[0], c[1])).unzip(); + + // Z = np.fft.fft(z) + $fft_func(&mut z_even, &mut z_odd, Direction::Forward); + + let mut z_x_re = Vec::with_capacity(z_even.len()); + let mut z_x_im = Vec::with_capacity(z_even.len()); + let mut z_y_re = Vec::with_capacity(z_even.len()); + let mut z_y_im = Vec::with_capacity(z_even.len()); + + z_x_re.push(0.0); + z_x_im.push(0.0); + z_y_re.push(0.0); + z_y_im.push(0.0); + + // Zminconj = np.roll(np.flip(Z), 1).conj() + // Zx = 0.5 * (Z + Zminconj) + // Zy = -0.5j * (Z - Zminconj) + z_even + .iter() + .skip(1) + .zip(z_odd.iter().skip(1)) + .zip(z_even.iter().skip(1).rev()) + .zip(z_odd.iter().skip(1).rev()) + .for_each(|(((z_e, z_o), z_e_mc), z_o_mc)| { + let a = *z_e; + let b = *z_o; + let c = *z_e_mc; + let d = -(*z_o_mc); + + let t = 0.5 * (a + c); + let u = 0.5 * (b + d); + let v = -0.5 * (a - c); + let w = 0.5 * (b - d); + + z_x_re.push(t); + z_x_im.push(u); + z_y_re.push(w); + z_y_im.push(v); + }); + + let a = z_even[0]; + let b = z_odd[0]; + let c = z_even[0]; + let d = -z_odd[0]; let t = 0.5 * (a + c); let u = 0.5 * (b + d); let v = -0.5 * (a - c); let w = 0.5 * (b - d); - z_x_re.push(t); - z_x_im.push(u); - z_y_re.push(w); - z_y_im.push(v); - }); - - let a = z_even[0]; - let b = z_odd[0]; - let c = z_even[0]; - let d = -z_odd[0]; - - let t = 0.5 * (a + c); - let u = 0.5 * (b + d); - let v = -0.5 * (a - c); - let w = 0.5 * (b - d); - - z_x_re[0] = t; - z_x_im[0] = u; - z_y_re[0] = w; - z_y_im[0] = v; - - let (twiddle_re, twiddle_im): (Vec, Vec) = compute_twiddle_factors(big_n); - - // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) - - for i in 0..big_n / 2 { - let zx_re = z_x_re[i]; - let zx_im = z_x_im[i]; - let zy_re = z_y_re[i]; - let zy_im = z_y_im[i]; - let w_re = twiddle_re[i]; - let w_im = twiddle_im[i]; - - let wz_re = w_re * zy_re - w_im * zy_im; - let wz_im = w_re * zy_im + w_im * zy_re; - - // Zx + W * Zy - output_re[i] = zx_re + wz_re; - output_im[i] = zx_im + wz_im; - } + z_x_re[0] = t; + z_x_im[0] = u; + z_y_re[0] = w; + z_y_im[0] = v; - for i in 0..big_n / 2 { - let zx_re = z_x_re[i]; - let zx_im = z_x_im[i]; - let zy_re = z_y_re[i]; - let zy_im = z_y_im[i]; - let w_re = twiddle_re[i]; - let w_im = twiddle_im[i]; + let (twiddle_re, twiddle_im): (Vec<$precision>, Vec<$precision>) = generate_twiddles(big_n / 2, Direction::Forward); - let wz_re = w_re * zy_re - w_im * zy_im; - let wz_im = w_re * zy_im + w_im * zy_re; + // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) - // Zx - W * Zy - output_re[i + big_n / 2] = zx_re - wz_re; - output_im[i + big_n / 2] = zx_im - wz_im; - } + for i in 0..big_n / 2 { + let zx_re = z_x_re[i]; + let zx_im = z_x_im[i]; + let zy_re = z_y_re[i]; + let zy_im = z_y_im[i]; + let w_re = twiddle_re[i]; + let w_im = twiddle_im[i]; + + let wz_re = w_re * zy_re - w_im * zy_im; + let wz_im = w_re * zy_im + w_im * zy_re; + + // Zx + W * Zy + output_re[i] = zx_re + wz_re; + output_im[i] = zx_im + wz_im; + } + + for i in 0..big_n / 2 { + let zx_re = z_x_re[i]; + let zx_im = z_x_im[i]; + let zy_re = z_y_re[i]; + let zy_im = z_y_im[i]; + let w_re = twiddle_re[i]; + let w_im = twiddle_im[i]; + + let wz_re = w_re * zy_re - w_im * zy_im; + let wz_im = w_re * zy_im + w_im * zy_re; + + // Zx - W * Zy + output_re[i + big_n / 2] = zx_re - wz_re; + output_im[i + big_n / 2] = zx_im - wz_im; + } + } + + }; } +impl_r2c_fft!(r2c_fft_f32, f32, fft_32); +impl_r2c_fft!(r2c_fft_f64, f64, fft_64); + + #[cfg(test)] mod tests { use utilities::assert_float_closeness; @@ -131,7 +122,7 @@ mod tests { use super::*; macro_rules! impl_r2c_vs_c2c_test { - ($func:ident, $precision:ty, $fft_precision:ident, $rftt_funct:ident) => { + ($func:ident, $precision:ty, $fft_precision:ident, $rfft_func:ident, $epsilon:literal) => { #[test] fn $func() { for n in 4..=11 { @@ -139,9 +130,9 @@ mod tests { let input_re: Vec<$precision> = (1..=big_n).map(|i| i as $precision).collect(); // Length is 7, which is a prime number let (mut output_re, mut output_im) = (vec![0.0; big_n], vec![0.0; big_n]); - $rftt_funct(&input_re, &mut output_re, &mut output_im); + $rfft_func(&input_re, &mut output_re, &mut output_im); - let mut input_re: Vec<_> = (1..=big_n).map(|i| i as $precision).collect(); + let mut input_re: Vec<$precision> = (1..=big_n).map(|i| i as $precision).collect(); let mut input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example $fft_precision(&mut input_re, &mut input_im, Direction::Forward); @@ -151,14 +142,14 @@ mod tests { .zip(input_re.iter()) .zip(input_im.iter()) .for_each(|(((a_re, a_im), e_re), e_im)| { - assert_float_closeness(*a_re, *e_re, 1e-6); - assert_float_closeness(*a_im, *e_im, 1e-6); + assert_float_closeness(*a_re, *e_re, $epsilon); + assert_float_closeness(*a_im, *e_im, $epsilon); }); } } }; } - impl_r2c_vs_c2c_test!(r2c_vs_c2c_f64, f64, fft_64, r2c_fft_f64); - // impl_r2c_vs_c2c_test!(r2c_vs_c2c_f32, fft_32, r2c_fft_f32); + impl_r2c_vs_c2c_test!(r2c_vs_c2c_f64, f64, fft_64, r2c_fft_f64, 1e-6); + impl_r2c_vs_c2c_test!(r2c_vs_c2c_f32, f32, fft_32, r2c_fft_f32, 3.5); } diff --git a/utilities/src/lib.rs b/utilities/src/lib.rs index f7eff6f..47ad1b6 100644 --- a/utilities/src/lib.rs +++ b/utilities/src/lib.rs @@ -17,7 +17,7 @@ use rustfft::num_traits::Float; pub fn assert_float_closeness(actual: T, expected: T, epsilon: T) { if (actual - expected).abs() >= epsilon { panic!( - "Assertion failed: {actual} too far from expected value {expected} (with epsilon {epsilon})", + "Assertion failed: actual value {actual} too far from expected value {expected} (with epsilon {epsilon})", ); } } From c22235ba327531967b570aef2230cf9807b8038f Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sat, 8 Jun 2024 15:01:53 -0400 Subject: [PATCH 14/38] Add intructions for benchmarking using criterion --- assets/lines.svg | 276 ++++++++++++++++++++++++++++++++++++++++++++++ benches/README.md | 23 ++++ benches/bench.rs | 47 ++++++++ src/fft.rs | 16 ++- 4 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 assets/lines.svg create mode 100644 benches/bench.rs diff --git a/assets/lines.svg b/assets/lines.svg new file mode 100644 index 0000000..aa74b76 --- /dev/null +++ b/assets/lines.svg @@ -0,0 +1,276 @@ + + + +Gnuplot +Produced by GNUPLOT 6.0 patchlevel 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 2 + + + + + + + + + + + + + 4 + + + + + + + + + + + + + 6 + + + + + + + + + + + + + 8 + + + + + + + + + + + + + 10 + + + + + + + + + + + + + 12 + + + + + + + + + + + + + 14 + + + + + + + + + + + + + 16 + + + + + 0 + + + + + 200000 + + + + + 400000 + + + + + 600000 + + + + + 800000 + + + + + 1x106 + + + + + 1.2x106 + + + + + + + + + c2c_fft + + + + + c2c_fft + + + + + + gnuplot_plot_2 + + + + + + + + + + + + r2c_fft + + + + + r2c_fft + + + + + + gnuplot_plot_4 + + + + + + + + + + + + + + + + + + + + Average time (ms) + + + + + Input Size (Elements) + + + + + + + r2c_versus_c2c: Comparison + + + + + + + diff --git a/benches/README.md b/benches/README.md index 3e84f65..c231c86 100644 --- a/benches/README.md +++ b/benches/README.md @@ -87,6 +87,29 @@ The generated images will be saved in your working directory. | Active Toolchain | nightly-x86_64-unknown-linux-gnu (default) | | Rustc Version | rustc 1.79.0-nightly (7f2fc33da 2024-04-22) | +## Measuruing throughput with `criterion` + +0. Install gnuplot if you want `criterion` to use gnuplot as the plotting backend. + On macOS, just run: +```bash +brew install gnuplot +``` + +1. Run benchmarks +```bash +cargo bench +``` + +2. Open the report using a browser. The report will be located in the `target` directory. + For example, from the root of the `PhastFT` repository (on macOS) run: +```bash +open -a firefox target/criterion/r2c_versus_c2c/report/index.html +``` + +

+ Real-to-Complex FFT vs. Complex-to-Complex FFT + + ## Profiling Navigate to the cloned repo: diff --git a/benches/bench.rs b/benches/bench.rs new file mode 100644 index 0000000..17eaa8a --- /dev/null +++ b/benches/bench.rs @@ -0,0 +1,47 @@ +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use phastft::{fft::r2c_fft_f64, fft_64, planner::Direction}; +use utilities::gen_random_signal; + +fn criterion_benchmark(c: &mut Criterion) { + let sizes = vec![1 << 10, 1 << 12, 1 << 14, 1 << 16, 1 << 18, 1 << 20]; + + let mut group = c.benchmark_group("r2c_versus_c2c"); + for &size in &sizes { + group.throughput(Throughput::Elements(size as u64)); + + group.bench_with_input(BenchmarkId::new("r2c_fft", size), &size, |b, &size| { + let mut s_re = vec![0.0; size]; + let mut s_im = vec![0.0; size]; + gen_random_signal(&mut s_re, &mut s_im); + + b.iter(|| { + let mut output_re = vec![0.0; size]; + let mut output_im = vec![0.0; size]; + r2c_fft_f64( + black_box(&mut s_re), + black_box(&mut output_re), + black_box(&mut output_im), + ); + }); + }); + + group.bench_with_input(BenchmarkId::new("c2c_fft", size), &size, |b, &size| { + let mut s_re = vec![0.0; size]; + let mut s_im = vec![0.0; size]; + gen_random_signal(&mut s_re, &mut s_im); + s_im = vec![0.0; size]; + + b.iter(|| { + fft_64( + black_box(&mut s_re), + black_box(&mut s_im), + Direction::Forward, + ); + }); + }); + } + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/src/fft.rs b/src/fft.rs index 8a6de9e..0934d99 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,5 +1,5 @@ //! Implementation of Real valued FFT -use crate::{fft_32, fft_64, Direction, twiddles::generate_twiddles}; +use crate::{fft_32, fft_64, twiddles::generate_twiddles, Direction}; #[macro_export] macro_rules! impl_r2c_fft { @@ -9,7 +9,11 @@ macro_rules! impl_r2c_fft { /// # Panics /// /// Panics if `output_re.len() != output_im.len()` and `input_re.len()` == `output_re.len()` - pub fn $func_name(input_re: &[$precision], output_re: &mut [$precision], output_im: &mut [$precision]) { + pub fn $func_name( + input_re: &[$precision], + output_re: &mut [$precision], + output_im: &mut [$precision], + ) { assert!(output_re.len() == output_im.len() && input_re.len() == output_re.len()); let big_n = input_re.len(); @@ -71,7 +75,8 @@ macro_rules! impl_r2c_fft { z_y_re[0] = w; z_y_im[0] = v; - let (twiddle_re, twiddle_im): (Vec<$precision>, Vec<$precision>) = generate_twiddles(big_n / 2, Direction::Forward); + let (twiddle_re, twiddle_im): (Vec<$precision>, Vec<$precision>) = + generate_twiddles(big_n / 2, Direction::Forward); // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) @@ -107,14 +112,12 @@ macro_rules! impl_r2c_fft { output_im[i + big_n / 2] = zx_im - wz_im; } } - }; } impl_r2c_fft!(r2c_fft_f32, f32, fft_32); impl_r2c_fft!(r2c_fft_f64, f64, fft_64); - #[cfg(test)] mod tests { use utilities::assert_float_closeness; @@ -132,7 +135,8 @@ mod tests { $rfft_func(&input_re, &mut output_re, &mut output_im); - let mut input_re: Vec<$precision> = (1..=big_n).map(|i| i as $precision).collect(); + let mut input_re: Vec<$precision> = + (1..=big_n).map(|i| i as $precision).collect(); let mut input_im = vec![0.0; input_re.len()]; // Assume the imaginary part is zero for this example $fft_precision(&mut input_re, &mut input_im, Direction::Forward); From ea6b825eb70f4604f6a2edd05e48b2e5f1e2d82c Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sat, 8 Jun 2024 15:02:54 -0400 Subject: [PATCH 15/38] Add subsection before graph --- benches/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benches/README.md b/benches/README.md index c231c86..08b8f1a 100644 --- a/benches/README.md +++ b/benches/README.md @@ -106,6 +106,8 @@ cargo bench open -a firefox target/criterion/r2c_versus_c2c/report/index.html ``` +### Results +

Real-to-Complex FFT vs. Complex-to-Complex FFT From 8cfa6504f5c3bfc75df3ccfdef145fa490410ff5 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sat, 8 Jun 2024 15:10:29 -0400 Subject: [PATCH 16/38] Fix benchmark plot rendering issue --- assets/lines.png | Bin 0 -> 279353 bytes assets/lines.svg | 276 ---------------------------------------------- benches/README.md | 3 +- 3 files changed, 1 insertion(+), 278 deletions(-) create mode 100644 assets/lines.png delete mode 100644 assets/lines.svg diff --git a/assets/lines.png b/assets/lines.png new file mode 100644 index 0000000000000000000000000000000000000000..6767aa686f4ab3a78f6a7fcb9a0d3cc610e392fe GIT binary patch literal 279353 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 - - -Gnuplot -Produced by GNUPLOT 6.0 patchlevel 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - 2 - - - - - - - - - - - - - 4 - - - - - - - - - - - - - 6 - - - - - - - - - - - - - 8 - - - - - - - - - - - - - 10 - - - - - - - - - - - - - 12 - - - - - - - - - - - - - 14 - - - - - - - - - - - - - 16 - - - - - 0 - - - - - 200000 - - - - - 400000 - - - - - 600000 - - - - - 800000 - - - - - 1x106 - - - - - 1.2x106 - - - - - - - - - c2c_fft - - - - - c2c_fft - - - - - - gnuplot_plot_2 - - - - - - - - - - - - r2c_fft - - - - - r2c_fft - - - - - - gnuplot_plot_4 - - - - - - - - - - - - - - - - - - - - Average time (ms) - - - - - Input Size (Elements) - - - - - - - r2c_versus_c2c: Comparison - - - - - - - diff --git a/benches/README.md b/benches/README.md index 08b8f1a..feb8018 100644 --- a/benches/README.md +++ b/benches/README.md @@ -108,8 +108,7 @@ open -a firefox target/criterion/r2c_versus_c2c/report/index.html ### Results -

- Real-to-Complex FFT vs. Complex-to-Complex FFT +![Alt text](../assets/lines.png) ## Profiling From 90251a8a0f3cfb3bd29b56bbb7313ed03fd98340 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sun, 9 Jun 2024 14:46:17 -0400 Subject: [PATCH 17/38] Add python bindings for Real FFT --- pyphastft/src/lib.rs | 23 ++++- pyphastft/vis_qt.py | 208 +++++++++++++++++++++---------------------- 2 files changed, 123 insertions(+), 108 deletions(-) diff --git a/pyphastft/src/lib.rs b/pyphastft/src/lib.rs index f69b64f..ae1ebbc 100644 --- a/pyphastft/src/lib.rs +++ b/pyphastft/src/lib.rs @@ -1,5 +1,5 @@ -use numpy::PyReadwriteArray1; -use phastft::{fft_64 as fft_64_rs, planner::Direction}; +use numpy::{PyReadonlyArray1, PyReadwriteArray1}; +use phastft::{fft_64 as fft_64_rs, fft::r2c_fft_f64, planner::Direction}; use pyo3::prelude::*; #[pyfunction] @@ -18,9 +18,28 @@ fn fft(mut reals: PyReadwriteArray1, mut imags: PyReadwriteArray1, dir ); } +#[pyfunction] +fn rfft(reals: PyReadonlyArray1, direction: char) -> (Vec, Vec) { + assert!(direction == 'f' || direction == 'r'); + let _dir = if direction == 'f' { + Direction::Forward + } else { + Direction::Reverse + }; + + let big_n = reals.as_slice().unwrap().len(); + + let mut output_re = vec![0.0; big_n]; + let mut output_im = vec![0.0; big_n]; + r2c_fft_f64(reals.as_slice().unwrap(), &mut output_re, &mut output_im); + (output_re, output_im) +} + + /// A Python module implemented in Rust. #[pymodule] fn pyphastft(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(fft, m)?)?; + m.add_function(wrap_pyfunction!(rfft, m)?)?; Ok(()) } diff --git a/pyphastft/vis_qt.py b/pyphastft/vis_qt.py index 2a5c5e7..2396ebf 100644 --- a/pyphastft/vis_qt.py +++ b/pyphastft/vis_qt.py @@ -1,115 +1,111 @@ import sys - import numpy as np import pyaudio import pyqtgraph as pg -from pyphastft import fft +from pyphastft import rfft from pyqtgraph.Qt import QtWidgets, QtCore - class RealTimeAudioSpectrum(QtWidgets.QWidget): - def __init__(self, parent=None): - super(RealTimeAudioSpectrum, self).__init__(parent) - self.n_fft_bins = 1024 # Increased FFT size for better frequency resolution - self.n_display_bins = 32 # Maintain the same number of bars in the display - self.sample_rate = 44100 - self.smoothing_factor = 0.1 # Smaller value for more smoothing - self.ema_fft_data = np.zeros( - self.n_display_bins - ) # Adjusted to the number of display bins - self.init_ui() - self.init_audio_stream() - - def init_ui(self): - self.layout = QtWidgets.QVBoxLayout(self) - self.plot_widget = pg.PlotWidget() - self.layout.addWidget(self.plot_widget) - - # Customize plot aesthetics - self.plot_widget.setBackground("k") - self.plot_item = self.plot_widget.getPlotItem() - self.plot_item.setTitle( - "Real-Time Audio Spectrum Visualizer powered by PhastFT", - color="w", - size="16pt", - ) - - # Hide axis labels - self.plot_item.getAxis("left").hide() - self.plot_item.getAxis("bottom").hide() - - # Set fixed ranges for the x and y axes to prevent them from jumping - self.plot_item.setXRange(0, self.sample_rate / 2, padding=0) - self.plot_item.setYRange(0, 1, padding=0) - - self.bar_width = ( - (self.sample_rate / 2) / self.n_display_bins * 0.90 - ) # Adjusted width for display bins - - # Calculate bar positions so they are centered with respect to their frequency values - self.freqs = np.linspace( - 0 + self.bar_width / 2, - self.sample_rate / 2 - self.bar_width / 2, - self.n_display_bins, - ) - - self.bar_graph = pg.BarGraphItem( - x=self.freqs, - height=np.zeros(self.n_display_bins), - width=self.bar_width, - brush=pg.mkBrush("m"), - ) - self.plot_item.addItem(self.bar_graph) - - self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.update) - self.timer.start(50) # Update interval in milliseconds - - def init_audio_stream(self): - self.p = pyaudio.PyAudio() - self.stream = self.p.open( - format=pyaudio.paFloat32, - channels=1, - rate=self.sample_rate, - input=True, - frames_per_buffer=self.n_fft_bins, # This should match the FFT size - stream_callback=self.audio_callback, - ) - self.stream.start_stream() - - def audio_callback(self, in_data, frame_count, time_info, status): - audio_data = np.frombuffer(in_data, dtype=np.float32) - reals = np.zeros(self.n_fft_bins) - imags = np.zeros(self.n_fft_bins) - reals[: len(audio_data)] = audio_data # Fill the reals array with audio data - fft(reals, imags, direction="f") - fft_magnitude = np.sqrt(reals**2 + imags**2)[: self.n_fft_bins // 2] - - # Aggregate or interpolate FFT data to fit into display bins - new_fft_data = np.interp( - np.linspace(0, len(fft_magnitude), self.n_display_bins), - np.arange(len(fft_magnitude)), - fft_magnitude, - ) - - # Apply exponential moving average filter - self.ema_fft_data = self.ema_fft_data * self.smoothing_factor + new_fft_data * ( - 1.0 - self.smoothing_factor - ) - return in_data, pyaudio.paContinue - - def update(self): - self.bar_graph.setOpts(height=self.ema_fft_data, width=self.bar_width) - - def closeEvent(self, event): - self.stream.stop_stream() - self.stream.close() - self.p.terminate() - event.accept() - + def __init__(self, parent=None): + super(RealTimeAudioSpectrum, self).__init__(parent) + self.n_fft_bins = 1024 + self.n_display_bins = 64 + self.sample_rate = 44100 + self.smoothing_factor = 0.1 # Fine-tuned smoothing factor + self.ema_fft_data = np.zeros(self.n_display_bins) + self.init_ui() + self.init_audio_stream() + + def init_ui(self): + self.layout = QtWidgets.QVBoxLayout(self) + self.plot_widget = pg.PlotWidget() + self.layout.addWidget(self.plot_widget) + + self.plot_widget.setBackground("k") + self.plot_item = self.plot_widget.getPlotItem() + self.plot_item.setTitle( + "Real-Time Audio Spectrum Visualizer powered by PhastFT", + color="w", + size="16pt", + ) + + self.plot_item.getAxis("left").hide() + self.plot_item.getAxis("bottom").hide() + + self.plot_item.setXRange(0, self.sample_rate / 2, padding=0) + self.plot_item.setYRange(0, 1, padding=0) + + self.bar_width = (self.sample_rate / 2) / self.n_display_bins * 0.8 + self.freqs = np.linspace( + 0, self.sample_rate / 2, self.n_display_bins, endpoint=False + ) + + self.bar_graph = pg.BarGraphItem( + x=self.freqs, + height=np.zeros(self.n_display_bins), + width=self.bar_width, + brush=pg.mkBrush("m"), + ) + self.plot_item.addItem(self.bar_graph) + + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.update) + self.timer.start(50) + + def init_audio_stream(self): + self.p = pyaudio.PyAudio() + self.stream = self.p.open( + format=pyaudio.paFloat32, + channels=1, + rate=self.sample_rate, + input=True, + frames_per_buffer=self.n_fft_bins, + stream_callback=self.audio_callback, + ) + self.stream.start_stream() + + def audio_callback(self, in_data, frame_count, time_info, status): + audio_data = np.frombuffer(in_data, dtype=np.float32) + audio_data = np.ascontiguousarray(audio_data, dtype=np.float64) + reals, imags = rfft(audio_data, direction="f") + reals = np.ascontiguousarray(reals) + imags = np.ascontiguousarray(imags) + fft_magnitude = np.sqrt(reals**2 + imags**2)[: self.n_fft_bins // 2] + + new_fft_data = np.interp( + np.linspace(0, len(fft_magnitude), self.n_display_bins), + np.arange(len(fft_magnitude)), + fft_magnitude, + ) + + new_fft_data = np.log1p(new_fft_data) # Apply logarithmic scaling + + self.ema_fft_data = self.ema_fft_data * self.smoothing_factor + new_fft_data * ( + 1.0 - self.smoothing_factor + ) + + return in_data, pyaudio.paContinue + + def update(self): + # Normalize the FFT data to ensure it fits within the display range + max_value = np.max(self.ema_fft_data) + if max_value > 0: + normalized_fft_data = self.ema_fft_data / max_value + else: + normalized_fft_data = self.ema_fft_data + + self.bar_graph.setOpts(height=normalized_fft_data, width=self.bar_width) + + def closeEvent(self, event): + self.stream.stop_stream() + self.stream.close() + self.p.terminate() + event.accept() if __name__ == "__main__": - app = QtWidgets.QApplication(sys.argv) - window = RealTimeAudioSpectrum() - window.show() - sys.exit(app.exec_()) + app = QtWidgets.QApplication(sys.argv) + window = RealTimeAudioSpectrum() + window.show() + sys.exit(app.exec_()) + + From 0da032e022cb314497c8c64c5437d9a30c6e3fb2 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 10 Jun 2024 09:39:03 -0400 Subject: [PATCH 18/38] Add dependencies to run audio visualizer example --- pyphastft/{vis_qt.py => audio_visual.py} | 0 pyphastft/requirements.txt | 7 +++++++ 2 files changed, 7 insertions(+) rename pyphastft/{vis_qt.py => audio_visual.py} (100%) create mode 100644 pyphastft/requirements.txt diff --git a/pyphastft/vis_qt.py b/pyphastft/audio_visual.py similarity index 100% rename from pyphastft/vis_qt.py rename to pyphastft/audio_visual.py diff --git a/pyphastft/requirements.txt b/pyphastft/requirements.txt new file mode 100644 index 0000000..122615f --- /dev/null +++ b/pyphastft/requirements.txt @@ -0,0 +1,7 @@ +maturin==1.6.0 +pip==24.0 +PyAudio==0.2.14 +pylint==3.2.3 +pyphastft==0.2.1 +PyQt5==5.15.10 +pyqtgraph==0.13.7 From 9625a0790228016b435ab951da12841dc427d5d4 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 10 Jun 2024 17:55:11 -0400 Subject: [PATCH 19/38] Remove superfluous macro export --- src/fft.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fft.rs b/src/fft.rs index 0934d99..61d7378 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,7 +1,6 @@ //! Implementation of Real valued FFT use crate::{fft_32, fft_64, twiddles::generate_twiddles, Direction}; -#[macro_export] macro_rules! impl_r2c_fft { ($func_name:ident, $precision:ty, $fft_func:ident) => { /// Implementation of Real-Valued FFT From 715342c5c6213ae70de257463322d4d4d1ce14ed Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 10 Jun 2024 17:56:32 -0400 Subject: [PATCH 20/38] Update test hook to run for all features --- hooks/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/pre-commit b/hooks/pre-commit index 6d3f529..f01a5f6 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -15,7 +15,7 @@ then exit 1 fi -if ! cargo test +if ! cargo test --all-features then echo "There are some test issues." exit 1 From fc1fb55307f72f325adf02befc097e4fffe3cb53 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 10 Jun 2024 17:58:19 -0400 Subject: [PATCH 21/38] Update CI tests to test all features --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 157d5aa..626f294 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -63,6 +63,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test + args: --all-features coverage: runs-on: ubuntu-latest From a17daa409b61405c7d25ef030301472ad3abd798 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 10 Jun 2024 21:05:55 -0400 Subject: [PATCH 22/38] Add docs for Real FFT --- src/fft.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/fft.rs b/src/fft.rs index 61d7378..313b291 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -3,11 +3,43 @@ use crate::{fft_32, fft_64, twiddles::generate_twiddles, Direction}; macro_rules! impl_r2c_fft { ($func_name:ident, $precision:ty, $fft_func:ident) => { - /// Implementation of Real-Valued FFT + /// Performs a Real-Valued Fast Fourier Transform (FFT) + /// + /// This function computes the FFT of a real-valued input signal and produces + /// complex-valued output. The implementation follows the principles of splitting + /// the input into even and odd components and then performing the FFT on these + /// components. + /// + /// # Arguments + /// + /// * `input_re` - A slice containing the real-valued input signal. + /// * `output_re` - A mutable slice to store the real parts of the FFT output. + /// * `output_im` - A mutable slice to store the imaginary parts of the FFT output. /// /// # Panics /// /// Panics if `output_re.len() != output_im.len()` and `input_re.len()` == `output_re.len()` + /// + /// # Examples + /// + /// ``` + /// use phastft::fft::{r2c_fft_f32, r2c_fft_f64}; + /// + /// let big_n = 16; + /// let input: Vec = (1..=big_n).map(|x| x as f64).collect(); + /// let mut output_re = vec![0.0; big_n]; + /// let mut output_im = vec![0.0; big_n]; + /// r2c_fft_f64(&input, &mut output_re, &mut output_im); + /// + /// let input: Vec = (1..=big_n).map(|x| x as f32).collect(); + /// let mut output_re: Vec = vec![0.0; 16]; + /// let mut output_im: Vec = vec![0.0; 16]; + /// r2c_fft_f32(&input, &mut output_re, &mut output_im); + /// ``` + /// # References + /// + /// This implementation is based on the concepts discussed in + /// [Levente Kovács' post](https://kovleventer.com/blog/fft_real/). pub fn $func_name( input_re: &[$precision], output_re: &mut [$precision], From 3d1959112382daf307a09c4b2afca8c9333233b8 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Tue, 11 Jun 2024 12:40:43 -0400 Subject: [PATCH 23/38] Cleanup docs for R2C FFT --- src/fft.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 313b291..fbadf34 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -12,9 +12,11 @@ macro_rules! impl_r2c_fft { /// /// # Arguments /// - /// * `input_re` - A slice containing the real-valued input signal. - /// * `output_re` - A mutable slice to store the real parts of the FFT output. - /// * `output_im` - A mutable slice to store the imaginary parts of the FFT output. + /// `input_re` - A slice containing the real-valued input signal. + /// + /// `output_re` - A mutable slice to store the real parts of the FFT output. + /// + /// `output_im` - A mutable slice to store the imaginary parts of the FFT output. /// /// # Panics /// From 62cc47a200b8eafb29b4f0addc41ae933d9037db Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Fri, 14 Jun 2024 14:51:55 -0400 Subject: [PATCH 24/38] Add inverse of R2C FFT (f64 only) --- pyphastft/src/lib.rs | 7 ++- src/fft.rs | 107 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/pyphastft/src/lib.rs b/pyphastft/src/lib.rs index ae1ebbc..a2d805e 100644 --- a/pyphastft/src/lib.rs +++ b/pyphastft/src/lib.rs @@ -1,5 +1,5 @@ use numpy::{PyReadonlyArray1, PyReadwriteArray1}; -use phastft::{fft_64 as fft_64_rs, fft::r2c_fft_f64, planner::Direction}; +use phastft::{fft::r2c_fft_f64, fft_64 as fft_64_rs, planner::Direction}; use pyo3::prelude::*; #[pyfunction] @@ -26,15 +26,14 @@ fn rfft(reals: PyReadonlyArray1, direction: char) -> (Vec, Vec) { } else { Direction::Reverse }; - + let big_n = reals.as_slice().unwrap().len(); let mut output_re = vec![0.0; big_n]; let mut output_im = vec![0.0; big_n]; r2c_fft_f64(reals.as_slice().unwrap(), &mut output_re, &mut output_im); (output_re, output_im) -} - +} /// A Python module implemented in Rust. #[pymodule] diff --git a/src/fft.rs b/src/fft.rs index fbadf34..07a4fe8 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,4 +1,6 @@ //! Implementation of Real valued FFT +use std::simd::prelude::f64x8; + use crate::{fft_32, fft_64, twiddles::generate_twiddles, Direction}; macro_rules! impl_r2c_fft { @@ -151,6 +153,88 @@ macro_rules! impl_r2c_fft { impl_r2c_fft!(r2c_fft_f32, f32, fft_32); impl_r2c_fft!(r2c_fft_f64, f64, fft_64); +/// Performs a Real-Valued, Inverse, Fast Fourier Transform (FFT) +/// +/// # Panics +/// +/// Panics if `reals.len() != imags.len()` *and* `reals.len() != output.len()` +/// +pub fn r2c_ifft_f64(reals: &mut [f64], imags: &mut [f64], output: &mut [f64]) { + assert!(reals.len() == imags.len() && reals.len() == output.len()); + + let big_n = reals.len(); + + let mut z_x_re = vec![0.0; big_n / 2]; + let mut z_x_im = vec![0.0; big_n / 2]; + + let (reals_first_half, reals_second_half) = reals.split_at(big_n / 2); + let (imags_first_half, imags_second_half) = imags.split_at(big_n / 2); + + // Compute Zx + for i in 0..big_n / 2 { + let re = 0.5 * (reals_first_half[i] + reals_second_half[i]); + let im = 0.5 * (imags_first_half[i] + imags_second_half[i]); + + z_x_re[i] = re; + z_x_im[i] = im; + } + + let mut z_y_re = vec![0.0; big_n / 2]; + let mut z_y_im = vec![0.0; big_n / 2]; + let (twiddles_re, twiddles_im) = generate_twiddles::(big_n / 2, Direction::Reverse); + + // Compute W * Zy + for i in 0..big_n / 2 { + let a = reals_first_half[i]; + let b = imags_first_half[i]; + let c = reals_second_half[i]; + let d = imags_second_half[i]; + + let e = a - c; + let f = b - d; + let k = -0.5 * f; + let l = 0.5 * e; + + let m = twiddles_re[i]; + let n = twiddles_im[i]; + + z_y_re[i] = k * m - l * n; + z_y_im[i] = k * n + l * m; + } + + const CHUNK_SIZE: usize = 8; + + // Compute Zx + Zy + z_x_re + .chunks_exact_mut(CHUNK_SIZE) + .zip(z_x_im.chunks_exact_mut(CHUNK_SIZE)) + .zip(z_y_re.chunks_exact(CHUNK_SIZE)) + .zip(z_y_im.chunks_exact(CHUNK_SIZE)) + .for_each(|(((zxr, zxi), zyr), zyi)| { + let mut z_x_r = f64x8::from_slice(zxr); + let mut z_x_i = f64x8::from_slice(zxi); + let z_y_r = f64x8::from_slice(zyr); + let z_y_i = f64x8::from_slice(zyi); + + z_x_r += z_y_r; + z_x_i += z_y_i; + zxr.copy_from_slice(z_x_r.as_array()); + zxi.copy_from_slice(z_x_i.as_array()); + }); + + fft_64(&mut z_x_re, &mut z_x_im, Direction::Reverse); + + // Store reals in the even indices, and imaginaries in the odd indices + output + .chunks_exact_mut(2) + .zip(z_x_re) + .zip(z_x_im) + .for_each(|((out, z_r), z_i)| { + out[0] = z_r; + out[1] = z_i; + }); +} + #[cfg(test)] mod tests { use utilities::assert_float_closeness; @@ -189,4 +273,27 @@ mod tests { impl_r2c_vs_c2c_test!(r2c_vs_c2c_f64, f64, fft_64, r2c_fft_f64, 1e-6); impl_r2c_vs_c2c_test!(r2c_vs_c2c_f32, f32, fft_32, r2c_fft_f32, 3.5); + + #[test] + fn fw_inv_eq_identity() { + let n = 4; + let big_n = 1 << n; + let expected_signal: Vec = (1..=big_n).map(|s| s as f64).collect(); + + let mut output_re = vec![0.0; big_n]; + let mut output_im = vec![0.0; big_n]; + r2c_fft_f64(&expected_signal, &mut output_re, &mut output_im); + + let mut actual_signal = vec![0.0; big_n]; + r2c_ifft_f64(&mut output_re, &mut output_im, &mut actual_signal); + + for (z_a, z_e) in actual_signal.iter().zip(expected_signal.iter()) { + assert_float_closeness(*z_a, *z_e, 1e-10); + } + + println!( + "expected signal: {:?}\nactual signal: {:?}", + expected_signal, actual_signal + ); + } } From 6b4b03cee2e1308ab7adcff052d5fa2aaadffa30 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sat, 15 Jun 2024 15:11:56 -0400 Subject: [PATCH 25/38] Add throughput benchmark for `realfft` --- Cargo.toml | 1 + benches/bench.rs | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d9c39de..24b3549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ multiversion = "0.7" utilities = { path = "utilities" } fftw = "0.8.0" criterion = "0.5.1" +realfft = "3.3.0" [[bench]] name = "bench" diff --git a/benches/bench.rs b/benches/bench.rs index 17eaa8a..2c5c60f 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -1,9 +1,13 @@ use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; -use phastft::{fft::r2c_fft_f64, fft_64, planner::Direction}; +use realfft::num_complex::Complex; +use realfft::RealFftPlanner; use utilities::gen_random_signal; +use phastft::{fft::r2c_fft_f64, fft_64, planner::Direction}; + fn criterion_benchmark(c: &mut Criterion) { - let sizes = vec![1 << 10, 1 << 12, 1 << 14, 1 << 16, 1 << 18, 1 << 20]; + // let sizes = vec![1 << 10, 1 << 12, 1 << 14, 1 << 16, 1 << 18, 1 << 20]; + let sizes = vec![1 << 18, 1 << 20]; let mut group = c.benchmark_group("r2c_versus_c2c"); for &size in &sizes { @@ -39,6 +43,20 @@ fn criterion_benchmark(c: &mut Criterion) { ); }); }); + + group.bench_with_input(BenchmarkId::new("real_fft", size), &size, |b, &size| { + let mut s_re = vec![0.0; size]; + let mut s_im = vec![0.0; size]; + gen_random_signal(&mut s_re, &mut s_im); + let mut output = vec![Complex::default(); s_re.len() / 2 + 1]; + + b.iter(|| { + let mut planner = RealFftPlanner::::new(); + let fft = planner.plan_fft_forward(s_re.len()); + fft.process(&mut s_re, &mut output) + .expect("fft.process() failed!"); + }); + }); } group.finish(); } From 70ea630bc4a3c0d4f8d8c72290b8b1b40bb60e65 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sun, 16 Jun 2024 22:47:50 -0400 Subject: [PATCH 26/38] Use iterators in untangle step --- src/fft.rs | 79 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 07a4fe8..1d9d744 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -36,8 +36,8 @@ macro_rules! impl_r2c_fft { /// r2c_fft_f64(&input, &mut output_re, &mut output_im); /// /// let input: Vec = (1..=big_n).map(|x| x as f32).collect(); - /// let mut output_re: Vec = vec![0.0; 16]; - /// let mut output_im: Vec = vec![0.0; 16]; + /// let mut output_re: Vec = vec![0.0; big_n]; + /// let mut output_im: Vec = vec![0.0; big_n]; /// r2c_fft_f32(&input, &mut output_re, &mut output_im); /// ``` /// # References @@ -59,6 +59,11 @@ macro_rules! impl_r2c_fft { // Z = np.fft.fft(z) $fft_func(&mut z_even, &mut z_odd, Direction::Forward); + // let mut z_x_re = vec![0.0; big_n / 2]; + // let mut z_x_im = vec![0.0; big_n / 2]; + // let mut z_y_re = vec![0.0; big_n / 2]; + // let mut z_y_im = vec![0.0; big_n / 2]; + let mut z_x_re = Vec::with_capacity(z_even.len()); let mut z_x_im = Vec::with_capacity(z_even.len()); let mut z_y_re = Vec::with_capacity(z_even.len()); @@ -115,37 +120,45 @@ macro_rules! impl_r2c_fft { // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) - for i in 0..big_n / 2 { - let zx_re = z_x_re[i]; - let zx_im = z_x_im[i]; - let zy_re = z_y_re[i]; - let zy_im = z_y_im[i]; - let w_re = twiddle_re[i]; - let w_im = twiddle_im[i]; - - let wz_re = w_re * zy_re - w_im * zy_im; - let wz_im = w_re * zy_im + w_im * zy_re; - - // Zx + W * Zy - output_re[i] = zx_re + wz_re; - output_im[i] = zx_im + wz_im; - } - - for i in 0..big_n / 2 { - let zx_re = z_x_re[i]; - let zx_im = z_x_im[i]; - let zy_re = z_y_re[i]; - let zy_im = z_y_im[i]; - let w_re = twiddle_re[i]; - let w_im = twiddle_im[i]; - - let wz_re = w_re * zy_re - w_im * zy_im; - let wz_im = w_re * zy_im + w_im * zy_re; - - // Zx - W * Zy - output_re[i + big_n / 2] = zx_re - wz_re; - output_im[i + big_n / 2] = zx_im - wz_im; - } + z_x_re + .iter() + .zip(z_x_im.iter()) + .zip(z_y_re.iter()) + .zip(z_y_im.iter()) + .zip(twiddle_re.iter()) + .zip(twiddle_im.iter()) + .zip(output_re[..big_n / 2].iter_mut()) + .zip(output_im[..big_n / 2].iter_mut()) + .for_each( + |(((((((zx_re, zx_im), zy_re), zy_im), w_re), w_im), o_re), o_im)| { + let wz_re = w_re * zy_re - w_im * zy_im; + let wz_im = w_re * zy_im + w_im * zy_re; + + // Zx + W * Zy + *o_re = zx_re + wz_re; + *o_im = zx_im + wz_im; + }, + ); + + z_x_re + .iter() + .zip(z_x_im.iter()) + .zip(z_y_re.iter()) + .zip(z_y_im.iter()) + .zip(twiddle_re.iter()) + .zip(twiddle_im.iter()) + .zip(output_re[big_n / 2..].iter_mut()) + .zip(output_im[big_n / 2..].iter_mut()) + .for_each( + |(((((((zx_re, zx_im), zy_re), zy_im), w_re), w_im), o_re), o_im)| { + let wz_re = w_re * zy_re - w_im * zy_im; + let wz_im = w_re * zy_im + w_im * zy_re; + + // Zx + W * Zy + *o_re = zx_re - wz_re; + *o_im = zx_im - wz_im; + }, + ); } }; } From c069bc786bba99c06d9edf532575d80af2f476e7 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 17 Jun 2024 18:03:55 -0400 Subject: [PATCH 27/38] Pre-allocate vectors in lieu of `with_capacity` --- src/fft.rs | 56 +++++++++++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 1d9d744..79fe8b4 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -59,20 +59,10 @@ macro_rules! impl_r2c_fft { // Z = np.fft.fft(z) $fft_func(&mut z_even, &mut z_odd, Direction::Forward); - // let mut z_x_re = vec![0.0; big_n / 2]; - // let mut z_x_im = vec![0.0; big_n / 2]; - // let mut z_y_re = vec![0.0; big_n / 2]; - // let mut z_y_im = vec![0.0; big_n / 2]; - - let mut z_x_re = Vec::with_capacity(z_even.len()); - let mut z_x_im = Vec::with_capacity(z_even.len()); - let mut z_y_re = Vec::with_capacity(z_even.len()); - let mut z_y_im = Vec::with_capacity(z_even.len()); - - z_x_re.push(0.0); - z_x_im.push(0.0); - z_y_re.push(0.0); - z_y_im.push(0.0); + let mut z_x_re = vec![0.0; big_n / 2]; + let mut z_x_im = vec![0.0; big_n / 2]; + let mut z_y_re = vec![0.0; big_n / 2]; + let mut z_y_im = vec![0.0; big_n / 2]; // Zminconj = np.roll(np.flip(Z), 1).conj() // Zx = 0.5 * (Z + Zminconj) @@ -83,22 +73,28 @@ macro_rules! impl_r2c_fft { .zip(z_odd.iter().skip(1)) .zip(z_even.iter().skip(1).rev()) .zip(z_odd.iter().skip(1).rev()) - .for_each(|(((z_e, z_o), z_e_mc), z_o_mc)| { - let a = *z_e; - let b = *z_o; - let c = *z_e_mc; - let d = -(*z_o_mc); - - let t = 0.5 * (a + c); - let u = 0.5 * (b + d); - let v = -0.5 * (a - c); - let w = 0.5 * (b - d); - - z_x_re.push(t); - z_x_im.push(u); - z_y_re.push(w); - z_y_im.push(v); - }); + .zip(z_x_re[1..].iter_mut()) + .zip(z_x_im[1..].iter_mut()) + .zip(z_y_re[1..].iter_mut()) + .zip(z_y_im[1..].iter_mut()) + .for_each( + |(((((((z_e, z_o), z_e_mc), z_o_mc), zx_re), zx_im), zy_re), zy_im)| { + let a = *z_e; + let b = *z_o; + let c = *z_e_mc; + let d = -(*z_o_mc); + + let t = 0.5 * (a + c); + let u = 0.5 * (b + d); + let v = -0.5 * (a - c); + let w = 0.5 * (b - d); + + *zx_re = t; + *zx_im = u; + *zy_re = w; + *zy_im = v; + }, + ); let a = z_even[0]; let b = z_odd[0]; From cc539077098347bb06f6cf6a134746dbf7251935 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 17 Jun 2024 18:19:30 -0400 Subject: [PATCH 28/38] Replace `skip(1)` by using slices --- benches/bench.rs | 3 +-- src/fft.rs | 9 ++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/benches/bench.rs b/benches/bench.rs index 2c5c60f..27a37f0 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -6,8 +6,7 @@ use utilities::gen_random_signal; use phastft::{fft::r2c_fft_f64, fft_64, planner::Direction}; fn criterion_benchmark(c: &mut Criterion) { - // let sizes = vec![1 << 10, 1 << 12, 1 << 14, 1 << 16, 1 << 18, 1 << 20]; - let sizes = vec![1 << 18, 1 << 20]; + let sizes = vec![1 << 10, 1 << 12, 1 << 14, 1 << 16, 1 << 18, 1 << 20]; let mut group = c.benchmark_group("r2c_versus_c2c"); for &size in &sizes { diff --git a/src/fft.rs b/src/fft.rs index 79fe8b4..49fcf00 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -67,12 +67,11 @@ macro_rules! impl_r2c_fft { // Zminconj = np.roll(np.flip(Z), 1).conj() // Zx = 0.5 * (Z + Zminconj) // Zy = -0.5j * (Z - Zminconj) - z_even + z_even[1..] .iter() - .skip(1) - .zip(z_odd.iter().skip(1)) - .zip(z_even.iter().skip(1).rev()) - .zip(z_odd.iter().skip(1).rev()) + .zip(z_odd[1..].iter()) + .zip(z_even[1..].iter().rev()) + .zip(z_odd[1..].iter().rev()) .zip(z_x_re[1..].iter_mut()) .zip(z_x_im[1..].iter_mut()) .zip(z_y_re[1..].iter_mut()) From 02f4056ca61995544e63b19427839ef58f5e6729 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 17 Jun 2024 18:48:21 -0400 Subject: [PATCH 29/38] Fuse separate iterators together --- src/fft.rs | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 49fcf00..ea6439b 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -114,6 +114,8 @@ macro_rules! impl_r2c_fft { generate_twiddles(big_n / 2, Direction::Forward); // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) + let (output_re_first_half, output_re_second_half) = output_re.split_at_mut(big_n / 2); + let (output_im_first_half, output_im_second_half) = output_im.split_at_mut(big_n / 2); z_x_re .iter() @@ -122,36 +124,28 @@ macro_rules! impl_r2c_fft { .zip(z_y_im.iter()) .zip(twiddle_re.iter()) .zip(twiddle_im.iter()) - .zip(output_re[..big_n / 2].iter_mut()) - .zip(output_im[..big_n / 2].iter_mut()) + .zip(output_re_first_half) + .zip(output_im_first_half) + .zip(output_re_second_half) + .zip(output_im_second_half) .for_each( - |(((((((zx_re, zx_im), zy_re), zy_im), w_re), w_im), o_re), o_im)| { + |( + ( + (((((((zx_re, zx_im), zy_re), zy_im), w_re), w_im), o_re_fh), o_im_fh), + o_re_sh, + ), + o_im_sh, + )| { let wz_re = w_re * zy_re - w_im * zy_im; let wz_im = w_re * zy_im + w_im * zy_re; // Zx + W * Zy - *o_re = zx_re + wz_re; - *o_im = zx_im + wz_im; - }, - ); - - z_x_re - .iter() - .zip(z_x_im.iter()) - .zip(z_y_re.iter()) - .zip(z_y_im.iter()) - .zip(twiddle_re.iter()) - .zip(twiddle_im.iter()) - .zip(output_re[big_n / 2..].iter_mut()) - .zip(output_im[big_n / 2..].iter_mut()) - .for_each( - |(((((((zx_re, zx_im), zy_re), zy_im), w_re), w_im), o_re), o_im)| { - let wz_re = w_re * zy_re - w_im * zy_im; - let wz_im = w_re * zy_im + w_im * zy_re; + *o_re_fh = zx_re + wz_re; + *o_im_fh = zx_im + wz_im; - // Zx + W * Zy - *o_re = zx_re - wz_re; - *o_im = zx_im - wz_im; + // Zx - W * Zy + *o_re_sh = zx_re - wz_re; + *o_im_sh = zx_im - wz_im; }, ); } From e2d8695e5976c6820e5139474a3c998c1be976b9 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 17 Jun 2024 19:08:37 -0400 Subject: [PATCH 30/38] Use SIMD twiddle factor generator for N/2 >= 16 --- src/fft.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index ea6439b..ea322f0 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,10 +1,11 @@ //! Implementation of Real valued FFT use std::simd::prelude::f64x8; +use crate::twiddles::{generate_twiddles_simd_32, generate_twiddles_simd_64}; use crate::{fft_32, fft_64, twiddles::generate_twiddles, Direction}; macro_rules! impl_r2c_fft { - ($func_name:ident, $precision:ty, $fft_func:ident) => { + ($func_name:ident, $precision:ty, $fft_func:ident, $gen_twiddles_simd:ident) => { /// Performs a Real-Valued Fast Fourier Transform (FFT) /// /// This function computes the FFT of a real-valued input signal and produces @@ -110,8 +111,11 @@ macro_rules! impl_r2c_fft { z_y_re[0] = w; z_y_im[0] = v; - let (twiddle_re, twiddle_im): (Vec<$precision>, Vec<$precision>) = - generate_twiddles(big_n / 2, Direction::Forward); + let (twiddle_re, twiddle_im): (Vec<$precision>, Vec<$precision>) = if big_n / 2 >= 16 { + $gen_twiddles_simd(big_n / 2, Direction::Forward) + } else { + generate_twiddles(big_n / 2, Direction::Forward) + }; // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) let (output_re_first_half, output_re_second_half) = output_re.split_at_mut(big_n / 2); @@ -152,8 +156,8 @@ macro_rules! impl_r2c_fft { }; } -impl_r2c_fft!(r2c_fft_f32, f32, fft_32); -impl_r2c_fft!(r2c_fft_f64, f64, fft_64); +impl_r2c_fft!(r2c_fft_f32, f32, fft_32, generate_twiddles_simd_32); +impl_r2c_fft!(r2c_fft_f64, f64, fft_64, generate_twiddles_simd_64); /// Performs a Real-Valued, Inverse, Fast Fourier Transform (FFT) /// From 892868f901cfb727f401e521fb62056cf05d931a Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Tue, 18 Jun 2024 16:58:19 -0400 Subject: [PATCH 31/38] Generate twiddles once for untangling & Planner --- examples/profile.rs | 13 ++++++++++++- src/fft.rs | 36 +++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/examples/profile.rs b/examples/profile.rs index 6725046..941f420 100644 --- a/examples/profile.rs +++ b/examples/profile.rs @@ -1,9 +1,11 @@ use std::env; use std::str::FromStr; +use phastft::fft::r2c_fft_f64; use phastft::fft_64; use phastft::planner::Direction; +#[allow(dead_code)] fn benchmark_fft(num_qubits: usize) { let n = 1 << num_qubits; let mut reals: Vec = (1..=n).map(|i| i as f64).collect(); @@ -11,10 +13,19 @@ fn benchmark_fft(num_qubits: usize) { fft_64(&mut reals, &mut imags, Direction::Forward); } +fn benchmark_r2c_fft(n: usize) { + let big_n = 1 << n; + let reals: Vec = (1..=big_n).map(|i| i as f64).collect(); + let mut output_re = vec![0.0; big_n]; + let mut output_im = vec![0.0; big_n]; + r2c_fft_f64(&reals, &mut output_re, &mut output_im); +} + fn main() { let args: Vec = env::args().collect(); assert_eq!(args.len(), 2, "Usage {} ", args[0]); let n = usize::from_str(&args[1]).unwrap(); - benchmark_fft(n); + // benchmark_fft(n); + benchmark_r2c_fft(n); } diff --git a/src/fft.rs b/src/fft.rs index ea322f0..3acacdf 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -1,11 +1,15 @@ //! Implementation of Real valued FFT use std::simd::prelude::f64x8; -use crate::twiddles::{generate_twiddles_simd_32, generate_twiddles_simd_64}; -use crate::{fft_32, fft_64, twiddles::generate_twiddles, Direction}; +use crate::planner::{Planner32, Planner64}; +use crate::twiddles::filter_twiddles; +use crate::{ + fft_32_with_opts_and_plan, fft_64, fft_64_with_opts_and_plan, twiddles::generate_twiddles, + Direction, Options, +}; macro_rules! impl_r2c_fft { - ($func_name:ident, $precision:ty, $fft_func:ident, $gen_twiddles_simd:ident) => { + ($func_name:ident, $precision:ty, $planner:ident, $fft_w_opts_and_plan:ident) => { /// Performs a Real-Valued Fast Fourier Transform (FFT) /// /// This function computes the FFT of a real-valued input signal and produces @@ -57,9 +61,19 @@ macro_rules! impl_r2c_fft { let (mut z_even, mut z_odd): (Vec<_>, Vec<_>) = input_re.chunks_exact(2).map(|c| (c[0], c[1])).unzip(); - // Z = np.fft.fft(z) - $fft_func(&mut z_even, &mut z_odd, Direction::Forward); + let mut planner = <$planner>::new(big_n, Direction::Forward); + + // save these for the untanngling step + let twiddle_re = planner.twiddles_re.clone(); + let twiddle_im = planner.twiddles_im.clone(); + + // We only need (N / 2) / 2 twiddle factors for the actual FFT call, so we filter + filter_twiddles(&mut planner.twiddles_re, &mut planner.twiddles_im); + + let opts = Options::guess_options(z_even.len()); + $fft_w_opts_and_plan(&mut z_even, &mut z_odd, &opts, &mut planner); + // Z = np.fft.fft(z) let mut z_x_re = vec![0.0; big_n / 2]; let mut z_x_im = vec![0.0; big_n / 2]; let mut z_y_re = vec![0.0; big_n / 2]; @@ -111,12 +125,6 @@ macro_rules! impl_r2c_fft { z_y_re[0] = w; z_y_im[0] = v; - let (twiddle_re, twiddle_im): (Vec<$precision>, Vec<$precision>) = if big_n / 2 >= 16 { - $gen_twiddles_simd(big_n / 2, Direction::Forward) - } else { - generate_twiddles(big_n / 2, Direction::Forward) - }; - // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) let (output_re_first_half, output_re_second_half) = output_re.split_at_mut(big_n / 2); let (output_im_first_half, output_im_second_half) = output_im.split_at_mut(big_n / 2); @@ -156,8 +164,8 @@ macro_rules! impl_r2c_fft { }; } -impl_r2c_fft!(r2c_fft_f32, f32, fft_32, generate_twiddles_simd_32); -impl_r2c_fft!(r2c_fft_f64, f64, fft_64, generate_twiddles_simd_64); +impl_r2c_fft!(r2c_fft_f32, f32, Planner32, fft_32_with_opts_and_plan); +impl_r2c_fft!(r2c_fft_f64, f64, Planner64, fft_64_with_opts_and_plan); /// Performs a Real-Valued, Inverse, Fast Fourier Transform (FFT) /// @@ -245,6 +253,8 @@ pub fn r2c_ifft_f64(reals: &mut [f64], imags: &mut [f64], output: &mut [f64]) { mod tests { use utilities::assert_float_closeness; + use crate::fft_32; + use super::*; macro_rules! impl_r2c_vs_c2c_test { From bde116378d772891ac27b076edb7896335bc9b66 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Thu, 15 Aug 2024 14:37:43 -0400 Subject: [PATCH 32/38] Fix bug in benchmarks and fix clippy complaints --- benches/bench.rs | 27 +++++++++++++-------------- examples/profile.rs | 5 +---- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/benches/bench.rs b/benches/bench.rs index 680960d..0a44d18 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -5,7 +5,8 @@ use utilities::gen_random_signal; use num_traits::Float; use phastft::{ - fft_32_with_opts_and_plan, fft_64_with_opts_and_plan, + fft::r2c_fft_f64, + fft_32_with_opts_and_plan, fft_64, fft_64_with_opts_and_plan, options::Options, planner::{Direction, Planner32, Planner64}, }; @@ -13,11 +14,8 @@ use rand::{ distributions::{Distribution, Standard}, thread_rng, Rng, }; -use utilities::rustfft::num_complex::Complex; use utilities::rustfft::FftPlanner; -use phastft::{fft::r2c_fft_f64, fft_64, planner::Direction}; - fn benchmark_r2c_vs_c2c(c: &mut Criterion) { let sizes = vec![1 << 10, 1 << 12, 1 << 14, 1 << 16, 1 << 18, 1 << 20]; @@ -69,8 +67,8 @@ fn benchmark_r2c_vs_c2c(c: &mut Criterion) { .expect("fft.process() failed!"); }); }); - } - + } +} const LENGTHS: &[usize] = &[ 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, @@ -230,12 +228,13 @@ fn benchmark_inverse_f64(c: &mut Criterion) { } } -criterion_group!( - benches, - benchmark_forward_f32, - benchmark_inverse_f32, - benchmark_forward_f64, - benchmark_inverse_f64 - benchmark_r2c_vs_c2c -); +fn critertion_benchmark(c: &mut Criterion) { + benchmark_forward_f32(c); + benchmark_inverse_f32(c); + benchmark_forward_f64(c); + benchmark_inverse_f64(c); + benchmark_r2c_vs_c2c(c); +} + +criterion_group!(benches, critertion_benchmark,); criterion_main!(benches); diff --git a/examples/profile.rs b/examples/profile.rs index 13d2e59..1399b38 100644 --- a/examples/profile.rs +++ b/examples/profile.rs @@ -3,13 +3,10 @@ use std::str::FromStr; use phastft::fft::r2c_fft_f64; use phastft::fft_64; -use phastft::planner::Direction; - -use utilities::gen_random_signal; - use phastft::fft_64_with_opts_and_plan; use phastft::options::Options; use phastft::planner::{Direction, Planner64}; +use utilities::gen_random_signal; fn benchmark_fft_64(n: usize) { let big_n = 1 << n; From e7dbd86db4bc195aeb9af09274916cce84d6de7d Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Thu, 15 Aug 2024 14:43:42 -0400 Subject: [PATCH 33/38] Comment out uneeded example code --- benches/bench.rs | 2 +- examples/profile.rs | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/benches/bench.rs b/benches/bench.rs index 0a44d18..d464f37 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -32,7 +32,7 @@ fn benchmark_r2c_vs_c2c(c: &mut Criterion) { let mut output_re = vec![0.0; size]; let mut output_im = vec![0.0; size]; r2c_fft_f64( - black_box(&mut s_re), + black_box(&s_re), black_box(&mut output_re), black_box(&mut output_im), ); diff --git a/examples/profile.rs b/examples/profile.rs index 1399b38..910b20e 100644 --- a/examples/profile.rs +++ b/examples/profile.rs @@ -2,23 +2,23 @@ use std::env; use std::str::FromStr; use phastft::fft::r2c_fft_f64; -use phastft::fft_64; -use phastft::fft_64_with_opts_and_plan; -use phastft::options::Options; -use phastft::planner::{Direction, Planner64}; -use utilities::gen_random_signal; +// use phastft::fft_64; +// use phastft::fft_64_with_opts_and_plan; +// use phastft::options::Options; +// use phastft::planner::{Direction, Planner64}; +// use utilities::gen_random_signal; -fn benchmark_fft_64(n: usize) { - let big_n = 1 << n; - let mut reals = vec![0.0; big_n]; - let mut imags = vec![0.0; big_n]; - gen_random_signal(&mut reals, &mut imags); - - let planner = Planner64::new(reals.len(), Direction::Forward); - let opts = Options::guess_options(reals.len()); - - fft_64_with_opts_and_plan(&mut reals, &mut imags, &opts, &planner); -} +// fn benchmark_fft_64(n: usize) { +// let big_n = 1 << n; +// let mut reals = vec![0.0; big_n]; +// let mut imags = vec![0.0; big_n]; +// gen_random_signal(&mut reals, &mut imags); +// +// let planner = Planner64::new(reals.len(), Direction::Forward); +// let opts = Options::guess_options(reals.len()); +// +// fft_64_with_opts_and_plan(&mut reals, &mut imags, &opts, &planner); +// } fn benchmark_r2c_fft(n: usize) { let big_n = 1 << n; From 0c10592182bf458b6e5cf011f1bd3e3dc4d3414b Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Thu, 15 Aug 2024 15:01:47 -0400 Subject: [PATCH 34/38] Fix bug in r2c fft using extra planner - This is a quick, but hacky and inefficient way to use the twiddle factors generated by the `Planner`, but it works. - Add more details to assert statements for debugging --- src/fft.rs | 9 +++++---- src/lib.rs | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 3acacdf..0c42be1 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -2,7 +2,6 @@ use std::simd::prelude::f64x8; use crate::planner::{Planner32, Planner64}; -use crate::twiddles::filter_twiddles; use crate::{ fft_32_with_opts_and_plan, fft_64, fft_64_with_opts_and_plan, twiddles::generate_twiddles, Direction, Options, @@ -64,11 +63,13 @@ macro_rules! impl_r2c_fft { let mut planner = <$planner>::new(big_n, Direction::Forward); // save these for the untanngling step - let twiddle_re = planner.twiddles_re.clone(); - let twiddle_im = planner.twiddles_im.clone(); + let twiddle_re = planner.twiddles_re; + let twiddle_im = planner.twiddles_im; + + planner = <$planner>::new(big_n / 2, Direction::Forward); // We only need (N / 2) / 2 twiddle factors for the actual FFT call, so we filter - filter_twiddles(&mut planner.twiddles_re, &mut planner.twiddles_im); + // filter_twiddles(&mut planner.twiddles_re, &mut planner.twiddles_im); let opts = Options::guess_options(z_even.len()); $fft_w_opts_and_plan(&mut z_even, &mut z_odd, &opts, &mut planner); diff --git a/src/lib.rs b/src/lib.rs index 8965e7b..5a5ee94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,10 +92,10 @@ macro_rules! impl_fft_interleaved_for { }; } -#[doc(cfg(feature = "complex-nums"))] +// #[doc(cfg(feature = "complex-nums"))] #[cfg(feature = "complex-nums")] impl_fft_interleaved_for!(fft_32_interleaved, f32, fft_32, deinterleave_complex32); -#[doc(cfg(feature = "complex-nums"))] +// #[doc(cfg(feature = "complex-nums"))] #[cfg(feature = "complex-nums")] impl_fft_interleaved_for!(fft_64_interleaved, f64, fft_64, deinterleave_complex64); @@ -129,7 +129,7 @@ macro_rules! impl_fft_with_opts_and_plan_for { opts: &Options, planner: &$planner, ) { - assert!(reals.len() == imags.len() && reals.len().is_power_of_two()); + assert!(reals.len() == imags.len() && reals.len().is_power_of_two(), "reals.len() and imags.len() must be equal, and both should be a power of 2. Actual lengths - reals: {} imags: {}", reals.len(), imags.len()); let n: usize = reals.len().ilog2() as usize; // Use references to avoid unnecessary clones @@ -138,7 +138,7 @@ macro_rules! impl_fft_with_opts_and_plan_for { // We shouldn't be able to execute FFT if the # of twiddles isn't equal to the distance // between pairs - assert!(twiddles_re.len() == reals.len() / 2 && twiddles_im.len() == imags.len() / 2); + assert!(twiddles_re.len() == reals.len() / 2 && twiddles_im.len() == imags.len() / 2,"got: {} == {} and {} == {}", twiddles_re.len(), reals.len() / 2, twiddles_im.len(), imags.len() / 2); match planner.direction { Direction::Reverse => { From 763b4d509c89c606ff1df4bf623ab9f6d0fe443a Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Thu, 15 Aug 2024 17:19:28 -0400 Subject: [PATCH 35/38] Use `Twiddles` iter for untangling step in R2C FFT --- src/fft.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index 0c42be1..a12ddc1 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -3,7 +3,8 @@ use std::simd::prelude::f64x8; use crate::planner::{Planner32, Planner64}; use crate::{ - fft_32_with_opts_and_plan, fft_64, fft_64_with_opts_and_plan, twiddles::generate_twiddles, + fft_32_with_opts_and_plan, fft_64, fft_64_with_opts_and_plan, + twiddles::{generate_twiddles, Twiddles}, Direction, Options, }; @@ -60,19 +61,21 @@ macro_rules! impl_r2c_fft { let (mut z_even, mut z_odd): (Vec<_>, Vec<_>) = input_re.chunks_exact(2).map(|c| (c[0], c[1])).unzip(); - let mut planner = <$planner>::new(big_n, Direction::Forward); + // let mut planner = <$planner>::new(big_n, Direction::Forward); // save these for the untanngling step - let twiddle_re = planner.twiddles_re; - let twiddle_im = planner.twiddles_im; + // let twiddle_re = planner.twiddles_re; + // let twiddle_im = planner.twiddles_im; + let stride = big_n / 2; + let twiddles_iter = Twiddles::<$precision>::new(stride); - planner = <$planner>::new(big_n / 2, Direction::Forward); + let planner = <$planner>::new(big_n / 2, Direction::Forward); // We only need (N / 2) / 2 twiddle factors for the actual FFT call, so we filter // filter_twiddles(&mut planner.twiddles_re, &mut planner.twiddles_im); let opts = Options::guess_options(z_even.len()); - $fft_w_opts_and_plan(&mut z_even, &mut z_odd, &opts, &mut planner); + $fft_w_opts_and_plan(&mut z_even, &mut z_odd, &opts, &planner); // Z = np.fft.fft(z) let mut z_x_re = vec![0.0; big_n / 2]; @@ -135,19 +138,18 @@ macro_rules! impl_r2c_fft { .zip(z_x_im.iter()) .zip(z_y_re.iter()) .zip(z_y_im.iter()) - .zip(twiddle_re.iter()) - .zip(twiddle_im.iter()) .zip(output_re_first_half) .zip(output_im_first_half) .zip(output_re_second_half) .zip(output_im_second_half) + .zip(twiddles_iter) .for_each( |( ( - (((((((zx_re, zx_im), zy_re), zy_im), w_re), w_im), o_re_fh), o_im_fh), - o_re_sh, + ((((((zx_re, zx_im), zy_re), zy_im), o_re_fh), o_im_fh), o_re_sh), + o_im_sh, ), - o_im_sh, + (w_re, w_im), )| { let wz_re = w_re * zy_re - w_im * zy_im; let wz_im = w_re * zy_im + w_im * zy_re; From de59553e9959d78fb3f893c200f345012d5488f0 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Fri, 16 Aug 2024 20:18:58 -0400 Subject: [PATCH 36/38] Move planners out of r2c vs c2c benchmarks --- benches/bench.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/benches/bench.rs b/benches/bench.rs index d464f37..9a49f74 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -6,7 +6,7 @@ use utilities::gen_random_signal; use num_traits::Float; use phastft::{ fft::r2c_fft_f64, - fft_32_with_opts_and_plan, fft_64, fft_64_with_opts_and_plan, + fft_32_with_opts_and_plan, fft_64_with_opts_and_plan, options::Options, planner::{Direction, Planner32, Planner64}, }; @@ -27,10 +27,10 @@ fn benchmark_r2c_vs_c2c(c: &mut Criterion) { let mut s_re = vec![0.0; size]; let mut s_im = vec![0.0; size]; gen_random_signal(&mut s_re, &mut s_im); + let mut output_re = vec![0.0; size]; + let mut output_im = vec![0.0; size]; b.iter(|| { - let mut output_re = vec![0.0; size]; - let mut output_im = vec![0.0; size]; r2c_fft_f64( black_box(&s_re), black_box(&mut output_re), @@ -45,11 +45,15 @@ fn benchmark_r2c_vs_c2c(c: &mut Criterion) { gen_random_signal(&mut s_re, &mut s_im); s_im = vec![0.0; size]; + let options = Options::guess_options(s_re.len()); + let planner = Planner64::new(s_re.len(), Direction::Reverse); + b.iter(|| { - fft_64( + fft_64_with_opts_and_plan( black_box(&mut s_re), black_box(&mut s_im), - Direction::Forward, + black_box(&options), + black_box(&planner), ); }); }); @@ -59,11 +63,12 @@ fn benchmark_r2c_vs_c2c(c: &mut Criterion) { let mut s_im = vec![0.0; size]; gen_random_signal(&mut s_re, &mut s_im); let mut output = vec![Complex::default(); s_re.len() / 2 + 1]; + let mut planner = RealFftPlanner::::new(); b.iter(|| { - let mut planner = RealFftPlanner::::new(); - let fft = planner.plan_fft_forward(s_re.len()); - fft.process(&mut s_re, &mut output) + planner + .plan_fft_forward(s_re.len()) + .process(&mut s_re, &mut output) .expect("fft.process() failed!"); }); }); From 3e819b40bcd94e862dd4973f780de760ec8a20af Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sat, 17 Aug 2024 13:04:20 -0400 Subject: [PATCH 37/38] Replace naive deinterleave in R2C FFT --- src/fft.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index a12ddc1..d75c75f 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -5,6 +5,7 @@ use crate::planner::{Planner32, Planner64}; use crate::{ fft_32_with_opts_and_plan, fft_64, fft_64_with_opts_and_plan, twiddles::{generate_twiddles, Twiddles}, + utils::deinterleave, Direction, Options, }; @@ -58,8 +59,8 @@ macro_rules! impl_r2c_fft { let big_n = input_re.len(); // Splitting odd and even - let (mut z_even, mut z_odd): (Vec<_>, Vec<_>) = - input_re.chunks_exact(2).map(|c| (c[0], c[1])).unzip(); + + let (mut z_even, mut z_odd): (Vec<_>, Vec<_>) = deinterleave(&input_re); // let mut planner = <$planner>::new(big_n, Direction::Forward); From d0996474a8bc16e40dba36ad7622661a0bc20070 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sat, 17 Aug 2024 19:39:39 -0400 Subject: [PATCH 38/38] Add more comments to explain r2c fft --- src/fft.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/fft.rs b/src/fft.rs index d75c75f..13606bd 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -58,18 +58,11 @@ macro_rules! impl_r2c_fft { assert!(output_re.len() == output_im.len() && input_re.len() == output_re.len()); let big_n = input_re.len(); - // Splitting odd and even - + // Split real signal of size `N` into odd (size `N/2`) and even (size `N/2`) via deinterleave + // The even part is now the "real" components and the odd part is now the "imaginary" components let (mut z_even, mut z_odd): (Vec<_>, Vec<_>) = deinterleave(&input_re); - // let mut planner = <$planner>::new(big_n, Direction::Forward); - - // save these for the untanngling step - // let twiddle_re = planner.twiddles_re; - // let twiddle_im = planner.twiddles_im; let stride = big_n / 2; - let twiddles_iter = Twiddles::<$precision>::new(stride); - let planner = <$planner>::new(big_n / 2, Direction::Forward); // We only need (N / 2) / 2 twiddle factors for the actual FFT call, so we filter @@ -133,6 +126,7 @@ macro_rules! impl_r2c_fft { // Zall = np.concatenate([Zx + W*Zy, Zx - W*Zy]) let (output_re_first_half, output_re_second_half) = output_re.split_at_mut(big_n / 2); let (output_im_first_half, output_im_second_half) = output_im.split_at_mut(big_n / 2); + let twiddles_iter = Twiddles::<$precision>::new(stride); z_x_re .iter()