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