use std::f64::consts::PI;
use scirs2_core::numeric::Complex64;
use crate::error::{FFTError, FFTResult};
fn fft_radix2(data: &mut [Complex64]) -> FFTResult<()> {
let n = data.len();
if n == 0 {
return Ok(());
}
if n & (n - 1) != 0 {
return Err(FFTError::ValueError(format!(
"ambiguity FFT requires power-of-2 length, got {n}"
)));
}
let mut j = 0usize;
for i in 1..n {
let mut bit = n >> 1;
while j & bit != 0 {
j ^= bit;
bit >>= 1;
}
j ^= bit;
if i < j {
data.swap(i, j);
}
}
let mut len = 2usize;
while len <= n {
let half = len / 2;
let angle = -2.0 * PI / len as f64;
let wlen = Complex64::new(angle.cos(), angle.sin());
for i in (0..n).step_by(len) {
let mut w = Complex64::new(1.0, 0.0);
for k in 0..half {
let u = data[i + k];
let v = data[i + k + half] * w;
data[i + k] = u + v;
data[i + k + half] = u - v;
w *= wlen;
}
}
len <<= 1;
}
Ok(())
}
fn next_pow2(n: usize) -> usize {
if n == 0 {
return 1;
}
let mut p = 1;
while p < n {
p <<= 1;
}
p
}
#[derive(Debug, Clone)]
pub struct AmbiguityConfig {
pub normalize: bool,
pub n_doppler_bins: usize,
}
impl Default for AmbiguityConfig {
fn default() -> Self {
Self {
normalize: true,
n_doppler_bins: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct AmbiguityResult {
pub data: Vec<Vec<f64>>,
pub delay_bins: Vec<f64>,
pub doppler_bins: Vec<f64>,
pub peak_delay: usize,
pub peak_doppler: usize,
pub peak_magnitude: f64,
}
pub fn compute_ambiguity_function(
signal: &[Complex64],
config: &AmbiguityConfig,
) -> FFTResult<AmbiguityResult> {
let n = signal.len();
if n < 2 {
return Err(FFTError::ValueError(format!(
"signal length must be ≥ 2, got {n}"
)));
}
let nd_raw = if config.n_doppler_bins == 0 {
n
} else {
config.n_doppler_bins
};
let nd_fft = next_pow2(nd_raw.max(n));
let n_delay = 2 * n - 1;
let mut data: Vec<Vec<f64>> = Vec::with_capacity(n_delay);
for d in 0..n_delay {
let k: i64 = d as i64 - (n as i64 - 1);
let mut z: Vec<Complex64> = vec![Complex64::new(0.0, 0.0); nd_fft];
for nn in 0..n {
let m = nn as i64 + k;
if m >= 0 && m < n as i64 {
let s_m = signal[m as usize];
let s_n_conj = Complex64::new(signal[nn].re, -signal[nn].im);
z[nn] = s_m * s_n_conj;
}
}
fft_radix2(&mut z)?;
let row: Vec<f64> = (0..nd_raw)
.map(|f| {
let idx = f % nd_fft; z[idx].re * z[idx].re + z[idx].im * z[idx].im
})
.collect();
data.push(row);
}
let (mut peak_delay, mut peak_doppler, mut peak_magnitude) = (0usize, 0usize, 0.0_f64);
for (di, row) in data.iter().enumerate() {
for (fi, &val) in row.iter().enumerate() {
if val > peak_magnitude {
peak_magnitude = val;
peak_delay = di;
peak_doppler = fi;
}
}
}
if config.normalize && peak_magnitude > 1e-30 {
let inv = 1.0 / peak_magnitude;
for row in data.iter_mut() {
for v in row.iter_mut() {
*v *= inv;
}
}
peak_magnitude = 1.0;
}
let delay_bins: Vec<f64> = (0..n_delay).map(|d| d as f64 - (n as f64 - 1.0)).collect();
let doppler_bins: Vec<f64> = (0..nd_raw).map(|f| f as f64 / nd_raw as f64).collect();
Ok(AmbiguityResult {
data,
delay_bins,
doppler_bins,
peak_delay,
peak_doppler,
peak_magnitude,
})
}
pub fn narrow_ambiguity_function(
signal: &[Complex64],
n_delay_bins: usize,
config: &AmbiguityConfig,
) -> FFTResult<AmbiguityResult> {
let n = signal.len();
if n < 2 {
return Err(FFTError::ValueError(format!(
"signal length must be ≥ 2, got {n}"
)));
}
let half_d = (n_delay_bins / 2) as i64;
let nd_raw = if config.n_doppler_bins == 0 {
n
} else {
config.n_doppler_bins
};
let nd_fft = next_pow2(nd_raw.max(n));
let mut data: Vec<Vec<f64>> = Vec::with_capacity(n_delay_bins);
let mut delay_bins_vec: Vec<f64> = Vec::with_capacity(n_delay_bins);
for d in 0..n_delay_bins {
let k: i64 = d as i64 - half_d;
delay_bins_vec.push(k as f64);
let mut z: Vec<Complex64> = vec![Complex64::new(0.0, 0.0); nd_fft];
for nn in 0..n {
let m = nn as i64 + k;
if m >= 0 && m < n as i64 {
let s_m = signal[m as usize];
let s_n_conj = Complex64::new(signal[nn].re, -signal[nn].im);
z[nn] = s_m * s_n_conj;
}
}
fft_radix2(&mut z)?;
let row: Vec<f64> = (0..nd_raw)
.map(|f| {
let idx = f % nd_fft;
z[idx].re * z[idx].re + z[idx].im * z[idx].im
})
.collect();
data.push(row);
}
let (mut peak_delay, mut peak_doppler, mut peak_magnitude) = (0usize, 0usize, 0.0_f64);
for (di, row) in data.iter().enumerate() {
for (fi, &val) in row.iter().enumerate() {
if val > peak_magnitude {
peak_magnitude = val;
peak_delay = di;
peak_doppler = fi;
}
}
}
if config.normalize && peak_magnitude > 1e-30 {
let inv = 1.0 / peak_magnitude;
for row in data.iter_mut() {
for v in row.iter_mut() {
*v *= inv;
}
}
peak_magnitude = 1.0;
}
let doppler_bins: Vec<f64> = (0..nd_raw).map(|f| f as f64 / nd_raw as f64).collect();
Ok(AmbiguityResult {
data,
delay_bins: delay_bins_vec,
doppler_bins,
peak_delay,
peak_doppler,
peak_magnitude,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn make_chirp(n: usize, rate: f64) -> Vec<Complex64> {
(0..n)
.map(|i| {
let t = i as f64 / n as f64;
let phase = PI * rate * t * t;
Complex64::new(phase.cos(), phase.sin())
})
.collect()
}
#[test]
fn test_config_default() {
let cfg = AmbiguityConfig::default();
assert!(cfg.normalize);
assert_eq!(cfg.n_doppler_bins, 0);
}
#[test]
fn test_result_shape() {
let n = 16;
let signal: Vec<Complex64> = (0..n).map(|_| Complex64::new(1.0, 0.0)).collect();
let cfg = AmbiguityConfig::default();
let result = compute_ambiguity_function(&signal, &cfg).expect("ambiguity ok");
assert_eq!(result.data.len(), 2 * n - 1);
assert_eq!(result.data[0].len(), n);
assert_eq!(result.delay_bins.len(), 2 * n - 1);
assert_eq!(result.doppler_bins.len(), n);
}
#[test]
fn test_peak_at_origin_for_constant_signal() {
let n = 16;
let signal: Vec<Complex64> = (0..n).map(|_| Complex64::new(1.0, 0.0)).collect();
let cfg = AmbiguityConfig::default();
let result = compute_ambiguity_function(&signal, &cfg).expect("ambiguity ok");
let zero_delay_idx = n - 1;
assert_eq!(
result.peak_delay, zero_delay_idx,
"peak delay bin should be {zero_delay_idx} (zero lag)"
);
assert_eq!(result.peak_doppler, 0);
}
#[test]
fn test_peak_magnitude_normalised() {
let n = 8;
let signal = make_chirp(n, 4.0);
let cfg = AmbiguityConfig {
normalize: true,
..Default::default()
};
let result = compute_ambiguity_function(&signal, &cfg).expect("ambiguity ok");
assert!(
(result.peak_magnitude - 1.0).abs() < 1e-9,
"peak={}",
result.peak_magnitude
);
}
#[test]
fn test_symmetry_property() {
let n = 8;
let signal: Vec<Complex64> = (0..n)
.map(|i| Complex64::new((2.0 * PI * i as f64 / n as f64).cos(), 0.0))
.collect();
let cfg = AmbiguityConfig {
normalize: false,
..Default::default()
};
let result = compute_ambiguity_function(&signal, &cfg).expect("ambiguity ok");
let nd_d = result.doppler_bins.len();
for d in 0..(2 * n - 1) {
let d_mirror = (2 * n - 2) - d;
for f in 0..nd_d {
let f_mirror = (nd_d - f) % nd_d;
let v1 = result.data[d][f];
let v2 = result.data[d_mirror][f_mirror];
assert!(
(v1 - v2).abs() < 1e-6,
"symmetry failed at d={d} f={f}: {v1} vs {v2}"
);
}
}
}
#[test]
fn test_narrow_ambiguity_shape() {
let n = 16;
let signal: Vec<Complex64> = (0..n).map(|_| Complex64::new(1.0, 0.0)).collect();
let cfg = AmbiguityConfig::default();
let n_delay = 5;
let result = narrow_ambiguity_function(&signal, n_delay, &cfg).expect("narrow ok");
assert_eq!(result.data.len(), n_delay);
assert_eq!(result.data[0].len(), n);
}
#[test]
fn test_short_signal_error() {
let signal = vec![Complex64::new(1.0, 0.0)]; let cfg = AmbiguityConfig::default();
assert!(compute_ambiguity_function(&signal, &cfg).is_err());
}
}