use crate::context::GpuContext;
use crate::error::{GpuError, GpuResult};
use crate::fft::Fft1d;
use std::sync::Arc;
use tracing::debug;
pub const MAX_FFT_CONVOLUTION_SIZE: usize = 2048;
#[inline]
pub fn complex_multiply(ar: f32, ai: f32, br: f32, bi: f32) -> (f32, f32) {
(ar * br - ai * bi, ar * bi + ai * br)
}
pub fn convolve_reference(signal: &[f32], kernel: &[f32]) -> Vec<f32> {
if signal.is_empty() || kernel.is_empty() {
return vec![];
}
let output_len = signal.len() + kernel.len() - 1;
let mut output = vec![0.0_f32; output_len];
for (i, &s) in signal.iter().enumerate() {
for (j, &k) in kernel.iter().enumerate() {
output[i + j] += s * k;
}
}
output
}
pub struct FftConvolution {
ctx: Arc<GpuContext>,
}
impl FftConvolution {
pub fn new(ctx: Arc<GpuContext>) -> Self {
Self { ctx }
}
pub fn convolve(&self, signal: &[f32], kernel: &[f32]) -> GpuResult<Vec<f32>> {
if signal.is_empty() || kernel.is_empty() {
return Ok(vec![]);
}
let output_len = signal.len() + kernel.len() - 1;
let fft_size = output_len.next_power_of_two();
if fft_size > MAX_FFT_CONVOLUTION_SIZE {
return Err(GpuError::InvalidKernelParams {
reason: format!(
"FFT convolution size {} exceeds maximum {}; \
use overlap-save for long signals",
fft_size, MAX_FFT_CONVOLUTION_SIZE
),
});
}
let fft_size_u32 = fft_size as u32;
let fft_size_u32 = fft_size_u32.max(4);
let fft_size = fft_size_u32 as usize;
debug!(
"FftConvolution::convolve signal_len={} kernel_len={} output_len={} fft_size={}",
signal.len(),
kernel.len(),
output_len,
fft_size
);
let mut signal_padded = signal.to_vec();
signal_padded.resize(fft_size, 0.0_f32);
let signal_imag = vec![0.0_f32; fft_size];
let mut kernel_padded = kernel.to_vec();
kernel_padded.resize(fft_size, 0.0_f32);
let kernel_imag = vec![0.0_f32; fft_size];
let fft_fwd = Fft1d::new(&self.ctx, fft_size_u32, false)?;
let (s_re, s_im) = fft_fwd.execute(&self.ctx, &signal_padded, &signal_imag)?;
let (k_re, k_im) = fft_fwd.execute(&self.ctx, &kernel_padded, &kernel_imag)?;
let mut y_re = vec![0.0_f32; fft_size];
let mut y_im = vec![0.0_f32; fft_size];
for i in 0..fft_size {
let (r, im) = complex_multiply(s_re[i], s_im[i], k_re[i], k_im[i]);
y_re[i] = r;
y_im[i] = im;
}
let fft_inv = Fft1d::new(&self.ctx, fft_size_u32, true)?;
let (result_re, _result_im) = fft_inv.execute(&self.ctx, &y_re, &y_im)?;
Ok(result_re[..output_len].to_vec())
}
pub fn correlate(&self, signal: &[f32], kernel: &[f32]) -> GpuResult<Vec<f32>> {
if signal.is_empty() || kernel.is_empty() {
return Ok(vec![]);
}
let reversed_kernel: Vec<f32> = kernel.iter().copied().rev().collect();
self.convolve(signal, &reversed_kernel)
}
pub fn convolve_batch(&self, signals: &[Vec<f32>], kernel: &[f32]) -> GpuResult<Vec<Vec<f32>>> {
let mut results = Vec::with_capacity(signals.len());
for signal in signals {
let output = self.convolve(signal, kernel)?;
results.push(output);
}
Ok(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complex_multiply_real_unit() {
let (r, i) = complex_multiply(1.0, 0.0, 1.0, 0.0);
assert!((r - 1.0).abs() < 1e-6, "real(1*1) should be 1, got {r}");
assert!(i.abs() < 1e-6, "imag(1*1) should be 0, got {i}");
}
#[test]
fn test_complex_multiply_imaginary_squared() {
let (r, i) = complex_multiply(0.0, 1.0, 0.0, 1.0);
assert!((r + 1.0).abs() < 1e-6, "real(i²) should be -1, got {r}");
assert!(i.abs() < 1e-6, "imag(i²) should be 0, got {i}");
}
#[test]
fn test_complex_multiply_real_times_imaginary() {
let (r, i) = complex_multiply(1.0, 0.0, 0.0, 1.0);
assert!(r.abs() < 1e-6, "real(1*i) should be 0, got {r}");
assert!((i - 1.0).abs() < 1e-6, "imag(1*i) should be 1, got {i}");
}
#[test]
fn test_convolve_reference_identity_kernel() {
let result = convolve_reference(&[1.0, 2.0, 3.0], &[1.0]);
assert_eq!(result.len(), 3);
assert!((result[0] - 1.0).abs() < 1e-6);
assert!((result[1] - 2.0).abs() < 1e-6);
assert!((result[2] - 3.0).abs() < 1e-6);
}
#[test]
fn test_convolve_reference_delta_kernel() {
let result = convolve_reference(&[1.0, 2.0, 3.0], &[0.0, 1.0, 0.0]);
assert_eq!(result.len(), 5);
assert!((result[0] - 0.0).abs() < 1e-6, "output[0]={}", result[0]);
assert!((result[1] - 1.0).abs() < 1e-6, "output[1]={}", result[1]);
assert!((result[2] - 2.0).abs() < 1e-6, "output[2]={}", result[2]);
assert!((result[3] - 3.0).abs() < 1e-6, "output[3]={}", result[3]);
assert!((result[4] - 0.0).abs() < 1e-6, "output[4]={}", result[4]);
}
#[test]
fn test_convolve_reference_box_blur() {
let result = convolve_reference(&[1.0, 1.0, 1.0, 1.0, 1.0], &[0.5, 0.5]);
assert_eq!(result.len(), 6);
assert!((result[0] - 0.5).abs() < 1e-6, "output[0]={}", result[0]);
assert!((result[1] - 1.0).abs() < 1e-6, "output[1]={}", result[1]);
assert!((result[2] - 1.0).abs() < 1e-6, "output[2]={}", result[2]);
assert!((result[3] - 1.0).abs() < 1e-6, "output[3]={}", result[3]);
assert!((result[4] - 1.0).abs() < 1e-6, "output[4]={}", result[4]);
assert!((result[5] - 0.5).abs() < 1e-6, "output[5]={}", result[5]);
}
#[test]
fn test_convolve_reference_empty_inputs() {
assert!(convolve_reference(&[], &[1.0, 2.0]).is_empty());
assert!(convolve_reference(&[1.0, 2.0], &[]).is_empty());
assert!(convolve_reference(&[], &[]).is_empty());
}
#[test]
fn test_convolve_reference_polynomial_multiplication() {
let result = convolve_reference(&[1.0, 1.0], &[1.0, 1.0]);
assert_eq!(result.len(), 3);
assert!((result[0] - 1.0).abs() < 1e-6, "coeff x^0 = {}", result[0]);
assert!((result[1] - 2.0).abs() < 1e-6, "coeff x^1 = {}", result[1]);
assert!((result[2] - 1.0).abs() < 1e-6, "coeff x^2 = {}", result[2]);
}
#[test]
fn test_max_fft_convolution_size_is_power_of_two() {
assert!(
MAX_FFT_CONVOLUTION_SIZE.is_power_of_two(),
"MAX_FFT_CONVOLUTION_SIZE ({}) must be a power of two",
MAX_FFT_CONVOLUTION_SIZE
);
}
}