use super::grid::ResourceGrid;
use super::wiener::{Correlation2d, DelaySpectrum, DopplerSpectrum, Wiener2d, WienerPlan};
use comnum_core::{Error, Result, ensure, ensure_shape};
use comnum_dsp::estimation::frequency_correlation;
use comnum_dsp::interpolation::{self, LowPassInterpolator};
use comnum_dsp::resample::resample_fft;
use comnum_math::C64;
use ndarray::Array2;
#[derive(Clone, Debug, PartialEq)]
pub struct PowerDelayProfile {
delays: Vec<f64>,
powers: Vec<f64>,
n_fft: usize,
}
impl PowerDelayProfile {
pub fn new(delays: Vec<f64>, powers: &[f64], n_fft: usize) -> Result<PowerDelayProfile> {
comnum_core::same_len(powers.len(), delays.len(), "powers and delays of the profile")?;
ensure(!delays.is_empty() && n_fft >= 1, || "a power-delay profile needs a tap and an FFT size".into())?;
ensure(delays.iter().all(|d| *d >= 0.0 && d.is_finite()), || {
format!("negative or non-finite delays {delays:?}")
})?;
ensure(powers.iter().all(|p| *p >= 0.0 && p.is_finite()), || {
format!("negative or non-finite powers {powers:?}")
})?;
let t: f64 = powers.iter().sum();
ensure(t > 0.0, || "the powers of the profile sum to zero".into())?;
Ok(PowerDelayProfile { delays, powers: powers.iter().map(|p| p / t).collect(), n_fft })
}
pub fn uniform(max_delay: usize, n_fft: usize) -> Result<PowerDelayProfile> {
let n = max_delay.max(1);
PowerDelayProfile::new((0..n).map(|d| d as f64).collect(), &vec![1.0; n], n_fft)
}
pub fn delays(&self) -> &[f64] {
&self.delays
}
pub fn powers(&self) -> &[f64] {
&self.powers
}
pub fn n_fft(&self) -> usize {
self.n_fft
}
pub fn correlation(&self, delta: f64) -> C64 {
frequency_correlation(&self.delays, &self.powers, delta, self.n_fft)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RandomDelayProfile {
pub paths: usize,
pub max_delay: f64,
pub tau_rms: Option<f64>,
pub first_at_zero: bool,
pub n_fft: usize,
}
impl RandomDelayProfile {
fn random_term(&self, delta: f64) -> C64 {
let a = C64::new(self.tau_rms.map_or(0.0, |t| 1.0 / t), 2.0 * std::f64::consts::PI * delta / self.n_fft as f64);
let al = a * self.max_delay;
if al.norm() < 1e-12 { C64::new(1.0, 0.0) } else { (C64::new(1.0, 0.0) - (-al).exp()) / al }
}
pub fn correlation(&self, delta: f64) -> C64 {
let random = if self.first_at_zero { self.paths.saturating_sub(1) } else { self.paths } as f64;
let fixed = if self.first_at_zero { 1.0 } else { 0.0 };
let total = fixed + random * self.random_term(0.0).re;
(C64::new(fixed, 0.0) + self.random_term(delta) * random) / total
}
pub fn correlation_matrix(&self, n: usize) -> Array2<C64> {
Array2::from_shape_fn((n, n), |(i, j)| self.correlation(i as f64 - j as f64))
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub enum FrequencyInterpolation {
Linear,
Lmmse(PowerDelayProfile),
SecondOrder,
Spline,
LowPass {
half: usize,
alpha: f64,
},
TimeDomain,
}
impl FrequencyInterpolation {
pub fn from_name(name: &str, pdp: Option<PowerDelayProfile>) -> Result<FrequencyInterpolation> {
Ok(match name.to_ascii_lowercase().as_str() {
"linear" => FrequencyInterpolation::Linear,
"lmmse" => FrequencyInterpolation::Lmmse(
pdp.ok_or_else(|| Error::Config("lmmse interpolation needs a power-delay profile".into()))?,
),
"second_order" => FrequencyInterpolation::SecondOrder,
"spline" => FrequencyInterpolation::Spline,
"lowpass" => FrequencyInterpolation::LowPass { half: 4, alpha: 0.5 },
"time_domain" => FrequencyInterpolation::TimeDomain,
other => {
return Err(Error::Config(format!(
"unknown interpolation {other:?}: linear, lmmse, second_order, spline, lowpass, time_domain"
)));
}
})
}
}
pub fn estimate(grid: &ResourceGrid, rx: &[C64], noise_var: f64, method: &FrequencyInterpolation) -> Result<Vec<C64>> {
estimate_port(grid, rx, &PortEstimation { method: method.clone(), noise_var, ..PortEstimation::default() })
}
#[derive(Clone, Debug, PartialEq)]
pub struct PortEstimation {
pub method: FrequencyInterpolation,
pub noise_var: f64,
pub port: usize,
pub n_ports: usize,
}
impl Default for PortEstimation {
fn default() -> PortEstimation {
PortEstimation { method: FrequencyInterpolation::Linear, noise_var: 0.0, port: 0, n_ports: 1 }
}
}
impl PortEstimation {
pub fn check(&self) -> Result<()> {
let (v, p, n) = (self.noise_var, self.port, self.n_ports);
ensure(v >= 0.0 && v.is_finite(), || format!("the noise variance must be finite and non-negative, got {v}"))?;
ensure(p < n, || format!("no port {p} out of {n}"))
}
}
pub fn estimate_port(grid: &ResourceGrid, rx: &[C64], settings: &PortEstimation) -> Result<Vec<C64>> {
settings.check()?;
let PortEstimation { method, noise_var, port, n_ports } = settings;
let (noise_var, port, n_ports) = (*noise_var, *port, *n_ports);
grid.check()?;
let (k_n, l_n) = (grid.n_subcarriers, grid.n_symbols);
ensure_shape(rx.len() == k_n * l_n, || format!("{} REs for a grid of {k_n} × {l_n}", rx.len()))?;
let symbols = grid.pilot_symbols();
if symbols.is_empty() {
return Err(Error::Config("the grid has no pilots".into()));
}
let mut per_symbol = Vec::with_capacity(symbols.len());
let mut lowpass_plans: Vec<(Vec<usize>, LowPassInterpolator)> = Vec::new();
let mut lmmse_plans: Vec<((Vec<usize>, u64), WienerPlan)> = Vec::new();
for &l in &symbols {
let ks = grid.port_pilots(l, port, n_ports);
if ks.is_empty() {
return Err(Error::Config(format!("OFDM symbol {l} has no pilot of port {port} out of {n_ports}")));
}
let pilots: Vec<C64> = ks.iter().map(|&k| grid.pilot(k, l)).collect();
let hp: Vec<C64> = ks.iter().zip(&pilots).map(|(&k, p)| rx[l * k_n + k] / p).collect();
let full = match method {
FrequencyInterpolation::Linear => interpolation::linear(&ks, &hp, k_n)?,
FrequencyInterpolation::SecondOrder => interpolation::quadratic(&ks, &hp, k_n)?,
FrequencyInterpolation::Spline => interpolation::spline(&ks, &hp, k_n)?,
FrequencyInterpolation::LowPass { half, alpha } => {
cached(&mut lowpass_plans, &ks, || LowPassInterpolator::new(&ks, k_n, *half, *alpha))?.apply(&hp)?
}
FrequencyInterpolation::TimeDomain => {
let spacing = k_n / ks.len();
if ks.iter().enumerate().any(|(i, &k)| k != i * spacing) || spacing * ks.len() != k_n {
return Err(Error::Config(format!(
"time-domain interpolation needs pilots on 0, L, 2L, … of the {k_n} subcarriers"
)));
}
resample_fft(&hp, k_n)
}
FrequencyInterpolation::Lmmse(pdp) => {
let energy = pilots.iter().map(C64::norm_sqr).sum::<f64>() / pilots.len() as f64;
let plan = cached(&mut lmmse_plans, &(ks.clone(), energy.to_bits()), || {
let corr = Correlation2d {
fd_ts: 0.0,
doppler: DopplerSpectrum::Uniform,
delay: DelaySpectrum::Profile(pdp.clone()),
};
let pilots: Vec<(usize, usize)> = ks.iter().map(|&k| (0, k)).collect();
let targets: Vec<(usize, usize)> = (0..k_n).map(|q| (0, q)).collect();
Wiener2d::new(pilots, corr, noise_var / energy, ks.len())?.plan(&targets)
})?;
plan.apply(&hp)?
}
};
per_symbol.push(full);
}
let mut out = vec![C64::new(0.0, 0.0); k_n * l_n];
for k in 0..k_n {
let vals: Vec<C64> = per_symbol.iter().map(|v| v[k]).collect();
for (l, v) in interpolation::linear(&symbols, &vals, l_n)?.into_iter().enumerate() {
out[l * k_n + k] = v;
}
}
Ok(out)
}
fn cached<'a, K: PartialEq + Clone, P>(
cache: &'a mut Vec<(K, P)>,
key: &K,
build: impl FnOnce() -> Result<P>,
) -> Result<&'a P> {
let i = if let Some(i) = cache.iter().position(|(k, _)| k == key) {
i
} else {
cache.push((key.clone(), build()?));
cache.len() - 1
};
Ok(&cache[i].1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ofdm::grid::PilotPattern;
use comnum_core::rng::Rng;
use comnum_math::cis;
fn channel(grid: &ResourceGrid, pdp: &PowerDelayProfile, s2: f64, rng: &mut Rng) -> (Vec<C64>, Vec<C64>) {
let taps: Vec<C64> = pdp.powers.iter().map(|p| rng.complex_gaussian() * p.sqrt()).collect();
let hk: Vec<C64> = (0..grid.n_subcarriers)
.map(|k| {
pdp.delays
.iter()
.zip(&taps)
.map(|(d, t)| t * cis(-2.0 * std::f64::consts::PI * k as f64 * d / pdp.n_fft as f64))
.sum()
})
.collect();
let tx = grid.map(&vec![C64::new(1.0, 0.0); grid.n_data()]).unwrap();
let h: Vec<C64> = (0..tx.len()).map(|i| hk[i % grid.n_subcarriers]).collect();
let rx = tx.iter().zip(&h).map(|(x, g)| x * g + rng.complex_gaussian() * s2.sqrt()).collect();
(h, rx)
}
#[test]
fn lmmse_beats_linear_interpolation_and_ls_has_the_noise_variance() {
let grid =
ResourceGrid { n_subcarriers: 64, n_symbols: 2, pattern: PilotPattern::Block { period: 1 }, seed: 1 };
let pdp = PowerDelayProfile::new(vec![0.0, 2.0, 5.0], &[1.0, 0.5, 0.25], 64).unwrap();
let mut rng = Rng::new(2);
let s2 = 0.1;
let (mut e_ls, mut e_mm) = (0.0, 0.0);
for _ in 0..200 {
let (h, rx) = channel(&grid, &pdp, s2, &mut rng);
let a = estimate(&grid, &rx, s2, &FrequencyInterpolation::Linear).unwrap();
let b = estimate(&grid, &rx, s2, &FrequencyInterpolation::Lmmse(pdp.clone())).unwrap();
e_ls += a.iter().zip(&h).map(|(x, y)| (x - y).norm_sqr()).sum::<f64>() / a.len() as f64;
e_mm += b.iter().zip(&h).map(|(x, y)| (x - y).norm_sqr()).sum::<f64>() / b.len() as f64;
}
assert!((e_ls / 200.0 / s2 - 1.0).abs() < 0.05);
assert!(e_mm / 200.0 < 0.1 * s2, "{}", e_mm / 200.0);
}
#[test]
fn the_interpolators_built_once_give_the_former_estimates() {
let former = |grid: &ResourceGrid, rx: &[C64], s2: f64, method: &FrequencyInterpolation| {
let k_n = grid.n_subcarriers;
let symbols = grid.pilot_symbols();
let per_symbol: Vec<Vec<C64>> = symbols
.iter()
.map(|&l| {
let ks = grid.port_pilots(l, 0, 1);
let pilots: Vec<C64> = ks.iter().map(|&k| grid.pilot(k, l)).collect();
let hp: Vec<C64> = ks.iter().zip(&pilots).map(|(&k, p)| rx[l * k_n + k] / p).collect();
match method {
FrequencyInterpolation::LowPass { half, alpha } => {
interpolation::lowpass(&ks, &hp, k_n, *half, *alpha).unwrap()
}
FrequencyInterpolation::Lmmse(pdp) => {
let r_pp = Array2::from_shape_fn((ks.len(), ks.len()), |(i, j)| {
pdp.correlation(ks[i] as f64 - ks[j] as f64)
});
let r_qp = Array2::from_shape_fn((k_n, ks.len()), |(q, j)| {
pdp.correlation(q as f64 - ks[j] as f64)
});
let energy = pilots.iter().map(C64::norm_sqr).sum::<f64>() / pilots.len() as f64;
comnum_dsp::estimation::lmmse(&r_qp, &r_pp, s2 / energy, &hp).unwrap()
}
_ => unreachable!(),
}
})
.collect();
let mut out = vec![C64::new(0.0, 0.0); k_n * grid.n_symbols];
for k in 0..k_n {
let vals: Vec<C64> = per_symbol.iter().map(|v| v[k]).collect();
for (l, v) in interpolation::linear(&symbols, &vals, grid.n_symbols).unwrap().into_iter().enumerate() {
out[l * k_n + k] = v;
}
}
out
};
let pdp = PowerDelayProfile::new(vec![0.0, 2.0, 5.0], &[1.0, 0.5, 0.25], 64).unwrap();
let mut rng = Rng::new(6);
for pattern in [PilotPattern::Comb { spacing: 4 }, PilotPattern::Lattice { freq: 3, time: 2 }] {
let grid = ResourceGrid { n_subcarriers: 48, n_symbols: 6, pattern, seed: 2 };
let (_, rx) = channel(&grid, &pdp, 0.05, &mut rng);
for method in
[FrequencyInterpolation::Lmmse(pdp.clone()), FrequencyInterpolation::LowPass { half: 4, alpha: 0.5 }]
{
assert_eq!(
estimate(&grid, &rx, 0.05, &method).unwrap(),
former(&grid, &rx, 0.05, &method),
"{method:?}"
);
}
}
}
#[test]
fn boosted_pilots_lower_the_noise_of_the_estimates() {
use crate::ofdm::grid::PilotTable;
let boosted = |a: f64| {
let t = PilotTable::new(64, 2, (0..2).flat_map(|l| (0..64).map(move |k| ((k, l), C64::new(a, 0.0)))));
ResourceGrid::from_table(t.unwrap())
};
let pdp = PowerDelayProfile::new(vec![0.0, 2.0, 5.0], &[1.0, 0.5, 0.25], 64).unwrap();
let mut rng = Rng::new(3);
let s2 = 0.1;
let mut mse = [[0.0; 2]; 2];
for (i, a) in [1.0, 2.0].into_iter().enumerate() {
let grid = boosted(a);
for _ in 0..200 {
let (h, rx) = channel(&grid, &pdp, s2, &mut rng);
let ls = estimate(&grid, &rx, s2, &FrequencyInterpolation::Linear).unwrap();
let mm = estimate(&grid, &rx, s2, &FrequencyInterpolation::Lmmse(pdp.clone())).unwrap();
mse[i][0] += ls.iter().zip(&h).map(|(x, y)| (x - y).norm_sqr()).sum::<f64>() / ls.len() as f64 / 200.0;
mse[i][1] += mm.iter().zip(&h).map(|(x, y)| (x - y).norm_sqr()).sum::<f64>() / mm.len() as f64 / 200.0;
}
}
assert!((mse[0][0] / s2 - 1.0).abs() < 0.05, "{mse:?}");
assert!((mse[1][0] / (s2 / 4.0) - 1.0).abs() < 0.05, "{mse:?}");
assert!((mse[0][1] / mse[1][1] / 4.0 - 1.0).abs() < 0.15, "{mse:?}");
}
}