Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduction of Gibbs Phenomenon on FFT compression #124

Merged
merged 8 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 73 additions & 14 deletions brro-compressor/src/compressor/fft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

use crate::{optimizer::utils::DataStats, utils::error::calculate_error};
use crate::{
optimizer::utils::DataStats,
utils::{error::calculate_error, next_size},
};
use bincode::{Decode, Encode};
use rustfft::{num_complex::Complex, FftPlanner};
use std::{cmp::Ordering, collections::BinaryHeap};
Expand Down Expand Up @@ -205,6 +208,23 @@ impl FFT {
y
}

/// Given an array of size N, it returns the next best FFT size with the
/// begining and the ended padded to improve Gibbs on the edges of the frame
fn gibbs_sizing(data: &[f64]) -> Vec<f64> {
cjrolo marked this conversation as resolved.
Show resolved Hide resolved
let data_len = data.len();
let added_len = next_size(data_len) - data_len;
debug!("Gibbs sizing, padding with {}", added_len);
let prefix_len = added_len / 2;
let suffix_len = added_len - prefix_len;
// Extend the beginning and the end with the first and last value
let mut prefix = vec![data[0]; prefix_len];
let suffix = vec![*data.last().unwrap(); suffix_len];
prefix.extend(data);
prefix.extend(suffix);
trace!("Gibbs constructed data: {:?}", prefix);
prefix
}

/// Rounds a number to the specified number of decimal places
// TODO: Move this into utils? I think this will be helpfull somewhere else.
fn round(&self, x: f32, decimals: u32) -> f64 {
Expand Down Expand Up @@ -283,7 +303,7 @@ impl FFT {
self.frequencies = FFT::fft_trim(&mut buffer, max_freq);
}

/// Compress data via FFT - VERY EXPENSIVE
/// Compress data via FFT - EXPENSIVE
/// This picks a set of data, computes the FFT, and optimizes the number of frequencies to store to match
/// the max allowed error.
/// NOTE: This does not otimize for smallest possible error, just being smaller than the error.
Expand All @@ -292,16 +312,29 @@ impl FFT {
debug!("Same max and min, we're done here!");
return;
}
// Variables
let len = data.len();
let len_f32 = len as f32;
if !len.is_power_of_two() {

if !data.len().is_power_of_two() {
warn!("Slow FFT, data segment is not a power of 2!");
}
// Let's start from the defaults values for frequencies
let max_freq = if 3 >= (len / 100) { 3 } else { len / 100 };
let max_freq = if 3 >= (data.len() / 100) {
3
} else {
data.len() / 100
};

// Should we apply a Gibbs sizing?
let g_data: Vec<f64> = if data.len() >= 128 {
cjrolo marked this conversation as resolved.
Show resolved Hide resolved
FFT::gibbs_sizing(data)
} else {
data.to_vec()
};

let len = g_data.len();
let len_f32 = len as f32;

// Clean the data
let mut buffer = FFT::optimize(data);
let mut buffer = FFT::optimize(&g_data);

// Create the FFT planners
let mut planner = FftPlanner::new();
Expand Down Expand Up @@ -331,7 +364,7 @@ impl FFT {
.iter()
.map(|&f| self.round(f.re / len_f32, DECIMAL_PRECISION.into()))
.collect();
current_err = calculate_error(data, &out_data);
current_err = calculate_error(&g_data, &out_data);
trace!("Current Err: {}", current_err);
// Max iterations is 22 (We start at 10%, we can go to 95% and 1% at a time)
match iterations {
Expand Down Expand Up @@ -419,21 +452,36 @@ impl FFT {
debug!("Same max and min, faster decompression!");
return vec![self.max_value as f64; frame_size];
}
// Was this processed to reduce the Gibbs phenomeon?
let trim_sizes = if frame_size >= 128 {
let added_len = next_size(frame_size) - frame_size;
let prefix_len = added_len / 2;
let suffix_len = added_len - prefix_len;
debug!(
"Gibbs sizing detected, removing padding with {} len",
added_len
);
(prefix_len, suffix_len)
} else {
(0, 0)
};
let gibbs_frame_size = frame_size + trim_sizes.0 + trim_sizes.1;
// Vec to process the ifft
let mut data = self.get_mirrored_freqs(frame_size);
let mut data = self.get_mirrored_freqs(gibbs_frame_size);
// Plan the ifft
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_inverse(frame_size);
let fft = planner.plan_fft_inverse(gibbs_frame_size);
// run the ifft
fft.process(&mut data);
// We need this for normalization
let len = frame_size as f32;
let len = gibbs_frame_size as f32;
// We only need the real part
let out_data = data
let out_data: Vec<f64> = data
.iter()
.map(|&f| self.round(f.re / len, DECIMAL_PRECISION.into()))
.collect();
out_data
// Trim the excess data
out_data[trim_sizes.0..out_data.len() - trim_sizes.1].to_vec()
cjrolo marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -563,6 +611,17 @@ mod tests {
assert!(e <= 0.01);
}

#[test]
fn test_gibbs_sizing() {
let mut vector1 = vec![2.0; 2048];
vector1[0] = 1.0;
vector1[2047] = 3.0;
let vector1_sized = FFT::gibbs_sizing(&vector1);
assert!(vector1_sized.len() == 2187);
assert!(vector1_sized[2] == 1.0);
assert!(vector1_sized[2185] == 3.0);
}

#[test]
fn test_static_and_trim() {
// This vector should lead to 11 frequencies
Expand Down
35 changes: 35 additions & 0 deletions brro-compressor/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ pub fn prev_power_of_two(n: usize) -> usize {
(1 << highest_bit_set_idx) & n
}

/// Given a number N, checks what is the next number that is in the form of (2^N * 3^M)
pub fn next_size(mut n: usize) -> usize {
n += 1;
while !is_decomposable(n) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all this looping looks slow, are you sure there isnt a way to get the same result with an expression instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prime numbers are a pain! And I don't see how I can improve this. Let me see if I find a better solution.
This is only run once per frame on compression and decompression.
Given that we force frames into a subset of sizes (and power of 2^N) I don't expect this to loop for long. On larger numbers it might become problematic.
Alternative is providing a table of next number for the expected frame sizes and calculate for everything else? (We have some pre-defined frame sizes hardcoded anyway).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A table does sound like a good idea.
Could be left for follow up work.

n += 1;
}
n
}

/// Checks if a number is in the form of (2^N * 3^M), usefull for FFT sizing
pub fn is_decomposable(mut n: usize) -> bool {
while n % 2 == 0 {
n /= 2;
}
while n % 3 == 0 {
n /= 3;
}
n == 1
}

/// Converts a float to u64 with a given precision
pub fn f64_to_u64(number: f64, precision: usize) -> u64 {
// TODO: Panic on overflow
Expand Down Expand Up @@ -69,4 +89,19 @@ mod tests {
assert_eq!(round_and_limit_f64(1., 2., 4., 1), 2.0);
assert_eq!(round_and_limit_f64(3.123452312, 2., 4., 3), 3.123);
}

#[test]
fn test_is_decomposable() {
assert!(is_decomposable(2048));
assert!(is_decomposable(512));
}

#[test]
fn test_next_size() {
assert_eq!(next_size(2048), 2187);
assert_eq!(next_size(512), 576);
assert_eq!(next_size(256), 288);
assert_eq!(next_size(128), 144);
assert_eq!(next_size(12432), 13122);
}
}
4 changes: 2 additions & 2 deletions brro-compressor/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn test_compressor_idw_lossless() {

#[test]
fn test_compressor_idw_lossy() {
test_lossy_compression("fft")
test_lossy_compression("idw")
}

#[test]
Expand All @@ -24,7 +24,7 @@ fn test_compressor_polynomial_lossless() {

#[test]
fn test_compressor_polynomial_lossy() {
test_lossy_compression("fft")
test_lossy_compression("polynomial")
}

#[test]
Expand Down
Loading