use no_denormals::*;
use num_complex::Complex;
use rayon::prelude::*;
use std::f64::consts::TAU;
use wide::f64x4;
pub fn bilinear_tf_distribution<F: Fn(f64, f64, f64) -> f64 + Sync>(
input: &[f64],
kernel: F,
alpha: f64,
) -> Vec<Vec<f64>> {
no_denormals(|| {
let len = input.len();
let ambiguity = ambiguity(input);
let weighted: Vec<Vec<Complex<f64>>> = (0..len)
.into_par_iter()
.map(|eta| {
(0..len)
.map(|tau| ambiguity[eta][tau] * kernel(eta as f64, tau as f64, alpha))
.collect()
})
.collect();
let intermediate: Vec<Vec<Complex<f64>>> = weighted
.par_iter()
.map(|row| {
(0..len)
.map(|freq| complex_phase_dot(row, TAU * freq as f64 / len as f64))
.collect()
})
.collect();
let mut columns = vec![vec![Complex::new(0.0, 0.0); len]; len];
for (eta, row) in intermediate.iter().enumerate() {
for (freq, value) in row.iter().enumerate() {
columns[freq][eta] = *value;
}
}
let by_freq: Vec<Vec<f64>> = columns
.par_iter()
.map(|column| {
(0..len)
.map(|time| {
complex_phase_dot(column, TAU * time as f64 / len as f64).re
/ (len * len) as f64
})
.collect()
})
.collect();
let mut result = vec![vec![0.0; len]; len];
for (freq, row) in by_freq.iter().enumerate() {
for (time, value) in row.iter().enumerate() {
result[time][freq] = *value;
}
}
result
})
}
#[inline]
fn ambiguity(input: &[f64]) -> Vec<Vec<Complex<f64>>> {
let len = input.len();
let by_tau: Vec<Vec<Complex<f64>>> = (0..len)
.into_par_iter()
.map(|tau| {
let half = tau / 2;
if half >= len - half {
return vec![Complex::new(0.0, 0.0); len];
}
let products: Vec<f64> = (half..len - half)
.map(|t| input[t + half] * input[t - half])
.collect();
(0..len)
.map(|eta| {
let angle_step = -TAU * eta as f64 / len as f64;
let rotation = Complex::new(0.0, angle_step * half as f64).exp();
rotation * phase_dot_real(&products, angle_step)
})
.collect()
})
.collect();
let mut result = vec![vec![Complex::new(0.0, 0.0); len]; len];
for (tau, row) in by_tau.iter().enumerate() {
for (eta, value) in row.iter().enumerate() {
result[eta][tau] = *value;
}
}
result
}
#[inline]
fn phase_dot_real(values: &[f64], angle_step: f64) -> Complex<f64> {
let mut acc_re = f64x4::ZERO;
let mut acc_im = f64x4::ZERO;
let mut angles = f64x4::new([0.0, angle_step, 2.0 * angle_step, 3.0 * angle_step]);
let step = f64x4::splat(4.0 * angle_step);
let chunks = values.chunks_exact(4);
let remainder = chunks.remainder();
for chunk in chunks {
let v = f64x4::new([chunk[0], chunk[1], chunk[2], chunk[3]]);
let (sin_a, cos_a) = angles.sin_cos();
acc_re += v * cos_a;
acc_im += v * sin_a;
angles += step;
}
let mut re = acc_re.reduce_add();
let mut im = acc_im.reduce_add();
let processed = values.len() - remainder.len();
for (k, &value) in remainder.iter().enumerate() {
let angle = angle_step * (processed + k) as f64;
re += value * angle.cos();
im += value * angle.sin();
}
Complex::new(re, im)
}
#[inline]
fn complex_phase_dot(values: &[Complex<f64>], angle_step: f64) -> Complex<f64> {
let mut acc_re = f64x4::ZERO;
let mut acc_im = f64x4::ZERO;
let mut angles = f64x4::new([0.0, angle_step, 2.0 * angle_step, 3.0 * angle_step]);
let step = f64x4::splat(4.0 * angle_step);
let chunks = values.chunks_exact(4);
let remainder = chunks.remainder();
for chunk in chunks {
let vre = f64x4::new([chunk[0].re, chunk[1].re, chunk[2].re, chunk[3].re]);
let vim = f64x4::new([chunk[0].im, chunk[1].im, chunk[2].im, chunk[3].im]);
let (sin_a, cos_a) = angles.sin_cos();
acc_re += vre * cos_a - vim * sin_a;
acc_im += vre * sin_a + vim * cos_a;
angles += step;
}
let mut re = acc_re.reduce_add();
let mut im = acc_im.reduce_add();
let processed = values.len() - remainder.len();
for (k, value) in remainder.iter().enumerate() {
let angle = angle_step * (processed + k) as f64;
let (sin_a, cos_a) = angle.sin_cos();
re += value.re * cos_a - value.im * sin_a;
im += value.re * sin_a + value.im * cos_a;
}
Complex::new(re, im)
}
#[inline(always)]
pub fn wigner(_: f64, _: f64, _: f64) -> f64 {
1.0
}
#[inline]
pub fn choi_williams(eta: f64, tau: f64, alpha: f64) -> f64 {
(-alpha * (eta * eta) * (tau * tau) / 2.0).exp()
}
#[inline]
pub fn rihaczek(eta: f64, tau: f64, alpha: f64) -> f64 {
(-alpha * eta * tau).exp()
}
#[inline]
pub fn cone_shape(eta: f64, tau: f64, alpha: f64) -> f64 {
let pi_eta_tau = std::f64::consts::PI * eta * tau;
if pi_eta_tau == 0.0 {
return 1.0;
}
(pi_eta_tau.sin() / pi_eta_tau) * (-2.0 * std::f64::consts::PI * alpha * tau.powi(2)).exp()
}
#[cfg(test)]
mod tests {
use super::*;
use rand::prelude::*;
fn naive_reference<F: Fn(f64, f64, f64) -> f64>(
input: &[f64],
kernel: F,
alpha: f64,
) -> Vec<Vec<f64>> {
let len = input.len();
let ambiguity = ambiguity(input);
(0..len)
.map(|time| {
(0..len)
.map(|freq| {
let mut sum = 0.0;
for eta in 0..len {
for tau in 0..len {
let exponent =
TAU * ((time * eta + freq * tau) as f64) / len as f64;
sum += (ambiguity[eta][tau]
* kernel(eta as f64, tau as f64, alpha)
* Complex::new(0.0, exponent).exp())
.re;
}
}
sum / (len * len) as f64
})
.collect()
})
.collect()
}
fn assert_close(a: &[Vec<f64>], b: &[Vec<f64>], tolerance: f64) {
assert_eq!(a.len(), b.len());
for (row_a, row_b) in a.iter().zip(b) {
assert_eq!(row_a.len(), row_b.len());
for (&x, &y) in row_a.iter().zip(row_b) {
assert!((x - y).abs() < tolerance, "expected {y}, got {x}");
}
}
}
#[test]
fn matches_naive_reference() {
let mut rng = StdRng::seed_from_u64(42);
let input: Vec<f64> = (0..21).map(|_| rng.gen_range(-1.0..1.0)).collect();
for (kernel, alpha) in [
(wigner as fn(f64, f64, f64) -> f64, 0.001),
(choi_williams, 0.05),
(rihaczek, 0.02),
(cone_shape, 0.001),
] {
let optimized = bilinear_tf_distribution(&input, kernel, alpha);
let reference = naive_reference(&input, kernel, alpha);
assert_close(&optimized, &reference, 1e-8);
}
}
#[test]
fn runs_on_realistic_signal_length() {
let mut input = vec![0.0; 128];
input.par_iter_mut().for_each(|element| {
*element = thread_rng().gen_range(-1.0..1.0);
});
let result = bilinear_tf_distribution(&input, cone_shape, 0.001);
assert_eq!(result.len(), 128);
for row in &result {
assert_eq!(row.len(), 128);
assert!(row.iter().all(|value| value.is_finite()));
}
}
}