1use super::grid::ResourceGrid;
53use super::wiener::{Correlation2d, DelaySpectrum, DopplerSpectrum, Wiener2d, WienerPlan};
54use comnum_core::{Error, Result, ensure, ensure_shape};
55use comnum_dsp::estimation::frequency_correlation;
56use comnum_dsp::interpolation::{self, LowPassInterpolator};
57use comnum_dsp::resample::resample_fft;
58use comnum_math::C64;
59use ndarray::Array2;
60
61#[derive(Clone, Debug, PartialEq)]
64pub struct PowerDelayProfile {
65 delays: Vec<f64>,
66 powers: Vec<f64>,
67 n_fft: usize,
68}
69
70impl PowerDelayProfile {
71 pub fn new(delays: Vec<f64>, powers: &[f64], n_fft: usize) -> Result<PowerDelayProfile> {
88 comnum_core::same_len(powers.len(), delays.len(), "powers and delays of the profile")?;
89 ensure(!delays.is_empty() && n_fft >= 1, || "a power-delay profile needs a tap and an FFT size".into())?;
90 ensure(delays.iter().all(|d| *d >= 0.0 && d.is_finite()), || {
91 format!("negative or non-finite delays {delays:?}")
92 })?;
93 ensure(powers.iter().all(|p| *p >= 0.0 && p.is_finite()), || {
94 format!("negative or non-finite powers {powers:?}")
95 })?;
96 let t: f64 = powers.iter().sum();
97 ensure(t > 0.0, || "the powers of the profile sum to zero".into())?;
98 Ok(PowerDelayProfile { delays, powers: powers.iter().map(|p| p / t).collect(), n_fft })
99 }
100
101 pub fn uniform(max_delay: usize, n_fft: usize) -> Result<PowerDelayProfile> {
107 let n = max_delay.max(1);
108 PowerDelayProfile::new((0..n).map(|d| d as f64).collect(), &vec![1.0; n], n_fft)
109 }
110
111 pub fn delays(&self) -> &[f64] {
113 &self.delays
114 }
115
116 pub fn powers(&self) -> &[f64] {
118 &self.powers
119 }
120
121 pub fn n_fft(&self) -> usize {
123 self.n_fft
124 }
125
126 pub fn correlation(&self, delta: f64) -> C64 {
129 frequency_correlation(&self.delays, &self.powers, delta, self.n_fft)
130 }
131}
132
133#[derive(Clone, Copy, Debug, PartialEq)]
158pub struct RandomDelayProfile {
159 pub paths: usize,
161 pub max_delay: f64,
163 pub tau_rms: Option<f64>,
165 pub first_at_zero: bool,
167 pub n_fft: usize,
169}
170
171impl RandomDelayProfile {
172 fn random_term(&self, delta: f64) -> C64 {
174 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);
175 let al = a * self.max_delay;
176 if al.norm() < 1e-12 { C64::new(1.0, 0.0) } else { (C64::new(1.0, 0.0) - (-al).exp()) / al }
177 }
178
179 pub fn correlation(&self, delta: f64) -> C64 {
182 let random = if self.first_at_zero { self.paths.saturating_sub(1) } else { self.paths } as f64;
183 let fixed = if self.first_at_zero { 1.0 } else { 0.0 };
184 let total = fixed + random * self.random_term(0.0).re;
185 (C64::new(fixed, 0.0) + self.random_term(delta) * random) / total
186 }
187
188 pub fn correlation_matrix(&self, n: usize) -> Array2<C64> {
190 Array2::from_shape_fn((n, n), |(i, j)| self.correlation(i as f64 - j as f64))
191 }
192}
193
194#[non_exhaustive]
196#[derive(Clone, Debug, PartialEq)]
197pub enum FrequencyInterpolation {
198 Linear,
200 Lmmse(PowerDelayProfile),
202 SecondOrder,
204 Spline,
206 LowPass {
209 half: usize,
211 alpha: f64,
213 },
214 TimeDomain,
218}
219
220impl FrequencyInterpolation {
221 pub fn from_name(name: &str, pdp: Option<PowerDelayProfile>) -> Result<FrequencyInterpolation> {
236 Ok(match name.to_ascii_lowercase().as_str() {
237 "linear" => FrequencyInterpolation::Linear,
238 "lmmse" => FrequencyInterpolation::Lmmse(
239 pdp.ok_or_else(|| Error::Config("lmmse interpolation needs a power-delay profile".into()))?,
240 ),
241 "second_order" => FrequencyInterpolation::SecondOrder,
242 "spline" => FrequencyInterpolation::Spline,
243 "lowpass" => FrequencyInterpolation::LowPass { half: 4, alpha: 0.5 },
244 "time_domain" => FrequencyInterpolation::TimeDomain,
245 other => {
246 return Err(Error::Config(format!(
247 "unknown interpolation {other:?}: linear, lmmse, second_order, spline, lowpass, time_domain"
248 )));
249 }
250 })
251 }
252}
253
254pub fn estimate(grid: &ResourceGrid, rx: &[C64], noise_var: f64, method: &FrequencyInterpolation) -> Result<Vec<C64>> {
261 estimate_port(grid, rx, &PortEstimation { method: method.clone(), noise_var, ..PortEstimation::default() })
262}
263
264#[derive(Clone, Debug, PartialEq)]
267pub struct PortEstimation {
268 pub method: FrequencyInterpolation,
270 pub noise_var: f64,
272 pub port: usize,
274 pub n_ports: usize,
276}
277
278impl Default for PortEstimation {
279 fn default() -> PortEstimation {
281 PortEstimation { method: FrequencyInterpolation::Linear, noise_var: 0.0, port: 0, n_ports: 1 }
282 }
283}
284
285impl PortEstimation {
286 pub fn check(&self) -> Result<()> {
292 let (v, p, n) = (self.noise_var, self.port, self.n_ports);
293 ensure(v >= 0.0 && v.is_finite(), || format!("the noise variance must be finite and non-negative, got {v}"))?;
294 ensure(p < n, || format!("no port {p} out of {n}"))
295 }
296}
297
298pub fn estimate_port(grid: &ResourceGrid, rx: &[C64], settings: &PortEstimation) -> Result<Vec<C64>> {
306 settings.check()?;
307 let PortEstimation { method, noise_var, port, n_ports } = settings;
308 let (noise_var, port, n_ports) = (*noise_var, *port, *n_ports);
309 grid.check()?;
310 let (k_n, l_n) = (grid.n_subcarriers, grid.n_symbols);
311 ensure_shape(rx.len() == k_n * l_n, || format!("{} REs for a grid of {k_n} × {l_n}", rx.len()))?;
312 let symbols = grid.pilot_symbols();
313 if symbols.is_empty() {
314 return Err(Error::Config("the grid has no pilots".into()));
315 }
316 let mut per_symbol = Vec::with_capacity(symbols.len());
319 let mut lowpass_plans: Vec<(Vec<usize>, LowPassInterpolator)> = Vec::new();
320 let mut lmmse_plans: Vec<((Vec<usize>, u64), WienerPlan)> = Vec::new();
321 for &l in &symbols {
322 let ks = grid.port_pilots(l, port, n_ports);
323 if ks.is_empty() {
324 return Err(Error::Config(format!("OFDM symbol {l} has no pilot of port {port} out of {n_ports}")));
325 }
326 let pilots: Vec<C64> = ks.iter().map(|&k| grid.pilot(k, l)).collect();
327 let hp: Vec<C64> = ks.iter().zip(&pilots).map(|(&k, p)| rx[l * k_n + k] / p).collect();
328 let full = match method {
329 FrequencyInterpolation::Linear => interpolation::linear(&ks, &hp, k_n)?,
330 FrequencyInterpolation::SecondOrder => interpolation::quadratic(&ks, &hp, k_n)?,
331 FrequencyInterpolation::Spline => interpolation::spline(&ks, &hp, k_n)?,
332 FrequencyInterpolation::LowPass { half, alpha } => {
333 cached(&mut lowpass_plans, &ks, || LowPassInterpolator::new(&ks, k_n, *half, *alpha))?.apply(&hp)?
334 }
335 FrequencyInterpolation::TimeDomain => {
336 let spacing = k_n / ks.len();
337 if ks.iter().enumerate().any(|(i, &k)| k != i * spacing) || spacing * ks.len() != k_n {
338 return Err(Error::Config(format!(
339 "time-domain interpolation needs pilots on 0, L, 2L, … of the {k_n} subcarriers"
340 )));
341 }
342 resample_fft(&hp, k_n)
343 }
344 FrequencyInterpolation::Lmmse(pdp) => {
345 let energy = pilots.iter().map(C64::norm_sqr).sum::<f64>() / pilots.len() as f64;
347 let plan = cached(&mut lmmse_plans, &(ks.clone(), energy.to_bits()), || {
349 let corr = Correlation2d {
350 fd_ts: 0.0,
351 doppler: DopplerSpectrum::Uniform,
352 delay: DelaySpectrum::Profile(pdp.clone()),
353 };
354 let pilots: Vec<(usize, usize)> = ks.iter().map(|&k| (0, k)).collect();
355 let targets: Vec<(usize, usize)> = (0..k_n).map(|q| (0, q)).collect();
356 Wiener2d::new(pilots, corr, noise_var / energy, ks.len())?.plan(&targets)
357 })?;
358 plan.apply(&hp)?
359 }
360 };
361 per_symbol.push(full);
362 }
363 let mut out = vec![C64::new(0.0, 0.0); k_n * l_n];
365 for k in 0..k_n {
366 let vals: Vec<C64> = per_symbol.iter().map(|v| v[k]).collect();
367 for (l, v) in interpolation::linear(&symbols, &vals, l_n)?.into_iter().enumerate() {
368 out[l * k_n + k] = v;
369 }
370 }
371 Ok(out)
372}
373
374fn cached<'a, K: PartialEq + Clone, P>(
376 cache: &'a mut Vec<(K, P)>,
377 key: &K,
378 build: impl FnOnce() -> Result<P>,
379) -> Result<&'a P> {
380 let i = if let Some(i) = cache.iter().position(|(k, _)| k == key) {
381 i
382 } else {
383 cache.push((key.clone(), build()?));
384 cache.len() - 1
385 };
386 Ok(&cache[i].1)
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use crate::ofdm::grid::PilotPattern;
393 use comnum_core::rng::Rng;
394 use comnum_math::cis;
395
396 fn channel(grid: &ResourceGrid, pdp: &PowerDelayProfile, s2: f64, rng: &mut Rng) -> (Vec<C64>, Vec<C64>) {
398 let taps: Vec<C64> = pdp.powers.iter().map(|p| rng.complex_gaussian() * p.sqrt()).collect();
399 let hk: Vec<C64> = (0..grid.n_subcarriers)
400 .map(|k| {
401 pdp.delays
402 .iter()
403 .zip(&taps)
404 .map(|(d, t)| t * cis(-2.0 * std::f64::consts::PI * k as f64 * d / pdp.n_fft as f64))
405 .sum()
406 })
407 .collect();
408 let tx = grid.map(&vec![C64::new(1.0, 0.0); grid.n_data()]).unwrap();
409 let h: Vec<C64> = (0..tx.len()).map(|i| hk[i % grid.n_subcarriers]).collect();
410 let rx = tx.iter().zip(&h).map(|(x, g)| x * g + rng.complex_gaussian() * s2.sqrt()).collect();
411 (h, rx)
412 }
413
414 #[test]
415 fn lmmse_beats_linear_interpolation_and_ls_has_the_noise_variance() {
416 let grid =
417 ResourceGrid { n_subcarriers: 64, n_symbols: 2, pattern: PilotPattern::Block { period: 1 }, seed: 1 };
418 let pdp = PowerDelayProfile::new(vec![0.0, 2.0, 5.0], &[1.0, 0.5, 0.25], 64).unwrap();
419 let mut rng = Rng::new(2);
420 let s2 = 0.1;
421 let (mut e_ls, mut e_mm) = (0.0, 0.0);
422 for _ in 0..200 {
423 let (h, rx) = channel(&grid, &pdp, s2, &mut rng);
424 let a = estimate(&grid, &rx, s2, &FrequencyInterpolation::Linear).unwrap();
425 let b = estimate(&grid, &rx, s2, &FrequencyInterpolation::Lmmse(pdp.clone())).unwrap();
426 e_ls += a.iter().zip(&h).map(|(x, y)| (x - y).norm_sqr()).sum::<f64>() / a.len() as f64;
427 e_mm += b.iter().zip(&h).map(|(x, y)| (x - y).norm_sqr()).sum::<f64>() / b.len() as f64;
428 }
429 assert!((e_ls / 200.0 / s2 - 1.0).abs() < 0.05);
431 assert!(e_mm / 200.0 < 0.1 * s2, "{}", e_mm / 200.0);
433 }
434
435 #[test]
436 fn the_interpolators_built_once_give_the_former_estimates() {
437 let former = |grid: &ResourceGrid, rx: &[C64], s2: f64, method: &FrequencyInterpolation| {
440 let k_n = grid.n_subcarriers;
441 let symbols = grid.pilot_symbols();
442 let per_symbol: Vec<Vec<C64>> = symbols
443 .iter()
444 .map(|&l| {
445 let ks = grid.port_pilots(l, 0, 1);
446 let pilots: Vec<C64> = ks.iter().map(|&k| grid.pilot(k, l)).collect();
447 let hp: Vec<C64> = ks.iter().zip(&pilots).map(|(&k, p)| rx[l * k_n + k] / p).collect();
448 match method {
449 FrequencyInterpolation::LowPass { half, alpha } => {
450 interpolation::lowpass(&ks, &hp, k_n, *half, *alpha).unwrap()
451 }
452 FrequencyInterpolation::Lmmse(pdp) => {
453 let r_pp = Array2::from_shape_fn((ks.len(), ks.len()), |(i, j)| {
454 pdp.correlation(ks[i] as f64 - ks[j] as f64)
455 });
456 let r_qp = Array2::from_shape_fn((k_n, ks.len()), |(q, j)| {
457 pdp.correlation(q as f64 - ks[j] as f64)
458 });
459 let energy = pilots.iter().map(C64::norm_sqr).sum::<f64>() / pilots.len() as f64;
460 comnum_dsp::estimation::lmmse(&r_qp, &r_pp, s2 / energy, &hp).unwrap()
461 }
462 _ => unreachable!(),
463 }
464 })
465 .collect();
466 let mut out = vec![C64::new(0.0, 0.0); k_n * grid.n_symbols];
467 for k in 0..k_n {
468 let vals: Vec<C64> = per_symbol.iter().map(|v| v[k]).collect();
469 for (l, v) in interpolation::linear(&symbols, &vals, grid.n_symbols).unwrap().into_iter().enumerate() {
470 out[l * k_n + k] = v;
471 }
472 }
473 out
474 };
475 let pdp = PowerDelayProfile::new(vec![0.0, 2.0, 5.0], &[1.0, 0.5, 0.25], 64).unwrap();
476 let mut rng = Rng::new(6);
477 for pattern in [PilotPattern::Comb { spacing: 4 }, PilotPattern::Lattice { freq: 3, time: 2 }] {
478 let grid = ResourceGrid { n_subcarriers: 48, n_symbols: 6, pattern, seed: 2 };
479 let (_, rx) = channel(&grid, &pdp, 0.05, &mut rng);
480 for method in
481 [FrequencyInterpolation::Lmmse(pdp.clone()), FrequencyInterpolation::LowPass { half: 4, alpha: 0.5 }]
482 {
483 assert_eq!(
484 estimate(&grid, &rx, 0.05, &method).unwrap(),
485 former(&grid, &rx, 0.05, &method),
486 "{method:?}"
487 );
488 }
489 }
490 }
491
492 #[test]
493 fn boosted_pilots_lower_the_noise_of_the_estimates() {
494 use crate::ofdm::grid::PilotTable;
495 let boosted = |a: f64| {
497 let t = PilotTable::new(64, 2, (0..2).flat_map(|l| (0..64).map(move |k| ((k, l), C64::new(a, 0.0)))));
498 ResourceGrid::from_table(t.unwrap())
499 };
500 let pdp = PowerDelayProfile::new(vec![0.0, 2.0, 5.0], &[1.0, 0.5, 0.25], 64).unwrap();
501 let mut rng = Rng::new(3);
502 let s2 = 0.1;
503 let mut mse = [[0.0; 2]; 2];
504 for (i, a) in [1.0, 2.0].into_iter().enumerate() {
505 let grid = boosted(a);
506 for _ in 0..200 {
507 let (h, rx) = channel(&grid, &pdp, s2, &mut rng);
508 let ls = estimate(&grid, &rx, s2, &FrequencyInterpolation::Linear).unwrap();
509 let mm = estimate(&grid, &rx, s2, &FrequencyInterpolation::Lmmse(pdp.clone())).unwrap();
510 mse[i][0] += ls.iter().zip(&h).map(|(x, y)| (x - y).norm_sqr()).sum::<f64>() / ls.len() as f64 / 200.0;
511 mse[i][1] += mm.iter().zip(&h).map(|(x, y)| (x - y).norm_sqr()).sum::<f64>() / mm.len() as f64 / 200.0;
512 }
513 }
514 assert!((mse[0][0] / s2 - 1.0).abs() < 0.05, "{mse:?}");
515 assert!((mse[1][0] / (s2 / 4.0) - 1.0).abs() < 0.05, "{mse:?}");
516 assert!((mse[0][1] / mse[1][1] / 4.0 - 1.0).abs() < 0.15, "{mse:?}");
518 }
519}