use super::estimation::PowerDelayProfile;
use comnum_core::{Result, ensure, ensure_shape};
use comnum_dsp::estimation::{Lmmse, lmmse_mse};
use comnum_math::linalg::CMat;
use comnum_math::{C64, bessel_j0, sinc};
use std::f64::consts::PI;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DopplerSpectrum {
Uniform,
Jakes,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DelaySpectrum {
Uniform {
tau_df: f64,
},
Profile(PowerDelayProfile),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Correlation2d {
pub fd_ts: f64,
pub doppler: DopplerSpectrum,
pub delay: DelaySpectrum,
}
impl Correlation2d {
pub fn at(&self, dl: isize, dk: isize) -> C64 {
let x = 2.0 * self.fd_ts * dl as f64;
let rt = match self.doppler {
DopplerSpectrum::Uniform => sinc(x),
DopplerSpectrum::Jakes => bessel_j0(PI * x),
};
let rf = match &self.delay {
DelaySpectrum::Uniform { tau_df } => C64::new(sinc(2.0 * tau_df * dk as f64), 0.0),
DelaySpectrum::Profile(p) => p.correlation(dk as f64),
};
rf * rt
}
fn frequency_weight(&self) -> f64 {
match &self.delay {
DelaySpectrum::Uniform { tau_df } => *tau_df,
DelaySpectrum::Profile(p) => p.delays().iter().fold(0.0f64, |m, &d| m.max(d)) / p.n_fft() as f64,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Wiener2d {
pilots: Vec<(usize, usize)>,
corr: Correlation2d,
noise_to_pilot: f64,
n_taps: usize,
}
impl Wiener2d {
pub fn new(
pilots: Vec<(usize, usize)>,
corr: Correlation2d,
noise_to_pilot: f64,
n_taps: usize,
) -> Result<Wiener2d> {
ensure(!pilots.is_empty(), || "the interpolator needs pilots".into())?;
ensure((1..=pilots.len()).contains(&n_taps), || {
format!("{n_taps} taps for {} pilots: between 1 and the number of pilots", pilots.len())
})?;
ensure(noise_to_pilot >= 0.0 && corr.fd_ts >= 0.0, || "the noise and the Doppler must be non-negative".into())?;
Ok(Wiener2d { pilots, corr, noise_to_pilot, n_taps })
}
pub fn pilots(&self) -> &[(usize, usize)] {
&self.pilots
}
pub fn n_taps(&self) -> usize {
self.n_taps
}
pub fn taps(&self, l: usize, k: usize) -> Vec<usize> {
let (wt, wf) = (self.corr.fd_ts, self.corr.frequency_weight());
let mut order: Vec<(f64, usize, usize)> = self
.pilots
.iter()
.enumerate()
.map(|(i, &(pl, pk))| {
let (dl, dk) = (pl.abs_diff(l), pk.abs_diff(k));
(dl as f64 * wt + dk as f64 * wf, dl, i)
})
.collect();
order.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
let mut taps: Vec<usize> = order.into_iter().take(self.n_taps).map(|(_, _, i)| i).collect();
taps.sort_unstable();
taps
}
fn groups(&self, targets: &[(usize, usize)]) -> Vec<(Vec<usize>, Vec<usize>)> {
let mut groups: Vec<(Vec<usize>, Vec<usize>)> = Vec::new();
let mut index: std::collections::HashMap<Vec<usize>, usize> = std::collections::HashMap::new();
for (t, &(l, k)) in targets.iter().enumerate() {
let taps = self.taps(l, k);
if let Some(&g) = index.get(&taps) {
groups[g].1.push(t);
} else {
index.insert(taps.clone(), groups.len());
groups.push((taps, vec![t]));
}
}
groups
}
fn group_system(&self, targets: &[(usize, usize)], members: &[usize], taps: &[usize]) -> (CMat, CMat) {
let d = |a: usize, b: usize| a as isize - b as isize;
let theta = CMat::from_shape_fn((members.len(), taps.len()), |(i, j)| {
let ((l, k), (pl, pk)) = (targets[members[i]], self.pilots[taps[j]]);
self.corr.at(d(l, pl), d(k, pk))
});
let r = CMat::from_shape_fn((taps.len(), taps.len()), |(i, j)| {
let ((il, ik), (jl, jk)) = (self.pilots[taps[i]], self.pilots[taps[j]]);
self.corr.at(d(il, jl), d(ik, jk))
});
(theta, r)
}
pub fn plan(&self, targets: &[(usize, usize)]) -> Result<WienerPlan> {
let groups = self
.groups(targets)
.into_iter()
.map(|(taps, members)| {
let (theta, r) = self.group_system(targets, &members, &taps);
Ok((taps, members, Lmmse::new(theta, &r, self.noise_to_pilot)?))
})
.collect::<Result<_>>()?;
Ok(WienerPlan { n_pilots: self.pilots.len(), n_targets: targets.len(), groups })
}
pub fn estimate(&self, h_ls: &[C64], targets: &[(usize, usize)]) -> Result<Vec<C64>> {
self.plan(targets)?.apply(h_ls)
}
pub fn mse(&self, targets: &[(usize, usize)]) -> Result<Vec<f64>> {
let mut out = vec![0.0; targets.len()];
for (taps, members) in self.groups(targets) {
let (theta, r) = self.group_system(targets, &members, &taps);
let mse = lmmse_mse(&theta, &r, self.noise_to_pilot, &vec![1.0; members.len()])?;
for (m, v) in members.iter().zip(mse) {
out[*m] = v;
}
}
Ok(out)
}
}
#[derive(Clone, Debug)]
pub struct WienerPlan {
n_pilots: usize,
n_targets: usize,
groups: Vec<(Vec<usize>, Vec<usize>, Lmmse)>,
}
impl WienerPlan {
pub fn apply(&self, h_ls: &[C64]) -> Result<Vec<C64>> {
ensure_shape(h_ls.len() == self.n_pilots, || {
format!("{} LS estimates for {} pilots", h_ls.len(), self.n_pilots)
})?;
let mut out = vec![C64::new(0.0, 0.0); self.n_targets];
for (taps, members, lmmse) in &self.groups {
let obs: Vec<C64> = taps.iter().map(|&t| h_ls[t]).collect();
for (m, v) in members.iter().zip(lmmse.apply(&obs)?) {
out[*m] = v;
}
}
Ok(out)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RobustDesign {
pub k: usize,
pub k_o: usize,
pub omega_d: f64,
}
impl RobustDesign {
pub fn new(k: usize, tmax_ts: f64, fmax_tf: f64) -> Result<RobustDesign> {
ensure(k > 0, || "the design needs tones".into())?;
ensure(tmax_ts >= 0.0 && (0.0..=0.5).contains(&fmax_tf), || {
format!("the delay spread must be non-negative and f_max T_f in [0, 1/2], got {tmax_ts} and {fmax_tf}")
})?;
let k_o = ((k as f64 * tmax_ts).floor() as usize + 1).min(k);
Ok(RobustDesign { k, k_o, omega_d: 2.0 * PI * fmax_tf })
}
pub fn mse(&self, rho: f64) -> f64 {
let a = self.k_o as f64 * rho / self.k as f64;
if self.omega_d == 0.0 {
return 0.0;
}
a * (1.0 - (PI / (a * self.omega_d) + 1.0).powf(-self.omega_d / PI))
}
pub fn fir(&self, taps: usize, rho: f64) -> Result<(Vec<f64>, f64)> {
ensure(taps > 0, || "the filter needs at least one tap".into())?;
let a = self.k_o as f64 * rho / self.k as f64;
let r = |n: usize| sinc(n as f64 * self.omega_d / PI);
let m =
CMat::from_shape_fn((taps, taps), |(i, j)| C64::new(r(i.abs_diff(j)) + if i == j { a } else { 0.0 }, 0.0));
let b = CMat::from_shape_fn((taps, 1), |(i, _)| C64::new(r(i), 0.0));
let c: Vec<f64> =
comnum_math::linalg::solve_hpd(&m.view(), &b.view())?.column(0).iter().map(|v| v.re).collect();
let mse = a * c[0];
Ok((c, mse))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn corr() -> Correlation2d {
Correlation2d { fd_ts: 0.05, doppler: DopplerSpectrum::Uniform, delay: DelaySpectrum::Uniform { tau_df: 0.04 } }
}
#[test]
fn a_pilot_alone_is_estimated_with_the_mmse_of_its_own_noise() {
let w = Wiener2d::new(vec![(0, 0)], corr(), 0.1, 1).unwrap();
assert!((w.mse(&[(0, 0)]).unwrap()[0] - (1.0 - 1.0 / 1.1)).abs() < 1e-12);
let h = w.estimate(&[C64::new(1.1, 0.0)], &[(0, 0)]).unwrap();
assert!((h[0] - C64::new(1.0, 0.0)).norm() < 1e-12);
}
#[test]
fn more_taps_never_increase_the_error_and_the_taps_are_the_nearest() {
let pilots: Vec<(usize, usize)> =
(0..9).step_by(3).flat_map(|l| (0..30).step_by(6).map(move |k| (l, k))).collect();
let mut last = f64::INFINITY;
for n in [1, 4, 9, 15] {
let j = Wiener2d::new(pilots.clone(), corr(), 0.01, n).unwrap().mse(&[(4, 9)]).unwrap()[0];
assert!(j <= last + 1e-12, "{n} taps: {j} after {last}");
last = j;
}
let w = Wiener2d::new(pilots.clone(), corr(), 0.01, 2).unwrap();
let near: Vec<(usize, usize)> = w.taps(3, 7).into_iter().map(|i| pilots[i]).collect();
assert_eq!(near, vec![(0, 6), (3, 6)]);
}
#[test]
fn the_robust_fir_tends_to_eq_57_and_one_tap_is_the_static_mmse() {
let d = RobustDesign::new(120, 20e-6 / 160e-6, 0.008).unwrap();
let rho = 0.1;
let a = d.k_o as f64 * rho / 120.0;
assert!((d.fir(1, rho).unwrap().1 - a / (1.0 + a)).abs() < 1e-15);
let long = d.fir(400, rho).unwrap().1;
assert!(long > d.mse(rho) && long < 1.1 * d.mse(rho), "{long} {}", d.mse(rho));
assert!(RobustDesign::new(0, 0.1, 0.01).is_err() && RobustDesign::new(8, 0.1, 0.6).is_err());
assert!(d.fir(0, rho).is_err());
}
#[test]
fn invalid_configurations_are_errors() {
assert!(Wiener2d::new(vec![], corr(), 0.1, 1).is_err());
assert!(Wiener2d::new(vec![(0, 0)], corr(), 0.1, 2).is_err());
assert!(Wiener2d::new(vec![(0, 0)], corr(), -0.1, 1).is_err());
let w = Wiener2d::new(vec![(0, 0), (0, 6)], corr(), 0.1, 1).unwrap();
assert!(w.estimate(&[C64::new(1.0, 0.0)], &[(0, 1)]).is_err());
}
}