use crate::{check_input, check_rate};
use comnum_core::signal::{Signal, Symbols};
use comnum_core::traits::{Demodulate, Modulate};
use comnum_core::units::Hertz;
use comnum_core::{Result, ensure, ensure_shape};
use comnum_dsp::signal::{extend_with_prefix, strip_prefix};
use comnum_dsp::sync::carrier;
use comnum_math::fft;
use comnum_math::{C64, cis, db2lin};
use std::f64::consts::TAU;
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Unwrap {
None,
Stream,
}
impl std::str::FromStr for Unwrap {
type Err = comnum_core::Error;
fn from_str(s: &str) -> Result<Unwrap> {
match s.to_ascii_lowercase().as_str() {
"none" => Ok(Unwrap::None),
"stream" => Ok(Unwrap::Stream),
_ => Err(comnum_core::Error::Config(format!("unwrap must be none or stream, got {s:?}"))),
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Receiver {
Arctangent(Unwrap),
Linear,
Enhanced,
}
impl Default for Receiver {
fn default() -> Receiver {
Receiver::Arctangent(Unwrap::None)
}
}
impl std::str::FromStr for Receiver {
type Err = comnum_core::Error;
fn from_str(s: &str) -> Result<Receiver> {
match s.to_ascii_lowercase().as_str() {
"arctangent" | "atan" => Ok(Receiver::Arctangent(Unwrap::None)),
"linear" => Ok(Receiver::Linear),
"enhanced" => Ok(Receiver::Enhanced),
_ => Err(comnum_core::Error::Config(format!(
"the receiver must be arctangent, linear or enhanced, got {s:?}"
))),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CeOfdm {
pub n_subcarriers: usize,
pub oversampling: usize,
pub h: f64,
pub n_cp: usize,
pub receiver: Receiver,
pub sample_rate: Hertz,
}
impl CeOfdm {
pub fn n_fft(&self) -> usize {
2 * self.oversampling * (self.n_subcarriers + 1)
}
pub fn block_len(&self) -> usize {
self.n_fft() + self.n_cp
}
pub fn sps(&self) -> f64 {
self.block_len() as f64 / self.n_subcarriers as f64
}
pub fn noise_variance(&self, esn0_db: f64) -> f64 {
self.sps() / db2lin(esn0_db)
}
pub fn new(n_subcarriers: usize, oversampling: usize, h: f64) -> Result<CeOfdm> {
let ce =
CeOfdm { n_subcarriers, oversampling, h, n_cp: 0, receiver: Receiver::default(), sample_rate: Hertz(1.0) };
ce.check()?;
Ok(ce)
}
pub fn check(&self) -> Result<()> {
ensure(self.n_subcarriers >= 1 && self.oversampling >= 1, || "CE-OFDM needs K ≥ 1 and J ≥ 1".into())?;
ensure(self.h > 0.0 && self.h.is_finite(), || {
format!("the modulation index must be positive, got {}", self.h)
})?;
ensure(self.n_cp <= self.n_fft(), || {
format!("a prefix of {} samples is longer than the block of {}", self.n_cp, self.n_fft())
})?;
check_rate(self.sample_rate)
}
pub fn ofdm_signal(&self, d: &[C64]) -> Result<Vec<f64>> {
self.check()?;
let (k, n) = (self.n_subcarriers, self.n_fft());
ensure_shape(d.len() % k == 0, || format!("{} symbols is not a multiple of K = {k}", d.len()))?;
let mut out = Vec::with_capacity(d.len() / k * n);
for block in d.chunks(k) {
out.extend(self.synthesize(block));
}
Ok(out)
}
fn synthesize(&self, d: &[C64]) -> Vec<f64> {
let (k, n) = (self.n_subcarriers, self.n_fft());
let mut spec = vec![C64::new(0.0, 0.0); n];
for (i, &v) in d.iter().enumerate() {
spec[i + 1] = v;
spec[n - 1 - i] = v.conj();
}
let scale = n as f64 / (2.0 * k as f64).sqrt();
fft::ifft(&spec).into_iter().map(|v| v.re * scale).collect()
}
fn data_bins(&self, x: &[f64]) -> Vec<C64> {
let spec = fft::fft(&x.iter().map(|&v| C64::new(v, 0.0)).collect::<Vec<_>>());
spec[1..=self.n_subcarriers].to_vec()
}
pub fn modulate(&self, d: &[C64]) -> Result<Vec<C64>> {
let x = self.ofdm_signal(d)?;
let n = self.n_fft();
let g = TAU * self.h;
let mut out = Vec::with_capacity(x.len() / n * self.block_len());
for block in x.chunks(n) {
let s: Vec<C64> = block.iter().map(|&v| cis(g * v)).collect();
extend_with_prefix(&mut out, &s, self.n_cp)?;
}
Ok(out)
}
pub fn demodulate_with_receiver(&self, r: &[C64], receiver: Receiver) -> Result<Vec<C64>> {
self.check_block(r.len())?;
let (proxy, gain): (Vec<f64>, f64) = match receiver {
Receiver::Arctangent(unwrap) => (self.received_phase(r, unwrap), 1.0),
Receiver::Linear => (r.iter().map(|v| v.im).collect(), theory::receiver_gain(self, receiver)),
Receiver::Enhanced => {
(r.iter().map(|v| v.im * (2.0 - v.re)).collect(), theory::receiver_gain(self, receiver))
}
};
self.project_checked(&proxy, gain)
}
fn check_block(&self, len: usize) -> Result<()> {
self.check()?;
let l = self.block_len();
ensure_shape(len % l == 0, || format!("{len} samples is not a multiple of N + N_cp = {l}"))
}
#[cfg(test)]
fn project(&self, psi: &[f64], gain: f64) -> Result<Vec<C64>> {
self.check_block(psi.len())?;
self.project_checked(psi, gain)
}
fn project_checked(&self, psi: &[f64], gain: f64) -> Result<Vec<C64>> {
let (k, n, l) = (self.n_subcarriers, self.n_fft(), self.block_len());
let scale = (2.0 * k as f64).sqrt() / (n as f64 * TAU * self.h * gain);
let mut out = Vec::with_capacity(psi.len() / l * k);
for block in psi.chunks(l) {
out.extend(self.data_bins(strip_prefix(block, self.n_cp)?).into_iter().map(|v| v * scale));
}
Ok(out)
}
pub fn received_phase(&self, r: &[C64], unwrap: Unwrap) -> Vec<f64> {
let theta: Vec<f64> = r.iter().map(|v| v.arg()).collect();
match unwrap {
Unwrap::None => theta,
Unwrap::Stream => carrier::unwrap(&theta, TAU),
}
}
}
impl Modulate for CeOfdm {
fn modulate_into(&self, x: &Symbols, out: &mut Signal) -> Result<()> {
out.set_stream(&CeOfdm::modulate(self, crate::one_stream(x, "CE-OFDM")?)?, self.sample_rate);
Ok(())
}
}
impl Demodulate for CeOfdm {
fn demodulate_into(&mut self, y: &Signal, out: &mut Symbols) -> Result<()> {
check_input(y, self.sample_rate, "CE-OFDM")?;
out.set_stream(&self.demodulate_with_receiver(y.stream(0), self.receiver)?);
Ok(())
}
}
pub mod gamp;
pub mod theory;
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
use comnum_mapping::constellation::{Constellation, ConstellationKind};
#[test]
fn the_traits_use_the_receiver_and_the_rate_of_the_configuration() {
let c = Constellation::qam(4).unwrap().normalized();
let (_, d) = comnum_mapping::source::symbols(&c, 32 * 4, None, &mut Rng::new(3)).unwrap();
let mut ce = CeOfdm {
n_cp: 4,
receiver: Receiver::Enhanced,
sample_rate: Hertz(2e6),
..CeOfdm::new(32, 2, 0.02).unwrap()
};
let x = Modulate::modulate(&ce, &Symbols::from_stream(d.clone()).unwrap()).unwrap();
assert_eq!((x.stream(0), x.sample_rate()), (&ce.modulate(&d).unwrap()[..], Hertz(2e6)));
let back = Demodulate::demodulate(&mut ce, &x).unwrap();
assert_eq!(back.as_slice(), &ce.demodulate_with_receiver(x.stream(0), Receiver::Enhanced).unwrap()[..]);
let slow = Signal::from_stream(x.stream(0).to_vec(), Hertz(1e6)).unwrap();
assert!(Demodulate::demodulate(&mut ce, &slow).is_err());
}
#[test]
fn the_projection_inverts_the_ofdm_signal_and_the_phase_variance_is_2pi_h() {
let ce = CeOfdm { n_cp: 5, ..CeOfdm::new(32, 16, 0.6).unwrap() };
let c = Constellation::qam(16).unwrap().normalized();
let (_, d) = comnum_mapping::source::symbols(&c, 32 * 200, None, &mut Rng::new(2)).unwrap();
let x = ce.ofdm_signal(&d).unwrap();
let var = x.iter().map(|v| v * v).sum::<f64>() / x.len() as f64;
assert!((var - 1.0).abs() < 0.03, "{var}");
let s = ce.modulate(&d).unwrap();
assert_eq!(s.len(), 200 * ce.block_len());
assert!((s[0] - s[ce.n_fft()]).norm() < 1e-12);
let e = ce.demodulate_with_receiver(&s, Receiver::Arctangent(Unwrap::Stream)).unwrap();
assert!(e.iter().zip(&d).all(|(a, b)| (a - b).norm() < 1e-8));
let w = ce.demodulate_with_receiver(&s, Receiver::Arctangent(Unwrap::None)).unwrap();
assert!(w.iter().zip(&d).map(|(a, b)| (a - b).norm()).fold(0.0, f64::max) > 0.1);
}
#[test]
fn the_linear_theory_is_proakis_with_the_snr_scaled_by_2pi_h_squared() {
let ce = CeOfdm::new(32, 4, 0.05).unwrap();
let g = (TAU * 0.05).powi(2) * 100.0;
let s = theory::snr(&ce, 20.0).unwrap();
assert!((s - g).abs() < 1e-9 * g);
let ser_ce = theory::ser_esn0(&ce, ConstellationKind::Qam, 16, 20.0).unwrap();
let proakis = comnum_mapping::theory::ser(ConstellationKind::Qam, 16, comnum_math::lin2db(g / 4.0)).unwrap();
assert!((ser_ce - proakis).abs() < 1e-12);
let cp = CeOfdm { n_cp: ce.n_fft() / 4, ..ce.clone() };
assert!((theory::snr(&cp, 20.0).unwrap() / s - 0.8).abs() < 1e-12);
let c = Constellation::qam(16).unwrap().normalized();
let (_, d) = comnum_mapping::source::symbols(&c, 32 * 2000, None, &mut Rng::new(5)).unwrap();
let mut rng = Rng::new(6);
let ce = CeOfdm::new(32, 2, 0.1).unwrap();
let esn0_db = 20.0;
let sigma = ce.noise_variance(esn0_db).sqrt();
let r: Vec<C64> = ce.modulate(&d).unwrap().into_iter().map(|v| v + rng.complex_gaussian() * sigma).collect();
let e = ce.demodulate_with_receiver(&r, Receiver::Arctangent(Unwrap::None)).unwrap();
let errors = c.decide(&e).iter().zip(c.decide(&d)).filter(|(a, b)| *a != b).count();
let ser = errors as f64 / d.len() as f64;
let want = theory::ser_esn0(&ce, ConstellationKind::Qam, 16, esn0_db).unwrap();
assert!((ser / want - 1.0).abs() < 0.15, "{ser} vs {want}");
}
#[test]
fn a_cycle_slip_adds_the_response_of_the_theory() {
let ce = CeOfdm { n_cp: 6, ..CeOfdm::new(31, 2, 0.8 / TAU).unwrap() };
let c = Constellation::qam(4).unwrap().normalized();
let (_, d) = comnum_mapping::source::symbols(&c, 31, None, &mut Rng::new(4)).unwrap();
let s = ce.modulate(&d).unwrap();
let clean = ce.demodulate_with_receiver(&s, Receiver::Arctangent(Unwrap::Stream)).unwrap();
for start in [0usize, 1, 40, 127] {
let mut psi = ce.received_phase(&s, Unwrap::Stream);
for v in &mut psi[ce.n_cp + start..] {
*v += TAU;
}
let slipped = ce.project(&psi, 1.0).unwrap();
let want = theory::cycle_slip_response(&ce, start).unwrap();
for ((a, b), w) in slipped.iter().zip(&clean).zip(&want) {
assert!((a - b - w).norm() < 1e-9, "{start}: {} vs {w}", a - b);
}
}
assert!(theory::cycle_slip_response(&ce, ce.n_fft()).is_err());
}
#[test]
fn the_linear_receivers_follow_the_bussgang_analysis() {
let c = Constellation::qam(4).unwrap().normalized();
let mut rng = Rng::new(9);
let (_, d) = comnum_mapping::source::symbols(&c, 32 * 3000, None, &mut rng).unwrap();
for (two_pi_h, ebn0_db) in [(0.3, 15.0), (0.5, 10.5), (0.7, 8.0)] {
let ce = CeOfdm::new(32, 2, two_pi_h / TAU).unwrap();
let esn0_db = ebn0_db + comnum_math::lin2db(2.0);
let sigma = ce.noise_variance(esn0_db).sqrt();
let r: Vec<C64> =
ce.modulate(&d).unwrap().into_iter().map(|v| v + rng.complex_gaussian() * sigma).collect();
for receiver in [Receiver::Linear, Receiver::Enhanced] {
let e = ce.demodulate_with_receiver(&r, receiver).unwrap();
let bits: usize =
c.decide(&e).iter().zip(c.decide(&d)).map(|(a, b)| (a ^ b).count_ones() as usize).sum();
let ber = bits as f64 / (2.0 * d.len() as f64);
let want = theory::ber_receiver(&CeOfdm { receiver, ..ce.clone() }, ConstellationKind::Qam, 4, esn0_db)
.unwrap();
assert!((ber / want - 1.0).abs() < 0.1, "2πh = {two_pi_h}, {receiver:?}: {ber:e} vs {want:e}");
}
}
let ce = CeOfdm::new(32, 2, 0.01 / TAU).unwrap();
let lin = theory::subcarrier_snr(&CeOfdm { receiver: Receiver::Linear, ..ce.clone() }, 30.0).unwrap();
let flat = theory::snr(&ce, 30.0).unwrap();
assert!(lin.iter().all(|s| (s / flat - 1.0).abs() < 1e-3), "{lin:?} vs {flat}");
}
}
#[cfg(test)]
mod literature {
use super::{CeOfdm, theory};
use comnum_mapping::constellation::ConstellationKind;
use comnum_math::{db2lin, qfunc};
#[test]
fn thompson_2008_eq_15_linear_ber() {
for (m_pam, m_qam) in [(4usize, 16usize), (8, 64)] {
let log2m = (m_pam as f64).log2();
for h in [0.05, 0.1, 0.3] {
for ebn0_db in [6.0, 10.0, 14.0] {
let ce = CeOfdm::new(64, 4, h).unwrap();
let esn0_db = ebn0_db + comnum_math::lin2db(2.0 * log2m);
let lib = theory::ber_esn0(&ce, ConstellationKind::Qam, m_qam, esn0_db).unwrap();
let arg = std::f64::consts::TAU
* h
* (6.0 * log2m / ((m_pam * m_pam - 1) as f64) * db2lin(ebn0_db)).sqrt();
let paper = 2.0 * (m_pam - 1) as f64 / (m_pam as f64 * log2m) * qfunc(arg);
assert!(lib >= paper * (1.0 - 1e-12), "M = {m_pam}, h = {h}: {lib} below the bound {paper}");
if paper < 1e-2 {
assert!((lib / paper - 1.0).abs() < 0.01, "M = {m_pam}, h = {h}: {lib} vs {paper}");
}
}
}
}
}
}