pub mod budget;
pub mod correction;
pub mod detection;
pub mod dvbt;
pub mod estimation;
pub mod grid;
pub mod link;
pub mod lte;
pub mod nr;
pub mod sync;
pub mod wiener;
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_math::C64;
use comnum_math::fft;
#[derive(Clone, Debug, PartialEq)]
pub struct Ofdm {
pub n_fft: usize,
pub n_active: usize,
pub oversampling: usize,
pub n_cp: usize,
pub dc_null: bool,
pub dft_spread: bool,
pub sample_rate: Hertz,
}
impl Ofdm {
pub fn new(n_fft: usize, n_active: usize) -> Result<Ofdm> {
let o = Ofdm {
n_fft,
n_active,
oversampling: 4,
n_cp: 0,
dc_null: false,
dft_spread: false,
sample_rate: Hertz(1.0),
};
o.check()?;
Ok(o)
}
pub fn check(&self) -> Result<()> {
ensure(self.n_active >= 1 && self.n_active + usize::from(self.dc_null) <= self.n_fft, || {
format!("{} active subcarriers do not fit a grid of {}", self.n_active, self.n_fft)
})?;
ensure(self.oversampling >= 1, || "the oversampling factor must be at least 1".into())?;
check_rate(self.sample_rate)
}
pub fn fft_len(&self) -> usize {
self.oversampling * self.n_fft
}
pub fn block_len(&self) -> usize {
self.oversampling * (self.n_fft + self.n_cp)
}
pub fn subcarrier(&self, i: usize) -> isize {
let k = i as isize - (self.n_active / 2) as isize;
if self.dc_null && k >= 0 { k + 1 } else { k }
}
pub fn bin(&self, i: usize) -> usize {
let skip = usize::from(self.dc_null && i >= self.n_active / 2);
fft::centred_bin(i + skip, self.n_active, self.fft_len())
}
pub fn pulse(&self, i: usize, n: usize) -> (usize, Vec<C64>) {
let (len, cp) = (self.fft_len(), self.oversampling * self.n_cp);
let (bin, scale) = (self.bin(i) as f64, 1.0 / (len as f64).sqrt());
let values = (0..self.block_len())
.map(|t| comnum_math::cis(std::f64::consts::TAU * bin * (t as f64 - cp as f64) / len as f64) * scale)
.collect();
(n * self.block_len(), values)
}
pub fn window(&self, i: usize, n: usize) -> (usize, Vec<C64>) {
let (start, mut values) = self.pulse(i, n);
let cp = self.oversampling * self.n_cp;
values.drain(..cp);
(start + cp, values)
}
pub fn is_active_bin(&self, bin: usize) -> bool {
(0..self.n_active).any(|i| self.bin(i) == bin)
}
fn scale(&self) -> f64 {
self.fft_len() as f64 / (self.n_active as f64).sqrt()
}
pub fn spread(&self, data: &[C64]) -> Vec<C64> {
if self.dft_spread {
let s = 1.0 / (data.len() as f64).sqrt();
fft::fft(data).into_iter().map(|v| v * s).collect()
} else {
data.to_vec()
}
}
pub fn despread(&self, active: &[C64]) -> Vec<C64> {
if self.dft_spread {
let s = (active.len() as f64).sqrt();
fft::ifft(active).into_iter().map(|v| v * s).collect()
} else {
active.to_vec()
}
}
pub fn to_time(&self, active: &[C64]) -> Result<Vec<C64>> {
ensure_shape(active.len() == self.n_active, || {
format!("{} values for {} active subcarriers", active.len(), self.n_active)
})?;
let mut bins = vec![C64::new(0.0, 0.0); self.fft_len()];
for (i, v) in active.iter().enumerate() {
bins[self.bin(i)] = *v;
}
let s = self.scale();
Ok(fft::ifft(&bins).into_iter().map(|v| v * s).collect())
}
pub fn to_active(&self, block: &[C64]) -> Result<Vec<C64>> {
ensure_shape(block.len() == self.fft_len(), || {
format!("a block of {} samples for an IFFT of {}", block.len(), self.fft_len())
})?;
let bins = fft::fft(block);
let s = 1.0 / self.scale();
Ok((0..self.n_active).map(|i| bins[self.bin(i)] * s).collect())
}
pub fn filter(&self, block: &[C64]) -> Result<Vec<C64>> {
self.to_time(&self.to_active(block)?)
}
pub fn with_prefix(&self, block: &[C64]) -> Result<Vec<C64>> {
let mut out = Vec::with_capacity(block.len() + self.oversampling * self.n_cp);
extend_with_prefix(&mut out, block, self.oversampling * self.n_cp)?;
Ok(out)
}
pub fn windowed(&self, samples: &[C64], ramp: usize) -> Result<Vec<C64>> {
self.check()?;
let prefix = self.oversampling * self.n_cp;
ensure(ramp <= prefix, || format!("a ramp of {ramp} samples exceeds the prefix of {prefix}"))?;
let len = self.block_len();
ensure_shape(samples.len() % len == 0, || {
format!("{} samples is not a multiple of the block length {len}", samples.len())
})?;
let mut out = samples.to_vec();
if ramp == 0 || samples.is_empty() {
return Ok(out);
}
let n_blocks = samples.len() / len;
for b in 0..n_blocks {
let o = b * len;
let prev = (b + n_blocks - 1) % n_blocks * len + prefix;
for n in 0..ramp {
let w = 0.5 * (1.0 - (std::f64::consts::PI * (n as f64 + 0.5) / ramp as f64).cos());
out[o + n] = samples[o + n] * w + samples[prev + n] * (1.0 - w);
}
}
Ok(out)
}
pub fn modulate(&self, data: &[C64]) -> Result<Vec<C64>> {
Ok(self.modulate_with(data, &NoReduction)?.0)
}
pub fn demodulate(&self, samples: &[C64]) -> Result<Vec<C64>> {
let blocks = samples.len() / self.block_len().max(1);
self.demodulate_with(samples, &NoReduction, &vec![0; blocks])
}
pub fn papr_db_per_block(&self, samples: &[C64]) -> Vec<f64> {
let cp = self.oversampling * self.n_cp;
samples.chunks_exact(self.block_len()).map(|b| comnum_math::papr_db(&b[cp..])).collect()
}
pub fn modulate_with(&self, data: &[C64], reduction: &dyn PaprReduction) -> Result<(Vec<C64>, Vec<usize>)> {
self.check()?;
let k = reduction.data_len(self);
ensure(k >= 1, || "no data subcarrier".into())?;
ensure_shape(data.len() % k == 0, || format!("{} symbols is not a multiple of {k} per block", data.len()))?;
let mut out = Vec::with_capacity(data.len() / k * self.block_len());
let mut side = Vec::with_capacity(data.len() / k);
for chunk in data.chunks(k) {
let (block, s) = reduction.reduce(self, chunk)?;
extend_with_prefix(&mut out, &block, self.oversampling * self.n_cp)?;
side.push(s);
}
Ok((out, side))
}
pub fn demodulate_with(&self, samples: &[C64], reduction: &dyn PaprReduction, side: &[usize]) -> Result<Vec<C64>> {
self.check()?;
let len = self.block_len();
ensure_shape(samples.len() % len == 0, || {
format!("{} samples is not a multiple of the block length {len}", samples.len())
})?;
ensure_shape(side.len() == samples.len() / len, || {
format!("{} side informations for {} blocks", side.len(), samples.len() / len)
})?;
let mut out = Vec::new();
for (block, &s) in samples.chunks(len).zip(side) {
let mut b = strip_prefix(block, self.oversampling * self.n_cp)?.to_vec();
reduction.restore_time(&mut b);
out.extend(reduction.recover(self, self.to_active(&b)?, s)?);
}
Ok(out)
}
}
pub trait PaprReduction {
fn data_len(&self, ofdm: &Ofdm) -> usize {
ofdm.n_active
}
fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)>;
fn restore_time(&self, _block: &mut [C64]) {}
fn recover(&self, ofdm: &Ofdm, active: Vec<C64>, _side: usize) -> Result<Vec<C64>> {
Ok(ofdm.despread(&active))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct NoReduction;
impl PaprReduction for NoReduction {
fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
Ok((ofdm.to_time(&ofdm.spread(data))?, 0))
}
}
impl Modulate for Ofdm {
fn modulate_into(&self, x: &Symbols, out: &mut Signal) -> Result<()> {
out.set_stream(&Ofdm::modulate(self, crate::one_stream(x, "OFDM")?)?, self.sample_rate);
Ok(())
}
}
impl Demodulate for Ofdm {
fn demodulate_into(&mut self, y: &Signal, out: &mut Symbols) -> Result<()> {
check_input(y, self.sample_rate, "OFDM")?;
out.set_stream(&Ofdm::demodulate(self, y.stream(0))?);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
#[test]
fn the_raised_cosine_window_overlaps_the_ramps_and_keeps_the_fft_part() {
let ofdm = Ofdm { n_cp: 8, oversampling: 2, ..Ofdm::new(32, 32).unwrap() };
let mut rng = Rng::new(7);
let data: Vec<C64> = (0..96).map(|_| rng.complex_gaussian()).collect();
let x = ofdm.modulate(&data).unwrap();
let y = ofdm.windowed(&x, 12).unwrap();
assert_eq!(y.len(), x.len());
let len = ofdm.block_len();
for b in 0..3 {
let o = b * len;
assert!(y[o + 16..o + len].iter().zip(&x[o + 16..o + len]).all(|(a, b)| (a - b).norm() < 1e-12));
let prev = (b + 2) % 3 * len + 16;
for n in 0..12 {
let w = 0.5 * (1.0 - (std::f64::consts::PI * (n as f64 + 0.5) / 12.0).cos());
let expected = x[o + n] * w + x[prev + n] * (1.0 - w);
assert!((y[o + n] - expected).norm() < 1e-12);
}
}
assert_eq!(ofdm.windowed(&x, 0).unwrap(), x);
assert!(ofdm.windowed(&x, 17).is_err());
assert!(ofdm.windowed(&x[1..], 4).is_err());
}
#[test]
fn the_window_leaves_a_periodic_stream_unchanged() {
let ofdm = Ofdm { n_cp: 4, oversampling: 1, dc_null: false, ..Ofdm::new(16, 1).unwrap() };
let data = vec![C64::new(1.0, 0.0); 3];
let x = ofdm.modulate(&data).unwrap();
let y = ofdm.windowed(&x, 4).unwrap();
assert!(y.iter().zip(&x).all(|(a, b)| (a - b).norm() < 1e-12));
}
#[test]
fn the_bins_are_the_subcarriers_modulo_the_fft_size() {
for (n_fft, n_active) in [(8, 4), (16, 7), (64, 64), (64, 63), (256, 200), (12, 1)] {
for dc_null in [false, true] {
for oversampling in [1, 4] {
let ofdm = Ofdm { oversampling, dc_null, ..Ofdm::new(n_fft, n_active).unwrap() };
if ofdm.check().is_err() {
continue;
}
for i in 0..n_active {
let old = ofdm.subcarrier(i).rem_euclid(ofdm.fft_len() as isize) as usize;
assert_eq!(ofdm.bin(i), old, "{n_fft} {n_active} {dc_null} {oversampling} {i}");
}
}
}
}
}
#[test]
fn round_trip_unit_power_and_band_occupancy() {
let mut rng = Rng::new(1);
for (dc_null, spread) in [(false, false), (true, false), (false, true)] {
let o = Ofdm { n_cp: 8, dc_null, dft_spread: spread, ..Ofdm::new(64, 48).unwrap() };
let d: Vec<C64> = (0..48 * 200).map(|_| rng.complex_gaussian()).collect();
let x = o.modulate(&d).unwrap();
assert!(o.demodulate(&x).unwrap().iter().zip(&d).all(|(a, b)| (a - b).norm() < 1e-10));
let p = x.iter().map(C64::norm_sqr).sum::<f64>() / x.len() as f64;
assert!((p - 1.0).abs() < 0.03, "power {p}");
let block = &x[8 * 4..8 * 4 + o.fft_len()];
let spec = fft::fft(block);
for (b, v) in spec.iter().enumerate() {
if !o.is_active_bin(b) {
assert!(v.norm() < 1e-9);
}
}
if dc_null {
assert!(!o.is_active_bin(0));
}
}
assert!(Ofdm::new(64, 65).is_err() && Ofdm::new(64, 0).is_err());
}
#[test]
fn the_traits_wrap_the_methods() {
let o = Ofdm::new(32, 20).unwrap();
let d = Symbols::from_stream(vec![C64::new(1.0, 0.0); 40]).unwrap();
let x = Modulate::modulate(&o, &d).unwrap();
assert_eq!((x.len(), x.sample_rate()), (2 * 128, Hertz(1.0)));
let mut o2 = o.clone();
let back = Demodulate::demodulate(&mut o2, &x).unwrap();
assert!(back.as_slice().iter().all(|v| (v - C64::new(1.0, 0.0)).norm() < 1e-12));
let mut lte = Ofdm { sample_rate: Hertz(128.0 * 15e3), ..o };
let y = Modulate::modulate(<e, &d).unwrap();
assert_eq!(y.sample_rate(), Hertz(1.92e6));
assert!(Demodulate::demodulate(&mut lte, &x).is_err() && Demodulate::demodulate(&mut lte, &y).is_ok());
assert!(Ofdm { sample_rate: Hertz(0.0), ..o2 }.check().is_err());
}
}