Skip to main content

comnum_waveform/
ufmc.rs

1//! Universal filtered multicarrier (UFMC): OFDM filtered per sub-band.
2//!
3//! # Model
4//!
5//! $K = BQ$ contiguous subcarriers centred on the carrier, out of a grid of $N$, are
6//! grouped in $B$ sub-bands of $Q$ subcarriers. Every sub-band is brought to the time
7//! domain by an IDFT of size $N$ (no cyclic prefix) and filtered by a filter of $L$ taps
8//! centred on it; the symbol is the sum of the $B$ filtered components, $N + L - 1$
9//! samples, and consecutive symbols do not overlap (Schaich & Wild 2014, Sec. 3):
10//!
11//! $$
12//! \mathbf{x} = \sum_{i=1}^{B} \mathbf{F}_{i} \mathbf{V}_{i} \mathbf{s}_{i},
13//! $$
14//!
15//! with $\mathbf{V}_{i}$ the columns of the IDFT of the sub-band and $\mathbf{F}_{i}$ the
16//! Toeplitz matrix of the filter $f_{i}\[l\] = w\[l\]\, e^{j 2\pi c_{i} l / N}$, $w$ a
17//! Dolph–Chebyshev window ([`comnum_dsp::window::chebyshev`]) normalised to unit energy and
18//! $c_{i}$ the centre of the sub-band. The receiver appends zeros up to $2N$ samples and
19//! takes the DFT: the even bins $2k$ carry the subcarriers $k$, $\mathbf{s}_{k}$ times the
20//! response of its filter at $k/N$, which a one-tap equaliser removes. The other
21//! sub-bands vanish there exactly (the IDFT of a sub-band is zero at the bins of the
22//! others): UFMC is orthogonal in the complex sense, unlike FBMC/OQAM, and needs no
23//! prefix; the filters lower the sidelobes between sub-bands.
24//!
25//! The symbols on the subcarriers have unit energy: the IDFT is scaled by
26//! $1/\sqrt{K}$ as in [`crate::ofdm`].
27//!
28//! # Examples
29//! ```
30//! use comnum_waveform::ufmc::Ufmc;
31//! use comnum_math::C64;
32//! // 4 sub-bands of 12 subcarriers on a grid of 64, L = 16
33//! let u = Ufmc { n_fft: 64, n_subbands: 4, subband_size: 12, filter_len: 16, ..Ufmc::default() };
34//! u.check()?;
35//! let d: Vec<C64> = (0..96).map(|i| C64::new(if i % 3 == 0 { 1.0 } else { -1.0 }, 1.0)).collect();
36//! let x = u.modulate(&d)?;
37//! assert_eq!(x.len(), 2 * (64 + 15));
38//! let back = u.demodulate(&x)?;
39//! assert!(back.iter().zip(&d).all(|(a, b)| (a - b).norm() < 1e-9));
40//! # Ok::<(), comnum_core::Error>(())
41//! ```
42//!
43//! # References
44//!
45//! 1. F. Schaich and T. Wild, "Waveform contenders for 5G — OFDM vs. FBMC vs. UFMC,"
46//!    *Proc. 6th Int. Symp. Communications, Control and Signal Processing (ISCCSP)*,
47//!    2014.
48//! 2. F. J. Harris, "On the use of windows for harmonic analysis with the discrete
49//!    Fourier transform," *Proc. IEEE*, 66(1), 51–83, 1978.
50
51use crate::{check_input, check_rate};
52use comnum_core::signal::{Signal, Symbols};
53use comnum_core::traits::{Demodulate, Modulate};
54use comnum_core::units::Hertz;
55use comnum_core::{Result, ensure, ensure_shape};
56use comnum_dsp::filter::{convolve_full, normalize_energy, response_at};
57use comnum_dsp::window::chebyshev;
58use comnum_math::{C64, cis, fft};
59
60/// A UFMC modem (see the module): its fields are its parameters, set by struct update
61/// from [`Ufmc::default`] and checked by [`Ufmc::check`] (and by the modulator and the
62/// demodulator).
63#[derive(Clone, Debug, PartialEq)]
64pub struct Ufmc {
65    /// Size of the grid and of the IDFT, $N$.
66    pub n_fft: usize,
67    /// Sub-bands $B$.
68    pub n_subbands: usize,
69    /// Subcarriers per sub-band, $Q$.
70    pub subband_size: usize,
71    /// Taps of the filters, $L$ (1: plain OFDM without prefix).
72    pub filter_len: usize,
73    /// Attenuation of the sidelobes of the Dolph–Chebyshev filters (dB).
74    pub sidelobe_db: f64,
75    /// The rate of the samples, $N \Delta f$: the rate of the [`Signal`] of [`Modulate`]
76    /// and the rate [`Demodulate`] expects; 1 Hz by default (time in samples).
77    pub sample_rate: Hertz,
78}
79
80impl Default for Ufmc {
81    /// 4 sub-bands of 12 subcarriers on a grid of 64, filtered by Dolph–Chebyshev filters
82    /// of 16 taps whose sidelobes are 40 dB below their main lobe, at the sample rate
83    /// 1 Hz (the example of the module).
84    fn default() -> Ufmc {
85        Ufmc { n_fft: 64, n_subbands: 4, subband_size: 12, filter_len: 16, sidelobe_db: 40.0, sample_rate: Hertz(1.0) }
86    }
87}
88
89impl Ufmc {
90    /// Check the configuration.
91    ///
92    /// # Errors
93    /// [`comnum_core::Error::Config`] if the sub-bands do not fit the grid, if the filter has
94    /// no tap or is longer than $N + 1$ taps (a symbol must fit the DFT of $2N$ of the
95    /// receiver), or for a sample rate that is not positive and finite.
96    pub fn check(&self) -> Result<()> {
97        let (b, q, n) = (self.n_subbands, self.subband_size, self.n_fft);
98        ensure(b >= 1 && q >= 1 && b * q <= n, || format!("{b} sub-bands of {q} do not fit a grid of {n}"))?;
99        ensure(self.filter_len >= 1 && self.filter_len <= n + 1, || {
100            format!("a filter of {} taps for a grid of {n}: between 1 and N + 1", self.filter_len)
101        })?;
102        ensure(self.sidelobe_db.is_finite(), || "the sidelobe attenuation must be finite".into())?;
103        check_rate(self.sample_rate)
104    }
105
106    /// The number of active subcarriers $K = BQ$.
107    pub fn n_active(&self) -> usize {
108        self.n_subbands * self.subband_size
109    }
110
111    /// Samples per symbol, $N + L - 1$.
112    pub fn block_len(&self) -> usize {
113        self.n_fft + self.filter_len - 1
114    }
115
116    /// The subcarrier index (centred on the carrier) of the active subcarrier `i`.
117    pub fn subcarrier(&self, i: usize) -> isize {
118        i as isize - (self.n_active() / 2) as isize
119    }
120
121    /// The Dolph–Chebyshev window of unit energy.
122    fn window(&self) -> Vec<f64> {
123        let mut w = chebyshev(self.filter_len, self.sidelobe_db);
124        normalize_energy(&mut w);
125        w
126    }
127
128    /// The taps of the filter of sub-band `b`: the window shifted to the centre of the
129    /// sub-band.
130    pub fn subband_taps(&self, b: usize) -> Vec<C64> {
131        self.shifted(&self.window(), b)
132    }
133
134    fn shifted(&self, window: &[f64], b: usize) -> Vec<C64> {
135        let first = self.subcarrier(b * self.subband_size) as f64;
136        let centre = first + (self.subband_size as f64 - 1.0) / 2.0;
137        // centred in time too, so that the response is real at the centre of the band
138        let mid = (window.len() as f64 - 1.0) / 2.0;
139        window
140            .iter()
141            .enumerate()
142            .map(|(l, w)| cis(std::f64::consts::TAU * centre * (l as f64 - mid) / self.n_fft as f64) * *w)
143            .collect()
144    }
145
146    /// The response of the filter of the sub-band of the active subcarrier `i` at its
147    /// frequency, the gain the receiver divides by.
148    pub fn response(&self, i: usize) -> C64 {
149        self.response_of(&self.subband_taps(i / self.subband_size), i)
150    }
151
152    fn response_of(&self, taps: &[C64], i: usize) -> C64 {
153        response_at(taps, self.subcarrier(i) as f64, self.n_fft as f64)
154    }
155
156    /// Modulate the symbols `data`, $K$ per UFMC symbol.
157    ///
158    /// # Errors
159    /// [`comnum_core::Error::Shape`] unless `data` holds whole symbols, and the errors of
160    /// [`Ufmc::check`].
161    pub fn modulate(&self, data: &[C64]) -> Result<Vec<C64>> {
162        self.check()?;
163        let (n, k) = (self.n_fft, self.n_active());
164        ensure_shape(data.len() % k == 0, || format!("{} symbols for {k} subcarriers", data.len()))?;
165        let scale = n as f64 / (k as f64).sqrt();
166        let window = self.window();
167        let filters: Vec<Vec<C64>> = (0..self.n_subbands).map(|b| self.shifted(&window, b)).collect();
168        let mut out = Vec::with_capacity(data.len() / k * self.block_len());
169        for symbol in data.chunks_exact(k) {
170            let mut x = vec![C64::new(0.0, 0.0); self.block_len()];
171            for (b, f) in filters.iter().enumerate() {
172                let mut grid = vec![C64::new(0.0, 0.0); n];
173                for q in 0..self.subband_size {
174                    let i = b * self.subband_size + q;
175                    grid[fft::centred_bin(i, self.n_active(), n)] = symbol[i] * scale;
176                }
177                for (xt, v) in x.iter_mut().zip(convolve_full(f, &fft::ifft(&grid))) {
178                    *xt += v;
179                }
180            }
181            out.extend(x);
182        }
183        Ok(out)
184    }
185
186    /// Demodulate whole symbols: zeros up to $2N$, DFT, the even bins divided by the
187    /// response of the filters.
188    ///
189    /// # Errors
190    /// [`comnum_core::Error::Shape`] unless `samples` holds whole symbols, and the errors
191    /// of [`Ufmc::check`].
192    pub fn demodulate(&self, samples: &[C64]) -> Result<Vec<C64>> {
193        self.check()?;
194        let (n, len) = (self.n_fft, self.block_len());
195        ensure_shape(samples.len() % len == 0, || format!("{} samples for symbols of {len}", samples.len()))?;
196        let scale = n as f64 / (self.n_active() as f64).sqrt();
197        let window = self.window();
198        let filters: Vec<Vec<C64>> = (0..self.n_subbands).map(|b| self.shifted(&window, b)).collect();
199        let gains: Vec<C64> =
200            (0..self.n_active()).map(|i| self.response_of(&filters[i / self.subband_size], i) * scale).collect();
201        let mut out = Vec::with_capacity(samples.len() / len * self.n_active());
202        for block in samples.chunks_exact(len) {
203            let mut buf = block.to_vec();
204            buf.resize(2 * n, C64::new(0.0, 0.0));
205            let spec = fft::fft(&buf);
206            for (i, g) in gains.iter().enumerate() {
207                // the DFT of twice the size: the subcarrier k is the bin 2k
208                let bin = 2 * fft::centred_bin(i, self.n_active(), n);
209                out.push(spec[bin] / g);
210            }
211        }
212        Ok(out)
213    }
214}
215
216/// UFMC as a [`Modulate`] block: one stream of symbols into one stream of samples at
217/// [`Ufmc::sample_rate`].
218impl Modulate for Ufmc {
219    fn modulate_into(&self, x: &Symbols, out: &mut Signal) -> Result<()> {
220        out.set_stream(&Ufmc::modulate(self, crate::one_stream(x, "UFMC")?)?, self.sample_rate);
221        Ok(())
222    }
223}
224
225/// UFMC as a [`Demodulate`] block: one stream of samples at [`Ufmc::sample_rate`].
226impl Demodulate for Ufmc {
227    /// # Errors
228    /// [`comnum_core::Error::Config`] for several streams or a signal at another sample rate,
229    /// and the errors of [`Ufmc::demodulate`].
230    fn demodulate_into(&mut self, y: &Signal, out: &mut Symbols) -> Result<()> {
231        check_input(y, self.sample_rate, "UFMC")?;
232        out.set_stream(&Ufmc::demodulate(self, y.stream(0))?);
233        Ok(())
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use comnum_core::rng::Rng;
241
242    /// The window was normalised and the responses evaluated by hand before
243    /// `comnum_dsp::filter::{normalize_energy, response_at}`: the same taps, gains and
244    /// symbols to the last bit.
245    #[test]
246    fn the_shared_filter_helpers_are_the_former_code() {
247        let u = Ufmc::default();
248        let w = chebyshev(16, 40.0);
249        let norm = w.iter().map(|v| v * v).sum::<f64>().sqrt();
250        let old: Vec<f64> = w.into_iter().map(|v| v / norm).collect();
251        assert_eq!(u.window(), old);
252        for i in 0..u.n_active() {
253            let taps = u.subband_taps(i / u.subband_size);
254            let k = u.subcarrier(i) as f64;
255            let former: C64 =
256                taps.iter().enumerate().map(|(l, f)| f * cis(-std::f64::consts::TAU * k * l as f64 / 64.0)).sum();
257            assert_eq!(u.response(i), former);
258        }
259    }
260
261    #[test]
262    fn the_sub_bands_are_orthogonal_and_the_filters_unit_energy() {
263        let u = Ufmc { n_fft: 128, n_subbands: 6, filter_len: 32, sidelobe_db: 50.0, ..Ufmc::default() };
264        let e: f64 = u.subband_taps(0).iter().map(C64::norm_sqr).sum();
265        assert!((e - 1.0).abs() < 1e-12);
266        // one sub-band alone leaves nothing on the others
267        let mut d = vec![C64::new(0.0, 0.0); u.n_active()];
268        for v in &mut d[24..36] {
269            *v = C64::new(1.0, -1.0);
270        }
271        let y = u.demodulate(&u.modulate(&d).unwrap()).unwrap();
272        assert!(y.iter().zip(&d).all(|(a, b)| (a - b).norm() < 1e-10));
273    }
274
275    #[test]
276    fn the_sidelobes_fall_faster_than_those_of_ofdm() {
277        // the spectrum of random symbols on the first sub-band only, 8 subcarriers past
278        // its edge relative to its centre, against the same sub-band without filter
279        // (L = 1: plain OFDM)
280        let mut r = Rng::new(2);
281        let mut level = |u: &Ufmc| {
282            let n = u.n_fft;
283            let mut p = vec![0.0; 8 * n];
284            for _ in 0..200 {
285                let mut d = vec![C64::new(0.0, 0.0); u.n_active()];
286                for v in &mut d[..12] {
287                    *v = C64::new(if r.below(2) == 0 { 1.0 } else { -1.0 }, if r.below(2) == 0 { 1.0 } else { -1.0 });
288                }
289                let mut x = u.modulate(&d).unwrap();
290                x.resize(8 * n, C64::new(0.0, 0.0));
291                for (a, b) in p.iter_mut().zip(fft::fft(&x)) {
292                    *a += b.norm_sqr();
293                }
294            }
295            // half-way between subcarriers, where the sinc of OFDM has its sidelobes
296            let at = |k: isize| p[(8 * k + 4).rem_euclid(8 * n as isize) as usize];
297            let first = u.subcarrier(0);
298            comnum_math::lin2db(at(first - 8) / at(first + 6))
299        };
300        let ufmc = level(&Ufmc { n_fft: 128, filter_len: 33, sidelobe_db: 60.0, ..Ufmc::default() });
301        let ofdm = level(&Ufmc { n_fft: 128, filter_len: 1, sidelobe_db: 60.0, ..Ufmc::default() });
302        assert!(ufmc < ofdm - 20.0, "UFMC {ufmc} dB, OFDM {ofdm} dB");
303    }
304
305    #[test]
306    fn filters_longer_than_the_receiver_dft_are_rejected() {
307        assert!(Ufmc { filter_len: 65, ..Ufmc::default() }.check().is_ok());
308        assert!(Ufmc { filter_len: 66, ..Ufmc::default() }.check().is_err());
309        assert!(Ufmc { filter_len: 0, ..Ufmc::default() }.check().is_err());
310        assert!(Ufmc { n_subbands: 6, ..Ufmc::default() }.check().is_err());
311    }
312}