use super::Ofdm;
use comnum_core::rng::Rng;
use comnum_core::{Result, ensure, ensure_shape};
use comnum_dsp::sync::frame::{delayed_correlation, windowed_energy};
use comnum_mapping::source::qpsk;
use comnum_math::{C64, cis, diric, sinc};
use std::f64::consts::TAU;
pub fn preamble(ofdm: &Ofdm, seed: u64) -> Result<Vec<C64>> {
let mut r = Rng::new(seed);
let active: Vec<C64> = (0..ofdm.n_active)
.map(|i| {
let s = qpsk(&mut r) * std::f64::consts::SQRT_2;
if ofdm.subcarrier(i).rem_euclid(2) == 0 { s } else { C64::new(0.0, 0.0) }
})
.collect();
ofdm.with_prefix(&ofdm.to_time(&active)?)
}
pub fn schmidl_cox(r: &[C64], half: usize) -> Vec<f64> {
metric(r, half).0
}
fn metric(r: &[C64], half: usize) -> (Vec<f64>, Vec<C64>) {
let p = delayed_correlation(r, half, half);
let e = windowed_energy(r, half);
let m = p
.iter()
.enumerate()
.map(|(d, p)| {
let e = e[d + half];
if e > 0.0 { p.norm_sqr() / (e * e) } else { 0.0 }
})
.collect();
(m, p)
}
pub const PLATEAU_FRACTION: f64 = 0.9;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Estimate {
pub timing: usize,
pub cfo: f64,
pub peak: f64,
}
pub fn synchronize(r: &[C64], ofdm: &Ofdm) -> Result<Estimate> {
let half = ofdm.fft_len() / 2;
ensure_shape(r.len() >= 2 * half, || {
format!("the received block of {} samples is shorter than the preamble of {}", r.len(), 2 * half)
})?;
let (m, p) = metric(r, half);
let (imax, peak) = comnum_math::argmax(m.iter().copied()).unwrap_or((0, 0.0));
let thr = PLATEAU_FRACTION * peak;
let mut a = imax;
while a > 0 && m[a - 1] >= thr {
a -= 1;
}
let mut b = imax;
while b + 1 < m.len() && m[b + 1] >= thr {
b += 1;
}
let d = (a + b) / 2;
Ok(Estimate { timing: d, cfo: p[d].arg() / std::f64::consts::PI, peak })
}
pub const MOOSE_ICI_BOUND: f64 = 0.5947;
pub fn repeated_preamble(ofdm: &Ofdm, seed: u64) -> Result<Vec<C64>> {
let mut r = Rng::new(seed);
let active: Vec<C64> = (0..ofdm.n_active).map(|_| qpsk(&mut r)).collect();
let block = ofdm.to_time(&active)?;
let mut out = ofdm.with_prefix(&block)?;
out.extend_from_slice(&block);
Ok(out)
}
pub fn moose(r: &[C64], start: usize, ofdm: &Ofdm) -> Result<f64> {
let n = ofdm.fft_len();
ensure_shape(start + 2 * n <= r.len(), || {
format!("the two blocks of {n} samples from {start} end after the {} received samples", r.len())
})?;
let y1 = ofdm.to_active(&r[start..start + n])?;
let y2 = ofdm.to_active(&r[start + n..start + 2 * n])?;
let s: C64 = y2.iter().zip(&y1).map(|(b, a)| b * a.conj()).sum();
Ok(s.arg() / TAU)
}
pub fn moose_std(es_n0_lin: f64) -> f64 {
1.0 / (TAU * es_n0_lin.sqrt())
}
pub fn cfo_coefficient(n: usize, d: isize, eps: f64) -> C64 {
let x = d as f64 + eps;
let nf = n as f64;
cis(std::f64::consts::PI * (nf - 1.0) * x / nf) * diric(TAU * x / nf, n)
}
pub fn ici_power(ofdm: &Ofdm, i: usize, eps: f64) -> f64 {
let n = ofdm.fft_len();
let k = ofdm.subcarrier(i);
(0..ofdm.n_active).filter(|&l| l != i).map(|l| cfo_coefficient(n, ofdm.subcarrier(l) - k, eps).norm_sqr()).sum()
}
pub fn cfo_snr_bound(snr_lin: f64, eps: f64) -> f64 {
let s = (std::f64::consts::PI * eps).sin();
sinc(eps).powi(2) / (1.0 / snr_lin + MOOSE_ICI_BOUND * s * s)
}
pub fn cp_ml(r: &[C64], ofdm: &Ofdm, snr_lin: f64) -> Result<Estimate> {
let (n, l) = (ofdm.fft_len(), ofdm.oversampling * ofdm.n_cp);
ensure(l > 0, || "the estimator needs a cyclic prefix".into())?;
ensure(snr_lin > 0.0, || format!("the SNR must be positive, got {snr_lin}"))?;
ensure_shape(r.len() >= n + l, || format!("{} samples are shorter than one symbol of {}", r.len(), n + l))?;
let rho = snr_lin / (snr_lin + 1.0);
let p = delayed_correlation(r, n, l);
let e = windowed_energy(r, l);
let lambda = p.iter().enumerate().map(|(t, pt)| pt.norm() - rho * 0.5 * (e[t] + e[t + n]));
let (theta, peak) = comnum_math::argmax(lambda).unwrap_or((0, f64::NEG_INFINITY));
Ok(Estimate { timing: theta, cfo: p[theta].arg() / TAU, peak })
}
#[cfg(test)]
mod tests {
use super::*;
fn offset(r: &[C64], eps: f64, n: usize) -> Vec<C64> {
let mut y = r.to_vec();
comnum_dsp::signal::rotate(&mut y, TAU * eps / n as f64, 0.0);
y
}
#[test]
fn synchronize_reuses_the_correlation_of_the_metric() {
let ofdm = Ofdm { n_cp: 16, ..Ofdm::new(128, 100).unwrap() };
let half = ofdm.fft_len() / 2;
let mut rng = Rng::new(12);
let mut rx: Vec<C64> = (0..300).map(|_| rng.complex_gaussian() * 0.3).collect();
rx.extend(preamble(&ofdm, 5).unwrap().iter().map(|v| v + rng.complex_gaussian() * 0.3));
rx.extend((0..ofdm.block_len()).map(|_| rng.complex_gaussian()));
let rx = offset(&rx, 0.17, ofdm.fft_len());
let e = synchronize(&rx, &ofdm).unwrap();
let m = schmidl_cox(&rx, half);
let (imax, peak) = m.iter().enumerate().fold((0, 0.0), |acc, (i, v)| if *v > acc.1 { (i, *v) } else { acc });
let (mut a, mut b) = (imax, imax);
while a > 0 && m[a - 1] >= 0.9 * peak {
a -= 1;
}
while b + 1 < m.len() && m[b + 1] >= 0.9 * peak {
b += 1;
}
let d = (a + b) / 2;
let p: C64 = (0..half).map(|k| rx[d + k].conj() * rx[d + k + half]).sum();
assert_eq!((e.timing, e.peak), (d, peak));
let cfo = p.arg() / std::f64::consts::PI;
assert!((e.cfo - cfo).abs() <= 1e-13 * cfo.abs(), "{} against {cfo}", e.cfo);
}
#[test]
fn schmidl_cox_on_the_shared_kernel_is_the_former_metric() {
let mut rng = Rng::new(9);
let r: Vec<C64> = (0..300).map(|_| rng.complex_gaussian()).collect();
let former: Vec<f64> = (0..=r.len() - 2 * 64)
.map(|d| {
let p: C64 = (0..64).map(|m| r[d + m].conj() * r[d + m + 64]).sum();
let e: f64 = (0..64).map(|m| r[d + m + 64].norm_sqr()).sum();
if e > 0.0 { p.norm_sqr() / (e * e) } else { 0.0 }
})
.collect();
let m = schmidl_cox(&r, 64);
assert_eq!(m.len(), former.len());
for (d, (a, b)) in m.iter().zip(&former).enumerate() {
assert!((a - b).abs() <= 1e-13 * b, "d = {d}: {a} against {b}");
if d % 64 == 0 {
assert_eq!(a, b, "d = {d}");
}
}
}
#[test]
fn moose_and_the_cyclic_prefix_are_exact_without_noise() {
let ofdm = Ofdm { n_cp: 8, oversampling: 1, ..Ofdm::new(64, 52).unwrap() };
let n = ofdm.fft_len();
let p = repeated_preamble(&ofdm, 3).unwrap();
assert_eq!(p.len(), 8 + 2 * n);
for eps in [-0.45, -0.1, 0.0, 0.23, 0.49] {
assert!((moose(&offset(&p, eps, n), 8, &ofdm).unwrap() - eps).abs() < 1e-12, "{eps}");
}
let mut rng = Rng::new(5);
let data: Vec<C64> = (0..52).map(|_| qpsk(&mut rng)).collect();
let mut r: Vec<C64> = (0..30).map(|_| rng.complex_gaussian() * 0.01).collect();
r.extend(ofdm.modulate(&data).unwrap());
r.extend((0..n + 8 - 30).map(|_| rng.complex_gaussian() * 0.01));
let e = cp_ml(&offset(&r, 0.2, n), &ofdm, 1e4).unwrap();
assert_eq!(e.timing, 30);
assert!((e.cfo - 0.2).abs() < 1e-3, "{}", e.cfo);
assert!(cp_ml(&r, &Ofdm { n_cp: 0, ..ofdm }, 10.0).is_err() && cp_ml(&r[..60], &ofdm, 10.0).is_err());
assert!(moose(&p, 9, &ofdm).is_err());
}
#[test]
fn the_interference_stays_below_moose_bound() {
let ofdm = Ofdm { oversampling: 1, ..Ofdm::new(256, 193).unwrap() };
for eps in [0.05, 0.25, 0.5] {
let s2 = (std::f64::consts::PI * eps).sin().powi(2);
let worst = (0..193).map(|i| ici_power(&ofdm, i, eps)).fold(0.0, f64::max);
assert!(worst <= MOOSE_ICI_BOUND * s2 && worst > 0.0, "{eps}: {worst}");
}
assert!(ici_power(&ofdm, 7, 0.0) < 1e-25); assert!((cfo_snr_bound(100.0, 0.0) - 100.0).abs() < 1e-12);
assert!(cfo_snr_bound(f64::INFINITY, 0.1).is_finite());
}
#[test]
fn the_preamble_has_two_identical_halves_and_noise_hurts_gracefully() {
let ofdm = Ofdm { n_cp: 16, ..Ofdm::new(128, 100).unwrap() };
let p = preamble(&ofdm, 2).unwrap();
let cp = 4 * 16;
let half = ofdm.fft_len() / 2;
for i in 0..half {
assert!((p[cp + i] - p[cp + half + i]).norm() < 1e-12);
}
let mut rng = Rng::new(3);
let mut err = 0.0;
for t in 0..100 {
let mut rx: Vec<C64> = (0..200).map(|_| rng.complex_gaussian() * 0.1f64.sqrt()).collect();
let eps = -0.3 + 0.006 * f64::from(t);
rx.extend(p.iter().map(|v| v + rng.complex_gaussian() * 0.1f64.sqrt()));
rx.extend((0..ofdm.block_len()).map(|_| rng.complex_gaussian() * 1.1f64.sqrt()));
let rx = offset(&rx, eps, ofdm.fft_len());
let e = synchronize(&rx, &ofdm).unwrap();
assert!(e.timing >= 200 && e.timing <= 200 + cp + 8, "timing {}", e.timing);
err += (e.cfo - eps).powi(2);
}
assert!((err / 100.0).sqrt() < 0.01);
}
}