use crate::error::{FFTError, FFTResult};
use crate::fft::{fft, ifft};
use scirs2_core::numeric::Complex64;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct AmbiguityConfig {
pub max_delay: usize,
pub max_doppler: usize,
pub fs: f64,
}
impl Default for AmbiguityConfig {
fn default() -> Self {
Self {
max_delay: 64,
max_doppler: 64,
fs: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct AmbiguityResult {
pub delays: Vec<f64>,
pub dopplers: Vec<f64>,
pub surface: Vec<Vec<f64>>,
}
#[derive(Debug, Clone)]
pub struct WidebandAmbiguityResult {
pub delays: Vec<f64>,
pub scales: Vec<f64>,
pub surface: Vec<Vec<f64>>,
}
pub fn auto_ambiguity(signal: &[f64], config: &AmbiguityConfig) -> FFTResult<AmbiguityResult> {
let x: Vec<Complex64> = signal.iter().map(|&v| Complex64::new(v, 0.0)).collect();
auto_ambiguity_complex(&x, config)
}
pub fn auto_ambiguity_surface(
signal: &[f64],
config: &AmbiguityConfig,
) -> FFTResult<AmbiguityResult> {
let mut result = auto_ambiguity(signal, config)?;
for row in &mut result.surface {
for v in row.iter_mut() {
*v = *v * *v;
}
}
Ok(result)
}
fn auto_ambiguity_complex(x: &[Complex64], config: &AmbiguityConfig) -> FFTResult<AmbiguityResult> {
let n = x.len();
if n == 0 {
return Err(FFTError::ValueError("Signal is empty".to_string()));
}
let max_delay = config.max_delay.min(n - 1);
let max_doppler = config.max_doppler.min(n);
let num_delays = 2 * max_delay + 1;
let delays: Vec<f64> = (0..num_delays)
.map(|i| {
let tau = i as i64 - max_delay as i64;
tau as f64 / config.fs
})
.collect();
let doppler_fft_len = 2 * max_doppler;
let doppler_fft_len = if doppler_fft_len == 0 {
1
} else {
doppler_fft_len
};
let dopplers: Vec<f64> = (0..doppler_fft_len)
.map(|k| {
let k_shifted = if k < doppler_fft_len / 2 {
k as f64
} else {
k as f64 - doppler_fft_len as f64
};
k_shifted * config.fs / doppler_fft_len as f64
})
.collect();
let mut surface: Vec<Vec<f64>> = Vec::with_capacity(num_delays);
for delay_idx in 0..num_delays {
let tau = delay_idx as i64 - max_delay as i64;
let half_tau_pos = tau.div_euclid(2);
let half_tau_neg = tau - half_tau_pos;
let mut product = vec![Complex64::new(0.0, 0.0); doppler_fft_len];
for t in 0..doppler_fft_len.min(n) {
let idx_plus = t as i64 + half_tau_pos;
let idx_minus = t as i64 - half_tau_neg;
if idx_plus >= 0
&& (idx_plus as usize) < n
&& idx_minus >= 0
&& (idx_minus as usize) < n
{
product[t] = x[idx_plus as usize] * x[idx_minus as usize].conj();
}
}
let spectrum = fft(&product, None)?;
let row: Vec<f64> = spectrum.iter().map(|c| c.norm()).collect();
surface.push(row);
}
Ok(AmbiguityResult {
delays,
dopplers,
surface,
})
}
pub fn cross_ambiguity(
x: &[f64],
y: &[f64],
config: &AmbiguityConfig,
) -> FFTResult<AmbiguityResult> {
let xc: Vec<Complex64> = x.iter().map(|&v| Complex64::new(v, 0.0)).collect();
let yc: Vec<Complex64> = y.iter().map(|&v| Complex64::new(v, 0.0)).collect();
cross_ambiguity_complex(&xc, &yc, config)
}
pub fn cross_ambiguity_surface(
x: &[f64],
y: &[f64],
config: &AmbiguityConfig,
) -> FFTResult<AmbiguityResult> {
let mut result = cross_ambiguity(x, y, config)?;
for row in &mut result.surface {
for v in row.iter_mut() {
*v = *v * *v;
}
}
Ok(result)
}
fn cross_ambiguity_complex(
x: &[Complex64],
y: &[Complex64],
config: &AmbiguityConfig,
) -> FFTResult<AmbiguityResult> {
let nx = x.len();
let ny = y.len();
if nx == 0 || ny == 0 {
return Err(FFTError::ValueError(
"Signals must be non-empty".to_string(),
));
}
let n = nx.max(ny);
let max_delay = config.max_delay.min(n - 1);
let max_doppler = config.max_doppler.min(n);
let num_delays = 2 * max_delay + 1;
let delays: Vec<f64> = (0..num_delays)
.map(|i| {
let tau = i as i64 - max_delay as i64;
tau as f64 / config.fs
})
.collect();
let doppler_fft_len = (2 * max_doppler).max(1);
let dopplers: Vec<f64> = (0..doppler_fft_len)
.map(|k| {
let k_shifted = if k < doppler_fft_len / 2 {
k as f64
} else {
k as f64 - doppler_fft_len as f64
};
k_shifted * config.fs / doppler_fft_len as f64
})
.collect();
let mut surface: Vec<Vec<f64>> = Vec::with_capacity(num_delays);
for delay_idx in 0..num_delays {
let tau = delay_idx as i64 - max_delay as i64;
let half_tau_pos = tau.div_euclid(2);
let half_tau_neg = tau - half_tau_pos;
let mut product = vec![Complex64::new(0.0, 0.0); doppler_fft_len];
for t in 0..doppler_fft_len.min(n) {
let idx_x = t as i64 + half_tau_pos;
let idx_y = t as i64 - half_tau_neg;
if idx_x >= 0 && (idx_x as usize) < nx && idx_y >= 0 && (idx_y as usize) < ny {
product[t] = x[idx_x as usize] * y[idx_y as usize].conj();
}
}
let spectrum = fft(&product, None)?;
let row: Vec<f64> = spectrum.iter().map(|c| c.norm()).collect();
surface.push(row);
}
Ok(AmbiguityResult {
delays,
dopplers,
surface,
})
}
pub fn wideband_ambiguity(
signal: &[f64],
max_delay: usize,
num_scales: usize,
max_scale_ratio: f64,
fs: f64,
) -> FFTResult<WidebandAmbiguityResult> {
let n = signal.len();
if n == 0 {
return Err(FFTError::ValueError("Signal is empty".to_string()));
}
if max_scale_ratio <= 0.0 {
return Err(FFTError::ValueError(
"max_scale_ratio must be positive".to_string(),
));
}
if num_scales == 0 {
return Err(FFTError::ValueError(
"num_scales must be positive".to_string(),
));
}
let max_delay = max_delay.min(n - 1);
let num_delays = 2 * max_delay + 1;
let delays: Vec<f64> = (0..num_delays)
.map(|i| (i as i64 - max_delay as i64) as f64 / fs)
.collect();
let log_min = -(max_scale_ratio.ln());
let log_max = max_scale_ratio.ln();
let scales: Vec<f64> = if num_scales == 1 {
vec![1.0]
} else {
(0..num_scales)
.map(|i| {
let frac = i as f64 / (num_scales - 1) as f64;
(log_min + frac * (log_max - log_min)).exp()
})
.collect()
};
let mut surface: Vec<Vec<f64>> = Vec::with_capacity(num_delays);
for delay_idx in 0..num_delays {
let tau = delay_idx as i64 - max_delay as i64;
let mut row = Vec::with_capacity(num_scales);
for &s in &scales {
let sqrt_s = s.abs().sqrt();
let mut accum = Complex64::new(0.0, 0.0);
for t in 0..n {
let scaled_idx = s * (t as f64 - tau as f64);
let idx = scaled_idx.round() as i64;
if idx >= 0 && (idx as usize) < n {
let x_t = Complex64::new(signal[t], 0.0);
let x_scaled = Complex64::new(signal[idx as usize], 0.0);
accum += x_t * x_scaled.conj();
}
}
row.push((accum * sqrt_s).norm());
}
surface.push(row);
}
Ok(WidebandAmbiguityResult {
delays,
scales,
surface,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_ambiguity_origin_is_energy() {
let signal: Vec<f64> = (0..128)
.map(|i| (2.0 * PI * 10.0 * i as f64 / 128.0).sin())
.collect();
let energy: f64 = signal.iter().map(|&v| v * v).sum();
let config = AmbiguityConfig {
max_delay: 32,
max_doppler: 64,
fs: 1.0,
};
let result = auto_ambiguity(&signal, &config).expect("Ambiguity should succeed");
let tau_zero_idx = config.max_delay; let nu_zero_idx = 0;
let a_00 = result.surface[tau_zero_idx][nu_zero_idx];
let ratio = a_00 / energy;
assert!(
(ratio - 1.0).abs() < 0.3,
"|A(0,0)| = {}, energy = {}, ratio = {}",
a_00,
energy,
ratio
);
}
#[test]
fn test_ambiguity_delay_symmetry() {
let signal: Vec<f64> = (0..64)
.map(|i| (2.0 * PI * 5.0 * i as f64 / 64.0).cos())
.collect();
let config = AmbiguityConfig {
max_delay: 16,
max_doppler: 32,
fs: 1.0,
};
let result = auto_ambiguity(&signal, &config).expect("Ambiguity should succeed");
let centre = config.max_delay;
for d in 1..=config.max_delay.min(10) {
let row_pos = &result.surface[centre + d];
let row_neg = &result.surface[centre - d];
let diff = (row_pos[0] - row_neg[0]).abs();
let max_val = row_pos[0].max(row_neg[0]).max(1e-15);
assert!(
diff / max_val < 0.5,
"Symmetry broken at delay {}: pos={}, neg={}",
d,
row_pos[0],
row_neg[0]
);
}
}
#[test]
fn test_cross_ambiguity_equals_auto() {
let signal: Vec<f64> = (0..64)
.map(|i| (2.0 * PI * 8.0 * i as f64 / 64.0).sin())
.collect();
let config = AmbiguityConfig {
max_delay: 16,
max_doppler: 32,
fs: 1.0,
};
let auto_result = auto_ambiguity(&signal, &config).expect("Auto ambiguity should succeed");
let cross_result =
cross_ambiguity(&signal, &signal, &config).expect("Cross ambiguity should succeed");
for (auto_row, cross_row) in auto_result.surface.iter().zip(cross_result.surface.iter()) {
for (&a, &c) in auto_row.iter().zip(cross_row.iter()) {
let diff = (a - c).abs();
let max_val = a.max(c).max(1e-15);
assert!(
diff / max_val < 1e-10 || diff < 1e-10,
"Auto/cross mismatch: {} vs {}",
a,
c
);
}
}
}
#[test]
fn test_wideband_ambiguity_origin() {
let signal: Vec<f64> = (0..64)
.map(|i| (2.0 * PI * 5.0 * i as f64 / 64.0).sin())
.collect();
let energy: f64 = signal.iter().map(|&v| v * v).sum();
let result =
wideband_ambiguity(&signal, 16, 11, 2.0, 1.0).expect("Wideband should succeed");
let scale_1_idx = result
.scales
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
let da = (*a - 1.0).abs();
let db = (*b - 1.0).abs();
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.expect("scales should not be empty");
let tau_0_idx = 16;
let a_origin = result.surface[tau_0_idx][scale_1_idx];
let ratio = a_origin / energy;
assert!(
ratio > 0.5 && ratio < 2.0,
"Wideband A(0,1) = {}, energy = {}, ratio = {}",
a_origin,
energy,
ratio
);
}
#[test]
fn test_ambiguity_empty_signal() {
let config = AmbiguityConfig::default();
let empty: Vec<f64> = vec![];
assert!(auto_ambiguity(&empty, &config).is_err());
assert!(cross_ambiguity(&empty, &[1.0], &config).is_err());
assert!(wideband_ambiguity(&empty, 10, 5, 2.0, 1.0).is_err());
}
#[test]
fn test_ambiguity_max_at_origin() {
let signal: Vec<f64> = (0..128)
.map(|i| (2.0 * PI * 10.0 * i as f64 / 128.0).sin())
.collect();
let config = AmbiguityConfig {
max_delay: 32,
max_doppler: 64,
fs: 1.0,
};
let result = auto_ambiguity_surface(&signal, &config).expect("Surface should succeed");
let tau_zero_idx = config.max_delay;
let origin_val = result.surface[tau_zero_idx][0];
let global_max = result
.surface
.iter()
.flat_map(|row| row.iter())
.copied()
.fold(f64::NEG_INFINITY, f64::max);
assert!(
origin_val >= global_max * 0.9,
"Origin ({}) should be near global max ({})",
origin_val,
global_max
);
}
}