use super::{CeOfdm, Receiver, TAU};
use comnum_core::{Result, ensure};
use comnum_mapping::constellation::ConstellationKind;
use comnum_math::{C64, erf, fft, qfunc};
pub fn snr(ce: &CeOfdm, esn0_db: f64) -> Result<f64> {
ce.check()?;
let (k, n) = (ce.n_subcarriers, ce.n_fft());
Ok((TAU * ce.h).powi(2) * n as f64 / (k as f64 * ce.noise_variance(esn0_db)))
}
fn ebn0_db(snr: f64, m: usize) -> f64 {
comnum_math::lin2db((snr / (m as f64).log2()).max(1e-300))
}
pub fn ser_esn0(ce: &CeOfdm, kind: ConstellationKind, m: usize, esn0_db: f64) -> Result<f64> {
comnum_mapping::theory::ser(kind, m, ebn0_db(snr(ce, esn0_db)?, m))
}
fn ber_at(snr: f64, m: usize, kind: ConstellationKind) -> Result<f64> {
let e = ebn0_db(snr, m);
comnum_mapping::theory::ber_exact(kind, m, e).or_else(|_| comnum_mapping::theory::ber(kind, m, e))
}
pub fn ber_esn0(ce: &CeOfdm, kind: ConstellationKind, m: usize, esn0_db: f64) -> Result<f64> {
ber_at(snr(ce, esn0_db)?, m, kind)
}
pub fn phase_autocorrelation(ce: &CeOfdm) -> Result<Vec<f64>> {
ce.check()?;
let (k, n) = (ce.n_subcarriers, ce.n_fft());
Ok((0..n).map(|m| (1..=k).map(|i| (TAU * (i * m % n) as f64 / n as f64).cos()).sum::<f64>() / k as f64).collect())
}
fn circular_spectrum(r: &[f64]) -> Vec<f64> {
fft::fft(&r.iter().map(|&v| C64::new(v, 0.0)).collect::<Vec<_>>()).into_iter().map(|v| v.re).collect()
}
pub fn psd(ce: &CeOfdm) -> Result<Vec<f64>> {
let rho = phase_autocorrelation(ce)?;
let v = (TAU * ce.h).powi(2);
Ok(circular_spectrum(&rho.iter().map(|r| (-v * (1.0 - r)).exp()).collect::<Vec<_>>()))
}
pub fn imp_powers(two_pi_h: f64, n: usize) -> Vec<f64> {
let v = two_pi_h * two_pi_h;
let mut p = Vec::with_capacity(n);
let mut term = (-v).exp();
for i in 0..n {
p.push(term);
term *= v / (i + 1) as f64;
}
p
}
pub fn occupied_bandwidth(sigma: f64, fraction: f64) -> Result<f64> {
ensure(sigma > 0.0 && sigma.is_finite(), || format!("the standard deviation must be positive, got {sigma}"))?;
ensure(fraction > 0.0 && fraction < 1.0, || format!("the fraction must lie in (0, 1), got {fraction}"))?;
let v = sigma * sigma;
let n_terms = (v + 12.0 * sigma + 30.0).ceil() as usize;
let p = imp_powers(sigma, n_terms + 1);
let norm = 1.0 - p[0];
let q = 400usize;
let width = 2 * q + 1;
let mut density = vec![1.0 / width as f64; width];
let mut cdf = vec![0.0; n_terms * q + 1];
for (i, &pi) in p.iter().enumerate().skip(1) {
if i > 1 {
let mut next = vec![0.0; density.len() + width - 1];
let mut prefix = vec![0.0; density.len() + 1];
for (j, &d) in density.iter().enumerate() {
prefix[j + 1] = prefix[j] + d;
}
for (j, out) in next.iter_mut().enumerate() {
let hi = (j + 1).min(density.len());
let lo = (j + 1).saturating_sub(width);
*out = (prefix[hi] - prefix[lo]) / width as f64;
}
density = next;
}
let centre = i * q;
let mut inside = density[centre];
for (j, slot) in cdf.iter_mut().enumerate() {
if j > 0 && j <= centre {
inside += density[centre - j] + density[centre + j];
}
*slot += pi / norm * inside;
}
}
let j = cdf.iter().position(|&c| c >= fraction).unwrap_or(cdf.len() - 1);
let z = if j == 0 { 0.0 } else { (j - 1) as f64 + (fraction - cdf[j - 1]) / (cdf[j] - cdf[j - 1]) };
Ok(z / q as f64)
}
pub fn optimum_gain(two_pi_h: f64, theta_max: Option<f64>) -> f64 {
let g = two_pi_h * two_pi_h;
match theta_max {
None => g,
Some(t) => g * erf(t / (std::f64::consts::SQRT_2 * two_pi_h)),
}
}
pub fn ber_optimum(two_pi_h: f64, ebn0_db: f64) -> f64 {
qfunc((optimum_gain(two_pi_h, None) * 2.0 * comnum_math::db2lin(ebn0_db)).sqrt())
}
pub fn cycle_slip_response(ce: &CeOfdm, start: usize) -> Result<Vec<C64>> {
ce.check()?;
let (k, n) = (ce.n_subcarriers, ce.n_fft());
ensure(start < n, || format!("the slip must start within the block of {n} samples, got {start}"))?;
let scale = (2.0 * k as f64).sqrt() / (n as f64 * ce.h);
Ok((1..=k)
.map(|i| {
let w = |m: usize| comnum_math::cis(-TAU * (i * m % n) as f64 / n as f64);
scale * (w(start) - 1.0) / (1.0 - w(1))
})
.collect())
}
pub fn receiver_gain(ce: &CeOfdm, receiver: Receiver) -> f64 {
let v = (TAU * ce.h).powi(2);
match receiver {
Receiver::Arctangent(_) => 1.0,
Receiver::Linear => (-v / 2.0).exp(),
Receiver::Enhanced => 2.0 * (-v / 2.0).exp() - (-2.0 * v).exp(),
}
}
fn sin_sin(a: f64, b: f64, v: f64, rho: f64) -> f64 {
(-(a * a + b * b) * v / 2.0).exp() * (a * b * rho * v).sinh()
}
pub fn subcarrier_snr(ce: &CeOfdm, esn0_db: f64) -> Result<Vec<f64>> {
let receiver = ce.receiver;
let flat = snr(ce, esn0_db)?;
let (k, n) = (ce.n_subcarriers, ce.n_fft());
let v = (TAU * ce.h).powi(2);
let alpha = receiver_gain(ce, receiver);
let sigma2 = ce.noise_variance(esn0_db);
let rho = phase_autocorrelation(ce)?;
let (noise, signal): (f64, Box<dyn Fn(f64) -> f64>) = match receiver {
Receiver::Arctangent(_) => return Ok(vec![flat; k]),
Receiver::Linear => (sigma2 / 2.0, Box::new(move |r| sin_sin(1.0, 1.0, v, r))),
Receiver::Enhanced => (
sigma2 / 2.0 * (5.0 - 4.0 * (-v / 2.0).exp()) + sigma2 * sigma2 / 4.0,
Box::new(move |r| {
4.0 * sin_sin(1.0, 1.0, v, r) - 2.0 * sin_sin(1.0, 2.0, v, r) + 0.25 * sin_sin(2.0, 2.0, v, r)
}),
),
};
let re: Vec<f64> = rho
.iter()
.enumerate()
.map(|(m, &r)| signal(r) - alpha * alpha * v * r + if m == 0 { noise } else { 0.0 })
.collect();
let se = circular_spectrum(&re);
Ok((1..=k).map(|i| n as f64 * v * alpha * alpha / (2.0 * k as f64 * se[i])).collect())
}
pub fn ser_receiver(ce: &CeOfdm, kind: ConstellationKind, m: usize, esn0_db: f64) -> Result<f64> {
let snrs = subcarrier_snr(ce, esn0_db)?;
let mut total = 0.0;
for &s in &snrs {
total += comnum_mapping::theory::ser(kind, m, ebn0_db(s, m))?;
}
Ok(total / snrs.len() as f64)
}
pub fn ber_receiver(ce: &CeOfdm, kind: ConstellationKind, m: usize, esn0_db: f64) -> Result<f64> {
let snrs = subcarrier_snr(ce, esn0_db)?;
let mut total = 0.0;
for &s in &snrs {
total += ber_at(s, m, kind)?;
}
Ok(total / snrs.len() as f64)
}
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
use comnum_mapping::constellation::Constellation;
#[test]
fn the_psd_is_that_of_a_simulated_block() {
let ce = CeOfdm::new(32, 4, 1.0 / TAU).unwrap();
let n = ce.n_fft();
let theory = psd(&ce).unwrap();
assert!((theory.iter().sum::<f64>() / n as f64 - 1.0).abs() < 1e-12);
let c = Constellation::qam(4).unwrap().normalized();
let blocks = 4000;
let (_, d) = comnum_mapping::source::symbols(&c, 32 * blocks, None, &mut Rng::new(2)).unwrap();
let s = ce.modulate(&d).unwrap();
let mut sim = vec![0.0; n];
for block in s.chunks(n) {
for (acc, v) in sim.iter_mut().zip(fft::fft(block)) {
*acc += v.norm_sqr() / (n * blocks) as f64;
}
}
let band = |v: &[f64], f: &dyn Fn(usize) -> bool| (0..n).filter(|&k| f(k)).map(|k| v[k]).sum::<f64>();
let inside = |k: usize| k >= 1 && (k <= 32 || k >= n - 32);
for (name, f) in [
("line", &(|k: usize| k == 0) as &dyn Fn(usize) -> bool),
("band", &inside),
("outside", &|k| k != 0 && !inside(k)),
] {
let (t, m) = (band(&theory, f), band(&sim, f));
assert!((m / t - 1.0).abs() < 0.03, "{name}: {m} vs {t}");
}
let top = theory[1];
for (k, (t, m)) in theory.iter().zip(&sim).enumerate() {
if *t > 1e-2 * top {
assert!((m / t - 1.0).abs() < 0.15, "bin {k}: {m} vs {t}");
}
}
}
#[test]
fn the_optimum_gain_is_the_distance_of_one_bit() {
let c = Constellation::qam(4).unwrap().normalized();
let mut rng = Rng::new(7);
for (two_pi_h, theta_max) in
[(0.5, None), (1.25, None), (1.5, Some(std::f64::consts::PI)), (1.5, Some(0.9 * std::f64::consts::PI))]
{
let ce = CeOfdm::new(31, 4, two_pi_h / TAU).unwrap();
let phase = |x: f64| {
let p = two_pi_h * x;
comnum_math::cis(theta_max.map_or(p, |t| p.clamp(-t, t)))
};
let mut acc = 0.0;
let trials = 3000;
for _ in 0..trials {
let (_, d) = comnum_mapping::source::symbols(&c, 31, None, &mut rng).unwrap();
let mut e = d.clone();
let k = rng.below(31);
e[k] = C64::new(-e[k].re, e[k].im);
let (x1, x2) = (ce.ofdm_signal(&d).unwrap(), ce.ofdm_signal(&e).unwrap());
let dy: f64 = x1.iter().zip(&x2).map(|(a, b)| (phase(*a) - phase(*b)).norm_sqr()).sum();
let dx: f64 = x1.iter().zip(&x2).map(|(a, b)| (a - b).powi(2)).sum();
let ex: f64 = x1.iter().map(|a| a * a).sum();
acc += (dy / ce.n_fft() as f64) / (dx / ex);
}
let g = acc / trials as f64;
let want = optimum_gain(two_pi_h, theta_max);
assert!((g / want - 1.0).abs() < 0.03, "2πh = {two_pi_h}, {theta_max:?}: {g} vs {want}");
}
let ce = CeOfdm::new(31, 4, 0.6 / TAU).unwrap();
for eb in [4.0, 8.0] {
let lin = ber_esn0(&ce, ConstellationKind::Qam, 4, eb + comnum_math::lin2db(2.0)).unwrap();
assert!((ber_optimum(0.6, eb) / lin - 1.0).abs() < 1e-9);
}
}
#[test]
fn the_occupied_bandwidth_tends_to_the_message_band_and_grows_as_the_index() {
assert!((occupied_bandwidth(0.01, 0.99).unwrap() - 0.99).abs() < 3e-3);
let b: Vec<f64> = [0.5, 1.0, 2.0, 3.0].iter().map(|&s| occupied_bandwidth(s, 0.99).unwrap()).collect();
assert!(b.windows(2).all(|w| w[1] > w[0]), "{b:?}");
let wide = occupied_bandwidth(6.0, 0.99).unwrap();
let woodward = 2.5758 * 6.0 / 3f64.sqrt();
assert!((wide / woodward - 1.0).abs() < 0.03, "{wide} vs {woodward}");
assert!(occupied_bandwidth(0.0, 0.99).is_err() && occupied_bandwidth(1.0, 1.0).is_err());
}
}
#[cfg(test)]
mod literature {
use super::*;
use crate::ce_ofdm::Receiver;
#[test]
fn ahmed_2015_eq_8_series_of_the_enhanced_receiver() {
let x: f64 = 0.2;
let exact = x.sin() * (2.0 - x.cos());
let head = x + x.powi(3) / 3.0 - 7.0 * x.powi(5) / 60.0 + 31.0 * x.powi(7) / 2520.0;
let c11 = 1022.0 / 39_916_800.0;
let c9 = (exact - head - c11 * x.powi(11)) / x.powi(9);
assert!((c9 / (-127.0 / 181_440.0) - 1.0).abs() < 1e-3, "{c9}");
assert!((c9 / (-41.0 / 60_480.0) - 1.0).abs() > 0.02);
}
#[test]
fn ahmed_2015_eqs_17_and_26_are_the_series_of_the_bussgang_analysis() {
for v in [1e-3, 1e-2, 0.04] {
let exact = 4.0 - 4.0 * (-v / 2.0f64).exp() + (1.0 + (-2.0 * v).exp()) / 2.0;
let series = 1.0 + v + v * v / 2.0 - 7.0 / 12.0 * v.powi(3);
assert!((exact - series).abs() < v.powi(4), "{v}");
let ce = CeOfdm::new(512, 2, v.sqrt() / TAU).unwrap();
let n = 1024.0;
let gain = receiver_gain(&ce, Receiver::Linear);
assert!((gain - (1.0 - v * (n - 1.0) / (2.0 * n))).abs() < v * v, "{v}");
}
}
#[test]
fn ahmed_2012_eqs_13_14_and_fig_6_cycle_slips() {
let k = 32;
let ce = CeOfdm::new(k, 32, 0.8 / TAU).unwrap();
let n = ce.n_fft();
let pi_h_cn = std::f64::consts::PI * ce.h / (k as f64).sqrt();
let mut extremes = [(0.0f64, 0.0f64); 10];
for start in 0..n {
let r = cycle_slip_response(&ce, start).unwrap();
let t = start as f64 / n as f64;
for (i, ext) in extremes.iter_mut().enumerate() {
let kk = (i + 1) as f64;
let cos_sub = std::f64::consts::SQRT_2 * r[i].re;
let sin_sub = -std::f64::consts::SQRT_2 * r[i].im;
let want_cos = -(TAU * kk * t).sin() / (kk * pi_h_cn);
let want_sin = ((TAU * kk * t).cos() - 1.0) / (kk * pi_h_cn);
let tol = 0.01 / (kk * pi_h_cn) * kk;
assert!((cos_sub - want_cos).abs() < tol && (sin_sub - want_sin).abs() < tol, "k = {kk}, t = {t}");
*ext = (ext.0.max(cos_sub), ext.1.min(sin_sub));
}
}
let scale = 1.0 / (std::f64::consts::PI * 0.8 / TAU * (2.0f64 / 64.0).sqrt());
assert!((scale - 14.14).abs() < 0.01);
for (i, (hi, lo)) in extremes.iter().enumerate() {
let kk = (i + 1) as f64;
assert!(
(hi * kk * pi_h_cn - 1.0).abs() < 0.02 && (lo * kk * pi_h_cn + 2.0).abs() < 0.04,
"k = {kk}: {hi} {lo}"
);
}
}
#[test]
fn guerreiro_2015_eq_13_and_fig_4() {
let (t, w) = comnum_math::gauss_hermite(80);
for phi in [0.5, 1.0, 2.0] {
let p = imp_powers(phi, 8);
for (m, pm) in p.iter().enumerate() {
let mut acc = C64::new(0.0, 0.0);
for (ti, wi) in t.iter().zip(&w) {
let (mut h0, mut h1) = (1.0, 2.0 * ti);
let hm = if m == 0 {
1.0
} else {
for j in 1..m {
(h0, h1) = (h1, 2.0 * ti * h1 - 2.0 * j as f64 * h0);
}
h1
};
acc += wi * hm * comnum_math::cis(std::f64::consts::SQRT_2 * phi * ti);
}
let fact: f64 = (1..=m).map(|j| j as f64).product();
let quad = acc.norm_sqr() / std::f64::consts::PI / (2f64.powi(m as i32) * fact);
assert!((quad - pm).abs() < 1e-10, "φ₀ = {phi}, m = {m}: {quad} vs {pm}");
}
}
for (two_pi_h, read) in [(0.25, 0.94), (0.5, 0.785), (1.0, 0.375), (1.25, 0.22), (1.5, 0.11), (1.75, 0.05)] {
let p0 = imp_powers(two_pi_h, 1)[0];
assert!((p0 - read).abs() < 0.015, "2πh = {two_pi_h}: {p0} vs {read}");
}
}
#[test]
fn guerreiro_2016_optimum_gains() {
let db = |x: f64| comnum_math::lin2db(optimum_gain(x, None));
assert!((db(1.25) - 1.9).abs() < 0.05 && (db(1.5) - 3.5).abs() < 0.05);
assert!(db(0.99) < 0.0 && db(1.01) > 0.0);
}
#[test]
fn pacheco_2006_fig_1_bandwidth() {
for (sigma, read) in [(0.5, 1.5), (1.0, 1.88), (1.1, 2.0), (1.5, 2.55), (2.0, 3.2), (2.5, 3.9), (3.0, 4.65)] {
let b = occupied_bandwidth(sigma, 0.99).unwrap();
assert!((b / read - 1.0).abs() < 0.04, "σ = {sigma}: {b} vs {read}");
}
}
}