Skip to main content

comnum_waveform/ofdm/
mod.rs

1//! OFDM and DFT-spread OFDM.
2//!
3//! A block carries $K$ symbols on $K$ contiguous subcarriers centred on the carrier,
4//! out of a grid of $N$ subcarriers; the time signal is computed by an IFFT of size
5//! $L N$, an oversampling by $L$ that the peak measurements need (the peaks of the
6//! continuous signal fall between the Nyquist-rate samples; $L = 4$ is the usual
7//! choice), and a cyclic prefix of $N_{\mathrm{cp}}$ Nyquist-rate samples ($L N_{\mathrm{cp}}$
8//! samples) is prepended.
9//!
10//! # Model
11//!
12//! With $X_{k}$ the symbol of subcarrier $k \in \\{-K/2, \dots, K/2 - 1\\}$ (unit average
13//! energy),
14//!
15//! $$
16//! x\[n\] = \frac{1}{\sqrt{K}} \sum_{k} X_{k}\\, e^{\\,j 2 \pi k n / (L N)}, \qquad
17//! n = 0, \dots, L N - 1,
18//! $$
19//!
20//! so that $\mathbb{E}|x\[n\]|^{2} = 1$: the samples have unit average power whatever $K$,
21//! $N$ and $L$. With `dft_spread`, the $K$ data symbols are first transformed by a
22//! unitary DFT of size $K$ (localised SC-FDMA, the uplink of LTE): the signal is then a
23//! single-carrier signal interpolated by the subcarrier mapping, whose PAPR is that of
24//! the constellation, not that of a Gaussian signal.
25//!
26//! The submodules build the rest of an OFDM link: [`grid`] (data and pilot resource
27//! elements), [`estimation`] (LS and LMMSE channel estimation on the grid), [`wiener`]
28//! (two-dimensional Wiener interpolation of pilots, Hoeher et al. 1997), [`sync`]
29//! (Schmidl & Cox timing and frequency synchronisation), [`budget`] (the SNR loss of
30//! residual synchronisation and estimation errors, Speth et al. 1999), [`detection`] (MIMO-OFDM,
31//! per-subcarrier detection) and [`correction`] (pilot-based gain correction against a
32//! cochannel interferer, Cimini 1985); the signals of the standards are in [`nr`], [`lte`]
33//! (the synchronisation, reference and broadcast signals of the LTE downlink) and [`dvbt`].
34//!
35//! # Examples
36//! ```
37//! use comnum_waveform::ofdm::Ofdm;
38//! use comnum_math::C64;
39//! let ofdm = Ofdm { n_cp: 16, ..Ofdm::new(256, 200)? };   // L = 4 by default
40//! let d: Vec<C64> = (0..400).map(|i| C64::new(if i % 3 == 0 { 1.0 } else { -1.0 }, 0.0)).collect();
41//! let x = ofdm.modulate(&d)?;
42//! assert_eq!(x.len(), 2 * 4 * (256 + 16));
43//! let back = ofdm.demodulate(&x)?;
44//! assert!(back.iter().zip(&d).all(|(a, b)| (a - b).norm() < 1e-12));
45//! # Ok::<(), comnum_core::Error>(())
46//! ```
47//!
48//! # References
49//!
50//! 1. R. van Nee and R. Prasad, *OFDM for Wireless Multimedia Communications*, Artech
51//!    House, 2000.
52//! 2. H. G. Myung, J. Lim and D. J. Goodman, "Single carrier FDMA for uplink wireless
53//!    transmission," *IEEE Veh. Technol. Mag.*, 1(3), 30–38, 2006.
54
55pub mod budget;
56pub mod correction;
57pub mod detection;
58pub mod dvbt;
59pub mod estimation;
60pub mod grid;
61pub mod link;
62pub mod lte;
63pub mod nr;
64pub mod sync;
65pub mod wiener;
66
67use crate::{check_input, check_rate};
68use comnum_core::signal::{Signal, Symbols};
69use comnum_core::traits::{Demodulate, Modulate};
70use comnum_core::units::Hertz;
71use comnum_core::{Result, ensure, ensure_shape};
72use comnum_dsp::signal::{extend_with_prefix, strip_prefix};
73use comnum_math::C64;
74use comnum_math::fft;
75
76/// An OFDM (or DFT-spread OFDM) modulator and demodulator.
77#[derive(Clone, Debug, PartialEq)]
78pub struct Ofdm {
79    /// Size $N$ of the subcarrier grid.
80    pub n_fft: usize,
81    /// Number $K$ of active (modulated) subcarriers, centred on the carrier.
82    pub n_active: usize,
83    /// Oversampling factor $L$ (IFFT of size $L N$).
84    pub oversampling: usize,
85    /// Cyclic prefix, in Nyquist-rate samples ($L N_{\mathrm{cp}}$ samples).
86    pub n_cp: usize,
87    /// Leave the DC subcarrier empty (the active subcarriers skip $k = 0$).
88    pub dc_null: bool,
89    /// DFT spreading of the data (localised SC-FDMA).
90    pub dft_spread: bool,
91    /// The rate of the modulated samples, $L N \Delta f$ ($\Delta f$ the subcarrier
92    /// spacing): the rate of the [`Signal`] of [`Modulate`], and the rate [`Demodulate`]
93    /// expects; 1 Hz by default (time in samples).
94    pub sample_rate: Hertz,
95}
96
97impl Ofdm {
98    /// $K$ active subcarriers out of $N$, oversampling 4, no prefix, at the sample rate
99    /// 1 Hz.
100    ///
101    /// # Errors
102    /// [`comnum_core::Error::Config`] unless $1 \le K \le N$.
103    pub fn new(n_fft: usize, n_active: usize) -> Result<Ofdm> {
104        let o = Ofdm {
105            n_fft,
106            n_active,
107            oversampling: 4,
108            n_cp: 0,
109            dc_null: false,
110            dft_spread: false,
111            sample_rate: Hertz(1.0),
112        };
113        o.check()?;
114        Ok(o)
115    }
116
117    /// Validate the configuration.
118    ///
119    /// # Errors
120    /// [`comnum_core::Error::Config`] for an empty or overfull grid, a zero oversampling or a
121    /// sample rate that is not positive and finite.
122    pub fn check(&self) -> Result<()> {
123        ensure(self.n_active >= 1 && self.n_active + usize::from(self.dc_null) <= self.n_fft, || {
124            format!("{} active subcarriers do not fit a grid of {}", self.n_active, self.n_fft)
125        })?;
126        ensure(self.oversampling >= 1, || "the oversampling factor must be at least 1".into())?;
127        check_rate(self.sample_rate)
128    }
129
130    /// Size of the IFFT, $L N$.
131    pub fn fft_len(&self) -> usize {
132        self.oversampling * self.n_fft
133    }
134
135    /// Samples per block, prefix included, $L (N + N_{\mathrm{cp}})$.
136    pub fn block_len(&self) -> usize {
137        self.oversampling * (self.n_fft + self.n_cp)
138    }
139
140    /// Subcarrier index $k$ of the active subcarrier `i`.
141    pub fn subcarrier(&self, i: usize) -> isize {
142        let k = i as isize - (self.n_active / 2) as isize;
143        if self.dc_null && k >= 0 { k + 1 } else { k }
144    }
145
146    /// FFT bin of the active subcarrier `i` ([`fft::centred_bin`], one bin further from
147    /// the subcarrier 0 on with `dc_null`).
148    pub fn bin(&self, i: usize) -> usize {
149        let skip = usize::from(self.dc_null && i >= self.n_active / 2);
150        fft::centred_bin(i + skip, self.n_active, self.fft_len())
151    }
152
153    /// The transmit pulse of the active subcarrier `i` of symbol `n`, prefix included, on
154    /// its support: `(first sample, values)`, $\frac{1}{\sqrt{LN}} e^{j 2\pi k (t -
155    /// L N_{\mathrm{cp}})/(LN)}$ for $t = 0, \dots, L(N + N_{\mathrm{cp}}) - 1$ (energy
156    /// $(N + N_{\mathrm{cp}})/N$); a column of the matrix $\mathbf{G}$ of Nissel, Schwarz &
157    /// Rupp 2017, eq. (20).
158    pub fn pulse(&self, i: usize, n: usize) -> (usize, Vec<C64>) {
159        let (len, cp) = (self.fft_len(), self.oversampling * self.n_cp);
160        let (bin, scale) = (self.bin(i) as f64, 1.0 / (len as f64).sqrt());
161        let values = (0..self.block_len())
162            .map(|t| comnum_math::cis(std::f64::consts::TAU * bin * (t as f64 - cp as f64) / len as f64) * scale)
163            .collect();
164        (n * self.block_len(), values)
165    }
166
167    /// The receive pulse (the window of the demodulator) of the active subcarrier `i` of
168    /// symbol `n`: the [`Ofdm::pulse`] without its prefix, of unit energy, on its support
169    /// `(first sample, values)`.
170    pub fn window(&self, i: usize, n: usize) -> (usize, Vec<C64>) {
171        let (start, mut values) = self.pulse(i, n);
172        let cp = self.oversampling * self.n_cp;
173        values.drain(..cp);
174        (start + cp, values)
175    }
176
177    /// Whether an FFT bin carries an active subcarrier.
178    pub fn is_active_bin(&self, bin: usize) -> bool {
179        (0..self.n_active).any(|i| self.bin(i) == bin)
180    }
181
182    fn scale(&self) -> f64 {
183        self.fft_len() as f64 / (self.n_active as f64).sqrt()
184    }
185
186    /// The values of the active subcarriers of `data` (the identity, or the unitary DFT
187    /// of size $K$ with `dft_spread`).
188    pub fn spread(&self, data: &[C64]) -> Vec<C64> {
189        if self.dft_spread {
190            let s = 1.0 / (data.len() as f64).sqrt();
191            fft::fft(data).into_iter().map(|v| v * s).collect()
192        } else {
193            data.to_vec()
194        }
195    }
196
197    /// The inverse of [`Ofdm::spread`].
198    pub fn despread(&self, active: &[C64]) -> Vec<C64> {
199        if self.dft_spread {
200            let s = (active.len() as f64).sqrt();
201            fft::ifft(active).into_iter().map(|v| v * s).collect()
202        } else {
203            active.to_vec()
204        }
205    }
206
207    /// The time block (without prefix, $L N$ samples) of the values of the active
208    /// subcarriers.
209    ///
210    /// # Errors
211    /// [`comnum_core::Error::Shape`] if `active` does not have $K$ values.
212    pub fn to_time(&self, active: &[C64]) -> Result<Vec<C64>> {
213        ensure_shape(active.len() == self.n_active, || {
214            format!("{} values for {} active subcarriers", active.len(), self.n_active)
215        })?;
216        let mut bins = vec![C64::new(0.0, 0.0); self.fft_len()];
217        for (i, v) in active.iter().enumerate() {
218            bins[self.bin(i)] = *v;
219        }
220        let s = self.scale();
221        Ok(fft::ifft(&bins).into_iter().map(|v| v * s).collect())
222    }
223
224    /// The values of the active subcarriers of a time block (without prefix).
225    ///
226    /// # Errors
227    /// [`comnum_core::Error::Shape`] if `block` does not have $L N$ samples.
228    pub fn to_active(&self, block: &[C64]) -> Result<Vec<C64>> {
229        ensure_shape(block.len() == self.fft_len(), || {
230            format!("a block of {} samples for an IFFT of {}", block.len(), self.fft_len())
231        })?;
232        let bins = fft::fft(block);
233        let s = 1.0 / self.scale();
234        Ok((0..self.n_active).map(|i| bins[self.bin(i)] * s).collect())
235    }
236
237    /// Keep only the active subcarriers of a time block (an ideal band-pass filter).
238    ///
239    /// # Errors
240    /// As [`Ofdm::to_active`].
241    pub fn filter(&self, block: &[C64]) -> Result<Vec<C64>> {
242        self.to_time(&self.to_active(block)?)
243    }
244
245    /// Prepend the cyclic prefix to a time block.
246    ///
247    /// # Errors
248    /// [`comnum_core::Error::Shape`] if the prefix is longer than the block.
249    pub fn with_prefix(&self, block: &[C64]) -> Result<Vec<C64>> {
250        let mut out = Vec::with_capacity(block.len() + self.oversampling * self.n_cp);
251        extend_with_prefix(&mut out, block, self.oversampling * self.n_cp)?;
252        Ok(out)
253    }
254
255    /// The raised-cosine window of whole blocks of [`Ofdm::block_len`] samples (prefix
256    /// included): the first `ramp` samples of every prefix rise as
257    ///
258    /// $$
259    /// w\[n\] = \frac{1}{2}\left(1 - \cos\frac{\pi (n + 1/2)}{R}\right), \qquad n = 0 \dots R - 1,
260    /// $$
261    ///
262    /// a cyclic postfix of $R$ samples of the block falls as $1 - w$ and overlaps the
263    /// ramp of the next block (the postfix of the last block wraps onto the first: the
264    /// stream stays periodic), so the output has the length of the input and the FFT
265    /// part of every block is untouched. The pulse of a block then lasts
266    /// $L(N + N_{\mathrm{cp}}) + R$ samples with $(1 - \cos)$ edges: the raised-cosine
267    /// impulse shape of windowed OFDM (Saul, *Proc. IEEE VTC2004-Fall*, 2004, eq. (1):
268    /// a guard interval and a roll-off of 11.1 %), whose out-of-band sidelobes fall as
269    /// $f^{-3}$ instead of the $f^{-1}$ of the rectangular pulse. `ramp = 0` returns the
270    /// samples unchanged.
271    ///
272    /// # Errors
273    /// [`comnum_core::Error::Config`] if `ramp` exceeds the prefix $L N_{\mathrm{cp}}$,
274    /// [`comnum_core::Error::Shape`] for a partial block.
275    ///
276    /// # Examples
277    /// ```
278    /// use comnum_waveform::ofdm::Ofdm;
279    /// use comnum_math::C64;
280    /// let ofdm = Ofdm { n_cp: 4, oversampling: 1, ..Ofdm::new(16, 16).unwrap() };
281    /// let data: Vec<C64> = (0..32).map(|k| C64::new(if k % 3 == 0 { 1.0 } else { -1.0 }, 0.0)).collect();
282    /// let x = ofdm.modulate(&data).unwrap();
283    /// let y = ofdm.windowed(&x, 4).unwrap();
284    /// assert_eq!(y.len(), x.len());
285    /// // the FFT part of every block is untouched
286    /// assert!(y[4..20].iter().zip(&x[4..20]).all(|(a, b)| (a - b).norm() < 1e-12));
287    /// ```
288    pub fn windowed(&self, samples: &[C64], ramp: usize) -> Result<Vec<C64>> {
289        self.check()?;
290        let prefix = self.oversampling * self.n_cp;
291        ensure(ramp <= prefix, || format!("a ramp of {ramp} samples exceeds the prefix of {prefix}"))?;
292        let len = self.block_len();
293        ensure_shape(samples.len() % len == 0, || {
294            format!("{} samples is not a multiple of the block length {len}", samples.len())
295        })?;
296        let mut out = samples.to_vec();
297        if ramp == 0 || samples.is_empty() {
298            return Ok(out);
299        }
300        let n_blocks = samples.len() / len;
301        for b in 0..n_blocks {
302            let o = b * len;
303            // the postfix of the previous block: its FFT part continued cyclically
304            let prev = (b + n_blocks - 1) % n_blocks * len + prefix;
305            for n in 0..ramp {
306                let w = 0.5 * (1.0 - (std::f64::consts::PI * (n as f64 + 0.5) / ramp as f64).cos());
307                out[o + n] = samples[o + n] * w + samples[prev + n] * (1.0 - w);
308            }
309        }
310        Ok(out)
311    }
312
313    /// Modulate data symbols (a multiple of $K$): $L (N + N_{\mathrm{cp}})$ samples per block.
314    ///
315    /// # Errors
316    /// [`comnum_core::Error::Shape`] if the number of symbols is not a multiple of $K$, and the
317    /// errors of [`Ofdm::check`].
318    pub fn modulate(&self, data: &[C64]) -> Result<Vec<C64>> {
319        Ok(self.modulate_with(data, &NoReduction)?.0)
320    }
321
322    /// Demodulate whole blocks into data symbols.
323    ///
324    /// # Errors
325    /// [`comnum_core::Error::Shape`] if the number of samples is not a multiple of the block length.
326    pub fn demodulate(&self, samples: &[C64]) -> Result<Vec<C64>> {
327        let blocks = samples.len() / self.block_len().max(1);
328        self.demodulate_with(samples, &NoReduction, &vec![0; blocks])
329    }
330
331    /// The PAPR (dB) of every whole block of `samples`, the cyclic prefix excluded
332    /// ([`comnum_math::papr_db`] of the useful part).
333    pub fn papr_db_per_block(&self, samples: &[C64]) -> Vec<f64> {
334        let cp = self.oversampling * self.n_cp;
335        samples.chunks_exact(self.block_len()).map(|b| comnum_math::papr_db(&b[cp..])).collect()
336    }
337
338    /// Modulate with a PAPR reduction technique: the samples and the side information
339    /// of every block (the selected candidate of SLM or PTS, 0 otherwise).
340    ///
341    /// # Errors
342    /// As [`Ofdm::modulate`], with the number of data symbols per block of the technique,
343    /// and the errors of the technique.
344    pub fn modulate_with(&self, data: &[C64], reduction: &dyn PaprReduction) -> Result<(Vec<C64>, Vec<usize>)> {
345        self.check()?;
346        let k = reduction.data_len(self);
347        ensure(k >= 1, || "no data subcarrier".into())?;
348        ensure_shape(data.len() % k == 0, || format!("{} symbols is not a multiple of {k} per block", data.len()))?;
349        let mut out = Vec::with_capacity(data.len() / k * self.block_len());
350        let mut side = Vec::with_capacity(data.len() / k);
351        for chunk in data.chunks(k) {
352            let (block, s) = reduction.reduce(self, chunk)?;
353            extend_with_prefix(&mut out, &block, self.oversampling * self.n_cp)?;
354            side.push(s);
355        }
356        Ok((out, side))
357    }
358
359    /// Demodulate blocks transmitted with a PAPR reduction technique and its side
360    /// information.
361    ///
362    /// # Errors
363    /// [`comnum_core::Error::Shape`] for a partial block or a side information of another length.
364    pub fn demodulate_with(&self, samples: &[C64], reduction: &dyn PaprReduction, side: &[usize]) -> Result<Vec<C64>> {
365        self.check()?;
366        let len = self.block_len();
367        ensure_shape(samples.len() % len == 0, || {
368            format!("{} samples is not a multiple of the block length {len}", samples.len())
369        })?;
370        ensure_shape(side.len() == samples.len() / len, || {
371            format!("{} side informations for {} blocks", side.len(), samples.len() / len)
372        })?;
373        let mut out = Vec::new();
374        for (block, &s) in samples.chunks(len).zip(side) {
375            let mut b = strip_prefix(block, self.oversampling * self.n_cp)?.to_vec();
376            reduction.restore_time(&mut b);
377            out.extend(reduction.recover(self, self.to_active(&b)?, s)?);
378        }
379        Ok(out)
380    }
381}
382
383/// A PAPR reduction technique applied block by block by [`Ofdm::modulate_with`].
384pub trait PaprReduction {
385    /// Data symbols per block (all the active subcarriers by default).
386    fn data_len(&self, ofdm: &Ofdm) -> usize {
387        ofdm.n_active
388    }
389
390    /// The time block (without prefix) of one block of data, and its side information.
391    ///
392    /// # Errors
393    /// For a configuration the technique cannot handle.
394    fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)>;
395
396    /// Undo a memoryless transformation of the time block at the receiver (the
397    /// expansion of companding); nothing by default.
398    fn restore_time(&self, _block: &mut [C64]) {}
399
400    /// The data of a block from the values of its active subcarriers and its side
401    /// information (despreading only, by default).
402    ///
403    /// # Errors
404    /// For an invalid side information.
405    fn recover(&self, ofdm: &Ofdm, active: Vec<C64>, _side: usize) -> Result<Vec<C64>> {
406        Ok(ofdm.despread(&active))
407    }
408}
409
410/// Plain OFDM.
411#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
412pub struct NoReduction;
413
414impl PaprReduction for NoReduction {
415    fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
416        Ok((ofdm.to_time(&ofdm.spread(data))?, 0))
417    }
418}
419
420/// OFDM as a [`Modulate`] block: one stream of data symbols into one stream of samples
421/// at [`Ofdm::sample_rate`].
422impl Modulate for Ofdm {
423    fn modulate_into(&self, x: &Symbols, out: &mut Signal) -> Result<()> {
424        out.set_stream(&Ofdm::modulate(self, crate::one_stream(x, "OFDM")?)?, self.sample_rate);
425        Ok(())
426    }
427}
428
429/// OFDM as a [`Demodulate`] block: one stream of samples at [`Ofdm::sample_rate`].
430impl Demodulate for Ofdm {
431    /// # Errors
432    /// [`comnum_core::Error::Config`] for several streams or a signal at another sample rate,
433    /// and the errors of [`Ofdm::demodulate`].
434    fn demodulate_into(&mut self, y: &Signal, out: &mut Symbols) -> Result<()> {
435        check_input(y, self.sample_rate, "OFDM")?;
436        out.set_stream(&Ofdm::demodulate(self, y.stream(0))?);
437        Ok(())
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use comnum_core::rng::Rng;
445
446    /// The window keeps the FFT part of every block, keeps the length, and the two ramps
447    /// of an overlap sum to one; a too-long ramp and a partial block are errors.
448    #[test]
449    fn the_raised_cosine_window_overlaps_the_ramps_and_keeps_the_fft_part() {
450        let ofdm = Ofdm { n_cp: 8, oversampling: 2, ..Ofdm::new(32, 32).unwrap() };
451        let mut rng = Rng::new(7);
452        let data: Vec<C64> = (0..96).map(|_| rng.complex_gaussian()).collect();
453        let x = ofdm.modulate(&data).unwrap();
454        let y = ofdm.windowed(&x, 12).unwrap();
455        assert_eq!(y.len(), x.len());
456        let len = ofdm.block_len();
457        for b in 0..3 {
458            let o = b * len;
459            assert!(y[o + 16..o + len].iter().zip(&x[o + 16..o + len]).all(|(a, b)| (a - b).norm() < 1e-12));
460            // a constant block would stay constant: the ramps sum to one
461            let prev = (b + 2) % 3 * len + 16;
462            for n in 0..12 {
463                let w = 0.5 * (1.0 - (std::f64::consts::PI * (n as f64 + 0.5) / 12.0).cos());
464                let expected = x[o + n] * w + x[prev + n] * (1.0 - w);
465                assert!((y[o + n] - expected).norm() < 1e-12);
466            }
467        }
468        assert_eq!(ofdm.windowed(&x, 0).unwrap(), x);
469        assert!(ofdm.windowed(&x, 17).is_err());
470        assert!(ofdm.windowed(&x[1..], 4).is_err());
471    }
472
473    /// A constant stream (every block the same, the data all ones on one subcarrier at
474    /// DC) is unchanged by the window: the postfix of a block equals the prefix of the
475    /// next.
476    #[test]
477    fn the_window_leaves_a_periodic_stream_unchanged() {
478        let ofdm = Ofdm { n_cp: 4, oversampling: 1, dc_null: false, ..Ofdm::new(16, 1).unwrap() };
479        let data = vec![C64::new(1.0, 0.0); 3];
480        let x = ofdm.modulate(&data).unwrap();
481        let y = ofdm.windowed(&x, 4).unwrap();
482        assert!(y.iter().zip(&x).all(|(a, b)| (a - b).norm() < 1e-12));
483    }
484
485    /// The bins were computed from the signed subcarrier index before they came from
486    /// `fft::centred_bin`: the same bins.
487    #[test]
488    fn the_bins_are_the_subcarriers_modulo_the_fft_size() {
489        for (n_fft, n_active) in [(8, 4), (16, 7), (64, 64), (64, 63), (256, 200), (12, 1)] {
490            for dc_null in [false, true] {
491                for oversampling in [1, 4] {
492                    let ofdm = Ofdm { oversampling, dc_null, ..Ofdm::new(n_fft, n_active).unwrap() };
493                    if ofdm.check().is_err() {
494                        continue;
495                    }
496                    for i in 0..n_active {
497                        let old = ofdm.subcarrier(i).rem_euclid(ofdm.fft_len() as isize) as usize;
498                        assert_eq!(ofdm.bin(i), old, "{n_fft} {n_active} {dc_null} {oversampling} {i}");
499                    }
500                }
501            }
502        }
503    }
504
505    #[test]
506    fn round_trip_unit_power_and_band_occupancy() {
507        let mut rng = Rng::new(1);
508        for (dc_null, spread) in [(false, false), (true, false), (false, true)] {
509            let o = Ofdm { n_cp: 8, dc_null, dft_spread: spread, ..Ofdm::new(64, 48).unwrap() };
510            let d: Vec<C64> = (0..48 * 200).map(|_| rng.complex_gaussian()).collect();
511            let x = o.modulate(&d).unwrap();
512            assert!(o.demodulate(&x).unwrap().iter().zip(&d).all(|(a, b)| (a - b).norm() < 1e-10));
513            let p = x.iter().map(C64::norm_sqr).sum::<f64>() / x.len() as f64;
514            assert!((p - 1.0).abs() < 0.03, "power {p}");
515            // nothing outside the active bins
516            let block = &x[8 * 4..8 * 4 + o.fft_len()];
517            let spec = fft::fft(block);
518            for (b, v) in spec.iter().enumerate() {
519                if !o.is_active_bin(b) {
520                    assert!(v.norm() < 1e-9);
521                }
522            }
523            if dc_null {
524                assert!(!o.is_active_bin(0));
525            }
526        }
527        assert!(Ofdm::new(64, 65).is_err() && Ofdm::new(64, 0).is_err());
528    }
529
530    #[test]
531    fn the_traits_wrap_the_methods() {
532        let o = Ofdm::new(32, 20).unwrap();
533        let d = Symbols::from_stream(vec![C64::new(1.0, 0.0); 40]).unwrap();
534        let x = Modulate::modulate(&o, &d).unwrap();
535        assert_eq!((x.len(), x.sample_rate()), (2 * 128, Hertz(1.0)));
536        let mut o2 = o.clone();
537        let back = Demodulate::demodulate(&mut o2, &x).unwrap();
538        assert!(back.as_slice().iter().all(|v| (v - C64::new(1.0, 0.0)).norm() < 1e-12));
539        // a real rate: 15 kHz subcarriers, L N = 128 samples per symbol
540        let mut lte = Ofdm { sample_rate: Hertz(128.0 * 15e3), ..o };
541        let y = Modulate::modulate(&lte, &d).unwrap();
542        assert_eq!(y.sample_rate(), Hertz(1.92e6));
543        assert!(Demodulate::demodulate(&mut lte, &x).is_err() && Demodulate::demodulate(&mut lte, &y).is_ok());
544        assert!(Ofdm { sample_rate: Hertz(0.0), ..o2 }.check().is_err());
545    }
546}