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(<e, &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}