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::filter::{convolve_full, normalize_energy, response_at};
use comnum_dsp::window::chebyshev;
use comnum_math::{C64, cis, fft};
#[derive(Clone, Debug, PartialEq)]
pub struct Ufmc {
pub n_fft: usize,
pub n_subbands: usize,
pub subband_size: usize,
pub filter_len: usize,
pub sidelobe_db: f64,
pub sample_rate: Hertz,
}
impl Default for Ufmc {
fn default() -> Ufmc {
Ufmc { n_fft: 64, n_subbands: 4, subband_size: 12, filter_len: 16, sidelobe_db: 40.0, sample_rate: Hertz(1.0) }
}
}
impl Ufmc {
pub fn check(&self) -> Result<()> {
let (b, q, n) = (self.n_subbands, self.subband_size, self.n_fft);
ensure(b >= 1 && q >= 1 && b * q <= n, || format!("{b} sub-bands of {q} do not fit a grid of {n}"))?;
ensure(self.filter_len >= 1 && self.filter_len <= n + 1, || {
format!("a filter of {} taps for a grid of {n}: between 1 and N + 1", self.filter_len)
})?;
ensure(self.sidelobe_db.is_finite(), || "the sidelobe attenuation must be finite".into())?;
check_rate(self.sample_rate)
}
pub fn n_active(&self) -> usize {
self.n_subbands * self.subband_size
}
pub fn block_len(&self) -> usize {
self.n_fft + self.filter_len - 1
}
pub fn subcarrier(&self, i: usize) -> isize {
i as isize - (self.n_active() / 2) as isize
}
fn window(&self) -> Vec<f64> {
let mut w = chebyshev(self.filter_len, self.sidelobe_db);
normalize_energy(&mut w);
w
}
pub fn subband_taps(&self, b: usize) -> Vec<C64> {
self.shifted(&self.window(), b)
}
fn shifted(&self, window: &[f64], b: usize) -> Vec<C64> {
let first = self.subcarrier(b * self.subband_size) as f64;
let centre = first + (self.subband_size as f64 - 1.0) / 2.0;
let mid = (window.len() as f64 - 1.0) / 2.0;
window
.iter()
.enumerate()
.map(|(l, w)| cis(std::f64::consts::TAU * centre * (l as f64 - mid) / self.n_fft as f64) * *w)
.collect()
}
pub fn response(&self, i: usize) -> C64 {
self.response_of(&self.subband_taps(i / self.subband_size), i)
}
fn response_of(&self, taps: &[C64], i: usize) -> C64 {
response_at(taps, self.subcarrier(i) as f64, self.n_fft as f64)
}
pub fn modulate(&self, data: &[C64]) -> Result<Vec<C64>> {
self.check()?;
let (n, k) = (self.n_fft, self.n_active());
ensure_shape(data.len() % k == 0, || format!("{} symbols for {k} subcarriers", data.len()))?;
let scale = n as f64 / (k as f64).sqrt();
let window = self.window();
let filters: Vec<Vec<C64>> = (0..self.n_subbands).map(|b| self.shifted(&window, b)).collect();
let mut out = Vec::with_capacity(data.len() / k * self.block_len());
for symbol in data.chunks_exact(k) {
let mut x = vec![C64::new(0.0, 0.0); self.block_len()];
for (b, f) in filters.iter().enumerate() {
let mut grid = vec![C64::new(0.0, 0.0); n];
for q in 0..self.subband_size {
let i = b * self.subband_size + q;
grid[fft::centred_bin(i, self.n_active(), n)] = symbol[i] * scale;
}
for (xt, v) in x.iter_mut().zip(convolve_full(f, &fft::ifft(&grid))) {
*xt += v;
}
}
out.extend(x);
}
Ok(out)
}
pub fn demodulate(&self, samples: &[C64]) -> Result<Vec<C64>> {
self.check()?;
let (n, len) = (self.n_fft, self.block_len());
ensure_shape(samples.len() % len == 0, || format!("{} samples for symbols of {len}", samples.len()))?;
let scale = n as f64 / (self.n_active() as f64).sqrt();
let window = self.window();
let filters: Vec<Vec<C64>> = (0..self.n_subbands).map(|b| self.shifted(&window, b)).collect();
let gains: Vec<C64> =
(0..self.n_active()).map(|i| self.response_of(&filters[i / self.subband_size], i) * scale).collect();
let mut out = Vec::with_capacity(samples.len() / len * self.n_active());
for block in samples.chunks_exact(len) {
let mut buf = block.to_vec();
buf.resize(2 * n, C64::new(0.0, 0.0));
let spec = fft::fft(&buf);
for (i, g) in gains.iter().enumerate() {
let bin = 2 * fft::centred_bin(i, self.n_active(), n);
out.push(spec[bin] / g);
}
}
Ok(out)
}
}
impl Modulate for Ufmc {
fn modulate_into(&self, x: &Symbols, out: &mut Signal) -> Result<()> {
out.set_stream(&Ufmc::modulate(self, crate::one_stream(x, "UFMC")?)?, self.sample_rate);
Ok(())
}
}
impl Demodulate for Ufmc {
fn demodulate_into(&mut self, y: &Signal, out: &mut Symbols) -> Result<()> {
check_input(y, self.sample_rate, "UFMC")?;
out.set_stream(&Ufmc::demodulate(self, y.stream(0))?);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
#[test]
fn the_shared_filter_helpers_are_the_former_code() {
let u = Ufmc::default();
let w = chebyshev(16, 40.0);
let norm = w.iter().map(|v| v * v).sum::<f64>().sqrt();
let old: Vec<f64> = w.into_iter().map(|v| v / norm).collect();
assert_eq!(u.window(), old);
for i in 0..u.n_active() {
let taps = u.subband_taps(i / u.subband_size);
let k = u.subcarrier(i) as f64;
let former: C64 =
taps.iter().enumerate().map(|(l, f)| f * cis(-std::f64::consts::TAU * k * l as f64 / 64.0)).sum();
assert_eq!(u.response(i), former);
}
}
#[test]
fn the_sub_bands_are_orthogonal_and_the_filters_unit_energy() {
let u = Ufmc { n_fft: 128, n_subbands: 6, filter_len: 32, sidelobe_db: 50.0, ..Ufmc::default() };
let e: f64 = u.subband_taps(0).iter().map(C64::norm_sqr).sum();
assert!((e - 1.0).abs() < 1e-12);
let mut d = vec![C64::new(0.0, 0.0); u.n_active()];
for v in &mut d[24..36] {
*v = C64::new(1.0, -1.0);
}
let y = u.demodulate(&u.modulate(&d).unwrap()).unwrap();
assert!(y.iter().zip(&d).all(|(a, b)| (a - b).norm() < 1e-10));
}
#[test]
fn the_sidelobes_fall_faster_than_those_of_ofdm() {
let mut r = Rng::new(2);
let mut level = |u: &Ufmc| {
let n = u.n_fft;
let mut p = vec![0.0; 8 * n];
for _ in 0..200 {
let mut d = vec![C64::new(0.0, 0.0); u.n_active()];
for v in &mut d[..12] {
*v = C64::new(if r.below(2) == 0 { 1.0 } else { -1.0 }, if r.below(2) == 0 { 1.0 } else { -1.0 });
}
let mut x = u.modulate(&d).unwrap();
x.resize(8 * n, C64::new(0.0, 0.0));
for (a, b) in p.iter_mut().zip(fft::fft(&x)) {
*a += b.norm_sqr();
}
}
let at = |k: isize| p[(8 * k + 4).rem_euclid(8 * n as isize) as usize];
let first = u.subcarrier(0);
comnum_math::lin2db(at(first - 8) / at(first + 6))
};
let ufmc = level(&Ufmc { n_fft: 128, filter_len: 33, sidelobe_db: 60.0, ..Ufmc::default() });
let ofdm = level(&Ufmc { n_fft: 128, filter_len: 1, sidelobe_db: 60.0, ..Ufmc::default() });
assert!(ufmc < ofdm - 20.0, "UFMC {ufmc} dB, OFDM {ofdm} dB");
}
#[test]
fn filters_longer_than_the_receiver_dft_are_rejected() {
assert!(Ufmc { filter_len: 65, ..Ufmc::default() }.check().is_ok());
assert!(Ufmc { filter_len: 66, ..Ufmc::default() }.check().is_err());
assert!(Ufmc { filter_len: 0, ..Ufmc::default() }.check().is_err());
assert!(Ufmc { n_subbands: 6, ..Ufmc::default() }.check().is_err());
}
}