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    /// Whether an FFT bin carries an active subcarrier.
154    pub fn is_active_bin(&self, bin: usize) -> bool {
155        (0..self.n_active).any(|i| self.bin(i) == bin)
156    }
157
158    fn scale(&self) -> f64 {
159        self.fft_len() as f64 / (self.n_active as f64).sqrt()
160    }
161
162    /// The values of the active subcarriers of `data` (the identity, or the unitary DFT
163    /// of size $K$ with `dft_spread`).
164    pub fn spread(&self, data: &[C64]) -> Vec<C64> {
165        if self.dft_spread {
166            let s = 1.0 / (data.len() as f64).sqrt();
167            fft::fft(data).into_iter().map(|v| v * s).collect()
168        } else {
169            data.to_vec()
170        }
171    }
172
173    /// The inverse of [`Ofdm::spread`].
174    pub fn despread(&self, active: &[C64]) -> Vec<C64> {
175        if self.dft_spread {
176            let s = (active.len() as f64).sqrt();
177            fft::ifft(active).into_iter().map(|v| v * s).collect()
178        } else {
179            active.to_vec()
180        }
181    }
182
183    /// The time block (without prefix, $L N$ samples) of the values of the active
184    /// subcarriers.
185    ///
186    /// # Errors
187    /// [`comnum_core::Error::Shape`] if `active` does not have $K$ values.
188    pub fn to_time(&self, active: &[C64]) -> Result<Vec<C64>> {
189        ensure_shape(active.len() == self.n_active, || {
190            format!("{} values for {} active subcarriers", active.len(), self.n_active)
191        })?;
192        let mut bins = vec![C64::new(0.0, 0.0); self.fft_len()];
193        for (i, v) in active.iter().enumerate() {
194            bins[self.bin(i)] = *v;
195        }
196        let s = self.scale();
197        Ok(fft::ifft(&bins).into_iter().map(|v| v * s).collect())
198    }
199
200    /// The values of the active subcarriers of a time block (without prefix).
201    ///
202    /// # Errors
203    /// [`comnum_core::Error::Shape`] if `block` does not have $L N$ samples.
204    pub fn to_active(&self, block: &[C64]) -> Result<Vec<C64>> {
205        ensure_shape(block.len() == self.fft_len(), || {
206            format!("a block of {} samples for an IFFT of {}", block.len(), self.fft_len())
207        })?;
208        let bins = fft::fft(block);
209        let s = 1.0 / self.scale();
210        Ok((0..self.n_active).map(|i| bins[self.bin(i)] * s).collect())
211    }
212
213    /// Keep only the active subcarriers of a time block (an ideal band-pass filter).
214    ///
215    /// # Errors
216    /// As [`Ofdm::to_active`].
217    pub fn filter(&self, block: &[C64]) -> Result<Vec<C64>> {
218        self.to_time(&self.to_active(block)?)
219    }
220
221    /// Prepend the cyclic prefix to a time block.
222    ///
223    /// # Errors
224    /// [`comnum_core::Error::Shape`] if the prefix is longer than the block.
225    pub fn with_prefix(&self, block: &[C64]) -> Result<Vec<C64>> {
226        let mut out = Vec::with_capacity(block.len() + self.oversampling * self.n_cp);
227        extend_with_prefix(&mut out, block, self.oversampling * self.n_cp)?;
228        Ok(out)
229    }
230
231    /// The raised-cosine window of whole blocks of [`Ofdm::block_len`] samples (prefix
232    /// included): the first `ramp` samples of every prefix rise as
233    ///
234    /// $$
235    /// w\[n\] = \frac{1}{2}\left(1 - \cos\frac{\pi (n + 1/2)}{R}\right), \qquad n = 0 \dots R - 1,
236    /// $$
237    ///
238    /// a cyclic postfix of $R$ samples of the block falls as $1 - w$ and overlaps the
239    /// ramp of the next block (the postfix of the last block wraps onto the first: the
240    /// stream stays periodic), so the output has the length of the input and the FFT
241    /// part of every block is untouched. The pulse of a block then lasts
242    /// $L(N + N_{\mathrm{cp}}) + R$ samples with $(1 - \cos)$ edges: the raised-cosine
243    /// impulse shape of windowed OFDM (Saul, *Proc. IEEE VTC2004-Fall*, 2004, eq. (1):
244    /// a guard interval and a roll-off of 11.1 %), whose out-of-band sidelobes fall as
245    /// $f^{-3}$ instead of the $f^{-1}$ of the rectangular pulse. `ramp = 0` returns the
246    /// samples unchanged.
247    ///
248    /// # Errors
249    /// [`comnum_core::Error::Config`] if `ramp` exceeds the prefix $L N_{\mathrm{cp}}$,
250    /// [`comnum_core::Error::Shape`] for a partial block.
251    ///
252    /// # Examples
253    /// ```
254    /// use comnum_waveform::ofdm::Ofdm;
255    /// use comnum_math::C64;
256    /// let ofdm = Ofdm { n_cp: 4, oversampling: 1, ..Ofdm::new(16, 16).unwrap() };
257    /// let data: Vec<C64> = (0..32).map(|k| C64::new(if k % 3 == 0 { 1.0 } else { -1.0 }, 0.0)).collect();
258    /// let x = ofdm.modulate(&data).unwrap();
259    /// let y = ofdm.windowed(&x, 4).unwrap();
260    /// assert_eq!(y.len(), x.len());
261    /// // the FFT part of every block is untouched
262    /// assert!(y[4..20].iter().zip(&x[4..20]).all(|(a, b)| (a - b).norm() < 1e-12));
263    /// ```
264    pub fn windowed(&self, samples: &[C64], ramp: usize) -> Result<Vec<C64>> {
265        self.check()?;
266        let prefix = self.oversampling * self.n_cp;
267        ensure(ramp <= prefix, || format!("a ramp of {ramp} samples exceeds the prefix of {prefix}"))?;
268        let len = self.block_len();
269        ensure_shape(samples.len() % len == 0, || {
270            format!("{} samples is not a multiple of the block length {len}", samples.len())
271        })?;
272        let mut out = samples.to_vec();
273        if ramp == 0 || samples.is_empty() {
274            return Ok(out);
275        }
276        let n_blocks = samples.len() / len;
277        for b in 0..n_blocks {
278            let o = b * len;
279            // the postfix of the previous block: its FFT part continued cyclically
280            let prev = (b + n_blocks - 1) % n_blocks * len + prefix;
281            for n in 0..ramp {
282                let w = 0.5 * (1.0 - (std::f64::consts::PI * (n as f64 + 0.5) / ramp as f64).cos());
283                out[o + n] = samples[o + n] * w + samples[prev + n] * (1.0 - w);
284            }
285        }
286        Ok(out)
287    }
288
289    /// Modulate data symbols (a multiple of $K$): $L (N + N_{\mathrm{cp}})$ samples per block.
290    ///
291    /// # Errors
292    /// [`comnum_core::Error::Shape`] if the number of symbols is not a multiple of $K$, and the
293    /// errors of [`Ofdm::check`].
294    pub fn modulate(&self, data: &[C64]) -> Result<Vec<C64>> {
295        Ok(self.modulate_with(data, &NoReduction)?.0)
296    }
297
298    /// Demodulate whole blocks into data symbols.
299    ///
300    /// # Errors
301    /// [`comnum_core::Error::Shape`] if the number of samples is not a multiple of the block length.
302    pub fn demodulate(&self, samples: &[C64]) -> Result<Vec<C64>> {
303        let blocks = samples.len() / self.block_len().max(1);
304        self.demodulate_with(samples, &NoReduction, &vec![0; blocks])
305    }
306
307    /// The PAPR (dB) of every whole block of `samples`, the cyclic prefix excluded
308    /// ([`comnum_math::papr_db`] of the useful part).
309    pub fn papr_db_per_block(&self, samples: &[C64]) -> Vec<f64> {
310        let cp = self.oversampling * self.n_cp;
311        samples.chunks_exact(self.block_len()).map(|b| comnum_math::papr_db(&b[cp..])).collect()
312    }
313
314    /// Modulate with a PAPR reduction technique: the samples and the side information
315    /// of every block (the selected candidate of SLM or PTS, 0 otherwise).
316    ///
317    /// # Errors
318    /// As [`Ofdm::modulate`], with the number of data symbols per block of the technique,
319    /// and the errors of the technique.
320    pub fn modulate_with(&self, data: &[C64], reduction: &dyn PaprReduction) -> Result<(Vec<C64>, Vec<usize>)> {
321        self.check()?;
322        let k = reduction.data_len(self);
323        ensure(k >= 1, || "no data subcarrier".into())?;
324        ensure_shape(data.len() % k == 0, || format!("{} symbols is not a multiple of {k} per block", data.len()))?;
325        let mut out = Vec::with_capacity(data.len() / k * self.block_len());
326        let mut side = Vec::with_capacity(data.len() / k);
327        for chunk in data.chunks(k) {
328            let (block, s) = reduction.reduce(self, chunk)?;
329            extend_with_prefix(&mut out, &block, self.oversampling * self.n_cp)?;
330            side.push(s);
331        }
332        Ok((out, side))
333    }
334
335    /// Demodulate blocks transmitted with a PAPR reduction technique and its side
336    /// information.
337    ///
338    /// # Errors
339    /// [`comnum_core::Error::Shape`] for a partial block or a side information of another length.
340    pub fn demodulate_with(&self, samples: &[C64], reduction: &dyn PaprReduction, side: &[usize]) -> Result<Vec<C64>> {
341        self.check()?;
342        let len = self.block_len();
343        ensure_shape(samples.len() % len == 0, || {
344            format!("{} samples is not a multiple of the block length {len}", samples.len())
345        })?;
346        ensure_shape(side.len() == samples.len() / len, || {
347            format!("{} side informations for {} blocks", side.len(), samples.len() / len)
348        })?;
349        let mut out = Vec::new();
350        for (block, &s) in samples.chunks(len).zip(side) {
351            let mut b = strip_prefix(block, self.oversampling * self.n_cp)?.to_vec();
352            reduction.restore_time(&mut b);
353            out.extend(reduction.recover(self, self.to_active(&b)?, s)?);
354        }
355        Ok(out)
356    }
357}
358
359/// A PAPR reduction technique applied block by block by [`Ofdm::modulate_with`].
360pub trait PaprReduction {
361    /// Data symbols per block (all the active subcarriers by default).
362    fn data_len(&self, ofdm: &Ofdm) -> usize {
363        ofdm.n_active
364    }
365
366    /// The time block (without prefix) of one block of data, and its side information.
367    ///
368    /// # Errors
369    /// For a configuration the technique cannot handle.
370    fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)>;
371
372    /// Undo a memoryless transformation of the time block at the receiver (the
373    /// expansion of companding); nothing by default.
374    fn restore_time(&self, _block: &mut [C64]) {}
375
376    /// The data of a block from the values of its active subcarriers and its side
377    /// information (despreading only, by default).
378    ///
379    /// # Errors
380    /// For an invalid side information.
381    fn recover(&self, ofdm: &Ofdm, active: Vec<C64>, _side: usize) -> Result<Vec<C64>> {
382        Ok(ofdm.despread(&active))
383    }
384}
385
386/// Plain OFDM.
387#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
388pub struct NoReduction;
389
390impl PaprReduction for NoReduction {
391    fn reduce(&self, ofdm: &Ofdm, data: &[C64]) -> Result<(Vec<C64>, usize)> {
392        Ok((ofdm.to_time(&ofdm.spread(data))?, 0))
393    }
394}
395
396/// OFDM as a [`Modulate`] block: one stream of data symbols into one stream of samples
397/// at [`Ofdm::sample_rate`].
398impl Modulate for Ofdm {
399    fn modulate_into(&self, x: &Symbols, out: &mut Signal) -> Result<()> {
400        out.set_stream(&Ofdm::modulate(self, crate::one_stream(x, "OFDM")?)?, self.sample_rate);
401        Ok(())
402    }
403}
404
405/// OFDM as a [`Demodulate`] block: one stream of samples at [`Ofdm::sample_rate`].
406impl Demodulate for Ofdm {
407    /// # Errors
408    /// [`comnum_core::Error::Config`] for several streams or a signal at another sample rate,
409    /// and the errors of [`Ofdm::demodulate`].
410    fn demodulate_into(&mut self, y: &Signal, out: &mut Symbols) -> Result<()> {
411        check_input(y, self.sample_rate, "OFDM")?;
412        out.set_stream(&Ofdm::demodulate(self, y.stream(0))?);
413        Ok(())
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420    use comnum_core::rng::Rng;
421
422    /// The window keeps the FFT part of every block, keeps the length, and the two ramps
423    /// of an overlap sum to one; a too-long ramp and a partial block are errors.
424    #[test]
425    fn the_raised_cosine_window_overlaps_the_ramps_and_keeps_the_fft_part() {
426        let ofdm = Ofdm { n_cp: 8, oversampling: 2, ..Ofdm::new(32, 32).unwrap() };
427        let mut rng = Rng::new(7);
428        let data: Vec<C64> = (0..96).map(|_| rng.complex_gaussian()).collect();
429        let x = ofdm.modulate(&data).unwrap();
430        let y = ofdm.windowed(&x, 12).unwrap();
431        assert_eq!(y.len(), x.len());
432        let len = ofdm.block_len();
433        for b in 0..3 {
434            let o = b * len;
435            assert!(y[o + 16..o + len].iter().zip(&x[o + 16..o + len]).all(|(a, b)| (a - b).norm() < 1e-12));
436            // a constant block would stay constant: the ramps sum to one
437            let prev = (b + 2) % 3 * len + 16;
438            for n in 0..12 {
439                let w = 0.5 * (1.0 - (std::f64::consts::PI * (n as f64 + 0.5) / 12.0).cos());
440                let expected = x[o + n] * w + x[prev + n] * (1.0 - w);
441                assert!((y[o + n] - expected).norm() < 1e-12);
442            }
443        }
444        assert_eq!(ofdm.windowed(&x, 0).unwrap(), x);
445        assert!(ofdm.windowed(&x, 17).is_err());
446        assert!(ofdm.windowed(&x[1..], 4).is_err());
447    }
448
449    /// A constant stream (every block the same, the data all ones on one subcarrier at
450    /// DC) is unchanged by the window: the postfix of a block equals the prefix of the
451    /// next.
452    #[test]
453    fn the_window_leaves_a_periodic_stream_unchanged() {
454        let ofdm = Ofdm { n_cp: 4, oversampling: 1, dc_null: false, ..Ofdm::new(16, 1).unwrap() };
455        let data = vec![C64::new(1.0, 0.0); 3];
456        let x = ofdm.modulate(&data).unwrap();
457        let y = ofdm.windowed(&x, 4).unwrap();
458        assert!(y.iter().zip(&x).all(|(a, b)| (a - b).norm() < 1e-12));
459    }
460
461    /// The bins were computed from the signed subcarrier index before they came from
462    /// `fft::centred_bin`: the same bins.
463    #[test]
464    fn the_bins_are_the_subcarriers_modulo_the_fft_size() {
465        for (n_fft, n_active) in [(8, 4), (16, 7), (64, 64), (64, 63), (256, 200), (12, 1)] {
466            for dc_null in [false, true] {
467                for oversampling in [1, 4] {
468                    let ofdm = Ofdm { oversampling, dc_null, ..Ofdm::new(n_fft, n_active).unwrap() };
469                    if ofdm.check().is_err() {
470                        continue;
471                    }
472                    for i in 0..n_active {
473                        let old = ofdm.subcarrier(i).rem_euclid(ofdm.fft_len() as isize) as usize;
474                        assert_eq!(ofdm.bin(i), old, "{n_fft} {n_active} {dc_null} {oversampling} {i}");
475                    }
476                }
477            }
478        }
479    }
480
481    #[test]
482    fn round_trip_unit_power_and_band_occupancy() {
483        let mut rng = Rng::new(1);
484        for (dc_null, spread) in [(false, false), (true, false), (false, true)] {
485            let o = Ofdm { n_cp: 8, dc_null, dft_spread: spread, ..Ofdm::new(64, 48).unwrap() };
486            let d: Vec<C64> = (0..48 * 200).map(|_| rng.complex_gaussian()).collect();
487            let x = o.modulate(&d).unwrap();
488            assert!(o.demodulate(&x).unwrap().iter().zip(&d).all(|(a, b)| (a - b).norm() < 1e-10));
489            let p = x.iter().map(C64::norm_sqr).sum::<f64>() / x.len() as f64;
490            assert!((p - 1.0).abs() < 0.03, "power {p}");
491            // nothing outside the active bins
492            let block = &x[8 * 4..8 * 4 + o.fft_len()];
493            let spec = fft::fft(block);
494            for (b, v) in spec.iter().enumerate() {
495                if !o.is_active_bin(b) {
496                    assert!(v.norm() < 1e-9);
497                }
498            }
499            if dc_null {
500                assert!(!o.is_active_bin(0));
501            }
502        }
503        assert!(Ofdm::new(64, 65).is_err() && Ofdm::new(64, 0).is_err());
504    }
505
506    #[test]
507    fn the_traits_wrap_the_methods() {
508        let o = Ofdm::new(32, 20).unwrap();
509        let d = Symbols::from_stream(vec![C64::new(1.0, 0.0); 40]).unwrap();
510        let x = Modulate::modulate(&o, &d).unwrap();
511        assert_eq!((x.len(), x.sample_rate()), (2 * 128, Hertz(1.0)));
512        let mut o2 = o.clone();
513        let back = Demodulate::demodulate(&mut o2, &x).unwrap();
514        assert!(back.as_slice().iter().all(|v| (v - C64::new(1.0, 0.0)).norm() < 1e-12));
515        // a real rate: 15 kHz subcarriers, L N = 128 samples per symbol
516        let mut lte = Ofdm { sample_rate: Hertz(128.0 * 15e3), ..o };
517        let y = Modulate::modulate(&lte, &d).unwrap();
518        assert_eq!(y.sample_rate(), Hertz(1.92e6));
519        assert!(Demodulate::demodulate(&mut lte, &x).is_err() && Demodulate::demodulate(&mut lte, &y).is_ok());
520        assert!(Ofdm { sample_rate: Hertz(0.0), ..o2 }.check().is_err());
521    }
522}