From b1d70f1381bca89d0eabdc7c9edab7af8007050b Mon Sep 17 00:00:00 2001 From: HEnquist Date: Thu, 12 Jun 2025 22:03:26 +0200 Subject: [PATCH] Bump version, update dependencies, tweak docs --- Cargo.toml | 8 +-- README.md | 90 +++++++++++++++---------- src/lib.rs | 191 +++++------------------------------------------------ 3 files changed, 75 insertions(+), 214 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4688e1c..4f97bcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "realfft" -version = "3.4.0" +version = "3.5.0" authors = ["HEnquist "] edition = "2018" description = "Real-to-complex forward FFT and complex-to-real inverse FFT for Rust" @@ -22,11 +22,11 @@ neon = ["rustfft/neon"] wasm_simd = ["rustfft/wasm_simd"] [dependencies] -rustfft = { version = "6.2.0", default-features = false } +rustfft = { version = "6.4.0", default-features = false } [dev-dependencies] -criterion = "0.3" -rand = "0.8.5" +criterion = "0.6" +rand = "0.9" [[bench]] name = "realfft" diff --git a/README.md b/README.md index 4af3add..63d9661 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ -# realfft - -## RealFFT: Real-to-complex forward FFT and complex-to-real inverse FFT based on RustFFT +# RealFFT: Real-to-complex forward FFT and complex-to-real inverse FFT based on RustFFT This library is a wrapper for [RustFFT](https://crates.io/crates/rustfft) that enables fast and convenient FFT of real-valued data. -The API is designed to be as similar as possible to RustFFT. Also, the feature flags are passed -on to RustFFT, allowing selection of its features – they do not affect RealFFT itself. +The API is designed to be as similar as possible to RustFFT. +Also, the [feature flags](#cargo-features) are passed on to RustFFT, allowing +selection of its features – they do not affect RealFFT itself. Using this library instead of RustFFT directly avoids the need of converting real-valued data to complex before performing a FFT. @@ -25,29 +24,29 @@ there is an improvement of about a factor 2. The difference shrinks for shorter lengths, and around 30 elements there is no longer any difference. -### Why use real-to-complex FFT? -#### Using a complex-to-complex FFT +## Why use real-to-complex FFT? +### Using a complex-to-complex FFT A simple way to transform a real valued signal is to convert it to complex, and then use a complex-to-complex FFT. Let's assume `x` is a 6 element long real vector: -``` +```text x = [x0r, x1r, x2r, x3r, x4r, x5r] ``` We now convert `x` to complex by adding an imaginary part with value zero. Using the notation `(xNr, xNi)` for the complex value `xN`, this becomes: -``` +```text x_c = [(x0r, 0), (x1r, 0), (x2r, 0), (x3r, 0), (x4r, 0), (x5r, 0)] ``` Performing a normal complex FFT, the result of `FFT(x_c)` is: -``` +```text FFT(x_c) = [(X0r, X0i), (X1r, X1i), (X2r, X2i), (X3r, X3i), (X4r, X4i), (X5r, X5i)] ``` But because our `x_c` is real-valued (all imaginary parts are zero), some of this becomes redundant: -``` +```text FFT(x_c) = [(X0r, 0), (X1r, X1i), (X2r, X2i), (X3r, 0), (X2r, -X2i), (X1r, -X1i)] ``` @@ -57,54 +56,54 @@ But it still takes time for the FFT to calculate the redundant values. Converting the input data to complex also takes a little bit of time. If the length of `x` instead had been 7, result would have been: -``` +```text FFT(x_c) = [(X0r, 0), (X1r, X1i), (X2r, X2i), (X3r, X3i), (X3r, -X3i), (X2r, -X2i), (X1r, -X1i)] ``` The result is similar, but this time there is no zero at `X3i`. Also in this case we got the same number of independent values as we started with. -#### Real-to-complex +### Real-to-complex Using a real-to-complex FFT removes the need for converting the input data to complex. It also avoids calculating the redundant output values. The result for 6 elements is: -``` +```text RealFFT(x) = [(X0r, 0), (X1r, X1i), (X2r, X2i), (X3r, 0)] ``` The result for 7 elements is: -``` +```text RealFFT(x) = [(X0r, 0), (X1r, X1i), (X2r, X2i), (X3r, X3i)] ``` This is the data layout output by the real-to-complex FFT, and the one expected as input to the complex-to-real inverse FFT. -### Scaling +## Scaling RealFFT matches the behaviour of RustFFT and does not normalize the output of either forward or inverse FFT. To get normalized results, each element must be scaled by `1/sqrt(length)`, where `length` is the length of the real-valued signal. If the processing involves both an FFT and an iFFT step, it is advisable to merge the two normalization steps to a single, by scaling by `1/length`. -### Documentation +## Documentation The full documentation can be generated by rustdoc. To generate and view it run: -``` +```sh cargo doc --open ``` -### Benchmarks +## Benchmarks To run a set of benchmarks comparing real-to-complex FFT with standard complex-to-complex, type: -``` +```sh cargo bench ``` The results are printed while running, and are compiled into an html report containing much more details. To view, open `target/criterion/report/index.html` in a browser. -### Example +## Example Transform a signal, and then inverse transform the result. ```rust use realfft::RealFftPlanner; @@ -141,20 +140,41 @@ assert_eq!(outdata.len(), length); c2r.process(&mut spectrum, &mut outdata).unwrap(); ``` -#### Versions -- 3.4.0: Fix undefined behavior reported by Miri. - Update to latest RustFFT. -- 3.3.0: Add method for getting the length of the complex input/output. - Bugfix: clean up numerical noise in the zero imaginary components. -- 3.2.0: Allow scratch buffer to be larger than needed. -- 3.1.0: Update to RustFFT 6.1 with Neon support. -- 3.0.2: Fix confusing typos in errors about scratch length. -- 3.0.1: More helpful error messages, fix confusing typos. -- 3.0.0: Improved error reporting. -- 2.0.1: Minor bugfix. -- 2.0.0: Update RustFFT to 6.0.0 and num-complex to 0.4.0. -- 1.1.0: Add missing Sync+Send. -- 1.0.0: First version with new api. +## Cargo features +RealFFT has the same set of cargo feature flags as RustFFT. +These features are used to control the features of RustFFT +and they do not affect RealFFT itself. +The default is to enable the default features of RustFFT. +See the [RustFFT documentation](https://docs.rs/rustfft/latest/rustfft/#feature-flags) +for more details. + +## Versions +- 3.5.0 + - Add cargo features to set the corresponding RustFFT features. +- 3.4.0 + - Fix undefined behavior reported by Miri. + - Update to latest RustFFT. +- 3.3.0 + - Add method for getting the length of the complex input/output. + - Bugfix: clean up numerical noise in the zero imaginary components. +- 3.2.0 + - Allow scratch buffer to be larger than needed. +- 3.1.0 + - Update to RustFFT 6.1 with Neon support. +- 3.0.2 + - Fix confusing typos in errors about scratch length. +- 3.0.1 + - More helpful error messages, fix confusing typos. +- 3.0.0 + - Improved error reporting. +- 2.0.1 + - Minor bugfix. +- 2.0.0 + - Update RustFFT to 6.0.0 and num-complex to 0.4.0. +- 1.1.0 + - Add missing Sync+Send. +- 1.0.0 + - First version with new api. #### Compatibility diff --git a/src/lib.rs b/src/lib.rs index c1b3bf3..54aa63c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,163 +1,4 @@ -//! # RealFFT: Real-to-complex forward FFT and complex-to-real inverse FFT based on RustFFT -//! -//! This library is a wrapper for [RustFFT](https://crates.io/crates/rustfft) -//! that enables fast and convenient FFT of real-valued data. -//! The API is designed to be as similar as possible to RustFFT. Also, the feature flags are passed -//! on to RustFFT, allowing selection of its features – they do not affect RealFFT itself. -//! -//! Using this library instead of RustFFT directly avoids the need of converting -//! real-valued data to complex before performing a FFT. -//! If the length is even, it also enables faster computations by using a complex FFT of half the length. -//! It then packs a real-valued signal of length N into an N/2 long complex buffer, -//! which is transformed using a standard complex-to-complex FFT. -//! The FFT result is then post-processed to give only the first half of the complex spectrum, -//! as an N/2+1 long complex vector. -//! -//! The inverse FFT goes through the same steps backwards, -//! to transform a complex spectrum of length N/2+1 to a real-valued signal of length N. -//! -//! The speed increase compared to just converting the input to a length N complex vector -//! and using a length N complex-to-complex FFT depends on the length of the input data. -//! The largest improvements are for longer FFTs and for lengths over around 1000 elements -//! there is an improvement of about a factor 2. -//! The difference shrinks for shorter lengths, -//! and around 30 elements there is no longer any difference. -//! -//! ## Why use real-to-complex FFT? -//! ### Using a complex-to-complex FFT -//! A simple way to transform a real valued signal is to convert it to complex, -//! and then use a complex-to-complex FFT. -//! -//! Let's assume `x` is a 6 element long real vector: -//! ```text -//! x = [x0r, x1r, x2r, x3r, x4r, x5r] -//! ``` -//! -//! We now convert `x` to complex by adding an imaginary part with value zero. -//! Using the notation `(xNr, xNi)` for the complex value `xN`, this becomes: -//! ```text -//! x_c = [(x0r, 0), (x1r, 0), (x2r, 0), (x3r, 0), (x4r, 0), (x5r, 0)] -//! ``` -//! -//! Performing a normal complex FFT, the result of `FFT(x_c)` is: -//! ```text -//! FFT(x_c) = [(X0r, X0i), (X1r, X1i), (X2r, X2i), (X3r, X3i), (X4r, X4i), (X5r, X5i)] -//! ``` -//! -//! But because our `x_c` is real-valued (all imaginary parts are zero), some of this becomes redundant: -//! ```text -//! FFT(x_c) = [(X0r, 0), (X1r, X1i), (X2r, X2i), (X3r, 0), (X2r, -X2i), (X1r, -X1i)] -//! ``` -//! -//! The last two values are the complex conjugates of `X1` and `X2`. Additionally, `X0i` and `X3i` are zero. -//! As we can see, the output contains 6 independent values, and the rest is redundant. -//! But it still takes time for the FFT to calculate the redundant values. -//! Converting the input data to complex also takes a little bit of time. -//! -//! If the length of `x` instead had been 7, result would have been: -//! ```text -//! FFT(x_c) = [(X0r, 0), (X1r, X1i), (X2r, X2i), (X3r, X3i), (X3r, -X3i), (X2r, -X2i), (X1r, -X1i)] -//! ``` -//! -//! The result is similar, but this time there is no zero at `X3i`. -//! Also in this case we got the same number of independent values as we started with. -//! -//! ### Real-to-complex -//! Using a real-to-complex FFT removes the need for converting the input data to complex. -//! It also avoids calculating the redundant output values. -//! -//! The result for 6 elements is: -//! ```text -//! RealFFT(x) = [(X0r, 0), (X1r, X1i), (X2r, X2i), (X3r, 0)] -//! ``` -//! -//! The result for 7 elements is: -//! ```text -//! RealFFT(x) = [(X0r, 0), (X1r, X1i), (X2r, X2i), (X3r, X3i)] -//! ``` -//! -//! This is the data layout output by the real-to-complex FFT, -//! and the one expected as input to the complex-to-real inverse FFT. -//! -//! ## Scaling -//! RealFFT matches the behaviour of RustFFT and does not normalize the output of either forward or inverse FFT. -//! To get normalized results, each element must be scaled by `1/sqrt(length)`, -//! where `length` is the length of the real-valued signal. -//! If the processing involves both a forward and an inverse FFT step, -//! it is advisable to merge the two normalization steps to a single, by scaling by `1/length`. -//! -//! ## Documentation -//! -//! The full documentation can be generated by rustdoc. To generate and view it run: -//! ```text -//! cargo doc --open -//! ``` -//! -//! ## Benchmarks -//! -//! To run a set of benchmarks comparing real-to-complex FFT with standard complex-to-complex, type: -//! ```text -//! cargo bench -//! ``` -//! The results are printed while running, and are compiled into an html report containing much more details. -//! To view, open `target/criterion/report/index.html` in a browser. -//! -//! ## Example -//! Transform a signal, and then inverse transform the result. -//! ``` -//! use realfft::RealFftPlanner; -//! use rustfft::num_complex::Complex; -//! use rustfft::num_traits::Zero; -//! -//! let length = 256; -//! -//! // make a planner -//! let mut real_planner = RealFftPlanner::::new(); -//! -//! // create a FFT -//! let r2c = real_planner.plan_fft_forward(length); -//! // make a dummy real-valued signal (filled with zeros) -//! let mut indata = r2c.make_input_vec(); -//! // make a vector for storing the spectrum -//! let mut spectrum = r2c.make_output_vec(); -//! -//! // Are they the length we expect? -//! assert_eq!(indata.len(), length); -//! assert_eq!(spectrum.len(), length/2+1); -//! -//! // forward transform the signal -//! r2c.process(&mut indata, &mut spectrum).unwrap(); -//! -//! // create an inverse FFT -//! let c2r = real_planner.plan_fft_inverse(length); -//! -//! // create a vector for storing the output -//! let mut outdata = c2r.make_output_vec(); -//! assert_eq!(outdata.len(), length); -//! -//! // inverse transform the spectrum back to a real-valued signal -//! c2r.process(&mut spectrum, &mut outdata).unwrap(); -//! ``` -//! -//! ### Versions -//! - 3.4.0: Fix undefined behavior reported by Miri. -//! Update to latest RustFFT. -//! - 3.3.0: Add method for getting the length of the complex input/output. -//! Bugfix: clean up numerical noise in the zero imaginary components. -//! - 3.2.0: Allow scratch buffer to be larger than needed. -//! - 3.1.0: Update to RustFFT 6.1 with Neon support. -//! - 3.0.2: Fix confusing typos in errors about scratch length. -//! - 3.0.1: More helpful error messages, fix confusing typos. -//! - 3.0.0: Improved error reporting. -//! - 2.0.1: Minor bugfix. -//! - 2.0.0: Update RustFFT to 6.0.0 and num-complex to 0.4.0. -//! - 1.1.0: Add missing Sync+Send. -//! - 1.0.0: First version with new api. -//! -//! ### Compatibility -//! -//! The `realfft` crate has the same rustc version requirements as RustFFT. -//! The minimum rustc version is 1.61. +#![doc = include_str!("../README.md")] pub use rustfft::num_complex; pub use rustfft::num_traits; @@ -983,9 +824,9 @@ mod tests { let mut out_a = c2r.make_output_vec(); let mut indata = c2r.make_input_vec(); let mut rustfft_check: Vec> = vec![Complex::zero(); length]; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); for val in indata.iter_mut() { - *val = Complex::new(rng.gen::(), rng.gen::()); + *val = Complex::new(rng.random::(), rng.random::()); } indata[0].im = 0.0; if length % 2 == 0 { @@ -1032,9 +873,9 @@ mod tests { let mut out_a = c2r.make_output_vec(); let mut indata = c2r.make_input_vec(); let mut rustfft_check: Vec> = vec![Complex::zero(); length]; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); for val in indata.iter_mut() { - *val = Complex::new(rng.gen::(), rng.gen::()); + *val = Complex::new(rng.random::(), rng.random::()); } indata[0].im = 0.0; if length % 2 == 0 { @@ -1080,11 +921,11 @@ mod tests { let c2r = real_planner.plan_fft_inverse(length); let mut out_a = c2r.make_output_vec(); let mut indata = c2r.make_input_vec(); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // Make some valid data for val in indata.iter_mut() { - *val = Complex::new(rng.gen::(), rng.gen::()); + *val = Complex::new(rng.random::(), rng.random::()); } indata[0].im = 0.0; indata[50].im = 0.0; @@ -1093,7 +934,7 @@ mod tests { // Make some invalid data, first point invalid for val in indata.iter_mut() { - *val = Complex::new(rng.gen::(), rng.gen::()); + *val = Complex::new(rng.random::(), rng.random::()); } indata[50].im = 0.0; let res = c2r.process(&mut indata, &mut out_a); @@ -1102,7 +943,7 @@ mod tests { // Make some invalid data, last point invalid for val in indata.iter_mut() { - *val = Complex::new(rng.gen::(), rng.gen::()); + *val = Complex::new(rng.random::(), rng.random::()); } indata[0].im = 0.0; let res = c2r.process(&mut indata, &mut out_a); @@ -1118,11 +959,11 @@ mod tests { let c2r = real_planner.plan_fft_inverse(length); let mut out_a = c2r.make_output_vec(); let mut indata = c2r.make_input_vec(); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // Make some valid data for val in indata.iter_mut() { - *val = Complex::new(rng.gen::(), rng.gen::()); + *val = Complex::new(rng.random::(), rng.random::()); } indata[0].im = 0.0; // this should be ok @@ -1130,7 +971,7 @@ mod tests { // Make some invalid data, first point invalid for val in indata.iter_mut() { - *val = Complex::new(rng.gen::(), rng.gen::()); + *val = Complex::new(rng.random::(), rng.random::()); } let res = c2r.process(&mut indata, &mut out_a); assert!(res.is_err()); @@ -1145,9 +986,9 @@ mod tests { let r2c = real_planner.plan_fft_forward(length); let mut out_a = r2c.make_output_vec(); let mut indata = r2c.make_input_vec(); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); for val in indata.iter_mut() { - *val = rng.gen::(); + *val = rng.random::(); } let mut rustfft_check = indata .iter() @@ -1183,9 +1024,9 @@ mod tests { let r2c = real_planner.plan_fft_forward(length); let mut out_a = r2c.make_output_vec(); let mut indata = r2c.make_input_vec(); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); for val in indata.iter_mut() { - *val = rng.gen::(); + *val = rng.random::(); } let mut rustfft_check = indata .iter()